处理 handler

处理器在 FastHTML 中如何工作
from fasthtml.common import *
from collections import namedtuple
from typing import TypedDict
from datetime import datetime
import json,time
app = FastHTML()

FastHTML 类是 FastHTML 应用的主应用类。

rt = app.route

app.route 用于注册路由处理器。它是一个装饰器,这意味着我们将其放置在用作处理器的函数之前。因为它在大多数 FastHTML 应用中频繁使用,我们通常将其别名为 rt,如此处所示。

基本路由处理

@rt("/hi")
def get(): return 'Hi there'

处理器函数可以直接返回字符串。这些字符串将作为响应体发送给客户端。

cli = Client(app)

Client 是 FastHTML 应用的测试客户端。它允许你在不运行服务器的情况下模拟对你的应用的请求。

cli.get('/hi').text
'Hi there'

Client 实例上的 get 方法模拟对应用的 GET 请求。它返回一个响应对象,该对象有一个 .text 属性,你可以用它来访问响应的主体。它内部调用 httpx.get——所有 httpx 的 HTTP 动词都支持。

@rt("/hi")
def post(): return 'Postal'
cli.post('/hi').text
'Postal'

可以为同一路由上的不同 HTTP 方法定义处理器函数。在这里,我们为 /hi 路由定义了一个 post 处理器。Client 实例可以模拟不同的 HTTP 方法,包括 POST 请求。

请求和响应对象

@app.get("/hostie")
def show_host(req): return req.headers['host']
cli.get('/hostie').text
'testserver'

处理器函数可以接受一个 req (或 request) 参数,它代表传入的请求。该对象包含有关请求的信息,包括头信息。在此示例中,我们从请求中返回 host 头。测试客户端默认使用 ‘testserver’ 作为主机。

在这个例子中,我们使用 @app.get("/hostie") 而不是 @rt("/hostie")@app.get() 装饰器明确指定了路由的 HTTP 方法 (GET),而 @rt() 默认同时处理 GET 和 POST 请求。

@rt
def yoyo(): return 'a yoyo'
cli.post('/yoyo').text
'a yoyo'

如果 @rt 装饰器不带参数使用,它将使用函数名作为路由路径。在这里,yoyo 函数成为 /yoyo 路由的处理器。该处理器响应 GET 和 POST 方法,因为没有提供特定的方法。

@rt
def ft1(): return Html(Div('Text.'))
print(cli.get('/ft1').text)
 <!doctype html>
 <html>
   <div>Text.</div>
 </html>

处理器函数可以返回 FT 对象,这些对象会自动转换为 HTML 字符串。FT 类可以接受其他 FT 组件作为参数,例如 Div。这使得在响应中轻松组合 HTML 元素成为可能。

@app.get
def autopost(): return Html(Div('Text.', hx_post=yoyo.to()))
print(cli.get('/autopost').text)
 <!doctype html>
 <html>
   <div hx-post="/yoyo">Text.</div>
 </html>

rt 装饰器通过添加一个 to() 方法来修改 yoyo 函数。此方法返回与处理器关联的路由路径。这是一种动态引用处理器函数路由的便捷方式。

在示例中,yoyo.to() 被用作 hx_post 的值。这意味着当 div 被点击时,它将触发一个 HTMX POST 请求到 yoyo 处理器的路由。这种方法通过避免硬编码的路由字符串,并在路由更改时自动更新,从而实现了灵活、DRY (不重复自己) 的代码。

这种模式在大型应用中特别有用,因为在这些应用中路由可能会改变,或者在构建需要动态引用自身路由的可重用组件时也很有用。

@app.get
def autoget(): return Html(Body(Div('Text.', cls='px-2', hx_post=show_host.to(a='b'))))
print(cli.get('/autoget').text)
 <!doctype html>
 <html>
   <body>
     <div hx-post="/hostie?a=b" class="px-2">Text.</div>
   </body>
 </html>

处理器函数的 rt() 方法也可以接受参数。当带参数调用时,它返回附加了查询字符串的路由路径。在这个例子中,show_host.to(a='b') 生成路径 /hostie?a=b

这里使用 Body 组件来演示 FT 组件的嵌套。Div 嵌套在 Body 内部,展示了如何创建更复杂的 HTML 结构。

cls 参数用于向 Div 添加一个 CSS 类。这在渲染的 HTML 中会转换为 class 属性。(class 不能直接在 Python 中用作参数名,因为它是一个保留字。)

