pydantic-settings update
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
---
|
||||
name: nicegui
|
||||
description: 'Reference hub for NiceGUI and FastAPI application structure, ASGI and Uvicorn startup, UI composition, styling, bindable state, interactions, troubleshooting, testing, and source documentation. Use when planning, implementing, reviewing, deploying, or debugging NiceGUI applications; load only the references relevant to the task.'
|
||||
description: 'Reference hub for NiceGUI and FastAPI application structure, typed configuration, ASGI and Uvicorn startup, UI composition, styling, bindable state, interactions, troubleshooting, testing, and source documentation. Use when planning, implementing, reviewing, deploying, or debugging NiceGUI applications; load only the references relevant to the task.'
|
||||
x-personal-mcp:
|
||||
id: nicegui
|
||||
version: 2.3.0
|
||||
version: 2.4.0
|
||||
tags:
|
||||
- nicegui
|
||||
- fastapi
|
||||
- asgi
|
||||
- uvicorn
|
||||
- pydantic-settings
|
||||
- configuration
|
||||
- deployment
|
||||
- ui
|
||||
- architecture
|
||||
@@ -61,6 +63,7 @@ Load [FastAPI and Uvicorn startup](./references/fastapi-uvicorn-startup.md) for:
|
||||
- choosing between `ui.run()` and `ui.run_with()`
|
||||
- understanding the parent FastAPI app and NiceGUI's internal app
|
||||
- composing ASGI lifespan and mounted routes
|
||||
- loading one typed settings snapshot for server and application configuration
|
||||
- serving an app instance or factory with Uvicorn
|
||||
- exposing programmatic startup through `[project.scripts]`
|
||||
- reload, worker, and process-local state constraints
|
||||
|
||||
@@ -10,8 +10,10 @@ Use this reference when FastAPI owns the application and NiceGUI is one part of
|
||||
```mermaid
|
||||
flowchart TD
|
||||
E["Project script: my-app"] --> M["main()"]
|
||||
M --> S["get_settings()"]
|
||||
M --> U["uvicorn.run()"]
|
||||
U --> F["create_app()"]
|
||||
F --> S
|
||||
F --> P["Parent FastAPI app"]
|
||||
P --> A["API routes and middleware"]
|
||||
P -->|"mount_path=/gui"| N["NiceGUI App"]
|
||||
@@ -23,6 +25,7 @@ The objects have separate responsibilities:
|
||||
| Object | Owner | Responsibility |
|
||||
| --- | --- | --- |
|
||||
| Parent `FastAPI` instance | Application code | Root ASGI app, API routes, middleware, lifespan, and mounted applications |
|
||||
| `Settings` instance | Application code | Immutable, process-local configuration snapshot shared by startup and composition |
|
||||
| `nicegui.app` | NiceGUI | A process-local [`App`](https://github.com/zauberzeug/nicegui/blob/main/nicegui/app/app.py) instance that subclasses `FastAPI` |
|
||||
| `ui.run_with(parent_app)` | NiceGUI integration | Configures NiceGUI, mounts `nicegui.app` into `parent_app`, and integrates lifecycle handling |
|
||||
| Uvicorn | Server process | Imports or receives the root ASGI app, opens sockets, drives lifespan, and serves requests |
|
||||
@@ -70,9 +73,58 @@ Keep application composition importable and server startup explicit:
|
||||
└─ src/
|
||||
└─ my_app/
|
||||
├─ __init__.py
|
||||
├─ config.py
|
||||
└─ main.py
|
||||
```
|
||||
|
||||
```python title="src/my_app/config.py"
|
||||
from functools import cache
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, SecretStr
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class ServerSettings(BaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
host: str = '0.0.0.0'
|
||||
port: int = 8000
|
||||
log_level: Literal['critical', 'error', 'warning', 'info', 'debug', 'trace'] = (
|
||||
'info'
|
||||
)
|
||||
reload: bool = False
|
||||
|
||||
|
||||
class GuiSettings(BaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
mount_path: str = '/gui'
|
||||
storage_secret: SecretStr | None = None
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix='MY_APP_',
|
||||
env_nested_delimiter='__',
|
||||
env_file='.env',
|
||||
env_file_encoding='utf-8',
|
||||
frozen=True,
|
||||
)
|
||||
|
||||
server: ServerSettings = Field(default_factory=ServerSettings)
|
||||
gui: GuiSettings = Field(default_factory=GuiSettings)
|
||||
|
||||
|
||||
@cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
```
|
||||
|
||||
`ServerSettings` and `GuiSettings` inherit from `BaseModel` because they share one application owner, source policy, and process lifecycle. The root `BaseSettings` reads the sources once and validates one atomic snapshot. Environment variables use names such as `MY_APP_SERVER__PORT`, `MY_APP_SERVER__RELOAD`, `MY_APP_GUI__MOUNT_PATH`, and `MY_APP_GUI__STORAGE_SECRET`.
|
||||
|
||||
The argument-free [`functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache) provider is appropriate here because both the project entry point and Uvicorn's zero-argument factory need process-lifetime access. Each reload or worker process gets its own settings instance. Do not add override arguments to `get_settings()`; inject a `Settings` instance directly into `create_app()` in tests or alternate composition roots. See the [Pydantic settings implementation guide](../../pydantic-settings/SKILL.md) for source precedence, independent settings boundaries, cache clearing, and runtime reload guidance.
|
||||
|
||||
```python title="src/my_app/main.py"
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
@@ -81,6 +133,8 @@ import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from nicegui import ui
|
||||
|
||||
from my_app.config import Settings, get_settings
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
||||
@@ -97,8 +151,10 @@ def register_pages() -> None:
|
||||
ui.label('Dashboard')
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
def create_app(settings: Settings | None = None) -> FastAPI:
|
||||
settings = settings or get_settings()
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
app.state.settings = settings
|
||||
|
||||
@app.get('/healthz')
|
||||
def health() -> dict[str, str]:
|
||||
@@ -107,19 +163,25 @@ def create_app() -> FastAPI:
|
||||
register_pages()
|
||||
ui.run_with(
|
||||
app,
|
||||
mount_path='/gui',
|
||||
storage_secret='replace-with-a-secret-from-settings',
|
||||
mount_path=settings.gui.mount_path,
|
||||
storage_secret=(
|
||||
settings.gui.storage_secret.get_secret_value()
|
||||
if settings.gui.storage_secret is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
return app
|
||||
|
||||
|
||||
def main() -> None:
|
||||
settings = get_settings()
|
||||
uvicorn.run(
|
||||
'my_app.main:create_app',
|
||||
factory=True,
|
||||
host='0.0.0.0',
|
||||
port=8000,
|
||||
log_level='info',
|
||||
host=settings.server.host,
|
||||
port=settings.server.port,
|
||||
log_level=settings.server.log_level,
|
||||
reload=settings.server.reload,
|
||||
)
|
||||
|
||||
|
||||
@@ -127,7 +189,7 @@ if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
The `storage_secret` is optional unless the application uses `ui.storage.user` or `ui.storage.browser`. Load the real value from typed settings or an environment-backed secret rather than committing it.
|
||||
The `storage_secret` is optional unless the application uses `ui.storage.user` or `ui.storage.browser`. `SecretStr` prevents accidental plaintext display in logs and model representations, while `get_secret_value()` unwraps it only at the NiceGUI integration boundary. Supply production secrets through environment variables or a supported settings secret source rather than committing them.
|
||||
|
||||
The example passes an [import string and `factory=True`](https://www.uvicorn.org/settings/#application) to Uvicorn. Uvicorn imports `my_app.main`, calls the zero-argument `create_app` factory, and serves the returned parent FastAPI app. Import strings are also required when Uvicorn creates reload or worker subprocesses; passing `create_app()` directly only supports the simple single-process case.
|
||||
|
||||
@@ -161,6 +223,7 @@ requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"fastapi",
|
||||
"nicegui",
|
||||
"pydantic-settings",
|
||||
"uvicorn[standard]",
|
||||
]
|
||||
|
||||
@@ -183,24 +246,18 @@ uv run my-app
|
||||
|
||||
The uv [project entry-point documentation](https://docs.astral.sh/uv/concepts/projects/config/#entry-points) requires a build system so uv installs the project and generates its command. The `[project.scripts]` target follows the [PyPA entry-point specification](https://packaging.python.org/en/latest/specifications/entry-points/#use-for-scripts): its generated wrapper imports `main`, calls it without arguments, and uses the return value as the process exit status. Returning `None` means successful completion.
|
||||
|
||||
Use application settings or environment variables for host, port, and logging configuration. Add an explicit CLI parser only when the project command needs user-supplied arguments; the entry-point callable itself still receives no arguments.
|
||||
The settings model now owns host, port, logging, reload, mount path, and storage-secret configuration. Add an explicit CLI settings source or another CLI parser only when the project command needs user-supplied arguments; the entry-point callable itself still receives no arguments.
|
||||
|
||||
## Development Reload
|
||||
|
||||
Because `main()` supplies an import string, it can enable Uvicorn reload for local development:
|
||||
|
||||
```python
|
||||
def main() -> None:
|
||||
uvicorn.run(
|
||||
'my_app.main:create_app',
|
||||
factory=True,
|
||||
host='127.0.0.1',
|
||||
port=8000,
|
||||
reload=True,
|
||||
)
|
||||
```dotenv title=".env"
|
||||
MY_APP_SERVER__HOST=127.0.0.1
|
||||
MY_APP_SERVER__RELOAD=true
|
||||
```
|
||||
|
||||
Keep reload disabled in production. Uvicorn documents [`reload` and `workers` as mutually exclusive](https://www.uvicorn.org/settings/#production), and each worker would have independent NiceGUI state, lifespan resources, and WebSocket connections. Use one worker by default unless the application has explicitly validated session affinity and externalized every stateful dependency needed across processes.
|
||||
The cached settings object is a process-start snapshot. Changing an environment variable or dotenv file does not mutate a running instance; restart the process, or let the development reloader create a new worker when a watched file changes. Keep reload disabled in production. Uvicorn documents [`reload` and `workers` as mutually exclusive](https://www.uvicorn.org/settings/#production), and each worker would have independent settings, NiceGUI state, lifespan resources, and WebSocket connections. Use one worker by default unless the application has explicitly validated session affinity and externalized every stateful dependency needed across processes.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
@@ -212,6 +269,8 @@ Keep reload disabled in production. Uvicorn documents [`reload` and `workers` as
|
||||
| Calling `uvicorn.run()` at module import time | Importing the module starts a blocking server and breaks subprocess startup | Call it from `main()` |
|
||||
| Top-level `ui.label(...)` with `ui.run_with()` | Script-mode elements are discarded by this integration | Register UI in `@ui.page` functions or a root callable |
|
||||
| Multiple workers by default | Process-local UI state and WebSockets are not automatically shared | Start with one worker and validate a distributed design explicitly |
|
||||
| Reconstructing `Settings()` throughout the app | Re-reads sources and obscures the active configuration lifecycle | Inject the startup snapshot or use the argument-free provider at framework boundaries |
|
||||
| Adding kwargs to cached `get_settings()` | Retains one hidden process-lifetime instance per argument combination | Construct explicit `Settings(...)` overrides and inject them |
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -220,11 +279,16 @@ Use `TestClient` as a context manager so the parent ASGI lifespan runs:
|
||||
```python
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from my_app.config import GuiSettings, Settings
|
||||
from my_app.main import create_app
|
||||
|
||||
|
||||
def test_application_routes() -> None:
|
||||
with TestClient(create_app()) as client:
|
||||
settings = Settings(
|
||||
gui=GuiSettings(storage_secret='test-storage-secret'),
|
||||
)
|
||||
|
||||
with TestClient(create_app(settings)) as client:
|
||||
assert client.get('/healthz').json() == {'status': 'ok'}
|
||||
assert client.get('/gui/').status_code == 200
|
||||
```
|
||||
@@ -245,5 +309,7 @@ Also verify:
|
||||
- [ASGI lifespan protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html)
|
||||
- [Uvicorn settings](https://www.uvicorn.org/settings/)
|
||||
- [Uvicorn programmatic startup](https://www.uvicorn.org/#running-programmatically)
|
||||
- [Pydantic settings management](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/)
|
||||
- [`functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache)
|
||||
- [uv project entry points](https://docs.astral.sh/uv/concepts/projects/config/#entry-points)
|
||||
- [PyPA entry points specification](https://packaging.python.org/en/latest/specifications/entry-points/)
|
||||
Reference in New Issue
Block a user