---
# ============================================================================
# FastAPI Complete Stack Generator
# ============================================================================
# This template generates a complete FastAPI application from RDF domain model
#
# Input: domain.ttl (RDF ontology defining models, fields, routes)
# Output: Complete FastAPI project with models, routes, schemas, tests
#
# Usage: ggen template generate api-stack.tmpl --var project_name=my_api
# ============================================================================

rdf: domain.ttl

sparql:
  # Get all models with their metadata
  models: |
    PREFIX api: <http://api.example.org/schema#>
    PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
    SELECT ?model ?label ?tableName ?routePrefix ?description
    WHERE {
      ?model a api:Model ;
             rdfs:label ?label ;
             api:tableName ?tableName ;
             api:routePrefix ?routePrefix ;
             api:description ?description .
    }
    ORDER BY ?label

  # Get fields for each model
  fields: |
    PREFIX api: <http://api.example.org/schema#>
    PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
    SELECT ?model ?fieldName ?pythonType ?isRequired ?isPrimaryKey ?isForeignKey
           ?isSecret ?defaultValue ?minLength ?maxLength ?minValue ?maxValue
           ?pattern ?validation ?description
    WHERE {
      ?model a api:Model ;
             api:hasField ?field .
      ?field api:fieldName ?fieldName ;
             api:pythonType ?pythonType ;
             api:isRequired ?isRequired ;
             api:description ?description .
      OPTIONAL { ?field api:isPrimaryKey ?isPrimaryKey }
      OPTIONAL { ?field api:isForeignKey ?isForeignKey }
      OPTIONAL { ?field api:isSecret ?isSecret }
      OPTIONAL { ?field api:defaultValue ?defaultValue }
      OPTIONAL { ?field api:minLength ?minLength }
      OPTIONAL { ?field api:maxLength ?maxLength }
      OPTIONAL { ?field api:minValue ?minValue }
      OPTIONAL { ?field api:maxValue ?maxValue }
      OPTIONAL { ?field api:pattern ?pattern }
      OPTIONAL { ?field api:validation ?validation }
    }
    ORDER BY ?model ?fieldName

  # Get relationships
  relationships: |
    PREFIX api: <http://api.example.org/schema#>
    PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
    SELECT ?model ?relName ?relType ?targetModel ?targetLabel ?foreignKey ?joinTable ?description
    WHERE {
      ?model a api:Model ;
             api:hasRelationship ?rel .
      ?rel api:relationshipName ?relName ;
           api:relationType ?relType ;
           api:targetModel ?targetModel ;
           api:description ?description .
      ?targetModel rdfs:label ?targetLabel .
      OPTIONAL { ?rel api:foreignKey ?foreignKey }
      OPTIONAL { ?rel api:joinTable ?joinTable }
    }
    ORDER BY ?model ?relName

  # Get routes for each model
  routes: |
    PREFIX api: <http://api.example.org/schema#>
    PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
    SELECT ?model ?modelLabel ?httpMethod ?path ?operationId ?summary ?description
           ?requiresAuth ?pagination
    WHERE {
      ?model a api:Model ;
             rdfs:label ?modelLabel ;
             api:hasRoute ?route .
      ?route api:httpMethod ?httpMethod ;
             api:path ?path ;
             api:operationId ?operationId ;
             api:summary ?summary ;
             api:description ?description ;
             api:requiresAuth ?requiresAuth .
      OPTIONAL { ?route api:pagination ?pagination }
    }
    ORDER BY ?model ?httpMethod ?path

vars:
  project_name: "my_api"
  python_version: "3.11"
  fastapi_version: "0.104.1"

