fastapi-spawn 0.4.40__tar.gz → 0.5.1__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 (79) hide show
  1. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/PKG-INFO +1 -1
  2. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/cli.py +154 -117
  3. fastapi_spawn-0.5.1/fastapi_spawn/steps.py +455 -0
  4. fastapi_spawn-0.5.1/fastapi_spawn/templates/base/env.j2 +209 -0
  5. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/base/env_example.j2 +49 -29
  6. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/docker/docker-compose.yml.j2 +5 -5
  7. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/pyproject.toml +1 -1
  8. fastapi_spawn-0.4.40/fastapi_spawn/templates/base/env.j2 +0 -103
  9. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/.gitignore +0 -0
  10. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/LICENSE +0 -0
  11. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/README.md +0 -0
  12. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/__init__.py +0 -0
  13. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/config.py +0 -0
  14. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/constants.py +0 -0
  15. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/generator.py +0 -0
  16. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/interactive.py +0 -0
  17. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/alembic/alembic.ini.j2 +0 -0
  18. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/alembic/env.py.j2 +0 -0
  19. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/__init__.py.j2 +0 -0
  20. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/admin/setup.py.j2 +0 -0
  21. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/api/deps.py.j2 +0 -0
  22. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/api/graphql.py.j2 +0 -0
  23. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/api/v1/auth/router.py.j2 +0 -0
  24. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/api/v1/auth/sso.py.j2 +0 -0
  25. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/api/v1/health/router.py.j2 +0 -0
  26. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/api/v1/pagination/router.py.j2 +0 -0
  27. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/api/v1/payments/router.py.j2 +0 -0
  28. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/api/v1/permissions/router.py.j2 +0 -0
  29. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/api/v1/router.py.j2 +0 -0
  30. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/api/v1/streaming/router.py.j2 +0 -0
  31. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/api/v1/uploads/router.py.j2 +0 -0
  32. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/api/v1/ws/router.py.j2 +0 -0
  33. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/core/ai.py.j2 +0 -0
  34. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/core/cache.py.j2 +0 -0
  35. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/core/config.py.j2 +0 -0
  36. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/core/email.py.j2 +0 -0
  37. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/core/exceptions.py.j2 +0 -0
  38. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/core/logger.py.j2 +0 -0
  39. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/core/logging.py.j2 +0 -0
  40. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/core/monitoring.py.j2 +0 -0
  41. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/core/notifications.py.j2 +0 -0
  42. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/core/ocr.py.j2 +0 -0
  43. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/core/permissions.py.j2 +0 -0
  44. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/core/search.py.j2 +0 -0
  45. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/core/security.py.j2 +0 -0
  46. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/core/storage.py.j2 +0 -0
  47. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/core/vector_db.py.j2 +0 -0
  48. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/core/ws_manager.py.j2 +0 -0
  49. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/db/session.py.j2 +0 -0
  50. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/frontend/index.html.j2 +0 -0
  51. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/main.py.j2 +0 -0
  52. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/middleware/__init__.py.j2 +0 -0
  53. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/middleware/rate_limit.py.j2 +0 -0
  54. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/middleware/request_logger.py.j2 +0 -0
  55. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/app/middleware/response_format.py.j2 +0 -0
  56. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/base/Makefile.j2 +0 -0
  57. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/base/README.md.j2 +0 -0
  58. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/base/gitignore.j2 +0 -0
  59. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/base/pre_commit.j2 +0 -0
  60. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/base/pyproject.toml.j2 +0 -0
  61. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/ci/github/publish.yml.j2 +0 -0
  62. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/ci/github/tests.yml.j2 +0 -0
  63. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/ci/gitlab/gitlab-ci.yml.j2 +0 -0
  64. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/db/seed.py.j2 +0 -0
  65. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/docker/Dockerfile.j2 +0 -0
  66. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/docker/dockerignore.j2 +0 -0
  67. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/infra/docker/docker-compose.prod.yml.j2 +0 -0
  68. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/infra/helm/Chart.yaml.j2 +0 -0
  69. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/infra/helm/values.yaml.j2 +0 -0
  70. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/infra/terraform/main.tf.j2 +0 -0
  71. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/infra/terraform/variables.tf.j2 +0 -0
  72. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/root/main.py.j2 +0 -0
  73. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/tasks/arq_worker.py.j2 +0 -0
  74. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/tasks/celery_app.py.j2 +0 -0
  75. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/tasks/sample_tasks.py.j2 +0 -0
  76. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/tests/conftest.py.j2 +0 -0
  77. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/templates/tests/test_health.py.j2 +0 -0
  78. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/utils.py +0 -0
  79. {fastapi_spawn-0.4.40 → fastapi_spawn-0.5.1}/fastapi_spawn/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-spawn