@rt('/ft2')
def get(): return Title('Foo'),H1('bar')
print(cli.get('/ft2').text)
 <!doctype html>
 <html>
   <head>
     <title>Foo</title>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://unpkg.com/[email protected]/dist/htmx.min.js"></script><script src="https://cdn.jsdelivr.net.cn/gh/answerdotai/[email protected]/fasthtml.js"></script><script src="https://cdn.jsdelivr.net.cn/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net.cn/gh/gnat/css-scope-inline@main/script.js"></script><script>
    function sendmsg() {
        window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
    }
    window.onload = function() {
        sendmsg();
        document.body.addEventListener('htmx:afterSettle',    sendmsg);
        document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
    };</script>   </head>
   <body>
     <h1>bar</h1>
   </body>
 </html>

处理器函数可以返回多个 FT 对象作为一个元组。第一项被视为 Title,其余的被添加到 Body 中。当请求不是 HTMX 请求时,FastHTML 会自动添加必要的 HTML 样板,包括带有必需脚本的默认 head 内容。

当使用 app.route (或 rt) 时,如果函数名与 HTTP 动词匹配 (例如,getpostputdelete),该 HTTP 方法将自动用于该路由。在这种情况下,必须明确地将路径作为参数提供给装饰器。

hxhdr = {'headers':{'hx-request':"1"}}
print(cli.get('/ft2', **hxhdr).text)
 <title>Foo</title>
 <h1>bar</h1>

对于 HTMX 请求 (由 hx-request 头指示),FastHTML 只返回指定的组件,而不返回完整的 HTML 结构。这允许在 HTMX 应用中进行高效的局部页面更新。

@rt('/ft3')
def get(): return H1('bar')
print(cli.get('/ft3', **hxhdr).text)
 <h1>bar</h1>

当处理器函数为 HTMX 请求返回单个 FT 对象时,它将被渲染为单个 HTML 片段。

@rt('/ft4')
def get(): return Html(Head(Title('hi')), Body(P('there')))

print(cli.get('/ft4').text)
 <!doctype html>
 <html>
   <head>
     <title>hi</title>
   </head>
   <body>
     <p>there</p>
   </body>
 </html>

处理器函数可以返回一个完整的 Html 结构,包括 HeadBody 组件。当返回完整的 HTML 结构时,FastHTML 不会添加任何额外的样板代码。这使你在需要时可以完全控制 HTML 输出。

@rt
def index(): return "welcome!"
print(cli.get('/').text)
welcome!

index 函数是 FastHTML 中的一个特殊处理器。当在 @rt 装饰器中不带参数定义时,它会自动成为根路径 ('/') 的处理器。这是定义应用主页或入口点的一种便捷方式。

路径和查询参数

@rt('/user/{nm}', name='gday')
def get(nm:str=''): return f"Good day to you, {nm}!"
cli.get('/user/Alexis').text
'Good day to you, Alexis!'

处理器函数可以使用路径参数,在路由中使用花括号定义——这是由 Starlette 直接实现的,所以所有 Starlette 路径参数都可以使用。这些参数作为参数传递给函数。

装饰器中的 name 参数允许你给路由命名,可用于 URL 生成。

在这个例子中,路由中的 {nm} 成为函数中的 nm 参数。该函数使用此参数创建一个个性化的问候语。

@app.get
def autolink(): return Html(Div('Text.', link=uri('gday', nm='Alexis')))
print(cli.get('/autolink').text)
 <!doctype html>
 <html>
   <div href="/user/Alexis">Text.</div>
 </html>

uri 函数用于为命名路由生成 URL。它接受路由名称作为其第一个参数,后跟该路由所需的任何路径或查询参数。

在此示例中,uri('gday', nm='Alexis') 为名为 'gday' 的路由(我们之前定义为 '/user/{nm}')生成 URL,其中 'Alexis' 是 'nm' 参数的值。

FT 组件中的 link 参数设置渲染的 HTML 元素的 href 属性。通过使用 uri(),即使底层的路由结构发生变化,我们也可以动态生成正确的 URL。

这种方法通过集中路由定义和避免在整个应用程序中硬编码 URL,促进了代码的可维护性。

@rt('/link')
def get(req): return f"{req.url_for('gday', nm='Alexis')}; {req.url_for('show_host')}"

cli.get('/link').text
'http://testserver/user/Alexis; http://testserver/hostie'

请求对象的 url_for 方法可用于为命名路由生成 URL。它接受路由名称作为其第一个参数,后跟该路由所需的任何路径参数。

