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
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
apiVersion: apps/v1
|
|
2
|
+
kind: Deployment
|
|
3
|
+
metadata:
|
|
4
|
+
name: {{ slug }}
|
|
5
|
+
labels:
|
|
6
|
+
app: {{ slug }}
|
|
7
|
+
spec:
|
|
8
|
+
replicas: 2
|
|
9
|
+
selector:
|
|
10
|
+
matchLabels:
|
|
11
|
+
app: {{ slug }}
|
|
12
|
+
template:
|
|
13
|
+
metadata:
|
|
14
|
+
labels:
|
|
15
|
+
app: {{ slug }}
|
|
16
|
+
spec:
|
|
17
|
+
containers:
|
|
18
|
+
- name: {{ slug }}
|
|
19
|
+
image: ghcr.io/your-org/{{ slug }}:latest
|
|
20
|
+
ports:
|
|
21
|
+
- containerPort: 8000
|
|
22
|
+
envFrom:
|
|
23
|
+
- secretRef:
|
|
24
|
+
name: {{ slug }}-secrets
|
|
25
|
+
livenessProbe:
|
|
26
|
+
httpGet:
|
|
27
|
+
path: /api/v1/health/liveness
|
|
28
|
+
port: 8000
|
|
29
|
+
initialDelaySeconds: 15
|
|
30
|
+
periodSeconds: 20
|
|
31
|
+
readinessProbe:
|
|
32
|
+
httpGet:
|
|
33
|
+
path: /api/v1/health/readiness
|
|
34
|
+
port: 8000
|
|
35
|
+
initialDelaySeconds: 5
|
|
36
|
+
periodSeconds: 10
|
|
37
|
+
resources:
|
|
38
|
+
requests:
|
|
39
|
+
memory: "128Mi"
|
|
40
|
+
cpu: "100m"
|
|
41
|
+
limits:
|
|
42
|
+
memory: "512Mi"
|
|
43
|
+
cpu: "500m"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
replicaCount: 2
|
|
2
|
+
|
|
3
|
+
image:
|
|
4
|
+
repository: ghcr.io/your-org/{{ slug }}
|
|
5
|
+
tag: latest
|
|
6
|
+
pullPolicy: IfNotPresent
|
|
7
|
+
|
|
8
|
+
service:
|
|
9
|
+
type: ClusterIP
|
|
10
|
+
port: 8000
|
|
11
|
+
|
|
12
|
+
ingress:
|
|
13
|
+
enabled: false
|
|
14
|
+
host: {{ slug }}.example.com
|
|
15
|
+
|
|
16
|
+
env:
|
|
17
|
+
APP_ENV: production
|
|
18
|
+
DEBUG: "false"
|
|
19
|
+
|
|
20
|
+
resources:
|
|
21
|
+
requests:
|
|
22
|
+
memory: "128Mi"
|
|
23
|
+
cpu: "100m"
|
|
24
|
+
limits:
|
|
25
|
+
memory: "512Mi"
|
|
26
|
+
cpu: "500m"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
terraform {
|
|
2
|
+
required_providers {
|
|
3
|
+
aws = {
|
|
4
|
+
source = "hashicorp/aws"
|
|
5
|
+
version = "~> 5.0"
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
provider "aws" {
|
|
11
|
+
region = var.aws_region
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# ECR repository
|
|
15
|
+
resource "aws_ecr_repository" "app" {
|
|
16
|
+
name = "{{ slug }}"
|
|
17
|
+
image_tag_mutability = "MUTABLE"
|
|
18
|
+
image_scanning_configuration {
|
|
19
|
+
scan_on_push = true
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# ECS Cluster
|
|
24
|
+
resource "aws_ecs_cluster" "main" {
|
|
25
|
+
name = "{{ slug }}-cluster"
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
variable "aws_region" {
|
|
2
|
+
description = "AWS region to deploy to"
|
|
3
|
+
type = string
|
|
4
|
+
default = "us-east-1"
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
variable "app_name" {
|
|
8
|
+
description = "Application name"
|
|
9
|
+
type = string
|
|
10
|
+
default = "{{ slug }}"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
variable "container_port" {
|
|
14
|
+
description = "Port the container listens on"
|
|
15
|
+
type = number
|
|
16
|
+
default = 8000
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Entry point for {{ project_name }}.
|
|
2
|
+
|
|
3
|
+
Run with: uv run main.py
|
|
4
|
+
Or: uvicorn app.main:app --reload
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import uvicorn
|
|
8
|
+
|
|
9
|
+
if __name__ == "__main__":
|
|
10
|
+
uvicorn.run(
|
|
11
|
+
"app.main:app",
|
|
12
|
+
host="0.0.0.0",
|
|
13
|
+
port=8000,
|
|
14
|
+
reload=True,
|
|
15
|
+
log_level="info",
|
|
16
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Celery application factory for {{ project_name }}."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from celery import Celery
|
|
6
|
+
|
|
7
|
+
from app.core.config import settings
|
|
8
|
+
|
|
9
|
+
{% if broker == "redis" %}
|
|
10
|
+
celery_app = Celery(
|
|
11
|
+
"{{ slug }}",
|
|
12
|
+
broker=settings.REDIS_URL,
|
|
13
|
+
backend=settings.REDIS_URL,
|
|
14
|
+
)
|
|
15
|
+
{% elif broker == "rabbitmq" %}
|
|
16
|
+
celery_app = Celery(
|
|
17
|
+
"{{ slug }}",
|
|
18
|
+
broker=settings.RABBITMQ_URL,
|
|
19
|
+
backend="rpc://",
|
|
20
|
+
)
|
|
21
|
+
{% else %}
|
|
22
|
+
celery_app = Celery("{{ slug }}")
|
|
23
|
+
{% endif %}
|
|
24
|
+
|
|
25
|
+
celery_app.conf.update(
|
|
26
|
+
task_serializer="json",
|
|
27
|
+
result_serializer="json",
|
|
28
|
+
accept_content=["json"],
|
|
29
|
+
timezone="UTC",
|
|
30
|
+
enable_utc=True,
|
|
31
|
+
task_track_started=True,
|
|
32
|
+
worker_prefetch_multiplier=1,
|
|
33
|
+
task_acks_late=True,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Auto-discover tasks in the tasks/ package
|
|
37
|
+
celery_app.autodiscover_tasks(["tasks"])
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Sample Celery tasks for {{ project_name }}."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from tasks.celery_app import celery_app
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@celery_app.task(bind=True, name="tasks.sample.add", max_retries=3)
|
|
11
|
+
def add(self, x: int, y: int) -> int:
|
|
12
|
+
"""Simple addition task — use as a template."""
|
|
13
|
+
return x + y
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@celery_app.task(bind=True, name="tasks.sample.slow_task", max_retries=3)
|
|
17
|
+
def slow_task(self, seconds: int = 5) -> str:
|
|
18
|
+
"""Simulate a long-running task."""
|
|
19
|
+
time.sleep(seconds)
|
|
20
|
+
return f"Completed after {seconds}s"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@celery_app.task(bind=True, name="tasks.sample.send_email", max_retries=3)
|
|
24
|
+
def send_email(self, to: str, subject: str, body: str) -> dict:
|
|
25
|
+
"""Stub email sending task. Replace with real email logic."""
|
|
26
|
+
# TODO: integrate with SendGrid / SES / SMTP
|
|
27
|
+
return {"status": "sent", "to": to, "subject": subject}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""pytest configuration and fixtures for {{ project_name }}."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from httpx import ASGITransport, AsyncClient
|
|
7
|
+
|
|
8
|
+
from app.main import app
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture(scope="session")
|
|
12
|
+
def anyio_backend() -> str:
|
|
13
|
+
return "asyncio"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
async def client() -> AsyncClient: # type: ignore[return]
|
|
18
|
+
async with AsyncClient(
|
|
19
|
+
transport=ASGITransport(app=app),
|
|
20
|
+
base_url="http://testserver",
|
|
21
|
+
) as ac:
|
|
22
|
+
yield ac
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Health endpoint tests for {{ project_name }}."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from httpx import AsyncClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.anyio
|
|
10
|
+
async def test_health_ok(client: AsyncClient) -> None:
|
|
11
|
+
response = await client.get("/api/v1/health")
|
|
12
|
+
assert response.status_code == 200
|
|
13
|
+
data = response.json()
|
|
14
|
+
assert data["status"] == "ok"
|
|
15
|
+
assert "uptime_seconds" in data
|
|
16
|
+
assert data["service"] == "{{ project_name }}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.anyio
|
|
20
|
+
async def test_readiness(client: AsyncClient) -> None:
|
|
21
|
+
response = await client.get("/api/v1/health/readiness")
|
|
22
|
+
assert response.status_code == 200
|
|
23
|
+
assert response.json() == {"status": "ready"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.anyio
|
|
27
|
+
async def test_liveness(client: AsyncClient) -> None:
|
|
28
|
+
response = await client.get("/api/v1/health/liveness")
|
|
29
|
+
assert response.status_code == 200
|
|
30
|
+
assert response.json() == {"status": "alive"}
|
fastapi_spawn/utils.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Utility helpers for fastapi-spawn."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def to_snake_case(name: str) -> str:
|
|
10
|
+
"""Convert a string to snake_case."""
|
|
11
|
+
s = re.sub(r"[-\s]+", "_", name)
|
|
12
|
+
s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s)
|
|
13
|
+
s = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", s)
|
|
14
|
+
return s.lower()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def to_pascal_case(name: str) -> str:
|
|
18
|
+
"""Convert a snake_case or kebab-case string to PascalCase."""
|
|
19
|
+
return "".join(word.capitalize() for word in re.split(r"[-_\s]+", name))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def to_kebab_case(name: str) -> str:
|
|
23
|
+
"""Convert a string to kebab-case."""
|
|
24
|
+
s = re.sub(r"[_\s]+", "-", name)
|
|
25
|
+
s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1-\2", s)
|
|
26
|
+
s = re.sub(r"([a-z\d])([A-Z])", r"\1-\2", s)
|
|
27
|
+
return s.lower()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def render_tree(root: Path, prefix: str = "") -> str:
|
|
31
|
+
"""
|
|
32
|
+
Recursively render a directory tree as a Rich-compatible string.
|
|
33
|
+
Used in --dry-run mode.
|
|
34
|
+
"""
|
|
35
|
+
lines: list[str] = []
|
|
36
|
+
try:
|
|
37
|
+
items = sorted(root.iterdir(), key=lambda p: (p.is_file(), p.name))
|
|
38
|
+
except PermissionError:
|
|
39
|
+
return ""
|
|
40
|
+
for i, item in enumerate(items):
|
|
41
|
+
connector = "└── " if i == len(items) - 1 else "├── "
|
|
42
|
+
lines.append(prefix + connector + item.name)
|
|
43
|
+
if item.is_dir():
|
|
44
|
+
extension = " " if i == len(items) - 1 else "│ "
|
|
45
|
+
lines.append(render_tree(item, prefix + extension))
|
|
46
|
+
return "\n".join(filter(None, lines))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def collect_dry_run_paths(root: Path) -> list[str]:
|
|
50
|
+
"""
|
|
51
|
+
Walk a directory and return all relative file paths as strings.
|
|
52
|
+
Used by the generator in dry-run mode.
|
|
53
|
+
"""
|
|
54
|
+
paths = []
|
|
55
|
+
for path in sorted(root.rglob("*")):
|
|
56
|
+
if path.is_file():
|
|
57
|
+
paths.append(str(path.relative_to(root)))
|
|
58
|
+
return paths
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Input validators for fastapi-spawn."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from fastapi_spawn.constants import ORM, ORM_DB_COMPAT, Database
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def validate_project_name(name: str) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Validate that a project name is a valid Python package identifier
|
|
14
|
+
(allows hyphens which are converted to underscores).
|
|
15
|
+
Returns the cleaned name or raises ValueError.
|
|
16
|
+
"""
|
|
17
|
+
cleaned = name.strip()
|
|
18
|
+
if not cleaned:
|
|
19
|
+
raise ValueError("Project name cannot be empty.")
|
|
20
|
+
if not re.match(r"^[a-zA-Z][a-zA-Z0-9_-]*$", cleaned):
|
|
21
|
+
raise ValueError(
|
|
22
|
+
f"Invalid project name '{cleaned}'. "
|
|
23
|
+
"Use letters, numbers, hyphens, or underscores. Must start with a letter."
|
|
24
|
+
)
|
|
25
|
+
if len(cleaned) > 64:
|
|
26
|
+
raise ValueError("Project name must be 64 characters or fewer.")
|
|
27
|
+
return cleaned
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_orm_db_compat(orm: ORM, db: Database) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Raise ValueError if the selected ORM is not compatible with the selected DB.
|
|
33
|
+
"""
|
|
34
|
+
compatible_dbs = ORM_DB_COMPAT.get(orm, [])
|
|
35
|
+
if db not in compatible_dbs:
|
|
36
|
+
compat_str = ", ".join(d.value for d in compatible_dbs)
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"ORM '{orm.value}' is not compatible with database '{db.value}'. "
|
|
39
|
+
f"Compatible databases: {compat_str}"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def validate_output_dir(path: Path, force: bool) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Validate that the output directory can be safely created.
|
|
46
|
+
Raises ValueError if the directory exists and --force was not passed.
|
|
47
|
+
"""
|
|
48
|
+
if path.exists() and not force:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"Directory '{path}' already exists. Use --force to overwrite."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def questionary_validator(validate_fn): # type: ignore[no-untyped-def]
|
|
55
|
+
"""
|
|
56
|
+
Wrap a validation function for use as a questionary validator.
|
|
57
|
+
Returns a callable that returns True on success or an error string on failure.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def _inner(val: str): # type: ignore[no-untyped-def]
|
|
61
|
+
try:
|
|
62
|
+
validate_fn(val)
|
|
63
|
+
return True
|
|
64
|
+
except ValueError as exc:
|
|
65
|
+
return str(exc)
|
|
66
|
+
|
|
67
|
+
return _inner
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-spawn
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A powerful CLI tool to scaffold production-ready FastAPI projects with flexible database, auth, broker, and deployment options.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Bishwajitgarai/fastapi-spawn
|
|
6
|
+
Project-URL: Documentation, https://github.com/Bishwajitgarai/fastapi-spawn#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/Bishwajitgarai/fastapi-spawn
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/Bishwajitgarai/fastapi-spawn/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/Bishwajitgarai/fastapi-spawn/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Bishwajit Garai <bishwajitgarai@gmail.com>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: boilerplate,cli,devtools,docker,fastapi,generator,jwt,project-generator,python,scaffold,sqlalchemy,template
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Topic :: Utilities
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: click>=8.1.0
|
|
29
|
+
Requires-Dist: jinja2>=3.1.4
|
|
30
|
+
Requires-Dist: questionary>=2.0.1
|
|
31
|
+
Requires-Dist: rich>=13.7.0
|
|
32
|
+
Requires-Dist: tomli>=2.0.1; python_version < '3.11'
|
|
33
|
+
Requires-Dist: typer>=0.12.0
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: mypy>=1.10.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pre-commit>=3.7.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
<div align="center">
|
|
43
|
+
|
|
44
|
+
# ⚡ fastapi-spawn
|
|
45
|
+
|
|
46
|
+
**The most complete FastAPI project scaffolding CLI — built for modern Python development.**
|
|
47
|
+
|
|
48
|
+
[](https://pypi.org/project/fastapi-spawn/)
|
|
49
|
+
[](https://pypi.org/project/fastapi-spawn/)
|
|
50
|
+
[](LICENSE)
|
|
51
|
+
[](https://github.com/Bishwajitgarai/fastapi-spawn/actions)
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Why fastapi-spawn?
|
|
58
|
+
|
|
59
|
+
`fastapi-spawn` generates **production-ready** FastAPI projects in seconds. No boilerplate, no guesswork — just run one command and get a fully structured, configured project with the exact stack you need.
|
|
60
|
+
|
|
61
|
+
| Feature | scaffold-fastapi | **fastapi-spawn** |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| Interactive TUI | Basic prompts | ✅ Rich questionary TUI |
|
|
64
|
+
| Databases | PostgreSQL, MongoDB, SQLite | ✅ + MySQL |
|
|
65
|
+
| ORM | None | ✅ SQLAlchemy 2.x, Tortoise, Beanie |
|
|
66
|
+
| Migrations | ❌ | ✅ Alembic (async), Aerich |
|
|
67
|
+
| Auth | ❌ | ✅ JWT, OAuth2, API Key |
|
|
68
|
+
| Message brokers | Redis, RabbitMQ | ✅ + Kafka |
|
|
69
|
+
| File storage | ❌ | ✅ AWS S3 (boto3) |
|
|
70
|
+
| AI integration | ❌ | ✅ OpenAI, Anthropic |
|
|
71
|
+
| Config style | Single URL | ✅ Individual fields + `@property` URL |
|
|
72
|
+
| Entry point | app/main.py | ✅ Root `main.py` (`uv run main.py`) |
|
|
73
|
+
| CI/CD | GitHub Actions | ✅ GitHub Actions + GitLab CI |
|
|
74
|
+
| Observability | ❌ | ✅ /health /readiness /liveness |
|
|
75
|
+
| Logging | None | ✅ loguru / structlog / standard |
|
|
76
|
+
| Package manager | uv | ✅ uv (with `[tool.uv]` config) |
|
|
77
|
+
| Dry-run mode | ❌ | ✅ Preview tree before generating |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install fastapi-spawn
|
|
85
|
+
# or
|
|
86
|
+
uv pip install fastapi-spawn
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Quick Start
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Fully interactive — guided TUI
|
|
95
|
+
fastapi-spawn new my-api
|
|
96
|
+
|
|
97
|
+
# One-liner (all flags)
|
|
98
|
+
fastapi-spawn new my-api \
|
|
99
|
+
--db postgresql \
|
|
100
|
+
--orm sqlalchemy \
|
|
101
|
+
--migration alembic \
|
|
102
|
+
--auth jwt \
|
|
103
|
+
--broker redis \
|
|
104
|
+
--storage s3 \
|
|
105
|
+
--ai openai \
|
|
106
|
+
--stack full \
|
|
107
|
+
--ci github \
|
|
108
|
+
--log-lib loguru
|
|
109
|
+
|
|
110
|
+
# Preview without writing files
|
|
111
|
+
fastapi-spawn new my-api --dry-run
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Generated Project Structure
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
my-api/
|
|
120
|
+
├── app/
|
|
121
|
+
│ ├── api/
|
|
122
|
+
│ │ └── v1/
|
|
123
|
+
│ │ ├── health.py # /health /readiness /liveness
|
|
124
|
+
│ │ └── auth.py # JWT login + refresh
|
|
125
|
+
│ ├── core/
|
|
126
|
+
│ │ ├── config.py # Pydantic Settings v2 (individual env fields)
|
|
127
|
+
│ │ ├── logging.py # loguru / structlog / standard
|
|
128
|
+
│ │ ├── exceptions.py # Custom exception hierarchy
|
|
129
|
+
│ │ ├── security.py # JWT + bcrypt (when auth enabled)
|
|
130
|
+
│ │ ├── storage.py # AWS S3 utils (when s3 chosen)
|
|
131
|
+
│ │ └── ai.py # OpenAI / Anthropic client (when AI chosen)
|
|
132
|
+
│ ├── db/
|
|
133
|
+
│ │ └── session.py # Async SQLAlchemy / Tortoise / Beanie
|
|
134
|
+
│ ├── models/ # ORM models
|
|
135
|
+
│ ├── schemas/ # Pydantic schemas
|
|
136
|
+
│ ├── services/ # Business logic layer
|
|
137
|
+
│ └── repositories/ # Data access layer
|
|
138
|
+
├── tasks/ # Celery workers (root-level)
|
|
139
|
+
│ ├── celery_app.py
|
|
140
|
+
│ └── sample_tasks.py
|
|
141
|
+
├── migrations/ # Alembic migrations
|
|
142
|
+
│ ├── env.py # Async-compatible env
|
|
143
|
+
│ └── versions/
|
|
144
|
+
├── infra/
|
|
145
|
+
│ ├── docker/
|
|
146
|
+
│ ├── helm/
|
|
147
|
+
│ └── terraform/
|
|
148
|
+
├── tests/
|
|
149
|
+
│ ├── conftest.py
|
|
150
|
+
│ └── test_health.py
|
|
151
|
+
├── main.py # uv run main.py entry point
|
|
152
|
+
├── alembic.ini
|
|
153
|
+
├── Dockerfile # Uses uv for fast builds
|
|
154
|
+
├── docker-compose.yml # All services pre-configured
|
|
155
|
+
├── .env # gitignored
|
|
156
|
+
├── .env.example
|
|
157
|
+
├── .gitignore
|
|
158
|
+
├── .pre-commit-config.yaml
|
|
159
|
+
├── Makefile
|
|
160
|
+
└── pyproject.toml # uv-compatible
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Environment Variables
|
|
166
|
+
|
|
167
|
+
`fastapi-spawn` generates **individual env fields** (not URL strings) for every service, assembled into URLs via `@property`:
|
|
168
|
+
|
|
169
|
+
```env
|
|
170
|
+
# PostgreSQL — assembled into DATABASE_URL by settings
|
|
171
|
+
POSTGRES_USER=postgres
|
|
172
|
+
POSTGRES_PASSWORD=postgres
|
|
173
|
+
POSTGRES_HOST=localhost
|
|
174
|
+
POSTGRES_PORT=5432
|
|
175
|
+
POSTGRES_DB=my_api_db
|
|
176
|
+
|
|
177
|
+
# OpenAI — supports custom base URL for Azure / LM Studio
|
|
178
|
+
OPENAI_API_KEY=sk-placeholder
|
|
179
|
+
OPENAI_MODEL=gpt-4o
|
|
180
|
+
OPENAI_BASE_URL= # blank = api.openai.com
|
|
181
|
+
|
|
182
|
+
# Redis
|
|
183
|
+
REDIS_HOST=localhost
|
|
184
|
+
REDIS_PORT=6379
|
|
185
|
+
REDIS_PASSWORD=
|
|
186
|
+
REDIS_DB=0
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## All Options
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
fastapi-spawn new [OPTIONS] PROJECT_NAME
|
|
195
|
+
|
|
196
|
+
--db postgresql | mysql | mongodb | sqlite | none
|
|
197
|
+
--orm sqlalchemy | tortoise | beanie | none
|
|
198
|
+
--migration alembic | aerich | none
|
|
199
|
+
--auth jwt | oauth2 | api-key | none
|
|
200
|
+
--broker redis | rabbitmq | kafka | none
|
|
201
|
+
--cache redis | memcached | none
|
|
202
|
+
--storage s3 | local | none
|
|
203
|
+
--ai openai | anthropic | none
|
|
204
|
+
--stack minimal | standard | full
|
|
205
|
+
--ci github | gitlab | both | none
|
|
206
|
+
--log-lib loguru | structlog | standard
|
|
207
|
+
--no-docker Skip Docker files
|
|
208
|
+
--no-tests Skip test suite
|
|
209
|
+
--dry-run Preview file tree only
|
|
210
|
+
--force / -f Overwrite existing directory
|
|
211
|
+
--output / -o Output directory (default: .)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Subcommands
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
fastapi-spawn list-templates # Show all options + ORM/DB compatibility
|
|
218
|
+
fastapi-spawn validate FILE # Validate a .fastapi-spawn.toml
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## After Scaffolding
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
cd my-api
|
|
227
|
+
uv sync # Install all dependencies
|
|
228
|
+
uv run alembic upgrade head # Run DB migrations (if alembic)
|
|
229
|
+
docker compose up --build # Start all services
|
|
230
|
+
# or
|
|
231
|
+
uv run main.py # Run locally
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## ORM ↔ Database Compatibility
|
|
237
|
+
|
|
238
|
+
| ORM | Compatible Databases |
|
|
239
|
+
|---|---|
|
|
240
|
+
| `sqlalchemy` | postgresql, mysql, sqlite |
|
|
241
|
+
| `tortoise` | postgresql, mysql, sqlite |
|
|
242
|
+
| `beanie` | mongodb |
|
|
243
|
+
| `none` | any |
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Contributing
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
git clone https://github.com/Bishwajitgarai/fastapi-spawn
|
|
251
|
+
cd fastapi-spawn
|
|
252
|
+
uv sync --all-extras
|
|
253
|
+
uv run pytest
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
PRs are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## License
|
|
261
|
+
|
|
262
|
+
MIT © [Bishwajit Garai](https://github.com/Bishwajitgarai)
|