prodkit 0.1.3__tar.gz → 0.2.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.1.3 → prodkit-0.2.0}/.gitignore +2 -1
- prodkit-0.2.0/CHANGELOG.md +65 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/PKG-INFO +53 -7
- {prodkit-0.1.3 → prodkit-0.2.0}/README.md +47 -6
- {prodkit-0.1.3 → prodkit-0.2.0}/pyproject.toml +18 -1
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/__init__.py +5 -2
- prodkit-0.2.0/src/prodkit/cli/__init__.py +28 -0
- prodkit-0.2.0/src/prodkit/cli/app.py +260 -0
- prodkit-0.2.0/src/prodkit/cli/loader.py +75 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/contracts/plugin.py +38 -2
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/config.py +8 -0
- prodkit-0.2.0/src/prodkit/core/doctor.py +143 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/production.py +1 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/plugins/__init__.py +4 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/plugins/compression/__init__.py +11 -1
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/plugins/cors/__init__.py +19 -1
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/plugins/errors/__init__.py +54 -10
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/plugins/health/__init__.py +12 -1
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/plugins/logging/__init__.py +19 -1
- prodkit-0.2.0/src/prodkit/plugins/rate_limit/__init__.py +147 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/plugins/request_id/__init__.py +20 -1
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/plugins/security/__init__.py +47 -1
- prodkit-0.2.0/tests/integration/test_cli.py +132 -0
- prodkit-0.2.0/tests/unit/test_doctor.py +91 -0
- prodkit-0.2.0/tests/unit/test_rate_limit.py +97 -0
- prodkit-0.1.3/CHANGELOG.md +0 -39
- {prodkit-0.1.3 → prodkit-0.2.0}/LICENSE +0 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/contracts/__init__.py +0 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/__init__.py +0 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/context.py +0 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/event_bus.py +0 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/exceptions.py +0 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/lifecycle.py +0 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/plugin_manager.py +0 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/registry.py +0 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/py.typed +0 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/tests/__init__.py +0 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/tests/conftest.py +0 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/tests/integration/__init__.py +0 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/tests/integration/test_logging.py +0 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/tests/integration/test_production.py +0 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/tests/unit/__init__.py +0 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/tests/unit/test_config.py +0 -0
- {prodkit-0.1.3 → prodkit-0.2.0}/tests/unit/test_kernel.py +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.2.0] - 2026-07-22
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`prodkit` CLI** (install with `pip install prodkit[cli]`):
|
|
14
|
+
- `prodkit doctor` — production-readiness audit with a weighted 0–100 score;
|
|
15
|
+
`--strict --min-score N` turns it into a CI gate (non-zero exit below the
|
|
16
|
+
threshold).
|
|
17
|
+
- `prodkit inspect` — resolved config, active plugins, and middleware order.
|
|
18
|
+
- `prodkit plugins` — active plugins and the hooks each implements.
|
|
19
|
+
- `prodkit init [--example]` — scaffold a `prodkit.toml` (and starter app).
|
|
20
|
+
- **`Plugin.doctor(ctx)` hook** returning `Audit` findings; implemented by every
|
|
21
|
+
built-in plugin. `Audit` (name, status `ok`/`warn`/`fail`, detail,
|
|
22
|
+
recommendation, weight) is exported from the top-level package.
|
|
23
|
+
- Kernel doctor engine (`prodkit.core.doctor`) aggregating plugin audits with
|
|
24
|
+
config-level checks (environment, debug, disabled rate-limiting, low-entropy
|
|
25
|
+
secrets in the environment) into a score.
|
|
26
|
+
- **Rate-limiting plugin** (`rate-limit`, off by default): in-memory
|
|
27
|
+
fixed-window per-IP limiting (`rate_limit={"default": "100/minute"}`),
|
|
28
|
+
returning `429 application/problem+json` with a `Retry-After` header. Logs a
|
|
29
|
+
per-process-backend warning (shared Redis backend arrives in v0.3).
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Error responses now include an `instance` member (the request path). All
|
|
33
|
+
framework errors — including the rate limiter's 429 — share one
|
|
34
|
+
`problem_response` builder for a consistent RFC 9457 shape.
|
|
35
|
+
|
|
36
|
+
## [0.1.3] - 2026-07-20
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- README updated (badge cleanup); republished so the PyPI project page shows
|
|
40
|
+
the current README.
|
|
41
|
+
|
|
42
|
+
## [0.1.1] - 2026-07-20
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- README links that were relative (LICENSE, docs/ARCHITECTURE.md,
|
|
46
|
+
CONTRIBUTING.md, SECURITY.md) now use absolute GitHub URLs so they work on
|
|
47
|
+
the PyPI project page.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- Package author set to Pushkar Pant with contact email; author section added
|
|
51
|
+
to the README.
|
|
52
|
+
|
|
53
|
+
## [0.1.0] - 2026-07-16
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
- Kernel: layered configuration (args > env > `prodkit.toml` > profile > defaults),
|
|
57
|
+
plugin manager with dependency topological sort, service registry, event bus,
|
|
58
|
+
lifespan composition, fail-fast config validation.
|
|
59
|
+
- Plugin contract with async startup/shutdown hooks and prioritized middleware
|
|
60
|
+
registration.
|
|
61
|
+
- Environment profiles: `development`, `staging`, `production`.
|
|
62
|
+
- Built-in plugins: `request-id`, `logging` (structured JSON/console), `errors`
|
|
63
|
+
(RFC 9457 problem+json), `health` (`/health`, `/ready`, `/live`), `security`
|
|
64
|
+
(security headers, trusted hosts, HTTPS redirect), `cors`, `compression` (gzip).
|
|
65
|
+
- `Production(app)` one-line entrypoint.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: prodkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: The production framework for FastAPI. One line. Production ready.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Pushkarpant/PRODKIT
|
|
6
6
|
Project-URL: Documentation, https://github.com/Pushkarpant/PRODKIT#readme
|
|
@@ -30,14 +30,19 @@ Requires-Dist: pydantic>=2.5
|
|
|
30
30
|
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
31
31
|
Provides-Extra: brotli
|
|
32
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'
|
|
33
36
|
Provides-Extra: dev
|
|
34
37
|
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
35
38
|
Requires-Dist: import-linter>=2.0; extra == 'dev'
|
|
36
39
|
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
37
40
|
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
38
41
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
42
|
+
Requires-Dist: rich>=13; extra == 'dev'
|
|
39
43
|
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
40
44
|
Requires-Dist: tomli>=2.0; extra == 'dev'
|
|
45
|
+
Requires-Dist: typer>=0.12; extra == 'dev'
|
|
41
46
|
Description-Content-Type: text/markdown
|
|
42
47
|
|
|
43
48
|
# ProdKit
|
|
@@ -132,7 +137,9 @@ Production(app, environment="development")
|
|
|
132
137
|
| ❤️ **Health endpoints** | `/health`, `/live` (liveness) and `/ready` (readiness — aggregates checks from every plugin, 503 until all pass). Kubernetes-native. |
|
|
133
138
|
| 🌐 **CORS** | Explicit origins only; the wildcard-with-credentials footgun is refused at boot. |
|
|
134
139
|
| 📦 **Compression** | Gzip for responses over 500 bytes. |
|
|
135
|
-
|
|
|
140
|
+
| 🚦 **Rate limiting** | Opt-in per-IP limiting (`100/minute`), `429 problem+json` with `Retry-After`. In-memory backend (Redis in v0.3). |
|
|
141
|
+
| 🩺 **`prodkit doctor`** | CLI production-readiness audit with a 0–100 score. `--strict` gates CI. |
|
|
142
|
+
| 🔌 **Plugin system** | Every feature above is a plugin. Write your own with optional hooks incl. `doctor()`. |
|
|
136
143
|
|
|
137
144
|
## Configuration
|
|
138
145
|
|
|
@@ -221,6 +228,44 @@ Middleware registered by plugins carries an explicit integer priority, so
|
|
|
221
228
|
the middleware onion is always correctly ordered no matter what order
|
|
222
229
|
plugins load in (request-id outermost, compression innermost).
|
|
223
230
|
|
|
231
|
+
## CLI — `prodkit doctor`
|
|
232
|
+
|
|
233
|
+
Install the CLI extra and audit any `Production`-configured app:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
pip install "prodkit[cli]"
|
|
237
|
+
prodkit doctor --app main:app
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
```text
|
|
241
|
+
Production readiness
|
|
242
|
+
┌───┬───────────────────────┬─────────────────────┬─────────────────────────┐
|
|
243
|
+
│ ✔ │ Security headers │ nosniff, X-Frame ... │ │
|
|
244
|
+
│ ✔ │ Structured logging │ json @ INFO │ │
|
|
245
|
+
│ ✔ │ Error normalization │ 500s opaque │ │
|
|
246
|
+
│ ⚠ │ Content-Security-Po.. │ not set │ set a CSP to mitigate.. │
|
|
247
|
+
│ ⚠ │ Rate limiting │ disabled │ enable for public APIs │
|
|
248
|
+
└───┴───────────────────────┴─────────────────────┴─────────────────────────┘
|
|
249
|
+
Production score: 88/100 2 warning(s)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Make it a CI quality gate — fail the build below a threshold:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
prodkit doctor --app main:app --strict --min-score 90
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Other commands:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
prodkit inspect --app main:app # resolved config, plugins, middleware order
|
|
262
|
+
prodkit plugins --app main:app # active plugins and the hooks each implements
|
|
263
|
+
prodkit init --example # scaffold prodkit.toml (+ starter main.py)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Every plugin contributes findings via its `doctor(ctx)` hook, so your own
|
|
267
|
+
plugins score too.
|
|
268
|
+
|
|
224
269
|
## Plays Nice With Your App
|
|
225
270
|
|
|
226
271
|
- **Same app object.** Routes, dependencies, and existing middleware keep
|
|
@@ -232,12 +277,13 @@ plugins load in (request-id outermost, compression innermost).
|
|
|
232
277
|
|
|
233
278
|
## Project Status
|
|
234
279
|
|
|
235
|
-
**v0.
|
|
236
|
-
|
|
280
|
+
**v0.2.0 — alpha.** Core kernel, eight built-in plugins (incl. rate-limiting),
|
|
281
|
+
the `prodkit doctor` CLI with a production-readiness score, strict mypy, CI
|
|
282
|
+
across Python 3.10–3.13.
|
|
237
283
|
|
|
238
|
-
Roadmap: `prodkit doctor` CLI
|
|
239
|
-
|
|
240
|
-
(v0.
|
|
284
|
+
Roadmap: ✅ `prodkit doctor` CLI + rate limiting (v0.2), Prometheus metrics +
|
|
285
|
+
Redis backends (v0.3), Dockerfile/nginx/CI generators (v0.4), public plugin SDK
|
|
286
|
+
(v0.5), auth helpers (v0.6), stable API (v1.0).
|
|
241
287
|
Full details in [docs/ARCHITECTURE.md](https://github.com/Pushkarpant/PRODKIT/blob/main/docs/ARCHITECTURE.md).
|
|
242
288
|
|
|
243
289
|
## Contributing
|
|
@@ -90,7 +90,9 @@ Production(app, environment="development")
|
|
|
90
90
|
| ❤️ **Health endpoints** | `/health`, `/live` (liveness) and `/ready` (readiness — aggregates checks from every plugin, 503 until all pass). Kubernetes-native. |
|
|
91
91
|
| 🌐 **CORS** | Explicit origins only; the wildcard-with-credentials footgun is refused at boot. |
|
|
92
92
|
| 📦 **Compression** | Gzip for responses over 500 bytes. |
|
|
93
|
-
|
|
|
93
|
+
| 🚦 **Rate limiting** | Opt-in per-IP limiting (`100/minute`), `429 problem+json` with `Retry-After`. In-memory backend (Redis in v0.3). |
|
|
94
|
+
| 🩺 **`prodkit doctor`** | CLI production-readiness audit with a 0–100 score. `--strict` gates CI. |
|
|
95
|
+
| 🔌 **Plugin system** | Every feature above is a plugin. Write your own with optional hooks incl. `doctor()`. |
|
|
94
96
|
|
|
95
97
|
## Configuration
|
|
96
98
|
|
|
@@ -179,6 +181,44 @@ Middleware registered by plugins carries an explicit integer priority, so
|
|
|
179
181
|
the middleware onion is always correctly ordered no matter what order
|
|
180
182
|
plugins load in (request-id outermost, compression innermost).
|
|
181
183
|
|
|
184
|
+
## CLI — `prodkit doctor`
|
|
185
|
+
|
|
186
|
+
Install the CLI extra and audit any `Production`-configured app:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
pip install "prodkit[cli]"
|
|
190
|
+
prodkit doctor --app main:app
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```text
|
|
194
|
+
Production readiness
|
|
195
|
+
┌───┬───────────────────────┬─────────────────────┬─────────────────────────┐
|
|
196
|
+
│ ✔ │ Security headers │ nosniff, X-Frame ... │ │
|
|
197
|
+
│ ✔ │ Structured logging │ json @ INFO │ │
|
|
198
|
+
│ ✔ │ Error normalization │ 500s opaque │ │
|
|
199
|
+
│ ⚠ │ Content-Security-Po.. │ not set │ set a CSP to mitigate.. │
|
|
200
|
+
│ ⚠ │ Rate limiting │ disabled │ enable for public APIs │
|
|
201
|
+
└───┴───────────────────────┴─────────────────────┴─────────────────────────┘
|
|
202
|
+
Production score: 88/100 2 warning(s)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Make it a CI quality gate — fail the build below a threshold:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
prodkit doctor --app main:app --strict --min-score 90
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Other commands:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
prodkit inspect --app main:app # resolved config, plugins, middleware order
|
|
215
|
+
prodkit plugins --app main:app # active plugins and the hooks each implements
|
|
216
|
+
prodkit init --example # scaffold prodkit.toml (+ starter main.py)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Every plugin contributes findings via its `doctor(ctx)` hook, so your own
|
|
220
|
+
plugins score too.
|
|
221
|
+
|
|
182
222
|
## Plays Nice With Your App
|
|
183
223
|
|
|
184
224
|
- **Same app object.** Routes, dependencies, and existing middleware keep
|
|
@@ -190,12 +230,13 @@ plugins load in (request-id outermost, compression innermost).
|
|
|
190
230
|
|
|
191
231
|
## Project Status
|
|
192
232
|
|
|
193
|
-
**v0.
|
|
194
|
-
|
|
233
|
+
**v0.2.0 — alpha.** Core kernel, eight built-in plugins (incl. rate-limiting),
|
|
234
|
+
the `prodkit doctor` CLI with a production-readiness score, strict mypy, CI
|
|
235
|
+
across Python 3.10–3.13.
|
|
195
236
|
|
|
196
|
-
Roadmap: `prodkit doctor` CLI
|
|
197
|
-
|
|
198
|
-
(v0.
|
|
237
|
+
Roadmap: ✅ `prodkit doctor` CLI + rate limiting (v0.2), Prometheus metrics +
|
|
238
|
+
Redis backends (v0.3), Dockerfile/nginx/CI generators (v0.4), public plugin SDK
|
|
239
|
+
(v0.5), auth helpers (v0.6), stable API (v1.0).
|
|
199
240
|
Full details in [docs/ARCHITECTURE.md](https://github.com/Pushkarpant/PRODKIT/blob/main/docs/ARCHITECTURE.md).
|
|
200
241
|
|
|
201
242
|
## Contributing
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "prodkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "The production framework for FastAPI. One line. Production ready."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -43,6 +43,7 @@ dependencies = [
|
|
|
43
43
|
|
|
44
44
|
[project.optional-dependencies]
|
|
45
45
|
brotli = ["brotli-asgi>=1.4"]
|
|
46
|
+
cli = ["typer>=0.12", "rich>=13"]
|
|
46
47
|
dev = [
|
|
47
48
|
"pytest>=8.0",
|
|
48
49
|
"pytest-cov>=5.0",
|
|
@@ -51,8 +52,13 @@ dev = [
|
|
|
51
52
|
"mypy>=1.11",
|
|
52
53
|
"import-linter>=2.0",
|
|
53
54
|
"tomli>=2.0", # so mypy (python_version=3.10) can type-check the fallback import
|
|
55
|
+
"typer>=0.12", # CLI is type-checked and tested in CI
|
|
56
|
+
"rich>=13",
|
|
54
57
|
]
|
|
55
58
|
|
|
59
|
+
[project.scripts]
|
|
60
|
+
prodkit = "prodkit.cli:run"
|
|
61
|
+
|
|
56
62
|
[project.urls]
|
|
57
63
|
Homepage = "https://github.com/Pushkarpant/PRODKIT"
|
|
58
64
|
Documentation = "https://github.com/Pushkarpant/PRODKIT#readme"
|
|
@@ -88,12 +94,23 @@ select = [
|
|
|
88
94
|
[tool.ruff.lint.per-file-ignores]
|
|
89
95
|
"tests/**" = ["S101", "S106"] # asserts and test credentials are fine in tests
|
|
90
96
|
|
|
97
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
98
|
+
# typer's API is built on function-call defaults (Option/Argument); this is the
|
|
99
|
+
# documented pattern, not the B008 footgun.
|
|
100
|
+
extend-immutable-calls = ["typer.Option", "typer.Argument"]
|
|
101
|
+
|
|
91
102
|
[tool.mypy]
|
|
92
103
|
python_version = "3.10"
|
|
93
104
|
strict = true
|
|
94
105
|
packages = ["prodkit"]
|
|
95
106
|
mypy_path = "src"
|
|
96
107
|
|
|
108
|
+
[[tool.mypy.overrides]]
|
|
109
|
+
# typer's decorator-driven option defaults don't play well with strict mode.
|
|
110
|
+
module = "prodkit.cli.app"
|
|
111
|
+
disallow_untyped_decorators = false
|
|
112
|
+
disallow_any_decorated = false
|
|
113
|
+
|
|
97
114
|
[tool.pytest.ini_options]
|
|
98
115
|
testpaths = ["tests"]
|
|
99
116
|
addopts = "--cov=prodkit --cov-report=term-missing --cov-fail-under=90"
|
|
@@ -7,7 +7,7 @@ app = FastAPI()
|
|
|
7
7
|
Production(app)
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
from prodkit.contracts.plugin import Check, Plugin
|
|
10
|
+
from prodkit.contracts.plugin import Audit, Check, Plugin
|
|
11
11
|
from prodkit.core.config import (
|
|
12
12
|
CompressionConfig,
|
|
13
13
|
CORSConfig,
|
|
@@ -15,6 +15,7 @@ from prodkit.core.config import (
|
|
|
15
15
|
HealthConfig,
|
|
16
16
|
LoggingConfig,
|
|
17
17
|
ProdKitConfig,
|
|
18
|
+
RateLimitConfig,
|
|
18
19
|
RequestIDConfig,
|
|
19
20
|
SecurityConfig,
|
|
20
21
|
)
|
|
@@ -33,9 +34,10 @@ from prodkit.plugins import builtin_plugins
|
|
|
33
34
|
# root — the kernel itself never imports from prodkit.plugins.
|
|
34
35
|
set_builtin_factory(builtin_plugins)
|
|
35
36
|
|
|
36
|
-
__version__ = "0.
|
|
37
|
+
__version__ = "0.2.0"
|
|
37
38
|
|
|
38
39
|
__all__ = [
|
|
40
|
+
"Audit",
|
|
39
41
|
"CORSConfig",
|
|
40
42
|
"Check",
|
|
41
43
|
"CompressionConfig",
|
|
@@ -50,6 +52,7 @@ __all__ = [
|
|
|
50
52
|
"ProdKitConfigError",
|
|
51
53
|
"ProdKitError",
|
|
52
54
|
"Production",
|
|
55
|
+
"RateLimitConfig",
|
|
53
56
|
"RequestIDConfig",
|
|
54
57
|
"SecurityConfig",
|
|
55
58
|
"ServiceNotFoundError",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""ProdKit command-line interface.
|
|
2
|
+
|
|
3
|
+
The heavy CLI (typer + rich) lives in :mod:`prodkit.cli.app`; this module keeps
|
|
4
|
+
only a thin ``run()`` entry point so that a base install without the ``cli``
|
|
5
|
+
extra fails with an actionable message instead of an import traceback.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
_CLI_DEPS = {"typer", "rich", "click"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run() -> None:
|
|
16
|
+
"""Console-script entry point (``prodkit``)."""
|
|
17
|
+
try:
|
|
18
|
+
from prodkit.cli.app import app
|
|
19
|
+
except ModuleNotFoundError as exc: # pragma: no cover - exercised via packaging
|
|
20
|
+
if exc.name in _CLI_DEPS:
|
|
21
|
+
print(
|
|
22
|
+
"The prodkit CLI requires extra dependencies.\n"
|
|
23
|
+
" Install them with: pip install 'prodkit[cli]'",
|
|
24
|
+
file=sys.stderr,
|
|
25
|
+
)
|
|
26
|
+
raise SystemExit(1) from None
|
|
27
|
+
raise
|
|
28
|
+
app()
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""The typer + rich CLI application: doctor, inspect, plugins, init."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from prodkit import __version__
|
|
14
|
+
from prodkit.cli.loader import AppLoadError, load_production
|
|
15
|
+
from prodkit.contracts.plugin import Plugin
|
|
16
|
+
from prodkit.core.doctor import DoctorReport, run_doctor
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(
|
|
19
|
+
name="prodkit",
|
|
20
|
+
help="Audit and inspect the production-readiness of a FastAPI app.",
|
|
21
|
+
no_args_is_help=True,
|
|
22
|
+
add_completion=False,
|
|
23
|
+
)
|
|
24
|
+
console = Console()
|
|
25
|
+
err_console = Console(stderr=True)
|
|
26
|
+
|
|
27
|
+
_APP_OPTION = typer.Option(
|
|
28
|
+
None,
|
|
29
|
+
"--app",
|
|
30
|
+
"-a",
|
|
31
|
+
metavar="MODULE:ATTR",
|
|
32
|
+
help="Import path to your FastAPI app (default: autodetect main:app, app:app, ...).",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _supports_unicode() -> bool:
|
|
37
|
+
"""Whether the output stream can encode the status glyphs.
|
|
38
|
+
|
|
39
|
+
Windows terminals often default to cp1252, which can't encode ✔/⚠/✖; fall
|
|
40
|
+
back to ASCII markers there instead of crashing on a UnicodeEncodeError.
|
|
41
|
+
"""
|
|
42
|
+
encoding = console.encoding or "utf-8"
|
|
43
|
+
try:
|
|
44
|
+
"✔⚠✖".encode(encoding)
|
|
45
|
+
except (UnicodeEncodeError, LookupError):
|
|
46
|
+
return False
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_STATUS_ICON = (
|
|
51
|
+
{"ok": "[green]✔[/]", "warn": "[yellow]⚠[/]", "fail": "[red]✖[/]"}
|
|
52
|
+
if _supports_unicode()
|
|
53
|
+
else {"ok": "[green]OK[/]", "warn": "[yellow]![/]", "fail": "[red]X[/]"}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _version_callback(value: bool) -> None:
|
|
58
|
+
if value:
|
|
59
|
+
console.print(f"prodkit {__version__}")
|
|
60
|
+
raise typer.Exit()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.callback()
|
|
64
|
+
def main(
|
|
65
|
+
_version: bool = typer.Option(
|
|
66
|
+
False,
|
|
67
|
+
"--version",
|
|
68
|
+
callback=_version_callback,
|
|
69
|
+
is_eager=True,
|
|
70
|
+
help="Show version and exit.",
|
|
71
|
+
),
|
|
72
|
+
) -> None:
|
|
73
|
+
"""ProdKit CLI."""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _load(app_spec: str | None): # type: ignore[no-untyped-def]
|
|
77
|
+
try:
|
|
78
|
+
return load_production(app_spec)
|
|
79
|
+
except AppLoadError as exc:
|
|
80
|
+
err_console.print(f"[red]error:[/] {exc}")
|
|
81
|
+
raise typer.Exit(2) from None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# --------------------------------------------------------------------------
|
|
85
|
+
# doctor
|
|
86
|
+
# --------------------------------------------------------------------------
|
|
87
|
+
def _render_doctor(report: DoctorReport) -> None:
|
|
88
|
+
table = Table(title="Production readiness", show_lines=False, expand=False)
|
|
89
|
+
table.add_column("", justify="center", no_wrap=True)
|
|
90
|
+
table.add_column("Check", style="bold")
|
|
91
|
+
table.add_column("Detail")
|
|
92
|
+
table.add_column("Recommendation", style="dim")
|
|
93
|
+
for audit in report.audits:
|
|
94
|
+
table.add_row(
|
|
95
|
+
_STATUS_ICON[audit.status], audit.name, audit.detail, audit.recommendation or ""
|
|
96
|
+
)
|
|
97
|
+
console.print(table)
|
|
98
|
+
|
|
99
|
+
score = report.score
|
|
100
|
+
color = "green" if score >= 90 else "yellow" if score >= 70 else "red"
|
|
101
|
+
summary = f"[{color}]Production score: {score}/100[/]"
|
|
102
|
+
if report.failures:
|
|
103
|
+
summary += f" [red]{len(report.failures)} failing[/]"
|
|
104
|
+
if report.warnings:
|
|
105
|
+
summary += f" [yellow]{len(report.warnings)} warning(s)[/]"
|
|
106
|
+
console.print(Panel(summary, expand=False))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@app.command()
|
|
110
|
+
def doctor(
|
|
111
|
+
app_spec: str | None = _APP_OPTION,
|
|
112
|
+
strict: bool = typer.Option(
|
|
113
|
+
False, "--strict", help="Exit non-zero when the score is below --min-score (CI gate)."
|
|
114
|
+
),
|
|
115
|
+
min_score: int = typer.Option(90, "--min-score", help="Passing threshold for --strict."),
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Audit a FastAPI app and print a production-readiness score."""
|
|
118
|
+
report = run_doctor(_load(app_spec))
|
|
119
|
+
_render_doctor(report)
|
|
120
|
+
if strict and report.score < min_score:
|
|
121
|
+
err_console.print(
|
|
122
|
+
f"[red]doctor: score {report.score} is below the required {min_score}[/]"
|
|
123
|
+
)
|
|
124
|
+
raise typer.Exit(1)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# --------------------------------------------------------------------------
|
|
128
|
+
# inspect
|
|
129
|
+
# --------------------------------------------------------------------------
|
|
130
|
+
def _overridden_hooks(plugin: Plugin) -> list[str]:
|
|
131
|
+
hooks = (
|
|
132
|
+
"configure",
|
|
133
|
+
"register_middleware",
|
|
134
|
+
"register_routes",
|
|
135
|
+
"startup",
|
|
136
|
+
"shutdown",
|
|
137
|
+
"checks",
|
|
138
|
+
"doctor",
|
|
139
|
+
)
|
|
140
|
+
return [h for h in hooks if getattr(type(plugin), h) is not getattr(Plugin, h)]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@app.command()
|
|
144
|
+
def inspect(app_spec: str | None = _APP_OPTION) -> None:
|
|
145
|
+
"""Show the resolved config, active plugins, and middleware order."""
|
|
146
|
+
prod = _load(app_spec)
|
|
147
|
+
|
|
148
|
+
console.print(Panel(f"[bold]environment:[/] {prod.config.environment}", expand=False))
|
|
149
|
+
|
|
150
|
+
console.print("[bold]Resolved configuration[/]")
|
|
151
|
+
console.print_json(json.dumps(prod.config.model_dump(mode="json")))
|
|
152
|
+
|
|
153
|
+
plugins_table = Table(title="Active plugins", expand=False)
|
|
154
|
+
plugins_table.add_column("Plugin", style="bold")
|
|
155
|
+
plugins_table.add_column("Requires")
|
|
156
|
+
plugins_table.add_column("Hooks", style="dim")
|
|
157
|
+
for plugin in prod.plugins:
|
|
158
|
+
plugins_table.add_row(
|
|
159
|
+
plugin.name,
|
|
160
|
+
", ".join(plugin.requires) or "-",
|
|
161
|
+
", ".join(_overridden_hooks(plugin)) or "-",
|
|
162
|
+
)
|
|
163
|
+
console.print(plugins_table)
|
|
164
|
+
|
|
165
|
+
mw_table = Table(title="Middleware order (outermost first)", expand=False)
|
|
166
|
+
mw_table.add_column("Priority", justify="right")
|
|
167
|
+
mw_table.add_column("Middleware", style="bold")
|
|
168
|
+
mw_table.add_column("From plugin", style="dim")
|
|
169
|
+
for spec in sorted(prod.context.middleware_specs(), key=lambda s: s.priority):
|
|
170
|
+
mw_table.add_row(str(spec.priority), spec.cls.__name__, spec.plugin or "-")
|
|
171
|
+
console.print(mw_table)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# --------------------------------------------------------------------------
|
|
175
|
+
# plugins
|
|
176
|
+
# --------------------------------------------------------------------------
|
|
177
|
+
@app.command()
|
|
178
|
+
def plugins(app_spec: str | None = _APP_OPTION) -> None:
|
|
179
|
+
"""List the active plugins and the hooks each one implements."""
|
|
180
|
+
prod = _load(app_spec)
|
|
181
|
+
table = Table(title="Active plugins", expand=False)
|
|
182
|
+
table.add_column("Plugin", style="bold")
|
|
183
|
+
table.add_column("Requires")
|
|
184
|
+
table.add_column("Hooks", style="dim")
|
|
185
|
+
for plugin in prod.plugins:
|
|
186
|
+
table.add_row(
|
|
187
|
+
plugin.name,
|
|
188
|
+
", ".join(plugin.requires) or "-",
|
|
189
|
+
", ".join(_overridden_hooks(plugin)) or "-",
|
|
190
|
+
)
|
|
191
|
+
console.print(table)
|
|
192
|
+
console.print(f"[green]{len(prod.plugins)}[/] plugin(s) active.")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# --------------------------------------------------------------------------
|
|
196
|
+
# init
|
|
197
|
+
# --------------------------------------------------------------------------
|
|
198
|
+
_TOML_TEMPLATE = """\
|
|
199
|
+
# prodkit.toml — ProdKit configuration
|
|
200
|
+
# Resolution order (highest wins): Python args > env vars > this file > profile defaults
|
|
201
|
+
[prodkit]
|
|
202
|
+
environment = "production"
|
|
203
|
+
|
|
204
|
+
[logging]
|
|
205
|
+
level = "INFO"
|
|
206
|
+
format = "json"
|
|
207
|
+
|
|
208
|
+
[security]
|
|
209
|
+
hsts = true
|
|
210
|
+
# trusted_hosts = ["api.example.com"]
|
|
211
|
+
|
|
212
|
+
[cors]
|
|
213
|
+
enabled = false
|
|
214
|
+
# origins = ["https://app.example.com"]
|
|
215
|
+
|
|
216
|
+
[rate_limit]
|
|
217
|
+
enabled = false
|
|
218
|
+
default = "100/minute"
|
|
219
|
+
|
|
220
|
+
[compression]
|
|
221
|
+
minimum_size = 500
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
_EXAMPLE_TEMPLATE = """\
|
|
225
|
+
from fastapi import FastAPI
|
|
226
|
+
|
|
227
|
+
from prodkit import Production
|
|
228
|
+
|
|
229
|
+
app = FastAPI()
|
|
230
|
+
Production(app)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@app.get("/")
|
|
234
|
+
def root():
|
|
235
|
+
return {"message": "production ready"}
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@app.command()
|
|
240
|
+
def init(
|
|
241
|
+
path: Path = typer.Option(Path("."), "--path", help="Directory to write files into."),
|
|
242
|
+
example: bool = typer.Option(False, "--example", help="Also scaffold a minimal main.py."),
|
|
243
|
+
force: bool = typer.Option(False, "--force", help="Overwrite existing files."),
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Scaffold a prodkit.toml (and optionally a starter app)."""
|
|
246
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
247
|
+
toml_path = path / "prodkit.toml"
|
|
248
|
+
if toml_path.exists() and not force:
|
|
249
|
+
err_console.print(f"[red]error:[/] {toml_path} already exists (use --force to overwrite)")
|
|
250
|
+
raise typer.Exit(2)
|
|
251
|
+
toml_path.write_text(_TOML_TEMPLATE, encoding="utf-8")
|
|
252
|
+
console.print(f"[green]created[/] {toml_path}")
|
|
253
|
+
|
|
254
|
+
if example:
|
|
255
|
+
main_path = path / "main.py"
|
|
256
|
+
if main_path.exists() and not force:
|
|
257
|
+
err_console.print(f"[yellow]skipped[/] {main_path} already exists")
|
|
258
|
+
else:
|
|
259
|
+
main_path.write_text(_EXAMPLE_TEMPLATE, encoding="utf-8")
|
|
260
|
+
console.print(f"[green]created[/] {main_path}")
|