prodkit 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {prodkit-0.2.0 → prodkit-0.3.0}/CHANGELOG.md +32 -0
- prodkit-0.2.0/README.md → prodkit-0.3.0/PKG-INFO +120 -15
- prodkit-0.2.0/PKG-INFO → prodkit-0.3.0/README.md +50 -62
- {prodkit-0.2.0 → prodkit-0.3.0}/pyproject.toml +18 -1
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/__init__.py +7 -1
- prodkit-0.3.0/src/prodkit/contracts/__init__.py +5 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/contracts/plugin.py +8 -2
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/core/config.py +34 -1
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/core/production.py +3 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/plugins/__init__.py +12 -0
- prodkit-0.3.0/src/prodkit/plugins/_redis.py +34 -0
- prodkit-0.3.0/src/prodkit/plugins/cache/__init__.py +150 -0
- prodkit-0.3.0/src/prodkit/plugins/metrics/__init__.py +173 -0
- prodkit-0.3.0/src/prodkit/plugins/rate_limit/__init__.py +169 -0
- prodkit-0.3.0/src/prodkit/plugins/rate_limit/backends.py +117 -0
- prodkit-0.3.0/src/prodkit/plugins/tracing/__init__.py +231 -0
- prodkit-0.3.0/tests/unit/test_cache.py +151 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/tests/unit/test_config.py +44 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/tests/unit/test_doctor.py +5 -2
- prodkit-0.3.0/tests/unit/test_metrics.py +184 -0
- prodkit-0.3.0/tests/unit/test_rate_limit.py +216 -0
- prodkit-0.3.0/tests/unit/test_tracing.py +179 -0
- prodkit-0.2.0/src/prodkit/plugins/rate_limit/__init__.py +0 -147
- prodkit-0.2.0/tests/unit/__init__.py +0 -0
- prodkit-0.2.0/tests/unit/test_rate_limit.py +0 -97
- {prodkit-0.2.0 → prodkit-0.3.0}/.gitignore +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/LICENSE +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/cli/__init__.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/cli/app.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/cli/loader.py +0 -0
- {prodkit-0.2.0/src/prodkit/contracts → prodkit-0.3.0/src/prodkit/core}/__init__.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/core/context.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/core/doctor.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/core/event_bus.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/core/exceptions.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/core/lifecycle.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/core/plugin_manager.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/core/registry.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/plugins/compression/__init__.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/plugins/cors/__init__.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/plugins/errors/__init__.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/plugins/health/__init__.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/plugins/logging/__init__.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/plugins/request_id/__init__.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/plugins/security/__init__.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/src/prodkit/py.typed +0 -0
- {prodkit-0.2.0/src/prodkit/core → prodkit-0.3.0/tests}/__init__.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/tests/conftest.py +0 -0
- {prodkit-0.2.0/tests → prodkit-0.3.0/tests/integration}/__init__.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/tests/integration/test_cli.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/tests/integration/test_logging.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/tests/integration/test_production.py +0 -0
- {prodkit-0.2.0/tests/integration → prodkit-0.3.0/tests/unit}/__init__.py +0 -0
- {prodkit-0.2.0 → prodkit-0.3.0}/tests/unit/test_kernel.py +0 -0
|
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-08-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Prometheus metrics plugin** (`pip install prodkit[metrics]`): request
|
|
14
|
+
count, latency histogram, in-flight gauge. Exposed at `/metrics` (or a
|
|
15
|
+
custom path). Metrics use route templates as labels (bounded cardinality).
|
|
16
|
+
Dedicated `CollectorRegistry` per instance — no global-state collisions.
|
|
17
|
+
- **Cache plugin** (`cache={...}`): a named cache service published in the
|
|
18
|
+
registry under `"cache"`. Memory (per-process LRU with TTL) and Redis
|
|
19
|
+
(shared across workers) backends. Values must be JSON-serializable.
|
|
20
|
+
- **OpenTelemetry tracing plugin** (`pip install prodkit[otel]`): automatic
|
|
21
|
+
request spans with W3C `traceparent` propagation, configurable sampler
|
|
22
|
+
(`sample_rate`), and OTLP/console/none exporters. The `TracerProvider` is
|
|
23
|
+
published in the registry as `"tracer"`.
|
|
24
|
+
- **Redis backend for rate limiting** (`rate_limit.backend="redis"`): shared
|
|
25
|
+
aligned fixed-window counters across all workers and hosts. Fails open on
|
|
26
|
+
Redis errors (availability > strict limiting). Readiness check via `/ready`.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- CI installs `otel` and `metrics` extras for full test coverage.
|
|
30
|
+
- `TracingMiddleware` now uses `context.attach()`/`detach()` for correct W3C
|
|
31
|
+
trace context propagation (previously passed unsupported `context` kwarg).
|
|
32
|
+
- Dev dependencies now include `anyio` and `pytest-anyio` for async tests.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## [0.2.1] - 2026-07-23
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- README polish: PyPI/Python badges, table of contents, `prodkit[cli]` install
|
|
39
|
+
note, rate-limiting shown in the config examples, and a `doctor()` hook in the
|
|
40
|
+
plugin-authoring example. Docs-only release (no code changes).
|
|
41
|
+
|
|
10
42
|
## [0.2.0] - 2026-07-22
|
|
11
43
|
|
|
12
44
|
### Added
|
|
@@ -1,3 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: prodkit
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: The production framework for FastAPI. One line. Production ready.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Pushkarpant/PRODKIT
|
|
6
|
+
Project-URL: Documentation, https://github.com/Pushkarpant/PRODKIT#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/Pushkarpant/PRODKIT
|
|
8
|
+
Project-URL: Changelog, https://github.com/Pushkarpant/PRODKIT/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/Pushkarpant/PRODKIT/issues
|
|
10
|
+
Author-email: Pushkar Pant <pantpushkar4@gmail.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: fastapi,health-check,logging,metrics,middleware,observability,opentelemetry,production,security,tracing
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Framework :: FastAPI
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Requires-Dist: fastapi>=0.110
|
|
28
|
+
Requires-Dist: pydantic-settings>=2.1
|
|
29
|
+
Requires-Dist: pydantic>=2.5
|
|
30
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
31
|
+
Provides-Extra: all
|
|
32
|
+
Requires-Dist: brotli-asgi>=1.4; extra == 'all'
|
|
33
|
+
Requires-Dist: opentelemetry-api>=1.20; extra == 'all'
|
|
34
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.20; extra == 'all'
|
|
35
|
+
Requires-Dist: opentelemetry-sdk>=1.20; extra == 'all'
|
|
36
|
+
Requires-Dist: prometheus-client>=0.20; extra == 'all'
|
|
37
|
+
Requires-Dist: redis>=5.0; extra == 'all'
|
|
38
|
+
Requires-Dist: rich>=13; extra == 'all'
|
|
39
|
+
Requires-Dist: typer>=0.12; extra == 'all'
|
|
40
|
+
Provides-Extra: brotli
|
|
41
|
+
Requires-Dist: brotli-asgi>=1.4; extra == 'brotli'
|
|
42
|
+
Provides-Extra: cli
|
|
43
|
+
Requires-Dist: rich>=13; extra == 'cli'
|
|
44
|
+
Requires-Dist: typer>=0.12; extra == 'cli'
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: anyio[trio]>=4.0; extra == 'dev'
|
|
47
|
+
Requires-Dist: fakeredis[lua]>=2.21; extra == 'dev'
|
|
48
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
49
|
+
Requires-Dist: import-linter>=2.0; extra == 'dev'
|
|
50
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
51
|
+
Requires-Dist: opentelemetry-api>=1.20; extra == 'dev'
|
|
52
|
+
Requires-Dist: opentelemetry-sdk>=1.20; extra == 'dev'
|
|
53
|
+
Requires-Dist: prometheus-client>=0.20; extra == 'dev'
|
|
54
|
+
Requires-Dist: pytest-anyio>=0.0.0; extra == 'dev'
|
|
55
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
56
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
57
|
+
Requires-Dist: rich>=13; extra == 'dev'
|
|
58
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
59
|
+
Requires-Dist: tomli>=2.0; extra == 'dev'
|
|
60
|
+
Requires-Dist: typer>=0.12; extra == 'dev'
|
|
61
|
+
Provides-Extra: metrics
|
|
62
|
+
Requires-Dist: prometheus-client>=0.20; extra == 'metrics'
|
|
63
|
+
Provides-Extra: otel
|
|
64
|
+
Requires-Dist: opentelemetry-api>=1.20; extra == 'otel'
|
|
65
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.20; extra == 'otel'
|
|
66
|
+
Requires-Dist: opentelemetry-sdk>=1.20; extra == 'otel'
|
|
67
|
+
Provides-Extra: redis
|
|
68
|
+
Requires-Dist: redis>=5.0; extra == 'redis'
|
|
69
|
+
Description-Content-Type: text/markdown
|
|
70
|
+
|
|
1
71
|
# ProdKit
|
|
2
72
|
|
|
3
73
|
> **One line. Production ready.**
|
|
@@ -14,14 +84,24 @@ Production(app)
|
|
|
14
84
|
|
|
15
85
|
That's it. Your app now has security headers, structured JSON logging with
|
|
16
86
|
request-ID correlation, RFC 9457 error responses, Kubernetes-ready health
|
|
17
|
-
endpoints,
|
|
18
|
-
hardened for production, and pleasant in development.
|
|
87
|
+
endpoints, gzip compression, and opt-in rate limiting — configured to current
|
|
88
|
+
best practice, hardened for production, and pleasant in development. Then run
|
|
89
|
+
[`prodkit doctor`](#cli--prodkit-doctor) to score how production-ready it is.
|
|
19
90
|
|
|
91
|
+
[](https://pypi.org/project/prodkit/)
|
|
92
|
+
[](https://pypi.org/project/prodkit/)
|
|
20
93
|
[](https://github.com/Pushkarpant/PRODKIT/actions/workflows/ci.yml)
|
|
21
94
|
[](https://github.com/Pushkarpant/PRODKIT/blob/main/LICENSE)
|
|
22
95
|
|
|
23
96
|
---
|
|
24
97
|
|
|
98
|
+
**Contents:** [Why](#why) · [Install](#installation) · [Quick Start](#quick-start) ·
|
|
99
|
+
[What You Get](#what-you-get) · [Configuration](#configuration) ·
|
|
100
|
+
[Plugins](#writing-a-plugin) · [CLI / doctor](#cli--prodkit-doctor) ·
|
|
101
|
+
[Status & Roadmap](#project-status)
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
25
105
|
## Why
|
|
26
106
|
|
|
27
107
|
Every production FastAPI service re-implements the same ~500 lines of glue:
|
|
@@ -37,11 +117,21 @@ evolve, `pip install -U prodkit` updates every app you own.
|
|
|
37
117
|
[**`pip install prodkit`**](https://pypi.org/project/prodkit/)
|
|
38
118
|
|
|
39
119
|
```bash
|
|
40
|
-
pip install prodkit
|
|
120
|
+
pip install prodkit # the library
|
|
121
|
+
pip install "prodkit[cli]" # + the `prodkit doctor` CLI (typer + rich)
|
|
41
122
|
```
|
|
42
123
|
|
|
43
124
|
Requires Python 3.10+ and FastAPI 0.110+. The base install depends only on
|
|
44
|
-
FastAPI and Pydantic — nothing else.
|
|
125
|
+
FastAPI and Pydantic — nothing else. Optional extras:
|
|
126
|
+
|
|
127
|
+
| Extra | Adds |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `cli` | `prodkit doctor` CLI (typer + rich) |
|
|
130
|
+
| `metrics` | Prometheus `/metrics` endpoint |
|
|
131
|
+
| `otel` | OpenTelemetry tracing (OTLP export) |
|
|
132
|
+
| `redis` | Shared Redis backends (rate-limit, cache) |
|
|
133
|
+
| `brotli` | Brotli compression |
|
|
134
|
+
| `all` | Everything above |
|
|
45
135
|
|
|
46
136
|
## Quick Start
|
|
47
137
|
|
|
@@ -69,7 +159,9 @@ x-content-type-options: nosniff
|
|
|
69
159
|
x-frame-options: DENY
|
|
70
160
|
strict-transport-security: max-age=63072000; includeSubDomains
|
|
71
161
|
referrer-policy: strict-origin-when-cross-origin
|
|
72
|
-
|
|
162
|
+
permissions-policy: camera=(), microphone=(), geolocation=()
|
|
163
|
+
x-xss-protection: 0
|
|
164
|
+
content-type: application/json
|
|
73
165
|
```
|
|
74
166
|
|
|
75
167
|
For local development, flip the profile — pretty console logs, debug error
|
|
@@ -90,7 +182,10 @@ Production(app, environment="development")
|
|
|
90
182
|
| ❤️ **Health endpoints** | `/health`, `/live` (liveness) and `/ready` (readiness — aggregates checks from every plugin, 503 until all pass). Kubernetes-native. |
|
|
91
183
|
| 🌐 **CORS** | Explicit origins only; the wildcard-with-credentials footgun is refused at boot. |
|
|
92
184
|
| 📦 **Compression** | Gzip for responses over 500 bytes. |
|
|
93
|
-
| 🚦 **Rate limiting** | Opt-in per-IP limiting (`100/minute`), `429 problem+json` with `Retry-After`.
|
|
185
|
+
| 🚦 **Rate limiting** | Opt-in per-IP limiting (`100/minute`), `429 problem+json` with `Retry-After`. Memory or Redis backend (shared across workers). |
|
|
186
|
+
| 📊 **Prometheus metrics** | Request count, latency histogram, in-flight gauge at `/metrics`. Route-template labels (bounded cardinality). `pip install prodkit[metrics]` |
|
|
187
|
+
| 🗄️ **Cache service** | Memory (LRU + TTL) or Redis backend, published in the registry. `await cache.get(key)` / `.set(key, value, ttl=60)`. |
|
|
188
|
+
| 🔭 **OpenTelemetry tracing** | Automatic request spans with W3C `traceparent` propagation. OTLP, console, or none exporter. `pip install prodkit[otel]` |
|
|
94
189
|
| 🩺 **`prodkit doctor`** | CLI production-readiness audit with a 0–100 score. `--strict` gates CI. |
|
|
95
190
|
| 🔌 **Plugin system** | Every feature above is a plugin. Write your own with optional hooks incl. `doctor()`. |
|
|
96
191
|
|
|
@@ -111,6 +206,7 @@ Production(
|
|
|
111
206
|
cors={"origins": ["https://app.example.com"]}, # dict = configure & enable
|
|
112
207
|
compression=False, # bool = toggle
|
|
113
208
|
security={"trusted_hosts": ["api.example.com"]},
|
|
209
|
+
rate_limit={"default": "100/minute"}, # opt-in per-IP limiting
|
|
114
210
|
)
|
|
115
211
|
```
|
|
116
212
|
|
|
@@ -134,6 +230,10 @@ level = "INFO"
|
|
|
134
230
|
[cors]
|
|
135
231
|
enabled = true
|
|
136
232
|
origins = ["https://app.example.com"]
|
|
233
|
+
|
|
234
|
+
[rate_limit]
|
|
235
|
+
enabled = true
|
|
236
|
+
default = "100/minute"
|
|
137
237
|
```
|
|
138
238
|
|
|
139
239
|
### Fail-fast, refuse-unsafe
|
|
@@ -155,7 +255,7 @@ not warned about:
|
|
|
155
255
|
## Writing a Plugin
|
|
156
256
|
|
|
157
257
|
```python
|
|
158
|
-
from prodkit import Check, Plugin, Production
|
|
258
|
+
from prodkit import Audit, Check, Plugin, Production
|
|
159
259
|
|
|
160
260
|
class DatabasePlugin(Plugin):
|
|
161
261
|
name = "database"
|
|
@@ -167,13 +267,18 @@ class DatabasePlugin(Plugin):
|
|
|
167
267
|
async def shutdown(self, ctx):
|
|
168
268
|
await self.pool.close()
|
|
169
269
|
|
|
170
|
-
def checks(self, ctx):
|
|
270
|
+
def checks(self, ctx): # runtime readiness → /ready
|
|
171
271
|
return [Check(name="database", passed=self.pool.is_alive())]
|
|
172
272
|
|
|
273
|
+
def doctor(self, ctx): # static audit → prodkit doctor
|
|
274
|
+
return [Audit(name="Database pool",
|
|
275
|
+
status="ok", detail="connection pool configured")]
|
|
276
|
+
|
|
173
277
|
Production(app, plugins=[DatabasePlugin()])
|
|
174
278
|
```
|
|
175
279
|
|
|
176
|
-
Your
|
|
280
|
+
Your `checks()` result now shows up in `/ready` automatically, and your
|
|
281
|
+
`doctor()` findings roll into the `prodkit doctor` score. Plugins can declare
|
|
177
282
|
`requires = ("other-plugin",)` and the kernel activates them in dependency
|
|
178
283
|
order — cycles and missing dependencies fail at boot.
|
|
179
284
|
|
|
@@ -230,13 +335,13 @@ plugins score too.
|
|
|
230
335
|
|
|
231
336
|
## Project Status
|
|
232
337
|
|
|
233
|
-
**v0.
|
|
234
|
-
the `prodkit doctor` CLI
|
|
235
|
-
|
|
338
|
+
**v0.3.0 — alpha.** Core kernel, eleven built-in plugins (metrics, cache, tracing,
|
|
339
|
+
rate-limiting with Redis), the `prodkit doctor` CLI, strict mypy, CI across
|
|
340
|
+
Python 3.10–3.13.
|
|
236
341
|
|
|
237
|
-
Roadmap: ✅ `prodkit doctor` CLI + rate limiting (v0.2), Prometheus metrics +
|
|
238
|
-
Redis backends (v0.3), Dockerfile/nginx/CI generators
|
|
239
|
-
(v0.5), auth helpers (v0.6), stable API (v1.0).
|
|
342
|
+
Roadmap: ✅ `prodkit doctor` CLI + rate limiting (v0.2), ✅ Prometheus metrics +
|
|
343
|
+
Redis backends + OpenTelemetry tracing (v0.3), Dockerfile/nginx/CI generators
|
|
344
|
+
(v0.4), public plugin SDK (v0.5), auth helpers (v0.6), stable API (v1.0).
|
|
240
345
|
Full details in [docs/ARCHITECTURE.md](https://github.com/Pushkarpant/PRODKIT/blob/main/docs/ARCHITECTURE.md).
|
|
241
346
|
|
|
242
347
|
## Contributing
|
|
@@ -1,50 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: prodkit
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: The production framework for FastAPI. One line. Production ready.
|
|
5
|
-
Project-URL: Homepage, https://github.com/Pushkarpant/PRODKIT
|
|
6
|
-
Project-URL: Documentation, https://github.com/Pushkarpant/PRODKIT#readme
|
|
7
|
-
Project-URL: Repository, https://github.com/Pushkarpant/PRODKIT
|
|
8
|
-
Project-URL: Changelog, https://github.com/Pushkarpant/PRODKIT/blob/main/CHANGELOG.md
|
|
9
|
-
Project-URL: Issues, https://github.com/Pushkarpant/PRODKIT/issues
|
|
10
|
-
Author-email: Pushkar Pant <pantpushkar4@gmail.com>
|
|
11
|
-
License-Expression: MIT
|
|
12
|
-
License-File: LICENSE
|
|
13
|
-
Keywords: fastapi,health-check,logging,middleware,observability,production,security
|
|
14
|
-
Classifier: Development Status :: 3 - Alpha
|
|
15
|
-
Classifier: Framework :: FastAPI
|
|
16
|
-
Classifier: Intended Audience :: Developers
|
|
17
|
-
Classifier: Operating System :: OS Independent
|
|
18
|
-
Classifier: Programming Language :: Python :: 3
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
-
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
24
|
-
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
25
|
-
Classifier: Typing :: Typed
|
|
26
|
-
Requires-Python: >=3.10
|
|
27
|
-
Requires-Dist: fastapi>=0.110
|
|
28
|
-
Requires-Dist: pydantic-settings>=2.1
|
|
29
|
-
Requires-Dist: pydantic>=2.5
|
|
30
|
-
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
31
|
-
Provides-Extra: brotli
|
|
32
|
-
Requires-Dist: brotli-asgi>=1.4; extra == 'brotli'
|
|
33
|
-
Provides-Extra: cli
|
|
34
|
-
Requires-Dist: rich>=13; extra == 'cli'
|
|
35
|
-
Requires-Dist: typer>=0.12; extra == 'cli'
|
|
36
|
-
Provides-Extra: dev
|
|
37
|
-
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
38
|
-
Requires-Dist: import-linter>=2.0; extra == 'dev'
|
|
39
|
-
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
40
|
-
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
41
|
-
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
42
|
-
Requires-Dist: rich>=13; extra == 'dev'
|
|
43
|
-
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
44
|
-
Requires-Dist: tomli>=2.0; extra == 'dev'
|
|
45
|
-
Requires-Dist: typer>=0.12; extra == 'dev'
|
|
46
|
-
Description-Content-Type: text/markdown
|
|
47
|
-
|
|
48
1
|
# ProdKit
|
|
49
2
|
|
|
50
3
|
> **One line. Production ready.**
|
|
@@ -61,14 +14,24 @@ Production(app)
|
|
|
61
14
|
|
|
62
15
|
That's it. Your app now has security headers, structured JSON logging with
|
|
63
16
|
request-ID correlation, RFC 9457 error responses, Kubernetes-ready health
|
|
64
|
-
endpoints,
|
|
65
|
-
hardened for production, and pleasant in development.
|
|
17
|
+
endpoints, gzip compression, and opt-in rate limiting — configured to current
|
|
18
|
+
best practice, hardened for production, and pleasant in development. Then run
|
|
19
|
+
[`prodkit doctor`](#cli--prodkit-doctor) to score how production-ready it is.
|
|
66
20
|
|
|
21
|
+
[](https://pypi.org/project/prodkit/)
|
|
22
|
+
[](https://pypi.org/project/prodkit/)
|
|
67
23
|
[](https://github.com/Pushkarpant/PRODKIT/actions/workflows/ci.yml)
|
|
68
24
|
[](https://github.com/Pushkarpant/PRODKIT/blob/main/LICENSE)
|
|
69
25
|
|
|
70
26
|
---
|
|
71
27
|
|
|
28
|
+
**Contents:** [Why](#why) · [Install](#installation) · [Quick Start](#quick-start) ·
|
|
29
|
+
[What You Get](#what-you-get) · [Configuration](#configuration) ·
|
|
30
|
+
[Plugins](#writing-a-plugin) · [CLI / doctor](#cli--prodkit-doctor) ·
|
|
31
|
+
[Status & Roadmap](#project-status)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
72
35
|
## Why
|
|
73
36
|
|
|
74
37
|
Every production FastAPI service re-implements the same ~500 lines of glue:
|
|
@@ -84,11 +47,21 @@ evolve, `pip install -U prodkit` updates every app you own.
|
|
|
84
47
|
[**`pip install prodkit`**](https://pypi.org/project/prodkit/)
|
|
85
48
|
|
|
86
49
|
```bash
|
|
87
|
-
pip install prodkit
|
|
50
|
+
pip install prodkit # the library
|
|
51
|
+
pip install "prodkit[cli]" # + the `prodkit doctor` CLI (typer + rich)
|
|
88
52
|
```
|
|
89
53
|
|
|
90
54
|
Requires Python 3.10+ and FastAPI 0.110+. The base install depends only on
|
|
91
|
-
FastAPI and Pydantic — nothing else.
|
|
55
|
+
FastAPI and Pydantic — nothing else. Optional extras:
|
|
56
|
+
|
|
57
|
+
| Extra | Adds |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `cli` | `prodkit doctor` CLI (typer + rich) |
|
|
60
|
+
| `metrics` | Prometheus `/metrics` endpoint |
|
|
61
|
+
| `otel` | OpenTelemetry tracing (OTLP export) |
|
|
62
|
+
| `redis` | Shared Redis backends (rate-limit, cache) |
|
|
63
|
+
| `brotli` | Brotli compression |
|
|
64
|
+
| `all` | Everything above |
|
|
92
65
|
|
|
93
66
|
## Quick Start
|
|
94
67
|
|
|
@@ -116,7 +89,9 @@ x-content-type-options: nosniff
|
|
|
116
89
|
x-frame-options: DENY
|
|
117
90
|
strict-transport-security: max-age=63072000; includeSubDomains
|
|
118
91
|
referrer-policy: strict-origin-when-cross-origin
|
|
119
|
-
|
|
92
|
+
permissions-policy: camera=(), microphone=(), geolocation=()
|
|
93
|
+
x-xss-protection: 0
|
|
94
|
+
content-type: application/json
|
|
120
95
|
```
|
|
121
96
|
|
|
122
97
|
For local development, flip the profile — pretty console logs, debug error
|
|
@@ -137,7 +112,10 @@ Production(app, environment="development")
|
|
|
137
112
|
| ❤️ **Health endpoints** | `/health`, `/live` (liveness) and `/ready` (readiness — aggregates checks from every plugin, 503 until all pass). Kubernetes-native. |
|
|
138
113
|
| 🌐 **CORS** | Explicit origins only; the wildcard-with-credentials footgun is refused at boot. |
|
|
139
114
|
| 📦 **Compression** | Gzip for responses over 500 bytes. |
|
|
140
|
-
| 🚦 **Rate limiting** | Opt-in per-IP limiting (`100/minute`), `429 problem+json` with `Retry-After`.
|
|
115
|
+
| 🚦 **Rate limiting** | Opt-in per-IP limiting (`100/minute`), `429 problem+json` with `Retry-After`. Memory or Redis backend (shared across workers). |
|
|
116
|
+
| 📊 **Prometheus metrics** | Request count, latency histogram, in-flight gauge at `/metrics`. Route-template labels (bounded cardinality). `pip install prodkit[metrics]` |
|
|
117
|
+
| 🗄️ **Cache service** | Memory (LRU + TTL) or Redis backend, published in the registry. `await cache.get(key)` / `.set(key, value, ttl=60)`. |
|
|
118
|
+
| 🔭 **OpenTelemetry tracing** | Automatic request spans with W3C `traceparent` propagation. OTLP, console, or none exporter. `pip install prodkit[otel]` |
|
|
141
119
|
| 🩺 **`prodkit doctor`** | CLI production-readiness audit with a 0–100 score. `--strict` gates CI. |
|
|
142
120
|
| 🔌 **Plugin system** | Every feature above is a plugin. Write your own with optional hooks incl. `doctor()`. |
|
|
143
121
|
|
|
@@ -158,6 +136,7 @@ Production(
|
|
|
158
136
|
cors={"origins": ["https://app.example.com"]}, # dict = configure & enable
|
|
159
137
|
compression=False, # bool = toggle
|
|
160
138
|
security={"trusted_hosts": ["api.example.com"]},
|
|
139
|
+
rate_limit={"default": "100/minute"}, # opt-in per-IP limiting
|
|
161
140
|
)
|
|
162
141
|
```
|
|
163
142
|
|
|
@@ -181,6 +160,10 @@ level = "INFO"
|
|
|
181
160
|
[cors]
|
|
182
161
|
enabled = true
|
|
183
162
|
origins = ["https://app.example.com"]
|
|
163
|
+
|
|
164
|
+
[rate_limit]
|
|
165
|
+
enabled = true
|
|
166
|
+
default = "100/minute"
|
|
184
167
|
```
|
|
185
168
|
|
|
186
169
|
### Fail-fast, refuse-unsafe
|
|
@@ -202,7 +185,7 @@ not warned about:
|
|
|
202
185
|
## Writing a Plugin
|
|
203
186
|
|
|
204
187
|
```python
|
|
205
|
-
from prodkit import Check, Plugin, Production
|
|
188
|
+
from prodkit import Audit, Check, Plugin, Production
|
|
206
189
|
|
|
207
190
|
class DatabasePlugin(Plugin):
|
|
208
191
|
name = "database"
|
|
@@ -214,13 +197,18 @@ class DatabasePlugin(Plugin):
|
|
|
214
197
|
async def shutdown(self, ctx):
|
|
215
198
|
await self.pool.close()
|
|
216
199
|
|
|
217
|
-
def checks(self, ctx):
|
|
200
|
+
def checks(self, ctx): # runtime readiness → /ready
|
|
218
201
|
return [Check(name="database", passed=self.pool.is_alive())]
|
|
219
202
|
|
|
203
|
+
def doctor(self, ctx): # static audit → prodkit doctor
|
|
204
|
+
return [Audit(name="Database pool",
|
|
205
|
+
status="ok", detail="connection pool configured")]
|
|
206
|
+
|
|
220
207
|
Production(app, plugins=[DatabasePlugin()])
|
|
221
208
|
```
|
|
222
209
|
|
|
223
|
-
Your
|
|
210
|
+
Your `checks()` result now shows up in `/ready` automatically, and your
|
|
211
|
+
`doctor()` findings roll into the `prodkit doctor` score. Plugins can declare
|
|
224
212
|
`requires = ("other-plugin",)` and the kernel activates them in dependency
|
|
225
213
|
order — cycles and missing dependencies fail at boot.
|
|
226
214
|
|
|
@@ -277,13 +265,13 @@ plugins score too.
|
|
|
277
265
|
|
|
278
266
|
## Project Status
|
|
279
267
|
|
|
280
|
-
**v0.
|
|
281
|
-
the `prodkit doctor` CLI
|
|
282
|
-
|
|
268
|
+
**v0.3.0 — alpha.** Core kernel, eleven built-in plugins (metrics, cache, tracing,
|
|
269
|
+
rate-limiting with Redis), the `prodkit doctor` CLI, strict mypy, CI across
|
|
270
|
+
Python 3.10–3.13.
|
|
283
271
|
|
|
284
|
-
Roadmap: ✅ `prodkit doctor` CLI + rate limiting (v0.2), Prometheus metrics +
|
|
285
|
-
Redis backends (v0.3), Dockerfile/nginx/CI generators
|
|
286
|
-
(v0.5), auth helpers (v0.6), stable API (v1.0).
|
|
272
|
+
Roadmap: ✅ `prodkit doctor` CLI + rate limiting (v0.2), ✅ Prometheus metrics +
|
|
273
|
+
Redis backends + OpenTelemetry tracing (v0.3), Dockerfile/nginx/CI generators
|
|
274
|
+
(v0.4), public plugin SDK (v0.5), auth helpers (v0.6), stable API (v1.0).
|
|
287
275
|
Full details in [docs/ARCHITECTURE.md](https://github.com/Pushkarpant/PRODKIT/blob/main/docs/ARCHITECTURE.md).
|
|
288
276
|
|
|
289
277
|
## Contributing
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "prodkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "The production framework for FastAPI. One line. Production ready."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -19,6 +19,9 @@ keywords = [
|
|
|
19
19
|
"observability",
|
|
20
20
|
"logging",
|
|
21
21
|
"health-check",
|
|
22
|
+
"metrics",
|
|
23
|
+
"tracing",
|
|
24
|
+
"opentelemetry",
|
|
22
25
|
]
|
|
23
26
|
classifiers = [
|
|
24
27
|
"Development Status :: 3 - Alpha",
|
|
@@ -44,9 +47,19 @@ dependencies = [
|
|
|
44
47
|
[project.optional-dependencies]
|
|
45
48
|
brotli = ["brotli-asgi>=1.4"]
|
|
46
49
|
cli = ["typer>=0.12", "rich>=13"]
|
|
50
|
+
metrics = ["prometheus-client>=0.20"]
|
|
51
|
+
redis = ["redis>=5.0"]
|
|
52
|
+
otel = [
|
|
53
|
+
"opentelemetry-api>=1.20",
|
|
54
|
+
"opentelemetry-sdk>=1.20",
|
|
55
|
+
"opentelemetry-exporter-otlp-proto-grpc>=1.20",
|
|
56
|
+
]
|
|
57
|
+
all = ["prodkit[metrics,redis,otel,brotli,cli]"]
|
|
47
58
|
dev = [
|
|
48
59
|
"pytest>=8.0",
|
|
49
60
|
"pytest-cov>=5.0",
|
|
61
|
+
"anyio[trio]>=4.0",
|
|
62
|
+
"pytest-anyio>=0.0.0",
|
|
50
63
|
"httpx>=0.27",
|
|
51
64
|
"ruff>=0.6",
|
|
52
65
|
"mypy>=1.11",
|
|
@@ -54,6 +67,10 @@ dev = [
|
|
|
54
67
|
"tomli>=2.0", # so mypy (python_version=3.10) can type-check the fallback import
|
|
55
68
|
"typer>=0.12", # CLI is type-checked and tested in CI
|
|
56
69
|
"rich>=13",
|
|
70
|
+
"prometheus-client>=0.20",
|
|
71
|
+
"fakeredis[lua]>=2.21",
|
|
72
|
+
"opentelemetry-api>=1.20",
|
|
73
|
+
"opentelemetry-sdk>=1.20",
|
|
57
74
|
]
|
|
58
75
|
|
|
59
76
|
[project.scripts]
|
|
@@ -9,15 +9,18 @@ Production(app)
|
|
|
9
9
|
|
|
10
10
|
from prodkit.contracts.plugin import Audit, Check, Plugin
|
|
11
11
|
from prodkit.core.config import (
|
|
12
|
+
CacheConfig,
|
|
12
13
|
CompressionConfig,
|
|
13
14
|
CORSConfig,
|
|
14
15
|
ErrorsConfig,
|
|
15
16
|
HealthConfig,
|
|
16
17
|
LoggingConfig,
|
|
18
|
+
MetricsConfig,
|
|
17
19
|
ProdKitConfig,
|
|
18
20
|
RateLimitConfig,
|
|
19
21
|
RequestIDConfig,
|
|
20
22
|
SecurityConfig,
|
|
23
|
+
TracingConfig,
|
|
21
24
|
)
|
|
22
25
|
from prodkit.core.context import Context
|
|
23
26
|
from prodkit.core.exceptions import (
|
|
@@ -34,17 +37,19 @@ from prodkit.plugins import builtin_plugins
|
|
|
34
37
|
# root — the kernel itself never imports from prodkit.plugins.
|
|
35
38
|
set_builtin_factory(builtin_plugins)
|
|
36
39
|
|
|
37
|
-
__version__ = "0.
|
|
40
|
+
__version__ = "0.3.0"
|
|
38
41
|
|
|
39
42
|
__all__ = [
|
|
40
43
|
"Audit",
|
|
41
44
|
"CORSConfig",
|
|
45
|
+
"CacheConfig",
|
|
42
46
|
"Check",
|
|
43
47
|
"CompressionConfig",
|
|
44
48
|
"Context",
|
|
45
49
|
"ErrorsConfig",
|
|
46
50
|
"HealthConfig",
|
|
47
51
|
"LoggingConfig",
|
|
52
|
+
"MetricsConfig",
|
|
48
53
|
"Plugin",
|
|
49
54
|
"PluginDependencyError",
|
|
50
55
|
"PluginError",
|
|
@@ -56,5 +61,6 @@ __all__ = [
|
|
|
56
61
|
"RequestIDConfig",
|
|
57
62
|
"SecurityConfig",
|
|
58
63
|
"ServiceNotFoundError",
|
|
64
|
+
"TracingConfig",
|
|
59
65
|
"__version__",
|
|
60
66
|
]
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from collections.abc import Awaitable
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from typing import TYPE_CHECKING, ClassVar, Literal
|
|
7
8
|
|
|
@@ -72,8 +73,12 @@ class Plugin:
|
|
|
72
73
|
async def shutdown(self, ctx: Context) -> None:
|
|
73
74
|
"""Release resources gracefully."""
|
|
74
75
|
|
|
75
|
-
def checks(self, ctx: Context) -> list[Check]:
|
|
76
|
-
"""Readiness checks, aggregated by the health plugin's /ready.
|
|
76
|
+
def checks(self, ctx: Context) -> list[Check] | Awaitable[list[Check]]:
|
|
77
|
+
"""Readiness checks, aggregated by the health plugin's /ready.
|
|
78
|
+
|
|
79
|
+
May be overridden as ``async def`` (e.g. to ping a backend); the
|
|
80
|
+
health plugin awaits awaitable results.
|
|
81
|
+
"""
|
|
77
82
|
return []
|
|
78
83
|
|
|
79
84
|
def doctor(self, ctx: Context) -> list[Audit]:
|
|
@@ -89,6 +94,7 @@ class Plugin:
|
|
|
89
94
|
PRIORITY_REQUEST_ID = 100
|
|
90
95
|
PRIORITY_LOGGING = 200
|
|
91
96
|
PRIORITY_ERRORS = 250
|
|
97
|
+
PRIORITY_TRACING = 290
|
|
92
98
|
PRIORITY_METRICS = 300
|
|
93
99
|
PRIORITY_SECURITY = 400
|
|
94
100
|
PRIORITY_CORS = 500
|
|
@@ -93,7 +93,37 @@ class RateLimitConfig(_Section):
|
|
|
93
93
|
enabled: bool = False # opt-in: an unexpected 429 is worse than no limit
|
|
94
94
|
# "<count>/<second|minute|hour>", parsed and validated by the plugin.
|
|
95
95
|
default: str = "100/minute"
|
|
96
|
-
by: Literal["ip"] = "ip" #
|
|
96
|
+
by: Literal["ip"] = "ip" # keys on client IP; per-user/route land later
|
|
97
|
+
# "memory" is per-process; "redis" shares the limit across workers/hosts.
|
|
98
|
+
backend: Literal["memory", "redis"] = "memory"
|
|
99
|
+
redis_url: str = "redis://localhost:6379/0"
|
|
100
|
+
key_prefix: str = "prodkit:ratelimit:"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class MetricsConfig(_Section):
|
|
104
|
+
enabled: bool = False # opt-in: needs prodkit[metrics]
|
|
105
|
+
path: str = "/metrics"
|
|
106
|
+
buckets: list[float] | None = None # None -> prometheus-client defaults
|
|
107
|
+
# Paths never measured (the metrics path itself is always excluded).
|
|
108
|
+
exclude_paths: list[str] = Field(default_factory=list)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class CacheConfig(_Section):
|
|
112
|
+
enabled: bool = False # opt-in: a service other code asks for, not middleware
|
|
113
|
+
backend: Literal["memory", "redis"] = "memory"
|
|
114
|
+
redis_url: str = "redis://localhost:6379/0"
|
|
115
|
+
default_ttl: int = 300 # seconds; 0 = no expiry
|
|
116
|
+
key_prefix: str = "prodkit:cache:"
|
|
117
|
+
max_entries: int = 1024 # memory backend LRU bound
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TracingConfig(_Section):
|
|
121
|
+
enabled: bool = False # opt-in: needs prodkit[otel]
|
|
122
|
+
service_name: str | None = None # None -> the FastAPI app's title
|
|
123
|
+
exporter: Literal["otlp", "console", "none"] = "otlp"
|
|
124
|
+
# None defers to the standard OTEL_EXPORTER_OTLP_* environment variables.
|
|
125
|
+
endpoint: str | None = None
|
|
126
|
+
sample_rate: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
97
127
|
|
|
98
128
|
|
|
99
129
|
class ProdKitConfig(_Section):
|
|
@@ -109,6 +139,9 @@ class ProdKitConfig(_Section):
|
|
|
109
139
|
cors: CORSConfig = Field(default_factory=CORSConfig)
|
|
110
140
|
compression: CompressionConfig = Field(default_factory=CompressionConfig)
|
|
111
141
|
rate_limit: RateLimitConfig = Field(default_factory=RateLimitConfig)
|
|
142
|
+
metrics: MetricsConfig = Field(default_factory=MetricsConfig)
|
|
143
|
+
cache: CacheConfig = Field(default_factory=CacheConfig)
|
|
144
|
+
tracing: TracingConfig = Field(default_factory=TracingConfig)
|
|
112
145
|
|
|
113
146
|
|
|
114
147
|
# Profile defaults: applied beneath toml/env/args. The one-liner must be
|