Building UI with Pydantic/FastUI Library
前言
這篇筆記的內容及大綱,主要是針對 Pydantic/FastUI 這個由 Pydantic 延伸的 Library Project 所做的介紹,而相關的範例說明則取自於 FastUI & SQLModel - Database integration in a FastUI Application 這部 Youtube 視頻教學。
FastUI 是什麼?
FastUI 是一個基於 Python 的 Web 框架,旨在提供一種快速、簡單且高效的方式來創建 Web 應用程序的用戶介面。FastUI 允許開發者使用聲明式的 Python 代碼來定義 Web 應用程序的用戶介面,這使得開發者可以更專注於業務邏輯,而不用處理繁複的 JavaScript 或 npm 組態。
SQLModel 是什麼?
SQLModel 旨在簡化與 FastAPI 應用程式中的 SQL 資料庫的互動,它是由 FastAPI 作者所創建的。
它結合了 SQLAlchemy 和 Pydantic,並試圖盡可能地簡化您編寫的代碼,使您能夠將代碼重複減少到最低限度,同時獲得最佳的開發人員體驗。
事實上,SQLModel 是 Pydantic 和 SQLAlchemy 之上的一層薄薄的中介層,由作者經過精心設計以與兩者相容。這使得 SQLModel 既有 SQLAlchemy 簡化資料庫連接與 CRUD 操作的指令的優點,又具備 Pydantic 強型別驗證的強項,使得使用者能夠快速建立 for FastAPI 使用的 ViewModel,讓 API 程式碼變得簡潔美麗。
FastUI 的架構
FastUI 的架構主要由以下幾個部分組成:
-
fastui PyPI 包:提供了用於 UI 組件的 Pydantic 模型和一些實用程序。這些模型可以與任何 Python Web 框架一起使用,尤其是與 FastAPI 結合效果很好。
-
@pydantic/fastui npm 包:這是一個 React TypeScript 包,允許開發者在實現自己的組件時重用 FastUI 的機制和類型。
-
@pydantic/fastui-bootstrap npm 包:使用 Bootstrap 實現和定製所有 FastUI 組件。
-
@pydantic/fastui-prebuilt npm 包:這個包在 CDN 上提供了 FastUI React 應用程序的預構建版本,開發者可以在不安裝任何 npm 包或構建內容的情況下使用它。
這些組件共同協作,使得開發者可以通過簡潔的 API 來快速建立 Web 應用的用戶介面。
FastUI 的優缺點
-
優點:
-
快速開發:FastUI 提供了簡潔的 API 和聲明式代碼,使得開發速度大大提高。
-
易於擴展:由於使用了 Pydantic 和 React,開發者可以輕鬆地擴展和重用組件。
-
後端與前端分離:真正實現了後端定義業務邏輯,前端自由實現介面的分離。
-
簡化組態:無需處理 JavaScript 和 npm 工具的複雜性,Python 開發者可以在熟悉的環境中工作。
-
-
缺點:
-
學習曲線:對於不熟悉 React 或 TypeScript 的開發者來說,可能需要一些時間去適應。
-
功能尚未完善:作為一個相對較新的框架,FastUI 還在活躍開發中,可能會存在一些未解決的問題或功能缺失。
-
如何使用 FastUI 來開發 Web 應用程序
1 首先是要安裝 fastui 和 fastapi 這兩個 Python 包。
pip install fastui fastapi
2 然後依據 Web App 需求,使用 SQLModel 設計 App 需要的資料庫及資料表。
# db.py
from datetime import date
from sqlmodel import create_engine, SQLModel, Field
sqlite_url = "sqlite:///db.sqlite3"
class User(SQLModel, table=True):
id: int = Field(primary_key=True)
name: str
dob: date
# create the engine
engine = create_engine(sqlite_url, echo=True)
SQLModel.metadata.create_all(engine)
-
SQLModel 是赫赫有名的 FastAPI 作者的姊妹作,是一個用於定義資料模型的元件庫,它很好的融合了 SQLArchemy 的 Model 功能以及 Pydantic 的驗證功能,提供了一種簡單且強大的方式來定義資料庫表格。詳細可參考 SQLModel 官方文件
-
如上範例,我們引入了 SQLModel 元件庫,定義了一個 User 的資料表,包含 id, name, dob 三個欄位,並且使用 SQLModel 的 create_engine 既輕鬆又簡潔的建立了一個 SQLite 的資料庫引擎。
3 使用 FastAPI 框架來定義 Web API 及其他路由,並使用 FastUI 及其 Components 來建立前端元件,全程看不到任何的 Javascript 或其他前端技術的 Code。
# main.py
# demo code is from here: https://github.com/bugbytes-io/fastui-sqlmodel-demo/blob/master/final/requirements.txt
from contextlib import asynccontextmanager
from datetime import date
from typing import Annotated
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
from fastui import FastUI, AnyComponent, prebuilt_html, components as c
from fastui.components.display import DisplayMode, DisplayLookup
from fastui.events import GoToEvent, BackEvent
from fastui.forms import FormResponse, fastui_form
from pydantic import BaseModel, Field
from db import User, engine
from sqlmodel import Session, select
@asynccontextmanager
async def lifespan(app: FastAPI):
# define some users
users = [
User(id=1, name='John', dob=date(1990, 1, 1)),
User(id=2, name='Jack', dob=date(1991, 1, 1)),
User(id=3, name='Jill', dob=date(1992, 1, 1)),
User(id=4, name='Jane', dob=date(1993, 1, 1)),
]
with Session(engine) as session:
for user in users:
db_user = session.get(User, user.id)
if db_user is not None:
continue
session.add(user)
session.commit()
yield
app = FastAPI(lifespan=lifespan)
class UserForm(BaseModel):
name: str
dob: date
class DeleteUserForm(BaseModel):
confirm: bool
@app.get('/api/user/add/', response_model=FastUI, response_model_exclude_none=True)
def add_user():
return [
c.Page(
components=[
c.Heading(text='Add User', level=2),
c.Paragraph(text='Add a user to the system'),
c.ModelForm[UserForm](
submit_url='/api/user/add/'
),
]
)
]
@app.post('/api/user/add/')
async def add_user(form: Annotated[UserForm, fastui_form(UserForm)]) -> FormResponse:
with Session(engine) as session:
user = User(**form.model_dump())
session.add(user)
session.commit()
return FormResponse(event=GoToEvent(url='/'))
@app.get("/api/", response_model=FastUI, response_model_exclude_none=True)
def users_table() -> list[AnyComponent]:
with Session(engine) as session:
users = session.exec(select(User)).all()
return [
c.Page( # Page provides a basic container for components
components=[
c.Heading(text='Users', level=2), # renders `<h2>Users</h2>`
c.Table[User]( # c.Table is a generic component parameterized with the model used for rows
data=users,
# define two columns for the table
columns=[
# the first is the users, name rendered as a link to their profile
DisplayLookup(field='name', on_click=GoToEvent(url='/user/{id}/')),
# the second is the date of birth, rendered as a date
DisplayLookup(field='dob', mode=DisplayMode.date),
],
),
c.Div(components=[
c.Link(
components=[c.Button(text='Add User')],
on_click=GoToEvent(url='/user/add/'),
),
])
]
),
]
@app.get("/api/user/{user_id}/", response_model=FastUI, response_model_exclude_none=True)
def user_profile(user_id: int) -> list[AnyComponent]:
"""
User profile page, the frontend will fetch this when the user visits `/user/{id}/`.
"""
with Session(engine) as session:
user = session.get(User, user_id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return [
c.Page(
components=[
c.Heading(text=user.name, level=2),
c.Link(components=[c.Text(text='Back')], on_click=BackEvent()),
c.Details(data=user),
c.Div(components=[
c.Heading(text="Delete User?", level=4),
c.ModelForm[DeleteUserForm](
submit_url=f'/api/user/{user_id}/delete/',
class_name="text-left"
)
], class_name="card p-4 col-4")
]
),
]
@app.post('/api/user/{user_id}/delete/')
async def delete_user(
user_id: int,
form: Annotated[DeleteUserForm, fastui_form(DeleteUserForm)]
) -> FormResponse:
# delete the users
with Session(engine) as session:
user = session.get(User, user_id)
if user is not None:
session.delete(user)
session.commit()
return FormResponse(event=GoToEvent(url='/'))
@app.get('/{path:path}')
async def html_landing() -> HTMLResponse:
"""Simple HTML page which serves the React app, comes last as it matches all paths."""
return HTMLResponse(prebuilt_html(title='FastUI Demo'))
- 如上範例,我們定義了一個 FastAPI 的 Web API,並使用 FastUI 的 Components 來建立前端元件,包括了一個用戶列表頁面、用戶詳情頁面、用戶新增頁面、用戶刪除頁面等。
4 最後,安裝 uvicorn 這個常見於 FastAPI 程式中高性能的 ASGI 伺服器套件,使用 uvicorn 來啟動 FastAPI 應用程序,並在瀏覽器中訪問相應的 URL,即可看到我們定義的 Web 應用程序的用戶介面。
pip install uvicorn
uvicorn main:app --reload
# 或者為 uvicorn main:app --host 127.0.0.1 --port 8000 --reload
if __name__ == '__main__':
import uvicorn
uvicorn.run(app='main:app', host='127.0.0.1', port=8080, reload=True, debug=True, log_level='info')
結論
FastUI 是一個基於 Python 的 Web 框架,旨在提供一種快速、簡單且高效的方式來創建 Web 應用程序的用戶介面。它使用了 Pydantic 和 React 這兩個強大的庫,使得開發者可以通過簡潔的 API 來快速建立 Web 應用的用戶介面。雖然 FastUI 還在活躍開發中,但它已經展示了很大的潛力,未來有望成為 Python 開發者的首選 Web 框架之一。
FastUI 相關連結收藏
-
FastUI 官方文件: FastUI 官方文件
-
FastUI GitHub: FastUI GitHub
-
FastUI Full Components Demo: FastUI Full Demo,這些 Demo 的程式碼都來自於這裡: FastUI Demo Code