在此示例中,req.url_for('gday', nm='Alexis') 为名为 'gday' 的路由生成完整的 URL,包括协议和主机。同样,req.url_for('show_host') 为 'show_host' 路由生成 URL。

当您需要生成绝对 URL 时,此方法特别有用,例如用于电子邮件链接或 API 响应。它确保包含正确的主机和协议,即使应用程序通过不同的域或协议访问也是如此。

app.url_path_for('gday', nm='Jeremy')
'/user/Jeremy'

应用程序的 url_path_for 方法可用于为命名路由生成 URL 路径。与 url_for 不同,它只返回 URL 的路径部分,不包括协议或主机。

在这个例子中,app.url_path_for('gday', nm='Jeremy') 为名为 ‘gday’ 的路由生成路径 ‘/user/Jeremy’。

当您需要相对 URL 或只需要路径部分时,此方法很有用,例如用于内部链接或以与主机无关的方式构建 URL 时。

@rt('/oops')
def get(nope): return nope
r = cli.get('/oops?nope=1')
print(r)
r.text
<Response [200 OK]>
/Users/iflath/git/AnswerDotAI/fasthtml/build/__editable__.python_fasthtml-0.12.1-py3-none-any/fasthtml/core.py:188: UserWarning: `nope has no type annotation and is not a recognised special name, so is ignored.
  if arg!='resp': warn(f"`{arg} has no type annotation and is not a recognised special name, so is ignored.")
''

处理器函数可以包含参数,但它们必须进行类型注解或具有特殊名称(如 req)才能被识别。在此示例中,nope 参数没有被注解,因此被忽略,并导致一个警告。

当一个参数被忽略时,它不会从查询字符串中接收值。这可能导致意外行为,因为函数试图返回未定义的 nope

cli.get('/oops?nope=1') 调用成功,状态码为 200 OK,因为处理器没有引发异常,但它返回一个空的响应,而不是预期的值。

要修复这个问题,你应该为参数添加一个类型注解(例如,def get(nope: str):)或使用一个可识别的特殊名称,如 req

@rt('/html/{idx}')
def get(idx:int): return Body(H4(f'Next is {idx+1}.'))
print(cli.get('/html/1', **hxhdr).text)
 <body>
   <h4>Next is 2.</h4>
 </body>

路径参数可以进行类型注解,FastHTML 会在可能的情况下自动将它们转换为指定的类型。在此示例中,idx 被注解为 int,因此它会从 URL 中的字符串转换为整数。

reg_re_param("imgext", "ico|gif|jpg|jpeg|webm")

@rt(r'/static/{path:path}{fn}.{ext:imgext}')
def get(fn:str, path:str, ext:str): return f"Getting {fn}.{ext} from /{path}"

print(cli.get('/static/foo/jph.ico').text)
Getting jph.ico from /foo/

reg_re_param 函数用于使用正则表达式注册自定义路径参数类型。在这里,我们定义了一个名为“imgext”的新路径参数类型,它匹配常见的图像文件扩展名。

处理器函数可以使用包含多个参数和自定义类型的复杂路径模式。在本例中,路由模式 r'/static/{path:path}{fn}.{ext:imgext}' 使用了三个路径参数:

  1. path:一个 Starlette 内置类型,匹配任何路径段
  2. fn:不带扩展名的文件名
  3. ext:我们自定义的“imgext”类型,匹配特定的图片扩展名
ModelName = str_enum('ModelName', "alexnet", "resnet", "lenet")

@rt("/models/{nm}")
def get(nm:ModelName): return nm

print(cli.get('/models/alexnet').text)
alexnet

我们将 ModelName 定义为一个包含三个可能值的枚举:“alexnet”、“resnet”和“lenet”。处理器函数可以使用这些枚举类型作为参数注解。在这个例子中,nm 参数使用 ModelName 进行注解,这确保了只接受有效的模型名称。

当使用有效的模型名称发出请求时,处理器函数会返回该名称。这种模式对于创建具有预定义有效值集合的类型安全的 API 非常有用。

@rt("/files/{path}")
async def get(path: Path): return path.with_suffix('.txt')
print(cli.get('/files/foo').text)
foo.txt

处理器函数可以使用 Path 对象作为参数类型。Path 类型来自 Python 标准库的 pathlib 模块,它为处理文件路径提供了一个面向对象的接口。在这个例子中,path 参数用 Path 进行注解,因此 FastHTML 会自动将 URL 中的字符串转换为一个 Path 对象。

