prodkit 0.3.1__tar.gz → 0.4.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.3.1 → prodkit-0.4.0}/CHANGELOG.md +12 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/PKG-INFO +46 -6
- {prodkit-0.3.1 → prodkit-0.4.0}/README.md +45 -5
- {prodkit-0.3.1 → prodkit-0.4.0}/pyproject.toml +1 -1
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/__init__.py +1 -1
- prodkit-0.4.0/src/prodkit/cli/__main__.py +6 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/cli/app.py +171 -3
- prodkit-0.4.0/src/prodkit/generators/__init__.py +36 -0
- prodkit-0.4.0/src/prodkit/generators/base.py +104 -0
- prodkit-0.4.0/src/prodkit/generators/compose.py +95 -0
- prodkit-0.4.0/src/prodkit/generators/docker.py +140 -0
- prodkit-0.4.0/src/prodkit/generators/env.py +121 -0
- prodkit-0.4.0/src/prodkit/generators/github.py +87 -0
- prodkit-0.4.0/src/prodkit/generators/nginx.py +111 -0
- prodkit-0.4.0/tests/integration/test_cli_generate.py +131 -0
- prodkit-0.4.0/tests/unit/test_generators.py +211 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/.gitignore +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/LICENSE +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/cli/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/cli/loader.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/contracts/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/contracts/plugin.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/config.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/context.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/doctor.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/event_bus.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/exceptions.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/lifecycle.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/plugin_manager.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/production.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/registry.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/_redis.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/cache/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/compression/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/cors/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/errors/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/health/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/logging/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/metrics/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/rate_limit/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/rate_limit/backends.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/request_id/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/security/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/tracing/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/py.typed +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/tests/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/tests/conftest.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/tests/integration/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/tests/integration/test_cli.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/tests/integration/test_logging.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/tests/integration/test_production.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/tests/unit/__init__.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/tests/unit/test_cache.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/tests/unit/test_config.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/tests/unit/test_doctor.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/tests/unit/test_kernel.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/tests/unit/test_metrics.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/tests/unit/test_rate_limit.py +0 -0
- {prodkit-0.3.1 → prodkit-0.4.0}/tests/unit/test_tracing.py +0 -0
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-08-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`prodkit generate` CLI suite**:
|
|
14
|
+
- `prodkit generate docker`: hardened, multi-stage non-root Dockerfile and `.dockerignore`.
|
|
15
|
+
- `prodkit generate compose`: `docker-compose.yml` with healthchecks, environment mapping, and conditional Redis service detection.
|
|
16
|
+
- `prodkit generate nginx`: production reverse-proxy config with W3C traceparent pass-through, request-ID correlation, and gzip compression.
|
|
17
|
+
- `prodkit generate github`: `.github/workflows/ci.yml` with matrix testing across Python 3.10-3.13, linting, typechecking, coverage, and `prodkit doctor --strict` CI gate.
|
|
18
|
+
- `prodkit generate env`: `.env.example` template covering all `ProdKitConfig` parameters and defaults.
|
|
19
|
+
- `prodkit generate all`: single-command scaffolding for all deployment assets.
|
|
20
|
+
- **Generator Engine** (`prodkit.generators`): decoupled generator classes (`DockerGenerator`, `ComposeGenerator`, `NginxGenerator`, `GitHubGenerator`, `EnvGenerator`) supporting programmatic rendering, `--dry-run` previews, `--force` overwriting, and context-aware templating.
|
|
21
|
+
|
|
10
22
|
## [0.3.0] - 2026-08-14
|
|
11
23
|
|
|
12
24
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: prodkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -90,7 +90,7 @@ Production(app) # 👈 That's it. Production hardened.
|
|
|
90
90
|
[](https://github.com/Pushkarpant/PRODKIT/blob/main/LICENSE)
|
|
91
91
|
[](https://github.com/astral-sh/ruff)
|
|
92
92
|
|
|
93
|
-
[⚡ Quick Start](#quick-start) · [📦 Installation](#installation) · [✨ What You Get](#what-you-get) · [🩺 CLI Doctor](#cli-doctor) · [🎛️ Configuration](#configuration) · [🔌 Plugins](#plugins) · [🗺️ Roadmap](#roadmap)
|
|
93
|
+
[⚡ Quick Start](#quick-start) · [📦 Installation](#installation) · [✨ What You Get](#what-you-get) · [🩺 CLI Doctor](#cli-doctor) · [🚀 Generators](#generators) · [🎛️ Configuration](#configuration) · [🔌 Plugins](#plugins) · [🗺️ Roadmap](#roadmap)
|
|
94
94
|
|
|
95
95
|
---
|
|
96
96
|
|
|
@@ -201,7 +201,7 @@ pip install "prodkit[all]" # All available plugins and extras
|
|
|
201
201
|
|
|
202
202
|
| Extra | Adds | Dependencies |
|
|
203
203
|
|---|---|---|
|
|
204
|
-
| `cli` | `prodkit doctor`, `inspect`, `init`
|
|
204
|
+
| `cli` | `prodkit doctor`, `generate`, `inspect`, `init` | `typer`, `rich` |
|
|
205
205
|
| `metrics` | Prometheus metrics endpoint (`/metrics`) | `prometheus-client` |
|
|
206
206
|
| `otel` | W3C distributed tracing with OpenTelemetry | `opentelemetry-api`, `opentelemetry-sdk` |
|
|
207
207
|
| `redis` | Multi-worker rate limiting & distributed cache | `redis` / `fakeredis` |
|
|
@@ -281,6 +281,46 @@ Enforce production standards in GitHub Actions or GitLab CI:
|
|
|
281
281
|
prodkit doctor --app main:app --strict --min-score 90
|
|
282
282
|
```
|
|
283
283
|
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
<a id="generators"></a>
|
|
287
|
+
## 🚀 Infrastructure & Deployment Generators (`prodkit generate`)
|
|
288
|
+
|
|
289
|
+
Scaffold production-grade deployment assets tailored to your application's resolved configuration and active plugins:
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
# Generate all deployment assets at once
|
|
293
|
+
prodkit generate all
|
|
294
|
+
|
|
295
|
+
# Preview generated assets without writing to disk
|
|
296
|
+
prodkit generate all --dry-run
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
```text
|
|
300
|
+
Generated deployment assets
|
|
301
|
+
┌─────────┬──────────────────────────┬────────────────────────────────────────┐
|
|
302
|
+
│ Status │ File │ Description │
|
|
303
|
+
├─────────┼──────────────────────────┼────────────────────────────────────────┤
|
|
304
|
+
│ created │ Dockerfile │ Multi-stage, non-root production image │
|
|
305
|
+
│ created │ .dockerignore │ Docker build ignore file │
|
|
306
|
+
│ created │ docker-compose.yml │ Multi-service Compose definition │
|
|
307
|
+
│ created │ nginx.conf │ Hardened Nginx reverse-proxy │
|
|
308
|
+
│ created │ .github/workflows/ci.yml │ CI quality & testing with doctor gate │
|
|
309
|
+
│ created │ .env.example │ Environment configuration template │
|
|
310
|
+
└─────────┴──────────────────────────┴────────────────────────────────────────┘
|
|
311
|
+
Successfully generated 6 file(s).
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### 🧩 Individual Generators
|
|
315
|
+
|
|
316
|
+
| Command | Generated Artifact | Key Features |
|
|
317
|
+
|---|---|---|
|
|
318
|
+
| `prodkit generate docker` | `Dockerfile`, `.dockerignore` | Multi-stage builder, non-root `appuser:10001`, `HEALTHCHECK`, `uvicorn` workers |
|
|
319
|
+
| `prodkit generate compose` | `docker-compose.yml` | Healthcheck dependencies, auto-detects Redis services and network volumes |
|
|
320
|
+
| `prodkit generate nginx` | `nginx.conf` | Reverse-proxy, W3C `traceparent` propagation, `X-Request-ID`, gzip compression |
|
|
321
|
+
| `prodkit generate github` | `.github/workflows/ci.yml` | Python 3.10–3.13 matrix, `ruff`, `mypy --strict`, `pytest`, `prodkit doctor` gate |
|
|
322
|
+
| `prodkit generate env` | `.env.example` | Dynamic schema documentation with defaults and production notes |
|
|
323
|
+
|
|
284
324
|
<details>
|
|
285
325
|
<summary><b>🛠️ More CLI Commands (<code>inspect</code>, <code>plugins</code>, <code>init</code>)</b></summary>
|
|
286
326
|
|
|
@@ -408,9 +448,9 @@ Production(app, plugins=[DatabaseHealthPlugin()])
|
|
|
408
448
|
|---|---|---|
|
|
409
449
|
| **v0.1.0** | ✅ Released | Core kernel, security headers, JSON logs, RFC 9457 error normalization, `/health` |
|
|
410
450
|
| **v0.2.0** | ✅ Released | `prodkit doctor` CLI, readiness score, in-memory rate limiting |
|
|
411
|
-
| **v0.3.0** | ✅
|
|
412
|
-
| **v0.4.0** |
|
|
413
|
-
| **v0.5.0** |
|
|
451
|
+
| **v0.3.0** | ✅ Released | Prometheus metrics, Redis backends, OpenTelemetry tracing, Cache service |
|
|
452
|
+
| **v0.4.0** | ✅ **Current** | **`prodkit generate` (Dockerfile, nginx, docker-compose, GitHub Actions CI, .env)** |
|
|
453
|
+
| **v0.5.0** | 🚧 Next | Public Plugin SDK & Ecosystem (`prodkit-sentry`, entry-point discovery) |
|
|
414
454
|
| **v1.0.0** | 🎯 Milestone | Frozen Public API, LTS release, Production case studies |
|
|
415
455
|
|
|
416
456
|
---
|
|
@@ -20,7 +20,7 @@ Production(app) # 👈 That's it. Production hardened.
|
|
|
20
20
|
[](https://github.com/Pushkarpant/PRODKIT/blob/main/LICENSE)
|
|
21
21
|
[](https://github.com/astral-sh/ruff)
|
|
22
22
|
|
|
23
|
-
[⚡ Quick Start](#quick-start) · [📦 Installation](#installation) · [✨ What You Get](#what-you-get) · [🩺 CLI Doctor](#cli-doctor) · [🎛️ Configuration](#configuration) · [🔌 Plugins](#plugins) · [🗺️ Roadmap](#roadmap)
|
|
23
|
+
[⚡ Quick Start](#quick-start) · [📦 Installation](#installation) · [✨ What You Get](#what-you-get) · [🩺 CLI Doctor](#cli-doctor) · [🚀 Generators](#generators) · [🎛️ Configuration](#configuration) · [🔌 Plugins](#plugins) · [🗺️ Roadmap](#roadmap)
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
@@ -131,7 +131,7 @@ pip install "prodkit[all]" # All available plugins and extras
|
|
|
131
131
|
|
|
132
132
|
| Extra | Adds | Dependencies |
|
|
133
133
|
|---|---|---|
|
|
134
|
-
| `cli` | `prodkit doctor`, `inspect`, `init`
|
|
134
|
+
| `cli` | `prodkit doctor`, `generate`, `inspect`, `init` | `typer`, `rich` |
|
|
135
135
|
| `metrics` | Prometheus metrics endpoint (`/metrics`) | `prometheus-client` |
|
|
136
136
|
| `otel` | W3C distributed tracing with OpenTelemetry | `opentelemetry-api`, `opentelemetry-sdk` |
|
|
137
137
|
| `redis` | Multi-worker rate limiting & distributed cache | `redis` / `fakeredis` |
|
|
@@ -211,6 +211,46 @@ Enforce production standards in GitHub Actions or GitLab CI:
|
|
|
211
211
|
prodkit doctor --app main:app --strict --min-score 90
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
<a id="generators"></a>
|
|
217
|
+
## 🚀 Infrastructure & Deployment Generators (`prodkit generate`)
|
|
218
|
+
|
|
219
|
+
Scaffold production-grade deployment assets tailored to your application's resolved configuration and active plugins:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
# Generate all deployment assets at once
|
|
223
|
+
prodkit generate all
|
|
224
|
+
|
|
225
|
+
# Preview generated assets without writing to disk
|
|
226
|
+
prodkit generate all --dry-run
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
```text
|
|
230
|
+
Generated deployment assets
|
|
231
|
+
┌─────────┬──────────────────────────┬────────────────────────────────────────┐
|
|
232
|
+
│ Status │ File │ Description │
|
|
233
|
+
├─────────┼──────────────────────────┼────────────────────────────────────────┤
|
|
234
|
+
│ created │ Dockerfile │ Multi-stage, non-root production image │
|
|
235
|
+
│ created │ .dockerignore │ Docker build ignore file │
|
|
236
|
+
│ created │ docker-compose.yml │ Multi-service Compose definition │
|
|
237
|
+
│ created │ nginx.conf │ Hardened Nginx reverse-proxy │
|
|
238
|
+
│ created │ .github/workflows/ci.yml │ CI quality & testing with doctor gate │
|
|
239
|
+
│ created │ .env.example │ Environment configuration template │
|
|
240
|
+
└─────────┴──────────────────────────┴────────────────────────────────────────┘
|
|
241
|
+
Successfully generated 6 file(s).
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### 🧩 Individual Generators
|
|
245
|
+
|
|
246
|
+
| Command | Generated Artifact | Key Features |
|
|
247
|
+
|---|---|---|
|
|
248
|
+
| `prodkit generate docker` | `Dockerfile`, `.dockerignore` | Multi-stage builder, non-root `appuser:10001`, `HEALTHCHECK`, `uvicorn` workers |
|
|
249
|
+
| `prodkit generate compose` | `docker-compose.yml` | Healthcheck dependencies, auto-detects Redis services and network volumes |
|
|
250
|
+
| `prodkit generate nginx` | `nginx.conf` | Reverse-proxy, W3C `traceparent` propagation, `X-Request-ID`, gzip compression |
|
|
251
|
+
| `prodkit generate github` | `.github/workflows/ci.yml` | Python 3.10–3.13 matrix, `ruff`, `mypy --strict`, `pytest`, `prodkit doctor` gate |
|
|
252
|
+
| `prodkit generate env` | `.env.example` | Dynamic schema documentation with defaults and production notes |
|
|
253
|
+
|
|
214
254
|
<details>
|
|
215
255
|
<summary><b>🛠️ More CLI Commands (<code>inspect</code>, <code>plugins</code>, <code>init</code>)</b></summary>
|
|
216
256
|
|
|
@@ -338,9 +378,9 @@ Production(app, plugins=[DatabaseHealthPlugin()])
|
|
|
338
378
|
|---|---|---|
|
|
339
379
|
| **v0.1.0** | ✅ Released | Core kernel, security headers, JSON logs, RFC 9457 error normalization, `/health` |
|
|
340
380
|
| **v0.2.0** | ✅ Released | `prodkit doctor` CLI, readiness score, in-memory rate limiting |
|
|
341
|
-
| **v0.3.0** | ✅
|
|
342
|
-
| **v0.4.0** |
|
|
343
|
-
| **v0.5.0** |
|
|
381
|
+
| **v0.3.0** | ✅ Released | Prometheus metrics, Redis backends, OpenTelemetry tracing, Cache service |
|
|
382
|
+
| **v0.4.0** | ✅ **Current** | **`prodkit generate` (Dockerfile, nginx, docker-compose, GitHub Actions CI, .env)** |
|
|
383
|
+
| **v0.5.0** | 🚧 Next | Public Plugin SDK & Ecosystem (`prodkit-sentry`, entry-point discovery) |
|
|
344
384
|
| **v1.0.0** | 🎯 Milestone | Frozen Public API, LTS release, Production case studies |
|
|
345
385
|
|
|
346
386
|
---
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
"""The typer + rich CLI application: doctor, inspect, plugins, init."""
|
|
1
|
+
"""The typer + rich CLI application: doctor, inspect, plugins, init, generate."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
8
9
|
import typer
|
|
9
10
|
from rich.console import Console
|
|
@@ -14,10 +15,25 @@ from prodkit import __version__
|
|
|
14
15
|
from prodkit.cli.loader import AppLoadError, load_production
|
|
15
16
|
from prodkit.contracts.plugin import Plugin
|
|
16
17
|
from prodkit.core.doctor import DoctorReport, run_doctor
|
|
18
|
+
from prodkit.generators import (
|
|
19
|
+
ALL_GENERATORS,
|
|
20
|
+
BaseGenerator,
|
|
21
|
+
ComposeGenerator,
|
|
22
|
+
DockerGenerator,
|
|
23
|
+
EnvGenerator,
|
|
24
|
+
FileStatus,
|
|
25
|
+
GeneratedFile,
|
|
26
|
+
GeneratorContext,
|
|
27
|
+
GitHubGenerator,
|
|
28
|
+
NginxGenerator,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from prodkit.core.production import Production
|
|
17
33
|
|
|
18
34
|
app = typer.Typer(
|
|
19
35
|
name="prodkit",
|
|
20
|
-
help="Audit and
|
|
36
|
+
help="Audit, inspect, and generate production assets for a FastAPI app.",
|
|
21
37
|
no_args_is_help=True,
|
|
22
38
|
add_completion=False,
|
|
23
39
|
)
|
|
@@ -73,7 +89,7 @@ def main(
|
|
|
73
89
|
"""ProdKit CLI."""
|
|
74
90
|
|
|
75
91
|
|
|
76
|
-
def _load(app_spec: str | None)
|
|
92
|
+
def _load(app_spec: str | None) -> Production:
|
|
77
93
|
try:
|
|
78
94
|
return load_production(app_spec)
|
|
79
95
|
except AppLoadError as exc:
|
|
@@ -81,6 +97,16 @@ def _load(app_spec: str | None): # type: ignore[no-untyped-def]
|
|
|
81
97
|
raise typer.Exit(2) from None
|
|
82
98
|
|
|
83
99
|
|
|
100
|
+
def _load_optional(app_spec: str | None) -> Production | None:
|
|
101
|
+
try:
|
|
102
|
+
return load_production(app_spec)
|
|
103
|
+
except AppLoadError as exc:
|
|
104
|
+
if app_spec:
|
|
105
|
+
err_console.print(f"[red]error:[/] {exc}")
|
|
106
|
+
raise typer.Exit(2) from None
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
84
110
|
# --------------------------------------------------------------------------
|
|
85
111
|
# doctor
|
|
86
112
|
# --------------------------------------------------------------------------
|
|
@@ -258,3 +284,145 @@ def init(
|
|
|
258
284
|
else:
|
|
259
285
|
main_path.write_text(_EXAMPLE_TEMPLATE, encoding="utf-8")
|
|
260
286
|
console.print(f"[green]created[/] {main_path}")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# --------------------------------------------------------------------------
|
|
290
|
+
# generate
|
|
291
|
+
# --------------------------------------------------------------------------
|
|
292
|
+
generate_app = typer.Typer(
|
|
293
|
+
name="generate",
|
|
294
|
+
help="Generate production deployment assets (Dockerfile, compose, nginx, CI, env).",
|
|
295
|
+
no_args_is_help=True,
|
|
296
|
+
)
|
|
297
|
+
app.add_typer(generate_app)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _run_generators(
|
|
301
|
+
generators: list[type[BaseGenerator]],
|
|
302
|
+
path: Path,
|
|
303
|
+
app_spec: str | None,
|
|
304
|
+
force: bool,
|
|
305
|
+
dry_run: bool,
|
|
306
|
+
port: int,
|
|
307
|
+
) -> None:
|
|
308
|
+
prod = _load_optional(app_spec)
|
|
309
|
+
ctx = GeneratorContext(
|
|
310
|
+
production=prod,
|
|
311
|
+
root_dir=path,
|
|
312
|
+
force=force,
|
|
313
|
+
dry_run=dry_run,
|
|
314
|
+
port=port,
|
|
315
|
+
app_spec=app_spec or "main:app",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
table = Table(title="Generated deployment assets", expand=False)
|
|
319
|
+
table.add_column("Status", style="bold")
|
|
320
|
+
table.add_column("File", style="cyan")
|
|
321
|
+
table.add_column("Description", style="dim")
|
|
322
|
+
|
|
323
|
+
all_results: list[tuple[GeneratedFile, FileStatus]] = []
|
|
324
|
+
for gen_cls in generators:
|
|
325
|
+
gen = gen_cls()
|
|
326
|
+
results = gen.write(ctx)
|
|
327
|
+
all_results.extend(results)
|
|
328
|
+
|
|
329
|
+
status_labels: dict[FileStatus, str] = {
|
|
330
|
+
"created": "[green]created[/]",
|
|
331
|
+
"overwritten": "[blue]overwritten[/]",
|
|
332
|
+
"skipped": "[yellow]skipped[/]",
|
|
333
|
+
"dry-run": "[dim]dry-run[/]",
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if path != Path("."):
|
|
337
|
+
console.print(f"[bold]Destination:[/] {path}")
|
|
338
|
+
|
|
339
|
+
for gen_file, status in all_results:
|
|
340
|
+
target_display = str(gen_file.path)
|
|
341
|
+
table.add_row(status_labels[status], target_display, gen_file.description)
|
|
342
|
+
|
|
343
|
+
console.print(table)
|
|
344
|
+
|
|
345
|
+
created_count = sum(1 for _, s in all_results if s in ("created", "overwritten"))
|
|
346
|
+
skipped_count = sum(1 for _, s in all_results if s == "skipped")
|
|
347
|
+
|
|
348
|
+
if dry_run:
|
|
349
|
+
console.print("[dim]Dry-run mode: no files were written to disk.[/]")
|
|
350
|
+
elif created_count > 0 and skipped_count == 0:
|
|
351
|
+
console.print(f"[green]Successfully generated {created_count} file(s).[/]")
|
|
352
|
+
elif skipped_count > 0:
|
|
353
|
+
console.print(
|
|
354
|
+
f"[yellow]{skipped_count} file(s) already exist and were skipped. "
|
|
355
|
+
"Use --force to overwrite.[/]"
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@generate_app.command(name="docker")
|
|
360
|
+
def generate_docker(
|
|
361
|
+
path: Path = typer.Option(Path("."), "--path", "-p", help="Directory to write files into."),
|
|
362
|
+
app_spec: str | None = _APP_OPTION,
|
|
363
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files."),
|
|
364
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Preview files without writing."),
|
|
365
|
+
port: int = typer.Option(8000, "--port", help="Application container port."),
|
|
366
|
+
) -> None:
|
|
367
|
+
"""Generate a multi-stage, non-root Dockerfile and .dockerignore."""
|
|
368
|
+
_run_generators([DockerGenerator], path, app_spec, force, dry_run, port)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@generate_app.command(name="compose")
|
|
372
|
+
def generate_compose(
|
|
373
|
+
path: Path = typer.Option(Path("."), "--path", "-p", help="Directory to write files into."),
|
|
374
|
+
app_spec: str | None = _APP_OPTION,
|
|
375
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files."),
|
|
376
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Preview files without writing."),
|
|
377
|
+
port: int = typer.Option(8000, "--port", help="Application container port."),
|
|
378
|
+
) -> None:
|
|
379
|
+
"""Generate a docker-compose.yml file with services and healthchecks."""
|
|
380
|
+
_run_generators([ComposeGenerator], path, app_spec, force, dry_run, port)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@generate_app.command(name="nginx")
|
|
384
|
+
def generate_nginx(
|
|
385
|
+
path: Path = typer.Option(Path("."), "--path", "-p", help="Directory to write files into."),
|
|
386
|
+
app_spec: str | None = _APP_OPTION,
|
|
387
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files."),
|
|
388
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Preview files without writing."),
|
|
389
|
+
port: int = typer.Option(8000, "--port", help="Target upstream application port."),
|
|
390
|
+
) -> None:
|
|
391
|
+
"""Generate a production-hardened Nginx reverse-proxy configuration."""
|
|
392
|
+
_run_generators([NginxGenerator], path, app_spec, force, dry_run, port)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@generate_app.command(name="github")
|
|
396
|
+
def generate_github(
|
|
397
|
+
path: Path = typer.Option(Path("."), "--path", "-p", help="Directory to write files into."),
|
|
398
|
+
app_spec: str | None = _APP_OPTION,
|
|
399
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files."),
|
|
400
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Preview files without writing."),
|
|
401
|
+
port: int = typer.Option(8000, "--port", help="Application port."),
|
|
402
|
+
) -> None:
|
|
403
|
+
"""Generate a GitHub Actions CI workflow (.github/workflows/ci.yml)."""
|
|
404
|
+
_run_generators([GitHubGenerator], path, app_spec, force, dry_run, port)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@generate_app.command(name="env")
|
|
408
|
+
def generate_env(
|
|
409
|
+
path: Path = typer.Option(Path("."), "--path", "-p", help="Directory to write files into."),
|
|
410
|
+
app_spec: str | None = _APP_OPTION,
|
|
411
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files."),
|
|
412
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Preview files without writing."),
|
|
413
|
+
port: int = typer.Option(8000, "--port", help="Application port."),
|
|
414
|
+
) -> None:
|
|
415
|
+
"""Generate a documented .env.example with all configuration keys."""
|
|
416
|
+
_run_generators([EnvGenerator], path, app_spec, force, dry_run, port)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@generate_app.command(name="all")
|
|
420
|
+
def generate_all(
|
|
421
|
+
path: Path = typer.Option(Path("."), "--path", "-p", help="Directory to write files into."),
|
|
422
|
+
app_spec: str | None = _APP_OPTION,
|
|
423
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files."),
|
|
424
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Preview files without writing."),
|
|
425
|
+
port: int = typer.Option(8000, "--port", help="Application container port."),
|
|
426
|
+
) -> None:
|
|
427
|
+
"""Generate all deployment and infrastructure assets at once."""
|
|
428
|
+
_run_generators(list(ALL_GENERATORS), path, app_spec, force, dry_run, port)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Deployment and infrastructure asset generators for ProdKit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from prodkit.generators.base import (
|
|
6
|
+
BaseGenerator,
|
|
7
|
+
FileStatus,
|
|
8
|
+
GeneratedFile,
|
|
9
|
+
GeneratorContext,
|
|
10
|
+
)
|
|
11
|
+
from prodkit.generators.compose import ComposeGenerator
|
|
12
|
+
from prodkit.generators.docker import DockerGenerator
|
|
13
|
+
from prodkit.generators.env import EnvGenerator
|
|
14
|
+
from prodkit.generators.github import GitHubGenerator
|
|
15
|
+
from prodkit.generators.nginx import NginxGenerator
|
|
16
|
+
|
|
17
|
+
ALL_GENERATORS: tuple[type[BaseGenerator], ...] = (
|
|
18
|
+
DockerGenerator,
|
|
19
|
+
ComposeGenerator,
|
|
20
|
+
NginxGenerator,
|
|
21
|
+
GitHubGenerator,
|
|
22
|
+
EnvGenerator,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"ALL_GENERATORS",
|
|
27
|
+
"BaseGenerator",
|
|
28
|
+
"ComposeGenerator",
|
|
29
|
+
"DockerGenerator",
|
|
30
|
+
"EnvGenerator",
|
|
31
|
+
"FileStatus",
|
|
32
|
+
"GeneratedFile",
|
|
33
|
+
"GeneratorContext",
|
|
34
|
+
"GitHubGenerator",
|
|
35
|
+
"NginxGenerator",
|
|
36
|
+
]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Base classes, data models, and protocol for deployment generators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Literal
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from prodkit.core.production import Production
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
FileStatus = Literal["created", "skipped", "overwritten", "dry-run"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class GeneratedFile:
|
|
19
|
+
"""A generated file artifact with a relative destination path and content."""
|
|
20
|
+
|
|
21
|
+
path: Path
|
|
22
|
+
content: str
|
|
23
|
+
description: str = ""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class GeneratorContext:
|
|
28
|
+
"""Context passed to generators during template rendering and file writing."""
|
|
29
|
+
|
|
30
|
+
production: Production | None = None
|
|
31
|
+
root_dir: Path = field(default_factory=lambda: Path("."))
|
|
32
|
+
force: bool = False
|
|
33
|
+
dry_run: bool = False
|
|
34
|
+
port: int = 8000
|
|
35
|
+
app_spec: str = "main:app"
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def health_path(self) -> str:
|
|
39
|
+
if self.production is not None:
|
|
40
|
+
return self.production.config.health.health_path
|
|
41
|
+
return "/health"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def live_path(self) -> str:
|
|
45
|
+
if self.production is not None:
|
|
46
|
+
return self.production.config.health.live_path
|
|
47
|
+
return "/live"
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def metrics_path(self) -> str:
|
|
51
|
+
if self.production is not None:
|
|
52
|
+
return self.production.config.metrics.path
|
|
53
|
+
return "/metrics"
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def metrics_enabled(self) -> bool:
|
|
57
|
+
if self.production is not None:
|
|
58
|
+
return self.production.config.metrics.enabled
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def has_redis(self) -> bool:
|
|
63
|
+
if self.production is not None:
|
|
64
|
+
cfg = self.production.config
|
|
65
|
+
if cfg.rate_limit.enabled and cfg.rate_limit.backend == "redis":
|
|
66
|
+
return True
|
|
67
|
+
if cfg.cache.enabled and cfg.cache.backend == "redis":
|
|
68
|
+
return True
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class BaseGenerator(ABC):
|
|
73
|
+
"""Abstract base class implemented by each deployment asset generator."""
|
|
74
|
+
|
|
75
|
+
name: str = "base"
|
|
76
|
+
description: str = ""
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def generate(self, ctx: GeneratorContext) -> list[GeneratedFile]:
|
|
80
|
+
"""Render templates and return the list of generated files without writing."""
|
|
81
|
+
raise NotImplementedError
|
|
82
|
+
|
|
83
|
+
def write(self, ctx: GeneratorContext) -> list[tuple[GeneratedFile, FileStatus]]:
|
|
84
|
+
"""Render and optionally write the files to disk based on context options."""
|
|
85
|
+
generated_files = self.generate(ctx)
|
|
86
|
+
results: list[tuple[GeneratedFile, FileStatus]] = []
|
|
87
|
+
|
|
88
|
+
for gen_file in generated_files:
|
|
89
|
+
target_path = ctx.root_dir / gen_file.path
|
|
90
|
+
|
|
91
|
+
if ctx.dry_run:
|
|
92
|
+
results.append((gen_file, "dry-run"))
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
if target_path.exists() and not ctx.force:
|
|
96
|
+
results.append((gen_file, "skipped"))
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
status: FileStatus = "overwritten" if target_path.exists() else "created"
|
|
100
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
target_path.write_text(gen_file.content, encoding="utf-8")
|
|
102
|
+
results.append((gen_file, status))
|
|
103
|
+
|
|
104
|
+
return results
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Docker Compose generator: creates production-ready docker-compose.yml."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from prodkit.generators.base import BaseGenerator, GeneratedFile, GeneratorContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ComposeGenerator(BaseGenerator):
|
|
11
|
+
"""Generates docker-compose.yml with app service and optional Redis dependencies."""
|
|
12
|
+
|
|
13
|
+
name = "compose"
|
|
14
|
+
description = "Generate a production-ready docker-compose.yml"
|
|
15
|
+
|
|
16
|
+
def generate(self, ctx: GeneratorContext) -> list[GeneratedFile]:
|
|
17
|
+
app_env_entries = [
|
|
18
|
+
" - PRODKIT_ENVIRONMENT=production",
|
|
19
|
+
f" - PORT={ctx.port}",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
redis_service = ""
|
|
23
|
+
volumes_section = ""
|
|
24
|
+
depends_on_section = ""
|
|
25
|
+
|
|
26
|
+
if ctx.has_redis:
|
|
27
|
+
app_env_entries.extend(
|
|
28
|
+
[
|
|
29
|
+
" - PRODKIT_RATE_LIMIT__REDIS_URL=redis://redis:6379/0",
|
|
30
|
+
" - PRODKIT_CACHE__REDIS_URL=redis://redis:6379/0",
|
|
31
|
+
]
|
|
32
|
+
)
|
|
33
|
+
depends_on_section = """\
|
|
34
|
+
depends_on:
|
|
35
|
+
redis:
|
|
36
|
+
condition: service_healthy
|
|
37
|
+
"""
|
|
38
|
+
redis_service = """\
|
|
39
|
+
redis:
|
|
40
|
+
image: redis:7-alpine
|
|
41
|
+
restart: unless-stopped
|
|
42
|
+
ports:
|
|
43
|
+
- "6379:6379"
|
|
44
|
+
volumes:
|
|
45
|
+
- redis_data:/data
|
|
46
|
+
healthcheck:
|
|
47
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
48
|
+
interval: 10s
|
|
49
|
+
timeout: 3s
|
|
50
|
+
retries: 3
|
|
51
|
+
"""
|
|
52
|
+
volumes_section = """\
|
|
53
|
+
volumes:
|
|
54
|
+
redis_data:
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
env_block = "\n".join(app_env_entries)
|
|
58
|
+
|
|
59
|
+
compose_content = f"""\
|
|
60
|
+
# ==============================================================================
|
|
61
|
+
# Docker Compose configuration for FastAPI + ProdKit
|
|
62
|
+
# ==============================================================================
|
|
63
|
+
|
|
64
|
+
services:
|
|
65
|
+
app:
|
|
66
|
+
build:
|
|
67
|
+
context: .
|
|
68
|
+
dockerfile: Dockerfile
|
|
69
|
+
restart: unless-stopped
|
|
70
|
+
ports:
|
|
71
|
+
- "${{PORT:-{ctx.port}}}:{ctx.port}"
|
|
72
|
+
environment:
|
|
73
|
+
{env_block}
|
|
74
|
+
env_file:
|
|
75
|
+
- path: .env
|
|
76
|
+
required: false
|
|
77
|
+
{depends_on_section} healthcheck:
|
|
78
|
+
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:{ctx.port}{ctx.health_path}')"]
|
|
79
|
+
interval: 30s
|
|
80
|
+
timeout: 5s
|
|
81
|
+
retries: 3
|
|
82
|
+
start_period: 10s
|
|
83
|
+
|
|
84
|
+
{redis_service}{volumes_section}
|
|
85
|
+
networks:
|
|
86
|
+
default:
|
|
87
|
+
name: prodkit_net
|
|
88
|
+
"""
|
|
89
|
+
return [
|
|
90
|
+
GeneratedFile(
|
|
91
|
+
path=Path("docker-compose.yml"),
|
|
92
|
+
content=compose_content.strip() + "\n",
|
|
93
|
+
description="Docker Compose multi-service deployment definition",
|
|
94
|
+
)
|
|
95
|
+
]
|