Skip to content

Commit c40a186

Browse files
committed
fix(template): honor restored capability contracts
1 parent f892a28 commit c40a186

22 files changed

Lines changed: 228 additions & 102 deletions

File tree

docs/migration-traceability.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ legacy_source = "legacy/template/{{cookiecutter.project_slug}}/backend/app/core/
4545
target = "app/core/logging.py"
4646
allowed_transformations = ["删除用户/JWT 脱敏模式", "保留凭据脱敏"]
4747
removed_references = ["email、JWT、Bearer、refresh token 模式"]
48-
approved_deviation = ["类名明确为 SecretRedactionFilter"]
48+
approved_deviation = [""]
4949
verification = ["test_minimal_tree_contains_only_legacy_core_service"]
5050

5151
[[target]]

template/[% if ci_type == 'github' %].github[% endif %]/workflows/ci.yml.jinja

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
jobs:
88
quality:
99
runs-on: ubuntu-latest
10-
[% if database == "postgresql" or background_tasks == "taskiq" or enable_caching or rate_limiting == "redis" %] services:
10+
[% if database == "postgresql" or enable_redis %] services:
1111
[% if database == "postgresql" %] postgres:
1212
image: postgres:17-alpine
1313
env:
@@ -21,7 +21,7 @@ jobs:
2121
--health-interval 2s
2222
--health-timeout 3s
2323
--health-retries 15
24-
[% endif %][% if background_tasks == "taskiq" or enable_caching or rate_limiting == "redis" %] redis:
24+
[% endif %][% if enable_redis %] redis:
2525
image: redis:8-alpine
2626
ports:
2727
- 6379:6379
@@ -32,8 +32,8 @@ jobs:
3232
--health-retries 15
3333
[% endif %][% endif %] env:
3434
[% if database == "postgresql" %] DATABASE_URL: postgresql+asyncpg://postgres:postgres@127.0.0.1:5432/app
35-
[% endif %][% if background_tasks == "taskiq" or enable_caching or rate_limiting == "redis" %] REDIS_URL: redis://127.0.0.1:6379/0
36-
[% endif %][% if telemetry == "logfire" %] LOGFIRE_SEND_TO_LOGFIRE: "false"
35+
[% endif %][% if enable_redis %] REDIS_URL: redis://127.0.0.1:6379/0
36+
[% endif %][% if enable_logfire %] LOGFIRE_SEND_TO_LOGFIRE: "false"
3737
[% endif %] steps:
3838
- uses: actions/checkout@v4
3939
- uses: astral-sh/setup-uv@v6

