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.
Files changed (44) hide show
  1. {prodkit-0.1.3 → prodkit-0.2.0}/.gitignore +2 -1
  2. prodkit-0.2.0/CHANGELOG.md +65 -0
  3. {prodkit-0.1.3 → prodkit-0.2.0}/PKG-INFO +53 -7
  4. {prodkit-0.1.3 → prodkit-0.2.0}/README.md +47 -6
  5. {prodkit-0.1.3 → prodkit-0.2.0}/pyproject.toml +18 -1
  6. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/__init__.py +5 -2
  7. prodkit-0.2.0/src/prodkit/cli/__init__.py +28 -0
  8. prodkit-0.2.0/src/prodkit/cli/app.py +260 -0
  9. prodkit-0.2.0/src/prodkit/cli/loader.py +75 -0
  10. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/contracts/plugin.py +38 -2
  11. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/config.py +8 -0
  12. prodkit-0.2.0/src/prodkit/core/doctor.py +143 -0
  13. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/production.py +1 -0
  14. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/plugins/__init__.py +4 -0
  15. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/plugins/compression/__init__.py +11 -1
  16. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/plugins/cors/__init__.py +19 -1
  17. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/plugins/errors/__init__.py +54 -10
  18. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/plugins/health/__init__.py +12 -1
  19. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/plugins/logging/__init__.py +19 -1
  20. prodkit-0.2.0/src/prodkit/plugins/rate_limit/__init__.py +147 -0
  21. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/plugins/request_id/__init__.py +20 -1
  22. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/plugins/security/__init__.py +47 -1
  23. prodkit-0.2.0/tests/integration/test_cli.py +132 -0
  24. prodkit-0.2.0/tests/unit/test_doctor.py +91 -0
  25. prodkit-0.2.0/tests/unit/test_rate_limit.py +97 -0
  26. prodkit-0.1.3/CHANGELOG.md +0 -39
  27. {prodkit-0.1.3 → prodkit-0.2.0}/LICENSE +0 -0
  28. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/contracts/__init__.py +0 -0
  29. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/__init__.py +0 -0
  30. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/context.py +0 -0
  31. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/event_bus.py +0 -0
  32. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/exceptions.py +0 -0
  33. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/lifecycle.py +0 -0
  34. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/plugin_manager.py +0 -0
  35. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/core/registry.py +0 -0
  36. {prodkit-0.1.3 → prodkit-0.2.0}/src/prodkit/py.typed +0 -0
  37. {prodkit-0.1.3 → prodkit-0.2.0}/tests/__init__.py +0 -0
  38. {prodkit-0.1.3 → prodkit-0.2.0}/tests/conftest.py +0 -0
  39. {prodkit-0.1.3 → prodkit-0.2.0}/tests/integration/__init__.py +0 -0
  40. {prodkit-0.1.3 → prodkit-0.2.0}/tests/integration/test_logging.py +0 -0
  41. {prodkit-0.1.3 → prodkit-0.2.0}/tests/integration/test_production.py +0 -0
  42. {prodkit-0.1.3 → prodkit-0.2.0}/tests/unit/__init__.py +0 -0
  43. {prodkit-0.1.3 → prodkit-0.2.0}/tests/unit/test_config.py +0 -0
  44. {prodkit-0.1.3 → prodkit-0.2.0}/tests/unit/test_kernel.py +0 -0
@@ -24,4 +24,5 @@ dist/
24
24
  Thumbs.db
25
25
  LI.MD
26
26
  PUB.MD
27
- EX.MD
27
+ EX.MD
28
+ ss.md
@@ -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.1.3
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
- | 🔌 **Plugin system** | Every feature above is a plugin. Write your own with 6 optional hooks. |
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.1.0 — alpha.** Core kernel and seven built-in plugins, 60 tests, 98%
236
- coverage, strict mypy, CI across Python 3.10–3.13.
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 with a production-readiness score (v0.2),
239
- Prometheus metrics + Redis backends (v0.3), Dockerfile/nginx/CI generators
240
- (v0.4), public plugin SDK (v0.5), auth helpers (v0.6), stable API (v1.0).
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
- | 🔌 **Plugin system** | Every feature above is a plugin. Write your own with 6 optional hooks. |
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.1.0 — alpha.** Core kernel and seven built-in plugins, 60 tests, 98%
194
- coverage, strict mypy, CI across Python 3.10–3.13.
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 with a production-readiness score (v0.2),
197
- Prometheus metrics + Redis backends (v0.3), Dockerfile/nginx/CI generators
198
- (v0.4), public plugin SDK (v0.5), auth helpers (v0.6), stable API (v1.0).
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.1.3"
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.1.3"
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}")