From 88fa04c15b6e0ca3198080ae0f591a901d0a76a4 Mon Sep 17 00:00:00 2001 From: Utkarsh Mishra Date: Mon, 25 Aug 2025 16:12:03 +0200 Subject: [PATCH] feat: scaffold fastapi backend --- fastapi-llm-cookiecutter/.env.example | 35 ++ .../.github/workflows/ci.yml | 25 ++ fastapi-llm-cookiecutter/.gitignore | 27 ++ .../.pre-commit-config.yaml | 15 + fastapi-llm-cookiecutter/Makefile | 32 ++ fastapi-llm-cookiecutter/README.md | 25 ++ fastapi-llm-cookiecutter/app/__init__.py | 0 fastapi-llm-cookiecutter/app/main.py | 13 + fastapi-llm-cookiecutter/docker-compose.yml | 43 ++ .../docker/Dockerfile.api | 10 + .../docker/Dockerfile.worker | 10 + fastapi-llm-cookiecutter/pyproject.toml | 63 +++ fastapi-llm-cookiecutter/tests/test_health.py | 22 + fastapi-llm-cookiecutter/uv.lock | 415 ++++++++++++++++++ 14 files changed, 735 insertions(+) create mode 100644 fastapi-llm-cookiecutter/.env.example create mode 100644 fastapi-llm-cookiecutter/.github/workflows/ci.yml create mode 100644 fastapi-llm-cookiecutter/.gitignore create mode 100644 fastapi-llm-cookiecutter/.pre-commit-config.yaml create mode 100644 fastapi-llm-cookiecutter/Makefile create mode 100644 fastapi-llm-cookiecutter/README.md create mode 100644 fastapi-llm-cookiecutter/app/__init__.py create mode 100644 fastapi-llm-cookiecutter/app/main.py create mode 100644 fastapi-llm-cookiecutter/docker-compose.yml create mode 100644 fastapi-llm-cookiecutter/docker/Dockerfile.api create mode 100644 fastapi-llm-cookiecutter/docker/Dockerfile.worker create mode 100644 fastapi-llm-cookiecutter/pyproject.toml create mode 100644 fastapi-llm-cookiecutter/tests/test_health.py create mode 100644 fastapi-llm-cookiecutter/uv.lock diff --git a/fastapi-llm-cookiecutter/.env.example b/fastapi-llm-cookiecutter/.env.example new file mode 100644 index 0000000..060f3ae --- /dev/null +++ b/fastapi-llm-cookiecutter/.env.example @@ -0,0 +1,35 @@ +# Azure OpenAI +AZURE_OPENAI_ENDPOINT= +AZURE_OPENAI_API_KEY= +AZURE_OPENAI_DEPLOYMENT_CHAT= +AZURE_OPENAI_DEPLOYMENT_EMBED= + +# Azure AI Search +AZURE_SEARCH_ENDPOINT= +AZURE_SEARCH_API_KEY= +AZURE_SEARCH_INDEX_NAME=docs + +# Storage +AZURE_STORAGE_ACCOUNT_URL= +AZURE_STORAGE_CONTAINER=uploads + +# Content Safety +AZURE_CONTENT_SAFETY_ENDPOINT= +AZURE_CONTENT_SAFETY_API_KEY= + +# Postgres +DB_URL=postgresql+psycopg://user:pass@postgres:5432/app + +# Redis +REDIS_URL=redis://redis:6379/0 + +# App +APP_ENV=local +APP_PORT=8000 +RATE_LIMIT=60/minute +LOG_LEVEL=INFO + +# Telemetry +OTEL_EXPORTER_OTLP_ENDPOINT= +APPINSIGHTS_CONNECTION_STRING= +SENTRY_DSN= diff --git a/fastapi-llm-cookiecutter/.github/workflows/ci.yml b/fastapi-llm-cookiecutter/.github/workflows/ci.yml new file mode 100644 index 0000000..cad3bfa --- /dev/null +++ b/fastapi-llm-cookiecutter/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + lint-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install uv + run: pip install uv + - name: Install dependencies + run: uv pip sync + - name: Run pre-commit + run: | + pip install pre-commit + pre-commit run --all-files + - name: Run tests + run: pytest diff --git a/fastapi-llm-cookiecutter/.gitignore b/fastapi-llm-cookiecutter/.gitignore new file mode 100644 index 0000000..a2b8a74 --- /dev/null +++ b/fastapi-llm-cookiecutter/.gitignore @@ -0,0 +1,27 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +*.so +*.env +.env +.env.* +!.env.example +venv/ +.venv/ +build/ +dist/ +*.egg-info/ +.cache/ +.mypy_cache/ +.pytest_cache/ +.coverage +htmlcov/ + +# IDEs +.vscode/ +.idea/ + +# Misc +.DS_Store diff --git a/fastapi-llm-cookiecutter/.pre-commit-config.yaml b/fastapi-llm-cookiecutter/.pre-commit-config.yaml new file mode 100644 index 0000000..31e6a36 --- /dev/null +++ b/fastapi-llm-cookiecutter/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.5 + hooks: + - id: ruff + - id: ruff-format + - repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.6.1 + hooks: + - id: mypy + additional_dependencies: ["pydantic>=2"] diff --git a/fastapi-llm-cookiecutter/Makefile b/fastapi-llm-cookiecutter/Makefile new file mode 100644 index 0000000..26fcb47 --- /dev/null +++ b/fastapi-llm-cookiecutter/Makefile @@ -0,0 +1,32 @@ +.PHONY: dev up down fmt lint mypy test migrate upgrade seed + +dev: +uvicorn app.main:app --reload + +up: +docker-compose up -d + +down: +docker-compose down + +fmt: +black . +ruff --fix . + +lint: +ruff . + +mypy: +mypy . + +test: +pytest + +migrate: +alembic revision --autogenerate -m "migration" + +upgrade: +alembic upgrade head + +seed: +python scripts/seed.py diff --git a/fastapi-llm-cookiecutter/README.md b/fastapi-llm-cookiecutter/README.md new file mode 100644 index 0000000..92bb7ae --- /dev/null +++ b/fastapi-llm-cookiecutter/README.md @@ -0,0 +1,25 @@ +# FastAPI LLM Cookiecutter + +A production-ready FastAPI backend scaffold configured for Azure deployments. It includes +SSE streaming chat, Retrieval-Augmented Generation, background ingestion with Celery, +and observability integrations. + +## Quickstart + +```bash +uv sync +make dev +``` + +## Features +- SSE chat endpoint backed by Azure OpenAI +- RAG with Azure AI Search +- Background ingestion via Celery and Redis +- Azure Blob Storage for file handling +- Azure Content Safety integration +- Structured logging and OpenTelemetry +- Prometheus metrics +- Rate limiting with Redis +- CI/CD to Azure Container Apps + +Refer to [docs/](docs) for operations, security, and observability guides. diff --git a/fastapi-llm-cookiecutter/app/__init__.py b/fastapi-llm-cookiecutter/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi-llm-cookiecutter/app/main.py b/fastapi-llm-cookiecutter/app/main.py new file mode 100644 index 0000000..7c66598 --- /dev/null +++ b/fastapi-llm-cookiecutter/app/main.py @@ -0,0 +1,13 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/healthz") +async def healthz() -> dict[str, str]: + return {"status": "ok"} + + +@app.get("/readyz") +async def readyz() -> dict[str, str]: + return {"status": "ready"} diff --git a/fastapi-llm-cookiecutter/docker-compose.yml b/fastapi-llm-cookiecutter/docker-compose.yml new file mode 100644 index 0000000..4506ec6 --- /dev/null +++ b/fastapi-llm-cookiecutter/docker-compose.yml @@ -0,0 +1,43 @@ +version: "3.9" +services: + api: + build: + context: . + dockerfile: docker/Dockerfile.api + env_file: .env.example + ports: + - "8000:8000" + depends_on: + - redis + - postgres + - azurite + worker: + build: + context: . + dockerfile: docker/Dockerfile.worker + env_file: .env.example + depends_on: + - redis + - postgres + - azurite + redis: + image: redis:7-alpine + ports: + - "6379:6379" + postgres: + image: postgres:15-alpine + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: pass + POSTGRES_DB: app + ports: + - "5432:5432" + azurite: + image: mcr.microsoft.com/azure-storage/azurite + command: azurite-blob --blobHost 0.0.0.0 + ports: + - "10000:10000" + prometheus: + image: prom/prometheus + ports: + - "9090:9090" diff --git a/fastapi-llm-cookiecutter/docker/Dockerfile.api b/fastapi-llm-cookiecutter/docker/Dockerfile.api new file mode 100644 index 0000000..8b52c75 --- /dev/null +++ b/fastapi-llm-cookiecutter/docker/Dockerfile.api @@ -0,0 +1,10 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY pyproject.toml uv.lock ./ +RUN pip install --no-cache-dir uv && uv pip sync + +COPY . . + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/fastapi-llm-cookiecutter/docker/Dockerfile.worker b/fastapi-llm-cookiecutter/docker/Dockerfile.worker new file mode 100644 index 0000000..67ee12d --- /dev/null +++ b/fastapi-llm-cookiecutter/docker/Dockerfile.worker @@ -0,0 +1,10 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY pyproject.toml uv.lock ./ +RUN pip install --no-cache-dir uv && uv pip sync + +COPY . . + +CMD ["celery", "-A", "workers.celery_app", "worker", "-l", "info"] diff --git a/fastapi-llm-cookiecutter/pyproject.toml b/fastapi-llm-cookiecutter/pyproject.toml new file mode 100644 index 0000000..6a3f696 --- /dev/null +++ b/fastapi-llm-cookiecutter/pyproject.toml @@ -0,0 +1,63 @@ +[project] +name = "fastapi-llm-cookiecutter" +version = "0.1.0" +description = "FastAPI backend scaffold for Azure LLM with RAG and SSE" +requires-python = ">=3.11" +authors = [{name = "Utkarsh"}] +dependencies = [ + "fastapi", + "uvicorn[standard]", + "pydantic>=2", + "pydantic-settings", + "openai>=1", + "azure-search-documents", + "azure-ai-contentsafety", + "azure-storage-blob", + "azure-identity", + "celery", + "redis", + "fastapi-limiter", + "opentelemetry-sdk", + "opentelemetry-instrumentation-fastapi", + "prometheus-client", + "sentry-sdk", + "structlog", + "sqlalchemy>=2", + "alembic", + "psycopg[binary]", + "langchain-text-splitters", + "pyjwt", + "httpx", + "python-multipart", + "unstructured", + "pypdf", +] + +[project.optional-dependencies] +dev = [ + "ruff", + "black", + "mypy", + "pre-commit", + "pytest", + "pytest-asyncio", + "respx", +] + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 88 +target-version = ["py311"] + +[tool.ruff] +line-length = 88 +target-version = "py311" +select = ["E", "F", "I", "B"] +ignore = ["E501"] + +[tool.mypy] +python_version = "3.11" +strict = true diff --git a/fastapi-llm-cookiecutter/tests/test_health.py b/fastapi-llm-cookiecutter/tests/test_health.py new file mode 100644 index 0000000..6ecf629 --- /dev/null +++ b/fastapi-llm-cookiecutter/tests/test_health.py @@ -0,0 +1,22 @@ +import pytest +from httpx import ASGITransport, AsyncClient + +from app.main import app + + +@pytest.mark.asyncio +async def test_healthz() -> None: + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as ac: + resp = await ac.get("/healthz") + assert resp.status_code == 200 + assert resp.json() == {"status": "ok"} + + +@pytest.mark.asyncio +async def test_readyz() -> None: + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as ac: + resp = await ac.get("/readyz") + assert resp.status_code == 200 + assert resp.json() == {"status": "ready"} diff --git a/fastapi-llm-cookiecutter/uv.lock b/fastapi-llm-cookiecutter/uv.lock new file mode 100644 index 0000000..121bb1c --- /dev/null +++ b/fastapi-llm-cookiecutter/uv.lock @@ -0,0 +1,415 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml -o uv.lock --extra dev +aiofiles==24.1.0 + # via unstructured-client +alembic==1.16.4 + # via fastapi-llm-cookiecutter (pyproject.toml) +amqp==5.3.1 + # via kombu +annotated-types==0.7.0 + # via pydantic +anyio==4.10.0 + # via + # httpx + # openai + # starlette + # watchfiles +asgiref==3.9.1 + # via opentelemetry-instrumentation-asgi +azure-ai-contentsafety==1.0.0 + # via fastapi-llm-cookiecutter (pyproject.toml) +azure-common==1.1.28 + # via azure-search-documents +azure-core==1.35.0 + # via + # azure-ai-contentsafety + # azure-identity + # azure-search-documents + # azure-storage-blob +azure-identity==1.24.0 + # via fastapi-llm-cookiecutter (pyproject.toml) +azure-search-documents==11.5.3 + # via fastapi-llm-cookiecutter (pyproject.toml) +azure-storage-blob==12.26.0 + # via fastapi-llm-cookiecutter (pyproject.toml) +backoff==2.2.1 + # via unstructured +beautifulsoup4==4.13.5 + # via unstructured +billiard==4.2.1 + # via celery +black==25.1.0 + # via fastapi-llm-cookiecutter (pyproject.toml) +celery==5.5.3 + # via fastapi-llm-cookiecutter (pyproject.toml) +certifi==2025.8.3 + # via + # httpcore + # httpx + # requests + # sentry-sdk +cffi==1.17.1 + # via cryptography +cfgv==3.4.0 + # via pre-commit +charset-normalizer==3.4.3 + # via + # requests + # unstructured +click==8.2.1 + # via + # black + # celery + # click-didyoumean + # click-plugins + # click-repl + # nltk + # python-oxmsg + # uvicorn +click-didyoumean==0.3.1 + # via celery +click-plugins==1.1.1.2 + # via celery +click-repl==0.3.0 + # via celery +cryptography==45.0.6 + # via + # azure-identity + # azure-storage-blob + # msal + # pyjwt + # unstructured-client +dataclasses-json==0.6.7 + # via unstructured +distlib==0.4.0 + # via virtualenv +distro==1.9.0 + # via openai +emoji==2.14.1 + # via unstructured +fastapi==0.116.1 + # via + # fastapi-llm-cookiecutter (pyproject.toml) + # fastapi-limiter +fastapi-limiter==0.1.6 + # via fastapi-llm-cookiecutter (pyproject.toml) +filelock==3.19.1 + # via virtualenv +filetype==1.2.0 + # via unstructured +greenlet==3.2.4 + # via sqlalchemy +h11==0.16.0 + # via + # httpcore + # uvicorn +html5lib==1.1 + # via unstructured +httpcore==1.0.9 + # via + # httpx + # unstructured-client +httptools==0.6.4 + # via uvicorn +httpx==0.28.1 + # via + # fastapi-llm-cookiecutter (pyproject.toml) + # langsmith + # openai + # respx + # unstructured-client +identify==2.6.13 + # via pre-commit +idna==3.10 + # via + # anyio + # httpx + # requests +importlib-metadata==8.7.0 + # via opentelemetry-api +iniconfig==2.1.0 + # via pytest +isodate==0.7.2 + # via + # azure-ai-contentsafety + # azure-search-documents + # azure-storage-blob +jiter==0.10.0 + # via openai +joblib==1.5.1 + # via nltk +jsonpatch==1.33 + # via langchain-core +jsonpointer==3.0.0 + # via jsonpatch +kombu==5.5.4 + # via celery +langchain-core==0.3.74 + # via langchain-text-splitters +langchain-text-splitters==0.3.9 + # via fastapi-llm-cookiecutter (pyproject.toml) +langdetect==1.0.9 + # via unstructured +langsmith==0.4.16 + # via langchain-core +lxml==6.0.1 + # via unstructured +mako==1.3.10 + # via alembic +markupsafe==3.0.2 + # via mako +marshmallow==3.26.1 + # via dataclasses-json +msal==1.33.0 + # via + # azure-identity + # msal-extensions +msal-extensions==1.3.1 + # via azure-identity +mypy==1.17.1 + # via fastapi-llm-cookiecutter (pyproject.toml) +mypy-extensions==1.1.0 + # via + # black + # mypy + # typing-inspect +nltk==3.9.1 + # via unstructured +nodeenv==1.9.1 + # via pre-commit +numpy==2.3.2 + # via unstructured +olefile==0.47 + # via python-oxmsg +openai==1.101.0 + # via fastapi-llm-cookiecutter (pyproject.toml) +opentelemetry-api==1.36.0 + # via + # opentelemetry-instrumentation + # opentelemetry-instrumentation-asgi + # opentelemetry-instrumentation-fastapi + # opentelemetry-sdk + # opentelemetry-semantic-conventions +opentelemetry-instrumentation==0.57b0 + # via + # opentelemetry-instrumentation-asgi + # opentelemetry-instrumentation-fastapi +opentelemetry-instrumentation-asgi==0.57b0 + # via opentelemetry-instrumentation-fastapi +opentelemetry-instrumentation-fastapi==0.57b0 + # via fastapi-llm-cookiecutter (pyproject.toml) +opentelemetry-sdk==1.36.0 + # via fastapi-llm-cookiecutter (pyproject.toml) +opentelemetry-semantic-conventions==0.57b0 + # via + # opentelemetry-instrumentation + # opentelemetry-instrumentation-asgi + # opentelemetry-instrumentation-fastapi + # opentelemetry-sdk +opentelemetry-util-http==0.57b0 + # via + # opentelemetry-instrumentation-asgi + # opentelemetry-instrumentation-fastapi +orjson==3.11.2 + # via langsmith +packaging==25.0 + # via + # black + # kombu + # langchain-core + # langsmith + # marshmallow + # opentelemetry-instrumentation + # pytest +pathspec==0.12.1 + # via + # black + # mypy +platformdirs==4.3.8 + # via + # black + # virtualenv +pluggy==1.6.0 + # via pytest +pre-commit==4.3.0 + # via fastapi-llm-cookiecutter (pyproject.toml) +prometheus-client==0.22.1 + # via fastapi-llm-cookiecutter (pyproject.toml) +prompt-toolkit==3.0.51 + # via click-repl +psutil==7.0.0 + # via unstructured +psycopg==3.2.9 + # via fastapi-llm-cookiecutter (pyproject.toml) +psycopg-binary==3.2.9 + # via psycopg +pycparser==2.22 + # via cffi +pydantic==2.11.7 + # via + # fastapi-llm-cookiecutter (pyproject.toml) + # fastapi + # langchain-core + # langsmith + # openai + # pydantic-settings + # unstructured-client +pydantic-core==2.33.2 + # via pydantic +pydantic-settings==2.10.1 + # via fastapi-llm-cookiecutter (pyproject.toml) +pygments==2.19.2 + # via pytest +pyjwt==2.10.1 + # via + # fastapi-llm-cookiecutter (pyproject.toml) + # msal +pypdf==6.0.0 + # via + # fastapi-llm-cookiecutter (pyproject.toml) + # unstructured-client +pytest==8.4.1 + # via + # fastapi-llm-cookiecutter (pyproject.toml) + # pytest-asyncio +pytest-asyncio==1.1.0 + # via fastapi-llm-cookiecutter (pyproject.toml) +python-dateutil==2.9.0.post0 + # via celery +python-dotenv==1.1.1 + # via + # pydantic-settings + # uvicorn +python-iso639==2025.2.18 + # via unstructured +python-magic==0.4.27 + # via unstructured +python-multipart==0.0.20 + # via fastapi-llm-cookiecutter (pyproject.toml) +python-oxmsg==0.0.2 + # via unstructured +pyyaml==6.0.2 + # via + # langchain-core + # pre-commit + # uvicorn +rapidfuzz==3.13.0 + # via unstructured +redis==6.4.0 + # via + # fastapi-llm-cookiecutter (pyproject.toml) + # fastapi-limiter +regex==2025.7.34 + # via nltk +requests==2.32.5 + # via + # azure-core + # langsmith + # msal + # requests-toolbelt + # unstructured +requests-toolbelt==1.0.0 + # via + # langsmith + # unstructured-client +respx==0.22.0 + # via fastapi-llm-cookiecutter (pyproject.toml) +ruff==0.12.10 + # via fastapi-llm-cookiecutter (pyproject.toml) +sentry-sdk==2.35.0 + # via fastapi-llm-cookiecutter (pyproject.toml) +six==1.17.0 + # via + # azure-core + # html5lib + # langdetect + # python-dateutil +sniffio==1.3.1 + # via + # anyio + # openai +soupsieve==2.7 + # via beautifulsoup4 +sqlalchemy==2.0.43 + # via + # fastapi-llm-cookiecutter (pyproject.toml) + # alembic +starlette==0.47.3 + # via fastapi +structlog==25.4.0 + # via fastapi-llm-cookiecutter (pyproject.toml) +tenacity==9.1.2 + # via langchain-core +tqdm==4.67.1 + # via + # nltk + # openai + # unstructured +typing-extensions==4.15.0 + # via + # alembic + # anyio + # azure-core + # azure-identity + # azure-search-documents + # azure-storage-blob + # beautifulsoup4 + # fastapi + # langchain-core + # mypy + # openai + # opentelemetry-api + # opentelemetry-sdk + # opentelemetry-semantic-conventions + # psycopg + # pydantic + # pydantic-core + # python-oxmsg + # sqlalchemy + # starlette + # typing-inspect + # typing-inspection + # unstructured +typing-inspect==0.9.0 + # via dataclasses-json +typing-inspection==0.4.1 + # via + # pydantic + # pydantic-settings +tzdata==2025.2 + # via kombu +unstructured==0.18.13 + # via fastapi-llm-cookiecutter (pyproject.toml) +unstructured-client==0.42.3 + # via unstructured +urllib3==2.5.0 + # via + # requests + # sentry-sdk +uvicorn==0.35.0 + # via fastapi-llm-cookiecutter (pyproject.toml) +uvloop==0.21.0 + # via uvicorn +vine==5.1.0 + # via + # amqp + # celery + # kombu +virtualenv==20.34.0 + # via pre-commit +watchfiles==1.1.0 + # via uvicorn +wcwidth==0.2.13 + # via prompt-toolkit +webencodings==0.5.1 + # via html5lib +websockets==15.0.1 + # via uvicorn +wrapt==1.17.3 + # via + # opentelemetry-instrumentation + # unstructured +zipp==3.23.0 + # via importlib-metadata +zstandard==0.24.0 + # via langsmith