---
{# ============================================================================ #}
{# REQUIREMENTS.TXT #}
{# ============================================================================ #}
{# FILE: requirements.txt #}
# {{ project_name }} - FastAPI Application Dependencies
# Generated from RDF domain model

# Core Framework
fastapi=={{ fastapi_version }}
uvicorn[standard]==0.24.0
pydantic[email]==2.5.0
pydantic-settings==2.1.0

# Database
sqlalchemy==2.0.23
alembic==1.12.1
asyncpg==0.29.0

# Authentication & Security
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.6

# Utilities
python-dotenv==1.0.0
httpx==0.25.2

# Development
pytest==7.4.3
pytest-asyncio==0.21.1
pytest-cov==4.1.0
black==23.12.0
ruff==0.1.8

{# ============================================================================ #}
{# MAIN APPLICATION #}
{# ============================================================================ #}
{# FILE: main.py #}
"""
{{ project_name }} - FastAPI Application
Generated from RDF domain model
"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager

from api.routes import users, products, orders
from core.config import settings
from core.database import engine, Base


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Application lifespan events"""
    # Startup
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    yield
    # Shutdown
    await engine.dispose()


app = FastAPI(
    title="{{ project_name }}",
    description="E-commerce API generated from RDF domain model",
    version="1.0.0",
    lifespan=lifespan,
)

# CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.ALLOWED_ORIGINS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Include routers
app.include_router(users.router, prefix="/api/v1/users", tags=["users"])
app.include_router(products.router, prefix="/api/v1/products", tags=["products"])
app.include_router(orders.router, prefix="/api/v1/orders", tags=["orders"])


@app.get("/")
async def root():
    """Root endpoint"""
    return {
        "message": "Welcome to {{ project_name }}",
        "version": "1.0.0",
        "docs": "/docs",
    }


@app.get("/health")
async def health():
    """Health check endpoint"""
    return {"status": "healthy"}


{# ============================================================================ #}
{# CONFIGURATION #}
{# ============================================================================ #}
{# FILE: core/config.py #}
"""Application configuration"""
from pydantic_settings import BaseSettings
from typing import List


class Settings(BaseSettings):
    """Application settings"""

    # Application
    APP_NAME: str = "{{ project_name }}"
    DEBUG: bool = False

    # Database
    DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/{{ project_name }}"

    # Security
    SECRET_KEY: str = "change-me-in-production"
    ALGORITHM: str = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30

    # CORS
    ALLOWED_ORIGINS: List[str] = ["http://localhost:3000", "http://localhost:8000"]

    class Config:
        env_file = ".env"


settings = Settings()

{# FILE: core/__init__.py #}
"""Core application modules"""

{# ============================================================================ #}
{# DATABASE CONFIGURATION #}
{# ============================================================================ #}
{# FILE: core/database.py #}
"""Database configuration and session management"""
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import declarative_base

from core.config import settings

# Create async engine
engine = create_async_engine(
    settings.DATABASE_URL,
    echo=settings.DEBUG,
    future=True,
)

# Create async session maker
AsyncSessionLocal = async_sessionmaker(
    engine,
    class_=AsyncSession,
    expire_on_commit=False,
)

# Create declarative base
Base = declarative_base()


async def get_db():
    """Dependency to get database session"""
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

{# ============================================================================ #}
{# GENERATE MODELS #}
{# ============================================================================ #}
{% for model in query('models') %}
{# FILE: models/{{ model.label | lower }}.py #}
"""{{ model.description }}"""
from sqlalchemy import Column, String, Integer, Boolean, DateTime, Numeric, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID
from datetime import datetime
import uuid

from core.database import Base


class {{ model.label }}(Base):
    """{{ model.description }}"""
    __tablename__ = "{{ model.tableName }}"

    # Fields
{% for field in query('fields') if field.model == model.model %}
{% if field.isPrimaryKey %}
    {{ field.fieldName }} = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
{% elif field.isForeignKey %}
    {{ field.fieldName }} = Column(UUID(as_uuid=True), ForeignKey("{{ field.fieldName.replace('_id', '') }}s.id"), nullable={{ 'False' if field.isRequired else 'True' }})
{% elif field.pythonType == 'str' %}
    {{ field.fieldName }} = Column(String({{ field.maxLength if field.maxLength else 255 }}), nullable={{ 'False' if field.isRequired else 'True' }}{% if field.defaultValue %}, default={{ field.defaultValue }}{% endif %})
{% elif field.pythonType == 'int' %}
    {{ field.fieldName }} = Column(Integer, nullable={{ 'False' if field.isRequired else 'True' }}{% if field.defaultValue %}, default={{ field.defaultValue }}{% endif %})
{% elif field.pythonType == 'bool' %}
    {{ field.fieldName }} = Column(Boolean, nullable={{ 'False' if field.isRequired else 'True' }}, default={{ field.defaultValue if field.defaultValue else 'True' }})
{% elif field.pythonType == 'datetime' %}
    {{ field.fieldName }} = Column(DateTime, nullable={{ 'False' if field.isRequired else 'True' }}, default=datetime.utcnow)
{% elif field.pythonType == 'Decimal' %}
    {{ field.fieldName }} = Column(Numeric(precision=10, scale=2), nullable={{ 'False' if field.isRequired else 'True' }})
{% endif %}
{% endfor %}

    # Relationships
{% for rel in query('relationships') if rel.model == model.model %}
{% if rel.relType == 'oneToMany' %}
    {{ rel.relName }} = relationship("{{ rel.targetLabel }}", back_populates="{{ model.label | lower }}")
{% elif rel.relType == 'manyToOne' %}
    {{ rel.relName }} = relationship("{{ rel.targetLabel }}", back_populates="{{ rel.relName }}s")
{% elif rel.relType == 'manyToMany' %}
    {{ rel.relName }} = relationship("{{ rel.targetLabel }}", secondary="{{ rel.joinTable }}", back_populates="{{ model.label | lower }}s")
{% endif %}
{% endfor %}

    def __repr__(self):
        return f"<{{ model.label }}(id={self.id})>"

{% endfor %}

{# FILE: models/__init__.py #}
"""Database models"""
{% for model in query('models') %}
from .{{ model.label | lower }} import {{ model.label }}
{% endfor %}

__all__ = [
{% for model in query('models') %}
    "{{ model.label }}",
{% endfor %}
]

{# ============================================================================ #}
{# GENERATE SCHEMAS #}
{# ============================================================================ #}
{% for model in query('models') %}
{# FILE: schemas/{{ model.label | lower }}.py #}
"""{{ model.label }} Pydantic schemas"""
from pydantic import BaseModel, EmailStr, Field, ConfigDict
from typing import Optional, List
from datetime import datetime
from decimal import Decimal
from uuid import UUID


class {{ model.label }}Base(BaseModel):
    """Base {{ model.label }} schema"""
{% for field in query('fields') if field.model == model.model and not field.isPrimaryKey and not field.isSecret %}
{% if field.pythonType == 'EmailStr' %}
    {{ field.fieldName }}: {{ 'EmailStr' if field.isRequired else 'Optional[EmailStr] = None' }}
{% elif field.pythonType == 'str' %}
    {{ field.fieldName }}: {{ 'str' if field.isRequired else 'Optional[str] = None' }} = Field({% if field.minLength %}min_length={{ field.minLength }}, {% endif %}{% if field.maxLength %}max_length={{ field.maxLength }}, {% endif %}description="{{ field.description }}")
{% elif field.pythonType == 'int' %}
    {{ field.fieldName }}: {{ 'int' if field.isRequired else 'Optional[int] = None' }} = Field({% if field.minValue %}ge={{ field.minValue }}, {% endif %}description="{{ field.description }}")
{% elif field.pythonType == 'bool' %}
    {{ field.fieldName }}: {{ 'bool' if field.isRequired else 'Optional[bool] = None' }} = Field(description="{{ field.description }}")
{% elif field.pythonType == 'datetime' %}
    {{ field.fieldName }}: {{ 'datetime' if field.isRequired else 'Optional[datetime] = None' }} = Field(description="{{ field.description }}")
{% elif field.pythonType == 'Decimal' %}
    {{ field.fieldName }}: {{ 'Decimal' if field.isRequired else 'Optional[Decimal] = None' }} = Field({% if field.minValue %}ge={{ field.minValue }}, {% endif %}description="{{ field.description }}")
{% endif %}
{% endfor %}


class {{ model.label }}Create({{ model.label }}Base):
    """Schema for creating {{ model.label }}"""
{% for field in query('fields') if field.model == model.model and field.isSecret %}
    {{ field.fieldName }}: str = Field(min_length={{ field.minLength if field.minLength else 8 }}, description="{{ field.description }}")
{% endfor %}
    pass


class {{ model.label }}Update(BaseModel):
    """Schema for updating {{ model.label }}"""
{% for field in query('fields') if field.model == model.model and not field.isPrimaryKey %}
{% if field.pythonType == 'EmailStr' %}
    {{ field.fieldName }}: Optional[EmailStr] = None
{% elif field.pythonType == 'str' %}
    {{ field.fieldName }}: Optional[str] = Field(None, {% if field.minLength %}min_length={{ field.minLength }}, {% endif %}{% if field.maxLength %}max_length={{ field.maxLength }}, {% endif %}description="{{ field.description }}")
{% elif field.pythonType == 'int' %}
    {{ field.fieldName }}: Optional[int] = Field(None, {% if field.minValue %}ge={{ field.minValue }}, {% endif %}description="{{ field.description }}")
{% elif field.pythonType == 'bool' %}
    {{ field.fieldName }}: Optional[bool] = None
{% elif field.pythonType == 'datetime' %}
    {{ field.fieldName }}: Optional[datetime] = None
{% elif field.pythonType == 'Decimal' %}
    {{ field.fieldName }}: Optional[Decimal] = None
{% endif %}
{% endfor %}


class {{ model.label }}InDB({{ model.label }}Base):
    """Schema for {{ model.label }} from database"""
    id: UUID
{% for field in query('fields') if field.model == model.model and field.isPrimaryKey == false and field.isRequired %}
{% if field.pythonType == 'datetime' %}
    {{ field.fieldName }}: datetime
{% endif %}
{% endfor %}

    model_config = ConfigDict(from_attributes=True)


class {{ model.label }}Response({{ model.label }}InDB):
    """Schema for {{ model.label }} API response"""
    pass

{% endfor %}

{# FILE: schemas/__init__.py #}
"""Pydantic schemas"""
{% for model in query('models') %}
from .{{ model.label | lower }} import (
    {{ model.label }}Base,
    {{ model.label }}Create,
    {{ model.label }}Update,
    {{ model.label }}InDB,
    {{ model.label }}Response,
)
{% endfor %}

{# ============================================================================ #}
{# GENERATE ROUTES #}
{# ============================================================================ #}
{% for model in query('models') | unique(attribute='label') %}
{# FILE: api/routes/{{ model.label | lower }}s.py #}
"""{{ model.label }} API routes"""
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import List
from uuid import UUID

from core.database import get_db
from models.{{ model.label | lower }} import {{ model.label }}
from schemas.{{ model.label | lower }} import (
    {{ model.label }}Create,
    {{ model.label }}Update,
    {{ model.label }}Response,
)

router = APIRouter()


@router.get("/", response_model=List[{{ model.label }}Response])
async def list_{{ model.label | lower }}s(
    skip: int = Query(0, ge=0),
    limit: int = Query(100, ge=1, le=100),
    db: AsyncSession = Depends(get_db),
):
    """{{ model.description }} - List all"""
    result = await db.execute(
        select({{ model.label }}).offset(skip).limit(limit)
    )
    {{ model.label | lower }}s = result.scalars().all()
    return {{ model.label | lower }}s


@router.get("/{id}", response_model={{ model.label }}Response)
async def get_{{ model.label | lower }}(
    id: UUID,
    db: AsyncSession = Depends(get_db),
):
    """{{ model.description }} - Get by ID"""
    result = await db.execute(
        select({{ model.label }}).where({{ model.label }}.id == id)
    )
    {{ model.label | lower }} = result.scalar_one_or_none()

    if not {{ model.label | lower }}:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="{{ model.label }} not found",
        )

    return {{ model.label | lower }}


@router.post("/", response_model={{ model.label }}Response, status_code=status.HTTP_201_CREATED)
async def create_{{ model.label | lower }}(
    {{ model.label | lower }}_in: {{ model.label }}Create,
    db: AsyncSession = Depends(get_db),
):
    """{{ model.description }} - Create new"""
    {{ model.label | lower }} = {{ model.label }}(**{{ model.label | lower }}_in.model_dump())
    db.add({{ model.label | lower }})
    await db.commit()
    await db.refresh({{ model.label | lower }})
    return {{ model.label | lower }}


@router.put("/{id}", response_model={{ model.label }}Response)
async def update_{{ model.label | lower }}(
    id: UUID,
    {{ model.label | lower }}_in: {{ model.label }}Update,
    db: AsyncSession = Depends(get_db),
):
    """{{ model.label }} - Update existing"""
    result = await db.execute(
        select({{ model.label }}).where({{ model.label }}.id == id)
    )
    {{ model.label | lower }} = result.scalar_one_or_none()

    if not {{ model.label | lower }}:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="{{ model.label }} not found",
        )

    update_data = {{ model.label | lower }}_in.model_dump(exclude_unset=True)
    for field, value in update_data.items():
        setattr({{ model.label | lower }}, field, value)

    await db.commit()
    await db.refresh({{ model.label | lower }})
    return {{ model.label | lower }}

{% if model.label != 'Order' %}
@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_{{ model.label | lower }}(
    id: UUID,
    db: AsyncSession = Depends(get_db),
):
    """{{ model.label }} - Delete"""
    result = await db.execute(
        select({{ model.label }}).where({{ model.label }}.id == id)
    )
    {{ model.label | lower }} = result.scalar_one_or_none()

    if not {{ model.label | lower }}:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="{{ model.label }} not found",
        )

    await db.delete({{ model.label | lower }})
    await db.commit()
{% endif %}

{% endfor %}

{# FILE: api/__init__.py #}
"""API routes"""

{# FILE: api/routes/__init__.py #}
"""API route modules"""

{# ============================================================================ #}
{# GENERATE TESTS #}
{# ============================================================================ #}
{% for model in query('models') %}
{# FILE: tests/test_{{ model.label | lower }}s.py #}
"""Tests for {{ model.label }} endpoints"""
import pytest
from httpx import AsyncClient
from uuid import uuid4


@pytest.mark.asyncio
async def test_create_{{ model.label | lower }}(client: AsyncClient):
    """Test creating a {{ model.label | lower }}"""
    {{ model.label | lower }}_data = {
{% for field in query('fields') if field.model == model.model and not field.isPrimaryKey and field.isRequired and not field.defaultValue %}
{% if field.pythonType == 'EmailStr' %}
        "{{ field.fieldName }}": "test@example.com",
{% elif field.pythonType == 'str' and field.fieldName == 'username' %}
        "{{ field.fieldName }}": "testuser",
{% elif field.pythonType == 'str' and field.fieldName == 'password' %}
        "{{ field.fieldName }}": "securepass123",
{% elif field.pythonType == 'str' and field.fieldName == 'name' %}
        "{{ field.fieldName }}": "Test {{ model.label }}",
{% elif field.pythonType == 'str' and field.fieldName == 'category' %}
        "{{ field.fieldName }}": "electronics",
{% elif field.pythonType == 'str' and field.fieldName == 'status' %}
        "{{ field.fieldName }}": "pending",
{% elif field.pythonType == 'Decimal' and field.fieldName == 'price' %}
        "{{ field.fieldName }}": "99.99",
{% elif field.pythonType == 'Decimal' and field.fieldName == 'total' %}
        "{{ field.fieldName }}": "199.99",
{% elif field.pythonType == 'int' and field.fieldName == 'stock' %}
        "{{ field.fieldName }}": 100,
{% elif field.isForeignKey %}
        "{{ field.fieldName }}": str(uuid4()),
{% endif %}
{% endfor %}
    }

    response = await client.post("/api/v1/{{ model.label | lower }}s/", json={{ model.label | lower }}_data)
    assert response.status_code == 201
    data = response.json()
    assert "id" in data
{% for field in query('fields') if field.model == model.model and not field.isPrimaryKey and field.isRequired and not field.defaultValue and not field.isSecret %}
    assert "{{ field.fieldName }}" in data
{% endfor %}


@pytest.mark.asyncio
async def test_list_{{ model.label | lower }}s(client: AsyncClient):
    """Test listing {{ model.label | lower }}s"""
    response = await client.get("/api/v1/{{ model.label | lower }}s/")
    assert response.status_code == 200
    data = response.json()
    assert isinstance(data, list)


@pytest.mark.asyncio
async def test_get_{{ model.label | lower }}(client: AsyncClient, sample_{{ model.label | lower }}):
    """Test getting a {{ model.label | lower }} by ID"""
    response = await client.get(f"/api/v1/{{ model.label | lower }}s/{sample_{{ model.label | lower }}['id']}")
    assert response.status_code == 200
    data = response.json()
    assert data["id"] == sample_{{ model.label | lower }}["id"]


@pytest.mark.asyncio
async def test_get_{{ model.label | lower }}_not_found(client: AsyncClient):
    """Test getting non-existent {{ model.label | lower }}"""
    fake_id = str(uuid4())
    response = await client.get(f"/api/v1/{{ model.label | lower }}s/{fake_id}")
    assert response.status_code == 404


@pytest.mark.asyncio
async def test_update_{{ model.label | lower }}(client: AsyncClient, sample_{{ model.label | lower }}):
    """Test updating a {{ model.label | lower }}"""
    update_data = {
{% for field in query('fields') if field.model == model.model and not field.isPrimaryKey and not field.isForeignKey and field.pythonType == 'str' and not field.isSecret %}
        "{{ field.fieldName }}": "Updated {{ field.fieldName }}",
{% endfor %}
    }

    response = await client.put(
        f"/api/v1/{{ model.label | lower }}s/{sample_{{ model.label | lower }}['id']}",
        json=update_data,
    )
    assert response.status_code == 200
    data = response.json()
{% for field in query('fields') if field.model == model.model and not field.isPrimaryKey and not field.isForeignKey and field.pythonType == 'str' and not field.isSecret %}
    assert data["{{ field.fieldName }}"] == "Updated {{ field.fieldName }}"
{% endfor %}

{% if model.label != 'Order' %}
@pytest.mark.asyncio
async def test_delete_{{ model.label | lower }}(client: AsyncClient, sample_{{ model.label | lower }}):
    """Test deleting a {{ model.label | lower }}"""
    response = await client.delete(f"/api/v1/{{ model.label | lower }}s/{sample_{{ model.label | lower }}['id']}")
    assert response.status_code == 204

    # Verify deletion
    response = await client.get(f"/api/v1/{{ model.label | lower }}s/{sample_{{ model.label | lower }}['id']}")
    assert response.status_code == 404
{% endif %}

{% endfor %}

{# FILE: tests/conftest.py #}
"""Test configuration and fixtures"""
import pytest
import asyncio
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker

from main import app
from core.database import Base, get_db
from uuid import uuid4


# Test database URL
TEST_DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/test_{{ project_name }}"

# Create test engine
test_engine = create_async_engine(TEST_DATABASE_URL, echo=True)
TestSessionLocal = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)


@pytest.fixture(scope="session")
def event_loop():
    """Create event loop for async tests"""
    loop = asyncio.get_event_loop_policy().new_event_loop()
    yield loop
    loop.close()


@pytest.fixture(scope="function")
async def db_session():
    """Create test database session"""
    async with test_engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
        await conn.run_sync(Base.metadata.create_all)

    async with TestSessionLocal() as session:
        yield session
        await session.close()


@pytest.fixture(scope="function")
async def client(db_session):
    """Create test client with test database"""
    async def override_get_db():
        yield db_session

    app.dependency_overrides[get_db] = override_get_db

    async with AsyncClient(app=app, base_url="http://test") as ac:
        yield ac

    app.dependency_overrides.clear()


{% for model in query('models') %}
@pytest.fixture
async def sample_{{ model.label | lower }}(client: AsyncClient):
    """Create sample {{ model.label | lower }} for testing"""
    {{ model.label | lower }}_data = {
{% for field in query('fields') if field.model == model.model and not field.isPrimaryKey and field.isRequired and not field.defaultValue %}
{% if field.pythonType == 'EmailStr' %}
        "{{ field.fieldName }}": "sample@example.com",
{% elif field.pythonType == 'str' and field.fieldName == 'username' %}
        "{{ field.fieldName }}": "sampleuser",
{% elif field.pythonType == 'str' and field.fieldName == 'password' %}
        "{{ field.fieldName }}": "password123",
{% elif field.pythonType == 'str' and field.fieldName == 'name' %}
        "{{ field.fieldName }}": "Sample {{ model.label }}",
{% elif field.pythonType == 'str' and field.fieldName == 'category' %}
        "{{ field.fieldName }}": "electronics",
{% elif field.pythonType == 'str' and field.fieldName == 'status' %}
        "{{ field.fieldName }}": "pending",
{% elif field.pythonType == 'Decimal' and field.fieldName == 'price' %}
        "{{ field.fieldName }}": "49.99",
{% elif field.pythonType == 'Decimal' and field.fieldName == 'total' %}
        "{{ field.fieldName }}": "99.99",
{% elif field.pythonType == 'int' and field.fieldName == 'stock' %}
        "{{ field.fieldName }}": 50,
{% elif field.isForeignKey %}
        "{{ field.fieldName }}": str(uuid4()),
{% endif %}
{% endfor %}
    }

    response = await client.post("/api/v1/{{ model.label | lower }}s/", json={{ model.label | lower }}_data)
    return response.json()

{% endfor %}

{# FILE: tests/__init__.py #}
"""Test suite"""

{# ============================================================================ #}
{# ENVIRONMENT AND DOCKER #}
{# ============================================================================ #}
{# FILE: .env.example #}
# {{ project_name }} Environment Configuration

# Application
APP_NAME={{ project_name }}
DEBUG=True

# Database
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/{{ project_name }}

# Security
SECRET_KEY=your-secret-key-here-change-in-production
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30

# CORS
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8000

{# FILE: Dockerfile #}
FROM python:{{ python_version }}-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application
COPY . .

# Expose port
EXPOSE 8000

# Run application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

{# FILE: docker-compose.yml #}
version: '3.8'

services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/{{ project_name }}
    depends_on:
      - db
    volumes:
      - .:/app
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB={{ project_name }}
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

{# FILE: .gitignore #}
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Virtual environments
venv/
ENV/
env/

# IDE
.vscode/
.idea/
*.swp
*.swo

# Environment
.env
.env.local

# Database
*.db
*.sqlite3

# Testing
.pytest_cache/
.coverage
htmlcov/

{# FILE: README.md #}
# {{ project_name }}

E-commerce API generated from RDF domain model using ggen template engine.

## Features

- ✅ **Complete FastAPI application** with async support
- ✅ **Three models**: User, Product, Order
- ✅ **Full CRUD operations** for all models
- ✅ **Pydantic schemas** with validation
- ✅ **SQLAlchemy async models** with relationships
- ✅ **Comprehensive test suite** with pytest
- ✅ **Docker support** for easy deployment
- ✅ **OpenAPI documentation** at `/docs`

## Generated from RDF

This entire application was generated from a semantic domain model (`domain.ttl`) using a single template (`api-stack.tmpl`).

**Key insight**: The RDF defines the "what" (models, fields, relationships, routes), and the template defines the "how" (FastAPI, SQLAlchemy, Pydantic patterns).

## Quick Start

### Using Docker (Recommended)

```bash
# Start the application
docker-compose up

# Access the API at http://localhost:8000
# View docs at http://localhost:8000/docs
```

### Local Development

```bash
# Install dependencies
pip install -r requirements.txt

# Set up database
createdb {{ project_name }}

# Copy environment file
cp .env.example .env

# Run migrations (if using Alembic)
alembic upgrade head

# Start the server
uvicorn main:app --reload

# Run tests
pytest
```

## API Endpoints

### Users (`/api/v1/users`)
- `GET /` - List all users
- `GET /{id}` - Get user by ID
- `POST /` - Create new user
- `PUT /{id}` - Update user
- `DELETE /{id}` - Delete user

### Products (`/api/v1/products`)
- `GET /` - List all products
- `GET /{id}` - Get product by ID
- `POST /` - Create new product
- `PUT /{id}` - Update product
- `DELETE /{id}` - Delete product

### Orders (`/api/v1/orders`)
- `GET /` - List all orders
- `GET /{id}` - Get order by ID
- `POST /` - Create new order
- `PUT /{id}` - Update order status

## Project Structure

```
{{ project_name }}/
├── api/
│   └── routes/          # API route handlers
│       ├── users.py
│       ├── products.py
│       └── orders.py
├── core/                # Core configuration
│   ├── config.py        # Application settings
│   └── database.py      # Database setup
├── models/              # SQLAlchemy models
│   ├── user.py
│   ├── product.py
│   └── order.py
├── schemas/             # Pydantic schemas
│   ├── user.py
│   ├── product.py
│   └── order.py
├── tests/               # Test suite
│   ├── conftest.py
│   ├── test_users.py
│   ├── test_products.py
│   └── test_orders.py
├── main.py              # Application entry point
├── requirements.txt     # Python dependencies
└── docker-compose.yml   # Docker configuration
```

## Testing

```bash
# Run all tests
pytest

# Run with coverage
pytest --cov=. --cov-report=html

# Run specific test file
pytest tests/test_users.py
```

## The RDF → Code Pattern

This example demonstrates:

1. **Semantic Domain Modeling**: Define your domain in RDF/OWL
2. **SPARQL Queries**: Extract structured data from the ontology
3. **Template Generation**: Transform semantic data into code
4. **Multi-File Output**: Use `{# FILE: path #}` markers to split output

### Example: From RDF to Model

**RDF Definition**:
```turtle
:User a api:Model ;
    api:hasField :User_email .

:User_email a api:Field ;
    api:fieldName "email" ;
    api:pythonType "EmailStr" ;
    api:isRequired true ;
    api:validation "email" .
```

**SPARQL Query**:
```sparql
SELECT ?model ?fieldName ?pythonType ?isRequired
WHERE {
    ?model api:hasField ?field .
    ?field api:fieldName ?fieldName ;
           api:pythonType ?pythonType ;
           api:isRequired ?isRequired .
}
```

**Generated Code**:
```python
class User(Base):
    email = Column(String(255), nullable=False)
```

## License

MIT

## Generated By

ggen template engine - RDF-driven code generation
