5 Commits
Author SHA1 Message Date
John Lancaster cd11ea8255 fixed caching due to docs symlink 2026-07-31 15:39:09 -05:00
John Lancaster bc21643e8c jsfiddle prompt 2026-07-31 15:12:18 -05:00
John Lancaster 1ed5856db0 asyncgenerator fix 2026-07-30 20:51:16 -05:00
John Lancaster 70695ff218 toc update 2026-07-30 01:29:19 -05:00
John Lancaster a238fb4dc3 example of 2 database backends 2026-07-30 01:29:05 -05:00
11 changed files with 200 additions and 22 deletions
+1 -1
View File
@@ -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
-2
View File
@@ -6,5 +6,3 @@ services:
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8765:8765" - "8765:8765"
volumes:
- ./docs:/app/src/personal_mcp/docs
@@ -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.
+3 -3
View File
@@ -139,14 +139,14 @@ 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 async_sessionmaker, create_async_engine from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlmodel.ext.asyncio.session import AsyncSession 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)
@@ -160,7 +160,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
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
``` ```
@@ -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
@@ -130,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
@@ -139,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
@@ -172,7 +172,7 @@ 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")
@@ -277,7 +277,7 @@ 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
@@ -294,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
``` ```
@@ -42,7 +42,7 @@ Use migrations to provision an integration database when migrations are part of
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. 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 ```python
from collections.abc import AsyncIterator from collections.abc import AsyncGeneratorr
import pytest_asyncio import pytest_asyncio
from sqlalchemy.ext.asyncio import AsyncEngine from sqlalchemy.ext.asyncio import AsyncEngine
@@ -50,7 +50,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession
@pytest_asyncio.fixture @pytest_asyncio.fixture
async def session(test_engine: AsyncEngine) -> AsyncIterator[AsyncSession]: async def session(test_engine: AsyncEngine) -> AsyncGenerator[AsyncSession]:
async with test_engine.connect() as connection: async with test_engine.connect() as connection:
transaction = await connection.begin() transaction = await connection.begin()
test_session = AsyncSession( test_session = AsyncSession(
@@ -78,7 +78,7 @@ def app_with_test_session(
app: FastAPI, app: FastAPI,
session: AsyncSession, session: AsyncSession,
) -> FastAPI: ) -> FastAPI:
async def get_test_session() -> AsyncIterator[AsyncSession]: async def get_test_session() -> AsyncGenerator[AsyncSession]:
yield session yield session
app.dependency_overrides[get_session] = get_test_session app.dependency_overrides[get_session] = get_test_session
@@ -104,7 +104,7 @@ from sqlalchemy.ext.asyncio import AsyncEngine
@pytest_asyncio.fixture @pytest_asyncio.fixture
async def test_engine() -> AsyncIterator[AsyncEngine]: async def test_engine() -> AsyncGenerator[AsyncEngine]:
engine, _ = create_database("sqlite+aiosqlite://") engine, _ = create_database("sqlite+aiosqlite://")
async with engine.begin() as connection: async with engine.begin() as connection:
await connection.run_sync(SQLModel.metadata.create_all) await connection.run_sync(SQLModel.metadata.create_all)
@@ -126,7 +126,7 @@ def get_settings() -> Settings:
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. 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" ```python title="src/my_app/main.py"
from collections.abc import AsyncIterator from collections.abc import AsyncGeneratorr
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import uvicorn import uvicorn
@@ -137,7 +137,7 @@ from my_app.config import Settings, get_settings
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]: async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
app.state.ready = True app.state.ready = True
try: try:
yield yield
+113
View File
@@ -203,11 +203,124 @@ Use independent `BaseSettings` classes when the objects have genuinely independe
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. 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
class SqliteSettings(BaseModel):
driver: Literal["sqlite"] = "sqlite"
path: str = "app.db"
class PostgresSettings(BaseModel):
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
def get_engine(database_url: str) -> AsyncEngine:
return create_async_engine(
database_url,
pool_pre_ping=True,
)
async def dispose_engine(database_url: str) -> None:
engine = get_engine(database_url)
try:
await engine.dispose()
finally:
get_engine.cache_clear()
```
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.
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).
Quality gate: Quality gate:
1. Nested sections share one source policy and lifecycle. 1. Nested sections share one source policy and lifecycle.
2. Independent settings have distinct owners, prefixes, or lifecycles. 2. Independent settings have distinct owners, prefixes, or lifecycles.
3. The application does not repeatedly scan the same sources through accidental nested `BaseSettings` construction. 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. Own The Settings Lifecycle ### 7. Own The Settings Lifecycle
+3 -3
View File
@@ -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 (
+1
View File
@@ -95,6 +95,7 @@ nav = [
{ "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" }, { "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" },