snapstack 1.0.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 (102) hide show
  1. pysnap/__init__.py +1 -0
  2. pysnap/_shared/Dockerfile.j2 +34 -0
  3. pysnap/_shared/ci.yml.j2 +53 -0
  4. pysnap/_shared/docker-compose.yml.j2 +46 -0
  5. pysnap/_shared/dockerignore.j2 +13 -0
  6. pysnap/_shared/env_example.j2 +24 -0
  7. pysnap/_shared/gitignore.j2 +15 -0
  8. pysnap/commands/__init__.py +1 -0
  9. pysnap/commands/add.py +171 -0
  10. pysnap/commands/create.py +136 -0
  11. pysnap/commands/templates_cmd.py +134 -0
  12. pysnap/commands/update.py +133 -0
  13. pysnap/community.py +113 -0
  14. pysnap/config.py +76 -0
  15. pysnap/generator.py +262 -0
  16. pysnap/main.py +65 -0
  17. pysnap/manifest.py +101 -0
  18. pysnap/plugins.py +123 -0
  19. pysnap/preview.py +131 -0
  20. pysnap/prompts.py +217 -0
  21. pysnap/registry.py +123 -0
  22. pysnap/templates/django/.dockerignore.j2 +15 -0
  23. pysnap/templates/django/.github/workflows/ci.yml.j2 +34 -0
  24. pysnap/templates/django/.gitignore.j2 +14 -0
  25. pysnap/templates/django/Dockerfile.j2 +14 -0
  26. pysnap/templates/django/README.md.j2 +36 -0
  27. pysnap/templates/django/apps/__init__.py.j2 +0 -0
  28. pysnap/templates/django/apps/core/__init__.py.j2 +0 -0
  29. pysnap/templates/django/apps/core/apps.py.j2 +6 -0
  30. pysnap/templates/django/apps/core/urls.py.j2 +7 -0
  31. pysnap/templates/django/apps/core/views.py.j2 +6 -0
  32. pysnap/templates/django/apps/users/__init__.py.j2 +0 -0
  33. pysnap/templates/django/apps/users/apps.py.j2 +6 -0
  34. pysnap/templates/django/apps/users/models.py.j2 +14 -0
  35. pysnap/templates/django/apps/users/serializers.py.j2 +13 -0
  36. pysnap/templates/django/apps/users/urls.py.j2 +10 -0
  37. pysnap/templates/django/apps/users/views.py.j2 +22 -0
  38. pysnap/templates/django/config/__init__.py.j2 +0 -0
  39. pysnap/templates/django/config/asgi.py.j2 +9 -0
  40. pysnap/templates/django/config/settings.py.j2 +110 -0
  41. pysnap/templates/django/config/urls.py.j2 +12 -0
  42. pysnap/templates/django/config/wsgi.py.j2 +9 -0
  43. pysnap/templates/django/docker-compose.yml.j2 +29 -0
  44. pysnap/templates/django/manage.py.j2 +22 -0
  45. pysnap/templates/django/pyproject.toml.j2 +40 -0
  46. pysnap/templates/django/template.json +50 -0
  47. pysnap/templates/django/tests/__init__.py.j2 +1 -0
  48. pysnap/templates/django/tests/conftest.py.j2 +6 -0
  49. pysnap/templates/django/tests/test_health.py.j2 +9 -0
  50. pysnap/templates/fastapi/.dockerignore.j2 +8 -0
  51. pysnap/templates/fastapi/.github/workflows/ci.yml.j2 +46 -0
  52. pysnap/templates/fastapi/.gitignore.j2 +13 -0
  53. pysnap/templates/fastapi/Dockerfile.j2 +14 -0
  54. pysnap/templates/fastapi/README.md.j2 +57 -0
  55. pysnap/templates/fastapi/api/__init__.py.j2 +0 -0
  56. pysnap/templates/fastapi/api/routes/__init__.py.j2 +0 -0
  57. pysnap/templates/fastapi/api/routes/auth.py.j2 +18 -0
  58. pysnap/templates/fastapi/api/routes/health.py.j2 +8 -0
  59. pysnap/templates/fastapi/app/__init__.py.j2 +1 -0
  60. pysnap/templates/fastapi/core/__init__.py.j2 +0 -0
  61. pysnap/templates/fastapi/core/config.py.j2 +26 -0
  62. pysnap/templates/fastapi/core/security.py.j2 +22 -0
  63. pysnap/templates/fastapi/db/__init__.py.j2 +0 -0
  64. pysnap/templates/fastapi/db/base.py.j2 +5 -0
  65. pysnap/templates/fastapi/db/session.py.j2 +15 -0
  66. pysnap/templates/fastapi/docker-compose.yml.j2 +30 -0
  67. pysnap/templates/fastapi/main.py.j2 +27 -0
  68. pysnap/templates/fastapi/models/__init__.py.j2 +0 -0
  69. pysnap/templates/fastapi/models/user.py.j2 +13 -0
  70. pysnap/templates/fastapi/pyproject.toml.j2 +48 -0
  71. pysnap/templates/fastapi/schemas/__init__.py.j2 +0 -0
  72. pysnap/templates/fastapi/schemas/user.py.j2 +18 -0
  73. pysnap/templates/fastapi/template.json +53 -0
  74. pysnap/templates/fastapi/tests/__init__.py.j2 +0 -0
  75. pysnap/templates/fastapi/tests/conftest.py.j2 +9 -0
  76. pysnap/templates/fastapi/tests/test_health.py.j2 +9 -0
  77. pysnap/templates/flask/.dockerignore.j2 +14 -0
  78. pysnap/templates/flask/.github/workflows/ci.yml.j2 +34 -0
  79. pysnap/templates/flask/.gitignore.j2 +13 -0
  80. pysnap/templates/flask/Dockerfile.j2 +14 -0
  81. pysnap/templates/flask/README.md.j2 +34 -0
  82. pysnap/templates/flask/app/__init__.py.j2 +30 -0
  83. pysnap/templates/flask/app/config.py.j2 +23 -0
  84. pysnap/templates/flask/app/extensions.py.j2 +9 -0
  85. pysnap/templates/flask/app/models/__init__.py.j2 +1 -0
  86. pysnap/templates/flask/app/models/user.py.j2 +16 -0
  87. pysnap/templates/flask/app/routes/__init__.py.j2 +1 -0
  88. pysnap/templates/flask/app/routes/auth.py.j2 +31 -0
  89. pysnap/templates/flask/app/routes/health.py.j2 +11 -0
  90. pysnap/templates/flask/docker-compose.yml.j2 +29 -0
  91. pysnap/templates/flask/pyproject.toml.j2 +39 -0
  92. pysnap/templates/flask/template.json +44 -0
  93. pysnap/templates/flask/tests/__init__.py.j2 +1 -0
  94. pysnap/templates/flask/tests/conftest.py.j2 +16 -0
  95. pysnap/templates/flask/tests/test_health.py.j2 +8 -0
  96. pysnap/templates/flask/wsgi.py.j2 +8 -0
  97. pysnap/validator.py +89 -0
  98. snapstack-1.0.0.dist-info/METADATA +267 -0
  99. snapstack-1.0.0.dist-info/RECORD +102 -0
  100. snapstack-1.0.0.dist-info/WHEEL +4 -0
  101. snapstack-1.0.0.dist-info/entry_points.txt +2 -0
  102. snapstack-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,57 @@
