appgenerator-cli 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.
- appgenerator_cli-1.0.0.dist-info/METADATA +213 -0
- appgenerator_cli-1.0.0.dist-info/RECORD +48 -0
- appgenerator_cli-1.0.0.dist-info/WHEEL +4 -0
- appgenerator_cli-1.0.0.dist-info/entry_points.txt +4 -0
- pyforge/__init__.py +10 -0
- pyforge/commands/__init__.py +1 -0
- pyforge/commands/create.py +60 -0
- pyforge/generator.py +248 -0
- pyforge/main.py +32 -0
- pyforge/templates/ai/.env.example +29 -0
- pyforge/templates/ai/.gitignore +47 -0
- pyforge/templates/ai/Dockerfile +22 -0
- pyforge/templates/ai/README.md +97 -0
- pyforge/templates/ai/app/__init__.py +1 -0
- pyforge/templates/ai/app/agents/__init__.py +1 -0
- pyforge/templates/ai/app/agents/assistant.py +100 -0
- pyforge/templates/ai/app/chains/__init__.py +1 -0
- pyforge/templates/ai/app/chains/rag.py +50 -0
- pyforge/templates/ai/app/config.py +47 -0
- pyforge/templates/ai/app/tools/__init__.py +1 -0
- pyforge/templates/ai/app/tools/registry.py +19 -0
- pyforge/templates/ai/app/tools/search.py +34 -0
- pyforge/templates/ai/docker-compose.yml +39 -0
- pyforge/templates/ai/main.py +40 -0
- pyforge/templates/ai/pyproject.toml +28 -0
- pyforge/templates/ai/tests/__init__.py +1 -0
- pyforge/templates/ai/tests/conftest.py +21 -0
- pyforge/templates/ai/tests/test_agent.py +53 -0
- pyforge/templates/fastapi/.env.example +17 -0
- pyforge/templates/fastapi/.gitignore +42 -0
- pyforge/templates/fastapi/Dockerfile +27 -0
- pyforge/templates/fastapi/README.md +68 -0
- pyforge/templates/fastapi/app/__init__.py +1 -0
- pyforge/templates/fastapi/app/api/__init__.py +1 -0
- pyforge/templates/fastapi/app/api/v1/__init__.py +1 -0
- pyforge/templates/fastapi/app/api/v1/health.py +25 -0
- pyforge/templates/fastapi/app/config.py +45 -0
- pyforge/templates/fastapi/app/db/__init__.py +1 -0
- pyforge/templates/fastapi/app/db/session.py +35 -0
- pyforge/templates/fastapi/app/dependencies.py +12 -0
- pyforge/templates/fastapi/app/main.py +58 -0
- pyforge/templates/fastapi/app/models/__init__.py +4 -0
- pyforge/templates/fastapi/app/models/base.py +23 -0
- pyforge/templates/fastapi/docker-compose.yml +39 -0
- pyforge/templates/fastapi/pyproject.toml +29 -0
- pyforge/templates/fastapi/tests/__init__.py +1 -0
- pyforge/templates/fastapi/tests/conftest.py +46 -0
- pyforge/templates/fastapi/tests/test_health.py +13 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# ── Build stage ────────────────────────────────────────────────────────────────
|
|
2
|
+
FROM python:3.12-slim AS builder
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
# Install uv
|
|
7
|
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|
8
|
+
|
|
9
|
+
# Install dependencies (cached layer)
|
|
10
|
+
COPY pyproject.toml uv.lock* ./
|
|
11
|
+
RUN uv sync --frozen --no-dev
|
|
12
|
+
|
|
13
|
+
# ── Runtime stage ──────────────────────────────────────────────────────────────
|
|
14
|
+
FROM python:3.12-slim AS runtime
|
|
15
|
+
|
|
16
|
+
WORKDIR /app
|
|
17
|
+
|
|
18
|
+
# Copy venv from builder
|
|
19
|
+
COPY --from=builder /app/.venv /app/.venv
|
|
20
|
+
ENV PATH="/app/.venv/bin:$PATH"
|
|
21
|
+
|
|
22
|
+
# Copy application source
|
|
23
|
+
COPY app ./app
|
|
24
|
+
|
|
25
|
+
EXPOSE 8000
|
|
26
|
+
|
|
27
|
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# {{ project_name }}
|
|
2
|
+
|
|
3
|
+
A production-ready FastAPI backend, scaffolded by [AppGenerator](https://github.com/yourname/appgenerator-cli).
|
|
4
|
+
|
|
5
|
+
## Tech Stack
|
|
6
|
+
|
|
7
|
+
- **[FastAPI](https://fastapi.tiangolo.com/)** — high-performance async web framework
|
|
8
|
+
- **[SQLModel](https://sqlmodel.tiangolo.com/)** — type-safe ORM (built on SQLAlchemy + Pydantic)
|
|
9
|
+
- **[Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/)** — typed config from env
|
|
10
|
+
- **[Alembic](https://alembic.sqlalchemy.org/)** — database migrations
|
|
11
|
+
- **[uv](https://docs.astral.sh/uv/)** — blazing-fast package management
|
|
12
|
+
{% if postgres %}- **PostgreSQL** — relational database
|
|
13
|
+
{% endif %}{% if redis %}- **Redis** — caching layer
|
|
14
|
+
{% endif %}{% if docker %}- **Docker** — containerisation
|
|
15
|
+
{% endif %}
|
|
16
|
+
|
|
17
|
+
## Getting Started
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 1. Copy and fill in your environment variables
|
|
21
|
+
cp .env.example .env
|
|
22
|
+
|
|
23
|
+
# 2. Run the development server
|
|
24
|
+
uv run uvicorn app.main:app --reload
|
|
25
|
+
|
|
26
|
+
# 3. Visit the interactive docs
|
|
27
|
+
open http://localhost:8000/docs
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Project Structure
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
{{ project_name }}/
|
|
34
|
+
├── app/
|
|
35
|
+
│ ├── main.py # FastAPI application factory
|
|
36
|
+
│ ├── config.py # Typed settings (pydantic-settings)
|
|
37
|
+
│ ├── dependencies.py # Shared FastAPI dependencies
|
|
38
|
+
│ ├── api/
|
|
39
|
+
│ │ ├── __init__.py
|
|
40
|
+
│ │ └── v1/
|
|
41
|
+
│ │ ├── __init__.py
|
|
42
|
+
│ │ └── health.py
|
|
43
|
+
│ ├── models/
|
|
44
|
+
│ │ ├── __init__.py
|
|
45
|
+
│ │ └── base.py # SQLModel base + common mixins
|
|
46
|
+
│ └── db/
|
|
47
|
+
│ ├── __init__.py
|
|
48
|
+
│ └── session.py # Async database session
|
|
49
|
+
├── tests/
|
|
50
|
+
│ ├── conftest.py
|
|
51
|
+
│ └── test_health.py
|
|
52
|
+
├── .env.example
|
|
53
|
+
├── .gitignore
|
|
54
|
+
└── pyproject.toml
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Running Tests
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv run pytest
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Linting
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv run ruff check .
|
|
67
|
+
uv run ruff format .
|
|
68
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""{{ project_name }} application package."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API package."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API v1 package."""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Health-check endpoint — always the first router to wire up.
|
|
3
|
+
GET /api/v1/health → 200 OK
|
|
4
|
+
"""
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HealthResponse(BaseModel):
|
|
14
|
+
status: str
|
|
15
|
+
timestamp: datetime
|
|
16
|
+
version: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.get("/health", response_model=HealthResponse, summary="Service health check")
|
|
20
|
+
async def health_check() -> HealthResponse:
|
|
21
|
+
return HealthResponse(
|
|
22
|
+
status="ok",
|
|
23
|
+
timestamp=datetime.now(timezone.utc),
|
|
24
|
+
version="0.1.0",
|
|
25
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Typed application settings loaded from environment variables / .env file.
|
|
3
|
+
"""
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from pydantic import field_validator
|
|
8
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Settings(BaseSettings):
|
|
12
|
+
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
|
13
|
+
|
|
14
|
+
# App
|
|
15
|
+
app_name: str = "{{ project_name }}"
|
|
16
|
+
app_env: str = "development"
|
|
17
|
+
debug: bool = True
|
|
18
|
+
secret_key: str = "change-me"
|
|
19
|
+
|
|
20
|
+
# Server
|
|
21
|
+
host: str = "0.0.0.0"
|
|
22
|
+
port: int = 8000
|
|
23
|
+
cors_origins: list[str] = ["*"]
|
|
24
|
+
|
|
25
|
+
# Database
|
|
26
|
+
database_url: str = "sqlite+aiosqlite:///./{{ package_name }}.db"
|
|
27
|
+
{% if redis %}
|
|
28
|
+
# Redis
|
|
29
|
+
redis_url: str = "redis://localhost:6379/0"
|
|
30
|
+
{% endif %}
|
|
31
|
+
|
|
32
|
+
@field_validator("cors_origins", mode="before")
|
|
33
|
+
@classmethod
|
|
34
|
+
def parse_cors(cls, v: str | list[str]) -> list[str]:
|
|
35
|
+
if isinstance(v, str):
|
|
36
|
+
return [origin.strip() for origin in v.split(",")]
|
|
37
|
+
return v
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@lru_cache
|
|
41
|
+
def get_settings() -> Settings:
|
|
42
|
+
return Settings()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
settings = get_settings()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Database package."""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Async database session factory using SQLModel + SQLAlchemy.
|
|
3
|
+
"""
|
|
4
|
+
from collections.abc import AsyncGenerator
|
|
5
|
+
|
|
6
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
7
|
+
from sqlalchemy.orm import sessionmaker
|
|
8
|
+
from sqlmodel import SQLModel
|
|
9
|
+
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
10
|
+
|
|
11
|
+
from app.config import settings
|
|
12
|
+
|
|
13
|
+
engine = create_async_engine(
|
|
14
|
+
settings.database_url,
|
|
15
|
+
echo=settings.debug,
|
|
16
|
+
future=True,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
AsyncSessionLocal = sessionmaker( # type: ignore[call-overload]
|
|
20
|
+
bind=engine,
|
|
21
|
+
class_=AsyncSession,
|
|
22
|
+
expire_on_commit=False,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def init_db() -> None:
|
|
27
|
+
"""Create all tables on startup (use Alembic for production migrations)."""
|
|
28
|
+
async with engine.begin() as conn:
|
|
29
|
+
await conn.run_sync(SQLModel.metadata.create_all)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
|
33
|
+
"""FastAPI dependency that yields an async DB session."""
|
|
34
|
+
async with AsyncSessionLocal() as session:
|
|
35
|
+
yield session
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared FastAPI dependency functions (DB sessions, auth, pagination, etc.).
|
|
3
|
+
"""
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
from fastapi import Depends
|
|
7
|
+
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
8
|
+
|
|
9
|
+
from app.db.session import get_session
|
|
10
|
+
|
|
11
|
+
# Re-export as typed dependency alias for cleaner route signatures
|
|
12
|
+
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
{{ project_name }} — FastAPI application entry point.
|
|
3
|
+
"""
|
|
4
|
+
from collections.abc import AsyncGenerator
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
9
|
+
|
|
10
|
+
from app.api.v1 import health
|
|
11
|
+
from app.config import settings
|
|
12
|
+
from app.db.session import init_db
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@asynccontextmanager
|
|
16
|
+
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
17
|
+
"""Run startup/shutdown tasks."""
|
|
18
|
+
await init_db()
|
|
19
|
+
yield
|
|
20
|
+
# teardown (close connections, flush caches, etc.)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_app() -> FastAPI:
|
|
24
|
+
application = FastAPI(
|
|
25
|
+
title=settings.app_name,
|
|
26
|
+
version="0.1.0",
|
|
27
|
+
description="Generated by AppGenerator",
|
|
28
|
+
docs_url="/docs" if settings.debug else None,
|
|
29
|
+
redoc_url="/redoc" if settings.debug else None,
|
|
30
|
+
lifespan=lifespan,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# CORS
|
|
34
|
+
application.add_middleware(
|
|
35
|
+
CORSMiddleware,
|
|
36
|
+
allow_origins=settings.cors_origins,
|
|
37
|
+
allow_credentials=True,
|
|
38
|
+
allow_methods=["*"],
|
|
39
|
+
allow_headers=["*"],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Routers
|
|
43
|
+
application.include_router(health.router, prefix="/api/v1", tags=["health"])
|
|
44
|
+
|
|
45
|
+
return application
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
app = create_app()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
import uvicorn
|
|
53
|
+
uvicorn.run(
|
|
54
|
+
"app.main:app",
|
|
55
|
+
host=settings.host,
|
|
56
|
+
port=settings.port,
|
|
57
|
+
reload=settings.debug,
|
|
58
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base SQLModel classes with common fields (id, created_at, updated_at).
|
|
3
|
+
"""
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
from sqlmodel import Field, SQLModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def utcnow() -> datetime:
|
|
10
|
+
return datetime.now(timezone.utc)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TimestampMixin(SQLModel):
|
|
14
|
+
"""Adds created_at and updated_at to any model."""
|
|
15
|
+
|
|
16
|
+
created_at: datetime = Field(default_factory=utcnow, nullable=False)
|
|
17
|
+
updated_at: datetime = Field(default_factory=utcnow, nullable=False)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseModel(TimestampMixin, table=False):
|
|
21
|
+
"""Abstract base — inherit from this for all your DB models."""
|
|
22
|
+
|
|
23
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
version: "3.9"
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
api:
|
|
5
|
+
build: .
|
|
6
|
+
ports:
|
|
7
|
+
- "8000:8000"
|
|
8
|
+
env_file: .env
|
|
9
|
+
depends_on:
|
|
10
|
+
{% if postgres %}- postgres{% endif %}
|
|
11
|
+
{% if redis %}- redis{% endif %}
|
|
12
|
+
restart: unless-stopped
|
|
13
|
+
|
|
14
|
+
{% if postgres %}
|
|
15
|
+
postgres:
|
|
16
|
+
image: postgres:16-alpine
|
|
17
|
+
environment:
|
|
18
|
+
POSTGRES_DB: {{ package_name }}_db
|
|
19
|
+
POSTGRES_USER: postgres
|
|
20
|
+
POSTGRES_PASSWORD: postgres
|
|
21
|
+
ports:
|
|
22
|
+
- "5432:5432"
|
|
23
|
+
volumes:
|
|
24
|
+
- pg_data:/var/lib/postgresql/data
|
|
25
|
+
restart: unless-stopped
|
|
26
|
+
{% endif %}
|
|
27
|
+
|
|
28
|
+
{% if redis %}
|
|
29
|
+
redis:
|
|
30
|
+
image: redis:7-alpine
|
|
31
|
+
ports:
|
|
32
|
+
- "6379:6379"
|
|
33
|
+
restart: unless-stopped
|
|
34
|
+
{% endif %}
|
|
35
|
+
|
|
36
|
+
{% if postgres %}
|
|
37
|
+
volumes:
|
|
38
|
+
pg_data:
|
|
39
|
+
{% endif %}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "{{ project_name }}"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A FastAPI backend project"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
dependencies = []
|
|
7
|
+
|
|
8
|
+
[build-system]
|
|
9
|
+
requires = ["uv_build>=0.11.2,<0.12"]
|
|
10
|
+
build-backend = "uv_build"
|
|
11
|
+
|
|
12
|
+
[tool.uv.build-backend]
|
|
13
|
+
module-name = "app"
|
|
14
|
+
module-root = ""
|
|
15
|
+
|
|
16
|
+
[tool.ruff]
|
|
17
|
+
line-length = 100
|
|
18
|
+
target-version = "py310"
|
|
19
|
+
select = ["E", "F", "I", "N", "UP"]
|
|
20
|
+
|
|
21
|
+
[tool.mypy]
|
|
22
|
+
python_version = "3.10"
|
|
23
|
+
strict = true
|
|
24
|
+
ignore_missing_imports = true
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
testpaths = ["tests"]
|
|
28
|
+
asyncio_mode = "auto"
|
|
29
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Test suite."""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pytest configuration and shared fixtures.
|
|
3
|
+
"""
|
|
4
|
+
import pytest_asyncio
|
|
5
|
+
from httpx import ASGITransport, AsyncClient
|
|
6
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
7
|
+
from sqlalchemy.orm import sessionmaker
|
|
8
|
+
from sqlmodel import SQLModel
|
|
9
|
+
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
10
|
+
|
|
11
|
+
from app.db.session import get_session
|
|
12
|
+
from app.main import app
|
|
13
|
+
|
|
14
|
+
# Use an in-memory SQLite DB for tests
|
|
15
|
+
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest_asyncio.fixture(scope="session")
|
|
19
|
+
async def test_engine():
|
|
20
|
+
engine = create_async_engine(TEST_DATABASE_URL, echo=False)
|
|
21
|
+
async with engine.begin() as conn:
|
|
22
|
+
await conn.run_sync(SQLModel.metadata.create_all)
|
|
23
|
+
yield engine
|
|
24
|
+
await engine.dispose()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest_asyncio.fixture
|
|
28
|
+
async def db_session(test_engine):
|
|
29
|
+
AsyncTestSession = sessionmaker(
|
|
30
|
+
bind=test_engine, class_=AsyncSession, expire_on_commit=False
|
|
31
|
+
)
|
|
32
|
+
async with AsyncTestSession() as session:
|
|
33
|
+
yield session
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest_asyncio.fixture
|
|
37
|
+
async def client(db_session):
|
|
38
|
+
async def override_get_session():
|
|
39
|
+
yield db_session
|
|
40
|
+
|
|
41
|
+
app.dependency_overrides[get_session] = override_get_session
|
|
42
|
+
async with AsyncClient(
|
|
43
|
+
transport=ASGITransport(app=app), base_url="http://test"
|
|
44
|
+
) as ac:
|
|
45
|
+
yield ac
|
|
46
|
+
app.dependency_overrides.clear()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Tests for the health-check endpoint."""
|
|
2
|
+
import pytest
|
|
3
|
+
from httpx import AsyncClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.mark.asyncio
|
|
7
|
+
async def test_health_ok(client: AsyncClient) -> None:
|
|
8
|
+
response = await client.get("/api/v1/health")
|
|
9
|
+
assert response.status_code == 200
|
|
10
|
+
data = response.json()
|
|
11
|
+
assert data["status"] == "ok"
|
|
12
|
+
assert "timestamp" in data
|
|
13
|
+
assert "version" in data
|