Compare commits
19
Commits
b09409d842
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd11ea8255 | ||
|
|
bc21643e8c | ||
|
|
1ed5856db0 | ||
|
|
70695ff218 | ||
|
|
a238fb4dc3 | ||
|
|
b6f109cf91 | ||
|
|
3abafc4850 | ||
|
|
d4c7952175 | ||
|
|
da58e20b69 | ||
|
|
d79025538b | ||
|
|
7b1e5fcacb | ||
|
|
37461fd880 | ||
|
|
226f19b2c6 | ||
|
|
a18c8456d3 | ||
|
|
34e6d693ab | ||
|
|
efba051cb5 | ||
|
|
c9b6e137f2 | ||
|
|
8f26051a52 | ||
|
|
bc0d6ede49 |
Vendored
+2
-1
@@ -52,7 +52,8 @@
|
|||||||
"args": [
|
"args": [
|
||||||
"run",
|
"run",
|
||||||
"uvicorn",
|
"uvicorn",
|
||||||
"personal_mcp.main:app",
|
"personal_mcp.main:create_app",
|
||||||
|
"--factory",
|
||||||
"--host",
|
"--host",
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
"--port",
|
"--port",
|
||||||
|
|||||||
+2
-2
@@ -29,7 +29,7 @@ FROM python:3.14-slim AS runtime
|
|||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
PATH="/app/.venv/bin:$PATH" \
|
PATH="/app/.venv/bin:$PATH" \
|
||||||
PERSONAL_MCP_MOUNTS__DOCS_DIR=/app/docs
|
PERSONAL_MCP_SITE_DIR=/app/site
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
|||||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||||
--mount=type=bind,source=src/,target=src/ \
|
--mount=type=bind,source=src/,target=src/ \
|
||||||
uv sync --no-editable
|
uv sync --no-editable --refresh-package prompts
|
||||||
|
|
||||||
USER appuser
|
USER appuser
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -3,6 +3,6 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8765:8765"
|
- "8765:8765"
|
||||||
restart: unless-stopped
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
window.MathJax = {
|
||||||
|
tex: {
|
||||||
|
inlineMath: [['\\(', '\\)']],
|
||||||
|
displayMath: [['\\[', '\\]']],
|
||||||
|
processEscapes: true,
|
||||||
|
processEnvironments: true
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
ignoreHtmlClass: '.*|',
|
||||||
|
processHtmlClass: 'arithmatex'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document$.subscribe(() => {
|
||||||
|
MathJax.startup.output.clearCache();
|
||||||
|
MathJax.typesetClear();
|
||||||
|
MathJax.texReset();
|
||||||
|
MathJax.typesetPromise();
|
||||||
|
});
|
||||||
|
|
||||||
|
component$.subscribe(({ ref }) => {
|
||||||
|
if (ref.classList.contains('md-annotation')) {
|
||||||
|
MathJax.typesetPromise([ref]);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
name: jsfiddle-page-layout
|
||||||
|
description: Create a responsive sample page layout for a user-supplied domain and return paste-ready HTML and CSS for JSFiddle.
|
||||||
|
x-personal-mcp:
|
||||||
|
id: jsfiddle-page-layout
|
||||||
|
version: 1.0.0
|
||||||
|
tags:
|
||||||
|
- frontend
|
||||||
|
- html
|
||||||
|
- css
|
||||||
|
- jsfiddle
|
||||||
|
- layout
|
||||||
|
- prototyping
|
||||||
|
- prompts
|
||||||
|
capabilities:
|
||||||
|
- resource://prompts/jsfiddle-page-layout/document
|
||||||
|
arguments:
|
||||||
|
domain:
|
||||||
|
title: Domain
|
||||||
|
description: The product, service, organization, or subject the sample page should represent, including its audience when known.
|
||||||
|
required: true
|
||||||
|
layout_brief:
|
||||||
|
title: Layout brief
|
||||||
|
description: Optional page type, required sections, content priorities, visual direction, or constraints.
|
||||||
|
required: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# JSFiddle Page Layout
|
||||||
|
|
||||||
|
Create a polished sample page layout for the supplied domain. The result must run by pasting the markup and styles into the [JSFiddle](https://jsfiddle.net/) HTML and CSS panes.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
1. `domain`: the product, service, organization, or subject represented by the page, including its intended audience when known
|
||||||
|
2. `layout_brief`: optional page type, required sections, content priorities, visual direction, or constraints
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Infer the page's primary purpose, audience, content hierarchy, and most important user action from the inputs.
|
||||||
|
2. If the domain does not provide enough information to choose a useful page type or primary action, ask one concise clarification question before generating code.
|
||||||
|
3. Choose a visual direction and information density appropriate to the domain. Build the usable page itself, not a marketing explanation of the page.
|
||||||
|
4. Write semantic HTML with realistic domain-specific sample content. Do not use placeholder text such as lorem ipsum.
|
||||||
|
5. Build the layout with modern CSS, using [CSS Grid](https://css-tricks.com/complete-guide-css-grid-layout/) for two-dimensional page structure and [Flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) for one-dimensional alignment where each fits naturally.
|
||||||
|
6. Make the page responsive at narrow mobile and desktop widths without horizontal overflow, overlapping content, or clipped text.
|
||||||
|
7. Keep the example self-contained. Use no JavaScript, build tools, external stylesheets, images, or icon libraries unless the layout brief explicitly requires them.
|
||||||
|
8. Include accessible landmarks, heading order, labels, focus styles, color contrast, and reduced-motion handling when animation is present.
|
||||||
|
9. Use CSS custom properties for the color, typography, spacing, border, and shadow system. Avoid generic framework styling and tailor the visual language to the domain.
|
||||||
|
|
||||||
|
## Output Contract
|
||||||
|
|
||||||
|
Return exactly two fenced code blocks in this order:
|
||||||
|
|
||||||
|
1. An `html` block containing only the content for JSFiddle's HTML pane.
|
||||||
|
2. A `css` block containing only the content for JSFiddle's CSS pane.
|
||||||
|
|
||||||
|
Do not include setup instructions, design commentary, JavaScript, or prose outside the two code blocks.
|
||||||
|
|
||||||
|
## Quality Rules
|
||||||
|
|
||||||
|
1. Prefer semantic elements such as `header`, `nav`, `main`, `section`, `article`, `aside`, and `footer` when they match the content.
|
||||||
|
2. Reserve large display type for a true hero or primary page title; keep operational interfaces compact and easy to scan.
|
||||||
|
3. Use cards only for repeated items or genuinely framed tools. Do not place cards inside cards.
|
||||||
|
4. Use stable responsive constraints for grids, controls, media, and navigation so dynamic content does not shift the layout unexpectedly.
|
||||||
|
5. Avoid decorative gradients, floating color blobs, excessive rounding, and one-note palettes unless they are explicitly appropriate to the domain.
|
||||||
|
6. Ensure controls look and behave like their purpose, with visible hover and keyboard-focus states.
|
||||||
|
7. Keep all visible copy relevant to the fictional domain rather than describing the mockup or its implementation.
|
||||||
@@ -110,6 +110,14 @@ Pool sizing, overflow, recycle, pre-ping, isolation, statement timeouts, and hea
|
|||||||
|
|
||||||
See [observability and resilience](references/observability.md).
|
See [observability and resilience](references/observability.md).
|
||||||
|
|
||||||
|
### Test through the production seam
|
||||||
|
|
||||||
|
Keep the production engine and session-factory construction path intact in tests. Select a dedicated PostgreSQL, local SQLite, or in-memory SQLite URL at that seam, then override the request-session dependency only for the test lifetime. Use a test-scoped outer transaction with SAVEPOINT-backed session commits when application code calls `commit()`; it exercises normal transaction behavior while cleanup remains deterministic.
|
||||||
|
|
||||||
|
In-memory SQLite is suitable for serial tests. For multiple simultaneous sessions, use a named shared-cache SQLite URL or a temporary file, and retain PostgreSQL integration coverage for PostgreSQL-specific behavior.
|
||||||
|
|
||||||
|
See [database testing and fixture data](references/testing.md).
|
||||||
|
|
||||||
## Reference Map
|
## Reference Map
|
||||||
|
|
||||||
| Concept | Reference |
|
| Concept | Reference |
|
||||||
@@ -123,6 +131,7 @@ See [observability and resilience](references/observability.md).
|
|||||||
| Observability and resilience | [Observability reference](references/observability.md) |
|
| Observability and resilience | [Observability reference](references/observability.md) |
|
||||||
| SQLModel-first modeling | [SQLModel integration reference](references/sqlmodel.md) |
|
| SQLModel-first modeling | [SQLModel integration reference](references/sqlmodel.md) |
|
||||||
| CRUD repository and standalone functions | [Basic CRUD reference](references/crud.md) |
|
| CRUD repository and standalone functions | [Basic CRUD reference](references/crud.md) |
|
||||||
|
| Test database selection and fixture data | [Database testing reference](references/testing.md) |
|
||||||
|
|
||||||
## Canonical Composition Pattern
|
## Canonical Composition Pattern
|
||||||
|
|
||||||
@@ -130,23 +139,28 @@ This example shows the ownership boundaries. Adapt state storage and dependency
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
from contextlib import AsyncExitStack, asynccontextmanager
|
from contextlib import AsyncExitStack, asynccontextmanager
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncGeneratorr
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
|
||||||
async with AsyncExitStack() as stack:
|
async with AsyncExitStack() as stack:
|
||||||
engine = create_async_engine(settings.database_url)
|
engine = create_async_engine(settings.database_url)
|
||||||
stack.push_async_callback(engine.dispose)
|
stack.push_async_callback(engine.dispose)
|
||||||
|
|
||||||
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
session_factory = async_sessionmaker(
|
||||||
|
engine,
|
||||||
|
class_=AsyncSession,
|
||||||
|
expire_on_commit=False,
|
||||||
|
)
|
||||||
app.state.session_factory = session_factory
|
app.state.session_factory = session_factory
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
async def get_session() -> AsyncIterator[AsyncSession]:
|
async def get_session() -> AsyncGenerator[AsyncSession]:
|
||||||
async with app.state.session_factory() as session:
|
async with app.state.session_factory() as session:
|
||||||
yield session
|
yield session
|
||||||
```
|
```
|
||||||
@@ -173,6 +187,7 @@ When reviewing code, verify:
|
|||||||
- Relationship and deferred-column access cannot surprise the event loop with implicit I/O.
|
- Relationship and deferred-column access cannot surprise the event loop with implicit I/O.
|
||||||
- Pool and timeout settings are justified by deployment behavior.
|
- Pool and timeout settings are justified by deployment behavior.
|
||||||
- Tests exercise rollback, cleanup, concurrency, and lifespan behavior where relevant.
|
- Tests exercise rollback, cleanup, concurrency, and lifespan behavior where relevant.
|
||||||
|
- Tests use a dedicated database target and preserve production session mechanics.
|
||||||
|
|
||||||
## Anti-Patterns to Flag
|
## Anti-Patterns to Flag
|
||||||
|
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ This reference uses direct field arguments and full-update semantics to keep the
|
|||||||
Functions are the simplest default when grouping state or behavior in an object adds no value. Each function is a complete operation boundary: it can run standalone by resolving the cached factory from `database_url`, or compose into a caller-owned scope through `session`.
|
Functions are the simplest default when grouping state or behavior in an object adds no value. Each function is a complete operation boundary: it can run standalone by resolving the cached factory from `database_url`, or compose into a caller-owned scope through `session`.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
from .session import session_scope
|
from .session import session_scope
|
||||||
from .session import transaction_scope
|
from .session import transaction_scope
|
||||||
@@ -170,7 +170,7 @@ Update and delete load the row through the same session that mutates it. This av
|
|||||||
A repository can provide a stable domain-facing interface when several callers need the same grouped operations. It stores repeatable database configuration, never a mutable session. Every method delegates to the analogous function and exposes the same optional-session contract.
|
A repository can provide a stable domain-facing interface when several callers need the same grouped operations. It stores repeatable database configuration, never a mutable session. Every method delegates to the analogous function and exposes the same optional-session contract.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
class WidgetRepository:
|
class WidgetRepository:
|
||||||
def __init__(self, database_url: str) -> None:
|
def __init__(self, database_url: str) -> None:
|
||||||
|
|||||||
@@ -64,14 +64,14 @@ Resolve settings at the composition boundary and call `get_engine(settings.datab
|
|||||||
The lifespan context manager only connects the cached resource to FastAPI ownership:
|
The lifespan context manager only connects the cached resource to FastAPI ownership:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncGeneratorr
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
|
||||||
database_url = app.state.settings.database_url
|
database_url = app.state.settings.database_url
|
||||||
engine = get_engine(database_url)
|
engine = get_engine(database_url)
|
||||||
app.state.engine = engine
|
app.state.engine = engine
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Purpose: concept registry for the principles, mechanics, and implementation guid
|
|||||||
| Observability and resilience | [observability.md](observability.md) | adopted | mandatory | platform/backend | 2026-06-17 |
|
| Observability and resilience | [observability.md](observability.md) | adopted | mandatory | platform/backend | 2026-06-17 |
|
||||||
| SQLModel modeling and async boundaries | [sqlmodel.md](sqlmodel.md) | adopted | mandatory | platform/backend | 2026-07-26 |
|
| SQLModel modeling and async boundaries | [sqlmodel.md](sqlmodel.md) | adopted | mandatory | platform/backend | 2026-07-26 |
|
||||||
| Basic CRUD repository and functions | [crud.md](crud.md) | adopted | advisory | platform/backend | 2026-07-26 |
|
| Basic CRUD repository and functions | [crud.md](crud.md) | adopted | advisory | platform/backend | 2026-07-26 |
|
||||||
|
| Test database targets and fixture data | [testing.md](testing.md) | adopted | mandatory | platform/backend | 2026-07-30 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,38 @@ Define one canonical session model for FastAPI + SQLAlchemy asyncio:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Sessions and Transactions
|
||||||
|
|
||||||
|
A session and a transaction solve related but different problems:
|
||||||
|
|
||||||
|
| Concept | Responsibility | Typical lifetime |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `AsyncSession` | Provides the ORM workspace: executes queries, tracks loaded and changed objects in its identity map, and flushes pending changes. It also coordinates access to a database connection. | One request, task, or explicit unit of work. |
|
||||||
|
| Transaction | Defines the atomic database boundary: all work inside it commits together on success or rolls back together on failure. | One complete operation that must have a single outcome. |
|
||||||
|
|
||||||
|
A transaction belongs to a session; it is not an alternative to one. The session is the interface used by application and data-access code, while the transaction determines when that work becomes permanent. A session may coordinate sequential transactions during its lifetime, although short-lived application scopes commonly use one session for one transaction.
|
||||||
|
|
||||||
|
Use a session without a helper-owned commit boundary for independent reads or lower-level functions that must participate in whatever transaction their caller controls:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async with session_factory() as session:
|
||||||
|
item = await find_item(session, item_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
Use an explicit transaction for writes, read-modify-write operations, or several statements that must succeed or fail as one unit:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async with session_factory.begin() as session:
|
||||||
|
order = await create_order(session, order_data)
|
||||||
|
await reserve_inventory(session, order)
|
||||||
|
```
|
||||||
|
|
||||||
|
SQLAlchemy sessions use [autobegin](https://docs.sqlalchemy.org/en/21/orm/session_basics.html#auto-begin), so the first database operation normally starts a transaction even for a read. Therefore, “session-only” means that the surrounding helper owns only session lifetime and does not promise to commit; it does not mean that no database transaction exists. Closing such a session releases its resources and rolls back any unfinished transaction. An explicit `begin()` is valuable when application code must make the atomic boundary and commit ownership visible.
|
||||||
|
|
||||||
|
For most read-only operations, a session context is sufficient. Use an explicit transaction for reads when they need a defined consistency boundary, participate in a larger atomic operation, or use locking such as `SELECT ... FOR UPDATE`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Session Factory Mechanics
|
## Session Factory Mechanics
|
||||||
|
|
||||||
An `async_sessionmaker[AsyncSession]` is a reusable configuration object and callable session producer. It stores how sessions should be created, including the engine binding and options such as `expire_on_commit=False`. It is not itself a session, connection, or transaction, and calling it does not make a shared global `AsyncSession`.
|
An `async_sessionmaker[AsyncSession]` is a reusable configuration object and callable session producer. It stores how sessions should be created, including the engine binding and options such as `expire_on_commit=False`. It is not itself a session, connection, or transaction, and calling it does not make a shared global `AsyncSession`.
|
||||||
@@ -49,7 +81,8 @@ Cache it by the application-owned engine so repeated composition calls return th
|
|||||||
```python
|
```python
|
||||||
from functools import cache
|
from functools import cache
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
from .engine import dispose_engine
|
from .engine import dispose_engine
|
||||||
from .engine import get_engine
|
from .engine import get_engine
|
||||||
@@ -97,7 +130,7 @@ Otherwise, a later call can return a maker that still references the old engine
|
|||||||
A small [`asynccontextmanager`](https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager) can make repository methods composable. It borrows an existing session when supplied; otherwise it creates and closes one from a supplied factory:
|
A small [`asynccontextmanager`](https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager) can make repository methods composable. It borrows an existing session when supplied; otherwise it creates and closes one from a supplied factory:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncGeneratorr
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
|
||||||
@@ -106,7 +139,7 @@ async def session_scope(
|
|||||||
*,
|
*,
|
||||||
database_url: str,
|
database_url: str,
|
||||||
session: AsyncSession | None = None,
|
session: AsyncSession | None = None,
|
||||||
) -> AsyncIterator[AsyncSession]:
|
) -> AsyncGenerator[AsyncSession]:
|
||||||
if session is not None:
|
if session is not None:
|
||||||
yield session
|
yield session
|
||||||
return
|
return
|
||||||
@@ -139,14 +172,29 @@ async def transaction_scope(
|
|||||||
*,
|
*,
|
||||||
database_url: str,
|
database_url: str,
|
||||||
session: AsyncSession | None = None,
|
session: AsyncSession | None = None,
|
||||||
) -> AsyncIterator[AsyncSession]:
|
) -> AsyncGenerator[AsyncSession]:
|
||||||
if session is not None:
|
if session is not None:
|
||||||
if not session.in_transaction():
|
if not session.in_transaction():
|
||||||
raise RuntimeError("A supplied session must have an active transaction")
|
raise RuntimeError("A supplied session must have an active transaction")
|
||||||
yield session
|
yield session
|
||||||
return
|
return
|
||||||
|
|
||||||
async with get_session_factory(database_url).begin() as owned_session:
|
session_factory = get_session_factory(database_url)
|
||||||
|
async with session_factory.begin() as owned_session:
|
||||||
|
yield owned_session
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, `begin()` is intentionally called on the [`async_sessionmaker`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.async_sessionmaker.begin), not on an existing `AsyncSession`. The related APIs have different ownership semantics:
|
||||||
|
|
||||||
|
- `session_factory()` creates a session whose lifetime the surrounding code must manage; it does not commit automatically.
|
||||||
|
- `session_factory.begin()` creates a new session and transaction together, commits on successful exit or rolls back on exceptional exit, and then closes the session.
|
||||||
|
- `session.begin()` manages a transaction on an existing session but does not own or close that session.
|
||||||
|
|
||||||
|
The factory form is equivalent in ownership terms to creating a session and then entering that session's transaction:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async with session_factory() as owned_session:
|
||||||
|
async with owned_session.begin():
|
||||||
yield owned_session
|
yield owned_session
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -179,7 +227,7 @@ Pass the database URL to repository constructors. The repository stores repeatab
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
async def find_item(session: AsyncSession, item_id: int) -> Item | None:
|
async def find_item(session: AsyncSession, item_id: int) -> Item | None:
|
||||||
@@ -229,12 +277,12 @@ This preserves atomicity without making repository objects hold mutable `AsyncSe
|
|||||||
## Canonical FastAPI Dependency Pattern
|
## Canonical FastAPI Dependency Pattern
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncGenerator
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
type SessionFactory = async_sessionmaker[AsyncSession]
|
type SessionFactory = async_sessionmaker[AsyncSession]
|
||||||
@@ -246,7 +294,7 @@ def resolve_session_factory(request: Request) -> SessionFactory:
|
|||||||
|
|
||||||
async def get_db_session(
|
async def get_db_session(
|
||||||
session_factory: SessionFactory = Depends(resolve_session_factory),
|
session_factory: SessionFactory = Depends(resolve_session_factory),
|
||||||
) -> AsyncIterator[AsyncSession]:
|
) -> AsyncGenerator[AsyncSession]:
|
||||||
async with session_factory() as session:
|
async with session_factory() as session:
|
||||||
yield session
|
yield session
|
||||||
```
|
```
|
||||||
@@ -255,7 +303,7 @@ Route usage:
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
from .session import get_db_session
|
from .session import get_db_session
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ SQLModel is designed for FastAPI, built on Pydantic and SQLAlchemy, and intended
|
|||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- Default to SQLModel for new table models and API data models.
|
- Default to SQLModel for new table models and API data models.
|
||||||
- Keep SQLAlchemy async primitives as the runtime base: `create_async_engine`, `async_sessionmaker`, and `AsyncSession`.
|
- Keep SQLAlchemy engine and factory primitives as the runtime base: `create_async_engine` and `async_sessionmaker`. For SQLModel applications, use SQLModel's `AsyncSession` wrapper so its typed `exec()` API remains available.
|
||||||
- Keep transaction and session ownership policies identical whether models are SQLAlchemy Declarative or SQLModel.
|
- Keep transaction and session ownership policies identical whether models are SQLAlchemy Declarative or SQLModel.
|
||||||
- Use SQLModel inheritance to share validated fields while keeping table, create, update, and public contracts distinct where their semantics differ.
|
- Use SQLModel inheritance to share validated fields while keeping table, create, update, and public contracts distinct where their semantics differ.
|
||||||
- Use SQLAlchemy declarative models only for a concrete unsupported mapping or third-party constraint; document the reason.
|
- Use SQLAlchemy declarative models only for a concrete unsupported mapping or third-party constraint; document the reason.
|
||||||
@@ -72,8 +72,9 @@ class UserRead(UserBase):
|
|||||||
### Pattern B: Keep SQLModel models with the async runtime
|
### Pattern B: Keep SQLModel models with the async runtime
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
engine = create_async_engine(settings.database_url, pool_pre_ping=True)
|
engine = create_async_engine(settings.database_url, pool_pre_ping=True)
|
||||||
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||||
@@ -82,7 +83,7 @@ async with session_factory() as session:
|
|||||||
users = (await session.scalars(select(User))).all()
|
users = (await session.scalars(select(User))).all()
|
||||||
```
|
```
|
||||||
|
|
||||||
`sqlmodel.select()` keeps SQLModel's typing-oriented statement construction, while `AsyncSession.scalars()` and the surrounding lifecycle come from SQLAlchemy.
|
`sqlmodel.select()` keeps SQLModel's typing-oriented statement construction, and SQLModel's `AsyncSession` adds typed `exec()` results while retaining SQLAlchemy's async lifecycle and transaction behavior. Import `AsyncSession` from `sqlmodel.ext.asyncio.session` when working with SQLModel models; use SQLAlchemy's `AsyncSession` only when the code intentionally has no SQLModel dependency.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
# Testing Database Targets and Data
|
||||||
|
|
||||||
|
Use the same application database construction path in production and tests. Tests select a different URL and bind their request-session dependency to a test-scoped transaction; they do not replace repositories, services, or SQLAlchemy mechanics with mocks.
|
||||||
|
|
||||||
|
## Decision Table
|
||||||
|
|
||||||
|
| Test need | Database target | Isolation approach | What it proves |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Fast, serial application tests | `sqlite+aiosqlite://` | Per-test engine or outer transaction | ORM mappings and ordinary application behavior |
|
||||||
|
| Async code using multiple simultaneous sessions | Named SQLite shared-cache URL or temporary SQLite file | Per-test schema or cleanup strategy | Concurrent-session behavior without a database server |
|
||||||
|
| PostgreSQL-specific behavior | Dedicated PostgreSQL test database | Per-test outer transaction and SAVEPOINT | SQL, constraints, types, locking, and migrations that SQLite cannot represent |
|
||||||
|
|
||||||
|
SQLite is a useful fast target, not a drop-in PostgreSQL substitute. Keep a small PostgreSQL integration suite for PostgreSQL-specific queries, extensions, row locking, JSON semantics, collations, isolation, and migration validation.
|
||||||
|
|
||||||
|
## One Construction Path
|
||||||
|
|
||||||
|
Make the application factory accept a database URL or settings object, and keep engine and session-factory construction in one function. The only test-specific inputs should be the URL and, for request tests, the session dependency override.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
|
def create_database(
|
||||||
|
database_url: str,
|
||||||
|
) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]:
|
||||||
|
engine = create_async_engine(database_url)
|
||||||
|
session_factory = async_sessionmaker(
|
||||||
|
engine,
|
||||||
|
class_=AsyncSession,
|
||||||
|
expire_on_commit=False,
|
||||||
|
)
|
||||||
|
return engine, session_factory
|
||||||
|
```
|
||||||
|
|
||||||
|
Production passes its `postgresql+asyncpg://...` URL to `create_database()`. A local SQLite run passes `sqlite+aiosqlite:///./app.db`. Tests pass a dedicated test URL to the same function. Do not create an engine during module import: that makes it easy for tests to accidentally retain the production URL before an override is applied.
|
||||||
|
|
||||||
|
Use migrations to provision an integration database when migrations are part of the release contract. `metadata.create_all()` is appropriate for focused ORM tests only when it accurately represents the schema under test. Import all table models before creating metadata; [SQLModel documents that model-registration order matters](https://sqlmodel.tiangolo.com/tutorial/fastapi/tests/#import-table-models).
|
||||||
|
|
||||||
|
## Transactional Async Fixture
|
||||||
|
|
||||||
|
For tests that exercise code which calls `commit()`, start an outer transaction on one test connection. Bind the test `AsyncSession` to that connection and use `join_transaction_mode="create_savepoint"`. SQLAlchemy documents this as its test-suite pattern: session commits resolve a SAVEPOINT while fixture teardown rolls back the outer transaction.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from collections.abc import AsyncGeneratorr
|
||||||
|
|
||||||
|
import pytest_asyncio
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncEngine
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def session(test_engine: AsyncEngine) -> AsyncGenerator[AsyncSession]:
|
||||||
|
async with test_engine.connect() as connection:
|
||||||
|
transaction = await connection.begin()
|
||||||
|
test_session = AsyncSession(
|
||||||
|
bind=connection,
|
||||||
|
expire_on_commit=False,
|
||||||
|
join_transaction_mode="create_savepoint",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
yield test_session
|
||||||
|
finally:
|
||||||
|
await test_session.close()
|
||||||
|
await transaction.rollback()
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the test session through the normal FastAPI dependency seam, and always remove the override after the test. [FastAPI dependency overrides](https://fastapi.tiangolo.com/advanced/testing-dependencies/) are an application-level dictionary, so leaving one installed leaks test state.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pytest
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app_with_test_session(
|
||||||
|
app: FastAPI,
|
||||||
|
session: AsyncSession,
|
||||||
|
) -> FastAPI:
|
||||||
|
async def get_test_session() -> AsyncGenerator[AsyncSession]:
|
||||||
|
yield session
|
||||||
|
|
||||||
|
app.dependency_overrides[get_session] = get_test_session
|
||||||
|
try:
|
||||||
|
yield app
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
```
|
||||||
|
|
||||||
|
This fixture is deliberately serial: one mutable `AsyncSession` must not serve concurrent tasks. A test that verifies concurrently active sessions should create independent sessions from a factory and use a database target that supports independent connections.
|
||||||
|
|
||||||
|
## SQLite Targets
|
||||||
|
|
||||||
|
### Serial in-memory tests
|
||||||
|
|
||||||
|
Use `sqlite+aiosqlite://` for a fresh in-memory database when the test runs all database work serially. SQLAlchemy's `aiosqlite` dialect uses a single-connection `StaticPool` for this target, so all sessions share one SQLite transaction state. One session's rollback can discard another session's uncommitted work.
|
||||||
|
|
||||||
|
Create the schema and dispose the engine deterministically:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pytest_asyncio
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncEngine
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def test_engine() -> AsyncGenerator[AsyncEngine]:
|
||||||
|
engine, _ = create_database("sqlite+aiosqlite://")
|
||||||
|
async with engine.begin() as connection:
|
||||||
|
await connection.run_sync(SQLModel.metadata.create_all)
|
||||||
|
try:
|
||||||
|
yield engine
|
||||||
|
finally:
|
||||||
|
await engine.dispose()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Concurrent in-memory tests
|
||||||
|
|
||||||
|
Do not use the default `:memory:` target for tests that have multiple active sessions or tasks. Use a named shared-cache database instead, with a name unique to the test process:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sqlite+aiosqlite:///file:test-suite?mode=memory&cache=shared&uri=true
|
||||||
|
```
|
||||||
|
|
||||||
|
This lets connections share the same in-memory database while retaining independent transaction state. A temporary file URL such as `sqlite+aiosqlite:////tmp/test.db` is often simpler when test isolation or cleanup tooling already manages files.
|
||||||
|
|
||||||
|
For both SQLite forms, enable and test the constraints your application depends on. SQLite foreign-key enforcement is disabled by default, and its transaction behavior has driver-specific differences. Keep PostgreSQL integration coverage for behavior that SQLite cannot faithfully model.
|
||||||
|
|
||||||
|
## Test Data Practices
|
||||||
|
|
||||||
|
- Build only the data a test needs, through named factory functions or pytest fixtures rather than a large global seed.
|
||||||
|
- Give each fixture a domain meaning, such as `active_account`, `expired_subscription`, or `admin_user`; avoid opaque rows with unexplained defaults.
|
||||||
|
- Set values relevant to the assertion explicitly, including timestamps, permissions, statuses, and unique identifiers. Use fixed clocks or injected clock values instead of the wall clock.
|
||||||
|
- Construct object graphs through relationships, then `await session.flush()` before reading generated identifiers or passing foreign keys onward. `flush()` exercises database constraints without ending the test transaction.
|
||||||
|
- Seed prerequisite data before creating a client request. Let the endpoint own the mutation being asserted; do not pre-insert the row that the endpoint is supposed to create.
|
||||||
|
- Use `commit()` in fixture setup only when the test specifically needs to prove post-commit behavior. With the transactional fixture, this remains isolated through the outer rollback.
|
||||||
|
- Keep shared reference data immutable and explicit. If it must be reused for performance, load it once into a dedicated test database and reset all mutable tables between tests; never depend on test order.
|
||||||
|
- Include both valid and constraint-breaking graphs where a behavior depends on foreign keys, uniqueness, nullability, or cascading deletes. SQLite-only tests should not be the sole evidence for PostgreSQL constraints.
|
||||||
|
|
||||||
|
## Completion Checks
|
||||||
|
|
||||||
|
- A test run cannot reach the production URL; production credentials are absent from the test environment.
|
||||||
|
- Production PostgreSQL, local SQLite, and in-memory SQLite all use the same engine/session-factory construction path.
|
||||||
|
- Every test owns its override, connection, transaction, session, and engine cleanup.
|
||||||
|
- Test data is deterministic, minimal, and expresses the scenario under test.
|
||||||
|
- PostgreSQL integration tests cover every PostgreSQL-specific contract and run against migrations where migrations are shipped.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- [SQLAlchemy: joining a session into an external transaction](https://docs.sqlalchemy.org/en/21/orm/session_transaction.html#joining-a-session-into-an-external-transaction-such-as-for-test-suites)
|
||||||
|
- [SQLAlchemy asyncio extension](https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html)
|
||||||
|
- [SQLAlchemy SQLite dialect and async in-memory pooling](https://docs.sqlalchemy.org/en/21/dialects/sqlite.html#using-a-memory-database-with-multiple-coroutines)
|
||||||
|
- [FastAPI dependency overrides](https://fastapi.tiangolo.com/advanced/testing-dependencies/)
|
||||||
|
- [SQLModel testing with FastAPI](https://sqlmodel.tiangolo.com/tutorial/fastapi/tests/)
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
---
|
|
||||||
name: nicegui-ui-customization
|
|
||||||
description: 'Design and implement production NiceGUI UIs with reusable components, Tailwind-first styling, event-driven interactions, and troubleshooting for uploads, state, and static assets. Use when building or refactoring NiceGUI pages and interaction flows.'
|
|
||||||
x-personal-mcp:
|
|
||||||
id: nicegui-ui-customization
|
|
||||||
version: 1.0.0
|
|
||||||
tags:
|
|
||||||
- nicegui
|
|
||||||
- fastapi
|
|
||||||
- ui
|
|
||||||
- customization
|
|
||||||
- frontend
|
|
||||||
capabilities:
|
|
||||||
- resource://skills/nicegui-ui-customization/document
|
|
||||||
---
|
|
||||||
|
|
||||||
# NiceGUI UI Customization Workflow
|
|
||||||
|
|
||||||
Create, style, and ship production NiceGUI UI flows with a repeatable process. The workflow keeps structure in Python, favors Tailwind and Quasar APIs for styling, and uses event-driven interaction patterns over ad-hoc polling.
|
|
||||||
|
|
||||||
## When To Use
|
|
||||||
|
|
||||||
- Building a new NiceGUI page or dashboard
|
|
||||||
- Refactoring a page into reusable components
|
|
||||||
- Adding file upload, form submission, live status, or background-job UX
|
|
||||||
- Troubleshooting race conditions, stale assets, or inconsistent state updates
|
|
||||||
|
|
||||||
## Target Outcome
|
|
||||||
|
|
||||||
Deliver a responsive, accessible UI flow that:
|
|
||||||
|
|
||||||
- keeps clear boundaries between page adapters, reusable components, and services
|
|
||||||
- uses Tailwind-first styling with minimal custom CSS
|
|
||||||
- updates UI through events and bindings
|
|
||||||
- has validation, user feedback, and failure handling
|
|
||||||
- passes a production-readiness check at the end
|
|
||||||
|
|
||||||
## Progressive Loading References
|
|
||||||
|
|
||||||
Load these references only when needed:
|
|
||||||
|
|
||||||
- Architecture and styling rules: [architecture and styling](./references/architecture-and-styling.md)
|
|
||||||
- Event and state interaction patterns: [interaction patterns](./references/interaction-patterns.md)
|
|
||||||
- Troubleshooting and release gates: [troubleshooting and quality gates](./references/troubleshooting-and-quality-gates.md)
|
|
||||||
|
|
||||||
## Procedure
|
|
||||||
|
|
||||||
### 1. Define the UI Slice
|
|
||||||
|
|
||||||
- Capture the user-visible outcome for this task in one sentence.
|
|
||||||
- Identify route-level page modules to touch.
|
|
||||||
- Identify service operations needed by the UI.
|
|
||||||
|
|
||||||
Completion check:
|
|
||||||
|
|
||||||
- You can name the target page, component candidates, and service calls before coding.
|
|
||||||
|
|
||||||
### 2. Choose Component Extraction Strategy
|
|
||||||
|
|
||||||
Decision point:
|
|
||||||
|
|
||||||
- If a layout pattern appears in 2 or more pages, extract it to `ui/components/`.
|
|
||||||
- If a pattern is page-specific, keep it in the page module.
|
|
||||||
|
|
||||||
Completion check:
|
|
||||||
|
|
||||||
- Reused UI patterns are encapsulated as callable components.
|
|
||||||
|
|
||||||
### 3. Build Responsive Layout First
|
|
||||||
|
|
||||||
- Use Tailwind utility classes for structure and spacing.
|
|
||||||
- Use responsive breakpoints (`sm:`, `md:`, `lg:`).
|
|
||||||
- Reserve `.style()` for dynamic values that cannot be expressed with classes.
|
|
||||||
|
|
||||||
Completion check:
|
|
||||||
|
|
||||||
- Layout works at mobile and desktop widths without custom CSS overrides.
|
|
||||||
|
|
||||||
### 4. Add Reactive State And Events
|
|
||||||
|
|
||||||
- Use bindable dataclasses for local page state.
|
|
||||||
- Prefer event handlers (`on_click`, `on_upload`, etc.) over periodic polling.
|
|
||||||
- Trigger explicit refreshes with `@ui.refreshable` where needed.
|
|
||||||
|
|
||||||
Decision point by interaction type:
|
|
||||||
|
|
||||||
- File upload: validate size/type, delegate storage to a service, notify success/failure.
|
|
||||||
- Form submit: bind inputs to dataclass fields, validate in service layer, clear state on success.
|
|
||||||
- Real-time status: use SSE or WebSocket for push updates.
|
|
||||||
- Long jobs: run in background task, update status endpoint or stream.
|
|
||||||
|
|
||||||
Completion check:
|
|
||||||
|
|
||||||
- Every user action has explicit positive and negative feedback via `ui.notify()`.
|
|
||||||
|
|
||||||
### 5. Apply Styling Strategy
|
|
||||||
|
|
||||||
Preferred order:
|
|
||||||
|
|
||||||
1. Tailwind utility classes
|
|
||||||
2. Quasar props
|
|
||||||
3. Reusable styled component functions
|
|
||||||
|
|
||||||
Only if absolutely necessary:
|
|
||||||
|
|
||||||
- Load minimal custom CSS once at startup in `bootstrap.py`.
|
|
||||||
- Keep custom CSS tokenized (variables) and documented.
|
|
||||||
|
|
||||||
Completion check:
|
|
||||||
|
|
||||||
- Styling is mostly class/props-driven and not dependent on scattered ad-hoc CSS.
|
|
||||||
|
|
||||||
### 6. Harden Against Common Failures
|
|
||||||
|
|
||||||
- Prevent duplicate submissions by disabling controls during in-flight operations.
|
|
||||||
- Avoid overlapping timers for the same state target.
|
|
||||||
- Serialize dependent updates (`await` service call before mutation/render).
|
|
||||||
- Verify static mount paths and cache behavior for changed assets.
|
|
||||||
|
|
||||||
Completion check:
|
|
||||||
|
|
||||||
- Race conditions and stale asset symptoms are addressed with explicit safeguards.
|
|
||||||
|
|
||||||
### 7. Final Production Readiness Review
|
|
||||||
|
|
||||||
Pass all checks:
|
|
||||||
|
|
||||||
- Structure: pages, components, services follow one-way dependency flow.
|
|
||||||
- Responsiveness: tested at small and large viewport widths.
|
|
||||||
- Accessibility: labels, button text, and action visibility are clear.
|
|
||||||
- Reliability: validation and exception paths produce user-facing notifications.
|
|
||||||
- Maintainability: repeated UI patterns are extracted; business logic stays in services.
|
|
||||||
|
|
||||||
If any check fails, return to the relevant step and iterate.
|
|
||||||
|
|
||||||
## Completion Contract
|
|
||||||
|
|
||||||
This workflow is complete when:
|
|
||||||
|
|
||||||
- the page flow meets the target outcome
|
|
||||||
- architecture boundaries are preserved
|
|
||||||
- chosen interaction pattern is implemented with explicit success and failure feedback
|
|
||||||
- troubleshooting checks pass
|
|
||||||
- production-readiness gate passes
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
# Architecture and Styling Reference
|
|
||||||
|
|
||||||
## Project Boundaries
|
|
||||||
|
|
||||||
Use this dependency direction:
|
|
||||||
|
|
||||||
- pages import components and services
|
|
||||||
- components contain presentation logic only
|
|
||||||
- services contain business logic and do not import UI
|
|
||||||
- static assets are mounted and loaded once at bootstrap
|
|
||||||
|
|
||||||
Suggested module split:
|
|
||||||
|
|
||||||
```text
|
|
||||||
src/app/
|
|
||||||
ui/pages/
|
|
||||||
ui/components/
|
|
||||||
ui/static/
|
|
||||||
services/
|
|
||||||
api/
|
|
||||||
bootstrap.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Component Extraction Rules
|
|
||||||
|
|
||||||
Extract to ui/components when a pattern appears in two or more pages.
|
|
||||||
|
|
||||||
Keep in-page if the layout is specific to a single route.
|
|
||||||
|
|
||||||
```python
|
|
||||||
def card_section(title: str, content: str) -> ui.card:
|
|
||||||
with ui.card().classes("w-full max-w-md") as card:
|
|
||||||
ui.label(title).classes("text-lg font-bold")
|
|
||||||
ui.label(content).classes("text-gray-600")
|
|
||||||
return card
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tailwind-First Layout Pattern
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
1. Tailwind utility classes
|
|
||||||
2. Quasar props
|
|
||||||
3. Reusable styled component functions
|
|
||||||
4. Minimal custom CSS loaded once at bootstrap (only when needed)
|
|
||||||
|
|
||||||
```python
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
|
|
||||||
app.mount("/static", StaticFiles(directory="src/app/static"), name="static")
|
|
||||||
ui.add_css(open("src/app/static/css/base.css").read())
|
|
||||||
```
|
|
||||||
|
|
||||||
## Static Asset Rules
|
|
||||||
|
|
||||||
- Keep custom CSS small and tokenized with variables.
|
|
||||||
- Avoid per-page CSS injection.
|
|
||||||
- Verify static mount paths and reverse proxy rewrites.
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
!!! info "Primary sources"
|
|
||||||
- [NiceGUI elements](https://nicegui.io/documentation/element)
|
|
||||||
- [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)
|
|
||||||
+104
-153
@@ -1,206 +1,157 @@
|
|||||||
---
|
---
|
||||||
name: nicegui
|
name: nicegui
|
||||||
description: 'Design and scaffold a production-ready NiceGUI + FastAPI application architecture. Use for multi-page app planning, package boundaries, optional DB/LangGraph/docs integration, and implementation checklists.'
|
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: 1.0.0
|
version: 2.5.0
|
||||||
tags:
|
tags:
|
||||||
- nicegui
|
- nicegui
|
||||||
- fastapi
|
- fastapi
|
||||||
|
- asgi
|
||||||
|
- uvicorn
|
||||||
|
- pydantic-settings
|
||||||
|
- configuration
|
||||||
|
- deployment
|
||||||
- ui
|
- ui
|
||||||
- architecture
|
- architecture
|
||||||
|
- scaffolding
|
||||||
|
- customization
|
||||||
|
- frontend
|
||||||
|
- testing
|
||||||
|
- source-docs
|
||||||
capabilities:
|
capabilities:
|
||||||
- resource://skills/nicegui/document
|
- resource://skills/nicegui/document
|
||||||
---
|
---
|
||||||
|
|
||||||
# NiceGUI
|
# NiceGUI Reference
|
||||||
|
|
||||||
Design a production-minded NiceGUI + FastAPI architecture with clear boundaries, optional extensions, and a concrete implementation checklist.
|
Use this skill as a progressive reference for NiceGUI applications built with FastAPI. Start with the routing map, load only the material needed for the current question, and reconcile it with the target project's NiceGUI version and established conventions.
|
||||||
|
|
||||||
## When to Use
|
## When to Use
|
||||||
|
|
||||||
- You need a reusable architecture plan before implementing a NiceGUI app.
|
- Planning or reviewing NiceGUI application structure and FastAPI composition.
|
||||||
- You want FastAPI app-factory structure and lifespan wiring.
|
- Building or refactoring pages, components, layouts, and static assets.
|
||||||
- You need optional guidance for database, LangGraph workflows, or mounted static docs.
|
- Modeling UI state with bindings or bindable dataclasses.
|
||||||
- You want output that is concise, structured, and implementation-ready.
|
- Implementing forms, uploads, refreshes, live updates, or background work.
|
||||||
|
- Diagnosing UI state, concurrency, navigation, or asset problems.
|
||||||
|
- Verifying framework behavior against primary documentation.
|
||||||
|
|
||||||
## Inputs to Collect
|
## How to Use This Skill
|
||||||
|
|
||||||
Collect these inputs up front. If not provided, make safe defaults and state assumptions.
|
1. Classify the request using the discovery map below.
|
||||||
|
2. Load the smallest relevant reference, or at most two references for a mixed concern.
|
||||||
|
3. Inspect the target repository before applying guidance; preserve its sound local patterns.
|
||||||
|
4. Check the pinned NiceGUI and integration versions before relying on version-specific APIs.
|
||||||
|
5. Validate the changed behavior with focused tests and, for UI work, relevant viewport checks.
|
||||||
|
|
||||||
- Product scope and primary user journeys.
|
## Progressive Discovery Map
|
||||||
- Required pages and route map.
|
|
||||||
- Whether persistent data is required.
|
|
||||||
- Whether AI orchestration (multi-step, streaming, approvals) is required.
|
|
||||||
- Whether generated docs should be mounted in-app.
|
|
||||||
- Runtime/deployment constraints (single service vs split services, environment requirements).
|
|
||||||
|
|
||||||
## Outcome
|
### Application Architecture
|
||||||
|
|
||||||
Produce:
|
Load [application architecture](./references/architecture.md) for:
|
||||||
|
|
||||||
- A concise architecture explanation.
|
- FastAPI app factories and lifespan ownership
|
||||||
- How core services, UI pages, and UI components fit together.
|
- package boundaries and dependency direction
|
||||||
- Explicit decision on DB ownership or involvement.
|
- page registration and health routes
|
||||||
- Explicit decision on AI workflow (or no AI).
|
- optional persistence, LangGraph, or mounted documentation
|
||||||
- A checklist implementation plan organized by package and domain.
|
- async responsiveness and baseline tests
|
||||||
|
|
||||||
## Procedure
|
### FastAPI And Uvicorn Startup
|
||||||
|
|
||||||
1. Frame the baseline architecture.
|
Load [FastAPI and Uvicorn startup](./references/fastapi-uvicorn-startup.md) for:
|
||||||
2. Choose optional extensions (DB, AI, docs) using decision points below.
|
|
||||||
3. Map modules, dependencies, and key boundaries.
|
|
||||||
4. Define async behavior and UI responsiveness expectations.
|
|
||||||
5. Define key functions/classes and configuration surfaces.
|
|
||||||
6. Produce phased checklist with rollout or migration notes when relevant.
|
|
||||||
7. Run completion checks before returning.
|
|
||||||
|
|
||||||
### 1) Baseline architecture
|
- 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
|
||||||
|
|
||||||
Use a src-layout with FastAPI as the ASGI app and NiceGUI registered via composition.
|
### Components And Styling
|
||||||
|
|
||||||
- App factory pattern: `create_app()`.
|
Load [architecture and styling](./references/architecture-and-styling.md) for:
|
||||||
- Lifespan for startup and shutdown resource management.
|
|
||||||
- `api/` for HTTP handlers, `services/` for business logic.
|
|
||||||
- `ui/pages/` for page modules, `ui/components/` for shared UI.
|
|
||||||
- Health endpoint on FastAPI side: `/healthz`.
|
|
||||||
|
|
||||||
Recommended base shape:
|
- page, component, and service boundaries
|
||||||
|
- component extraction decisions
|
||||||
|
- Quasar props, Tailwind utilities, and custom CSS boundaries
|
||||||
|
- responsive layout and static asset conventions
|
||||||
|
- Tailwind and Quasar breakpoint scales, container queries, and responsive testing
|
||||||
|
- 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
|
||||||
|
|
||||||
```text
|
### Bindable State
|
||||||
.
|
|
||||||
├─ pyproject.toml
|
|
||||||
├─ .env.example
|
|
||||||
├─ README.md
|
|
||||||
├─ src/
|
|
||||||
│ └─ app/
|
|
||||||
│ ├─ __init__.py
|
|
||||||
│ ├─ main.py
|
|
||||||
│ ├─ bootstrap.py
|
|
||||||
│ ├─ config.py
|
|
||||||
│ ├─ logging.py
|
|
||||||
│ ├─ api/
|
|
||||||
│ │ ├─ __init__.py
|
|
||||||
│ │ └─ health.py
|
|
||||||
│ ├─ services/
|
|
||||||
│ │ ├─ __init__.py
|
|
||||||
│ │ └─ example_service.py
|
|
||||||
│ └─ ui/
|
|
||||||
│ ├─ __init__.py
|
|
||||||
│ ├─ components/
|
|
||||||
│ │ ├─ __init__.py
|
|
||||||
│ │ └─ nav.py
|
|
||||||
│ └─ pages/
|
|
||||||
│ ├─ __init__.py
|
|
||||||
│ ├─ home.py
|
|
||||||
│ ├─ dashboard.py
|
|
||||||
│ └─ about.py
|
|
||||||
└─ tests/
|
|
||||||
├─ test_health.py
|
|
||||||
└─ test_pages_registration.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2) Decision points
|
Load [bindable dataclasses](./references/binding-dataclasses.md) for:
|
||||||
|
|
||||||
#### Database needed?
|
- typed local UI state
|
||||||
|
- propagation and refresh behavior
|
||||||
|
- nested structures and strict bindings
|
||||||
|
- mutable defaults, performance, and version notes
|
||||||
|
|
||||||
- If no: keep `services/` pure and skip persistence layers.
|
### Interaction Patterns
|
||||||
- If yes: add `db/` package with engine/session/model/repository layering.
|
|
||||||
- Prefer one process-level engine and request-scoped sessions via `yield`.
|
|
||||||
- Prefer Alembic migrations for schema changes.
|
|
||||||
|
|
||||||
#### AI workflow needed?
|
Load [interaction patterns](./references/interaction-patterns.md) for:
|
||||||
|
|
||||||
- If no: keep `services/` focused on app logic only.
|
- uploads and form submission
|
||||||
- If yes: add `ai/` package (state, nodes, graph, runtime, contracts).
|
- explicit refreshes
|
||||||
- Keep graph internals out of `ui/pages/` and API handlers.
|
- server-sent events and WebSockets
|
||||||
- Use stable thread/session IDs for resumable flows.
|
- background work and duplicate-submission guards
|
||||||
|
|
||||||
#### Mounted docs needed?
|
### Troubleshooting And Quality
|
||||||
|
|
||||||
- If no: skip docs mounting.
|
Load [troubleshooting and quality gates](./references/troubleshooting-and-quality-gates.md) for:
|
||||||
- If yes: mount generated static site under configurable route (default `/docs`).
|
|
||||||
- Keep docs mounting in composition layer, not page modules.
|
|
||||||
|
|
||||||
### 3) Page and component registration
|
- upload failures and UI race conditions
|
||||||
|
- stale assets and navigation drift
|
||||||
|
- responsiveness, accessibility, reliability, and maintainability checks
|
||||||
|
|
||||||
- Require at minimum page modules for `/`, `/dashboard`, `/about`.
|
### Primary Sources
|
||||||
- Prefer explicit registration pattern:
|
|
||||||
- `ui/pages/__init__.py` exports `register_pages()`.
|
|
||||||
- Each page module exports `register_page()`.
|
|
||||||
- Shared shell components (header/nav/drawer) live in `ui/components/`.
|
|
||||||
|
|
||||||
### 4) Dependency direction rules
|
Load [source documentation](./references/source-documentation.md) when:
|
||||||
|
|
||||||
Prefer:
|
- behavior is version-sensitive or uncertain
|
||||||
|
- an integration recommendation needs verification
|
||||||
|
- upstream NiceGUI, FastAPI, Tailwind, Quasar, SQLAlchemy, Pydantic, or LangGraph documentation is required
|
||||||
|
|
||||||
- `main/bootstrap` -> `config/logging` + `api` + `ui/pages` + `services`
|
## Common Discovery Paths
|
||||||
- `api` -> `services`
|
|
||||||
- `ui/pages` -> `ui/components` + `services`
|
|
||||||
- `services` -> helpers/clients (and `db/` when enabled)
|
|
||||||
|
|
||||||
Avoid reverse imports from services into API or UI modules.
|
### New Application Or Architecture Review
|
||||||
|
|
||||||
### 5) Async and UI responsiveness rules
|
1. Load [application architecture](./references/architecture.md).
|
||||||
|
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.
|
||||||
|
|
||||||
- Prefer `async def` for page handlers, service methods, and integrations when the call path includes I/O.
|
### Page Or Component Work
|
||||||
- Use non-blocking clients/libraries where possible so long-running I/O does not freeze UI updates.
|
|
||||||
- Do not run blocking calls (`time.sleep`, blocking HTTP/database clients) in UI event handlers.
|
|
||||||
- For heavy CPU work, offload to worker/background execution and keep the UI loop free.
|
|
||||||
- Show progress states for long actions (disable action button, show spinner/progress text, re-enable on completion).
|
|
||||||
- Stream or chunk incremental results to the UI when workflows are multi-step or long-running.
|
|
||||||
- Keep cancellation and timeout behavior explicit for user-triggered long tasks.
|
|
||||||
- Ensure exceptions from async tasks are surfaced with user-friendly feedback and logged for diagnostics.
|
|
||||||
|
|
||||||
### 6) Testing minimums
|
1. Load [architecture and styling](./references/architecture-and-styling.md).
|
||||||
|
2. Add [interaction patterns](./references/interaction-patterns.md) or [bindable dataclasses](./references/binding-dataclasses.md) according to the page behavior.
|
||||||
|
|
||||||
- Test FastAPI health route behavior.
|
### Debugging Or Production Review
|
||||||
- Test page registration wiring.
|
|
||||||
- If DB enabled: session lifecycle and rollback behavior tests.
|
|
||||||
- If AI enabled: graph happy path and interrupt/resume coverage.
|
|
||||||
- If docs enabled: mounted docs route returns index page.
|
|
||||||
- For async flows: test long-running actions preserve UI responsiveness (loading state, completion state, and error state).
|
|
||||||
|
|
||||||
### 7) Styling architecture
|
1. Start with [troubleshooting and quality gates](./references/troubleshooting-and-quality-gates.md).
|
||||||
|
2. Follow the symptom to one detailed reference.
|
||||||
|
3. Confirm uncertain behavior in [source documentation](./references/source-documentation.md).
|
||||||
|
|
||||||
- Keep structure and layout in Python modules using NiceGUI class composition.
|
## General Defaults
|
||||||
- Keep visual polish in shared CSS files, loaded once at startup.
|
|
||||||
- Prefer semantic reusable classes over ad hoc per-page styling.
|
|
||||||
|
|
||||||
## Completion Checks
|
- Keep composition, transport, services, pages, and components directionally separated.
|
||||||
|
- Keep business logic out of UI components and event handlers.
|
||||||
|
- Avoid blocking I/O and CPU-heavy work in the UI event loop.
|
||||||
|
- Prefer event-driven updates and explicit refreshes over unrelated polling.
|
||||||
|
- Prefer Tailwind utilities, then Quasar props, then reusable component helpers; use minimal shared CSS when those are insufficient.
|
||||||
|
- Provide loading, success, and failure states for user-triggered work.
|
||||||
|
- Treat version-specific guidance as a prompt to verify the project's dependency version.
|
||||||
|
|
||||||
- Uses app factory and FastAPI lifespan.
|
## Reference Use Contract
|
||||||
- Pages are modularized (not single-file UI).
|
|
||||||
- Health endpoint exists on FastAPI side.
|
|
||||||
- Dependency direction is clean and one-way.
|
|
||||||
- Async-first guidance is applied where I/O exists, with explicit non-blocking UX states.
|
|
||||||
- Optional DB/AI/docs decisions are explicit and reflected in structure.
|
|
||||||
- Output includes architecture summary and package-organized checklist.
|
|
||||||
|
|
||||||
## Output Contract
|
When applying this skill:
|
||||||
|
|
||||||
Return:
|
- return only guidance relevant to the current task
|
||||||
|
- distinguish repository facts from reference recommendations
|
||||||
- Concise high-level architecture.
|
- cite the appropriate source reference for framework-level claims
|
||||||
- How core services, pages, and shared components fit.
|
- state assumptions when application requirements are missing
|
||||||
- DB involvement and ownership stance.
|
- report the focused checks used to validate implementation changes
|
||||||
- AI workflow stance and runtime flow.
|
|
||||||
- Checklist plan by package and domain:
|
|
||||||
- key functions/classes
|
|
||||||
- settings/config surfaces
|
|
||||||
- rollout/migration notes (when relevant)
|
|
||||||
|
|
||||||
## Guardrails
|
|
||||||
|
|
||||||
- Do not collapse all pages into one file.
|
|
||||||
- Do not use globals or implicit global side effects.
|
|
||||||
- Do not block UI event handlers with synchronous I/O or long CPU tasks.
|
|
||||||
- Always define loading/progress/error states for long user-triggered actions.
|
|
||||||
- Keep code minimal but production-minded.
|
|
||||||
- Prefer clarity and maintainability over clever abstractions.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- Architecture and integration details: [NiceGUI architecture reference](./references/architecture.md)
|
|
||||||
- Dataclass binding deep dive: [Bindable dataclasses in NiceGUI](./references/binding-dataclasses.md)
|
|
||||||
- Source documentation links: [NiceGUI source documentation](./references/source-documentation.md)
|
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
# NiceGUI Page Layout And Styling
|
||||||
|
|
||||||
|
Use this reference to structure NiceGUI pages, choose component boundaries, apply responsive layout, and introduce custom CSS without fighting Quasar's internal geometry.
|
||||||
|
|
||||||
|
## Ownership And Dependency Boundaries
|
||||||
|
|
||||||
|
Keep dependencies flowing in one direction:
|
||||||
|
|
||||||
|
- pages import components and services
|
||||||
|
- components contain presentation logic only
|
||||||
|
- services contain business logic and do not import UI
|
||||||
|
- bootstrap code mounts static assets and loads shared CSS once
|
||||||
|
|
||||||
|
Suggested module split:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/my_app/
|
||||||
|
ui/
|
||||||
|
pages/
|
||||||
|
components/
|
||||||
|
static/
|
||||||
|
services/
|
||||||
|
api/
|
||||||
|
```
|
||||||
|
|
||||||
|
Page modules should compose a route from reusable presentation and service calls. They should not own domain rules, persistence, or long-running synchronous work.
|
||||||
|
|
||||||
|
## Page Composition
|
||||||
|
|
||||||
|
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
|
||||||
|
def card_section(title: str, content: str) -> ui.card:
|
||||||
|
with ui.card().classes("w-full max-w-md") as card:
|
||||||
|
ui.label(title).classes("text-lg font-bold")
|
||||||
|
ui.label(content).classes("text-gray-600")
|
||||||
|
return card
|
||||||
|
```
|
||||||
|
|
||||||
|
Reusable components should accept data and event callbacks rather than import page state or business services implicitly.
|
||||||
|
|
||||||
|
## Styling Decision Order
|
||||||
|
|
||||||
|
NiceGUI wraps Quasar components. Choose the styling mechanism according to what it owns:
|
||||||
|
|
||||||
|
1. Use Quasar props for component appearance, density, labels, and popup behavior.
|
||||||
|
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
|
||||||
|
|
||||||
|
Support these layouts only:
|
||||||
|
|
||||||
|
- mobile: a single-column layout with wrapping toolbars and full-width controls
|
||||||
|
- landscape desktop: $1920 \times 1080$ with side-by-side panels where they improve scanning
|
||||||
|
- portrait desktop: $1080 \times 1920 with stacked panels or a narrow fixed sidebar
|
||||||
|
|
||||||
|
Build the mobile layout first, then add one desktop breakpoint when a row or grid needs more space. Prefer flex wrapping and fluid grids before adding another breakpoint. Use Tailwind classes for page layout and Quasar props for component behavior.
|
||||||
|
|
||||||
|
```python
|
||||||
|
with ui.row().classes('w-full flex-wrap gap-4 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 `min-w-0` for flexible children, `flex-wrap` for toolbars, and `max-w-* mx-auto` to keep portrait layouts readable. Do not add device-specific component trees, container queries, or custom breakpoints unless a supported layout demonstrates a concrete failure.
|
||||||
|
|
||||||
|
## Static Assets And Shared CSS
|
||||||
|
|
||||||
|
- 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:
|
||||||
|
|
||||||
|
\[
|
||||||
|
\begin{aligned}
|
||||||
|
h_{\mathrm{pre}} &= \frac{h_{\mathrm{visible}}}{s} \\
|
||||||
|
\text{where } s &= \text{the zoom scale}
|
||||||
|
\end{aligned}
|
||||||
|
\]
|
||||||
|
|
||||||
|
For a desired visual height of `90dvh` at \(1.2\times\):
|
||||||
|
|
||||||
|
\[
|
||||||
|
\frac{90\,\mathrm{dvh}}{1.2} = 75\,\mathrm{dvh}
|
||||||
|
\]
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Check each completed page at these three viewports:
|
||||||
|
|
||||||
|
1. A representative mobile viewport, such as $390 \times 844$.
|
||||||
|
2. Landscape desktop at $1920 \times 1080$.
|
||||||
|
3. Portrait desktop at $1080 \times 1920$.
|
||||||
|
|
||||||
|
Confirm that page sections do not overlap, toolbars wrap on mobile, desktop panels use the available space without becoming excessively wide, and dialogs remain visible and scroll to their final field.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
!!! info "Primary sources"
|
||||||
|
- [NiceGUI element styling and props](https://nicegui.io/documentation/element)
|
||||||
|
- [NiceGUI binding properties](https://nicegui.io/documentation/section_binding_properties)
|
||||||
|
- [Quasar components](https://quasar.dev/vue-components)
|
||||||
|
- [Quasar field](https://quasar.dev/vue-components/field/)
|
||||||
|
- [Quasar select](https://quasar.dev/vue-components/select/)
|
||||||
|
- [Tailwind responsive design](https://tailwindcss.com/docs/responsive-design)
|
||||||
|
- [MDN `zoom`](https://developer.mozilla.org/en-US/docs/Web/CSS/zoom)
|
||||||
@@ -1,32 +1,77 @@
|
|||||||
# NiceGUI Architecture Reference
|
# NiceGUI Application Architecture
|
||||||
|
|
||||||
This reference expands the workflow in the main skill file and is loaded only when needed.
|
Load this reference for application composition, package boundaries, and optional subsystem decisions.
|
||||||
|
|
||||||
## Baseline package boundaries
|
## Baseline Package Boundaries
|
||||||
|
|
||||||
- `main.py`: process entrypoint only.
|
- `main.py`: process entry point and app factory exposure.
|
||||||
- `bootstrap.py`: app composition, router wiring, page registration, lifespan orchestration.
|
- `bootstrap.py`: app composition, router wiring, page registration, and lifespan orchestration.
|
||||||
- `config.py`: typed settings and env parsing.
|
- `config.py`: typed settings and environment parsing.
|
||||||
- `logging.py`: centralized logging setup.
|
- `logging.py`: centralized logging setup.
|
||||||
- `api/`: HTTP transport layer; delegates to services.
|
- `api/`: HTTP transport that delegates to services.
|
||||||
- `services/`: business/use-case logic.
|
- `services/`: business and use-case logic.
|
||||||
- `ui/pages/`: route-level NiceGUI pages.
|
- `ui/pages/`: route-level NiceGUI pages.
|
||||||
- `ui/components/`: shared UI building blocks.
|
- `ui/components/`: shared presentation building blocks.
|
||||||
|
|
||||||
## Required baseline behavior
|
Recommended base shape:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
├─ pyproject.toml
|
||||||
|
├─ .env.example
|
||||||
|
├─ src/
|
||||||
|
│ └─ app/
|
||||||
|
│ ├─ __init__.py
|
||||||
|
│ ├─ main.py
|
||||||
|
│ ├─ bootstrap.py
|
||||||
|
│ ├─ config.py
|
||||||
|
│ ├─ logging.py
|
||||||
|
│ ├─ api/
|
||||||
|
│ │ ├─ __init__.py
|
||||||
|
│ │ └─ health.py
|
||||||
|
│ ├─ services/
|
||||||
|
│ │ ├─ __init__.py
|
||||||
|
│ │ └─ example_service.py
|
||||||
|
│ └─ ui/
|
||||||
|
│ ├─ __init__.py
|
||||||
|
│ ├─ components/
|
||||||
|
│ │ ├─ __init__.py
|
||||||
|
│ │ └─ nav.py
|
||||||
|
│ └─ pages/
|
||||||
|
│ ├─ __init__.py
|
||||||
|
│ ├─ home.py
|
||||||
|
│ ├─ dashboard.py
|
||||||
|
│ └─ about.py
|
||||||
|
└─ tests/
|
||||||
|
├─ test_health.py
|
||||||
|
└─ test_pages_registration.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Required Baseline Behavior
|
||||||
|
|
||||||
- FastAPI is the base ASGI app.
|
- FastAPI is the base ASGI app.
|
||||||
- NiceGUI pages are modular and registered from page modules.
|
- `create_app()` composes routes, resources, and NiceGUI.
|
||||||
- Minimum pages: `/`, `/dashboard`, `/about`.
|
- Lifespan owns startup and shutdown resources.
|
||||||
- FastAPI health route: `/healthz`.
|
- NiceGUI pages are modular and explicitly registered.
|
||||||
- Lifespan handles startup/shutdown resources.
|
- FastAPI exposes a health route such as `/healthz`.
|
||||||
- No global side effects at import time.
|
- Imports do not trigger runtime global side effects.
|
||||||
|
|
||||||
## Optional extension: Database
|
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).
|
||||||
|
|
||||||
Use only if persistence is required.
|
## Dependency Direction
|
||||||
|
|
||||||
Suggested additions:
|
Prefer:
|
||||||
|
|
||||||
|
- `main/bootstrap` -> `config/logging` + `api` + `ui/pages` + `services`
|
||||||
|
- `api` -> `services`
|
||||||
|
- `ui/pages` -> `ui/components` + `services`
|
||||||
|
- `services` -> helpers, clients, and `db/` when enabled
|
||||||
|
|
||||||
|
Avoid imports from services back into API or UI modules.
|
||||||
|
|
||||||
|
## Optional Persistence
|
||||||
|
|
||||||
|
Use only when the product requires durable data.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/app/db/
|
src/app/db/
|
||||||
@@ -37,19 +82,15 @@ src/app/db/
|
|||||||
└─ repositories/
|
└─ repositories/
|
||||||
```
|
```
|
||||||
|
|
||||||
Guidelines:
|
- Create one engine and sessionmaker per process.
|
||||||
|
- Provide request- or operation-scoped sessions with `yield`.
|
||||||
|
- Keep transaction boundaries explicit in service or repository flows.
|
||||||
|
- Never share sessions across concurrent tasks.
|
||||||
|
- Use Alembic as the schema migration source of truth.
|
||||||
|
|
||||||
- One engine and one sessionmaker per process.
|
## Optional LangGraph AI
|
||||||
- Request-scoped session dependency using `yield`.
|
|
||||||
- Explicit transaction boundaries in service/repository flows.
|
|
||||||
- Avoid shared sessions across concurrent tasks.
|
|
||||||
- Use Alembic as schema source of truth.
|
|
||||||
|
|
||||||
## Optional extension: LangGraph AI
|
Use only for multi-step orchestration, resumable work, streaming, or human approval.
|
||||||
|
|
||||||
Use only for multi-step AI orchestration or human-in-the-loop workflows.
|
|
||||||
|
|
||||||
Suggested additions:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/app/ai/
|
src/app/ai/
|
||||||
@@ -60,33 +101,37 @@ src/app/ai/
|
|||||||
└─ contracts.py
|
└─ contracts.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Guidelines:
|
- Keep graph internals outside API and UI modules.
|
||||||
|
- Invoke graphs through a service such as `services/ai_service.py`.
|
||||||
- Keep graph internals outside API/UI modules.
|
- Use stable thread or session IDs for resumable flows.
|
||||||
- Invoke graph through `services/ai_service.py`.
|
|
||||||
- Use stable thread/session IDs for resumable sessions.
|
|
||||||
- Keep interrupt payloads JSON-serializable.
|
- Keep interrupt payloads JSON-serializable.
|
||||||
|
|
||||||
## Optional extension: Mounted static docs
|
## Optional Mounted Docs
|
||||||
|
|
||||||
Use only when generated docs should be served in-app.
|
Use only when generated docs must be served by the application.
|
||||||
|
|
||||||
Suggested settings:
|
Suggested settings:
|
||||||
|
|
||||||
- `docs_enabled`
|
- `docs_enabled`
|
||||||
- `docs_mount_path`
|
- `docs_mount_path`
|
||||||
- `docs_site_dir`
|
- `docs_site_dir`
|
||||||
- `docs_require_build` (optional)
|
- `docs_require_build`
|
||||||
|
|
||||||
Guidelines:
|
Mount docs in the composition layer, normalize the mount path, avoid route conflicts, and define behavior for missing build artifacts.
|
||||||
|
|
||||||
- Mount docs in composition layer (`bootstrap.py`).
|
## Async And Responsiveness
|
||||||
- Normalize mount path and avoid route conflicts.
|
|
||||||
- Warn on missing build artifacts unless strict mode is enabled.
|
|
||||||
|
|
||||||
## Suggested output quality criteria
|
- Use `async def` where a handler or service path performs I/O.
|
||||||
|
- Prefer non-blocking clients and libraries.
|
||||||
|
- Offload CPU-heavy work to worker or background execution.
|
||||||
|
- Define progress, cancellation, timeout, completion, and error states for long actions.
|
||||||
|
- Stream or chunk results when workflows are long-running or multi-step.
|
||||||
|
|
||||||
- Clear architecture summary with assumptions.
|
## Testing Minimums
|
||||||
- Explicit decisions for DB, AI, and docs.
|
|
||||||
- Package-scoped implementation checklist.
|
- Test the FastAPI health route.
|
||||||
- Minimal test plan aligned to enabled features.
|
- Test page registration wiring.
|
||||||
|
- If persistence is enabled, test session lifecycle and rollback behavior.
|
||||||
|
- If AI is enabled, test happy paths and interrupt/resume behavior.
|
||||||
|
- If docs are enabled, test the mounted index route.
|
||||||
|
- For long actions, test loading, completion, and error states.
|
||||||
@@ -1,119 +1,100 @@
|
|||||||
# Binding Dataclasses Deep Dive
|
# Binding Dataclasses Deep Dive
|
||||||
|
|
||||||
This reference explains how to model state with NiceGUI bindable dataclasses and how to avoid common update and performance pitfalls.
|
Use this reference to model NiceGUI state with bindable dataclasses and avoid common propagation and performance pitfalls.
|
||||||
|
|
||||||
## Primary Sources
|
## Primary Sources
|
||||||
|
|
||||||
- NiceGUI binding docs: [Binding properties](https://www.nicegui.io/documentation/section_binding_properties)
|
- NiceGUI binding docs: [binding properties](https://www.nicegui.io/documentation/section_binding_properties)
|
||||||
- Python dataclass docs: [dataclasses module](https://docs.python.org/3/library/dataclasses.html)
|
- Python dataclass docs: [dataclasses module](https://docs.python.org/3/library/dataclasses.html)
|
||||||
- Data class design rationale: [PEP 557](https://peps.python.org/pep-0557/)
|
- Data class design rationale: [PEP 557](https://peps.python.org/pep-0557/)
|
||||||
|
|
||||||
## What bindable_dataclass changes
|
## Bindable Dataclass Behavior
|
||||||
|
|
||||||
`@binding.bindable_dataclass` extends standard dataclasses by turning fields into bindable properties so UI bindings can propagate immediately when a field is assigned.
|
`@binding.bindable_dataclass` extends standard dataclasses by turning fields into bindable properties, allowing UI bindings to propagate when a field is assigned.
|
||||||
|
|
||||||
Baseline pattern:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from nicegui import binding, ui
|
from nicegui import binding, ui
|
||||||
|
|
||||||
|
|
||||||
@binding.bindable_dataclass
|
@binding.bindable_dataclass
|
||||||
class Profile:
|
class Profile:
|
||||||
name: str = 'Ada'
|
name: str = "Ada"
|
||||||
age: int = 37
|
age: int = 37
|
||||||
|
|
||||||
|
|
||||||
profile = Profile()
|
profile = Profile()
|
||||||
|
|
||||||
ui.input('Name').bind_value(profile, 'name')
|
ui.input("Name").bind_value(profile, "name")
|
||||||
ui.number('Age', min=0).bind_value(profile, 'age')
|
ui.number("Age", min=0).bind_value(profile, "age")
|
||||||
ui.label().bind_text_from(profile, 'name', backward=lambda n: f'User: {n}')
|
ui.label().bind_text_from(profile, "name", backward=lambda name: f"User: {name}")
|
||||||
```
|
```
|
||||||
|
|
||||||
## Propagation model and performance
|
## Propagation And Performance
|
||||||
|
|
||||||
NiceGUI distinguishes between two link types:
|
NiceGUI distinguishes between two link types:
|
||||||
|
|
||||||
- Bindable properties: efficient, event-like propagation on assignment.
|
- Bindable properties propagate efficiently when values are assigned.
|
||||||
- Active links: polled in a refresh loop (default every 0.1s).
|
- Active links are checked in a refresh loop.
|
||||||
|
|
||||||
Practical implications:
|
Prefer bindable dataclasses for frequently updated form state. Keep binding transforms pure and inexpensive. If an application has many active links, tune `binding_refresh_interval` in `ui.run(...)` only after measuring the impact.
|
||||||
|
|
||||||
- Prefer bindable dataclasses for frequently updated form state.
|
## Dataclass Modeling Rules
|
||||||
- Keep transform functions pure and side-effect free.
|
|
||||||
- If many active links exist, tune `binding_refresh_interval` in `ui.run(...)` carefully.
|
|
||||||
|
|
||||||
## Dataclass modeling rules that matter for binding
|
|
||||||
|
|
||||||
- Use `field(default_factory=...)` for mutable defaults.
|
- Use `field(default_factory=...)` for mutable defaults.
|
||||||
- Avoid `frozen=True` for models that should be edited from UI controls.
|
- Avoid `frozen=True` for models edited by UI controls.
|
||||||
- Use `slots=True` only when you have confirmed compatibility with your inheritance and extension needs.
|
- Use `slots=True` only after confirming compatibility with inheritance and extension needs.
|
||||||
- Keep UI-editable fields explicit and typed.
|
- Keep UI-editable fields explicit and typed.
|
||||||
|
|
||||||
Example with safe mutable defaults:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from dataclasses import field
|
from dataclasses import field
|
||||||
|
|
||||||
from nicegui import binding
|
from nicegui import binding
|
||||||
|
|
||||||
|
|
||||||
@binding.bindable_dataclass
|
@binding.bindable_dataclass
|
||||||
class Filters:
|
class Filters:
|
||||||
query: str = ''
|
query: str = ""
|
||||||
tags: list[str] = field(default_factory=list)
|
tags: list[str] = field(default_factory=list)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Nested structures and binding paths
|
## Nested Structures
|
||||||
|
|
||||||
NiceGUI supports nested key paths via tuples for nested data (for example dictionaries and nested structures).
|
NiceGUI supports tuple paths for nested data structures.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from nicegui import ui
|
from nicegui import ui
|
||||||
|
|
||||||
data = {'user': {'name': 'Ada'}}
|
data = {"user": {"name": "Ada"}}
|
||||||
|
|
||||||
ui.input('Name').bind_value(data, ('user', 'name'))
|
ui.input("Name").bind_value(data, ("user", "name"))
|
||||||
ui.label().bind_text_from(data, ('user', 'name'))
|
ui.label().bind_text_from(data, ("user", "name"))
|
||||||
```
|
```
|
||||||
|
|
||||||
When using nested dataclasses, keep updates explicit and predictable at the field level.
|
Keep nested dataclass updates explicit and predictable at the field level.
|
||||||
|
|
||||||
## Strictness and refactor safety
|
## Strictness And Refactor Safety
|
||||||
|
|
||||||
Binding can warn when attributes do not exist.
|
|
||||||
|
|
||||||
- Object attributes are checked by default.
|
- Object attributes are checked by default.
|
||||||
- Dictionary keys are not checked by default.
|
- Dictionary keys are not checked by default.
|
||||||
- Use `strict=True` when you want missing-key warnings for dict-backed state.
|
- Use `strict=True` when missing dictionary keys should produce warnings.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from nicegui import app, ui
|
from nicegui import app, ui
|
||||||
|
|
||||||
ui.input().bind_value(app.storage.user, 'display_name', strict=True)
|
ui.input().bind_value(app.storage.user, "display_name", strict=True)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common pitfalls and safer alternatives
|
## Common Pitfalls
|
||||||
|
|
||||||
- Pitfall: mutating nested mutable values in place and expecting immediate UI sync.
|
- In-place mutation may not produce immediate UI synchronization. Assign the updated value back to the bound field.
|
||||||
- Safer alternative: assign back to the bound field after updates so change propagation is explicit.
|
- Heavy binding transforms can degrade refresh performance. Move expensive work to event handlers or services.
|
||||||
|
- State shared across unrelated pages or users can leak data. Scope models to the appropriate page, client, or user context.
|
||||||
|
|
||||||
- Pitfall: heavy transform functions in bindings.
|
## Version Checks
|
||||||
- Safer alternative: keep transformations cheap and deterministic; move heavy work to event handlers.
|
|
||||||
|
|
||||||
- Pitfall: one model shared across unrelated pages or users.
|
- `bindable_dataclass` was added in NiceGUI 2.11.0.
|
||||||
- Safer alternative: scope model instances to page/client/user context as needed.
|
- Depth-first binding propagation was documented in NiceGUI 2.16.0.
|
||||||
|
- Binding `strict` behavior was documented in NiceGUI 3.0.0.
|
||||||
|
- Tuple paths for nested properties were documented in NiceGUI 3.10.0.
|
||||||
|
|
||||||
## Version notes to remember
|
Verify these behaviors against the NiceGUI version pinned by the target project.
|
||||||
|
|
||||||
- `bindable_dataclass` added in NiceGUI 2.11.0.
|
|
||||||
- Binding `strict` behavior documented as added in NiceGUI 3.0.0.
|
|
||||||
- Tuple paths for nested properties documented as added in NiceGUI 3.10.0.
|
|
||||||
- Depth-first binding propagation update documented in NiceGUI 2.16.0.
|
|
||||||
|
|
||||||
Verify behavior against the NiceGUI version pinned in your project before relying on version-specific semantics.
|
|
||||||
|
|
||||||
## Quick checklist
|
|
||||||
|
|
||||||
- Choose bindable dataclasses for interactive form-like state.
|
|
||||||
- Use `default_factory` for mutable fields.
|
|
||||||
- Keep transform functions pure.
|
|
||||||
- Use strict mode intentionally.
|
|
||||||
- Re-check version notes before migration work.
|
|
||||||
@@ -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 AsyncGeneratorr
|
||||||
|
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) -> AsyncGenerator[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/)
|
||||||
@@ -1,6 +1,16 @@
|
|||||||
# Source Documentation
|
# Source Documentation
|
||||||
|
|
||||||
Use these links for framework-specific details.
|
Use these links to verify framework-specific behavior before relying on version-sensitive or integration-specific guidance.
|
||||||
|
|
||||||
|
## NiceGUI
|
||||||
|
|
||||||
|
!!! info "NiceGUI sources"
|
||||||
|
- [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)
|
||||||
|
- [Action events](https://www.nicegui.io/documentation/section_action_events)
|
||||||
|
- [Security best practices](https://www.nicegui.io/documentation/section_security)
|
||||||
|
|
||||||
## FastAPI
|
## FastAPI
|
||||||
|
|
||||||
@@ -8,40 +18,53 @@ Use these links for framework-specific details.
|
|||||||
- [Lifespan events](https://fastapi.tiangolo.com/advanced/events/)
|
- [Lifespan events](https://fastapi.tiangolo.com/advanced/events/)
|
||||||
- [Settings and environment variables](https://fastapi.tiangolo.com/advanced/settings/)
|
- [Settings and environment variables](https://fastapi.tiangolo.com/advanced/settings/)
|
||||||
- [Dependencies with yield](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/)
|
- [Dependencies with yield](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/)
|
||||||
- [SQL databases tutorial](https://fastapi.tiangolo.com/tutorial/sql-databases/)
|
- [Server-sent events](https://fastapi.tiangolo.com/advanced/server-sent-events/)
|
||||||
|
- [WebSockets](https://fastapi.tiangolo.com/advanced/websockets/)
|
||||||
|
|
||||||
## SQLAlchemy and Alembic
|
## 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
|
||||||
|
|
||||||
|
!!! info "Styling sources"
|
||||||
|
- [Tailwind utility-first styling](https://tailwindcss.com/docs/utility-first)
|
||||||
|
- [Tailwind responsive design and container queries](https://tailwindcss.com/docs/responsive-design)
|
||||||
|
- [Quasar components](https://quasar.dev/vue-components)
|
||||||
|
- [Quasar Screen plugin documentation source](https://github.com/quasarframework/quasar/blob/dev/docs/src/pages/options/screen-plugin.md)
|
||||||
|
- [CSS media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries)
|
||||||
|
- [CSS container queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries)
|
||||||
|
|
||||||
|
## Persistence
|
||||||
|
|
||||||
!!! info "Persistence sources"
|
!!! info "Persistence sources"
|
||||||
- [SQLAlchemy engine configuration and pooling](https://docs.sqlalchemy.org/en/20/core/engines.html)
|
- [SQLAlchemy engine configuration and pooling](https://docs.sqlalchemy.org/en/20/core/engines.html)
|
||||||
- [SQLAlchemy session lifecycle basics](https://docs.sqlalchemy.org/en/20/orm/session_basics.html)
|
- [SQLAlchemy session lifecycle](https://docs.sqlalchemy.org/en/20/orm/session_basics.html)
|
||||||
- [Alembic tutorial](https://alembic.sqlalchemy.org/en/latest/tutorial.html)
|
- [Alembic tutorial](https://alembic.sqlalchemy.org/en/latest/tutorial.html)
|
||||||
|
|
||||||
## Pydantic
|
## Configuration And Dataclasses
|
||||||
|
|
||||||
!!! info "Pydantic source"
|
!!! info "Python and Pydantic sources"
|
||||||
- [Pydantic settings management](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/)
|
- [Pydantic settings management](https://docs.pydantic.dev/latest/concepts/pydantic_settings/)
|
||||||
|
- [Python dataclasses](https://docs.python.org/3/library/dataclasses.html)
|
||||||
## NiceGUI
|
|
||||||
|
|
||||||
!!! info "NiceGUI sources"
|
|
||||||
- [Pages, routing, and FastAPI integration](https://www.nicegui.io/documentation/section_pages_routing)
|
|
||||||
- [Binding properties and bindable dataclass](https://www.nicegui.io/documentation/section_binding_properties)
|
|
||||||
- [Security best practices](https://www.nicegui.io/documentation/section_security)
|
|
||||||
|
|
||||||
## Python Dataclasses
|
|
||||||
|
|
||||||
!!! info "Python sources"
|
|
||||||
- [dataclasses module reference](https://docs.python.org/3/library/dataclasses.html)
|
|
||||||
- [PEP 557: Data Classes](https://peps.python.org/pep-0557/)
|
- [PEP 557: Data Classes](https://peps.python.org/pep-0557/)
|
||||||
|
|
||||||
## LangGraph
|
## LangGraph
|
||||||
|
|
||||||
!!! info "LangGraph sources"
|
!!! info "LangGraph sources"
|
||||||
- [Overview](https://docs.langchain.com/oss/python/langgraph/overview)
|
- [Overview](https://docs.langchain.com/oss/python/langgraph/overview)
|
||||||
- [Quickstart](https://docs.langchain.com/oss/python/langgraph/quickstart)
|
|
||||||
- [Workflows and agents](https://docs.langchain.com/oss/python/langgraph/workflows-agents)
|
- [Workflows and agents](https://docs.langchain.com/oss/python/langgraph/workflows-agents)
|
||||||
- [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence)
|
- [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence)
|
||||||
- [Memory concepts](https://docs.langchain.com/oss/python/concepts/memory)
|
|
||||||
- [Streaming](https://docs.langchain.com/oss/python/langgraph/streaming)
|
- [Streaming](https://docs.langchain.com/oss/python/langgraph/streaming)
|
||||||
- [Interrupts and human-in-the-loop](https://docs.langchain.com/oss/python/langgraph/interrupts)
|
- [Interrupts and human-in-the-loop](https://docs.langchain.com/oss/python/langgraph/interrupts)
|
||||||
@@ -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,235 @@ 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:
|
class ObservabilitySettings(BaseModel):
|
||||||
return (
|
log_level: str = "INFO"
|
||||||
"postgresql://"
|
json_logs: bool = True
|
||||||
f"{self.username}:{self.password.get_secret_value()}"
|
|
||||||
f"@{self.host}:{self.port}/mydatabase"
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_prefix="APP_",
|
||||||
|
env_nested_delimiter="__",
|
||||||
|
frozen=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
database: DatabaseSettings = Field(default_factory=DatabaseSettings)
|
||||||
|
observability: ObservabilitySettings = Field(
|
||||||
|
default_factory=ObservabilitySettings
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
_db_settings: ContextVar[DbSettings | None] = ContextVar("db_settings", default=None)
|
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.
|
||||||
_db_conn: ContextVar[Engine | None] = ContextVar("db_conn", default=None)
|
|
||||||
|
Use independent `BaseSettings` classes when the objects have genuinely independent ownership:
|
||||||
|
|
||||||
|
1. Different packages or deployable components own the schemas.
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Alternative Database Backends
|
||||||
|
|
||||||
|
When one application can run against one of several database backends, model the selected backend as a [discriminated union](https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions). Pydantic validates only the variant selected by `driver`, so required PostgreSQL values do not make a SQLite configuration fail, and vice versa.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Annotated, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, SecretStr
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
def get_db_settings(**kwargs) -> DbSettings:
|
class SqliteSettings(BaseModel):
|
||||||
settings = _db_settings.get()
|
driver: Literal["sqlite"] = "sqlite"
|
||||||
if settings is None:
|
path: str = "app.db"
|
||||||
settings = DbSettings(**kwargs)
|
|
||||||
_db_settings.set(settings)
|
|
||||||
cleanup_engine()
|
class PostgresSettings(BaseModel):
|
||||||
return settings
|
driver: Literal["postgres"] = "postgres"
|
||||||
|
host: str
|
||||||
|
port: int = 5432
|
||||||
|
database: str
|
||||||
|
user: str
|
||||||
|
password: SecretStr
|
||||||
|
|
||||||
|
|
||||||
|
DatabaseSettings = Annotated[
|
||||||
|
SqliteSettings | PostgresSettings,
|
||||||
|
Field(discriminator="driver"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_prefix="APP_",
|
||||||
|
env_nested_delimiter="__",
|
||||||
|
env_file=".env",
|
||||||
|
extra="ignore",
|
||||||
|
frozen=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
database: DatabaseSettings
|
||||||
|
```
|
||||||
|
|
||||||
|
Choose one configuration. A SQLite deployment requires no PostgreSQL variables:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
APP_DATABASE__DRIVER=sqlite
|
||||||
|
APP_DATABASE__PATH=./data/app.db
|
||||||
|
```
|
||||||
|
|
||||||
|
A PostgreSQL deployment requires only the PostgreSQL branch:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
APP_DATABASE__DRIVER=postgres
|
||||||
|
APP_DATABASE__HOST=db.internal
|
||||||
|
APP_DATABASE__PORT=5432
|
||||||
|
APP_DATABASE__DATABASE=app
|
||||||
|
APP_DATABASE__USER=app_user
|
||||||
|
APP_DATABASE__PASSWORD=provided-by-the-runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
After settings validation, select an async SQLAlchemy driver URL. This is a pure configuration step; create the engine, session factory, and sessions in their own lifecycle-managed providers:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from functools import cache
|
||||||
|
|
||||||
|
from sqlalchemy import URL
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
||||||
|
|
||||||
|
|
||||||
|
def get_database_url(
|
||||||
|
settings: Settings,
|
||||||
|
) -> str:
|
||||||
|
match settings.database:
|
||||||
|
case SqliteSettings(path=path):
|
||||||
|
url = URL.create(
|
||||||
|
drivername="sqlite+aiosqlite",
|
||||||
|
database=path,
|
||||||
|
)
|
||||||
|
case PostgresSettings() as database:
|
||||||
|
url = URL.create(
|
||||||
|
drivername="postgresql+asyncpg",
|
||||||
|
host=database.host,
|
||||||
|
port=database.port,
|
||||||
|
database=database.database,
|
||||||
|
user=database.user,
|
||||||
|
password=database.password.get_secret_value(),
|
||||||
|
)
|
||||||
|
return url.render_as_string(hide_password=False)
|
||||||
|
|
||||||
|
|
||||||
@cache
|
@cache
|
||||||
def get_db_engine() -> Engine:
|
def get_engine(database_url: str) -> AsyncEngine:
|
||||||
engine = _db_conn.get()
|
return create_async_engine(
|
||||||
if engine is None:
|
database_url,
|
||||||
engine = create_engine(get_db_settings().dsn)
|
pool_pre_ping=True,
|
||||||
_db_conn.set(engine)
|
)
|
||||||
return engine
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup_engine() -> None:
|
async def dispose_engine(database_url: str) -> None:
|
||||||
engine = _db_conn.get()
|
engine = get_engine(database_url)
|
||||||
if engine is not None:
|
try:
|
||||||
engine.dispose()
|
await engine.dispose()
|
||||||
_db_conn.set(None)
|
finally:
|
||||||
get_db_engine.cache_clear()
|
get_engine.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def get_session():
|
|
||||||
with Session(get_db_engine()) as session:
|
|
||||||
yield session
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Design notes:
|
At the composition boundary, resolve the URL once with `get_database_url(settings)` and use it to retrieve the cached engine. In FastAPI, expose that engine through lifespan and build one `async_sessionmaker` from it; each request or unit of work then creates its own `AsyncSession`. Do not call `aiosqlite.connect()` or `asyncpg.create_pool()` directly: `aiosqlite` and `asyncpg` are selected as SQLAlchemy drivers by the URL, while SQLAlchemy owns pooling, disposal, and session integration.
|
||||||
|
|
||||||
1. `get_db_settings` is the constructor/accessor for settings and can accept explicit overrides in tests.
|
The nested variants remain `BaseModel` classes. `Settings` is the only `BaseSettings` model and therefore the only object that reads environment variables, dotenv files, or secrets. This keeps one source policy and validated configuration snapshot while keeping the engine, session factory, and sessions in their distinct lifecycles. See the [SQLAlchemy asyncio extension](https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html), the [engine lifecycle guidance](../async-fastapi-sqlmodel/references/engine.md), and the [session lifecycle guidance](../async-fastapi-sqlmodel/references/session.md).
|
||||||
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.
|
|
||||||
4. `get_session` centralizes session creation so call sites never build engines directly.
|
|
||||||
|
|
||||||
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.
|
||||||
|
4. Each backend configuration validates without values required only by another backend.
|
||||||
|
5. One cached `AsyncEngine` exists per configured driver URL, while each request or unit of work receives a new `AsyncSession`.
|
||||||
|
|
||||||
### 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 +395,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 +444,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)
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ REQUIRED_LIBRARY_TAGS_BY_SKILL = {
|
|||||||
"fastapi-uv-docker": {"fastapi", "uv", "uvicorn", "docker"},
|
"fastapi-uv-docker": {"fastapi", "uv", "uvicorn", "docker"},
|
||||||
"mcp-details": {"mcp", "fastmcp"},
|
"mcp-details": {"mcp", "fastmcp"},
|
||||||
"nicegui": {"nicegui", "fastapi"},
|
"nicegui": {"nicegui", "fastapi"},
|
||||||
"nicegui-ui-customization": {"nicegui", "fastapi"},
|
|
||||||
"pytesting": {"pytest", "testing", "fastapi", "asyncio", "anyio"},
|
"pytesting": {"pytest", "testing", "fastapi", "asyncio", "anyio"},
|
||||||
"python-logging": {"python", "logging"},
|
"python-logging": {"python", "logging"},
|
||||||
"python-typing": {"python", "typing"},
|
"python-typing": {"python", "typing"},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncGenerator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -14,7 +14,7 @@ from personal_mcp.web.app import create_app
|
|||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture
|
@pytest_asyncio.fixture
|
||||||
async def client() -> AsyncIterator[AsyncClient]:
|
async def client() -> AsyncGenerator[AsyncClient]:
|
||||||
"""Provides an AsyncClient bound to a fresh application instance."""
|
"""Provides an AsyncClient bound to a fresh application instance."""
|
||||||
app = create_app()
|
app = create_app()
|
||||||
async with AsyncClient(
|
async with AsyncClient(
|
||||||
@@ -30,7 +30,7 @@ def mcp_session_factory():
|
|||||||
"""Provides an in-process context manager factory for MCP SDK sessions."""
|
"""Provides an in-process context manager factory for MCP SDK sessions."""
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def create_session(*, initialize: bool = True) -> AsyncIterator[ClientSession]:
|
async def create_session(*, initialize: bool = True) -> AsyncGenerator[ClientSession]:
|
||||||
app = create_app()
|
app = create_app()
|
||||||
mcp_url = f"http://testserver{app.state.settings.mounts.mcp}"
|
mcp_url = f"http://testserver{app.state.settings.mounts.mcp}"
|
||||||
async with (
|
async with (
|
||||||
|
|||||||
+13
-10
@@ -90,26 +90,26 @@ nav = [
|
|||||||
] },
|
] },
|
||||||
{ "Async SQLA" = [
|
{ "Async SQLA" = [
|
||||||
{ "Overview" = "skills/async-fastapi-sqlmodel/SKILL.md" },
|
{ "Overview" = "skills/async-fastapi-sqlmodel/SKILL.md" },
|
||||||
{ "Index" = "skills/async-fastapi-sqlmodel/references/index.md" },
|
|
||||||
{ "Engine" = "skills/async-fastapi-sqlmodel/references/engine.md" },
|
{ "Engine" = "skills/async-fastapi-sqlmodel/references/engine.md" },
|
||||||
{ "Session" = "skills/async-fastapi-sqlmodel/references/session.md" },
|
{ "Session" = "skills/async-fastapi-sqlmodel/references/session.md" },
|
||||||
{ "Tx" = "skills/async-fastapi-sqlmodel/references/transactions.md" },
|
{ "Tx" = "skills/async-fastapi-sqlmodel/references/transactions.md" },
|
||||||
{ "SQLModel" = "skills/async-fastapi-sqlmodel/references/sqlmodel.md" },
|
{ "SQLModel" = "skills/async-fastapi-sqlmodel/references/sqlmodel.md" },
|
||||||
|
{ "CRUD" = "skills/async-fastapi-sqlmodel/references/crud.md" },
|
||||||
|
{ "Testing" = "skills/async-fastapi-sqlmodel/references/testing.md" },
|
||||||
{ "IO" = "skills/async-fastapi-sqlmodel/references/implicit_io.md" },
|
{ "IO" = "skills/async-fastapi-sqlmodel/references/implicit_io.md" },
|
||||||
{ "Obs" = "skills/async-fastapi-sqlmodel/references/observability.md" },
|
{ "Obs" = "skills/async-fastapi-sqlmodel/references/observability.md" },
|
||||||
{ "Template" = "skills/async-fastapi-sqlmodel/references/template.md" },
|
{ "Template" = "skills/async-fastapi-sqlmodel/references/template.md" },
|
||||||
] },
|
] },
|
||||||
{ "NiceGUI" = [
|
{ "NiceGUI" = [
|
||||||
{ "Overview" = "skills/nicegui/SKILL.md" },
|
{ "Overview" = "skills/nicegui/SKILL.md" },
|
||||||
{ "Arch" = "skills/nicegui/references/architecture.md" },
|
{ "App Architecture" = "skills/nicegui/references/architecture.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" },
|
||||||
|
{ "Flows" = "skills/nicegui/references/interaction-patterns.md" },
|
||||||
|
{ "Quality" = "skills/nicegui/references/troubleshooting-and-quality-gates.md" },
|
||||||
{ "Sources" = "skills/nicegui/references/source-documentation.md" },
|
{ "Sources" = "skills/nicegui/references/source-documentation.md" },
|
||||||
] },
|
] },
|
||||||
{ "NiceGUI Fine-Tuning" = [
|
|
||||||
{ "Overview" = "skills/nicegui-ui-customization/SKILL.md" },
|
|
||||||
{ "Style" = "skills/nicegui-ui-customization/references/architecture-and-styling.md" },
|
|
||||||
{ "Flows" = "skills/nicegui-ui-customization/references/interaction-patterns.md" },
|
|
||||||
{ "Quality" = "skills/nicegui-ui-customization/references/troubleshooting-and-quality-gates.md" },
|
|
||||||
] },
|
|
||||||
{ "Pytest" = [
|
{ "Pytest" = [
|
||||||
{ "Overview" = "skills/pytesting/SKILL.md" },
|
{ "Overview" = "skills/pytesting/SKILL.md" },
|
||||||
{ "Docs" = "skills/pytesting/references/pytest-docs.md" },
|
{ "Docs" = "skills/pytesting/references/pytest-docs.md" },
|
||||||
@@ -140,7 +140,6 @@ nav = [
|
|||||||
] },
|
] },
|
||||||
{ "Zensical" = [
|
{ "Zensical" = [
|
||||||
{ "Overview" = "skills/zensical-docs/SKILL.md" },
|
{ "Overview" = "skills/zensical-docs/SKILL.md" },
|
||||||
{ "Map" = "skills/zensical-docs/references/index.md" },
|
|
||||||
{ "Features" = "skills/zensical-docs/references/zensical-features.md" },
|
{ "Features" = "skills/zensical-docs/references/zensical-features.md" },
|
||||||
{ "Theme" = "skills/zensical-docs/references/theme-customization-and-icons.md" },
|
{ "Theme" = "skills/zensical-docs/references/theme-customization-and-icons.md" },
|
||||||
{ "Quality" = "skills/zensical-docs/references/documentation-quality.md" },
|
{ "Quality" = "skills/zensical-docs/references/documentation-quality.md" },
|
||||||
@@ -166,7 +165,11 @@ extra_css = ["stylesheets/mermaid-override.css"]
|
|||||||
# The path provided should be relative to the "docs_dir".
|
# The path provided should be relative to the "docs_dir".
|
||||||
#
|
#
|
||||||
# Read more: https://zensical.org/docs/customization/#additional-javascript
|
# Read more: https://zensical.org/docs/customization/#additional-javascript
|
||||||
extra_javascript = ["javascripts/mermaid-override.js"]
|
extra_javascript = [
|
||||||
|
"javascripts/mermaid-override.js",
|
||||||
|
"javascripts/mathjax.js",
|
||||||
|
"https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js",
|
||||||
|
]
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# Section for configuring theme options
|
# Section for configuring theme options
|
||||||
|
|||||||
Reference in New Issue
Block a user