@malamute/ai-rules 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +270 -121
  2. package/bin/cli.js +5 -2
  3. package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
  4. package/configs/_shared/.claude/rules/conventions/git.md +265 -0
  5. package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
  6. package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
  7. package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
  8. package/configs/_shared/.claude/rules/devops/docker.md +275 -0
  9. package/configs/_shared/.claude/rules/devops/nx.md +194 -0
  10. package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
  11. package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
  12. package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
  13. package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
  14. package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
  15. package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
  16. package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
  17. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
  18. package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
  19. package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
  20. package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
  21. package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
  22. package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
  23. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
  24. package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
  25. package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
  26. package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
  27. package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
  28. package/configs/_shared/.claude/rules/quality/logging.md +45 -0
  29. package/configs/_shared/.claude/rules/quality/observability.md +240 -0
  30. package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
  31. package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
  32. package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
  33. package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
  34. package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
  35. package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
  36. package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
  37. package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
  38. package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
  39. package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
  40. package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
  41. package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
  42. package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
  43. package/configs/_shared/CLAUDE.md +52 -149
  44. package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
  45. package/configs/angular/.claude/rules/core/resource.md +285 -0
  46. package/configs/angular/.claude/rules/core/signals.md +323 -0
  47. package/configs/angular/.claude/rules/http.md +338 -0
  48. package/configs/angular/.claude/rules/routing.md +291 -0
  49. package/configs/angular/.claude/rules/ssr.md +312 -0
  50. package/configs/angular/.claude/rules/state/signal-store.md +408 -0
  51. package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
  52. package/configs/angular/.claude/rules/testing.md +7 -7
  53. package/configs/angular/.claude/rules/ui/aria.md +422 -0
  54. package/configs/angular/.claude/rules/ui/forms.md +424 -0
  55. package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
  56. package/configs/angular/.claude/settings.json +1 -0
  57. package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
  58. package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
  59. package/configs/angular/CLAUDE.md +24 -216
  60. package/configs/dotnet/.claude/rules/background-services.md +552 -0
  61. package/configs/dotnet/.claude/rules/configuration.md +426 -0
  62. package/configs/dotnet/.claude/rules/ddd.md +447 -0
  63. package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
  64. package/configs/dotnet/.claude/rules/mediatr.md +320 -0
  65. package/configs/dotnet/.claude/rules/middleware.md +489 -0
  66. package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
  67. package/configs/dotnet/.claude/rules/validation.md +388 -0
  68. package/configs/dotnet/.claude/settings.json +21 -3
  69. package/configs/dotnet/CLAUDE.md +53 -286
  70. package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
  71. package/configs/fastapi/.claude/rules/dependencies.md +170 -0
  72. package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
  73. package/configs/fastapi/.claude/rules/lifespan.md +274 -0
  74. package/configs/fastapi/.claude/rules/middleware.md +229 -0
  75. package/configs/fastapi/.claude/rules/pydantic.md +433 -0
  76. package/configs/fastapi/.claude/rules/responses.md +251 -0
  77. package/configs/fastapi/.claude/rules/routers.md +202 -0
  78. package/configs/fastapi/.claude/rules/security.md +222 -0
  79. package/configs/fastapi/.claude/rules/testing.md +251 -0
  80. package/configs/fastapi/.claude/rules/websockets.md +298 -0
  81. package/configs/fastapi/.claude/settings.json +33 -0
  82. package/configs/fastapi/CLAUDE.md +144 -0
  83. package/configs/flask/.claude/rules/blueprints.md +208 -0
  84. package/configs/flask/.claude/rules/cli.md +285 -0
  85. package/configs/flask/.claude/rules/configuration.md +281 -0
  86. package/configs/flask/.claude/rules/context.md +238 -0
  87. package/configs/flask/.claude/rules/error-handlers.md +278 -0
  88. package/configs/flask/.claude/rules/extensions.md +278 -0
  89. package/configs/flask/.claude/rules/flask.md +171 -0
  90. package/configs/flask/.claude/rules/marshmallow.md +206 -0
  91. package/configs/flask/.claude/rules/security.md +267 -0
  92. package/configs/flask/.claude/rules/testing.md +284 -0
  93. package/configs/flask/.claude/settings.json +33 -0
  94. package/configs/flask/CLAUDE.md +166 -0
  95. package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
  96. package/configs/nestjs/.claude/rules/filters.md +376 -0
  97. package/configs/nestjs/.claude/rules/interceptors.md +317 -0
  98. package/configs/nestjs/.claude/rules/middleware.md +321 -0
  99. package/configs/nestjs/.claude/rules/modules.md +26 -0
  100. package/configs/nestjs/.claude/rules/pipes.md +351 -0
  101. package/configs/nestjs/.claude/rules/websockets.md +451 -0
  102. package/configs/nestjs/.claude/settings.json +16 -2
  103. package/configs/nestjs/CLAUDE.md +57 -215
  104. package/configs/nextjs/.claude/rules/api-routes.md +358 -0
  105. package/configs/nextjs/.claude/rules/authentication.md +355 -0
  106. package/configs/nextjs/.claude/rules/components.md +52 -0
  107. package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
  108. package/configs/nextjs/.claude/rules/database.md +400 -0
  109. package/configs/nextjs/.claude/rules/middleware.md +303 -0
  110. package/configs/nextjs/.claude/rules/routing.md +324 -0
  111. package/configs/nextjs/.claude/rules/seo.md +350 -0
  112. package/configs/nextjs/.claude/rules/server-actions.md +353 -0
  113. package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
  114. package/configs/nextjs/.claude/settings.json +5 -0
  115. package/configs/nextjs/CLAUDE.md +69 -331
  116. package/package.json +23 -9
  117. package/src/cli.js +220 -0
  118. package/src/config.js +29 -0
  119. package/src/index.js +13 -0
  120. package/src/installer.js +361 -0
  121. package/src/merge.js +116 -0
  122. package/src/tech-config.json +29 -0
  123. package/src/utils.js +96 -0
  124. package/configs/python/.claude/rules/flask.md +0 -332
  125. package/configs/python/.claude/settings.json +0 -18
  126. package/configs/python/CLAUDE.md +0 -273
  127. package/src/install.js +0 -315
  128. /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
  129. /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
  130. /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
  131. /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
  132. /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
  133. /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