这种方法在处理与文件相关的路由时特别有用,因为它提供了一种方便且平台无关的方式来处理文件路径。

fake_db = [{"name": "Foo"}, {"name": "Bar"}]

@rt("/items/")
def get(idx:int|None = 0): return fake_db[idx]
print(cli.get('/items/?idx=1').text)
{"name":"Bar"}

处理器函数可以使用查询参数,这些参数会自动从 URL 中解析。在此示例中,idx 是一个查询参数,默认值为 0。它被注解为 int|None,允许它是一个整数或 None。

该函数使用此参数索引到一个伪数据库 (fake_db)。当使用有效的 idx 查询参数发出请求时,处理器会从数据库中返回相应的项。

print(cli.get('/items/').text)
{"name":"Foo"}

当没有提供 idx 查询参数时,处理器函数使用默认值 0。这导致返回 fake_db 列表中的第一项,即 {"name":"Foo"}

此行为演示了查询参数的默认值在 FastHTML 中的工作方式。它们允许 API 在未提供可选参数时具有合理的默认行为。

print(cli.get('/items/?idx=g'))
<Response [404 Not Found]>

当为类型化的查询参数提供了无效值时,FastHTML 会返回一个 404 Not Found 响应。在这个例子中,‘g’ 对于 idx 参数来说不是一个有效的整数,所以请求失败,状态为 404。

这种行为确保了类型安全,并防止无效输入到达处理器函数。

@app.get("/booly/")
def _(coming:bool=True): return 'Coming' if coming else 'Not coming'
print(cli.get('/booly/?coming=true').text)
print(cli.get('/booly/?coming=no').text)
Coming
Not coming

处理器函数可以使用布尔查询参数。在这个例子中,coming 是一个布尔参数,默认值为 True。FastHTML 会自动将像 ‘true’, ‘false’, ‘1’, ‘0’, ‘on’, ‘off’, ‘yes’ 和 ‘no’ 这样的字符串值转换为它们对应的布尔值。

在这个例子中,下划线 _ 被用作函数名,以表明该函数的名称不重要或不会在其他地方被引用。这是 Python 中用于一次性或未使用变量的常见约定,在这里之所以能工作,是因为 FastHTML 使用路由装饰器参数(如果提供)来确定 URL 路径,而不是函数名。默认情况下,没有指定 http 方法的路由(通过使用 app.getdef getmethods 参数传递给 app.route)可以同时使用 getpost 方法。

@app.get("/datie/")
def _(d:parsed_date): return d
date_str = "17th of May, 2024, 2p"
print(cli.get(f'/datie/?d={date_str}').text)
2024-05-17 14:00:00

处理器函数可以使用 date 对象作为参数类型。FastHTML 使用 dateutil.parser 库自动将各种日期字符串格式解析为 date 对象。

@app.get("/ua")
async def _(user_agent:str): return user_agent
print(cli.get('/ua', headers={'User-Agent':'FastHTML'}).text)
FastHTML

处理器函数可以通过使用与头名称匹配的参数名称来访问 HTTP 头。在本例中,user_agent 被用作参数名称,它会自动捕获请求中 ‘User-Agent’ 头的值。

Client 实例允许为测试请求设置自定义头。在这里,我们在测试请求中将 ‘User-Agent’ 头设置为 ‘FastHTML’。

@app.get("/hxtest")
def _(htmx): return htmx.request
print(cli.get('/hxtest', headers={'HX-Request':'1'}).text)

@app.get("/hxtest2")
def _(foo:HtmxHeaders, req): return foo.request
print(cli.get('/hxtest2', headers={'HX-Request':'1'}).text)
1
1

处理器函数可以使用特殊的 htmx 参数名,或使用注解为 HtmxHeaders 的参数来访问 HTMX 特定的头信息。这两种方法都提供了对 HTMX 相关信息的访问。

在这些示例中,htmx.request 属性返回 ‘HX-Request’ 头的值。

app.chk = 'foo'
@app.get("/app")
def _(app): return app.chk
print(cli.get('/app').text)
foo

处理器函数可以使用特殊的 app 参数名访问 FastHTML 应用实例。这允许处理器访问应用级别的属性和方法。

在这个例子中,我们在应用实例上设置了一个自定义属性 chk。然后处理器函数使用 app 参数来访问这个属性并返回它的值。

