Compare commits
3
Commits
c9b6e137f2
...
a18c8456d3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a18c8456d3 | ||
|
|
34e6d693ab | ||
|
|
efba051cb5 |
@@ -1,12 +1,17 @@
|
|||||||
---
|
---
|
||||||
name: nicegui
|
name: nicegui
|
||||||
description: 'Reference hub for NiceGUI and FastAPI application structure, UI composition, styling, bindable state, interactions, troubleshooting, testing, and source documentation. Use when planning, implementing, reviewing, 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:
|
x-personal-mcp:
|
||||||
id: nicegui
|
id: nicegui
|
||||||
version: 2.0.0
|
version: 2.4.0
|
||||||
tags:
|
tags:
|
||||||
- nicegui
|
- nicegui
|
||||||
- fastapi
|
- fastapi
|
||||||
|
- asgi
|
||||||
|
- uvicorn
|
||||||
|
- pydantic-settings
|
||||||
|
- configuration
|
||||||
|
- deployment
|
||||||
- ui
|
- ui
|
||||||
- architecture
|
- architecture
|
||||||
- scaffolding
|
- scaffolding
|
||||||
@@ -51,14 +56,31 @@ Load [application architecture](./references/architecture.md) for:
|
|||||||
- optional persistence, LangGraph, or mounted documentation
|
- optional persistence, LangGraph, or mounted documentation
|
||||||
- async responsiveness and baseline tests
|
- async responsiveness and baseline tests
|
||||||
|
|
||||||
|
### FastAPI And Uvicorn Startup
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
### Components And Styling
|
### Components And Styling
|
||||||
|
|
||||||
Load [architecture and styling](./references/architecture-and-styling.md) for:
|
Load [architecture and styling](./references/architecture-and-styling.md) for:
|
||||||
|
|
||||||
- page, component, and service boundaries
|
- page, component, and service boundaries
|
||||||
- component extraction decisions
|
- component extraction decisions
|
||||||
- Tailwind and Quasar styling order
|
- Quasar props, Tailwind utilities, and custom CSS boundaries
|
||||||
- responsive layout and static asset conventions
|
- responsive layout and static asset conventions
|
||||||
|
- uniformly scaling dialogs on mobile
|
||||||
|
- preserving Quasar field proportions
|
||||||
|
- keeping detached `QSelect` menus anchored
|
||||||
|
- sizing scrollable dialog cards under CSS `zoom`
|
||||||
|
- validating zoomed controls with Playwright or a browser
|
||||||
|
|
||||||
### Bindable State
|
### Bindable State
|
||||||
|
|
||||||
@@ -99,7 +121,8 @@ Load [source documentation](./references/source-documentation.md) when:
|
|||||||
### New Application Or Architecture Review
|
### New Application Or Architecture Review
|
||||||
|
|
||||||
1. Load [application architecture](./references/architecture.md).
|
1. Load [application architecture](./references/architecture.md).
|
||||||
2. Add [architecture and styling](./references/architecture-and-styling.md) only when page and component design is in scope.
|
2. Add [FastAPI and Uvicorn startup](./references/fastapi-uvicorn-startup.md) when FastAPI owns the application or startup must be exposed as a project command.
|
||||||
|
3. Add [architecture and styling](./references/architecture-and-styling.md) only when page and component design is in scope.
|
||||||
|
|
||||||
### Page Or Component Work
|
### Page Or Component Work
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,54 @@
|
|||||||
# Architecture and Styling Reference
|
# NiceGUI Page Layout And Styling
|
||||||
|
|
||||||
## Project Boundaries
|
Use this reference to structure NiceGUI pages, choose component boundaries, apply responsive layout, and introduce custom CSS without fighting Quasar's internal geometry.
|
||||||
|
|
||||||
Use this dependency direction:
|
## Ownership And Dependency Boundaries
|
||||||
|
|
||||||
|
Keep dependencies flowing in one direction:
|
||||||
|
|
||||||
- pages import components and services
|
- pages import components and services
|
||||||
- components contain presentation logic only
|
- components contain presentation logic only
|
||||||
- services contain business logic and do not import UI
|
- services contain business logic and do not import UI
|
||||||
- static assets are mounted and loaded once at bootstrap
|
- bootstrap code mounts static assets and loads shared CSS once
|
||||||
|
|
||||||
Suggested module split:
|
Suggested module split:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/app/
|
src/my_app/
|
||||||
ui/pages/
|
ui/
|
||||||
ui/components/
|
pages/
|
||||||
ui/static/
|
components/
|
||||||
|
static/
|
||||||
services/
|
services/
|
||||||
api/
|
api/
|
||||||
bootstrap.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Component Extraction Rules
|
Page modules should compose a route from reusable presentation and service calls. They should not own domain rules, persistence, or long-running synchronous work.
|
||||||
|
|
||||||
Extract to ui/components when a pattern appears in two or more pages.
|
## Page Composition
|
||||||
|
|
||||||
Keep in-page if the layout is specific to a single route.
|
Build the outer layout before styling individual controls:
|
||||||
|
|
||||||
|
1. Define the page shell and width constraints.
|
||||||
|
2. Establish responsive rows, columns, gaps, and wrapping.
|
||||||
|
3. Add semantic sections and repeated components.
|
||||||
|
4. Configure Quasar component appearance with props.
|
||||||
|
5. Add custom CSS only for behavior that props and utilities cannot express safely.
|
||||||
|
|
||||||
|
```python
|
||||||
|
with ui.column().classes("w-full max-w-6xl mx-auto gap-6 px-4"):
|
||||||
|
page_header(title="Inventory")
|
||||||
|
|
||||||
|
with ui.row().classes("w-full gap-4 flex-wrap lg:flex-nowrap items-start"):
|
||||||
|
filters_panel().classes("w-full lg:w-72 shrink-0")
|
||||||
|
item_grid().classes("w-full flex-1 min-w-0")
|
||||||
|
```
|
||||||
|
|
||||||
|
Use stable width, minimum-width, and flex constraints so labels, icons, validation messages, and loaded content do not shift the surrounding layout.
|
||||||
|
|
||||||
|
## Component Extraction
|
||||||
|
|
||||||
|
Extract a presentation pattern to `ui/components/` when it appears on two or more pages or when it owns a meaningful interaction boundary. Keep one-off route layout in the page module.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def card_section(title: str, content: str) -> ui.card:
|
def card_section(title: str, content: str) -> ui.card:
|
||||||
@@ -35,43 +58,250 @@ def card_section(title: str, content: str) -> ui.card:
|
|||||||
return card
|
return card
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tailwind-First Layout Pattern
|
Reusable components should accept data and event callbacks rather than import page state or business services implicitly.
|
||||||
|
|
||||||
Use Tailwind utility classes for structure and spacing.
|
|
||||||
Use breakpoint classes for responsive behavior.
|
|
||||||
Use .style() only for values that must be computed dynamically.
|
|
||||||
|
|
||||||
```python
|
|
||||||
with ui.column().classes("w-full"):
|
|
||||||
with ui.row().classes("w-full gap-4 flex-wrap sm:flex-nowrap"):
|
|
||||||
ui.card().classes("flex-1 min-w-64")
|
|
||||||
ui.card().classes("flex-1 min-w-64")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Styling Decision Order
|
## Styling Decision Order
|
||||||
|
|
||||||
1. Tailwind utility classes
|
NiceGUI wraps Quasar components. Choose the styling mechanism according to what it owns:
|
||||||
2. Quasar props
|
|
||||||
3. Reusable styled component functions
|
1. Use Quasar props for component appearance, density, labels, and popup behavior.
|
||||||
4. Minimal custom CSS loaded once at bootstrap (only when needed)
|
2. Use NiceGUI `.classes()` and Tailwind utilities for width, spacing, alignment, and responsive layout.
|
||||||
|
3. Use reusable component functions for repeated visual patterns.
|
||||||
|
4. Use `.style()` for genuinely dynamic inline values.
|
||||||
|
5. Use minimal shared CSS only when props and utilities are insufficient.
|
||||||
|
|
||||||
|
Common Quasar props include:
|
||||||
|
|
||||||
|
- `outlined`
|
||||||
|
- `dense`
|
||||||
|
- `stack-label`
|
||||||
|
- `popup-content-class`
|
||||||
|
- `input-class`
|
||||||
|
- `input-style`
|
||||||
|
|
||||||
|
Avoid overriding internal selectors such as:
|
||||||
|
|
||||||
|
- `.q-field__label`
|
||||||
|
- `.q-field__native`
|
||||||
|
- `.q-field__control`
|
||||||
|
- `.q-field__input`
|
||||||
|
|
||||||
|
Quasar coordinates field height, padding, labels, values, icons, and floating-label transforms. Changing only one internal part tends to cause clipping or overlap.
|
||||||
|
|
||||||
|
## Responsive Layout
|
||||||
|
|
||||||
|
Use Tailwind breakpoint classes for ordinary page adaptation:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from fastapi.staticfiles import StaticFiles
|
with ui.row().classes("w-full gap-4 flex-wrap sm:flex-nowrap"):
|
||||||
|
ui.card().classes("w-full sm:flex-1 sm:min-w-64")
|
||||||
app.mount("/static", StaticFiles(directory="src/app/static"), name="static")
|
ui.card().classes("w-full sm:flex-1 sm:min-w-64")
|
||||||
ui.add_css(open("src/app/static/css/base.css").read())
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Static Asset Rules
|
- Start with a usable mobile layout, then add larger breakpoint behavior.
|
||||||
|
- Allow dense toolbars to wrap or collapse intentionally.
|
||||||
|
- Use `min-w-0` on flexible content that must shrink inside a row.
|
||||||
|
- Keep controls and primary actions visible without horizontal scrolling.
|
||||||
|
- Test the longest realistic labels, values, errors, and menu options.
|
||||||
|
|
||||||
- Keep custom CSS small and tokenized with variables.
|
## Static Assets And Shared CSS
|
||||||
- Avoid per-page CSS injection.
|
|
||||||
- Verify static mount paths and reverse proxy rewrites.
|
|
||||||
|
|
||||||
## Links
|
- Mount static assets from the composition layer.
|
||||||
|
- Load shared CSS once rather than injecting it from individual pages.
|
||||||
|
- Keep custom CSS tokenized with variables and scoped to application classes.
|
||||||
|
- Avoid broad rules against Quasar internals.
|
||||||
|
- Verify mount paths, reverse-proxy rewrites, and cache behavior.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
|
STATIC_DIR = Path(__file__).parent / "ui" / "static"
|
||||||
|
|
||||||
|
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||||
|
ui.add_css((STATIC_DIR / "css" / "base.css").read_text(encoding="utf-8"))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Responsive Dialog Pattern
|
||||||
|
|
||||||
|
Use whole-card scaling when a form dialog must become uniformly larger on mobile while preserving Quasar's internal proportions. Keep detached select menus unscaled and make the card itself scrollable.
|
||||||
|
|
||||||
|
### Use Normal Field Density
|
||||||
|
|
||||||
|
Normal Quasar fields are approximately `56px` high, while dense fields are approximately `40px` high. Remove `dense` when larger controls are needed.
|
||||||
|
|
||||||
|
```python
|
||||||
|
ui.input("Name").props("outlined")
|
||||||
|
ui.number("Quantity").props("outlined")
|
||||||
|
ui.select(...).props(
|
||||||
|
"outlined popup-content-class=app-item-detail-menu"
|
||||||
|
)
|
||||||
|
ui.textarea("Description").props("outlined autogrow")
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a scoped class to the dialog card:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ui.card().classes("app-detail-card app-item-detail-card")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scale The Complete Card
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--item-dialog-scale: 1;
|
||||||
|
--item-dialog-max-height: calc(100dvh - 3rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-item-detail-card {
|
||||||
|
width: min(50rem, 50vw);
|
||||||
|
max-height: var(--item-dialog-max-height);
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
zoom: var(--item-dialog-scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Restore Quasar's baseline if a global rule overrides it. */
|
||||||
|
.app-item-detail-card .q-field,
|
||||||
|
.app-item-detail-menu {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 599px) {
|
||||||
|
:root {
|
||||||
|
--item-dialog-scale: 1.2;
|
||||||
|
/* 75dvh becomes 90dvh after 1.2x zoom. */
|
||||||
|
--item-dialog-max-height: 75dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-item-detail-card {
|
||||||
|
width: 80vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-item-detail-menu {
|
||||||
|
font-size: 16.8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The main mobile tuning knob is:
|
||||||
|
|
||||||
|
```css
|
||||||
|
--item-dialog-scale: 1.2;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keep Detached Popups Unscaled
|
||||||
|
|
||||||
|
Do not apply `zoom` or `transform: scale()` to a `QSelect` popup menu. Quasar renders menus outside the dialog and positions them from the unscaled anchor geometry. Scaling the menu container afterward separates it from its field.
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.app-item-detail-card,
|
||||||
|
.app-item-detail-menu {
|
||||||
|
zoom: 1.2;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.app-item-detail-card {
|
||||||
|
zoom: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-item-detail-menu {
|
||||||
|
font-size: 16.8px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `popup-content-class=app-item-detail-menu` to target the detached menu and enlarge its text without changing its coordinate system.
|
||||||
|
|
||||||
|
### Account For Zoom When Scrolling
|
||||||
|
|
||||||
|
The card's pre-zoom maximum height must account for the scale:
|
||||||
|
|
||||||
|
$$
|
||||||
|
ext{pre-zoom max height}
|
||||||
|
=
|
||||||
|
\frac{\text{desired visible height}}{\text{scale}}
|
||||||
|
$$
|
||||||
|
|
||||||
|
For a desired visual height of `90dvh` at $1.2\times$:
|
||||||
|
|
||||||
|
$$
|
||||||
|
90 / 1.2 = 75
|
||||||
|
$$
|
||||||
|
|
||||||
|
Therefore:
|
||||||
|
|
||||||
|
```css
|
||||||
|
--item-dialog-max-height: 75dvh;
|
||||||
|
```
|
||||||
|
|
||||||
|
Apply scrolling to the card itself:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.app-item-detail-card {
|
||||||
|
max-height: var(--item-dialog-max-height);
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This keeps the dimmed page stationary while the form scrolls.
|
||||||
|
|
||||||
|
### Match The Quasar Breakpoint
|
||||||
|
|
||||||
|
Quasar's extra-small breakpoint ends at `599.98px`. A mobile-only rule can use:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@media (max-width: 599px) {
|
||||||
|
/* Mobile rules. */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirm custom breakpoint values against the target application's Quasar configuration.
|
||||||
|
|
||||||
|
## Validation Checklist
|
||||||
|
|
||||||
|
Test the complete page at representative mobile and desktop viewports. For a dialog, include a mobile viewport such as $390 \times 844$.
|
||||||
|
|
||||||
|
- Page sections do not overlap or introduce unintended horizontal scrolling.
|
||||||
|
- Responsive rows wrap or resize as designed.
|
||||||
|
- Dialog remains inside the viewport.
|
||||||
|
- Dialog has `scrollHeight > clientHeight` when its content is taller than its maximum height.
|
||||||
|
- Scrolling reaches the final form field.
|
||||||
|
- Select menus open directly against their fields.
|
||||||
|
- Menus have no horizontal overflow.
|
||||||
|
- Values and floating labels are not clipped.
|
||||||
|
- Select arrows and other icons scale with the card.
|
||||||
|
- Detached popup menus report `zoom: 1`.
|
||||||
|
|
||||||
|
As a precision check, compare the menu edge with the field edge using browser geometry. One corrected implementation measured within approximately `0.5px` horizontally and `0.14px` vertically; treat those values as an example observation, not a framework guarantee.
|
||||||
|
|
||||||
|
## Playwright Caveat
|
||||||
|
|
||||||
|
Playwright locator clicks can calculate incorrect coordinates for elements inside CSS `zoom`. A failed locator click does not necessarily mean browser interaction is broken.
|
||||||
|
|
||||||
|
For verification, either:
|
||||||
|
|
||||||
|
- click using manually adjusted visual coordinates
|
||||||
|
- trigger the element through DOM evaluation
|
||||||
|
- test the interaction manually in a real browser
|
||||||
|
|
||||||
|
Do not alter otherwise correct component styling solely to accommodate this automation limitation.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
!!! info "Primary sources"
|
!!! info "Primary sources"
|
||||||
- [NiceGUI elements](https://nicegui.io/documentation/element)
|
- [NiceGUI element styling and props](https://nicegui.io/documentation/element)
|
||||||
- [NiceGUI binding properties](https://nicegui.io/documentation/section_binding_properties)
|
- [NiceGUI binding properties](https://nicegui.io/documentation/section_binding_properties)
|
||||||
- [Tailwind utility-first styling](https://tailwindcss.com/docs/utility-first)
|
|
||||||
- [Quasar components](https://quasar.dev/vue-components)
|
- [Quasar components](https://quasar.dev/vue-components)
|
||||||
|
- [Quasar field](https://quasar.dev/vue-components/field/)
|
||||||
|
- [Quasar select](https://quasar.dev/vue-components/select/)
|
||||||
|
- [Quasar breakpoints](https://quasar.dev/style/breakpoints/)
|
||||||
|
- [Tailwind responsive design](https://tailwindcss.com/docs/responsive-design)
|
||||||
|
- [MDN `zoom`](https://developer.mozilla.org/en-US/docs/Web/CSS/zoom)
|
||||||
|
- [Playwright input actions](https://playwright.dev/docs/input)
|
||||||
@@ -56,6 +56,8 @@ Recommended base shape:
|
|||||||
- FastAPI exposes a health route such as `/healthz`.
|
- FastAPI exposes a health route such as `/healthz`.
|
||||||
- Imports do not trigger runtime global side effects.
|
- Imports do not trigger runtime global side effects.
|
||||||
|
|
||||||
|
For the ownership relationship between a caller-created FastAPI app, `nicegui.app`, `ui.run_with()`, Uvicorn, and a packaged startup command, load [FastAPI and Uvicorn startup](./fastapi-uvicorn-startup.md).
|
||||||
|
|
||||||
## Dependency Direction
|
## Dependency Direction
|
||||||
|
|
||||||
Prefer:
|
Prefer:
|
||||||
|
|||||||
@@ -0,0 +1,315 @@
|
|||||||
|
# FastAPI And Uvicorn Startup
|
||||||
|
|
||||||
|
Use this reference when FastAPI owns the application and NiceGUI is one part of it. The central distinction is between **composing an ASGI application** and **starting an ASGI server**:
|
||||||
|
|
||||||
|
- [`ui.run_with()`](https://github.com/zauberzeug/nicegui/blob/main/nicegui/ui_run_with.py) composes NiceGUI with a caller-owned FastAPI application. It does not start Uvicorn.
|
||||||
|
- [`uvicorn.run()`](https://www.uvicorn.org/#running-programmatically) starts the server and tells it which ASGI application to serve.
|
||||||
|
|
||||||
|
## Ownership Model
|
||||||
|
|
||||||
|
```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"]
|
||||||
|
U -->|"ASGI requests and lifespan"| P
|
||||||
|
```
|
||||||
|
|
||||||
|
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 |
|
||||||
|
|
||||||
|
Uvicorn must serve the **parent FastAPI app** when using `ui.run_with()`. Passing `nicegui.app` to `ui.run_with()` is rejected because it would mount NiceGUI into itself and recurse on unmatched routes.
|
||||||
|
|
||||||
|
## Choose One Startup Mode
|
||||||
|
|
||||||
|
### Let NiceGUI Own Startup
|
||||||
|
|
||||||
|
Use `ui.run()` when NiceGUI is the main application. Add ordinary FastAPI routes to the exported `nicegui.app` object:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from nicegui import app, ui
|
||||||
|
|
||||||
|
|
||||||
|
@app.get('/healthz')
|
||||||
|
def health() -> dict[str, str]:
|
||||||
|
return {'status': 'ok'}
|
||||||
|
|
||||||
|
|
||||||
|
@ui.page('/')
|
||||||
|
def home() -> None:
|
||||||
|
ui.label('Home')
|
||||||
|
|
||||||
|
|
||||||
|
ui.run()
|
||||||
|
```
|
||||||
|
|
||||||
|
In this mode, NiceGUI configures and starts its own [Uvicorn-derived server](https://github.com/zauberzeug/nicegui/blob/main/nicegui/server.py). Do not also call `uvicorn.run()`.
|
||||||
|
|
||||||
|
### Let FastAPI Own The Application
|
||||||
|
|
||||||
|
Use `ui.run_with()` when an existing FastAPI application owns middleware, API routers, OpenAPI configuration, lifespan resources, or deployment startup. The [official NiceGUI FastAPI example](https://github.com/zauberzeug/nicegui/blob/main/examples/fastapi/main.py) follows this model.
|
||||||
|
|
||||||
|
`mount_path` controls where the NiceGUI application appears externally. A NiceGUI page declared as `/` is reachable at `/gui/` when mounted at `/gui`, while parent routes such as `/healthz` remain at the root. A dedicated UI prefix usually makes ownership and route conflicts clearer than mounting both applications at `/`.
|
||||||
|
|
||||||
|
## Canonical Factory Layout
|
||||||
|
|
||||||
|
Keep application composition importable and server startup explicit:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
├─ pyproject.toml
|
||||||
|
└─ 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
|
||||||
|
|
||||||
|
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]:
|
||||||
|
app.state.ready = True
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
app.state.ready = False
|
||||||
|
|
||||||
|
|
||||||
|
def register_pages() -> None:
|
||||||
|
@ui.page('/')
|
||||||
|
def dashboard() -> None:
|
||||||
|
ui.label('Dashboard')
|
||||||
|
|
||||||
|
|
||||||
|
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]:
|
||||||
|
return {'status': 'ok'}
|
||||||
|
|
||||||
|
register_pages()
|
||||||
|
ui.run_with(
|
||||||
|
app,
|
||||||
|
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=settings.server.host,
|
||||||
|
port=settings.server.port,
|
||||||
|
log_level=settings.server.log_level,
|
||||||
|
reload=settings.server.reload,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
NiceGUI keeps framework state in its process-local app singleton. Treat `create_app()` as a once-per-worker factory. Calling it repeatedly in one interpreter can register the same pages and lifecycle handlers more than once; tests that create multiple apps must isolate or reset NiceGUI state.
|
||||||
|
|
||||||
|
## Lifespan Ordering
|
||||||
|
|
||||||
|
The [ASGI lifespan protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html) is driven by the server. Uvicorn sends startup before accepting requests and sends shutdown while terminating the process. Lifespan runs once per event loop, including once in each worker process.
|
||||||
|
|
||||||
|
Current NiceGUI source integrates with the parent application by:
|
||||||
|
|
||||||
|
1. Capturing the parent FastAPI lifespan context.
|
||||||
|
2. Mounting NiceGUI's internal app on the parent.
|
||||||
|
3. Replacing the parent lifespan with a wrapper.
|
||||||
|
4. Starting NiceGUI before entering the original parent lifespan.
|
||||||
|
5. Exiting the original parent lifespan before shutting down NiceGUI.
|
||||||
|
|
||||||
|
This exact ordering comes from the current [`ui.run_with` implementation](https://github.com/zauberzeug/nicegui/blob/main/nicegui/ui_run_with.py) and is version-sensitive. Check the pinned NiceGUI version before making one startup handler depend on another framework's internal ordering.
|
||||||
|
|
||||||
|
Create database pools, HTTP clients, and similar resources in the parent [FastAPI lifespan](https://fastapi.tiangolo.com/advanced/events/), then close them after `yield`. Do not create event-loop-bound resources at import time or assume that globals are shared between workers.
|
||||||
|
|
||||||
|
## Expose The Server As A Project Script
|
||||||
|
|
||||||
|
Map a command name to the no-argument startup function:
|
||||||
|
|
||||||
|
```toml title="pyproject.toml"
|
||||||
|
[project]
|
||||||
|
name = "my-app"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"nicegui",
|
||||||
|
"pydantic-settings",
|
||||||
|
"uvicorn[standard]",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
my-app = "my_app.main:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/my_app"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the installed command through uv:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```dotenv title=".env"
|
||||||
|
MY_APP_SERVER__HOST=127.0.0.1
|
||||||
|
MY_APP_SERVER__RELOAD=true
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
| Anti-pattern | Why it fails | Preferred approach |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `ui.run_with(nicegui.app)` | Mounts NiceGUI into itself | Pass a separately created `FastAPI()` instance |
|
||||||
|
| Calling both `ui.run()` and `ui.run_with()` | Gives two paths responsibility for startup | Choose one ownership model |
|
||||||
|
| `uvicorn.run(create_app(), reload=True)` | Reload subprocesses cannot import the app object | Use an import string with `factory=True` |
|
||||||
|
| 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
|
||||||
|
|
||||||
|
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:
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Also verify:
|
||||||
|
|
||||||
|
- startup resources exist while the client context is active and are released afterward
|
||||||
|
- the mounted UI returns HTML and parent API failures retain FastAPI's JSON responses
|
||||||
|
- `uv run my-app` starts the server and responds on both the API and UI paths
|
||||||
|
- shutdown signals complete without orphaned background tasks
|
||||||
|
|
||||||
|
## Primary Sources
|
||||||
|
|
||||||
|
- [NiceGUI pages, routing, and FastAPI integration](https://www.nicegui.io/documentation/section_pages_routing)
|
||||||
|
- [NiceGUI `ui.run_with` implementation](https://github.com/zauberzeug/nicegui/blob/main/nicegui/ui_run_with.py)
|
||||||
|
- [NiceGUI FastAPI example](https://github.com/zauberzeug/nicegui/blob/main/examples/fastapi/main.py)
|
||||||
|
- [FastAPI lifespan events](https://fastapi.tiangolo.com/advanced/events/)
|
||||||
|
- [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/)
|
||||||
@@ -6,6 +6,8 @@ Use these links to verify framework-specific behavior before relying on version-
|
|||||||
|
|
||||||
!!! info "NiceGUI sources"
|
!!! info "NiceGUI sources"
|
||||||
- [Pages, routing, and FastAPI integration](https://www.nicegui.io/documentation/section_pages_routing)
|
- [Pages, routing, and FastAPI integration](https://www.nicegui.io/documentation/section_pages_routing)
|
||||||
|
- [`ui.run_with` implementation](https://github.com/zauberzeug/nicegui/blob/main/nicegui/ui_run_with.py)
|
||||||
|
- [FastAPI integration example](https://github.com/zauberzeug/nicegui/blob/main/examples/fastapi/main.py)
|
||||||
- [Binding properties and bindable dataclasses](https://www.nicegui.io/documentation/section_binding_properties)
|
- [Binding properties and bindable dataclasses](https://www.nicegui.io/documentation/section_binding_properties)
|
||||||
- [Action events](https://www.nicegui.io/documentation/section_action_events)
|
- [Action events](https://www.nicegui.io/documentation/section_action_events)
|
||||||
- [Security best practices](https://www.nicegui.io/documentation/section_security)
|
- [Security best practices](https://www.nicegui.io/documentation/section_security)
|
||||||
@@ -19,6 +21,21 @@ Use these links to verify framework-specific behavior before relying on version-
|
|||||||
- [Server-sent events](https://fastapi.tiangolo.com/advanced/server-sent-events/)
|
- [Server-sent events](https://fastapi.tiangolo.com/advanced/server-sent-events/)
|
||||||
- [WebSockets](https://fastapi.tiangolo.com/advanced/websockets/)
|
- [WebSockets](https://fastapi.tiangolo.com/advanced/websockets/)
|
||||||
|
|
||||||
|
## ASGI And Uvicorn
|
||||||
|
|
||||||
|
!!! info "Server and lifespan sources"
|
||||||
|
- [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)
|
||||||
|
- [Uvicorn deployment](https://www.uvicorn.org/deployment/)
|
||||||
|
|
||||||
|
## uv And Project Scripts
|
||||||
|
|
||||||
|
!!! info "Packaging and command sources"
|
||||||
|
- [uv project entry points](https://docs.astral.sh/uv/concepts/projects/config/#entry-points)
|
||||||
|
- [uv project packaging](https://docs.astral.sh/uv/concepts/projects/config/#project-packaging)
|
||||||
|
- [PyPA entry points specification](https://packaging.python.org/en/latest/specifications/entry-points/)
|
||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
!!! info "Styling sources"
|
!!! info "Styling sources"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: pydantic-settings
|
name: pydantic-settings
|
||||||
description: "Practical guide for implementing typed application configuration with pydantic-settings. Use when designing BaseSettings models, choosing env naming strategy, configuring dotenv or secrets, and customizing source priority safely."
|
description: "Practical guide for implementing typed application configuration with pydantic-settings. Use when designing BaseSettings models, choosing nested or independent settings boundaries, managing settings lifecycles, configuring dotenv or secrets, and customizing source priority safely."
|
||||||
x-personal-mcp:
|
x-personal-mcp:
|
||||||
id: pydantic-settings
|
id: pydantic-settings
|
||||||
version: 1.0.0
|
version: 1.1.0
|
||||||
tags:
|
tags:
|
||||||
- python
|
- python
|
||||||
- pydantic
|
- pydantic
|
||||||
@@ -13,6 +13,8 @@ x-personal-mcp:
|
|||||||
- secrets
|
- secrets
|
||||||
- dotenv
|
- dotenv
|
||||||
- source-priority
|
- source-priority
|
||||||
|
- caching
|
||||||
|
- lifecycle
|
||||||
capabilities:
|
capabilities:
|
||||||
- resource://skills/pydantic-settings/document
|
- resource://skills/pydantic-settings/document
|
||||||
---
|
---
|
||||||
@@ -27,6 +29,8 @@ Use this skill to implement robust, typed application configuration with `pydant
|
|||||||
- You are migrating from ad-hoc `os.getenv(...)` calls.
|
- You are migrating from ad-hoc `os.getenv(...)` calls.
|
||||||
- You need predictable precedence across init args, env vars, dotenv files, and secrets.
|
- You need predictable precedence across init args, env vars, dotenv files, and secrets.
|
||||||
- You need nested settings models and reliable parsing behavior.
|
- You need nested settings models and reliable parsing behavior.
|
||||||
|
- You need to choose between one nested application settings object and independently owned settings objects.
|
||||||
|
- You need a deliberate construction, caching, or reload lifecycle.
|
||||||
- You need to customize settings sources or source order safely.
|
- You need to customize settings sources or source order safely.
|
||||||
|
|
||||||
## Procedure
|
## Procedure
|
||||||
@@ -53,6 +57,7 @@ class Settings(BaseSettings):
|
|||||||
env_file=".env",
|
env_file=".env",
|
||||||
env_file_encoding="utf-8",
|
env_file_encoding="utf-8",
|
||||||
extra="ignore",
|
extra="ignore",
|
||||||
|
frozen=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
debug: bool = False
|
debug: bool = False
|
||||||
@@ -154,101 +159,122 @@ Quality gate:
|
|||||||
1. No secret literals in repository code.
|
1. No secret literals in repository code.
|
||||||
2. Missing secrets behavior is understood per environment.
|
2. Missing secrets behavior is understood per environment.
|
||||||
|
|
||||||
### 6. Add ContextVar-Scoped Constructors And Accessors
|
### 6. Choose Nested Or Independent Settings Boundaries
|
||||||
|
|
||||||
When configuration and database resources should be request- or context-scoped, use `ContextVar` backed constructor and accessor methods.
|
Prefer one root `BaseSettings` object with nested `BaseModel` sections when the configuration belongs to one application lifecycle:
|
||||||
|
|
||||||
Example pattern:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from contextlib import contextmanager
|
from pydantic import BaseModel, Field
|
||||||
from contextvars import ContextVar
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
from functools import cache
|
|
||||||
|
|
||||||
from pydantic import SecretStr
|
|
||||||
from pydantic_settings import BaseSettings
|
|
||||||
from sqlmodel import Session, create_engine
|
|
||||||
from sqlalchemy import Engine
|
|
||||||
|
|
||||||
|
|
||||||
class DbSettings(BaseSettings):
|
class DatabaseSettings(BaseModel):
|
||||||
model_config = {
|
|
||||||
"env_prefix": "DB_",
|
|
||||||
"extra": "ignore",
|
|
||||||
}
|
|
||||||
|
|
||||||
host: str = "localhost"
|
host: str = "localhost"
|
||||||
port: int = 5432
|
port: int = 5432
|
||||||
username: str
|
|
||||||
password: SecretStr
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dsn(self) -> str:
|
|
||||||
return (
|
|
||||||
"postgresql://"
|
|
||||||
f"{self.username}:{self.password.get_secret_value()}"
|
|
||||||
f"@{self.host}:{self.port}/mydatabase"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_db_settings: ContextVar[DbSettings | None] = ContextVar("db_settings", default=None)
|
class ObservabilitySettings(BaseModel):
|
||||||
_db_conn: ContextVar[Engine | None] = ContextVar("db_conn", default=None)
|
log_level: str = "INFO"
|
||||||
|
json_logs: bool = True
|
||||||
|
|
||||||
|
|
||||||
def get_db_settings(**kwargs) -> DbSettings:
|
class Settings(BaseSettings):
|
||||||
settings = _db_settings.get()
|
model_config = SettingsConfigDict(
|
||||||
if settings is None:
|
env_prefix="APP_",
|
||||||
settings = DbSettings(**kwargs)
|
env_nested_delimiter="__",
|
||||||
_db_settings.set(settings)
|
frozen=True,
|
||||||
cleanup_engine()
|
)
|
||||||
return settings
|
|
||||||
|
|
||||||
|
database: DatabaseSettings = Field(default_factory=DatabaseSettings)
|
||||||
@cache
|
observability: ObservabilitySettings = Field(
|
||||||
def get_db_engine() -> Engine:
|
default_factory=ObservabilitySettings
|
||||||
engine = _db_conn.get()
|
)
|
||||||
if engine is None:
|
|
||||||
engine = create_engine(get_db_settings().dsn)
|
|
||||||
_db_conn.set(engine)
|
|
||||||
return engine
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup_engine() -> None:
|
|
||||||
engine = _db_conn.get()
|
|
||||||
if engine is not None:
|
|
||||||
engine.dispose()
|
|
||||||
_db_conn.set(None)
|
|
||||||
get_db_engine.cache_clear()
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def get_session():
|
|
||||||
with Session(get_db_engine()) as session:
|
|
||||||
yield session
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Design notes:
|
This produces names such as `APP_DATABASE__HOST` and gives the application one validated, atomic configuration snapshot. Nested sections should normally inherit from `BaseModel`, not `BaseSettings`; otherwise each nested settings model can collect sources independently and produce surprising results.
|
||||||
|
|
||||||
1. `get_db_settings` is the constructor/accessor for settings and can accept explicit overrides in tests.
|
Use independent `BaseSettings` classes when the objects have genuinely independent ownership:
|
||||||
2. `get_db_engine` is the constructor/accessor for the engine and reuses context-local state.
|
|
||||||
3. `cleanup_engine` must run when settings change so stale DSNs do not leak across contexts.
|
1. Different packages or deployable components own the schemas.
|
||||||
4. `get_session` centralizes session creation so call sites never build engines directly.
|
2. Each object needs its own env prefix or source policy.
|
||||||
|
3. A component is optional or loaded lazily.
|
||||||
|
4. Components need different reload lifecycles.
|
||||||
|
5. The same component must run outside the application.
|
||||||
|
|
||||||
|
Construct independent objects explicitly at the composition root and inject each dependency. Do not nest one `BaseSettings` class inside another merely to reuse its fields. Extract a shared `BaseModel` schema when models need common structure.
|
||||||
|
|
||||||
Quality gate:
|
Quality gate:
|
||||||
|
|
||||||
1. Overriding settings triggers engine cleanup and cache invalidation.
|
1. Nested sections share one source policy and lifecycle.
|
||||||
2. No module-level global engine is created outside accessors.
|
2. Independent settings have distinct owners, prefixes, or lifecycles.
|
||||||
3. Session creation always goes through `get_session()`.
|
3. The application does not repeatedly scan the same sources through accidental nested `BaseSettings` construction.
|
||||||
|
|
||||||
### 7. Add Focused Resource-Lifecycle Test
|
### 7. Own The Settings Lifecycle
|
||||||
|
|
||||||
Do not add tests that re-validate baseline `pydantic-settings` functionality (for example env parsing, alias semantics, or source precedence) unless you have custom behavior layered on top.
|
For most applications, construct settings once at the composition root and pass the validated object to services:
|
||||||
|
|
||||||
Minimum test to add (only when an engine accessor exists):
|
```python
|
||||||
|
def main() -> None:
|
||||||
|
settings = Settings()
|
||||||
|
application = Application(settings=settings)
|
||||||
|
application.run()
|
||||||
|
```
|
||||||
|
|
||||||
1. assert the database engine is not instantiated more than once for repeated accessor calls in the same lifecycle/context
|
This makes ownership, startup failure, and test overrides explicit. Treat the object as a snapshot: environment variables and files changing later do not update an existing instance. Prefer `frozen=True` for shared settings so consumers cannot silently mutate process-wide configuration.
|
||||||
|
|
||||||
If the project has no database engine accessor, skip this section.
|
Use [`functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache) only when process-lifetime singleton access is intentional and explicit injection is awkward, such as a framework dependency provider:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from functools import cache
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def get_settings() -> Settings:
|
||||||
|
return Settings()
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep the cached factory argument-free. Passing override kwargs creates one cached instance per argument combination, retains those values for the process lifetime, and obscures which configuration is active. In tests, instantiate `Settings(...)` directly or override the dependency; when a test must exercise the cached getter, isolate environment changes with `get_settings.cache_clear()` before and after the assertion.
|
||||||
|
|
||||||
|
`cache` is process-local. Every worker process gets its own instance, and concurrent first calls can construct more than one instance before the cache is populated. Settings construction must therefore be side-effect free; create engines, clients, and sessions in their own lifecycle-managed providers.
|
||||||
|
|
||||||
|
Quality gate:
|
||||||
|
|
||||||
|
1. Settings are created once per intended application or worker lifecycle.
|
||||||
|
2. Cached factories are argument-free and side-effect free.
|
||||||
|
3. Tests do not leak cached settings or environment changes.
|
||||||
|
4. Resource construction is separate from configuration parsing.
|
||||||
|
|
||||||
|
### 8. Reload Deliberately
|
||||||
|
|
||||||
|
Static service configuration should normally require a process restart. If runtime reload is a real requirement, construct a fresh settings instance and atomically replace the owned reference. Do not call `__init__()` on a shared instance: readers can observe mutation in progress, and resources derived from old values may remain alive.
|
||||||
|
|
||||||
|
Settings sources are synchronous. In an async application, construction or reload that reads dotenv, secrets, JSON, TOML, or YAML files should run in a worker thread:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
|
||||||
|
async def load_settings() -> Settings:
|
||||||
|
return await asyncio.to_thread(Settings)
|
||||||
|
```
|
||||||
|
|
||||||
|
Clearing `get_settings` is sufficient for controlled tests or single-threaded administration, but it is not an atomic live-reload protocol. Concurrent applications should own the current reference behind an application-specific lock or lifecycle manager, swap in a fully validated replacement, and then rebuild dependent resources.
|
||||||
|
|
||||||
|
Quality gate:
|
||||||
|
|
||||||
|
1. Reload creates and validates a replacement before publication.
|
||||||
|
2. Readers cannot observe a partially mutated object.
|
||||||
|
3. Dependent resources are recreated after the settings reference changes.
|
||||||
|
4. File-backed source reads do not block an async event loop.
|
||||||
|
|
||||||
|
### 9. Add Focused Lifecycle Tests
|
||||||
|
|
||||||
|
Do not add tests that re-validate baseline `pydantic-settings` functionality unless custom behavior is layered on top. Test the application-owned behavior instead:
|
||||||
|
|
||||||
|
1. Repeated cached getter calls return the same instance.
|
||||||
|
2. Cache clearing after an environment change returns a newly validated instance.
|
||||||
|
3. Explicitly injected settings bypass global cached state.
|
||||||
|
4. Reload swaps the settings snapshot and rebuilds dependent resources, when reload is supported.
|
||||||
|
|
||||||
Suggested invocation:
|
Suggested invocation:
|
||||||
|
|
||||||
@@ -256,13 +282,15 @@ Suggested invocation:
|
|||||||
|
|
||||||
## Completion Checks
|
## Completion Checks
|
||||||
|
|
||||||
1. A single typed settings model exists for the service boundary.
|
1. Settings ownership matches the application or component lifecycle.
|
||||||
2. Source precedence is documented and tested.
|
2. Source precedence is documented and tested.
|
||||||
3. Env naming conventions and aliases are explicit and stable.
|
3. Env naming conventions and aliases are explicit and stable.
|
||||||
4. Nested parsing behavior is tested when custom parsing behavior is added.
|
4. Nested parsing behavior is tested when custom parsing behavior is added.
|
||||||
5. Secrets and dotenv usage are environment-appropriate and do not leak sensitive defaults.
|
5. Secrets and dotenv usage are environment-appropriate and do not leak sensitive defaults.
|
||||||
6. Validation errors are actionable and fail fast for required values.
|
6. Validation errors are actionable and fail fast for required values.
|
||||||
7. If an engine accessor exists, engine construction occurs at most once per lifecycle/context.
|
7. Cached factories are argument-free, process-local, and cleared deliberately in tests.
|
||||||
|
8. Nested models share one source policy; independent settings have an explicit ownership reason.
|
||||||
|
9. Runtime reload, if supported, replaces a validated snapshot and rebuilds dependent resources.
|
||||||
|
|
||||||
## Output Contract
|
## Output Contract
|
||||||
|
|
||||||
@@ -303,6 +331,12 @@ Use these upstream docs when implementing or reviewing `pydantic-settings` behav
|
|||||||
- [Parsing environment variable values](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/#parsing-environment-variable-values)
|
- [Parsing environment variable values](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/#parsing-environment-variable-values)
|
||||||
- [Nested model default partial updates](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/#nested-model-default-partial-updates)
|
- [Nested model default partial updates](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/#nested-model-default-partial-updates)
|
||||||
|
|
||||||
|
### Lifecycle And Reloading
|
||||||
|
|
||||||
|
- [In-place reloading](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/#in-place-reloading)
|
||||||
|
- [Async environments](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/#async-environments)
|
||||||
|
- [`functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache)
|
||||||
|
|
||||||
### Dotenv And Secrets
|
### Dotenv And Secrets
|
||||||
|
|
||||||
- [Dotenv support](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/#dotenv-env-support)
|
- [Dotenv support](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/#dotenv-env-support)
|
||||||
|
|||||||
+2
-1
@@ -103,7 +103,8 @@ nav = [
|
|||||||
{ "NiceGUI" = [
|
{ "NiceGUI" = [
|
||||||
{ "Overview" = "skills/nicegui/SKILL.md" },
|
{ "Overview" = "skills/nicegui/SKILL.md" },
|
||||||
{ "App Architecture" = "skills/nicegui/references/architecture.md" },
|
{ "App Architecture" = "skills/nicegui/references/architecture.md" },
|
||||||
{ "Style" = "skills/nicegui/references/architecture-and-styling.md" },
|
{ "Startup" = "skills/nicegui/references/fastapi-uvicorn-startup.md" },
|
||||||
|
{ "Layout and Style" = "skills/nicegui/references/architecture-and-styling.md" },
|
||||||
{ "Binding" = "skills/nicegui/references/binding-dataclasses.md" },
|
{ "Binding" = "skills/nicegui/references/binding-dataclasses.md" },
|
||||||
{ "Flows" = "skills/nicegui/references/interaction-patterns.md" },
|
{ "Flows" = "skills/nicegui/references/interaction-patterns.md" },
|
||||||
{ "Quality" = "skills/nicegui/references/troubleshooting-and-quality-gates.md" },
|
{ "Quality" = "skills/nicegui/references/troubleshooting-and-quality-gates.md" },
|
||||||
|
|||||||
Reference in New Issue
Block a user