[web] FastHTML

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 可以把fast

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&lt;br&gt;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里面的路径,就用绝对路径。

存储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()

直接用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也最好别重名。

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(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")

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)