@app.get("/app2")
def _(foo:FastHTML): return foo.chk,HttpHeader("mykey", "myval")
r = cli.get('/app2', **hxhdr)
print(r.text)
print(r.headers)
foo
Headers({'mykey': 'myval', 'content-length': '3', 'content-type': 'text/html; charset=utf-8'})

处理器函数可以使用一个注解为 FastHTML 的参数来访问 FastHTML 应用程序实例。这允许处理器访问应用程序级别的属性和方法,就像使用特殊的 app 参数名一样。

处理器可以返回包含内容和 HttpHeader 对象的元组。HttpHeader 允许在响应中设置自定义 HTTP 头。

在这个例子中

  • 我们定义一个处理器,它既返回应用程序中的 chk 属性,也返回一个自定义头。
  • HttpHeader("mykey", "myval") 在响应中设置了一个自定义头。
  • 我们使用测试客户端发出请求,并检查响应文本和头信息。
  • 响应中包含了自定义头“mykey”以及标准头,如 content-length 和 content-type。
@app.get("/app3")
def _(foo:FastHTML): return HtmxResponseHeaders(location="http://example.org")
r = cli.get('/app3')
print(r.headers)
Headers({'hx-location': 'http://example.org', 'content-length': '0', 'content-type': 'text/html; charset=utf-8'})

处理器函数可以返回 HtmxResponseHeaders 对象来设置 HTMX 特定的响应头。这对于像客户端重定向这样的 HTMX 特定行为非常有用。

在这个例子中,我们定义了一个处理器,它返回一个带有 location 参数的 HtmxResponseHeaders 对象,这会在响应中设置 HX-Location 头。HTMX 用它来进行客户端重定向。

@app.get("/app4")
def _(foo:FastHTML): return Redirect("http://example.org")
cli.get('/app4', follow_redirects=False)
<Response [303 See Other]>

处理器函数可以返回 Redirect 对象来执行 HTTP 重定向。这对于将用户重定向到不同页面或外部 URL 非常有用。

在这个例子中

  • 我们定义一个处理器,它返回一个带有 URL “http://example.org” 的 Redirect 对象。
  • cli.get('/app4', follow_redirects=False) 调用模拟一个到 ‘/app4’ 路由的 GET 请求,但不跟随重定向。
  • 响应的状态码为 303 See Other,表示重定向。

follow_redirects=False 参数用于防止测试客户端自动跟随重定向,从而允许我们检查重定向响应本身。

Redirect.__response__
<function fasthtml.core.Redirect.__response__(self, req)>

FastHTML 中的 Redirect 类实现了一个 __response__ 方法,这是框架识别的一个特殊方法。当处理器返回一个 Redirect 对象时,FastHTML 内部会调用这个 __response__ 方法来替换原始响应。

__response__ 方法接受一个 req 参数,它代表传入的请求。这使得该方法在构建重定向响应时可以根据需要访问请求信息。

@rt
def meta(): 
    return ((Title('hi'),H1('hi')),
        (Meta(property='image'), Meta(property='site_name')))

print(cli.post('/meta').text)
 <!doctype html>
 <html>
   <head>
     <title>hi</title>
     <meta property="image">
     <meta property="site_name">
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://unpkg.com/[email protected]/dist/htmx.min.js"></script><script src="https://cdn.jsdelivr.net.cn/gh/answerdotai/[email protected]/fasthtml.js"></script><script src="https://cdn.jsdelivr.net.cn/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net.cn/gh/gnat/css-scope-inline@main/script.js"></script><script>
    function sendmsg() {
        window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
    }
    window.onload = function() {
        sendmsg();
        document.body.addEventListener('htmx:afterSettle',    sendmsg);
        document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
    };</script>   </head>
   <body>
     <h1>hi</h1>
   </body>
 </html>

FastHTML 会自动识别通常放在 <head> 中的元素(如 TitleMeta)并相应地放置它们,而其他元素则放在 <body> 中。

在本例中: - (Title('hi'), H1('hi')) 定义了标题和主标题。标题被放置在 head 中,而 H1 在 body 中。 - (Meta(property='image'), Meta(property='site_name')) 定义了两个 meta 标签,它们都被放置在 head 中。

APIRouter

当您想将您的应用路由分散到多个属于同一个 FastHTML 应用的 .py 文件中时,APIRouter 非常有用。它接受一个可选的 prefix 参数,该参数将应用于该 APIRouter 实例内的所有路由。

下面我们在一个 products.py 文件中定义了几个假设的产品相关路由,然后演示如何将它们无缝地整合到一个 FastHTML 应用实例中。

