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.
- pysnap/__init__.py +1 -0
- pysnap/_shared/Dockerfile.j2 +34 -0
- pysnap/_shared/ci.yml.j2 +53 -0
- pysnap/_shared/docker-compose.yml.j2 +46 -0
- pysnap/_shared/dockerignore.j2 +13 -0
- pysnap/_shared/env_example.j2 +24 -0
- pysnap/_shared/gitignore.j2 +15 -0
- pysnap/commands/__init__.py +1 -0
- pysnap/commands/add.py +171 -0
- pysnap/commands/create.py +136 -0
- pysnap/commands/templates_cmd.py +134 -0
- pysnap/commands/update.py +133 -0
- pysnap/community.py +113 -0
- pysnap/config.py +76 -0
- pysnap/generator.py +262 -0
- pysnap/main.py +65 -0
- pysnap/manifest.py +101 -0
- pysnap/plugins.py +123 -0
- pysnap/preview.py +131 -0
- pysnap/prompts.py +217 -0
- pysnap/registry.py +123 -0
- pysnap/templates/django/.dockerignore.j2 +15 -0
- pysnap/templates/django/.github/workflows/ci.yml.j2 +34 -0
- pysnap/templates/django/.gitignore.j2 +14 -0
- pysnap/templates/django/Dockerfile.j2 +14 -0
- pysnap/templates/django/README.md.j2 +36 -0
- pysnap/templates/django/apps/__init__.py.j2 +0 -0
- pysnap/templates/django/apps/core/__init__.py.j2 +0 -0
- pysnap/templates/django/apps/core/apps.py.j2 +6 -0
- pysnap/templates/django/apps/core/urls.py.j2 +7 -0
- pysnap/templates/django/apps/core/views.py.j2 +6 -0
- pysnap/templates/django/apps/users/__init__.py.j2 +0 -0
- pysnap/templates/django/apps/users/apps.py.j2 +6 -0
- pysnap/templates/django/apps/users/models.py.j2 +14 -0
- pysnap/templates/django/apps/users/serializers.py.j2 +13 -0
- pysnap/templates/django/apps/users/urls.py.j2 +10 -0
- pysnap/templates/django/apps/users/views.py.j2 +22 -0
- pysnap/templates/django/config/__init__.py.j2 +0 -0
- pysnap/templates/django/config/asgi.py.j2 +9 -0
- pysnap/templates/django/config/settings.py.j2 +110 -0
- pysnap/templates/django/config/urls.py.j2 +12 -0
- pysnap/templates/django/config/wsgi.py.j2 +9 -0
- pysnap/templates/django/docker-compose.yml.j2 +29 -0
- pysnap/templates/django/manage.py.j2 +22 -0
- pysnap/templates/django/pyproject.toml.j2 +40 -0
- pysnap/templates/django/template.json +50 -0
- pysnap/templates/django/tests/__init__.py.j2 +1 -0
- pysnap/templates/django/tests/conftest.py.j2 +6 -0
- pysnap/templates/django/tests/test_health.py.j2 +9 -0
- pysnap/templates/fastapi/.dockerignore.j2 +8 -0
- pysnap/templates/fastapi/.github/workflows/ci.yml.j2 +46 -0
- pysnap/templates/fastapi/.gitignore.j2 +13 -0
- pysnap/templates/fastapi/Dockerfile.j2 +14 -0
- pysnap/templates/fastapi/README.md.j2 +57 -0
- pysnap/templates/fastapi/api/__init__.py.j2 +0 -0
- pysnap/templates/fastapi/api/routes/__init__.py.j2 +0 -0
- pysnap/templates/fastapi/api/routes/auth.py.j2 +18 -0
- pysnap/templates/fastapi/api/routes/health.py.j2 +8 -0
- pysnap/templates/fastapi/app/__init__.py.j2 +1 -0
- pysnap/templates/fastapi/core/__init__.py.j2 +0 -0
- pysnap/templates/fastapi/core/config.py.j2 +26 -0
- pysnap/templates/fastapi/core/security.py.j2 +22 -0
- pysnap/templates/fastapi/db/__init__.py.j2 +0 -0
- pysnap/templates/fastapi/db/base.py.j2 +5 -0
- pysnap/templates/fastapi/db/session.py.j2 +15 -0
- pysnap/templates/fastapi/docker-compose.yml.j2 +30 -0
- pysnap/templates/fastapi/main.py.j2 +27 -0
- pysnap/templates/fastapi/models/__init__.py.j2 +0 -0
- pysnap/templates/fastapi/models/user.py.j2 +13 -0
- pysnap/templates/fastapi/pyproject.toml.j2 +48 -0
- pysnap/templates/fastapi/schemas/__init__.py.j2 +0 -0
- pysnap/templates/fastapi/schemas/user.py.j2 +18 -0
- pysnap/templates/fastapi/template.json +53 -0
- pysnap/templates/fastapi/tests/__init__.py.j2 +0 -0
- pysnap/templates/fastapi/tests/conftest.py.j2 +9 -0
- pysnap/templates/fastapi/tests/test_health.py.j2 +9 -0
- pysnap/templates/flask/.dockerignore.j2 +14 -0
- pysnap/templates/flask/.github/workflows/ci.yml.j2 +34 -0
- pysnap/templates/flask/.gitignore.j2 +13 -0
- pysnap/templates/flask/Dockerfile.j2 +14 -0
- pysnap/templates/flask/README.md.j2 +34 -0
- pysnap/templates/flask/app/__init__.py.j2 +30 -0
- pysnap/templates/flask/app/config.py.j2 +23 -0
- pysnap/templates/flask/app/extensions.py.j2 +9 -0
- pysnap/templates/flask/app/models/__init__.py.j2 +1 -0
- pysnap/templates/flask/app/models/user.py.j2 +16 -0
- pysnap/templates/flask/app/routes/__init__.py.j2 +1 -0
- pysnap/templates/flask/app/routes/auth.py.j2 +31 -0
- pysnap/templates/flask/app/routes/health.py.j2 +11 -0
- pysnap/templates/flask/docker-compose.yml.j2 +29 -0
- pysnap/templates/flask/pyproject.toml.j2 +39 -0
- pysnap/templates/flask/template.json +44 -0
- pysnap/templates/flask/tests/__init__.py.j2 +1 -0
- pysnap/templates/flask/tests/conftest.py.j2 +16 -0
- pysnap/templates/flask/tests/test_health.py.j2 +8 -0
- pysnap/templates/flask/wsgi.py.j2 +8 -0
- pysnap/validator.py +89 -0
- snapstack-1.0.0.dist-info/METADATA +267 -0
- snapstack-1.0.0.dist-info/RECORD +102 -0
- snapstack-1.0.0.dist-info/WHEEL +4 -0
- snapstack-1.0.0.dist-info/entry_points.txt +2 -0
- 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 @@
|
|
|
1
|
+
|
|
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,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,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,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 @@
|
|
|
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 }}"})
|