Python FastUI 元件庫介紹 -- Building UI with Pydantic/FastUI Library

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 相關連結收藏

參考資料

2個讚