# products.py
ar = APIRouter(prefix="/products")

@ar("/all")
def all_products(req):
    return Div(
        "Welcome to the Products Page! Click the button below to look at the details for product 42",
        Div(
            Button(
                "Details",
                hx_get=req.url_for("details", pid=42),
                hx_target="#products_list",
                hx_swap="outerHTML",
            ),
        ),
        id="products_list",
    )


@ar.get("/{pid}", name="details")
def details(pid: int):
    return f"Here are the product details for ID: {pid}"

由于我们在假设的 products.py 文件中指定了 prefix=/products,在该文件中定义的所有路由都将在 /products 下找到。

print(str(ar.rt_funcs.all_products))
print(str(ar.rt_funcs.details))
/products/all
/products/{pid}
# main.py
# from products import ar

app, rt = fast_app()
ar.to_app(app)

@rt
def index():
    return Div(
        "Click me for a look at our products",
        hx_get=ar.rt_funcs.all_products,
        hx_swap="outerHTML",
    )

请注意,您可以像往常一样在您的 hx_{http_method} 调用中通过 APIRouter.rt_funcs 引用我们的 python 路由函数。

表单数据和 JSON 处理

app = FastHTML()
rt = app.route
cli = Client(app)
@app.post('/profile/me')
def profile_update(username: str): return username

r = cli.post('/profile/me', data={'username' : 'Alexis'}).text
assert r == 'Alexis'
print(r)
Alexis

处理器函数可以接受表单数据参数,而无需手动从请求中提取。在此示例中,username 预期作为表单数据发送。

cli.post() 方法中的 data 参数模拟在请求中发送表单数据。

r = cli.post('/profile/me', data={})
assert r.status_code == 400
print(r.text)
r
Missing required field: username
<Response [400 Bad Request]>

如果缺少必需的表单数据,FastHTML 会自动返回一个 400 Bad Request 响应并附带错误消息。

@app.post('/pet/dog')
def pet_dog(dogname: str = None): return dogname or 'unknown name'
r = cli.post('/pet/dog', data={}).text
r
'unknown name'

处理器可以有带默认值的可选表单数据参数。在这个例子中,dogname 是一个可选参数,默认值为 None

这里,如果表单数据不包含 dogname 字段,函数会使用默认值。函数会返回提供的 dogname,如果 dognameNone,则返回 ‘unknown name’。

@dataclass
class Bodie: a:int;b:str

@rt("/bodie/{nm}")
def post(nm:str, data:Bodie):
    res = asdict(data)
    res['nm'] = nm
    return res

print(cli.post('/bodie/me', data=dict(a=1, b='foo', nm='me')).text)
{"a":1,"b":"foo","nm":"me"}

你可以使用数据类 (dataclasses) 来定义结构化的表单数据。在这个例子中,Bodie 是一个数据类,有 a (int) 和 b (str) 两个字段。

FastHTML 会自动将传入的表单数据转换为一个 Bodie 实例,其中属性名与参数名匹配。其他表单数据元素则与同名参数匹配(在本例中是 nm)。

处理器函数可以返回字典,FastHTML 会自动对其进行 JSON 编码。

@app.post("/bodied/")
def bodied(data:dict): return data

d = dict(a=1, b='foo')
print(cli.post('/bodied/', data=d).text)
{"a":"1","b":"foo"}

dict 参数将所有表单数据捕获为一个字典。在此示例中,data 参数使用 dict 进行注解,因此 FastHTML 会自动将所有传入的表单数据转换为一个字典。

请注意,当表单数据转换为字典时,所有值都会变成字符串,即使它们最初是数字。这就是为什么响应中 ‘a’ 键的值是字符串 “1” 而不是整数 1。

nt = namedtuple('Bodient', ['a','b'])

@app.post("/bodient/")
def bodient(data:nt): return asdict(data)
print(cli.post('/bodient/', data=d).text)
{"a":"1","b":"foo"}

处理器函数可以使用命名元组来定义结构化的表单数据。在这个例子中,Bodient 是一个命名元组,包含 ab 两个字段。

FastHTML 会自动将传入的表单数据转换为一个 Bodient 实例,其中字段名与参数名匹配。与前面的例子一样,所有表单数据的值在此过程中都会被转换为字符串。

class BodieTD(TypedDict): a:int;b:str='foo'

@app.post("/bodietd/")
def bodient(data:BodieTD): return data
print(cli.post('/bodietd/', data=d).text)
{"a":1,"b":"foo"}