3
- Version: 0.4.40
3
+ Version: 0.5.1
4
4
  Summary: A powerful CLI tool to scaffold production-ready FastAPI projects with flexible database, auth, broker, and deployment options.
5
5
  Project-URL: Homepage, https://github.com/Bishwajitgarai/fastapi-spawn
6
6
  Project-URL: Documentation, https://github.com/Bishwajitgarai/fastapi-spawn#readme
@@ -62,6 +62,14 @@ def version_callback(value: bool) -> None:
62
62
  raise typer.Exit()
63
63
 
64
64
 
65
+ @app.callback()
66
+ def main(
67
+ version: Optional[bool] = typer.Option(
68
+ None, "--version", "-v", callback=version_callback, is_eager=True, help="Show version"
69
+ ),
70
+ ) -> None:
71
+ pass
72
+
65
73
  @app.command("help", help="Show this help message and exit.")
66
74
  def show_help(ctx: typer.Context) -> None:
67
75
  """Print the global CLI help."""
@@ -485,6 +493,8 @@ _ADDABLE_FEATURES = {
485
493
  "opentelemetry": "OpenTelemetry distributed tracing",
486
494
  "sendgrid": "SendGrid email",
487
495
  "smtp": "SMTP email (fastapi-mail)",
496
+ "ses": "AWS SES email",
497
+ "resend": "Resend transactional email",
488
498
  "slack": "Slack webhook notifications",
489
499
  "discord": "Discord webhook notifications",
490
500
  "qdrant": "Qdrant vector database",
@@ -522,18 +532,35 @@ def add_feature(
522
532
  feature: Optional[str] = typer.Argument(None, help="Feature to add. Leave blank to see all available features."),
523
533
  project_dir: Path = typer.Option(Path("."), "--dir", "-d", help="Path to the existing project"),
524
534
  ) -> None:
535
+ import json
536
+ import shlex
537
+ import subprocess
538
+ from jinja2 import Environment, PackageLoader, StrictUndefined, TemplateNotFound
539
+ from fastapi_spawn.steps import FEATURE_ACTIONS
540
+
525
541
  _print_banner()
542
+
543
+ # Load tracking config
544
+ config_path = project_dir / ".fastapi-spawn.json"
545
+ project_status: dict = {}
546
+ if config_path.exists():
547
+ try:
548
+ project_status = json.loads(config_path.read_text(encoding="utf-8"))
549
+ except Exception:
550
+ console.print("[dim yellow]⚠ Could not parse .fastapi-spawn.json — starting fresh[/dim yellow]")
551
+
552
+ # Show feature table if no feature given
526
553
  if not feature:
527
- console.print("\n[bold cyan]Available features to add:[/bold cyan]\n")
528
- table = Table(box=None)
554
+ installed = set(project_status.get("installed_features", []))
555
+ table = Table(title="Available Features", box=None)
529
556
  table.add_column("Feature", style="bold green", justify="left")
530
557
  table.add_column("Description", style="white", justify="left")
531
-
558
+ table.add_column("Status", justify="center")
532
559
  for k, v in _ADDABLE_FEATURES.items():
533
- table.add_row(k, v)
534
-
560
+ status = "[bold green]✓ installed[/bold green]" if k in installed else "[dim]—[/dim]"
561
+ table.add_row(k, v, status)
535
562
  console.print(table)
536
- console.print("\n[dim]Run 'fastapi-spawn add <feature>' to add a feature.[/dim]\n")
563
+ console.print("\n[dim]Run: fastapi-spawn add <feature>[/dim]\n")
537
564
  raise typer.Exit(0)
538
565
 
539
566
  if feature not in _ADDABLE_FEATURES:
@@ -547,124 +574,134 @@ def add_feature(
547
574
  console.print(f"[bold red]✗ Directory not found:[/bold red] {project_dir.resolve()}")
548
575
  raise typer.Exit(1)
549
576
 
550
- if feature == "alembic":
551
- console.print("[bold cyan]→ Automatically adding Alembic...[/bold cyan]")
552
-
553
- # 1. Prompt for DB type
554
- from fastapi_spawn.constants import Database
555
- import questionary
556
-
557
- db_type = questionary.select(
558
- "Which database are you using?",
559
- choices=[Database.postgresql.value, Database.mysql.value, Database.sqlite.value],
560
- default=Database.postgresql.value
561
- ).ask()
562
-
563
- if not db_type:
564
- console.print("[bold red]✗ Database selection cancelled[/bold red]")
565
- raise typer.Exit(1)
566
-
567
- # 2. Run uv add alembic
568
- import subprocess
577
+ installed_features: list = project_status.get("installed_features", [])
578
+ if feature in installed_features:
579
+ console.print(f"[bold yellow]⚠ '{feature}' already installed in this project.[/bold yellow]")
580
+ raise typer.Exit(0)
581
+
582
+ action = FEATURE_ACTIONS.get(feature)
583
+ if not action:
584
+ console.print(f"[bold yellow]⚠ No automation defined for '{feature}'.[/bold yellow]")
585
+ raise typer.Exit(0)
586
+
587
+ console.print(Panel.fit(
588
+ f"[bold cyan]→ Adding:[/bold cyan] [bold]{feature}[/bold]\n[dim]{_ADDABLE_FEATURES[feature]}[/dim]",
589
+ border_style="cyan", padding=(0, 2),
590
+ ))
591
+
592
+ # [1/4] Install deps
593
+ deps = action.get("deps", [])
594
+ if deps:
595
+ console.print("\n[bold cyan][1/4] Installing dependencies...[/bold cyan]")
596
+ for d in deps:
597
+ console.print(f" [dim]uv add {d}[/dim]")
569
598
  try:
570
- console.print("[dim]Running: uv add alembic[/dim]")
571
- subprocess.run(["uv", "add", "alembic"], cwd=project_dir, check=True)
599
+ subprocess.run(["uv", "add"] + deps, cwd=project_dir, check=True)
600
+ console.print(" [bold green] Dependencies installed[/bold green]")
572
601
  except subprocess.CalledProcessError:
573
- console.print("[bold red]✗ Failed to add alembic dependency[/bold red]")
602
+ console.print(f" [bold red]✗ Failed to install deps[/bold red]")
574
603
  raise typer.Exit(1)
575
-
576
- # 3. Run alembic init
604
+ else:
605
+ console.print("\n[bold cyan][1/4] No extra dependencies needed.[/bold cyan]")
606
+
607
+ # [2/4] Generate files from templates
608
+ files = action.get("files", [])
609
+ if files:
610
+ console.print("\n[bold cyan][2/4] Generating files...[/bold cyan]")
611
+ jinja_env = Environment(
612
+ loader=PackageLoader("fastapi_spawn", "templates"),
613
+ undefined=StrictUndefined,
614
+ keep_trailing_newline=True,
615
+ trim_blocks=True,
616
+ lstrip_blocks=True,
617
+ )
618
+ ctx = {
619
+ "project_name": project_status.get("project_name", project_dir.name),
620
+ "db": project_status.get("db", "postgresql"),
621
+ "orm": project_status.get("orm", "sqlalchemy"),
622
+ "has_auth": feature == "auth" or "auth" in installed_features,
623
+ "has_alembic": feature == "alembic" or "alembic" in installed_features,
624
+ "has_broker": feature in ("celery", "arq") or any(f in installed_features for f in ("celery", "arq")),
625
+ "has_s3": feature in ("s3", "gcs", "cloudinary") or any(f in installed_features for f in ("s3", "gcs", "cloudinary")),
626
+ "has_ai": feature in ("openai", "anthropic", "gemini", "ollama", "langchain", "llamaindex"),
627
+ "ai_provider": feature if feature in ("openai", "anthropic", "gemini", "ollama", "langchain", "llamaindex") else project_status.get("ai", "none"),
628
+ "storage_provider": feature if feature in ("s3", "gcs", "cloudinary") else project_status.get("storage", "none"),
629
+ "email_provider": feature if feature in ("sendgrid", "smtp", "ses", "resend") else project_status.get("email", "none"),
630
+ "notify_provider": feature if feature in ("slack", "discord") else project_status.get("notify", "none"),
631
+ "vector_db": feature if feature in ("qdrant", "chroma", "pinecone", "elasticsearch") else project_status.get("vector_db", "none"),
632
+ "monitoring": feature if feature in ("sentry", "prometheus", "opentelemetry") else project_status.get("monitoring", "none"),
633
+ }
634
+ for template_path, dest_rel in files:
635
+ dest = project_dir / dest_rel
636
+ dest.parent.mkdir(parents=True, exist_ok=True)
637
+ try:
638
+ tmpl = jinja_env.get_template(template_path)
639
+ dest.write_text(tmpl.render(**ctx), encoding="utf-8")
640
+ console.print(f" [bold green]✓[/bold green] {dest_rel}")
641
+ except TemplateNotFound:
642
+ console.print(f" [dim yellow]⚠ No template for {dest_rel} — skipping[/dim yellow]")
643
+ except Exception as exc:
644
+ console.print(f" [bold red]✗ {dest_rel}: {exc}[/bold red]")
645
+ else:
646
+ console.print("\n[bold cyan][2/4] No files to generate.[/bold cyan]")
647
+
648
+ # [3/4] Append env vars to .env
649
+ env_vars = action.get("env_vars", [])
650
+ if env_vars:
651
+ console.print("\n[bold cyan][3/4] Updating .env...[/bold cyan]")
652
+ env_file = project_dir / ".env"
653
+ existing_env = env_file.read_text(encoding="utf-8") if env_file.exists() else ""
654
+ new_lines = [v for v in env_vars if v.split("=")[0].strip() not in existing_env]
655
+ if new_lines:
656
+ with env_file.open("a", encoding="utf-8") as f:
657
+ f.write(f"\n# Added by fastapi-spawn add {feature}\n")
658
+ for line in new_lines:
659
+ f.write(line + "\n")
660
+ console.print(f" [bold green]✓[/bold green] Added {len(new_lines)} env var(s)")
661
+ else:
662
+ console.print(" [dim]All env vars already in .env[/dim]")
663
+ else:
664
+ console.print("\n[bold cyan][3/4] No env vars needed.[/bold cyan]")
665
+
666
+ # [4/4] Run init commands / special handling
667
+ if action.get("_special") == "alembic":
668
+ console.print("\n[bold cyan][4/4] Initializing Alembic...[/bold cyan]")
577
669
  try:
578
- console.print("[dim]Running: uv run alembic init migrations[/dim]")
579
670
  subprocess.run(["uv", "run", "alembic", "init", "migrations"], cwd=project_dir, check=True)
671
+ console.print(" [bold green]✓ alembic init migrations[/bold green]")
672
+ db_val = project_status.get("db", "postgresql")
673
+ cfg = ProjectConfig(project_name=project_dir.name, db=Database(db_val), orm=ORM.sqlalchemy, migration=MigrationTool.alembic)
674
+ gen = ProjectGenerator(cfg, project_dir)
675
+ gen._render_to(project_dir / "migrations" / "env.py", "alembic/env.py.j2")
676
+ console.print(" [bold green]✓ Rendered async env.py[/bold green]")
580
677
  except subprocess.CalledProcessError:
581
- console.print("[bold red]✗ Failed to initialize alembic[/bold red]")
582
- raise typer.Exit(1)
583
-
584
- # 4. Render env.py
585
- console.print("[dim]Rendering async env.py...[/dim]")
586
- from fastapi_spawn.generator import ProjectGenerator
587
- from fastapi_spawn.config import ProjectConfig
588
-
589
- cfg = ProjectConfig(project_name=project_dir.name, db=Database(db_type), orm=ORM.sqlalchemy)
590
- cfg.has_alembic = True
591
- generator = ProjectGenerator(cfg, project_dir)
592
-
593
- generator._render_to(project_dir / "migrations" / "env.py", "alembic/env.py.j2")
594
-
595
- console.print("[bold green]✓ Alembic added successfully![/bold green]")
596
- console.print("\n[bold yellow]Next Steps:[/bold yellow]")
597
- console.print(" 1. Update [bold]alembic.ini[/bold] with your database URL.")
598
- console.print(" 2. Run [bold]uv run alembic revision --autogenerate -m 'initial'[/bold]")
599
- console.print(" 3. Run [bold]uv run alembic upgrade head[/bold]\n")
600
- raise typer.Exit(0)
678
+ console.print(" [bold red]✗ alembic init failed[/bold red]")
679
+ elif action.get("run_cmds"):
680
+ console.print("\n[bold cyan][4/4] Running commands...[/bold cyan]")
681
+ for cmd_str in action["run_cmds"]:
682
+ console.print(f" [dim]{cmd_str}[/dim]")
683
+ try:
684
+ subprocess.run(shlex.split(cmd_str), cwd=project_dir, check=True)
685
+ console.print(f" [bold green]✓[/bold green] done")
686
+ except subprocess.CalledProcessError:
687
+ console.print(f" [bold red]✗ Failed: {cmd_str}[/bold red]")
688
+ else:
689
+ console.print("\n[bold cyan][4/4] No init commands needed.[/bold cyan]")
601
690
 
602
- console.print(f"[bold cyan]→ Adding feature:[/bold cyan] [bold]{feature}[/bold] — {_ADDABLE_FEATURES[feature]}")
603
- console.print(f"[dim]Target project:[/dim] {project_dir.resolve()}\n")
604
- _feature_guidance(feature, project_dir)
605
-
606
-
607
- def _feature_guidance(feature: str, _project_dir: Path) -> None:
608
- """Print actionable steps for each addable feature."""
609
- _STEPS: dict[str, list[str]] = {
610
- "auth": ["Add deps: python-jose[cryptography], passlib[bcrypt], python-multipart", "Add app/core/security.py (JWT helpers)", "Add auth endpoints: app/api/v1/auth.py", "Add to .env: SECRET_KEY, ACCESS_TOKEN_EXPIRE_MINUTES=30, ALGORITHM=HS256"],
611
- "s3": ["Add dep: boto3>=1.34.0", "Create app/core/storage.py (upload, presigned_url, delete)", "Add to .env: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_S3_BUCKET, AWS_S3_ENDPOINT_URL (MinIO: http://localhost:9000)"],
612
- "gcs": ["Add dep: google-cloud-storage>=2.16.0", "Create app/core/storage.py (GCS client)", "Add to .env: GCS_PROJECT_ID, GCS_BUCKET, GOOGLE_APPLICATION_CREDENTIALS=./service-account.json"],
613
- "cloudinary":["Add dep: cloudinary>=1.40.0", "Create app/core/storage.py (Cloudinary upload/CDN)", "Add to .env: CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET"],
614
- "openai": ["Add dep: openai>=1.30.0", "Create app/core/ai.py (chat_completion, get_embedding)", "Add to .env: OPENAI_API_KEY, OPENAI_MODEL=gpt-4o, OPENAI_EMBEDDING_MODEL, OPENAI_BASE_URL="],
615
- "anthropic": ["Add dep: anthropic>=0.28.0", "Create app/core/ai.py (chat_completion)", "Add to .env: ANTHROPIC_API_KEY, ANTHROPIC_MODEL=claude-3-5-sonnet-20241022"],
616
- "gemini": ["Add dep: google-generativeai>=0.7.0", "Create app/core/ai.py (chat_completion, get_embedding)", "Add to .env: GEMINI_API_KEY, GEMINI_MODEL=gemini-1.5-pro"],
617
- "ollama": ["No API key needed — run: docker run -p 11434:11434 ollama/ollama", "Create app/core/ai.py (chat via httpx)", "Add to .env: OLLAMA_HOST=localhost, OLLAMA_PORT=11434, OLLAMA_MODEL=llama3"],
618
- "langchain": ["Add deps: langchain>=0.2.0, langchain-openai>=0.1.0", "Create app/core/ai.py (LangChain ChatOpenAI + embeddings)", "Add to .env: OPENAI_API_KEY, OPENAI_MODEL, OPENAI_BASE_URL"],
619
- "llamaindex":["Add deps: llama-index>=0.10.0, llama-index-llms-openai, llama-index-embeddings-openai", "Create app/core/ai.py (LlamaIndex VectorStoreIndex)", "Add to .env: OPENAI_API_KEY, OPENAI_MODEL, OPENAI_EMBEDDING_MODEL"],
620
- "alembic": ["Add dep: alembic>=1.13.0", "Run: alembic init migrations", "Replace migrations/env.py with async-compatible version", "Add to [tool.uv.scripts]: migrate = 'alembic upgrade head'", "Run: uv run migrate"],
621
- "celery": ["Add dep: celery[redis]>=5.3.6", "Create tasks/celery_app.py + tasks/sample_tasks.py", "Add to .env: REDIS_HOST, REDIS_PORT, REDIS_DB", "Add to [tool.uv.scripts]: worker = 'celery -A tasks.celery_app worker --loglevel=info'"],
622
- "arq": ["Add deps: arq>=0.25.0, redis[hiredis]>=5.0.0", "Create tasks/arq_worker.py (WorkerSettings, task defs)", "Run with: arq tasks.arq_worker.WorkerSettings"],
623
- "redis": ["Add dep: redis[hiredis]>=5.0.0", "Add to .env: REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, REDIS_DB", "Add redis_url @property to Settings in app/core/config.py"],
624
- "kafka": ["Add dep: aiokafka>=0.10.0", "Add to .env: KAFKA_HOST=localhost, KAFKA_PORT=9092"],
625
- "sentry": ["Add dep: sentry-sdk[fastapi]>=2.0.0", "Create app/core/monitoring.py (init_sentry)", "Add to .env: SENTRY_DSN=https://xxx@sentry.io/yyy", "Call init_sentry() in app/main.py on startup"],
626
- "prometheus":["Add dep: prometheus-fastapi-instrumentator>=7.0.0", "Create app/core/monitoring.py (init_prometheus)", "Call init_prometheus(app) in app/main.py — exposes /metrics"],
627
- "opentelemetry": ["Add dep: opentelemetry-sdk, opentelemetry-instrumentation-fastapi", "Set OTEL_EXPORTER_OTLP_ENDPOINT in .env or compose"],
628
- "sendgrid": ["Add dep: sendgrid>=6.11.0", "Create app/core/email.py", "Add to .env: SENDGRID_API_KEY, SENDGRID_FROM_EMAIL"],
629
- "smtp": ["Add dep: fastapi-mail>=1.4.1", "Create app/core/email.py", "Add to .env: SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM_EMAIL"],
630
- "ses": ["Add dep: boto3>=1.34.0", "Create app/core/email.py", "Add to .env: AWS_* credentials, SES_FROM_EMAIL"],
631
- "resend": ["Add dep: resend>=2.1.0", "Create app/core/email.py (Resend client)", "Add to .env: RESEND_API_KEY"],
632
- "slack": ["No extra dep (uses httpx)", "Create app/core/notifications.py", "Add to .env: SLACK_WEBHOOK_URL=https://hooks.slack.com/services/..."],
633
- "discord": ["No extra dep (uses httpx)", "Create app/core/notifications.py", "Add to .env: DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/..."],
634
- "qdrant": ["Add dep: qdrant-client[fastembed]>=1.9.0", "Create app/core/vector_db.py", "Add to .env: QDRANT_HOST=localhost, QDRANT_PORT=6333, QDRANT_API_KEY= (blank for local)"],
635
- "chroma": ["Add dep: chromadb>=0.5.0", "Create app/core/vector_db.py (persistent local client)", "No env vars needed — data stored in ./chroma_data"],
636
- "pinecone": ["Add dep: pinecone-client>=3.2.0", "Create app/core/vector_db.py", "Add to .env: PINECONE_API_KEY, PINECONE_INDEX_NAME"],
637
- "elasticsearch": ["Add dep: elasticsearch[async]>=8.13.0", "Create app/core/vector_db.py (kNN search)", "Add to .env: ELASTICSEARCH_HOST, ELASTICSEARCH_PORT, ELASTICSEARCH_API_KEY"],
638
- "meilisearch": ["Add dep: meilisearch>=0.30.0", "Create app/core/search.py (Meilisearch client)", "Add to .env: MEILISEARCH_HOST=http://localhost:7700, MEILISEARCH_API_KEY"],
639
- "opensearch": ["Add dep: opensearch-py[async]>=2.5.0", "Create app/core/search.py (OpenSearch client)", "Add to .env: OPENSEARCH_HOST, OPENSEARCH_PORT, OPENSEARCH_USER, OPENSEARCH_PASSWORD"],
640
- "vespa": ["Add dep: pyvespa>=0.40.0", "Create app/core/search.py (Vespa client)", "Add to .env: VESPA_ENDPOINT"],
641
- "websockets":["No extra dep (built into FastAPI)", "Create app/core/ws_manager.py (ConnectionManager)", "Create app/api/v1/ws/router.py — /ws/connect, /ws/connect/{room_id}"],
642
- "sse": ["Add dep: sse-starlette>=2.1.0", "Create app/api/v1/streaming/router.py", "Return EventSourceResponse(async_generator)"],
643
- "graphql": ["Add dep: strawberry-graphql[fastapi]>=0.227.0", "Create app/api/graphql.py (Query + Mutation + Subscription)", "Mount: app.include_router(graphql_router, prefix='/graphql')"],
644
- "stripe": ["Add dep: stripe>=9.0.0", "Create app/api/v1/payments/router.py (webhook endpoint)", "Add to .env: STRIPE_API_KEY, STRIPE_WEBHOOK_SECRET"],
645
- "sso": ["Add dep: fastapi-sso>=0.14.0", "Create app/api/v1/auth/sso.py (Google/Github/Microsoft SSO)", "Add to .env: GOOGLE_CLIENT_ID, GITHUB_CLIENT_ID, etc."],
646
- "sso-google": ["1. Use 'fastapi-spawn new temp_app --extra sso-google' and copy the resulting sso.py"],
647
- "sso-github": ["1. Use 'fastapi-spawn new temp_app --extra sso-github' and copy the resulting sso.py"],
648
- "sso-microsoft": ["1. Use 'fastapi-spawn new temp_app --extra sso-microsoft' and copy the resulting sso.py"],
649
- "seed": ["Add dep: faker>=25.0.0", "Create db/seed.py (generate 100 mock users/posts)", "Run: uv run python db/seed.py"],
650
- "ocr": ["Add deps: pymupdf>=1.24.0, pytesseract>=0.3.10", "Create app/core/ocr.py (PDF parsing pipeline)", "Install system deps: sudo apt install tesseract-ocr"],
651
- "rbac": ["Create app/core/permissions.py and app/api/v1/permissions/router.py", "Add app.include_router(permissions.router) to main.py"],
652
- "caching": ["Add dep: fastapi-cache2[redis]>=0.2.1", "Create app/core/cache.py", "Initialize cache in lifespan and use @cache(expire=60) on endpoints"],
653
- "response-format": ["Create app/middleware/response_format.py", "Add app.add_middleware(ResponseFormattingMiddleware) to main.py"],
654
- "admin": ["Add dep: sqladmin[full]>=0.16.1", "Create app/admin/setup.py", "Call setup_admin(app, engine) in main.py"],
655
- "pagination":["Add dep: fastapi-pagination>=0.12.0", "Create app/api/v1/pagination/router.py", "Call add_pagination(app) in main.py"],
656
- "uploads": ["Create app/api/v1/uploads/router.py", "Include router in app/api/v1/router.py"],
657
- "docker": ["Create Dockerfile (multi-stage, uv-based)", "Create docker-compose.yml with all selected services", "Create .dockerignore"],
658
- "ci": ["Create .github/workflows/tests.yml (matrix: 3.10, 3.11, 3.12)", "Create .github/workflows/publish.yml (v* tags → PyPI)", "Add PYPI_API_TOKEN to GitHub repo secrets"],
659
- "helm": ["Create infra/helm/Chart.yaml", "Create infra/helm/values.yaml (replicas, image, resources)", "Run: helm install my-release ./infra/helm"],
660
- "terraform": ["Create infra/terraform/main.tf (AWS ECR + ECS)", "Create infra/terraform/variables.tf", "Run: terraform -chdir=infra/terraform init && terraform apply"],
661
- }
662
-
663
- steps = _STEPS.get(feature, [])
664
- if steps:
665
- content = "\n".join(f" [dim]{i+1}.[/dim] {s}" for i, s in enumerate(steps))
666
- console.print(Panel(content, title=f"[bold cyan]Steps to add '{feature}'[/bold cyan]", border_style="cyan", padding=(0, 1)))
667
- console.print(f"\n[dim]Preview files:[/dim] [bold cyan]fastapi-spawn new <name> --{feature} --dry-run[/bold cyan]")
691
+ # Update tracking file
692
+ installed_features.append(feature)
693
+ project_status["installed_features"] = installed_features
694
+ try:
695
+ config_path.write_text(json.dumps(project_status, indent=2), encoding="utf-8")
696
+ console.print("\n[dim]ℹ .fastapi-spawn.json updated[/dim]")
697
+ except Exception:
698
+ pass
699
+
700
+ note = action.get("note")
701
+ msg = f"[bold green]✓ {feature.capitalize()} added successfully![/bold green]"
702
+ if note:
703
+ msg += f"\n[dim]{note}[/dim]"
704
+ console.print(Panel(msg, border_style="green", padding=(0, 2)))
668
705
 
669
706
 
670
707
  # ── Helpers ────────────────────────────────────────────────────────────────────