@@ -0,0 +1,170 @@
1
+ ---
2
+ paths:
3
+ - "**/*.py"
4
+ ---
5
+
6
+ # FastAPI Dependency Injection
7
+
8
+ ## Basic Dependencies
9
+
10
+ ```python
11
+ # GOOD - reusable dependency
12
+ from typing import Annotated
13
+ from fastapi import Depends
14
+
15
+ async def get_db():
16
+ async with async_session() as session:
17
+ yield session
18
+
19
+ DbSession = Annotated[AsyncSession, Depends(get_db)]
20
+
21
+ @router.get("/users/{user_id}")
22
+ async def get_user(user_id: int, db: DbSession) -> User:
23
+ return await db.get(User, user_id)
24
+ ```
25
+
26
+ ## Dependency with Parameters
27
+
28
+ ```python
29
+ # GOOD - parameterized dependency
30
+ def get_pagination(
31
+ page: int = Query(1, ge=1),
32
+ size: int = Query(20, ge=1, le=100),
33
+ ) -> Pagination:
34
+ return Pagination(page=page, size=size)
35
+
36
+ Paginated = Annotated[Pagination, Depends(get_pagination)]
37
+
38
+ @router.get("/users")
39
+ async def list_users(pagination: Paginated) -> Page[User]:
40
+ ...
41
+ ```
42
+
43
+ ## Class-Based Dependencies
44
+
45
+ ```python
46
+ # GOOD - stateful dependency
47
+ class RateLimiter:
48
+ def __init__(self, requests_per_minute: int):
49
+ self.rpm = requests_per_minute
50
+ self.requests: dict[str, list[float]] = {}
51
+
52
+ async def __call__(self, request: Request) -> None:
53
+ client_ip = request.client.host
54
+ now = time.time()
55
+
56
+ # Clean old requests
57
+ self.requests[client_ip] = [
58
+ t for t in self.requests.get(client_ip, [])
59
+ if now - t < 60
60
+ ]
61
+
62
+ if len(self.requests[client_ip]) >= self.rpm:
63
+ raise HTTPException(429, "Rate limit exceeded")
64
+
65
+ self.requests[client_ip].append(now)
66
+
67
+ rate_limiter = RateLimiter(requests_per_minute=60)
68
+
69
+ @router.get("/api/data", dependencies=[Depends(rate_limiter)])
70
+ async def get_data():
71
+ ...
72
+ ```
73
+
74
+ ## Authentication Dependencies
75
+
76
+ ```python
77
+ # GOOD - reusable auth dependency
78
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
79
+
80
+ async def get_current_user(
81
+ token: Annotated[str, Depends(oauth2_scheme)],
82
+ db: DbSession,
83
+ ) -> User:
84
+ payload = verify_token(token)
85
+ if not payload:
86
+ raise HTTPException(401, "Invalid token")
87
+
88
+ user = await db.get(User, payload["sub"])
89
+ if not user:
90
+ raise HTTPException(401, "User not found")
91
+
92
+ return user
93
+
94
+ CurrentUser = Annotated[User, Depends(get_current_user)]
95
+
96
+ # Role-based dependency
97
+ def require_role(role: str):
98
+ async def check_role(user: CurrentUser) -> User:
99
+ if user.role != role:
100
+ raise HTTPException(403, "Insufficient permissions")
101
+ return user
102
+ return check_role
103
+
104
+ AdminUser = Annotated[User, Depends(require_role("admin"))]
105
+ ```
106
+
107
+ ## Dependency Chaining
108
+
109
+ ```python
110
+ # Dependencies can depend on other dependencies
111
+ async def get_user_service(db: DbSession) -> UserService:
112
+ return UserService(db)
113
+
114
+ async def get_current_user_with_service(
115
+ token: Annotated[str, Depends(oauth2_scheme)],
116
+ service: Annotated[UserService, Depends(get_user_service)],
117
+ ) -> User:
118
+ return await service.get_by_token(token)
119
+ ```
120
+
121
+ ## Global Dependencies
122
+
123
+ ```python
124
+ # Apply to all routes in router
125
+ router = APIRouter(
126
+ prefix="/admin",
127
+ dependencies=[Depends(require_role("admin"))],
128
+ )
129
+
130
+ # Apply to entire app
131
+ app = FastAPI(dependencies=[Depends(verify_api_key)])
132
+ ```
133
+
134
+ ## Yield Dependencies (Context Managers)
135
+
136
+ ```python
137
+ # GOOD - cleanup after request
138
+ async def get_db_transaction():
139
+ async with async_session() as session:
140
+ async with session.begin():
141
+ yield session
142
+ # Commit happens automatically if no exception
143
+ # Rollback happens automatically on exception
144
+
145
+ # GOOD - resource cleanup
146
+ async def get_temp_file():
147
+ path = Path(tempfile.mktemp())
148
+ try:
149
+ yield path
150
+ finally:
151
+ path.unlink(missing_ok=True)
152
+ ```
153
+
154
+ ## Testing Dependencies
155
+
156
+ ```python
157
+ # Override dependencies in tests
158
+ from fastapi.testclient import TestClient
159
+
160
+ def get_test_db():
161
+ return TestDatabase()
162
+
163
+ app.dependency_overrides[get_db] = get_test_db
164
+
165
+ with TestClient(app) as client:
166
+ response = client.get("/users")
167
+
168
+ # Clean up
169
+ app.dependency_overrides.clear()
170
+ ```
@@ -1,6 +1,11 @@
1
1
  ---
