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.
- fastapi_spawn/__init__.py +6 -0
- fastapi_spawn/cli.py +387 -0
- fastapi_spawn/config.py +162 -0
- fastapi_spawn/constants.py +133 -0
- fastapi_spawn/generator.py +294 -0
- fastapi_spawn/interactive.py +192 -0
- fastapi_spawn/templates/alembic/alembic.ini.j2 +39 -0
- fastapi_spawn/templates/alembic/env.py.j2 +64 -0
- fastapi_spawn/templates/app/__init__.py.j2 +1 -0
- fastapi_spawn/templates/app/api/deps.py.j2 +39 -0
- fastapi_spawn/templates/app/api/v1/auth.py.j2 +59 -0
- fastapi_spawn/templates/app/api/v1/health.py.j2 +40 -0
- fastapi_spawn/templates/app/core/ai.py.j2 +76 -0
- fastapi_spawn/templates/app/core/config.py.j2 +177 -0
- fastapi_spawn/templates/app/core/exceptions.py.j2 +43 -0
- fastapi_spawn/templates/app/core/logging.py.j2 +70 -0
- fastapi_spawn/templates/app/core/security.py.j2 +42 -0
- fastapi_spawn/templates/app/core/storage.py.j2 +73 -0
- fastapi_spawn/templates/app/db/session.py.j2 +84 -0
- fastapi_spawn/templates/app/main.py.j2 +71 -0
- fastapi_spawn/templates/base/Makefile.j2 +45 -0
- fastapi_spawn/templates/base/README.md.j2 +74 -0
- fastapi_spawn/templates/base/env.j2 +82 -0
- fastapi_spawn/templates/base/env_example.j2 +85 -0
- fastapi_spawn/templates/base/gitignore.j2 +38 -0
- fastapi_spawn/templates/base/pre_commit.j2 +17 -0
- fastapi_spawn/templates/base/pyproject.toml.j2 +129 -0
- fastapi_spawn/templates/ci/github/publish.yml.j2 +32 -0
- fastapi_spawn/templates/ci/github/tests.yml.j2 +39 -0
- fastapi_spawn/templates/ci/gitlab/gitlab-ci.yml.j2 +29 -0
- fastapi_spawn/templates/docker/Dockerfile.j2 +17 -0
- fastapi_spawn/templates/docker/docker-compose.yml.j2 +97 -0
- fastapi_spawn/templates/docker/dockerignore.j2 +13 -0
- fastapi_spawn/templates/infra/docker/docker-compose.prod.yml.j2 +43 -0
- fastapi_spawn/templates/infra/helm/Chart.yaml.j2 +6 -0
- fastapi_spawn/templates/infra/helm/values.yaml.j2 +26 -0
- fastapi_spawn/templates/infra/terraform/main.tf.j2 +26 -0
- fastapi_spawn/templates/infra/terraform/variables.tf.j2 +17 -0
- fastapi_spawn/templates/root/main.py.j2 +16 -0
- fastapi_spawn/templates/tasks/celery_app.py.j2 +37 -0
- fastapi_spawn/templates/tasks/sample_tasks.py.j2 +27 -0
- fastapi_spawn/templates/tests/conftest.py.j2 +22 -0
- fastapi_spawn/templates/tests/test_health.py.j2 +30 -0
- fastapi_spawn/utils.py +58 -0
- fastapi_spawn/validators.py +67 -0
- fastapi_spawn-0.1.0.dist-info/METADATA +262 -0
- fastapi_spawn-0.1.0.dist-info/RECORD +50 -0
- fastapi_spawn-0.1.0.dist-info/WHEEL +4 -0
- fastapi_spawn-0.1.0.dist-info/entry_points.txt +2 -0
- fastapi_spawn-0.1.0.dist-info/licenses/LICENSE +21 -0
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()
|
fastapi_spawn/config.py
ADDED
|
@@ -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
|
+
|