template/[% if enable_docker %]deploy[% endif %]/compose.yaml.jinja

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@ services:
66
environment:
77
API_KEY: "${API_KEY:-[[ deployment_api_key ]]}"
88
[% if database == "postgresql" %] DATABASE_URL: postgresql+asyncpg://postgres:postgres@postgres:5432/app
9-
[% endif %][% if background_tasks == "taskiq" or enable_caching or rate_limiting == "redis" %] REDIS_URL: redis://redis:6379/0
10-
[% endif %][% if agent_capability == "pydantic-ai" %] LLM_BASE_URL: "${LLM_BASE_URL:-https://api.example.com/v1}"
9+
[% endif %][% if enable_redis %] REDIS_URL: redis://redis:6379/0
10+
[% endif %][% if ai_framework == "pydantic_ai" %] LLM_BASE_URL: "${LLM_BASE_URL:-https://api.example.com/v1}"
1111
LLM_API_KEY: "${LLM_API_KEY:-model-key-CHANGEME}"
1212
LLM_MODEL: "${LLM_MODEL:-model-name-CHANGEME}"
13-
[% endif %][% if telemetry == "logfire" %] LOGFIRE_TOKEN: "${LOGFIRE_TOKEN:-}"
13+
[% endif %][% if enable_logfire %] LOGFIRE_TOKEN: "${LOGFIRE_TOKEN:-}"
1414
[% endif %] ports:
1515
- "8000:8000"
16-
[% if database == "postgresql" or background_tasks == "taskiq" or enable_caching or rate_limiting == "redis" %] depends_on:
16+
[% if database == "postgresql" or enable_redis or background_tasks == "taskiq" %] depends_on:
1717
[% if database == "postgresql" %] migrate:
1818
condition: service_completed_successfully
19-
[% endif %][% if background_tasks == "taskiq" or enable_caching or rate_limiting == "redis" %] redis:
19+
[% endif %][% if enable_redis or background_tasks == "taskiq" %] redis:
2020
condition: service_healthy
2121
[% endif %][% endif %] healthcheck:
2222
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready')"]
@@ -31,7 +31,7 @@ services:
3131
command: ["uv", "run", "--no-sync", "taskiq", "worker", "app.tasks.taskiq:broker"]
3232
environment:
3333
REDIS_URL: redis://redis:6379/0
34-
[% if telemetry == "logfire" %] LOGFIRE_TOKEN: "${LOGFIRE_TOKEN:-}"
34+
[% if enable_logfire %] LOGFIRE_TOKEN: "${LOGFIRE_TOKEN:-}"
3535
[% endif %]
3636
depends_on:
3737
redis:
@@ -44,7 +44,7 @@ services:
4444
command: ["uv", "run", "--no-sync", "taskiq", "scheduler", "app.tasks.taskiq:scheduler"]
4545
environment:
4646
REDIS_URL: redis://redis:6379/0
47-
[% if telemetry == "logfire" %] LOGFIRE_TOKEN: "${LOGFIRE_TOKEN:-}"
47+
[% if enable_logfire %] LOGFIRE_TOKEN: "${LOGFIRE_TOKEN:-}"
4848
[% endif %]
4949
depends_on:
5050
redis:
@@ -76,7 +76,7 @@ services:
7676
retries: 15
7777
volumes:
7878
- postgres-data:/var/lib/postgresql/data
79-
[% endif %][% if background_tasks == "taskiq" or enable_caching or rate_limiting == "redis" %]
79+
[% endif %][% if enable_redis or background_tasks == "taskiq" %]
8080
redis:
8181
image: redis:8-alpine
8282
ports:
@@ -88,8 +88,8 @@ services:
8888
retries: 15
8989
volumes:
9090
- redis-data:/data
91-
[% endif %][% if database == "postgresql" or background_tasks == "taskiq" or enable_caching or rate_limiting == "redis" %]
91+
[% endif %][% if database == "postgresql" or enable_redis or background_tasks == "taskiq" %]
9292
volumes:
9393
[% if database == "postgresql" %] postgres-data:
94-
[% endif %][% if background_tasks == "taskiq" or enable_caching or rate_limiting == "redis" %] redis-data:
94+
[% endif %][% if enable_redis or background_tasks == "taskiq" %] redis-data:
9595
[% endif %][% endif %]

template/app/[% if agent_capability == 'pydantic-ai' %]agents[% endif %]/assistant.py.jinja renamed to template/app/[% if ai_framework == 'pydantic_ai' %]agents[% endif %]/assistant.py.jinja

File renamed without changes.

template/app/[% if agent_capability == 'pydantic-ai' %]services[% endif %]/agent.py.jinja renamed to template/app/[% if ai_framework == 'pydantic_ai' %]services[% endif %]/agent.py.jinja

File renamed without changes.

template/app/[% if background_tasks == 'taskiq' %]tasks[% endif %]/taskiq.py.jinja

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from __future__ import annotations
22

3-
[% if telemetry == "logfire" %]import logfire
3+
[% if enable_logfire %]import logfire
44
[% endif -%]
55
from taskiq import TaskiqScheduler
66
from taskiq.schedule_sources import LabelScheduleSource
77
from taskiq_redis import RedisAsyncResultBackend, RedisStreamBroker
88

99
from app.core.config import Settings
10-
[% if telemetry == "logfire" %]from app.core.telemetry import configure_telemetry
10+
[% if enable_logfire %]from app.core.telemetry import configure_telemetry
1111

1212
configure_telemetry()
1313
[% endif %]
@@ -27,7 +27,7 @@ scheduler = TaskiqScheduler(broker=broker, sources=[LabelScheduleSource(broker)]
2727
)
2828
async def example_task(value: str = "scheduled") -> dict[str, str]:
2929
"""Return a value so callers can observe successful worker execution."""
30-
[% if telemetry == "logfire" %] with logfire.span("Run Taskiq example task"):
30+
[% if enable_logfire %] with logfire.span("Run Taskiq example task"):
3131
return {"value": value}
3232
[% else %] return {"value": value}
3333
[% endif -%]

template/app/[% if database == 'postgresql' %]db[% endif %]/base.py.jinja

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
[% if orm == "sqlalchemy" %]from sqlalchemy.orm import DeclarativeBase
3+
[% if orm_type == "sqlalchemy" %]from sqlalchemy.orm import DeclarativeBase
44

55

66
class Base(DeclarativeBase):

template/app/[% if database == 'postgresql' %]db[% endif %]/session.py.jinja

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ from contextlib import asynccontextmanager
55