2
2
  paths:
3
- - "**/*.py"
3
+ - "**/routers/**/*.py"
4
+ - "**/routes/**/*.py"
5
+ - "**/api/**/*.py"
6
+ - "**/endpoints/**/*.py"
7
+ - "**/main.py"
8
+ - "**/app.py"
4
9
  ---
5
10
 
6
11
  # FastAPI Rules
@@ -270,3 +275,58 @@ app = FastAPI(
270
275
  if settings.environment == "production":
271
276
  app = FastAPI(docs_url=None, redoc_url=None)
272
277
  ```
278
+
279
+ ## Type Hints
280
+
281
+ Always use modern syntax (Python 3.10+):
282
+
283
+ ```python
284
+ # Modern type hints
285
+ async def get_user(user_id: int) -> User | None:
286
+ ...
287
+
288
+ def process_items(items: list[str]) -> dict[str, int]:
289
+ ...
290
+
291
+ # Annotated for dependency injection
292
+ DbSession = Annotated[AsyncSession, Depends(get_db)]
293
+ CurrentUser = Annotated[User, Depends(get_current_user)]
294
+
295
+ @router.get("/{user_id}")
296
+ async def get_user(
297
+ user_id: int,
298
+ db: DbSession,
299
+ current_user: CurrentUser,
300
+ ) -> UserResponse:
301
+ ...
302
+ ```
303
+
304
+ ## Custom Exception Handling
305
+
306
+ ```python
307
+ # Custom exceptions
308
+ class NotFoundError(Exception):
309
+ def __init__(self, resource: str, id: int):
310
+ self.resource = resource
311
+ self.id = id
312
+
313
+ class BusinessError(Exception):
314
+ def __init__(self, message: str, code: str):
315
+ self.message = message
316
+ self.code = code
317
+
318
+ # FastAPI exception handlers
319
+ @app.exception_handler(NotFoundError)
320
+ async def not_found_handler(request: Request, exc: NotFoundError):
321
+ return JSONResponse(
322
+ status_code=404,
323
+ content={"detail": f"{exc.resource} {exc.id} not found"}
324
+ )
325
+
326
+ @app.exception_handler(BusinessError)
327
+ async def business_error_handler(request: Request, exc: BusinessError):
328
+ return JSONResponse(
329
+ status_code=422,
330
+ content={"detail": exc.message, "code": exc.code}
331
+ )
332
+ ```
@@ -0,0 +1,274 @@
1
+ ---
2
+ paths:
3
+ - "**/*.py"
4
+ ---
5
+
6
+ # FastAPI Lifespan Management
7
+
8
+ ## Lifespan Context Manager
9
+
10
+ ```python
11
+ from contextlib import asynccontextmanager
12
+ from fastapi import FastAPI
13
+
14
+ @asynccontextmanager
15
+ async def lifespan(app: FastAPI):
16
+ # Startup: runs before app starts accepting requests
17
+ print("Starting up...")
18
+ app.state.db = await create_database_pool()
19
+ app.state.redis = await create_redis_pool()
20
+
21
+ yield # App runs here
22
+
23
+ # Shutdown: runs when app is shutting down
24
+ print("Shutting down...")
25
+ await app.state.db.close()
26
+ await app.state.redis.close()
27
+
28
+ app = FastAPI(lifespan=lifespan)
29
+ ```
30
+
31
+ ## Database Connection Pool
32
+
33
+ ```python
34
+ from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
35
+
36
+ @asynccontextmanager
37
+ async def lifespan(app: FastAPI):
38
+ # Create engine
39
+ engine = create_async_engine(
40
+ settings.database_url,
41
+ pool_size=10,
42
+ max_overflow=20,
43
+ pool_pre_ping=True,
44
+ )
45
+
46
+ # Create session factory
47
+ app.state.async_session = async_sessionmaker(
48
+ engine,
49
+ expire_on_commit=False,
50
+ )
51
+
52
+ yield
53
+
54
+ # Dispose engine
55
+ await engine.dispose()
56
+
57
+ # Dependency
58
+ async def get_db() -> AsyncGenerator[AsyncSession, None]:
59
+ async with app.state.async_session() as session:
60
+ yield session
61
+ ```
62
+
63
+ ## Redis Connection
64
+
65
+ ```python
66
+ import redis.asyncio as redis
67
+
68
+ @asynccontextmanager
69
+ async def lifespan(app: FastAPI):
70
+ # Redis pool
71
+ app.state.redis = await redis.from_url(
72
+ settings.redis_url,
73
+ encoding="utf-8",
74
+ decode_responses=True,
75
+ )
76
+
77
+ yield
78
+
79
+ await app.state.redis.close()
80
+
81
+ # Usage
82
+ @router.get("/cached/{key}")
83
+ async def get_cached(key: str, request: Request) -> dict:
84
+ value = await request.app.state.redis.get(key)
85
+ return {"key": key, "value": value}
86
+ ```
87
+
88
+ ## HTTP Client Pool
89
+
90
+ ```python
91
+ import httpx
92
+
93
+ @asynccontextmanager
94
+ async def lifespan(app: FastAPI):
95
+ # Create shared HTTP client
96
+ app.state.http_client = httpx.AsyncClient(
97
+ timeout=30.0,
98
+ limits=httpx.Limits(max_connections=100),
99
+ )
100
+
101
+ yield
102
+
103
+ await app.state.http_client.aclose()
104
+
105
+ # Usage
106
+ async def call_external_api(request: Request) -> dict:
107
+ client = request.app.state.http_client
108
+ response = await client.get("https://api.example.com/data")
109
+ return response.json()
110
+ ```
111
+
112
+ ## Background Task Scheduler
113
+
114
+ ```python
115
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
116
+
117
+ @asynccontextmanager
118
+ async def lifespan(app: FastAPI):
119
+ scheduler = AsyncIOScheduler()
120
+
121
+ # Add scheduled jobs
122
+ scheduler.add_job(
123
+ cleanup_expired_sessions,
124
+ "interval",
125
+ hours=1,
126
+ id="cleanup_sessions",
127
+ )
128
+
129
+ scheduler.add_job(
130
+ send_daily_reports,
131
+ "cron",
132
+ hour=9,
133
+ minute=0,
134
+ id="daily_reports",
135
+ )
136
+
137
+ scheduler.start()
138
+ app.state.scheduler = scheduler
139
+
140
+ yield
141
+
142
+ scheduler.shutdown(wait=True)
143
+ ```
144
+
145
+ ## Multiple Resources
146
+
147
+ ```python
148
+ @asynccontextmanager
149
+ async def lifespan(app: FastAPI):
150
+ # Initialize all resources
151
+ engine = create_async_engine(settings.database_url)
152
+ redis_pool = await redis.from_url(settings.redis_url)
153
+ http_client = httpx.AsyncClient()
154
+
155
+ # Store in app state
156
+ app.state.db_engine = engine
157
+ app.state.async_session = async_sessionmaker(engine)
158
+ app.state.redis = redis_pool
159
+ app.state.http_client = http_client
160
+
161
+ # Run migrations
162
+ async with engine.begin() as conn:
163
+ await conn.run_sync(Base.metadata.create_all)
164
+
165
+ # Log startup
166
+ logger.info("Application started", extra={
167
+ "database": settings.database_url,
168
+ "redis": settings.redis_url,
169
+ })
170
+
171
+ yield
172
+
173
+ # Cleanup in reverse order
174
+ await http_client.aclose()
175
+ await redis_pool.close()
176
+ await engine.dispose()
177
+
178
+ logger.info("Application shutdown complete")
179
+ ```
180
+
181
+ ## Health Checks with Lifespan
182
+
183
+ ```python
184
+ from dataclasses import dataclass, field
185
+ from datetime import datetime
186
+
187
+ @dataclass
188
+ class AppHealth:
189
+ started_at: datetime = field(default_factory=datetime.utcnow)
190
+ ready: bool = False
191
+ checks: dict = field(default_factory=dict)
192
+
193
+ @asynccontextmanager
194
+ async def lifespan(app: FastAPI):
195
+ health = AppHealth()
196
+ app.state.health = health
197
+
198
+ # Initialize resources
199
+ try:
200
+ app.state.db = await create_database_pool()
201
+ health.checks["database"] = "ok"
202
+ except Exception as e:
203
+ health.checks["database"] = f"error: {e}"
204
+
205
+ try:
206
+ app.state.redis = await create_redis_pool()
207
+ health.checks["redis"] = "ok"
208
+ except Exception as e:
209
+ health.checks["redis"] = f"error: {e}"
210
+
211
+ health.ready = all(v == "ok" for v in health.checks.values())
212
+
213
+ yield
214
+
215
+ health.ready = False
216
+ await cleanup_resources(app)
217
+
218
+ @router.get("/health/live")
219
+ async def liveness() -> dict:
220
+ return {"status": "ok"}
221
+
222
+ @router.get("/health/ready")
223
+ async def readiness(request: Request) -> dict:
224
+ health = request.app.state.health
225
+
226
+ if not health.ready:
227
+ raise HTTPException(503, detail=health.checks)
228
+
229
+ return {
230
+ "status": "ready",
231
+ "uptime": (datetime.utcnow() - health.started_at).total_seconds(),
232
+ "checks": health.checks,
233
+ }
234
+ ```
235
+
236
+ ## Deprecated: on_event
237
+
238
+ ```python
239
+ # DEPRECATED - Don't use this pattern
240
+ @app.on_event("startup")
241
+ async def startup():
242
+ ...
243
+
244
+ @app.on_event("shutdown")
245
+ async def shutdown():
246
+ ...
247
+
248
+ # Use lifespan instead (see above)
249
+ ```
250
+
251
+ ## Testing with Lifespan
252
+
253
+ ```python
254
+ import pytest
255
+ from httpx import AsyncClient, ASGITransport
256
+
257
+ @pytest.fixture
258
+ async def client():
259
+ # Lifespan is automatically handled
260
+ async with AsyncClient(
261
+ transport=ASGITransport(app=app),
262
+ base_url="http://test",
263
+ ) as client:
264
+ yield client
265
+
266
+ # Or override lifespan for tests
267
+ @asynccontextmanager
268
+ async def test_lifespan(app: FastAPI):
269
+ app.state.db = MockDatabase()
270
+ app.state.redis = MockRedis()
271
+ yield
272
+
273
+ app_for_testing = FastAPI(lifespan=test_lifespan)
274
+ ```