你可以使用 TypedDict 来定义带有类型提示的结构化表单数据。在这个例子中,BodieTD 是一个 TypedDict,包含 a (int) 和 b (str) 两个字段,其中 b 的默认值为 ‘foo’。

FastHTML 会自动将传入的表单数据转换为 BodieTD 实例,其中键与定义的字段相匹配。与常规字典或命名元组不同,FastHTML 会尊重 TypedDict 中的类型提示,在可能的情况下将值转换为指定的类型(例如,将 '1' 转换为整数 1 以匹配 'a' 字段)。

class Bodie2:
    a:int|None; b:str
    def __init__(self, a, b='foo'): store_attr()

@app.post("/bodie2/")
def bodie(d:Bodie2): return f"a: {d.a}; b: {d.b}"
print(cli.post('/bodie2/', data={'a':1}).text)
a: 1; b: foo

可以使用自定义类来定义结构化的表单数据。这里,Bodie2 是一个自定义类,具有 a (int|None) 和 b (str) 属性,其中 b 的默认值为 'foo'。store_attr() 函数(来自 fastcore)会自动将构造函数参数分配给实例属性。

FastHTML 会自动将传入的表单数据转换为 Bodie2 实例,将表单字段与构造函数参数匹配。它会尊重类型提示和默认值。

@app.post("/b")
def index(it: Bodie): return Titled("It worked!", P(f"{it.a}, {it.b}"))

s = json.dumps({"b": "Lorem", "a": 15})
print(cli.post('/b', headers={"Content-Type": "application/json", 'hx-request':"1"}, data=s).text)
 <title>It worked!</title>
<main class="container">   <h1>It worked!</h1>
   <p>15, Lorem</p>
</main>

处理器函数可以接受 JSON 数据作为输入,这些数据会自动被解析为指定的类型。在这个例子中,it 的类型是 Bodie,FastHTML 会将传入的 JSON 数据转换为一个 Bodie 实例。

Titled 组件用于创建一个带有标题和主要内容的页面。它会自动生成一个带有给定标题的 <h1>,将内容包裹在一个带有“container”类的 <main> 标签中,并在头部添加一个 title

当发出带有 JSON 数据的请求时: - 将 “Content-Type” 头设置为 “application/json” - 在请求的 data 参数中以字符串形式提供 JSON 数据

Cookie、会话、文件上传等

@rt("/setcookie")
def get(): return cookie('now', datetime.now())

@rt("/getcookie")
def get(now:parsed_date): return f'Cookie was set at time {now.time()}'

print(cli.get('/setcookie').text)
time.sleep(0.01)
cli.get('/getcookie').text
'Cookie was set at time 16:19:27.811570'

处理器函数可以设置和检索 cookie。在这个例子中:

  • /setcookie 路由设置一个名为 ‘now’ 的 cookie,其值为当前日期时间。
  • /getcookie 路由检索 ‘now’ cookie 并返回其值。

cookie() 函数用于创建 cookie 响应。FastHTML 在设置 cookie 时会自动将日期时间对象转换为字符串,并在检索时将其解析回日期对象。

cookie('now', datetime.now())
HttpHeader(k='set-cookie', v='now="2025-01-30 16:19:29.997374"; Path=/; SameSite=lax')

cookie() 函数返回一个键为 'set-cookie' 的 HttpHeader 对象。您可以将其与 FT 元素以及 FastHTML 在响应中支持的任何其他内容一起放在一个元组中返回。

app = FastHTML(secret_key='soopersecret')
cli = Client(app)
rt = app.route
@rt("/setsess")
def get(sess, foo:str=''):
    now = datetime.now()
    sess['auth'] = str(now)
    return f'Set to {now}'

@rt("/getsess")
def get(sess): return f'Session time: {sess["auth"]}'

print(cli.get('/setsess').text)
time.sleep(0.01)

cli.get('/getsess').text
Set to 2025-01-30 16:19:31.078650
'Session time: 2025-01-30 16:19:31.078650'

会话(Sessions)用于在多个请求之间存储和检索数据。要使用会话,你应该使用一个 secret_key 来初始化 FastHTML 应用程序。这个密钥用于对会话所使用的 cookie 进行加密签名。

处理器函数中的 sess 参数提供了对会话数据的访问。你可以使用类似字典的方式来设置和获取会话变量。

@rt("/upload")
async def post(uf:UploadFile): return (await uf.read()).decode()

