fastapi-spawn 0.1.0__py3-none-any.whl

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 (50) hide show
  1. fastapi_spawn/__init__.py +6 -0
  2. fastapi_spawn/cli.py +387 -0
  3. fastapi_spawn/config.py +162 -0
  4. fastapi_spawn/constants.py +133 -0
  5. fastapi_spawn/generator.py +294 -0
  6. fastapi_spawn/interactive.py +192 -0
  7. fastapi_spawn/templates/alembic/alembic.ini.j2 +39 -0
  8. fastapi_spawn/templates/alembic/env.py.j2 +64 -0
  9. fastapi_spawn/templates/app/__init__.py.j2 +1 -0
  10. fastapi_spawn/templates/app/api/deps.py.j2 +39 -0
  11. fastapi_spawn/templates/app/api/v1/auth.py.j2 +59 -0
  12. fastapi_spawn/templates/app/api/v1/health.py.j2 +40 -0
  13. fastapi_spawn/templates/app/core/ai.py.j2 +76 -0
  14. fastapi_spawn/templates/app/core/config.py.j2 +177 -0
  15. fastapi_spawn/templates/app/core/exceptions.py.j2 +43 -0
  16. fastapi_spawn/templates/app/core/logging.py.j2 +70 -0
  17. fastapi_spawn/templates/app/core/security.py.j2 +42 -0
  18. fastapi_spawn/templates/app/core/storage.py.j2 +73 -0
  19. fastapi_spawn/templates/app/db/session.py.j2 +84 -0
  20. fastapi_spawn/templates/app/main.py.j2 +71 -0
  21. fastapi_spawn/templates/base/Makefile.j2 +45 -0
  22. fastapi_spawn/templates/base/README.md.j2 +74 -0
  23. fastapi_spawn/templates/base/env.j2 +82 -0
  24. fastapi_spawn/templates/base/env_example.j2 +85 -0
  25. fastapi_spawn/templates/base/gitignore.j2 +38 -0
  26. fastapi_spawn/templates/base/pre_commit.j2 +17 -0
  27. fastapi_spawn/templates/base/pyproject.toml.j2 +129 -0
  28. fastapi_spawn/templates/ci/github/publish.yml.j2 +32 -0
  29. fastapi_spawn/templates/ci/github/tests.yml.j2 +39 -0
  30. fastapi_spawn/templates/ci/gitlab/gitlab-ci.yml.j2 +29 -0
  31. fastapi_spawn/templates/docker/Dockerfile.j2 +17 -0
  32. fastapi_spawn/templates/docker/docker-compose.yml.j2 +97 -0
  33. fastapi_spawn/templates/docker/dockerignore.j2 +13 -0
  34. fastapi_spawn/templates/infra/docker/docker-compose.prod.yml.j2 +43 -0
  35. fastapi_spawn/templates/infra/helm/Chart.yaml.j2 +6 -0
  36. fastapi_spawn/templates/infra/helm/values.yaml.j2 +26 -0
  37. fastapi_spawn/templates/infra/terraform/main.tf.j2 +26 -0
  38. fastapi_spawn/templates/infra/terraform/variables.tf.j2 +17 -0
  39. fastapi_spawn/templates/root/main.py.j2 +16 -0
  40. fastapi_spawn/templates/tasks/celery_app.py.j2 +37 -0
  41. fastapi_spawn/templates/tasks/sample_tasks.py.j2 +27 -0
  42. fastapi_spawn/templates/tests/conftest.py.j2 +22 -0
  43. fastapi_spawn/templates/tests/test_health.py.j2 +30 -0
  44. fastapi_spawn/utils.py +58 -0
  45. fastapi_spawn/validators.py +67 -0
  46. fastapi_spawn-0.1.0.dist-info/METADATA +262 -0
  47. fastapi_spawn-0.1.0.dist-info/RECORD +50 -0
  48. fastapi_spawn-0.1.0.dist-info/WHEEL +4 -0
  49. fastapi_spawn-0.1.0.dist-info/entry_points.txt +2 -0
  50. fastapi_spawn-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,6 @@
