NotStr=Safe
如果有html的string,需要放在Div里面,需要告知它不是str,而是html,比如
html_content = df.to_html()
# 用NotStr (general的)
Div(NotStr(content))
# 用 Safe可以(fasthtml-specific)
Div(Safe(content))
to_xml
可以把fastTag变成html语言
to_xml(Style("aaa")) # <style>aaa</style>\n
但它的type不是str,所以如果需要把df的某个column变成这样的话,需要在前面再加一个Str,比如
Str(to_xml(Div(..)))
另外还要注意,上面最后的部分总会多出来一个\n,如果要remove,用.strip()即可
to_xml(Style("aaa")).strip() # <style>aaa</style>
stip()不会把中间的\n去掉,只会去掉结尾的那个\n
@rt('/xx/yy')
这里面所有的relative指向,比如 ./other_route,都会在前面加上/xx/other_route
所以尽量避免这种route,可以用rt('/xx_yy')代替
Components
分割线,horizontal rule/line
Hr()
换行, break
Br()
NotStr, 保护<>格式不会变成别的
# Without NotStr: <br> is escaped
Div("Line1<br>Line2")
# => <div>Line1<br>Line2</div>
# With NotStr: <br> is rendered as HTML
Div(NotStr("Line1<br>Line2"))
# => <div>Line1<br>Line2</div>
SVG
matplotlib 保存的svg的图看起来是这样的:

