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.
Files changed (61) hide show
  1. {prodkit-0.3.1 → prodkit-0.4.0}/CHANGELOG.md +12 -0
  2. {prodkit-0.3.1 → prodkit-0.4.0}/PKG-INFO +46 -6
  3. {prodkit-0.3.1 → prodkit-0.4.0}/README.md +45 -5
  4. {prodkit-0.3.1 → prodkit-0.4.0}/pyproject.toml +1 -1
  5. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/__init__.py +1 -1
  6. prodkit-0.4.0/src/prodkit/cli/__main__.py +6 -0
  7. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/cli/app.py +171 -3
  8. prodkit-0.4.0/src/prodkit/generators/__init__.py +36 -0
  9. prodkit-0.4.0/src/prodkit/generators/base.py +104 -0
  10. prodkit-0.4.0/src/prodkit/generators/compose.py +95 -0
  11. prodkit-0.4.0/src/prodkit/generators/docker.py +140 -0
  12. prodkit-0.4.0/src/prodkit/generators/env.py +121 -0
  13. prodkit-0.4.0/src/prodkit/generators/github.py +87 -0
  14. prodkit-0.4.0/src/prodkit/generators/nginx.py +111 -0
  15. prodkit-0.4.0/tests/integration/test_cli_generate.py +131 -0
  16. prodkit-0.4.0/tests/unit/test_generators.py +211 -0
  17. {prodkit-0.3.1 → prodkit-0.4.0}/.gitignore +0 -0
  18. {prodkit-0.3.1 → prodkit-0.4.0}/LICENSE +0 -0
  19. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/cli/__init__.py +0 -0
  20. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/cli/loader.py +0 -0
  21. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/contracts/__init__.py +0 -0
  22. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/contracts/plugin.py +0 -0
  23. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/__init__.py +0 -0
  24. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/config.py +0 -0
  25. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/context.py +0 -0
  26. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/doctor.py +0 -0
  27. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/event_bus.py +0 -0
  28. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/exceptions.py +0 -0
  29. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/lifecycle.py +0 -0
  30. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/plugin_manager.py +0 -0
  31. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/production.py +0 -0
  32. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/core/registry.py +0 -0
  33. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/__init__.py +0 -0
  34. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/_redis.py +0 -0
  35. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/cache/__init__.py +0 -0
  36. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/compression/__init__.py +0 -0
  37. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/cors/__init__.py +0 -0
  38. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/errors/__init__.py +0 -0
  39. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/health/__init__.py +0 -0
  40. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/logging/__init__.py +0 -0
  41. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/metrics/__init__.py +0 -0
  42. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/rate_limit/__init__.py +0 -0
  43. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/rate_limit/backends.py +0 -0
  44. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/request_id/__init__.py +0 -0
  45. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/security/__init__.py +0 -0
  46. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/plugins/tracing/__init__.py +0 -0
  47. {prodkit-0.3.1 → prodkit-0.4.0}/src/prodkit/py.typed +0 -0
  48. {prodkit-0.3.1 → prodkit-0.4.0}/tests/__init__.py +0 -0
  49. {prodkit-0.3.1 → prodkit-0.4.0}/tests/conftest.py +0 -0
  50. {prodkit-0.3.1 → prodkit-0.4.0}/tests/integration/__init__.py +0 -0
  51. {prodkit-0.3.1 → prodkit-0.4.0}/tests/integration/test_cli.py +0 -0
  52. {prodkit-0.3.1 → prodkit-0.4.0}/tests/integration/test_logging.py +0 -0
  53. {prodkit-0.3.1 → prodkit-0.4.0}/tests/integration/test_production.py +0 -0
  54. {prodkit-0.3.1 → prodkit-0.4.0}/tests/unit/__init__.py +0 -0
  55. {prodkit-0.3.1 → prodkit-0.4.0}/tests/unit/test_cache.py +0 -0
  56. {prodkit-0.3.1 → prodkit-0.4.0}/tests/unit/test_config.py +0 -0
  57. {prodkit-0.3.1 → prodkit-0.4.0}/tests/unit/test_doctor.py +0 -0
  58. {prodkit-0.3.1 → prodkit-0.4.0}/tests/unit/test_kernel.py +0 -0
  59. {prodkit-0.3.1 → prodkit-0.4.0}/tests/unit/test_metrics.py +0 -0
  60. {prodkit-0.3.1 → prodkit-0.4.0}/tests/unit/test_rate_limit.py +0 -0
  61. {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.1
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
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](https://github.com/Pushkarpant/PRODKIT/blob/main/LICENSE)
91
91
  [![Code Style: Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg?style=for-the-badge)](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` commands | `typer`, `rich` |
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** | ✅ **Current** | **Prometheus metrics, Redis backends, OpenTelemetry tracing, Cache service** |
412
- | **v0.4.0** | 🚧 Next | `prodkit generate` (Dockerfile, nginx, docker-compose, GitHub Actions CI) |
413
- | **v0.5.0** | 📅 Planned | Public Plugin SDK & Ecosystem (`prodkit-sentry`, entry-point discovery) |
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
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](https://github.com/Pushkarpant/PRODKIT/blob/main/LICENSE)
21
21
  [![Code Style: Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg?style=for-the-badge)](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` commands | `typer`, `rich` |
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** | ✅ **Current** | **Prometheus metrics, Redis backends, OpenTelemetry tracing, Cache service** |
342
- | **v0.4.0** | 🚧 Next | `prodkit generate` (Dockerfile, nginx, docker-compose, GitHub Actions CI) |
343
- | **v0.5.0** | 📅 Planned | Public Plugin SDK & Ecosystem (`prodkit-sentry`, entry-point discovery) |
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
  ---
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "prodkit"
7
- version = "0.3.1"
7
+ version = "0.4.0"
8
8
  description = "The production framework for FastAPI. One line. Production ready."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -37,7 +37,7 @@ from prodkit.plugins import builtin_plugins
37
37
  # root — the kernel itself never imports from prodkit.plugins.
38
38
  set_builtin_factory(builtin_plugins)
39
39
 
40
- __version__ = "0.3.1"
40
+ __version__ = "0.4.0"
41
41
 
42
42
  __all__ = [
43
43
  "Audit",
@@ -0,0 +1,6 @@
1
+ """Executable entrypoint for ``python -m prodkit.cli``."""
2
+
3
+ from prodkit.cli import run
4
+
5
+ if __name__ == "__main__":
6
+ run()
@@ -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 inspect the production-readiness of a FastAPI app.",
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): # type: ignore[no-untyped-def]
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
+ ]