1
+ # {{ project_name }}
2
+
3
+ > Generated with [pysnap](https://github.com/yourusername/pysnap)
4
+
5
+ ## Stack
6
+
7
+ - **Framework:** FastAPI
8
+ - **Database:** {{ database | capitalize }}
9
+ - **Package Manager:** {{ package_manager }}
10
+ {%- if include_auth %}
11
+ - **Auth:** JWT
12
+ {%- endif %}
13
+ {%- if include_docker %}
14
+ - **Docker:** Yes
15
+ {%- endif %}
16
+ {%- if include_ci %}
17
+ - **CI/CD:** GitHub Actions
18
+ {%- endif %}
19
+
20
+ ## Getting Started
21
+
22
+ ```bash
23
+ {%- if package_manager == "uv" %}
24
+ uv sync
25
+ uv run uvicorn app.main:app --reload
26
+ {%- elif package_manager == "poetry" %}
27
+ poetry install
28
+ poetry run uvicorn app.main:app --reload
29
+ {%- else %}
30
+ pip install -e ".[dev]"
31
+ uvicorn app.main:app --reload
32
+ {%- endif %}
33
+ ```
34
+
35
+ Visit:
36
+ - **Docs:** http://localhost:8000/docs
37
+ - **Health:** http://localhost:8000/api/v1/health
38
+ {%- if include_tests %}
39
+
40
+ ## Tests
41
+
42
+ ```bash
43
+ {%- if package_manager == "uv" %}
44
+ uv run pytest tests/ -v
45
+ {%- else %}
46
+ pytest tests/ -v
47
+ {%- endif %}
48
+ ```
49
+ {%- endif %}
50
+ {%- if include_docker %}
51
+
52
+ ## Docker
53
+
54
+ ```bash
55
+ docker compose up --build
56
+ ```
57
+ {%- endif %}
File without changes
File without changes
@@ -0,0 +1,18 @@
1
+ from fastapi import APIRouter, HTTPException, status
2
+ from app.schemas.user import UserCreate, Token
3
+ from app.core.security import get_password_hash, create_access_token
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.post("/auth/register", status_code=status.HTTP_201_CREATED)
9
+ async def register(user_in: UserCreate):
10
+ hashed_password = get_password_hash(user_in.password)
11
+ # TODO: persist user to database
12
+ return {"message": "User created", "email": user_in.email}
13
+
14
+
15
+ @router.post("/auth/login", response_model=Token)
16
+ async def login(email: str, password: str):
17
+ # TODO: look up user from database
18
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
@@ -0,0 +1,8 @@
1
+ from fastapi import APIRouter
2
+
3
+ router = APIRouter()
4
+
5
+
6
+ @router.get("/health", summary="Health check")
7
+ async def health_check():
8
+ return {"status": "ok", "version": "0.1.0"}
File without changes
@@ -0,0 +1,26 @@
1
+ from pydantic_settings import BaseSettings
2
+ from typing import List
3
+
4
+
5
+ class Settings(BaseSettings):
6
+ PROJECT_NAME: str = "{{ project_name }}"
7
+ VERSION: str = "0.1.0"
8
+ DESCRIPTION: str = "Generated by pysnap"
9
+ API_PREFIX: str = "/api/v1"
10
+ DEBUG: bool = False
11
+ ALLOWED_ORIGINS: List[str] = ["http://localhost:3000", "http://localhost:8000"]
12
+ {%- if database == "postgresql" %}
13
+ DATABASE_URL: str = "postgresql+asyncpg://user:password@localhost:5432/{{ project_name_slug }}"
14
+ {%- elif database == "sqlite" %}
15
+ DATABASE_URL: str = "sqlite+aiosqlite:///./{{ project_name_slug }}.db"
16
+ {%- endif %}
17
+ {%- if include_auth %}
18
+ SECRET_KEY: str = "change-this-secret-key-in-production"
19
+ ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
20
+ ALGORITHM: str = "HS256"
21
+ {%- endif %}
22
+
23
+ model_config = {"env_file": ".env", "case_sensitive": True}
24
+
25
+
26
+ settings = Settings()
@@ -0,0 +1,22 @@
1
+ from datetime import datetime, timedelta, timezone
2
+ from typing import Optional
3
+ from jose import jwt
4
+ from passlib.context import CryptContext
5
+ from app.core.config import settings
6
+
7
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
8
+
9
+
10
+ def verify_password(plain: str, hashed: str) -> bool:
11
+ return pwd_context.verify(plain, hashed)
12
+
13
+
14
+ def get_password_hash(password: str) -> str:
15
+ return pwd_context.hash(password)
16
+
17
+
18
+ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
19
+ to_encode = data.copy()
20
+ expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES))
21
+ to_encode["exp"] = expire
22
+ return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
File without changes
@@ -0,0 +1,5 @@
1
+ from sqlalchemy.orm import DeclarativeBase
2
+
3
+
4
+ class Base(DeclarativeBase):
5
+ pass
@@ -0,0 +1,15 @@
1
+ from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
2
+ from app.core.config import settings
3
+
4
+ engine = create_async_engine(settings.DATABASE_URL, echo=settings.DEBUG)
5
+ AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
6
+
7
+
8
+ async def get_db() -> AsyncSession:
9
+ async with AsyncSessionLocal() as session:
10
+ try:
11
+ yield session
12
+ await session.commit()
13
+ except Exception:
14
+ await session.rollback()
15
+ raise
@@ -0,0 +1,30 @@
1
+ services:
2
+ api:
3
+ build: .
4
+ ports:
5
+ - "8000:8000"
6
+ env_file: .env
7
+ {%- if database == "postgresql" %}
8
+ depends_on:
9
+ db:
10
+ condition: service_healthy
11
+
12
+ db:
13
+ image: postgres:16-alpine
14
+ environment:
15
+ POSTGRES_USER: user
16
+ POSTGRES_PASSWORD: password
17
+ POSTGRES_DB: {{ project_name_slug }}
18
+ ports:
19
+ - "5432:5432"
20
+ volumes:
21
+ - postgres_data:/var/lib/postgresql/data
22
+ healthcheck:
23
+ test: ["CMD-SHELL", "pg_isready -U user -d {{ project_name_slug }}"]
24
+ interval: 5s
25
+ timeout: 5s
26
+ retries: 5
27
+
28
+ volumes:
29
+ postgres_data:
30
+ {%- endif %}
@@ -0,0 +1,27 @@
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from app.core.config import settings
4
+ from app.api.routes import health
5
+ {% if include_auth %}
6
+ from app.api.routes import auth
7
+ {% endif %}
8
+
9
+ app = FastAPI(
10
+ title=settings.PROJECT_NAME,
11
+ version=settings.VERSION,
12
+ description=settings.DESCRIPTION,
13
+ openapi_url=f"{settings.API_PREFIX}/openapi.json",
14
+ )
15
+
16
+ app.add_middleware(
17
+ CORSMiddleware,
18
+ allow_origins=settings.ALLOWED_ORIGINS,
19
+ allow_credentials=True,
20
+ allow_methods=["*"],
21
+ allow_headers=["*"],
22
+ )
23
+
24
+ app.include_router(health.router, prefix=settings.API_PREFIX, tags=["health"])
25
+ {% if include_auth %}
26
+ app.include_router(auth.router, prefix=settings.API_PREFIX, tags=["auth"])
27
+ {% endif %}
File without changes
@@ -0,0 +1,13 @@
1
+ from sqlalchemy import String, Boolean
2
+ from sqlalchemy.orm import Mapped, mapped_column
3
+ from app.db.base import Base
4
+
5
+
6
+ class User(Base):
7
+ __tablename__ = "users"
8
+
9
+ id: Mapped[int] = mapped_column(primary_key=True)
10
+ email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
11
+ hashed_password: Mapped[str] = mapped_column(String(255))
12
+ is_active: Mapped[bool] = mapped_column(Boolean, default=True)
13
+ is_superuser: Mapped[bool] = mapped_column(Boolean, default=False)
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "{{ project_name }}"
7
+ version = "0.1.0"
8
+ description = "Generated by pysnap"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "fastapi>=0.115.0",
12
+ "uvicorn[standard]>=0.32.0",
13
+ "pydantic>=2.0.0",
14
+ "pydantic-settings>=2.0.0",
15
+ {%- if database == "sqlite" %}
16
+ "sqlalchemy>=2.0.0",
17
+ "aiosqlite>=0.20.0",
18
+ {%- elif database == "postgresql" %}
19
+ "sqlalchemy>=2.0.0",
20
+ "asyncpg>=0.30.0",
21
+ "alembic>=1.13.0",
22
+ {%- endif %}
23
+ {%- if include_auth %}
24
+ "python-jose[cryptography]>=3.3.0",
25
+ "passlib[bcrypt]>=1.7.4",
26
+ "email-validator>=2.0.0",
27
+ {%- endif %}
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ dev = [
32
+ {%- if include_tests %}
33
+ "pytest>=8.0.0",
34
+ "pytest-asyncio>=0.24.0",
35
+ "httpx>=0.27.0",
36
+ {%- endif %}
37
+ "ruff>=0.8.0",
38
+ ]
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["app"]
42
+
43
+ [tool.pytest.ini_options]
44
+ asyncio_mode = "auto"
45
+
46
+ [tool.ruff]
47
+ line-length = 100
48
+ target-version = "py311"
File without changes
@@ -0,0 +1,18 @@
1
+ from pydantic import BaseModel, EmailStr
2
+
3
+
4
+ class UserCreate(BaseModel):
5
+ email: EmailStr
6
+ password: str
7
+
8
+
9
+ class UserOut(BaseModel):
10
+ id: int
11
+ email: EmailStr
12
+ is_active: bool
13
+ model_config = {"from_attributes": True}
14
+
15
+
16
+ class Token(BaseModel):
17
+ access_token: str
18
+ token_type: str = "bearer"
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "fastapi",
3
+ "display_name": "FastAPI",
4
+ "description": "Async FastAPI project with Pydantic v2, SQLAlchemy 2, and uvicorn",
5
+ "version": "1.0.0",
6
+ "files": {
7
+ "always": {
8
+ "app/__init__.py.j2": "app/__init__.py",
9
+ "main.py.j2": "app/main.py",
10
+ "core/__init__.py.j2": "app/core/__init__.py",
11
+ "core/config.py.j2": "app/core/config.py",
12
+ "api/__init__.py.j2": "app/api/__init__.py",
13
+ "api/routes/__init__.py.j2": "app/api/routes/__init__.py",
14
+ "api/routes/health.py.j2": "app/api/routes/health.py",
15
+ "models/__init__.py.j2": "app/models/__init__.py",
16
+ "schemas/__init__.py.j2": "app/schemas/__init__.py",
17
+ "pyproject.toml.j2": "pyproject.toml",
18
+ ".env.example.j2": ".env.example",
19
+ ".gitignore.j2": ".gitignore",
20
+ "README.md.j2": "README.md"
21
+ },
22
+ "when_database": {
23
+ "db/__init__.py.j2": "app/db/__init__.py",
24
+ "db/base.py.j2": "app/db/base.py",
25
+ "db/session.py.j2": "app/db/session.py"
26
+ },
27
+ "when_auth": {
28
+ "core/security.py.j2": "app/core/security.py",
29
+ "api/routes/auth.py.j2": "app/api/routes/auth.py",
30
+ "schemas/user.py.j2": "app/schemas/user.py"
31
+ },
32
+ "when_auth_and_database": {
33
+ "models/user.py.j2": "app/models/user.py"
34
+ },
35
+ "when_docker": {
36
+ "Dockerfile.j2": "Dockerfile",
37
+ "docker-compose.yml.j2": "docker-compose.yml",
38
+ ".dockerignore.j2": ".dockerignore"
39
+ },
40
+ "when_tests": {
41
+ "tests/__init__.py.j2": "tests/__init__.py",
42
+ "tests/conftest.py.j2": "tests/conftest.py",
43
+ "tests/test_health.py.j2": "tests/test_health.py"
44
+ },
45
+ "when_ci": {
46
+ ".github/workflows/ci.yml.j2": ".github/workflows/ci.yml"
47
+ }
48
+ },
49
+ "prompts": [],
50
+ "hooks": {
51
+ "post_generate": []
52
+ }
53
+ }
File without changes
@@ -0,0 +1,9 @@
1
+ import pytest
2
+ from httpx import AsyncClient, ASGITransport
3
+ from app.main import app
4
+
5
+
6
+ @pytest.fixture
7
+ async def client():
8
+ async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
9
+ yield ac
@@ -0,0 +1,9 @@
1
+ import pytest
2
+ from httpx import AsyncClient
3
+
4
+
5
+ @pytest.mark.asyncio
6
+ async def test_health_check(client: AsyncClient):
7
+ response = await client.get("/api/v1/health")
8
+ assert response.status_code == 200
9
+ assert response.json()["status"] == "ok"
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ .env
6
+ dist/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .coverage
10
+ htmlcov/
11
+ *.log
12
+ .DS_Store
13
+ Thumbs.db
14
+ .git/
@@ -0,0 +1,34 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint-and-test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Set up Python
15
+ uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.11"
18
+ {% if package_manager == "uv" %}
19
+ - name: Install uv
20
+ run: pip install uv
21
+ - name: Install dependencies
22
+ run: uv sync
23
+ - name: Lint
24
+ run: uv run ruff check .
25
+ - name: Test
26
+ run: uv run pytest
27
+ {% else %}
28
+ - name: Install dependencies
29
+ run: pip install -e ".[dev]"
30
+ - name: Lint
31
+ run: ruff check .
32
+ - name: Test
33
+ run: pytest
34
+ {% endif %}
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .env
5
+ .venv/
6
+ venv/
7
+ dist/
8
+ build/
9
+ *.db
10
+ .pytest_cache/
11
+ .ruff_cache/
12
+ htmlcov/
13
+ .coverage
@@ -0,0 +1,14 @@
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ {% if package_manager == "uv" -%}
4
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
5
+ COPY pyproject.toml uv.lock* ./
6
+ RUN uv sync --frozen --no-dev
7
+ COPY . .
8
+ CMD ["uv", "run", "flask", "--app", "wsgi", "run", "--host=0.0.0.0"]
9
+ {%- else -%}
10
+ COPY pyproject.toml ./
11
+ RUN pip install --no-cache-dir -e .
12
+ COPY . .
13
+ CMD ["flask", "--app", "wsgi", "run", "--host=0.0.0.0"]
14
+ {%- endif %}
@@ -0,0 +1,34 @@
1
+ # {{ project_name }}
2
+
3
+ A Flask project generated by [pysnap](https://github.com/pysnap-dev/pysnap).
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ {% if package_manager == "uv" %}
9
+ uv sync
10
+ uv run flask --app wsgi run --debug
11
+ {% else %}
12
+ pip install -e ".[dev]"
13
+ flask --app wsgi run --debug
14
+ {% endif %}
15
+ ```
16
+
17
+ {% if include_docker %}
18
+ ## Docker
19
+
20
+ ```bash
21
+ docker compose up
22
+ ```
23
+ {% endif %}
24
+ {% if include_tests %}
25
+ ## Tests
26
+
27
+ ```bash
28
+ {% if package_manager == "uv" %}
29
+ uv run pytest
30
+ {% else %}
31
+ pytest
32
+ {% endif %}
33
+ ```
34
+ {% endif %}
@@ -0,0 +1,30 @@
1
+ """Flask application factory for {{ project_name }}."""
2
+
3
+ from flask import Flask
4
+
5
+ from app.config import Config
6
+ from app.extensions import db
7
+
8
+
9
+ def create_app(config_class=Config) -> Flask:
10
+ """Create and configure the Flask application."""
11
+ app = Flask(__name__)
12
+ app.config.from_object(config_class)
13
+
14
+ # Initialize extensions
15
+ {% if database != "none" %}
16
+ db.init_app(app)
17
+ {% endif %}
18
+
19
+ # Register blueprints
20
+ from app.routes.health import health_bp
21
+
22
+ app.register_blueprint(health_bp)
23
+
24
+ {% if include_auth %}
25
+ from app.routes.auth import auth_bp
26
+
27
+ app.register_blueprint(auth_bp, url_prefix="/api/auth")
28
+ {% endif %}
29
+
30
+ return app
@@ -0,0 +1,23 @@
1
+ """Flask app configuration."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ class Config:
8
+ """Base configuration."""
9
+
10
+ SECRET_KEY: str = os.environ.get("SECRET_KEY", "insecure-dev-key-change-in-production")
11
+ DEBUG: bool = os.environ.get("DEBUG", "false").lower() == "true"
12
+ {% if database == "postgresql" %}
13
+ SQLALCHEMY_DATABASE_URI: str = os.environ.get(
14
+ "DATABASE_URL", "postgresql+psycopg://user:password@localhost/{{ project_name_slug }}"
15
+ )
16
+ {% elif database == "sqlite" %}
17
+ SQLALCHEMY_DATABASE_URI: str = os.environ.get(
18
+ "DATABASE_URL", f"sqlite:///{Path(__file__).parent.parent}/{{ project_name_slug }}.db"
19
+ )
20
+ {% else %}
21
+ SQLALCHEMY_DATABASE_URI: str = ""
22
+ {% endif %}
23
+ SQLALCHEMY_TRACK_MODIFICATIONS: bool = False
@@ -0,0 +1,9 @@
1
+ """Flask extensions -- initialized here, bound to app in create_app()."""
2
+
3
+ {% if database != "none" %}
4
+ from flask_sqlalchemy import SQLAlchemy
5
+
6
+ db = SQLAlchemy()
7
+ {% else %}
8
+ # No database extensions configured
9
+ {% endif %}
@@ -0,0 +1 @@
1
+ """Models package."""
@@ -0,0 +1,16 @@
1
+ from app.extensions import db
2
+
3
+
4
+ class User(db.Model):
5
+ """User model."""
6
+
7
+ __tablename__ = "users"
8
+
9
+ id = db.Column(db.Integer, primary_key=True)
10
+ email = db.Column(db.String(255), unique=True, nullable=False)
11
+ username = db.Column(db.String(100), unique=True, nullable=False)
12
+ password_hash = db.Column(db.String(255), nullable=False)
13
+ created_at = db.Column(db.DateTime, server_default=db.func.now())
14
+
15
+ def __repr__(self) -> str:
16
+ return f"<User {self.email}>"
@@ -0,0 +1 @@
1
+ """Routes package."""
@@ -0,0 +1,31 @@
1
+ """Auth blueprint."""
2
+
3
+ from flask import Blueprint, jsonify, request
4
+ from flask_jwt_extended import create_access_token, create_refresh_token, jwt_required, get_jwt_identity
5
+
6
+ auth_bp = Blueprint("auth", __name__)
7
+
8
+
9
+ @auth_bp.post("/login")
10
+ def login():
11
+ """Obtain a JWT token pair."""
12
+ data = request.get_json(silent=True) or {}
13
+ username = data.get("username")
14
+ password = data.get("password")
15
+
16
+ # TODO: Replace with real user lookup + password verification
17
+ if not username or not password:
18
+ return jsonify({"error": "username and password required"}), 400
19
+
20
+ access_token = create_access_token(identity=username)
21
+ refresh_token = create_refresh_token(identity=username)
22
+ return jsonify(access_token=access_token, refresh_token=refresh_token), 200
23
+
24
+
25
+ @auth_bp.post("/refresh")
26
+ @jwt_required(refresh=True)
27
+ def refresh():
28
+ """Refresh an access token using a refresh token."""
29
+ identity = get_jwt_identity()
30
+ access_token = create_access_token(identity=identity)
31
+ return jsonify(access_token=access_token), 200
@@ -0,0 +1,11 @@
1
+ """Health check blueprint."""
2
+
3
+ from flask import Blueprint, jsonify
4
+
5
+ health_bp = Blueprint("health", __name__, url_prefix="/api")
6
+
7
+
8
+ @health_bp.get("/health")
9
+ def health():
10
+ """Health check endpoint."""
11
+ return jsonify({"status": "ok", "service": "{{ project_name }}"})