添加图片注释,不超过 140 字(可选)
注意里面有xmlns:xlink,和xmlns, 是SVG 版本1.1。
text 部分可以加A xlink:href,会有超链接
<a xlink:href="{href}">{label}</a>
Pipeline
建立main.py 和subpage.py (这里的example是about.py)
main.py里
from fasthtml.common import *
from about import app as about_app
app, rt = fast_app()
app.mount("/about", about_app) # option 1
# app, rt = fast_app(routes=[Mount("/about", about_app)]) # option 2
# from about import rt as about_rt # option 3
# about_rt.to_app(app)
@rt
def index():
return Titled("Main Page",
Ul(
Li(A("Products",href='/about/')),
Li(A("Product #42"))
)
)
serve()
about.py里
from fasthtml.common import *
app, rt = fast_app()
# rt = APIRouter(prefix="/about") # correspond to option3
@rt
def index():
return Titled("HTMX Form Demo",
Form(
Input(name="text", placeholder="Enter your name"),
Button("Submit", type="submit"),
hx_post="./greet", # work best
# hx_post=greet, # does not work
# post=greet,# partially work, don't know why
hx_target="#result",
hx_swap="outerHTML"
),
Div(id="result")
)
@rt
def greet(text:str):
return Div(P(f"Hello, {text}!"),id='result')
serve()
hx_post =func 不work (会直接/greet 而不是/about/greet, 已经report bug), 但是post=func work,但是如果复杂的func,还是要用hx_post='./func', href也要用href='./func',索性统一一些,全部都改了吧。
mount 后
hx_post 和href都不会因为mount之后而更改path,所以需要相对路径。./
但是fastag里的东西会随着mount后更改path
Script(src="imports/alphafold_display.js")
mount 后(用prefix为query)会变成/query/imports/alphafold_display.js
解决办法就是用绝对路径,在前面加一个/
Script(src="/imports/alphafold_display.js")
另外如果是rt里的
@rt('/api/protein/{uniprot_id}'
mount后会在前面加上prefix
另外注意rt里的东西必须以/为开头。
ar 下面的
但凡在element里面的,会自动加上前缀/subpage
如果不在element里面的路径,就用绝对路径。
display svg with link
存储svg的时候一定要加
plt.rcParams['svg.fonttype'] = 'none'
plt.savefig('image.sv', bbox_inches='tight')
这样可以确保 gene_ID 的结构,方便后续更改。
读取svg
svg_path = Path("image.svg")
svg_html = svg_path.read_text()
然后改成这样的结构
<a xlink:href="/some/page"> ... </a>
注意加xlink,因为svg follows older XML-based specs, it requires the xlink:href attribute.
注意这个xlink和fasthtml里的AX不是一回事,那个是用于htmx link的
AX("Text", hx_get=some_route, target_id="div_id")
删掉width 和height info
svg_html = re.sub(r'\s(width|height)="[^"]*pt"', '', svg_html)
这样可以方便后面div调整size
Div(Safe(svg_html),style="width: 100%; max-width: 800px; height: auto;")
Routes
一种是这个样子的,只要rt后面括弧里有,就会以它的路径为准
@rt("/{gene_id}")
def gallery(gene_id: str):
然后链接就是/gene_id
如果在路径前面加前缀,里面所有的路径都会加一个前缀
@rt("/g/{gene_id}")
def gallery(gene_id: str):
然后就会导致图片的source 找不到。
还有一种是这样的
@rt
def gallery(gene_id: str):
以func名字为路径,然后link是/gallery?gene_id=xxx
最后这种不会打扰到路径。
replit的bug
好像无法detect到js 的变化。
改了半天js,发现一点变化都没有,到头来原来是因为replit没有实时detect。
需要重命名js文件才行,而且这样会丢失history,不过神奇的是,把文件名改回去之后,就又有history了,真是奇怪的bug
用id 而不用name
name一般是form里用来标记value的,fasthtml会在输入id的情况下,默认name=id。
这样在form里不需要填写name,输入id即可,会默认name也是,然后直接hx_post,然后def post的时候接受id name即可
Button type
fasthtml 的button type default是submit
别的type 还有reset, button。
其中button可以用来加上customize js
sess里面自动就有sess_id
也可以设置
if 'sess_id' not in sess: sess['sess_id'] = str(uuid.uuid4())
每天删除tmp老文件
方法1:cron
0 2 * * * find /path/to/your/tmp -type f -mtime +1 -delete
Runs every day at 2am, deletes files older than 1 day
方法2,插入一段code 到server()前:
from pathlib import Path
import time
from threading import Thread
def cleanup_tmp_folder(TMP_DIR=Path("tmp"),
CLEANUP_INTERVAL=86400, # check once per day
EXPIRATION_TIME=86400): # delete files older than 1 day
while True:
now = time.time()
for file in TMP_DIR.iterdir():
if file.is_file():
try:
age = now - file.stat().st_mtime
if age > EXPIRATION_TIME:
file.unlink()
print(f"[cleanup] Deleted: {file.name} (age: {age:.0f}s)")
except Exception as e:
print(f"[cleanup] Failed to delete {file.name}: {e}")
time.sleep(CLEANUP_INTERVAL)
def start_cleanup_thread(tmp_dir=Path("tmp"), interval=86400, expiration=86400):
thread = Thread(
target=cleanup_tmp_folder,
args=(tmp_dir, interval, expiration),
daemon=True
)
thread.start()
用的时候是
start_cleanup_thread()
htmx trick
更多的:
hx_swap
default是innerHTML, 可以改成别的
@rt
def index():
return Div(
"Products",
hx_get=all_products, hx_swap="outerHTML")
也可以是复杂一点的hx_swap="beforeend show:bottom"
@rt
def index(): return Div(hx_ext="sse", sse_connect="/numstream", hx_swap="beforeend show:bottom", sse_swap="message")
hx_trigger
fasthtml default:
- form 里,default hx_trigger是submit
- button, a, div 里,hx_trigger是 click
form里:(不需要放hx_trigger,默认的)
Form(hx_post=save_profile, hx_target="#msg")(
Input(name="email"),
Button("Submit")
)
button里
Button("Click me", hx_post=some_route, hx_target="#result")
hx_trigger='load, submit' 可以在页面load的时候就有placeholder post
grp = Form(
Group(
get_select('Phospho status',option_list=['CDDM_upper','CDDM_standard'],id='CDDM_select',cls='col-3'),
get_input(label_text='Phospho-site sequence',id='seq',cls='col-7',value='AAASQAA'),
Button('Load',cls="btn btn-primary col-2"),
cls='row g-2 align-items-stretch'
),hx_trigger='load, submit',
hx_post=single,hx_target="#ft",cls='mt-4')
hx_disabled_elt
hx_disabled_elt 可以避免用户反复去点提交,如果正在运行的话
send_select = Form(
Hidden(value=df.to_json(),id='upload_df'),
Div(select_db,button,cls='row g-2 my-1'),
post=search_pssm,
target_id='plot_area',
hx_indicator='#spinner',
hx_disabled_elt="#plot_button"
)
button = Div(Button('Search', cls="btn btn-primary",id='plot_button'),cls='col-1')
spinner=get_spinner(id='spinner')
hx_indicator
如上,可以显示正在运行的动画。
搭配
form + htmx
form + hx_post + hx_target
Form(hx_post=submit_profile, hx_target="#result")(...)
@rt
def index():
return (
Form(hx_post=upload, hx_target="#result")(
Input(type="file", name="file"),
Button("Upload", type="submit")),
Div(id="result")
)
尽量用button来submit,而不是直接在form里trigger
button + htmx
Button("Details",hx_get=details.to(pid=42),hx_target="#products_list",hx_swap="outerHTML",)
form + action 页面跳转
Form(action=profile)(...)
效果和form +method + action是一样的,因为default method 是post
Form(method="post", action=submit_profile)(...)
@dataclass
class Profile: email:str; phone:str; age:int
email = 'john@example.com'
profiles = {email: Profile(email=email, phone='123456789', age=5)}
profile_form = Form(method="post", action=profile)(
Fieldset(
Label('Email', Input(name="email")),
Label("Phone", Input(name="phone")),
Label("Age", Input(name="age"))),
Button("Save", type="submit"))
@rt
def profile(email:str): return fill_form(profile_form, profiles[email])
注意Button("Save", type="submit")
A + href 页面跳转
A("View Profile", href=profile.to(email="user@example.com"))
不用hardcode link
Form(method="post", action="/link”)
新版,form里用hx_post不要用post
@rt
def index():
return (
Form(hx_post=upload, hx_target="#result")(
Input(type="file", name="file"),
Button("Upload", type="submit")),
Div(id="result")
)
用func 而不用url
用
Form(action=submit_form)
而不用
Form(action="/submit-form")
用func.to(k=v) 而不要用/func?k=v
reference: https://www.fastht.ml/docs/ref/handlers.html#request-and-response-objects
# example 1
@rt
def yoyo(): return 'a yoyo'
@app.get
def autopost(): return Html(Div('Text.', hx_post=yoyo.to()))
# <div hx-post="/yoyo">Text.</div>
# example2
@app.get("/hostie")
def show_host(req): return req.headers['host']
@app.get
def autoget(): return Html(Body(Div('Text.', cls='px-2', hx_post=show_host.to(a='b'))))
# <div hx-post="/hostie?a=b" class="px-2">Text.</div>
新版,用hx_target而不要用target_id
target_id 前面不用加#
hx_target前面加#
# 新版
Button("Next", hx_get="/table-rows?offset=20", hx_target="#table-body")
# 旧版
Button("Next", hx_get="/table-rows?offset=20", target_id="table-body")
AX("Page 2", hx_get="/table-rows?offset=20", target_id="table-body", swap="outerHTML")
Route格式
@rt 官方用的最多的,新版本
如果用@rt,不指定route,那么后面的function名字就是它的route;所以作用和@app.get/post很像;避免@rt后面跟get或者post,最好用一个function命名它的route。
rt 既是get又是post
@rt
def sdf():
主页的话用这个
@rt
def index():
llm里面说,所以如果func name是index,会自动匹配/ 路线。
而不用这个(老的)
@rt("/") # 不要用这个
def get():
指定get或者post (其实不需要,可以自动识别)
@rt.get
def sdf():
@rt.post
def sdf():
route with variable
把variable放到rt('/{}')里,同时放到下面的function里,就可以直接query那个界面
@rt("/{data}")
def sdf(req, data:str, page: int = 1):
上面的page没有放到rt里,query的时候就会变成route?page=1,比如XX.com/sdf?page=1
还有
@rt("/{fname:path}.{ext:static}")
async def serve_static_file(fname:str, ext:str): return FileResponse(f'public/{fname}.{ext}')
@rt('/'), 旧版本
如果用@rt('/'),那么括弧里的就是route,会自动忽略function的name
- 后面可以跟def get()或者def post()
- 也可以跟一个cutomize的名字,比如def home(),那么它既是get又是post method,但是url是由rt决定的,而不是function名字,这里的话就是'/'
@rt("/") # 主页,/后面什么都不加
def get():
@rt("/sdf") # link是/sdf
def get():
@rt("/sdf")
def post():
@app.get ,旧版本
如果用@app.post或者@app.get,
- 需要一个customize的名字,比如def sdf(), 它的url跟function名字,那这里就是'/sdf'。
@app.post
def sdf(a,b)
APIRouter没有app.mount好用
main.py
from fasthtml.common import *
import about
app, rt = fast_app()
app.mount("/about", about.app)
@rt
def index():
return Titled("Main Page",
Ul(
Li(A("Products",href='/about/')),
Li(A("Product #42"))
)
)
serve()
about.py
from fasthtml.common import *
app, rt = fast_app()
@rt
def index():
return Titled("Main Page",
Ul(
Li(A("about")),
Li(A("about #42"))
)
)
serve()
测试about的时候,直接python about.py即可
APIRouter
每个subpage.py里的function名字不能重名,否则就会发生混乱用function的情况。
建一个subpage.py
ar = APIRouter(prefix="/converter") # 不要用 app,rt = fast_app(hdrs=links,pico=False)
@ar
def index():
@ar
def post_function(text: str):
# serve()
在main.py里
from fasthtml.common import *
from subpage import ar as subpage_ar
app,rt = fast_app(hdrs=links,pico=False)
subpage_ar.to_app(app)
@rt
def index():
return Div(A('converter',href=converter_ar.rt_funcs.index))
# 注意这里用的是converter_ar.rt_funcs.index
serve()
main.py里一定要有serve()
处理APIRouter里的link
直接用function的名字 (新版本)
hx_post后面可以跟一个function
Textarea(linkedomics_test,
placeholder='Gene site',
id='linkedomics_text',
rows=11,
post=convert_linkedomics,
target_id='output1',
hx_trigger='load, keyup changed delay:500ms',
)
#后面的function
@ar
def convert_linkedomics(linkedomics_text: str):
Others
save uploaded files to directory
from fasthtml.common import *
from pathlib import Path
app, rt = fast_app()
upload_dir = Path("filez")
upload_dir.mkdir(exist_ok=True)
@rt('/')
def get():
return Titled("Multiple File Upload Demo",
Article(
Form(hx_post=upload_many, hx_target="#result-many")(
Input(type="file", name="files", multiple=True),
Button("Upload", type="submit", cls='secondary'),
),
Div(id="result-many")
)
)
def FileMetaDataCard(file):
return Article(
Header(H3(file.filename)),
Ul(
Li('Size: ', file.size),
Li('Content Type: ', file.content_type),
Li('Headers: ', file.headers),
)
)
@rt
async def upload_many(files: list[UploadFile]):
cards = []
for file in files:
cards.append(FileMetaDataCard(file))
filebuffer = await file.read()
(upload_dir / file.filename).write_bytes(filebuffer)
return cards
serve()
fill_form
第一个Position是form,第二个是填的class信息
profile_form = Form(action=profile)(
Label("Email", Input(name="email")),
Button("Save", type="submit")
)
@rt
def profile(email:str): return fill_form(profile_form, profiles[email])
user_profile_path = profile.to(email="user@example.com") # '/profile?email=user%40example.com'
用dataclass
@dataclass
class Profile: email:str; phone:str; age:int
email = 'john@example.com'
profiles = {email: Profile(email=email, phone='123456789', age=5)}
@rt
def profile(email:str): return fill_form(profile_form, profiles[email])
profile_form = Form(method="post", action=profile)(
Fieldset(
Label('Email', Input(name="email")),
Label("Phone", Input(name="phone")),
Label("Age", Input(name="age"))),
Button("Save", type="submit"))
创建sqlite db
fastlite是sqlite的wrapper
from fastlite import *
db = database(':memory:') # or database('data/app.db')
class Book: isbn: str; title: str; pages: int; userid: int
books = db.create(Book, pk='isbn', transform=True)
class User: id: int; name: str; active: bool = True
users = db.create(User, transform=True) # If no pk is provided, id is used as the primary key.
user = users.insert(name='Alex',active=False)
users()
users(where="name='Alex'")
users[user.id]
user.name='Lauren'
user.active=True
users.update(user)
users.delete(user.id)
show ft in notebook
- show FT in notebook cell: use render_ft() 来替换%%s
- header style: use nb_hdrs=True 来替换show(*hdrs)
app,rt = fast_app(pico=False,hdrs=hdrs,nb_hdrs=True) # if nb_hdrs, no need to use show(*hdrs)
server = JupyUvi(app)
render_ft()
url?id=...
在这样的route下
@rt
def sdf(uniprot_id:str):
可以用'/sdf?uniprot_id=P10398' 这个路径query
用./得到mount后的路径
hx_get='/sdf'和hx_get='./sdf'的区别是,后者可以get mount后的路径
to_xml
如果用str(ft element)不会work,平时在jupyter 里直接敲ft,会出来xml的格式,但如果真的需要,用to_xml(ft_element)就可以。后面记得跟NotStr()
Table格式

添加图片注释,不超过 140 字(可选)
tr: table row
th: table header
td: table data
利用%%html和show可以在jupyter里显示style
from fasthtml.common import *
%%html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
#如果是自己电脑的localhost:8000
# according to video: https://www.youtube.com/watch?v=4ExE2GAw_po&t=936s
# and according to
# show(picocondlink)
# set_pico_cls()
def Search(*c, **kwargs)->FT:
"A PicoCSS Search, implemented as a Form with role 'search'"
return Form(*c, role="search", **kwargs)
show(Search(Input(type="search"), Button("Search")))
href的function不要重名,其它hx_post或hx_get的function也最好别重名。
hx_boost='true' & href=link 相当于hx_get=link
A(seq['name'], href =f'/sequence/{seq["id"]}', target_id='result',hx_boost='true')
和
A(seq['name'], hx_get =f'/sequence/{seq["id"]}', target_id='result')
是相等的
examples
click sth to show up sth
from fasthtml.common import *
app, rt = fast_app()
# This would typically come from a database. For this example, we'll use a simple list.
ids=[1,2,3]
@rt('/')
def get():
id_list = Ul(*[
Li(
A(idx, hx_get=f'/sequence/{idx}', target_id='result'),
) for idx in ids
])
return Titled("List",id_list,Div(id="result") )
@rt('/sequence/{id}')
def get(id: int):
return P(f'{id} is id!')
Components
flexbox
from fasthtml.common import *
gridlink = Link(
rel="stylesheet",
href=
"https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css",
type="text/css")
app, rt = fast_app(hdrs=(picolink, gridlink))
页面提示报错/成功,toasts
接着上面继续:
setup_toasts(app)
后面加在:
rt('/')
def post(sess, XXX:str):
if not XXX: return add_toast(sess, "Please enter a valid sequence", "error") # or "warning"
sess不需要给定一个class,一般是一个空的dict
A(cls='stretched-link')可以cover parent element
A(href=link, cls='stretched-link')
比如
Div(card,A(href=link, cls='stretched-link'))
Input + select options + button
可以直接用Search(input, options, button, hx_post, target_id)
Search(# Search is for pico style
Input(type="search",
name='name', #不用设置name,设置id也可以,default会让id=name
placeholder='Search name'),
Button("Search"),
hx_post="/sdf",
target_id='context')
picostyle是圆形的框框
也可以用Form(Group(XXXX),hx_post, target_id), 方形的框框。Group其实是Fieldset(),用来把几个component连在一起,通常用Form包裹。(不用的话会一行一行的)
Form(
Group(
Input(type='text',id='seq'),
Select(Option("all uppercase", value="upper", selected=True),
Option("contains lowercase", value="standard"),
id="select", cls = "box col-xs-3 col-sm-3 col-md-3 col-lg-4")),# flexbox
Button('Load'),
),
hx_post='/convert',target_id="ft")
Map + option
option_list = ['A','B']
Select(*map(Option,option_list))
用map可以巧妙的不需要重复写很多list
scroll
如果一个element太多,可以scroll,在外面套个div,设置style,overflow:auto即可
Div(element, style="height: 100px; overflow:auto")
upload/read csv or excel file
上传:
Form(
Group(
Input(id="myFile", type="file"),
Button("Submit")
),
hx_post="/", target_id='content')
load:
async def post(myFile:UploadFile):
bytes_data = await myFile.read()
df = bytes2df(bytes_data,file_type) # use StringIO for csv or BytesIO for excel
return df2html(df) # use NotStr for df.to_html(index=False,justify='left',classes=your css cls)
NotStr
有时候一些代码不需要render,直接就是html,比如
svg_data ="""<svg id='iamsvg' width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect fill="red" x="25" y="25" width="50" height="50">
</svg>"""
直接用Div包住只会显示上面的raw字符,那么就可以用NotStr
NotStr(svg_data)
Label
label是用来把text和input绑在一起的,如果点text,input里的东西也会有反应。
Label('put your value here',_for = 'idx') # 在for前加_是用来区分python的for,需要等于input的id
Input(type='range',min="1", max="10", value="1",hx_get="/sdf", id = "idx")
如果不想再Label里加_for,可以把label包在input外面
Label('put your value here', Input(...))
另外,如果input里面是htmx,则不需要在外面套上form
Undefined components
可以直接import,比如没有use,可以import Use
from fasthtml.components import Use
Bootstrap和picoCSS冲突
用的时候pico=False
app,rt = fast_app(hdrs=[bootstrap_css,bootstrap_js,bootstrap_icon,blank_style],pico=False)
Download static file with FileResponse
直接用A('download text', download=filename, href=file_path)是不行的
需要用到FileResponse
from starlette.responses import Response,StreamingResponse, FileResponse
A('Download Example File', download='example.csv', href='/download_db') #download是定义filename
@rt("/download_db")
def download_db():
return FileResponse('example.csv') # local file path
Download file on the fly with Response
A('Download',href='/download')
rt('/download')
def get():
df=pd.read_csv('sdf.csv')
csv_string=df.to_csv(index=False)
headers = {
'Content-Disposition': 'attachment; filename="data.csv"' # 这个是filename
}
return Response(csv_string, media_type='text/csv', headers=headers)
Pass dataframe to hx_post
dict_data =df.to_json()
a = Form(
Hidden(value=dict_data,id='dataframe'),
Button('Send Data'),
hx_post='/test',
hx_target='#response',
)
return a, Div(id='response')
@rt('/test')
def post(dataframe:str):
data_dict = json.loads(dataframe)
df = pd.DataFrame(data_dict)
df_table = NotStr(df.to_html())
return df_table,data_dict
Download df using javascript

添加图片注释,不超过 140 字(可选)
load javascript external file in hdrs
download_table = Script(src="download_table.js") #js script是ai生成的
为什么用external file,而不是本地把js用Script load进来呢?
一方面是美观,另一方面是本地的话不识别 \n ,必须要替换成 \n, 用Script(src)的方法load就不需要了替换了。
Get output into a textarea
hx_swap用innerText,就可以把output插入到textarea里
Form(
Group(
# Hidden()
Input(placeholder='Uniprot ID, example: P35222',type='txt',id='uniprot'),
Button('retrieve protein sequence',cls='col-3')
),
hx_post='/retrieve',
hx_swap='innerText',
target_id='full_seq'
)
Textarea(id='full_seq',rows='5')
textarea也可以继续套一个htmx ,比如
Form(
Textarea(id='full_seq',rows='5'),
Div(Button('send')),
hx_post='/query',
target_id='next_target'
)
Spinner
用hx_indicator可以trigger spinner,比如
Search(
select1, select2,input_txt,Button('Query'),
hx_post='/query',
target_id='table',
hx_indicator='#spinner'
)
spinner的class里需要注明是htmx-indicator, 否则收不到。下面是一个bootstrap spinner
spinner=Div(Div(
Span('Loading...', cls='visually-hidden'),
role='status',
cls='htmx-indicator spinner-border',
id='spinner'
),cls='d-flex justify-content-center')
Textarea 和 Input(type='text')的区别
textarea里面可直接放东西,还可以直接加htmx请求,但也允许外面套form
Textarea('sdfsdf',placeholder='text',rows=1,id='text',hx_trigger="keyup changed delay:500ms",hx...)
而input里不可以直接放,而是需要用value,如果需要发送请求, 外面套一个form
Input(value='sdfsdf', type='text', placeholder='text')
老版本
req.url_for (旧版本)
为什么用这个?
- mount后,一般hx_get和hx_post都可以用fasthtml里面的get和post=function,link会随着mount自动更改。
- 但href需要手动用req.url_for(func)来追踪link
可以跟一个route的id
@rt("/{data}",name='data_type')
def blabla(): ....
@app.get
def others(req):
req.url_for('data_type', data='sdf')
可以跟function name,app.get和rt都适用,后面跟param,比如data=data
@app.get("/hostie")
def show_host(req): return P('hi')
req.url_for('show_host') # return /hostie
@rt("/{data}")
def this(req,data:str):
return req.url_for('this',data=data)
给route 一个id
@rt("/{data}",name='sdf')
def get(req, data:str):
...
这样做的好处在于,如果mount,route会改变,但是通过id可以追踪到新的route
可以用req.url_for(id_name, variable)来追踪
比如把下面这个放到上面的function里,
req.url_for('sdf',data='aaaa')
就可以得到上面mount后的route
Mount
把subpage的py文件重命名为某一个page.py
然后 import page
app.mount("/page", page.app)
主页可以用/page/引过去
Mount 方法2
from page import app as page_app
app, rt = fast_app(hdrs=links, routes=[Mount("/page", enrich_app, name="page")])
#access through
A(href="/page")
Mount link
get a functions route after mount:
def some_func(req):
return href=f"{req.url_for('download')}"
利用req.url_for可以获取一个function的link
Deployment
登录ssh 上传文件
ssh server_IP,如果登录有问题,让管理员打开permission
会进入当前user目录下,让管理员(sudo user)到common目录 / 下创建一个folder,这样别人也可以看见。
改变folder的权限
sudo chmod 777 /folder
进入replit,files-->download as zip ,下载完成后把zip file名字前加上日期,比如20250809
scp file.zip user@server_IP: /folder
系统安装
查看哪个系统
cat /etc/os-release
我是red hat,用yum
sudo yum groupinstall "Development Tools" -y
sudo yum install -y gcc gcc-c++ make zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel tk-devel libffi-devel xz-devel wget
安装python,可以先查看python --version,如果老,比如3.9,可以安装>3.11
cd /usr/src
sudo wget # https://www.python.org/ftp/python/3.11.9/Python-3.11.9.tgz
sudo tar xzf Python-3.11.9.tgz
cd Python-3.11.9
sudo ./configure --enable-optimizations
sudo make -j$(nproc)
sudo make altinstall
用make altinstall 可以避免覆盖掉之前的python3 (也就是python3.9)
查看安装是否成功
python3.11 --version
设置python3.11为python,还可以保证system正常运行
echo "alias python=python3.11">>~/.bashrc
(alternative, skip)设置python 3.11为default (可能和system firewall 不compatible,如果不行,运行第二个,改回3.9,之后再改回来)
sudo alternatives --install /usr/bin/python3 python3 /usr/local/bin/python3.11 1
sudo alternatives --config python3
选择1,即可,然后python --version 查看,应该为3.11
安装requirements
pip防火墙申请

添加图片注释,不超过 140 字(可选)
之后安装:
pip install python-fasthtml python-katlas seaborn
运行网络
cd /folder
unzip file.zip
uvicorn main:app --host 0.0.0.0 --port 8000
然后登录http://server_ip:8000查看网页是否load正确。注意这里是http不是https。另外不用python main.py,好像是需要另外打开5001的port。
如果还是进入不了,让管理员打开8000的防火墙
sudo firewall-cmd --add-port=8000/tcp --permanent
sudo firewall-cmd --add-service=http --permanent
sudo firewall-cmd --reload
设置internal nginx work(necessary)
sudo setsebool -P httpd_can_network_connect 1
为了让server每次都保持connect
sudo vim /etc/systemd/system/fasthtml.service
然后放入下面的:
[Unit]
Description=FastHTML Application
After=network.target
[Service]
User=icai
WorkingDirectory=/katlas
ExecStart=/usr/bin/python3 -m uvicorn main:app --host 127.0.0.1 --port 8000
Restart=always
[Install]
WantedBy=multi-user.target
如果是 uv 环境,uv env python=3.11&&uv sync 之后,cp uv 到/usr,因为不这样的话/home 下的 uv 执行不了。
sudo cp /home/icai/.local/bin/uv /usr/local/bin/uv
sudo chmod 755 /usr/local/bin/uv
验证
/usr/local/bin/uv --version
之后更改 service 文件,注意我有 16 个 cpu,这里设置的 workers 是 6 个
sudo vim /etc/systemd/system/fasthtml.service
[Unit]
Description=FastHTML Application
After=network.target
[Service]
Type=simple
User=icai
WorkingDirectory=/home/icai/kdock-web
ExecStart=/usr/local/bin/uv run uvicorn main:app --host 127.0.0.1 --port 8000 --workers 6
Restart=always
RestartSec=5
LimitNOFILE=65535
Environment=UV_PROJECT_ENV=1
[Install]
WantedBy=multi-user.target
保存用ctrl+O (欧),回车。之后enable 它
sudo systemctl enable fasthtml
sudo systemctl start fasthtml
如果对文件有更改,需要
sudo systemctl daemon-reload
sudo systemctl reset-failed fasthtml
sudo systemctl start fasthtml
或者
sudo systemctl daemon-reload
sudo systemctl restart fasthtml
查看status
sudo systemctl status fasthtml
查看日志
sudo journalctl -u fasthtml -f
查看网络是否accessible (127.0.0.1和 0.0.0.0 的任何人访问不同,类似 local host,只能本地访问,所以只能用 curl,网页的话不会显示。想要chrome 网页显示,用 0000)
curl http://127... #知乎格式保存报错,用 http://127.0.0.1:8000
接下来需要移除掉port number,foward 8000到80,安装nginx
sudo yum install nginx -y
sudo systemctl enable nginx
sudo systemctl start nginx
sudo vim /etc/nginx/conf.d/fasthtml.conf
server {
listen 80;
server_name 10.68.4.119;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
或者如果需要websocket,用下面的:
server {
listen 80; # listen on port 80 for outside HTTP
server_name 10.XXXXX(server_IP); # server's IP or DNS name
location / {
proxy_pass http://127.0.0.1:8000; # local 8000
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
ctrl+O,enter。
sudo nginx -t
sudo systemctl restart nginx
查看网络是否accessible
curl http://10...... # http://10.68.4.119
查看browser能否打开,如果browser打不开,设置fire wall
HTTPS (production server only)
用production server 生成CSR
openssl req -new -newkey rsa:2048 -nodes -keyout Katlas.key -out Katlas.csr
填入信息
Country Name (2 letter code) [XX]: US
State or Province Name (full name) [Default]: Texas
Locality Name (e.g., city) [Default]: Houston
Organization Name (e.g., company) [Default]: MD Anderson
Organizational Unit Name (e.g., department) [Default]: IT
Common Name (e.g., your domain) [Default]: Katlas.mdanderson.org
Email Address [Default]: your-email@example.com
会有一个.csr 和 .key文件。
打开本机的 terminal(不是 ssh上),scp这两个文件到local
scp username@your-server-ip:/path/to/Katlas.csr ~/Downloads
提交csr文件到IT,获得一些certificates,下载Certificate (w/ issuer after)
check .key秘钥和certificate是否match
openssl x509 -noout -modulus -in org.pem| openssl md5
openssl rsa -noout -modulus -in Katlas.key | openssl md5
之后scp certificate 到 user@ip_server,这里是
scp katlas_mdanderson_org.pem icai@10.68.3.119:katlas_cert.pem
创建装它们的文件夹
sudo mkdir -p /etc/nginx/ssl
sudo mv katlas_cert.pem /etc/nginx/ssl/
sudo mv Katlas.key /etc/nginx/ssl/
如果后面报错,也有可能文件权限没给全
ls -l /etc/nginx/ssl/Katlas.key
sudo chown root:nginx /etc/nginx/ssl/Katlas.key
sudo chmod 640 /etc/nginx/ssl/Katlas.key
然后sudo nano /etc/nginx/conf.d/fasthtml.conf
server {
listen 80;
server_name katlas.mdanderson.org;
return 301 https://$host$request_uri; # Redirect HTTP to HTTPS
}
server {
listen 443 ssl;
server_name katlas.mdanderson.org;
ssl_certificate /etc/nginx/ssl/katlas_cert.pem;
ssl_certificate_key /etc/nginx/ssl/Katlas.key;
# ssl_trusted_certificate /etc/nginx/ssl/katlas_chain.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
重启
sudo nginx -t
sudo systemctl restart nginx
设置firewall
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
最后查看 curl https://address
每年certificate更新,都需要重新设置。
日志查询
# 1. 实时看 nginx 错误
sudo tail -f /var/log/nginx/error.log
# 2. 实时看访问
sudo tail -f /var/log/nginx/access.log
# 3. 看 FastHTML 后端
sudo journalctl -u fasthtml -f
# 4. TLS 验证
curl -Iv https://webaddress.org
更新
Log score of enrichment
Frequency matrix
sudo vim /etc/systemd/system/fasthtml.service
/home/icai/kdock-web/.venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 8000
/home/icai/kdock-web/.venv/bin/python -m uvicorn main:app --host 127.0.0.1 --port 8000
/home/icai/.local/bin/uv run uvicorn main:app --host 127.0.0.1 --port 8000
ExecStart=/usr/bin/python3 -m uvicorn main:app --host 127.0.0.1 --port 8000
ssh 10.68.4.119
sudo systemctl daemon-reload
sudo systemctl reset-failed fasthtml
sudo systemctl start fasthtml
sudo systemctl status fasthtml
sudo journalctl -u fasthtml
sudo journalctl -u fasthtml -f
sudo journalctl -u fasthtml --since "15 minutes ago"
scp icai@rosrlkatlas1:Katlas.csr ~/Downloads
openssl x509 -noout -modulus -in katlas_mdanderson_org.pem| openssl md5
sudo vim /usr/local/bin/fasthtml_alert.sh
#!/bin/bash
SERVICE="fasthtml"
HOST=$(hostname)
STATUS=$(systemctl status $SERVICE | head -n 20)
LOG=$(journalctl -u $SERVICE -n 50 --no-pager)
mail -s "[ALERT] FastHTML crashed on $HOST" icai@mdanderson.org <<EOF
FastHTML service has crashed or restarted.
=== STATUS ===
$STATUS
=== LAST LOGS ===
$LOG
EOF
sudo chmod +x /usr/local/bin/fasthtml_alert.sh
sudo vim /etc/systemd/system/fasthtml.service
[Service]里面加
OnFailure=fasthtml-alert@%n.service
sudo vim /etc/systemd/system/fasthtml-alert@.service
[Unit]
Description=FastHTML Alert for %i
[Service]
Type=oneshot
ExecStart=/usr/local/bin/fasthtml_alert.sh
sudo systemctl start fasthtml-alert@fasthtml.service
测试
sudo kill -9 $(systemctl show -p MainPID --value fasthtml)