1
+ """fastapi-spawn — Production-ready FastAPI project scaffolding CLI."""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "Bishwajit Garai"
5
+ __email__ = "bishwajitgarai@gmail.com"
6
+ __license__ = "MIT"
fastapi_spawn/cli.py ADDED
@@ -0,0 +1,387 @@
1
+ """Main CLI entry point for fastapi-spawn."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import typer
9
+ from rich import print as rprint
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+
14
+ from fastapi_spawn import __version__
15
+ from fastapi_spawn.config import ProjectConfig
16
+ from fastapi_spawn.constants import (
17
+ AIProvider,
18
+ AuthType,
19
+ Broker,
20
+ Cache,
21
+ CIProvider,
22
+ Database,
23
+ LogLibrary,
24
+ MigrationTool,
25
+ ORM,
26
+ Stack,
27
+ Storage,
28
+ )
29
+ from fastapi_spawn.generator import ProjectGenerator
30
+ from fastapi_spawn.interactive import run_interactive_flow
31
+ from fastapi_spawn.validators import validate_orm_db_compat, validate_output_dir, validate_project_name
32
+
33
+ app = typer.Typer(
34
+ name="fastapi-spawn",
35
+ help="[bold cyan]fastapi-spawn[/bold cyan] — Scaffold production-ready FastAPI projects in seconds.",
36
+ add_completion=True,
37
+ rich_markup_mode="rich",
38
+ no_args_is_help=True,
39
+ )
40
+ console = Console()
41
+
42
+
43
+ def version_callback(value: bool) -> None:
44
+ if value:
45
+ rprint(f"[bold cyan]fastapi-spawn[/bold cyan] version [bold]{__version__}[/bold]")
46
+ raise typer.Exit()
47
+
48
+
49
+ # ── `new` command ─────────────────────────────────────────────────────────────
50
+
51
+ @app.command("new", help="Create a new FastAPI project.")
52
+ def new(
53
+ project_name: Optional[str] = typer.Argument(None, help="Name of the new project"),
54
+ db: Optional[Database] = typer.Option(None, "--db", help="Database backend"),
55
+ orm: Optional[ORM] = typer.Option(None, "--orm", help="ORM / ODM"),
56
+ migration: Optional[MigrationTool] = typer.Option(None, "--migration", help="Migration tool (alembic, aerich, none)"),
57
+ auth: Optional[AuthType] = typer.Option(None, "--auth", help="Auth strategy"),
58
+ broker: Optional[Broker] = typer.Option(None, "--broker", help="Message broker"),
59
+ cache: Optional[Cache] = typer.Option(None, "--cache", help="Cache layer"),
60
+ storage: Optional[Storage] = typer.Option(None, "--storage", help="File storage (s3, local, none)"),
61
+ ai: Optional[AIProvider] = typer.Option(None, "--ai", help="AI provider (openai, anthropic, none)"),
62
+ stack: Optional[Stack] = typer.Option(None, "--stack", help="Deployment stack"),
63
+ ci: Optional[CIProvider] = typer.Option(None, "--ci", help="CI/CD provider"),
64
+ log_lib: Optional[LogLibrary] = typer.Option(None, "--log-lib", help="Logging library"),
65
+ no_docker: bool = typer.Option(False, "--no-docker", help="Skip Docker files"),
66
+ no_tests: bool = typer.Option(False, "--no-tests", help="Skip test suite"),
67
+ dry_run: bool = typer.Option(False, "--dry-run", help="Preview structure without writing files"),
68
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing directory"),
69
+ output: Path = typer.Option(Path("."), "--output", "-o", help="Output directory"),
70
+ version: Optional[bool] = typer.Option(
71
+ None, "--version", "-v", callback=version_callback, is_eager=True, help="Show version"
72
+ ),
73
+ ) -> None:
74
+ _print_banner()
75
+
76
+ needs_interactive = any(
77
+ x is None for x in [project_name, db, orm, migration, auth, broker, cache, storage, ai, stack, ci, log_lib]
78
+ )
79
+
80
+ if needs_interactive:
81
+ console.print("[dim]Some options are missing — launching interactive mode...[/dim]\n")
82
+ opts = run_interactive_flow(project_name or "")
83
+ project_name = project_name or opts["project_name"]
84
+ db = db or opts["db"]
85
+ orm = orm or opts["orm"]
86
+ migration = migration or opts["migration"]
87
+ auth = auth or opts["auth"]
88
+ broker = broker or opts["broker"]
89
+ cache = cache or opts["cache"]
90
+ storage = storage or opts["storage"]
91
+ ai = ai or opts["ai"]
92
+ stack = stack or opts["stack"]
93
+ ci = ci or opts["ci"]
94
+ log_lib = log_lib or opts["log_lib"]
95
+ include_docker = (not no_docker) and opts["include_docker"]
96
+ include_tests = (not no_tests) and opts["include_tests"]
97
+ else:
98
+ include_docker = not no_docker
99
+ include_tests = not no_tests
100
+
101
+ # Validate
102
+ try:
103
+ validate_project_name(project_name) # type: ignore[arg-type]
104
+ validate_orm_db_compat(orm, db) # type: ignore[arg-type]
105
+ if not dry_run:
106
+ validate_output_dir(output / project_name, force) # type: ignore[operator]
107
+ except ValueError as exc:
108
+ console.print(f"[bold red]✗ Error:[/bold red] {exc}")
109
+ raise typer.Exit(1) from exc
110
+
111
+ config = ProjectConfig(
112
+ project_name=project_name, # type: ignore[arg-type]
113
+ db=db, # type: ignore[arg-type]
114
+ orm=orm, # type: ignore[arg-type]
115
+ migration=migration, # type: ignore[arg-type]
116
+ auth=auth, # type: ignore[arg-type]
117
+ broker=broker, # type: ignore[arg-type]
118
+ cache=cache, # type: ignore[arg-type]
119
+ storage=storage, # type: ignore[arg-type]
120
+ ai=ai, # type: ignore[arg-type]
121
+ stack=stack, # type: ignore[arg-type]
122
+ ci=ci, # type: ignore[arg-type]
123
+ log_lib=log_lib, # type: ignore[arg-type]
124
+ include_docker=include_docker,
125
+ include_tests=include_tests,
126
+ dry_run=dry_run,
127
+ force=force,
128
+ )
129
+
130
+ _print_summary(config)
131
+
132
+ if dry_run:
133
+ console.print("\n[bold yellow]🔍 Dry-run mode — no files will be written.[/bold yellow]\n")
134
+
135
+ try:
136
+ generator = ProjectGenerator(config, output)
137
+ project_path = generator.generate()
138
+ except Exception as exc:
139
+ console.print(f"\n[bold red]✗ Generation failed:[/bold red] {exc}")
140
+ raise typer.Exit(1) from exc
141
+
142
+ if not dry_run:
143
+ console.print(f"\n[bold green]✓ Project created:[/bold green] {project_path.resolve()}")
144
+ _print_next_steps(config)
145
+
146
+
147
+ # ── `list-templates` command ───────────────────────────────────────────────────
148
+
149
+ @app.command("list-templates", help="List all available options and compatible combinations.")
150
+ def list_templates() -> None:
151
+ _print_banner()
152
+ table = Table(title="Available Options", border_style="cyan", show_lines=True)
153
+ table.add_column("Flag", style="bold cyan")
154
+ table.add_column("Choices", style="white")
155
+
156
+ table.add_row("--db", ", ".join(d.value for d in Database))
157
+ table.add_row("--orm", ", ".join(o.value for o in ORM))
158
+ table.add_row("--migration", ", ".join(m.value for m in MigrationTool))
159
+ table.add_row("--auth", ", ".join(a.value for a in AuthType))
160
+ table.add_row("--broker", ", ".join(b.value for b in Broker))
161
+ table.add_row("--cache", ", ".join(c.value for c in Cache))
162
+ table.add_row("--storage", ", ".join(s.value for s in Storage))
163
+ table.add_row("--ai", ", ".join(a.value for a in AIProvider))
164
+ table.add_row("--stack", ", ".join(s.value for s in Stack))
165
+ table.add_row("--ci", ", ".join(c.value for c in CIProvider))
166
+ table.add_row("--log-lib", ", ".join(l.value for l in LogLibrary))
167
+ console.print(table)
168
+
169
+
170
+ # ── `validate` command ─────────────────────────────────────────────────────────
171
+
172
+ @app.command("validate", help="Validate a .fastapi-spawn.toml config file.")
173
+ def validate_config(
174
+ config_file: Path = typer.Argument(..., help="Path to .fastapi-spawn.toml"),
175
+ ) -> None:
176
+ if not config_file.exists():
177
+ console.print(f"[bold red]✗ File not found:[/bold red] {config_file}")
178
+ raise typer.Exit(1)
179
+ console.print(f"[bold green]✓ Config file found:[/bold green] {config_file}")
180
+ console.print("[dim]Full TOML validation coming in a future release.[/dim]")
181
+
182
+
183
+ # ── `add` command ────────────────────────────────────────────────────────────
184
+
185
+ _ADDABLE_FEATURES = {
186
+ "auth": "Authentication (JWT / OAuth2 / API Key)",
187
+ "s3": "AWS S3 file storage (boto3)",
188
+ "openai": "OpenAI integration (chat + embeddings)",
189
+ "anthropic": "Anthropic Claude integration",
190
+ "alembic": "Alembic async database migrations",
191
+ "celery": "Celery worker + sample tasks",
192
+ "redis": "Redis cache / broker support",
193
+ "kafka": "Kafka broker support (aiokafka)",
194
+ "docker": "Dockerfile + docker-compose.yml",
195
+ "ci": "GitHub Actions CI/CD workflows",
196
+ "helm": "Helm chart (infra/helm/)",
197
+ "terraform": "Terraform scaffold (infra/terraform/)",
198
+ }
199
+
200
+
201
+ @app.command(
202
+ "add",
203
+ help="Add a feature to an [bold]existing[/bold] fastapi-spawn project.",
204
+ )
205
+ def add_feature(
206
+ feature: str = typer.Argument(
207
+ ...,
208
+ help=f"Feature to add. Choices: {', '.join(_ADDABLE_FEATURES)}",
209
+ ),
210
+ project_dir: Path = typer.Option(
211
+ Path("."),
212
+ "--dir", "-d",
213
+ help="Path to the existing project (default: current directory)",
214
+ ),
215
+ ) -> None:
216
+ _print_banner()
217
+
218
+ if feature not in _ADDABLE_FEATURES:
219
+ console.print(
220
+ f"[bold red]✗ Unknown feature:[/bold red] '{feature}'\n"
221
+ f"[dim]Available: {', '.join(_ADDABLE_FEATURES)}[/dim]"
222
+ )
223
+ raise typer.Exit(1)
224
+
225
+ if not project_dir.exists():
226
+ console.print(f"[bold red]✗ Directory not found:[/bold red] {project_dir.resolve()}")
227
+ raise typer.Exit(1)
228
+
229
+ console.print(
230
+ f"[bold cyan]→ Adding feature:[/bold cyan] [bold]{feature}[/bold] — "
231
+ f"{_ADDABLE_FEATURES[feature]}"
232
+ )
233
+ console.print(
234
+ f"[dim]Target project:[/dim] {project_dir.resolve()}"
235
+ )
236
+ console.print()
237
+
238
+ # Feature-specific guidance
239
+ _feature_guidance(feature, project_dir)
240
+
241
+
242
+ def _feature_guidance(feature: str, project_dir: Path) -> None:
243
+ """Print actionable instructions for each addable feature."""
244
+ steps: list[str] = []
245
+
246
+ if feature == "auth":
247
+ steps = [
248
+ "Add to pyproject.toml deps: python-jose[cryptography], passlib[bcrypt], python-multipart",
249
+ "Copy app/core/security.py — run: fastapi-spawn new <name> --auth jwt --dry-run to preview",
250
+ "Add auth endpoints to app/api/v1/auth.py",
251
+ "Add ACCESS_TOKEN_EXPIRE_MINUTES, SECRET_KEY to .env",
252
+ ]
253
+ elif feature == "s3":
254
+ steps = [
255
+ "Add to pyproject.toml deps: boto3>=1.34.0",
256
+ "Create app/core/storage.py with boto3 helpers",
257
+ "Add to .env: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_S3_BUCKET, AWS_S3_ENDPOINT_URL",
258
+ "Add to app/core/config.py: AWS_* individual fields + (optional) s3_endpoint property",
259
+ ]
260
+ elif feature == "openai":
261
+ steps = [
262
+ "Add to pyproject.toml deps: openai>=1.30.0",
263
+ "Create app/core/ai.py with AsyncOpenAI client",
264
+ "Add to .env: OPENAI_API_KEY=sk-placeholder, OPENAI_MODEL=gpt-4o, OPENAI_BASE_URL= ",
265
+ ]
266
+ elif feature == "anthropic":
267
+ steps = [
268
+ "Add to pyproject.toml deps: anthropic>=0.28.0",
269
+ "Create app/core/ai.py with AsyncAnthropic client",
270
+ "Add to .env: ANTHROPIC_API_KEY=sk-ant-placeholder, ANTHROPIC_MODEL=claude-3-5-sonnet-20241022",
271
+ ]
272
+ elif feature == "alembic":
273
+ steps = [
274
+ "Add to pyproject.toml deps: alembic>=1.13.0",
275
+ "Run: alembic init migrations",
276
+ "Replace migrations/env.py with async-compatible version",
277
+ "Add to [tool.uv.scripts]: migrate = 'alembic upgrade head'",
278
+ "Run: uv run migrate",
279
+ ]
280
+ elif feature == "celery":
281
+ steps = [
282
+ "Add to pyproject.toml deps: celery[redis]>=5.3.6",
283
+ "Create tasks/celery_app.py and tasks/sample_tasks.py",
284
+ "Add to .env: REDIS_HOST, REDIS_PORT, REDIS_DB",
285
+ "Add to [tool.uv.scripts]: worker = 'celery -A tasks.celery_app worker --loglevel=info'",
286
+ "Run: uv run worker",
287
+ ]
288
+ elif feature == "redis":
289
+ steps = [
290
+ "Add to pyproject.toml deps: redis[hiredis]>=5.0.0",
291
+ "Add to .env: REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, REDIS_DB",
292
+ "Add redis_url @property to Settings in app/core/config.py",
293
+ ]
294
+ elif feature == "kafka":
295
+ steps = [
296
+ "Add to pyproject.toml deps: aiokafka>=0.10.0",
297
+ "Add to .env: KAFKA_HOST=localhost, KAFKA_PORT=9092",
298
+ "Create tasks/ producer/consumer modules",
299
+ ]
300
+ elif feature == "docker":
301
+ steps = [
302
+ "Create Dockerfile (uses uv for fast builds)",
303
+ "Create docker-compose.yml with all services",
304
+ "Create .dockerignore",
305
+ "Run: docker compose up --build",
306
+ ]
307
+ elif feature == "ci":
308
+ steps = [
309
+ "Create .github/workflows/tests.yml (matrix: py3.10, 3.11, 3.12)",
310
+ "Create .github/workflows/publish.yml (triggered on v* tags)",
311
+ "Add PYPI_API_TOKEN to GitHub repo secrets",
312
+ ]
313
+ elif feature == "helm":
314
+ steps = [
315
+ "Create infra/helm/Chart.yaml",
316
+ "Create infra/helm/values.yaml with replica, image, resource config",
317
+ "Run: helm install my-release ./infra/helm",
318
+ ]
319
+ elif feature == "terraform":
320
+ steps = [
321
+ "Create infra/terraform/main.tf (AWS ECR + ECS scaffold)",
322
+ "Create infra/terraform/variables.tf",
323
+ "Run: terraform -chdir=infra/terraform init && terraform apply",
324
+ ]
325
+
326
+ if steps:
327
+ panel_content = "\n".join(f" [dim]{i+1}.[/dim] {s}" for i, s in enumerate(steps))
328
+ console.print(
329
+ Panel(
330
+ panel_content,
331
+ title=f"[bold cyan]Steps to add '{feature}'[/bold cyan]",
332
+ border_style="cyan",
333
+ padding=(0, 1),
334
+ )
335
+ )
336
+ console.print(
337
+ "\n[dim]Tip: Re-scaffold with --dry-run to preview exact file contents:[/dim]\n"
338
+ f" [bold cyan]fastapi-spawn new <name> --{feature if feature not in ('s3','openai','anthropic','celery','redis','kafka') else 'storage s3 --ai openai'} --dry-run[/bold cyan]"
339
+ )
340
+
341
+
342
+ # ── Helpers ────────────────────────────────────────────────────────────────────
343
+
344
+ def _print_banner() -> None:
345
+ console.print(
346
+ Panel.fit(
347
+ "[bold cyan]⚡ fastapi-spawn[/bold cyan] [dim]v{}[/dim]\n"
348
+ "[dim]Production-ready FastAPI project scaffolding[/dim]".format(__version__),
349
+ border_style="cyan",
350
+ padding=(0, 2),
351
+ )
352
+ )
353
+
354
+
355
+ def _print_summary(config: ProjectConfig) -> None:
356
+ table = Table(title="Project Configuration", border_style="dim", show_header=False, show_lines=False)
357
+ table.add_column("Key", style="bold dim")
358
+ table.add_column("Value", style="cyan")
359
+ for key, val in config.summary_lines():
360
+ table.add_row(key, val)
361
+ console.print(table)
362
+
363
+
364
+ def _print_next_steps(config: ProjectConfig) -> None:
365
+ steps = [
366
+ f" [bold cyan]cd {config.project_name}[/bold cyan]",
367
+ " [bold cyan]uv sync[/bold cyan]",
368
+ ]
369
+ if config.has_alembic:
370
+ steps.append(" [bold cyan]uv run alembic upgrade head[/bold cyan]")
371
+ if config.has_docker:
372
+ steps.append(" [bold cyan]docker compose up --build[/bold cyan]")
373
+ else:
374
+ steps.append(" [bold cyan]uv run main.py[/bold cyan]")
375
+
376
+ console.print(
377
+ Panel(
378
+ "\n".join(steps),
379
+ title="[bold green]Next Steps[/bold green]",
380
+ border_style="green",
381
+ padding=(0, 1),
382
+ )
383
+ )
384
+
385
+
386
+ if __name__ == "__main__":
387
+ app()
@@ -0,0 +1,162 @@
1
+ """Project configuration dataclass for fastapi-spawn."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from fastapi_spawn.constants import (
8
+ AIProvider,
9
+ AuthType,
10
+ Broker,
11
+ Cache,
12
+ CIProvider,
13
+ Database,
14
+ LogLibrary,
15
+ MigrationTool,
16
+ ORM,
17
+ Stack,
18
+ Storage,
19
+ )
20
+
21
+
22
+ @dataclass
23
+ class ProjectConfig:
24
+ """Holds all user-selected options for a new FastAPI project."""
25
+
26
+ project_name: str
27
+ db: Database = Database.postgresql
28
+ orm: ORM = ORM.sqlalchemy
29
+ auth: AuthType = AuthType.jwt
30
+ broker: Broker = Broker.none
31
+ cache: Cache = Cache.none
32
+ stack: Stack = Stack.standard
33
+ ci: CIProvider = CIProvider.github
34
+ log_lib: LogLibrary = LogLibrary.loguru
35
+ storage: Storage = Storage.none
36
+ migration: MigrationTool = MigrationTool.none
37
+ ai: AIProvider = AIProvider.none
38
+ include_docker: bool = True
39
+ include_tests: bool = True
40
+ include_makefile: bool = True
41
+ dry_run: bool = False
42
+ force: bool = False
43
+ # Derived fields (set post-init)
44
+ package_name: str = field(default="", init=False)
45
+ slug: str = field(default="", init=False)
46
+
47
+ def __post_init__(self) -> None:
48
+ self.slug = self.project_name.lower().replace("-", "_").replace(" ", "_")
49
+ self.package_name = self.slug
50
+
51
+ # ── Convenience predicates ─────────────────────────────────────────────
52
+
53
+ @property
54
+ def has_relational_db(self) -> bool:
55
+ return self.db in (Database.postgresql, Database.mysql, Database.sqlite)
56
+
57
+ @property
58
+ def has_mongo(self) -> bool:
59
+ return self.db == Database.mongodb
60
+
61
+ @property
62
+ def has_auth(self) -> bool:
63
+ return self.auth != AuthType.none
64
+
65
+ @property
66
+ def has_broker(self) -> bool:
67
+ return self.broker != Broker.none
68
+
69
+ @property
70
+ def has_cache(self) -> bool:
71
+ return self.cache != Cache.none
72
+
73
+ @property
74
+ def has_docker(self) -> bool:
75
+ return self.include_docker and self.stack != Stack.minimal
76
+
77
+ @property
78
+ def has_infra(self) -> bool:
79
+ return self.stack == Stack.full
80
+
81
+ @property
82
+ def has_ci(self) -> bool:
83
+ return self.ci != CIProvider.none
84
+
85
+ @property
86
+ def has_storage(self) -> bool:
87
+ return self.storage != Storage.none
88
+
89
+ @property
90
+ def has_s3(self) -> bool:
91
+ return self.storage == Storage.s3
92
+
93
+ @property
94
+ def has_migration(self) -> bool:
95
+ return self.migration != MigrationTool.none
96
+
97
+ @property
98
+ def has_alembic(self) -> bool:
99
+ return self.migration == MigrationTool.alembic
100
+
101
+ @property
102
+ def has_ai(self) -> bool:
103
+ return self.ai != AIProvider.none
104
+
105
+ # ── Template context ───────────────────────────────────────────────────
106
+
107
+ def to_context(self) -> dict:
108
+ """Return a Jinja2-ready template context dict."""
109
+ return {
110
+ "project_name": self.project_name,
111
+ "package_name": self.package_name,
112
+ "slug": self.slug,
113
+ "db": self.db.value,
114
+ "orm": self.orm.value,
115
+ "auth": self.auth.value,
116
+ "broker": self.broker.value,
117
+ "cache": self.cache.value,
118
+ "stack": self.stack.value,
119
+ "ci": self.ci.value,
120
+ "log_lib": self.log_lib.value,
121
+ "storage": self.storage.value,
122
+ "migration": self.migration.value,
123
+ "ai": self.ai.value,
124
+ # Booleans for easy Jinja2 conditionals
125
+ "has_relational_db": self.has_relational_db,
126
+ "has_mongo": self.has_mongo,
127
+ "has_auth": self.has_auth,
128
+ "has_broker": self.has_broker,
129
+ "has_cache": self.has_cache,
130
+ "has_docker": self.has_docker,
131
+ "has_infra": self.has_infra,
132
+ "has_ci": self.has_ci,
133
+ "has_storage": self.has_storage,
134
+ "has_s3": self.has_s3,
135
+ "has_migration": self.has_migration,
136
+ "has_alembic": self.has_alembic,
137
+ "has_ai": self.has_ai,
138
+ "include_tests": self.include_tests,
139
+ "include_makefile": self.include_makefile,
140
+ }
141
+
142
+ def summary_lines(self) -> list[tuple[str, str]]:
143
+ """Return key-value pairs for display in the rich summary panel."""
144
+ rows = [
145
+ ("Project", self.project_name),
146
+ ("Package", self.package_name),
147
+ ("Database", self.db.value),
148
+ ("ORM", self.orm.value),
149
+ ("Migrations", self.migration.value),
150
+ ("Auth", self.auth.value),
151
+ ("Broker", self.broker.value),
152
+ ("Cache", self.cache.value),
153
+ ("Storage", self.storage.value),
154
+ ("AI", self.ai.value),
155
+ ("Stack", self.stack.value),
156
+ ("CI/CD", self.ci.value),
157
+ ("Logging", self.log_lib.value),
158
+ ("Docker", "yes" if self.has_docker else "no"),
159
+ ("Tests", "yes" if self.include_tests else "no"),
160
+ ("Dry-run", "yes" if self.dry_run else "no"),
161
+ ]
162
+ return rows
@@ -0,0 +1,133 @@
1
+ """Constants and enums for fastapi-spawn."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class Database(str, Enum):
7
+ postgresql = "postgresql"
8
+ mysql = "mysql"
9
+ mongodb = "mongodb"
10
+ sqlite = "sqlite"
11
+ none = "none"
12
+
13
+
14
+ class ORM(str, Enum):
15
+ sqlalchemy = "sqlalchemy"
16
+ tortoise = "tortoise"
17
+ beanie = "beanie"
18
+ none = "none"
19
+
20
+
21
+ class AuthType(str, Enum):
22
+ jwt = "jwt"
23
+ oauth2 = "oauth2"
24
+ api_key = "api-key"
25
+ none = "none"
26
+
27
+
28
+ class Broker(str, Enum):
29
+ redis = "redis"
30
+ rabbitmq = "rabbitmq"
31
+ kafka = "kafka"
32
+ none = "none"
33
+
34
+
35
+ class Cache(str, Enum):
36
+ redis = "redis"
37
+ memcached = "memcached"
38
+ none = "none"
39
+
40
+
41
+ class Stack(str, Enum):
42
+ minimal = "minimal"
43
+ standard = "standard"
44
+ full = "full"
45
+
46
+
47
+ class CIProvider(str, Enum):
48
+ github = "github"
49
+ gitlab = "gitlab"
50
+ both = "both"
51
+ none = "none"
52
+
53
+
54
+ class LogLibrary(str, Enum):
55
+ loguru = "loguru"
56
+ structlog = "structlog"
57
+ standard = "standard"
58
+
59
+
60
+ class Storage(str, Enum):
61
+ s3 = "s3"
62
+ local = "local"
63
+ none = "none"
64
+
65
+
66
+ class MigrationTool(str, Enum):
67
+ alembic = "alembic" # SQLAlchemy / relational DBs
68
+ aerich = "aerich" # Tortoise ORM
69
+ none = "none"
70
+
71
+
72
+ class AIProvider(str, Enum):
73
+ openai = "openai"
74
+ anthropic = "anthropic"
75
+ none = "none"
76
+
77
+
78
+ # ORM ↔ Database compatibility matrix
79
+ ORM_DB_COMPAT: dict[str, list[str]] = {
80
+ ORM.sqlalchemy: [Database.postgresql, Database.mysql, Database.sqlite],
81
+ ORM.tortoise: [Database.postgresql, Database.mysql, Database.sqlite],
82
+ ORM.beanie: [Database.mongodb],
83
+ ORM.none: [Database.postgresql, Database.mysql, Database.mongodb, Database.sqlite, Database.none],
84
+ }
85
+
86
+ # Migration tool ↔ ORM compatibility
87
+ MIGRATION_ORM_COMPAT: dict[str, list[str]] = {
88
+ MigrationTool.alembic: [ORM.sqlalchemy],
89
+ MigrationTool.aerich: [ORM.tortoise],
90
+ MigrationTool.none: list(ORM),
91
+ }
92
+
93
+ # Human-readable labels
94
+ DB_LABELS = {
95
+ Database.postgresql: "PostgreSQL",
96
+ Database.mysql: "MySQL",
97
+ Database.mongodb: "MongoDB",
98
+ Database.sqlite: "SQLite",
99
+ Database.none: "No database",
100
+ }
101
+
102
+ AUTH_LABELS = {
103
+ AuthType.jwt: "JWT (JSON Web Tokens)",
104
+ AuthType.oauth2: "OAuth2 (Password flow)",
105
+ AuthType.api_key: "API Key",
106
+ AuthType.none: "No authentication",
107
+ }
108
+
109
+ BROKER_LABELS = {
110
+ Broker.redis: "Redis (via Celery)",
111
+ Broker.rabbitmq: "RabbitMQ (via Celery)",
112
+ Broker.kafka: "Kafka (via aiokafka)",
113
+ Broker.none: "No message broker",
114
+ }
115
+
116
+ STORAGE_LABELS = {
117
+ Storage.s3: "AWS S3 (via boto3)",
118
+ Storage.local: "Local filesystem",
119
+ Storage.none: "No file storage",
120
+ }
121
+
122
+ AI_LABELS = {
123
+ AIProvider.openai: "OpenAI (GPT-4o, GPT-4, embeddings)",
124
+ AIProvider.anthropic: "Anthropic (Claude 3.x)",
125
+ AIProvider.none: "No AI integration",
126
+ }
127
+
128
+ STACK_DESCRIPTIONS = {
129
+ Stack.minimal: "Core app only — no Docker, no infra",
130
+ Stack.standard: "App + Docker + GitHub CI",
131
+ Stack.full: "App + Docker + CI + Helm + Terraform",
132
+ }
133
+