Route lifecycle management for APIs — maintenance mode, env gating, deprecation, and more
Project description
Route(API) lifecycle management for Python web frameworks — maintenance mode, environment gating, deprecation, rate limiting, admin panels, and more. No restarts required.
[!WARNING] Early Access —
api-shieldis fully functional and ready to use. We are actively building on top of a solid foundation and your real-world experience is invaluable at this stage. If you have feedback, feature ideas, or suggestions, please open an issue — every voice helps shape the roadmap.
Key features
| Feature | Description |
|---|---|
| 🎨 Decorator-first DX | Route state lives next to the route definition, not in a separate config file |
| ⚡ Zero-restart control | State changes take effect immediately — no redeployment or server restart needed |
| 🛡️ Fail-open by default | If the backend is unreachable, requests pass through. Shield never takes down your API |
| 🔌 Pluggable backends | In-memory (default), file-based JSON, or Redis for multi-instance deployments |
| 🖥️ Admin dashboard | HTMX-powered UI with live SSE updates — no JS framework required |
| 🖱️ REST API + CLI | Full programmatic control from the terminal or CI pipelines — works over HTTPS remotely |
| 📄 OpenAPI integration | Disabled / env-gated routes hidden from /docs; deprecated routes flagged automatically |
| 📋 Audit log | Every state change is recorded: who, when, what route, old status → new status |
| ⏰ Scheduled windows | asyncio-native scheduler — maintenance windows activate and deactivate automatically |
| 🔔 Webhooks | Fire HTTP POST on every state change — built-in Slack formatter and custom formatters supported |
| 🎨 Custom responses | Return HTML, redirects, or any response shape for blocked routes — per-route or app-wide default |
| 🚦 Rate limiting | Per-IP, per-user, per-API-key, or global counters — tiered limits, burst allowance, runtime mutation |
Install
uv add "api-shield[all]"
# or: pip install "api-shield[all]"
Quickstart
from fastapi import FastAPI
from shield.core.config import make_engine
from shield.fastapi import (
ShieldMiddleware, ShieldAdmin, apply_shield_to_openapi,
maintenance, env_only, disabled, force_active, deprecated,
)
engine = make_engine()
app = FastAPI()
app.add_middleware(ShieldMiddleware, engine=engine)
@app.get("/payments")
@maintenance(reason="DB migration — back at 04:00 UTC")
async def get_payments():
return {"payments": []}
@app.get("/health")
@force_active
async def health():
return {"status": "ok"}
apply_shield_to_openapi(app, engine)
app.mount("/shield", ShieldAdmin(engine=engine, auth=("admin", "secret")))
GET /payments → 503 {"error": {"code": "MAINTENANCE_MODE", ...}}
GET /health → 200 always
Manage routes from the CLI — no code changes, no restarts:
shield config set-url http://localhost:8000/shield
shield login admin
shield status
shield enable GET:/payments
shield global enable --reason "Deploying v2" --exempt /health
Decorators
| Decorator | Effect | Status |
|---|---|---|
@maintenance(reason, start, end) |
Temporarily unavailable | 503 |
@disabled(reason) |
Permanently off | 503 |
@env_only("dev", "staging") |
Restricted to named environments | 404 elsewhere |
@deprecated(sunset, use_instead) |
Still works, injects deprecation headers | 200 |
@force_active |
Bypasses all shield checks | Always 200 |
@rate_limit("100/minute") |
Cap requests per IP, user, API key, or globally | 429 |
Custom responses
By default, blocked routes return a structured JSON error body. You can replace it with anything — HTML, a redirect, plain text, or your own JSON — in two ways:
Per-route — pass response= directly on the decorator:
from starlette.requests import Request
from starlette.responses import HTMLResponse, RedirectResponse
from shield.fastapi import maintenance, disabled
def maintenance_page(request: Request, exc: Exception) -> HTMLResponse:
return HTMLResponse(
f"<h1>Down for maintenance</h1><p>{exc.reason}</p>", status_code=503
)
@router.get("/payments")
@maintenance(reason="DB migration", response=maintenance_page)
async def payments():
return {"payments": []}
@router.get("/orders")
@maintenance(reason="Upgrade in progress", response=lambda *_: RedirectResponse("/status"))
async def orders():
return {"orders": []}
Global default — set once on ShieldMiddleware, applies to every route without a per-route factory:
app.add_middleware(
ShieldMiddleware,
engine=engine,
responses={
"maintenance": maintenance_page, # all maintenance routes
"disabled": lambda req, exc: HTMLResponse(
f"<h1>Gone</h1><p>{exc.reason}</p>", status_code=503
),
},
)
Resolution order: per-route response= → global responses[...] → built-in JSON. The factory can be sync or async and receives the live Request and the ShieldException that triggered the block.
Rate limiting
from shield.fastapi.decorators import rate_limit
@router.get("/public/posts")
@rate_limit("10/minute") # 10 req/min per IP
async def list_posts():
return {"posts": [...]}
@router.get("/users/me")
@rate_limit("100/minute", key="user") # per authenticated user
async def get_current_user():
...
@router.get("/reports")
@rate_limit( # tiered limits
{"free": "10/minute", "pro": "100/minute", "enterprise": "unlimited"},
key="user",
)
async def get_reports():
...
Policies can be mutated at runtime without redeploying (shield rl and shield rate-limits are aliases):
shield rl set GET:/public/posts 20/minute # raise the limit live
shield rl reset GET:/public/posts # clear counters
shield rl hits # blocked requests log
Requires api-shield[rate-limit]. Powered by limits.
Backends
| Backend | Persistence | Multi-instance |
|---|---|---|
MemoryBackend |
No | No |
FileBackend |
Yes | No |
RedisBackend |
Yes | Yes |
For rate limiting in multi-worker deployments, use RedisBackend — counters are atomic and shared across all processes.
Documentation
Full documentation at attakay78.github.io/api-shield
| Tutorial | Get started in 5 minutes |
| Decorators reference | All decorator options |
| Rate limiting | Per-IP, per-user, tiered limits |
| ShieldEngine reference | Programmatic control |
| Backends | Memory, File, Redis, custom |
| Admin dashboard | Mounting ShieldAdmin |
| CLI reference | All CLI commands |
| Production guide | Monitoring & deployment automation |
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file api_shield-0.5.0.tar.gz.
File metadata
- Download URL: api_shield-0.5.0.tar.gz
- Upload date:
- Size: 1.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a4724811bd1ade0d0bbc52e4f9a1fd84aed83cea4770bf0f0fc0a0f97fadc754
|
|
| MD5 |
1a1fe17e8740531ce4a3114459aa4252
|
|
| BLAKE2b-256 |
982e78307cd253e46baf322866c634de1841f888c483d0f7416658471bbdc920
|
Provenance
The following attestation bundles were made for api_shield-0.5.0.tar.gz:
Publisher:
release.yml on Attakay78/api-shield
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
api_shield-0.5.0.tar.gz -
Subject digest:
a4724811bd1ade0d0bbc52e4f9a1fd84aed83cea4770bf0f0fc0a0f97fadc754 - Sigstore transparency entry: 1117886771
- Sigstore integration time:
-
Permalink:
Attakay78/api-shield@62230ccd36edb276d1056170509a3710d71e0269 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Attakay78
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@62230ccd36edb276d1056170509a3710d71e0269 -
Trigger Event:
push
-
Statement type:
File details
Details for the file api_shield-0.5.0-py3-none-any.whl.
File metadata
- Download URL: api_shield-0.5.0-py3-none-any.whl
- Upload date:
- Size: 161.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e77f8af1f568b36363249120ee365377fd89ecc45a29e2138ce76145d5c71746
|
|
| MD5 |
ee128a02b1cd306d8cfb1340d1500c9f
|
|
| BLAKE2b-256 |
fcc05fc1a76d38a8bd12221a2e1f8ad20b9049733378719ce86a63e26b9cf660
|
Provenance
The following attestation bundles were made for api_shield-0.5.0-py3-none-any.whl:
Publisher:
release.yml on Attakay78/api-shield
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
api_shield-0.5.0-py3-none-any.whl -
Subject digest:
e77f8af1f568b36363249120ee365377fd89ecc45a29e2138ce76145d5c71746 - Sigstore transparency entry: 1117886795
- Sigstore integration time:
-
Permalink:
Attakay78/api-shield@62230ccd36edb276d1056170509a3710d71e0269 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Attakay78
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@62230ccd36edb276d1056170509a3710d71e0269 -
Trigger Event:
push
-
Statement type: