|
3 | 3 | Shows how to use shield decorators as FastAPI ``Depends()`` dependencies |
4 | 4 | instead of (or alongside) the middleware model. |
5 | 5 |
|
6 | | -Call ``configure_shield(app, engine)`` once and all decorator deps |
7 | | -(``maintenance``, ``disabled``, ``env_only``) find the engine automatically |
8 | | -via ``request.app.state.shield_engine`` — no ``engine=`` argument per route. |
9 | | -
|
10 | | -``ShieldMiddleware`` calls ``configure_shield`` automatically at ASGI startup, |
11 | | -so if you use middleware you don't need to call it manually. |
12 | | -
|
13 | | -Three patterns shown side by side: |
14 | | -
|
15 | | -1. **Decorator only** — ``@maintenance(reason="...")`` stamps ``__shield_meta__`` |
16 | | - on the function; ``ShieldRouter`` registers the state at startup; |
17 | | - ``ShieldMiddleware`` enforces it globally. |
18 | | -
|
19 | | -2. **Dep (zero-config)** — ``Depends(maintenance(reason="..."))`` with |
20 | | - ``configure_shield`` called once. Engine resolved from ``app.state`` |
21 | | - automatically. Toggle at runtime via CLI or dashboard without redeploying. |
22 | | -
|
23 | | -3. **Dep (explicit engine)** — ``Depends(maintenance(reason="...", engine=engine))``. |
24 | | - Targets a specific engine; useful when running multiple engines side by side. |
| 6 | +Call ``configure_shield(app, engine)`` once and all decorator deps find the |
| 7 | +engine automatically via ``request.app.state.shield_engine`` — no ``engine=`` |
| 8 | +argument per route. ``ShieldMiddleware`` calls ``configure_shield`` |
| 9 | +automatically at ASGI startup, so if you use middleware you don't need to |
| 10 | +call it manually. |
| 11 | +
|
| 12 | +Decorator support as ``Depends()``: |
| 13 | +
|
| 14 | + ✅ maintenance — raises 503 when route is in maintenance |
| 15 | + ✅ disabled — raises 503 when route is disabled |
| 16 | + ✅ env_only — raises 404 when accessed from the wrong environment |
| 17 | + ✅ deprecated — injects Deprecation / Sunset / Link headers on the response |
| 18 | + ❌ force_active — decorator-only; shield checks run in the middleware, which |
| 19 | + completes before any dependency is resolved. A dependency |
| 20 | + has no mechanism to retroactively bypass that check. |
25 | 21 |
|
26 | 22 | Run: |
27 | 23 | uv run uvicorn examples.fastapi.dependency_injection:app --reload |
|
37 | 33 |
|
38 | 34 | Try these requests: |
39 | 35 |
|
40 | | - curl http://localhost:8000/payments # → 503 MAINTENANCE_MODE |
41 | | - shield enable /payments # toggle off without redeploy |
42 | | - curl http://localhost:8000/payments # → 200 |
| 36 | + curl -i http://localhost:8000/payments # → 503 MAINTENANCE_MODE |
| 37 | + shield enable /payments # toggle off without redeploy |
| 38 | + curl -i http://localhost:8000/payments # → 200 |
43 | 39 |
|
44 | | - curl http://localhost:8000/old-endpoint # → 503 ROUTE_DISABLED |
45 | | - shield enable /old-endpoint # re-enable |
46 | | - curl http://localhost:8000/old-endpoint # → 200 |
47 | | -
|
48 | | - curl http://localhost:8000/debug # → 404 (production env) |
49 | | - APP_ENV=dev uv run uvicorn ... # → 200 |
50 | | -
|
51 | | - curl http://localhost:8000/health # → 200 always |
| 40 | + curl -i http://localhost:8000/old-endpoint # → 503 ROUTE_DISABLED |
| 41 | + curl -i http://localhost:8000/debug # → 404 in production env; set APP_ENV=production |
| 42 | + curl -i http://localhost:8000/v1/users # → 200 + Deprecation headers |
| 43 | + curl -i http://localhost:8000/health # → 200 always |
52 | 44 | """ |
53 | 45 |
|
54 | 46 | import os |
|
61 | 53 | ShieldMiddleware, |
62 | 54 | ShieldRouter, |
63 | 55 | apply_shield_to_openapi, |
| 56 | + deprecated, |
64 | 57 | disabled, |
65 | 58 | env_only, |
66 | 59 | force_active, |
67 | 60 | maintenance, |
68 | 61 | ) |
69 | 62 |
|
70 | | -CURRENT_ENV = os.getenv("APP_ENV", "production") |
| 63 | +CURRENT_ENV = os.getenv("APP_ENV", "dev") |
71 | 64 | engine = make_engine(current_env=CURRENT_ENV) |
72 | 65 | router = ShieldRouter(engine=engine) |
73 | 66 |
|
@@ -138,6 +131,37 @@ async def debug(): |
138 | 131 | return {"env": CURRENT_ENV} |
139 | 132 |
|
140 | 133 |
|
| 134 | +# @deprecated as a Depends() — injects Deprecation, Sunset, and Link headers |
| 135 | +# directly on the response without needing the middleware to do it. |
| 136 | +# Use this when you want header injection at the handler level rather than |
| 137 | +# globally via middleware. |
| 138 | +@router.get( |
| 139 | + "/v1/users", |
| 140 | + dependencies=[ |
| 141 | + Depends( |
| 142 | + deprecated( |
| 143 | + sunset="Sat, 01 Jan 2027 00:00:00 GMT", |
| 144 | + use_instead="/v2/users", |
| 145 | + ) |
| 146 | + ) |
| 147 | + ], |
| 148 | +) |
| 149 | +@deprecated(sunset="Sat, 01 Jan 2027 00:00:00 GMT", use_instead="/v2/users") |
| 150 | +async def v1_users(): |
| 151 | + """200 always, but carries Deprecation + Sunset + Link response headers.""" |
| 152 | + return {"users": [{"id": 1, "name": "Alice"}]} |
| 153 | + |
| 154 | + |
| 155 | +@router.get("/v2/users") |
| 156 | +async def v2_users(): |
| 157 | + """Active successor to /v1/users.""" |
| 158 | + return {"users": [{"id": 1, "name": "Alice"}]} |
| 159 | + |
| 160 | + |
| 161 | +# @force_active cannot be used as a Depends() — see module docstring for why. |
| 162 | +# It is applied as a decorator only. |
| 163 | + |
| 164 | + |
141 | 165 | app.include_router(router) |
142 | 166 | apply_shield_to_openapi(app, engine) |
143 | 167 |
|
|
0 commit comments