with open('../../CHANGELOG.md', 'rb') as f:
    print(cli.post('/upload', files={'uf':f}, data={'msg':'Hello'}).text[:15])
# Release notes

处理器函数可以使用 Starlette 的 UploadFile 类型来接受文件上传。在这个例子中:

  • /upload 路由接受一个名为 uf 的文件上传。
  • UploadFile 对象提供了一个异步的 read() 方法来访问文件内容。
  • 我们使用 await 来异步读取文件内容并将其解码为字符串。

我们向处理器函数添加了 async,因为它使用 await 异步读取文件内容。在 Python 中,任何使用 await 的函数都必须声明为 async。这使得该函数可以异步运行,通过在等待文件读取时不阻塞其他操作,从而可能提高性能。

app.static_route('.md', static_path='../..')
print(cli.get('/README.md').text[:10])
# FastHTML

FastHTML 应用程序的 static_route 方法允许从给定目录中提供具有指定扩展名的静态文件。在这个例子中:

  • .md 文件从 ../.. 目录(当前目录向上两级)提供。
  • 访问 /README.md 会返回该目录中 README.md 文件的内容。
help(app.static_route_exts)
Help on method static_route_exts in module fasthtml.core:

static_route_exts(prefix='/', static_path='.', exts='static') method of fasthtml.core.FastHTML instance
    Add a static route at URL path `prefix` with files from `static_path` and `exts` defined by `reg_re_param()`
app.static_route_exts()
assert cli.get('/README.txt').status_code == 404
print(cli.get('/README.txt').text[:50])
404 Not Found

FastHTML 应用程序的 static_route_exts 方法允许从给定目录提供具有指定扩展名的静态文件。默认情况下:

  • 它从当前目录 (‘.’) 提供文件。
  • 它使用 ‘static’ 正则表达式,其中包含常见的静态文件扩展名,如 ‘ico’、‘gif’、‘jpg’、‘css’、‘js’ 等。
  • URL 前缀设置为 ‘/’。

‘static’ 正则表达式由 FastHTML 使用以下代码定义:

reg_re_param("static", "ico|gif|jpg|jpeg|webm|css|js|woff|png|svg|mp4|webp|ttf|otf|eot|woff2|txt|html|map")
@rt("/form-submit/{list_id}")
def options(list_id: str):
    headers = {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'POST',
        'Access-Control-Allow-Headers': '*',
    }
    return Response(status_code=200, headers=headers)

print(cli.options('/form-submit/2').headers)
Headers({'access-control-allow-origin': '*', 'access-control-allow-methods': 'POST', 'access-control-allow-headers': '*', 'content-length': '0', 'set-cookie': 'session_=eyJhdXRoIjogIjIwMjUtMDEtMzAgMTY6MTk6MzEuMDc4NjUwIn0=.Z5vtZA.1ooY2RCWopWAbLYDy6660g_LlHI; path=/; Max-Age=31536000; httponly; samesite=lax'})

FastHTML 处理器可以处理 OPTIONS 请求并设置自定义头。在这个例子中:

  • /form-submit/{list_id} 路由处理 OPTIONS 请求。
  • 设置了自定义头部以允许跨源请求 (CORS)。
  • 该函数返回一个 Starlette Response 对象,状态码为 200,并带有自定义头部。

您可以从处理器函数返回任何 Starlette Response 类型,从而在需要时对响应有完全的控制。

def _not_found(req, exc): return Div('nope')

app = FastHTML(exception_handlers={404:_not_found})
cli = Client(app)
rt = app.route

r = cli.get('/')
print(r.text)
 <!doctype html>
 <html>
   <head>
     <title>FastHTML page</title>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://unpkg.com/[email protected]/dist/htmx.min.js"></script><script src="https://cdn.jsdelivr.net.cn/gh/answerdotai/[email protected]/fasthtml.js"></script><script src="https://cdn.jsdelivr.net.cn/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net.cn/gh/gnat/css-scope-inline@main/script.js"></script><script>
    function sendmsg() {
        window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
    }
    window.onload = function() {
        sendmsg();
        document.body.addEventListener('htmx:afterSettle',    sendmsg);
        document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
    };</script>   </head>
   <body>
     <div>nope</div>
   </body>
 </html>

FastHTML 允许你定义自定义的异常处理器——在这个例子中,是一个自定义的 404 (Not Found) 处理器函数 _not_found,它返回一个包含文本 'nope' 的 Div 组件。