66
from sqlalchemy import text
77
from sqlalchemy.exc import SQLAlchemyError
8-
[% if orm == "sqlalchemy" %]from sqlalchemy.ext.asyncio import (
8+
[% if orm_type == "sqlalchemy" %]from sqlalchemy.ext.asyncio import (
99
AsyncEngine,
1010
AsyncSession,
1111
async_sessionmaker,
@@ -14,8 +14,8 @@ from sqlalchemy.exc import SQLAlchemyError
1414
[% else %]from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
1515
from sqlmodel.ext.asyncio.session import AsyncSession
1616
[% endif %]
17-
from app.core.exceptions import PersistenceError
18-
[% if telemetry == "logfire" %]from app.core.telemetry import instrument_database
17+
from app.core.exceptions import DatabaseError
18+
[% if enable_logfire %]from app.core.telemetry import instrument_database
1919
[% endif %]
2020

2121
class DatabaseSessionManager:
@@ -29,7 +29,7 @@ class DatabaseSessionManager:
2929
if self._engine is not None:
3030
raise RuntimeError("Database session manager is already initialized")
3131
engine = create_async_engine(database_url, pool_pre_ping=True)
32-
[% if telemetry == "logfire" %] instrument_database(engine)
32+
[% if enable_logfire %] instrument_database(engine)
3333
[% endif %]
3434
session_factory = async_sessionmaker(
3535
bind=engine,
@@ -49,7 +49,7 @@ class DatabaseSessionManager:
4949
await session.commit()
5050
except SQLAlchemyError as exc:
5151
await session.rollback()
52-
raise PersistenceError(resource="transaction") from exc
52+
raise DatabaseError() from exc
5353
except BaseException:
5454
await session.rollback()
5555
raise

template/app/api/[% if agent_capability == 'pydantic-ai' %]agent.py[% endif %].jinja renamed to template/app/api/[% if ai_framework == 'pydantic_ai' %]agent.py[% endif %].jinja

File renamed without changes.

template/app/api/health.py.jinja

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
"""Public health probes."""
22

3+
[% if database == "postgresql" or enable_redis %]from datetime import UTC, datetime
4+
[% endif -%]
35
from typing import Any
46

57
from fastapi import APIRouter
8+
from fastapi.responses import JSONResponse
69

710
from app.core.config import settings
8-
from app.schemas.base import HealthDetailResponse, HealthResponse
11+
[% if enable_redis %]from app.core.redis import redis_resources
12+
[% endif %][% if database == "postgresql" %]from app.db.session import database
13+
[% endif %]from app.schemas.base import HealthDetailResponse, HealthResponse
914
from app.services.health import build_health_response
1015

1116
router = APIRouter(tags=["health"])
@@ -31,5 +36,42 @@ async def liveness_probe() -> dict[str, Any]:
3136

3237

3338
@router.get("/health/ready", response_model=None)
34-
async def readiness_probe() -> dict[str, Any]:
35-
return build_health_response(status="ready", checks={})
39+
async def readiness_probe() -> dict[str, Any] | JSONResponse:
40+
checks: dict[str, dict[str, Any]] = {}
41+
[% if database == "postgresql" %]
42+
try:
43+
started_at = datetime.now(UTC)
44+
await database.check_connection()
45+
latency_ms = (datetime.now(UTC) - started_at).total_seconds() * 1000
46+
checks["database"] = {
47+
"status": "healthy",
48+
"latency_ms": round(latency_ms, 2),
49+
"type": "postgresql",
50+
}
51+
except Exception as exc:
52+
checks["database"] = {
53+
"status": "unhealthy",
54+
"error": str(exc),
55+
"type": "postgresql",
56+
}
57+
[% endif %][% if enable_redis %]
58+
try:
59+
started_at = datetime.now(UTC)
60+
is_healthy = await redis_resources.client.ping()
61+
latency_ms = (datetime.now(UTC) - started_at).total_seconds() * 1000
62+
checks["redis"] = (
63+
{"status": "healthy", "latency_ms": round(latency_ms, 2)}
64+
if is_healthy
65+
else {"status": "unhealthy", "error": "Ping failed"}
66+
)
67+
except Exception as exc:
68+
checks["redis"] = {"status": "unhealthy", "error": str(exc)}
69+
[% endif %] all_healthy = all(check["status"] == "healthy" for check in checks.values())
70+
response_data = build_health_response(
71+
status="ready" if all_healthy else "not_ready",
72+
checks=checks,
73+
)
74+
response_data.update(checks)
75+
if not all_healthy:
76+
return JSONResponse(status_code=503, content=response_data)
77+
return response_data

0 commit comments

Comments
 (0)