Merge pull request #15 from fullsizemalt/claude/mvp-implementation-backend-2025-11-18

feat(backend): implement 7-MVP FastAPI backend

Complete FastAPI backend with:
- All 7 MVP models (blog, forum, merch, podcast, profiles, resources, tribute)
- SQLAlchemy ORM with PostgreSQL
- API endpoint stubs for all MVPs
- Docker & Docker Compose for production deployment
- Authentication models (RefreshToken, AuthAuditLog)
- Full requirements.txt and configuration

Ready for:
- Database migration generation
- Authentication implementation
- Production deployment to nexus-vector

Job ID: MTAD-IMPL-2025-11-18-CL
This commit is contained in:
admin 2025-11-18 00:46:55 +00:00
commit 19271a1af5
27 changed files with 1624 additions and 0 deletions

65
backend/.gitignore vendored Normal file
View file

@ -0,0 +1,65 @@
# Environment
.env
.env.local
.env.*.local
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
venv/
ENV/
env/
.venv
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
.hypothesis/
# Database
*.db
*.sqlite
*.sqlite3
# Logs
*.log
logs/
# Docker
docker-compose.override.yml
# Migrations (until created)
migrations/versions/*.py

52
backend/Dockerfile Normal file
View file

@ -0,0 +1,52 @@
# Multi-stage build for Python FastAPI backend
# Job ID: MTAD-IMPL-2025-11-18-CL
FROM python:3.11-slim as builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install dependencies
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
FROM python:3.11-slim
WORKDIR /app
# Install runtime dependencies only
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy Python packages from builder
COPY --from=builder /root/.local /root/.local
# Set environment variables
ENV PATH=/root/.local/bin:$PATH
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# Copy application code
COPY . .
# Create non-root user for security
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Expose port
EXPOSE 8000
# Run application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

350
backend/README.md Normal file
View file

@ -0,0 +1,350 @@
# MoreThanADiagnosis Backend API
FastAPI backend for the MoreThanADiagnosis community platform.
**Job ID**: MTAD-IMPL-2025-11-18-CL
## 🏗 Architecture
- **Framework**: FastAPI with Python 3.11
- **Database**: PostgreSQL with SQLAlchemy ORM
- **Cache**: Redis
- **Deployment**: Docker + Docker Compose on nexus-vector
## 📦 Project Structure
```
backend/
├── app/
│ ├── api/v1/ # API endpoints for 7 MVPs
│ │ ├── blog.py
│ │ ├── forum.py
│ │ ├── merch.py
│ │ ├── podcast.py
│ │ ├── profiles.py
│ │ ├── resources.py
│ │ ├── tribute.py
│ │ └── health.py
│ ├── models/ # SQLAlchemy models (7 MVPs + auth)
│ ├── schemas/ # Pydantic schemas (TODO)
│ ├── services/ # Business logic (TODO)
│ ├── middleware/ # Custom middleware (TODO)
│ ├── utils/ # Utilities (TODO)
│ ├── config.py # Configuration
│ ├── database.py # Database setup
│ └── main.py # FastAPI app entry
├── migrations/ # Alembic migrations (TODO)
├── tests/ # Unit and integration tests (TODO)
├── Dockerfile # Container image
├── docker-compose.yml # Local development setup
├── requirements.txt # Python dependencies
└── .env.example # Environment variables template
```
## 🚀 Quick Start
### Prerequisites
- Docker & Docker Compose
- Python 3.11+ (for local development)
- PostgreSQL (if not using Docker)
### Local Development
1. **Set up environment**
```bash
cp .env.example .env
# Edit .env with your configuration
```
2. **Install dependencies**
```bash
pip install -r requirements.txt
```
3. **Start services**
```bash
docker-compose up -d
```
4. **Run migrations** (TODO)
```bash
alembic upgrade head
```
5. **Start API**
```bash
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
```
6. **Access API**
- API: http://localhost:8000
- Docs: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
### Production Deployment (nexus-vector)
1. **Copy to nexus-vector**
```bash
scp -r backend/ admin@nexus-vector:/srv/containers/mtad-backend/
```
2. **Set production environment**
```bash
ssh admin@nexus-vector
cd /srv/containers/mtad-backend
cp .env.example .env
# Edit .env with production secrets
```
3. **Deploy with Docker Compose**
```bash
docker-compose up -d
```
4. **Verify health**
```bash
curl http://100.95.3.92:8000/health
```
## 📋 MVPs Implemented (Alphabetically)
### 1. Blog MVP
- List published blog posts
- Get individual posts by ID
- (TODO) Create, update, delete posts with authentication
### 2. Forum MVP
- List categories
- List threads per category
- List posts per thread
- (TODO) Create threads and posts with authentication
- (TODO) Emoji reactions and moderation reports
### 3. Merch MVP
- List products
- Get product details
- List orders
- (TODO) Create orders with authentication
### 4. Podcast MVP
- List episodes
- Get episode details
- (TODO) Upload and manage episodes
### 5. Profiles MVP
- Get user profile
- List public profiles
- (TODO) Update profile with authentication
### 6. Resources MVP
- List resources by access tier
- Get resources by ID or slug
- (TODO) Create resources with authorization
### 7. Tribute MVP
- List published tributes
- Get tribute details
- (TODO) Create tributes with authentication
## 🔐 Authentication & Authorization
**Status**: Specification approved (openspec/specs/authentication.md)
**TODO Implementation**:
- OAuth2/OIDC with PKCE
- JWT access tokens (15 min expiry)
- Refresh tokens with rotation (30 day expiry)
- RBAC: member, moderator, admin roles
- MFA (TOTP) support
- Password reset flow
- Account lockout protection
## 🗄 Database
**Status**: Schema specified (openspec/specs/data-model.md)
**Core Entities**:
- User (with email verification, MFA)
- Profile (display name, pseudonym, health journey)
- Forum (categories, threads, posts, reactions, reports)
- Blog (posts)
- Podcast (episodes)
- Resources (knowledge base)
- Tribute (memorials)
- Merch (products, orders)
- Consent (privacy compliance)
**Data Classification**:
- Public: Forum categories, public resources, published blog/podcast
- PII: Email, display names, shipping addresses, avatars
- PHI: Health journey, forum content (context-dependent), memorials
## 📊 API Endpoints
All endpoints use `/api/v1/` prefix.
### Blog
- `GET /api/v1/blog/` - List posts
- `GET /api/v1/blog/{post_id}` - Get post
### Forum
- `GET /api/v1/forum/categories` - List categories
- `GET /api/v1/forum/categories/{cat_id}/threads` - List threads
- `GET /api/v1/forum/threads/{thread_id}/posts` - List posts
### Merch
- `GET /api/v1/merch/products` - List products
- `GET /api/v1/merch/products/{id}` - Get product
- `GET /api/v1/merch/orders/{id}` - Get order
### Podcast
- `GET /api/v1/podcast/episodes` - List episodes
- `GET /api/v1/podcast/episodes/{id}` - Get episode
### Profiles
- `GET /api/v1/profiles/{user_id}` - Get profile
- `GET /api/v1/profiles/` - List public profiles
### Resources
- `GET /api/v1/resources/` - List resources
- `GET /api/v1/resources/{id}` - Get resource
- `GET /api/v1/resources/slug/{slug}` - Get by slug
### Tribute
- `GET /api/v1/tribute/` - List tributes
- `GET /api/v1/tribute/{id}` - Get tribute
### Health
- `GET /api/v1/health` - Health check
- `GET /api/v1/ready` - Readiness check
## 🧪 Testing
**Status**: Test infrastructure ready (pytest, pytest-asyncio)
**TODO**:
- Unit tests for models
- Integration tests for endpoints
- API contract tests
- Load testing
```bash
# Run tests
pytest tests/ -v
# Coverage report
pytest tests/ --cov=app --cov-report=html
```
## 🔄 Database Migrations
**Status**: Alembic configured, no migrations yet
```bash
# Create migration
alembic revision --autogenerate -m "Add forum tables"
# Apply migrations
alembic upgrade head
# Rollback
alembic downgrade -1
```
## 📝 Development Checklist
### Phase 1: Foundation (Current)
- [x] FastAPI project structure
- [x] Database models for all 7 MVPs
- [x] API endpoint stubs
- [x] Docker configuration
- [ ] Environment configuration finalization
- [ ] Database migrations
- [ ] Test infrastructure setup
### Phase 2: Authentication (Next)
- [ ] User registration/login
- [ ] Email verification
- [ ] Password hashing (Argon2)
- [ ] JWT token generation
- [ ] RBAC middleware
- [ ] MFA setup
### Phase 3: MVP Implementation
- [ ] Blog: Full CRUD with publishing
- [ ] Forum: Threading, moderation, reporting
- [ ] Merch: Shopping cart, checkout
- [ ] Podcast: Upload, streaming
- [ ] Profiles: User data management
- [ ] Resources: Knowledge base
- [ ] Tribute: Memorial management
### Phase 4: Integration & Deployment
- [ ] Frontend integration (Next.js web)
- [ ] Mobile integration (React Native/Expo)
- [ ] Production deployment
- [ ] Monitoring & alerting
- [ ] Performance optimization
## 🛠 Configuration
See `.env.example` for all available environment variables.
**Key Production Variables**:
```
ENV=production
DEBUG=false
DATABASE_URL=postgresql://user:pass@host:5432/db
SECRET_KEY=<generate-with-secrets>
CORS_ORIGINS=["https://morethanadiagnosis.com"]
```
## 📚 Documentation
- **OpenAPI/Swagger**: http://localhost:8000/docs
- **ReDoc**: http://localhost:8000/redoc
- **Architecture Spec**: ../../openspec/specs/architecture.md
- **Data Model**: ../../openspec/specs/data-model.md
- **Authentication Spec**: ../../openspec/specs/authentication.md
- **Design System**: ../../openspec/specs/design-system.md
## 🚨 Known Issues
- [ ] Authentication not implemented yet
- [ ] Database migrations not created yet
- [ ] Request/response schemas not defined
- [ ] Error handling needs standardization
- [ ] Logging configuration needs refinement
- [ ] Rate limiting not configured
- [ ] CORS not finalized for production
## 📦 Dependencies
See `requirements.txt` for full list. Key dependencies:
- **fastapi** - Web framework
- **sqlalchemy** - ORM
- **alembic** - Migrations
- **pydantic** - Validation
- **passlib/argon2-cffi** - Password hashing
- **python-jose** - JWT
- **pytest** - Testing
- **uvicorn** - ASGI server
## 🤝 Contributing
1. Follow OpenSpec lifecycle (propose → review → apply → archive)
2. Ensure all code links to approved specs
3. Add tests for new features
4. Update documentation
5. Follow Python/FastAPI best practices
## 📄 License
Part of MoreThanADiagnosis community platform.
---
**Status**: Foundation complete, MVP implementation ready
**Next Action**: Database migrations + authentication implementation
**Job ID**: MTAD-IMPL-2025-11-18-CL

View file

@ -0,0 +1 @@
"""API package."""

View file

@ -0,0 +1,22 @@
"""
API v1 router aggregation.
Job ID: MTAD-IMPL-2025-11-18-CL
"""
from fastapi import APIRouter
from app.api.v1 import blog, forum, merch, podcast, profiles, resources, tribute, health
api_router = APIRouter()
# Include routers for all 7 MVPs
api_router.include_router(blog.router, prefix="/blog", tags=["Blog"])
api_router.include_router(forum.router, prefix="/forum", tags=["Forum"])
api_router.include_router(merch.router, prefix="/merch", tags=["Merch"])
api_router.include_router(podcast.router, prefix="/podcast", tags=["Podcast"])
api_router.include_router(profiles.router, prefix="/profiles", tags=["Profiles"])
api_router.include_router(resources.router, prefix="/resources", tags=["Resources"])
api_router.include_router(tribute.router, prefix="/tribute", tags=["Tribute"])
api_router.include_router(health.router, tags=["Health"])
__all__ = ["api_router"]

View file

@ -0,0 +1,49 @@
"""
Blog MVP API endpoints.
Job ID: MTAD-IMPL-2025-11-18-CL
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import BlogPost
router = APIRouter()
@router.get("/")
async def list_blog_posts(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
"""List all published blog posts."""
posts = db.query(BlogPost).filter(BlogPost.published_at.isnot(None)).offset(skip).limit(limit).all()
return {"items": posts, "count": len(posts)}
@router.get("/{post_id}")
async def get_blog_post(post_id: str, db: Session = Depends(get_db)):
"""Get a specific blog post by ID."""
post = db.query(BlogPost).filter(BlogPost.id == post_id).first()
if not post:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Blog post not found")
return post
@router.post("/")
async def create_blog_post(title: str, slug: str, content: str, db: Session = Depends(get_db)):
"""Create a new blog post (requires authentication)."""
# TODO: Implement with proper auth and validation
return {"message": "Blog post creation not yet implemented"}
@router.put("/{post_id}")
async def update_blog_post(post_id: str, title: str, content: str, db: Session = Depends(get_db)):
"""Update a blog post (requires authentication and ownership)."""
# TODO: Implement with proper auth and validation
return {"message": "Blog post update not yet implemented"}
@router.delete("/{post_id}")
async def delete_blog_post(post_id: str, db: Session = Depends(get_db)):
"""Delete a blog post (requires authentication and ownership)."""
# TODO: Implement with proper auth and validation
return {"message": "Blog post deletion not yet implemented"}

View file

@ -0,0 +1,40 @@
"""Forum MVP API endpoints. Job ID: MTAD-IMPL-2025-11-18-CL"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import ForumCategory, ForumThread, ForumPost
router = APIRouter()
@router.get("/categories")
async def list_categories(db: Session = Depends(get_db)):
"""List all forum categories."""
categories = db.query(ForumCategory).order_by(ForumCategory.order).all()
return {"items": categories}
@router.get("/categories/{category_id}/threads")
async def list_threads(category_id: str, skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
"""List threads in a category."""
threads = db.query(ForumThread).filter(ForumThread.category_id == category_id).offset(skip).limit(limit).all()
return {"items": threads}
@router.get("/threads/{thread_id}/posts")
async def list_posts(thread_id: str, skip: int = 0, limit: int = 20, db: Session = Depends(get_db)):
"""List posts in a thread."""
posts = db.query(ForumPost).filter(ForumPost.thread_id == thread_id, ForumPost.deleted_at.is_(None)).offset(skip).limit(limit).all()
return {"items": posts}
@router.post("/categories")
async def create_category(name: str, db: Session = Depends(get_db)):
"""Create forum category (admin only)."""
return {"message": "Category creation not yet implemented"}
@router.post("/threads")
async def create_thread(category_id: str, title: str, db: Session = Depends(get_db)):
"""Create forum thread (requires authentication)."""
return {"message": "Thread creation not yet implemented"}
@router.post("/posts")
async def create_post(thread_id: str, content: str, db: Session = Depends(get_db)):
"""Create forum post (requires authentication)."""
return {"message": "Post creation not yet implemented"}

View file

@ -0,0 +1,14 @@
"""Health check endpoint. Job ID: MTAD-IMPL-2025-11-18-CL"""
from fastapi import APIRouter
router = APIRouter()
@router.get("/health")
async def health_check():
"""Health check for uptime monitoring."""
return {"status": "healthy", "service": "MoreThanADiagnosis API"}
@router.get("/ready")
async def readiness_check():
"""Readiness check for load balancer."""
return {"status": "ready"}

View file

@ -0,0 +1,39 @@
"""Merch MVP API endpoints. Job ID: MTAD-IMPL-2025-11-18-CL"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import MerchProduct, Order
router = APIRouter()
@router.get("/products")
async def list_products(skip: int = 0, limit: int = 20, db: Session = Depends(get_db)):
"""List all merch products."""
products = db.query(MerchProduct).offset(skip).limit(limit).all()
return {"items": products}
@router.get("/products/{product_id}")
async def get_product(product_id: str, db: Session = Depends(get_db)):
"""Get a specific product."""
product = db.query(MerchProduct).filter(MerchProduct.id == product_id).first()
if not product:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Product not found")
return product
@router.get("/orders/{order_id}")
async def get_order(order_id: str, db: Session = Depends(get_db)):
"""Get order details."""
order = db.query(Order).filter(Order.id == order_id).first()
if not order:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Order not found")
return order
@router.post("/products")
async def create_product(name: str, price: float, db: Session = Depends(get_db)):
"""Create product (admin only)."""
return {"message": "Product creation not yet implemented"}
@router.post("/orders")
async def create_order(items: list, db: Session = Depends(get_db)):
"""Create order (requires authentication)."""
return {"message": "Order creation not yet implemented"}

View file

@ -0,0 +1,27 @@
"""Podcast MVP API endpoints. Job ID: MTAD-IMPL-2025-11-18-CL"""
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import PodcastEpisode
router = APIRouter()
@router.get("/episodes")
async def list_episodes(skip: int = 0, limit: int = 20, db: Session = Depends(get_db)):
"""List all podcast episodes."""
episodes = db.query(PodcastEpisode).filter(PodcastEpisode.published_at.isnot(None)).order_by(PodcastEpisode.published_at.desc()).offset(skip).limit(limit).all()
return {"items": episodes}
@router.get("/episodes/{episode_id}")
async def get_episode(episode_id: str, db: Session = Depends(get_db)):
"""Get a specific episode."""
episode = db.query(PodcastEpisode).filter(PodcastEpisode.id == episode_id).first()
if not episode:
from fastapi import HTTPException, status
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Episode not found")
return episode
@router.post("/episodes")
async def create_episode(title: str, audio_url: str, db: Session = Depends(get_db)):
"""Create podcast episode (admin only)."""
return {"message": "Episode creation not yet implemented"}

View file

@ -0,0 +1,26 @@
"""Profiles MVP API endpoints. Job ID: MTAD-IMPL-2025-11-18-CL"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import User, Profile
router = APIRouter()
@router.get("/{user_id}")
async def get_profile(user_id: str, db: Session = Depends(get_db)):
"""Get user profile."""
profile = db.query(Profile).filter(Profile.user_id == user_id).first()
if not profile:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found")
return profile
@router.put("/{user_id}")
async def update_profile(user_id: str, display_name: str = None, bio: str = None, db: Session = Depends(get_db)):
"""Update profile (requires authentication)."""
return {"message": "Profile update not yet implemented"}
@router.get("/")
async def list_public_profiles(skip: int = 0, limit: int = 20, db: Session = Depends(get_db)):
"""List public user profiles."""
profiles = db.query(Profile).offset(skip).limit(limit).all()
return {"items": profiles}

View file

@ -0,0 +1,34 @@
"""Resources MVP API endpoints. Job ID: MTAD-IMPL-2025-11-18-CL"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import Resource
router = APIRouter()
@router.get("/")
async def list_resources(access_tier: str = "public", skip: int = 0, limit: int = 20, db: Session = Depends(get_db)):
"""List resources by access tier."""
resources = db.query(Resource).filter(Resource.access_tier == access_tier).offset(skip).limit(limit).all()
return {"items": resources}
@router.get("/{resource_id}")
async def get_resource(resource_id: str, db: Session = Depends(get_db)):
"""Get a specific resource."""
resource = db.query(Resource).filter(Resource.id == resource_id).first()
if not resource:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Resource not found")
return resource
@router.get("/slug/{slug}")
async def get_resource_by_slug(slug: str, db: Session = Depends(get_db)):
"""Get resource by slug."""
resource = db.query(Resource).filter(Resource.slug == slug).first()
if not resource:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Resource not found")
return resource
@router.post("/")
async def create_resource(title: str, slug: str, content: str, db: Session = Depends(get_db)):
"""Create resource (admin or authorized user)."""
return {"message": "Resource creation not yet implemented"}

View file

@ -0,0 +1,31 @@
"""Tribute MVP API endpoints. Job ID: MTAD-IMPL-2025-11-18-CL"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import TributeEntry
router = APIRouter()
@router.get("/")
async def list_tributes(skip: int = 0, limit: int = 20, db: Session = Depends(get_db)):
"""List published tribute entries."""
entries = db.query(TributeEntry).filter(TributeEntry.published == True).offset(skip).limit(limit).all()
return {"items": entries}
@router.get("/{tribute_id}")
async def get_tribute(tribute_id: str, db: Session = Depends(get_db)):
"""Get a specific tribute entry."""
entry = db.query(TributeEntry).filter(TributeEntry.id == tribute_id, TributeEntry.published == True).first()
if not entry:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tribute not found")
return entry
@router.post("/")
async def create_tribute(subject_name: str, memorial_text: str, db: Session = Depends(get_db)):
"""Create tribute entry (requires authentication)."""
return {"message": "Tribute creation not yet implemented"}
@router.put("/{tribute_id}")
async def update_tribute(tribute_id: str, memorial_text: str = None, db: Session = Depends(get_db)):
"""Update tribute (requires authentication and ownership)."""
return {"message": "Tribute update not yet implemented"}

86
backend/app/config.py Normal file
View file

@ -0,0 +1,86 @@
"""
Application configuration using Pydantic Settings.
Job ID: MTAD-IMPL-2025-11-18-CL
"""
from typing import List
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
"""Application settings loaded from environment variables."""
# Environment
env: str = "development"
debug: bool = False
# API
api_version: str = "v1"
api_title: str = "MoreThanADiagnosis API"
api_description: str = "Community Hub for Chronically/Terminally Ill Individuals"
# Database
database_url: str = "sqlite:///./test.db"
database_pool_size: int = 20
database_max_overflow: int = 10
# Security
secret_key: str = "change-me-in-production"
algorithm: str = "HS256"
access_token_expire_minutes: int = 15
refresh_token_expire_days: int = 30
password_hashing_algorithm: str = "argon2"
# CORS
cors_origins: List[str] = [
"https://morethanadiagnosis.com",
"http://localhost:3000",
"http://localhost:8081",
]
cors_credentials: bool = True
cors_methods: List[str] = ["*"]
cors_headers: List[str] = ["*"]
# Logging
log_level: str = "INFO"
log_format: str = "json"
# Observability
jaeger_enabled: bool = False
jaeger_host: str = "localhost"
jaeger_port: int = 6831
# Rate Limiting
rate_limit_enabled: bool = True
rate_limit_requests: int = 100
rate_limit_period: int = 60
# Redis
redis_url: str = "redis://localhost:6379/0"
# Email
smtp_server: str = "smtp.gmail.com"
smtp_port: int = 587
smtp_username: str = "noreply@morethanadiagnosis.com"
smtp_password: str = ""
smtp_from: str = "noreply@morethanadiagnosis.com"
# File Storage
storage_type: str = "s3"
s3_bucket: str = "morethanadiagnosis-media"
s3_region: str = "us-east-1"
s3_access_key: str = ""
s3_secret_key: str = ""
# Feature Flags
feature_mfa_enabled: bool = True
feature_social_login_enabled: bool = False
feature_pseudonym_enabled: bool = True
class Config:
env_file = ".env"
case_sensitive = False
settings = Settings()

64
backend/app/database.py Normal file
View file

@ -0,0 +1,64 @@
"""
Database configuration and session management.
Job ID: MTAD-IMPL-2025-11-18-CL
"""
from sqlalchemy import create_engine, event
from sqlalchemy.engine import Engine
from sqlalchemy.orm import declarative_base, sessionmaker
from app.config import settings
import logging
logger = logging.getLogger(__name__)
# Create database engine with connection pooling
engine = create_engine(
settings.database_url,
poolclass=None if "sqlite" in settings.database_url else None,
pool_size=settings.database_pool_size,
max_overflow=settings.database_max_overflow,
echo=settings.debug,
future=True,
)
# Session factory
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Base class for models
Base = declarative_base()
# Event listeners for PostgreSQL
@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_conn, connection_record):
"""Enable foreign keys for SQLite (if using SQLite)."""
if "sqlite" in settings.database_url:
cursor = dbapi_conn.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
def get_db():
"""Dependency for FastAPI to get database session."""
db = SessionLocal()
try:
yield db
finally:
db.close()
async def init_db():
"""Initialize database tables."""
try:
Base.metadata.create_all(bind=engine)
logger.info("Database tables created successfully")
except Exception as e:
logger.error(f"Error initializing database: {e}")
raise
async def close_db():
"""Close database connections."""
engine.dispose()
logger.info("Database connections closed")

88
backend/app/main.py Normal file
View file

@ -0,0 +1,88 @@
"""
FastAPI main application entry point.
Job ID: MTAD-IMPL-2025-11-18-CL
"""
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.config import settings
from app.database import init_db, close_db
from app.api.v1 import api_router
# Configure logging
logging.basicConfig(
level=settings.log_level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Lifespan context manager for startup and shutdown events."""
# Startup
logger.info("Starting MoreThanADiagnosis API")
await init_db()
logger.info("Database initialized")
yield
# Shutdown
logger.info("Shutting down MoreThanADiagnosis API")
await close_db()
logger.info("Database connections closed")
# Create FastAPI app
app = FastAPI(
title=settings.api_title,
description=settings.api_description,
version=settings.api_version,
lifespan=lifespan,
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=settings.cors_credentials,
allow_methods=settings.cors_methods,
allow_headers=settings.cors_headers,
)
# Include API routes
app.include_router(api_router, prefix=f"/api/{settings.api_version}")
# Health check endpoint
@app.get("/health", tags=["Health"])
async def health_check():
"""Health check endpoint for uptime monitoring."""
return {
"status": "healthy",
"version": settings.api_version,
"env": settings.env,
}
@app.get("/", tags=["Root"])
async def root():
"""Root endpoint."""
return {
"service": settings.api_title,
"version": settings.api_version,
"docs": "/docs",
"redoc": "/redoc",
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=8000,
reload=settings.debug,
log_level=settings.log_level.lower(),
)

View file

@ -0,0 +1,48 @@
"""
SQLAlchemy models package.
Job ID: MTAD-IMPL-2025-11-18-CL
"""
from app.models.user import User, Profile, Role, UserRole, Consent
from app.models.authentication import RefreshToken, AuthAuditLog
from app.models.forum import (
ForumCategory,
ForumThread,
ForumPost,
ForumReaction,
ForumReport,
)
from app.models.blog import BlogPost
from app.models.podcast import PodcastEpisode
from app.models.resources import Resource
from app.models.tribute import TributeEntry
from app.models.merch import MerchProduct, Order
__all__ = [
# User & Auth
"User",
"Profile",
"Role",
"UserRole",
"Consent",
"RefreshToken",
"AuthAuditLog",
# Forum
"ForumCategory",
"ForumThread",
"ForumPost",
"ForumReaction",
"ForumReport",
# Blog
"BlogPost",
# Podcast
"PodcastEpisode",
# Resources
"Resource",
# Tribute
"TributeEntry",
# Merch
"MerchProduct",
"Order",
]

View file

@ -0,0 +1,42 @@
"""
Authentication models - RefreshToken and AuthAuditLog.
Job ID: MTAD-IMPL-2025-11-18-CL
"""
from sqlalchemy import Column, String, DateTime, ForeignKey, Integer
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class RefreshToken(Base):
"""Refresh Token entity - Session management."""
__tablename__ = "refresh_tokens"
id = Column(String(36), primary_key=True, index=True)
user_id = Column(String(36), ForeignKey("users.id"), index=True, nullable=False)
token_hash = Column(String(255), unique=True, nullable=False)
expires_at = Column(DateTime, nullable=False, index=True)
created_at = Column(DateTime, server_default=func.now())
revoked_at = Column(DateTime, nullable=True)
# Relationships
user = relationship("User", back_populates="refresh_tokens")
class AuthAuditLog(Base):
"""Auth Audit Log - Compliance and security auditing."""
__tablename__ = "auth_audit_logs"
id = Column(String(36), primary_key=True, index=True)
user_id = Column(String(36), ForeignKey("users.id"), index=True, nullable=True)
event_type = Column(String(50), index=True, nullable=False) # signup, login_success, login_fail, password_reset, mfa_enable, etc.
ip_address = Column(String(45), nullable=True)
user_agent = Column(String(500), nullable=True)
created_at = Column(DateTime, server_default=func.now(), index=True)
# Relationships
user = relationship("User", back_populates="audit_logs")

View file

@ -0,0 +1,29 @@
"""
Blog MVP model - BlogPost.
Job ID: MTAD-IMPL-2025-11-18-CL
"""
from sqlalchemy import Column, String, Text, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class BlogPost(Base):
"""Blog Post - Published articles and content."""
__tablename__ = "blog_posts"
id = Column(String(36), primary_key=True, index=True)
author_id = Column(String(36), ForeignKey("users.id"), index=True, nullable=False)
title = Column(String(500), nullable=False, index=True)
slug = Column(String(500), unique=True, index=True, nullable=False)
content = Column(Text, nullable=False)
excerpt = Column(Text, nullable=True)
published_at = Column(DateTime, nullable=True, index=True)
created_at = Column(DateTime, server_default=func.now(), index=True)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), index=True)
# Relationships
author = relationship("User", back_populates="blog_posts")

102
backend/app/models/forum.py Normal file
View file

@ -0,0 +1,102 @@
"""
Forum MVP models - Categories, Threads, Posts, Reactions, Reports.
Job ID: MTAD-IMPL-2025-11-18-CL
"""
from sqlalchemy import Column, String, Text, Boolean, DateTime, Integer, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class ForumCategory(Base):
"""Forum Category - Discussion categories."""
__tablename__ = "forum_categories"
id = Column(String(36), primary_key=True, index=True)
name = Column(String(255), unique=True, nullable=False, index=True)
description = Column(Text, nullable=True)
order = Column(Integer, default=0, index=True)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# Relationships
threads = relationship("ForumThread", back_populates="category", cascade="all, delete-orphan")
class ForumThread(Base):
"""Forum Thread - Discussion thread."""
__tablename__ = "forum_threads"
id = Column(String(36), primary_key=True, index=True)
category_id = Column(String(36), ForeignKey("forum_categories.id"), index=True, nullable=False)
author_id = Column(String(36), ForeignKey("users.id"), index=True, nullable=False)
title = Column(String(500), nullable=False, index=True)
pinned = Column(Boolean, default=False, index=True)
locked = Column(Boolean, default=False, index=True)
created_at = Column(DateTime, server_default=func.now(), index=True)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), index=True)
# Relationships
category = relationship("ForumCategory", back_populates="threads")
author = relationship("User", back_populates="forum_threads")
posts = relationship("ForumPost", back_populates="thread", cascade="all, delete-orphan")
class ForumPost(Base):
"""Forum Post - Individual post/reply in a thread."""
__tablename__ = "forum_posts"
id = Column(String(36), primary_key=True, index=True)
thread_id = Column(String(36), ForeignKey("forum_threads.id"), index=True, nullable=False)
author_id = Column(String(36), ForeignKey("users.id"), index=True, nullable=False)
parent_post_id = Column(String(36), ForeignKey("forum_posts.id"), index=True, nullable=True)
content = Column(Text, nullable=False) # May contain PHI
deleted_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, server_default=func.now(), index=True)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), index=True)
# Relationships
thread = relationship("ForumThread", back_populates="posts")
author = relationship("User", back_populates="forum_posts")
reactions = relationship("ForumReaction", back_populates="post", cascade="all, delete-orphan")
reports = relationship("ForumReport", back_populates="post", cascade="all, delete-orphan")
class ForumReaction(Base):
"""Forum Reaction - Emoji reactions to posts."""
__tablename__ = "forum_reactions"
id = Column(String(36), primary_key=True, index=True)
post_id = Column(String(36), ForeignKey("forum_posts.id"), index=True, nullable=False)
user_id = Column(String(36), ForeignKey("users.id"), index=True, nullable=False)
emoji_code = Column(String(20), nullable=False) # e.g., "👍", "❤️", "😢"
created_at = Column(DateTime, server_default=func.now(), index=True)
# Relationships
post = relationship("ForumPost", back_populates="reactions")
user = relationship("User", back_populates="forum_reactions")
class ForumReport(Base):
"""Forum Report - Content moderation reports."""
__tablename__ = "forum_reports"
id = Column(String(36), primary_key=True, index=True)
post_id = Column(String(36), ForeignKey("forum_posts.id"), index=True, nullable=False)
reporter_id = Column(String(36), ForeignKey("users.id"), index=True, nullable=False)
reason = Column(String(255), nullable=False)
status = Column(String(50), default="open", index=True) # open, resolved, dismissed
moderator_notes = Column(Text, nullable=True)
resolved_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, server_default=func.now(), index=True)
# Relationships
post = relationship("ForumPost", back_populates="reports")
reporter = relationship("User", back_populates="forum_reports")

View file

@ -0,0 +1,63 @@
"""
Merch MVP models - MerchProduct and Order for commerce.
Job ID: MTAD-IMPL-2025-11-18-CL
"""
from sqlalchemy import Column, String, Text, Integer, Float, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class MerchProduct(Base):
"""Merch Product - Store products."""
__tablename__ = "merch_products"
id = Column(String(36), primary_key=True, index=True)
name = Column(String(255), nullable=False, index=True)
description = Column(Text, nullable=True)
price = Column(Float, nullable=False)
stock_count = Column(Integer, default=0)
image_url = Column(String(500), nullable=True)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# Relationships
order_items = relationship("OrderItem", back_populates="product", cascade="all, delete-orphan")
class Order(Base):
"""Order - Purchase orders."""
__tablename__ = "orders"
id = Column(String(36), primary_key=True, index=True)
user_id = Column(String(36), ForeignKey("users.id"), index=True, nullable=False)
total = Column(Float, nullable=False)
status = Column(String(50), default="pending", index=True) # pending, completed, cancelled
shipping_address = Column(String(500), nullable=True) # PII - Encrypted
created_at = Column(DateTime, server_default=func.now(), index=True)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# Relationships
user = relationship("User", back_populates="orders")
items = relationship("OrderItem", back_populates="order", cascade="all, delete-orphan")
class OrderItem(Base):
"""Order Item - Individual items in an order."""
__tablename__ = "order_items"
id = Column(String(36), primary_key=True, index=True)
order_id = Column(String(36), ForeignKey("orders.id"), index=True, nullable=False)
product_id = Column(String(36), ForeignKey("merch_products.id"), index=True, nullable=False)
quantity = Column(Integer, default=1)
price = Column(Float, nullable=False)
created_at = Column(DateTime, server_default=func.now())
# Relationships
order = relationship("Order", back_populates="items")
product = relationship("MerchProduct", back_populates="order_items")

View file

@ -0,0 +1,25 @@
"""
Podcast MVP model - PodcastEpisode.
Job ID: MTAD-IMPL-2025-11-18-CL
"""
from sqlalchemy import Column, String, Text, Integer, DateTime
from sqlalchemy.sql import func
from app.database import Base
class PodcastEpisode(Base):
"""Podcast Episode - Audio content episodes."""
__tablename__ = "podcast_episodes"
id = Column(String(36), primary_key=True, index=True)
title = Column(String(500), nullable=False, index=True)
description = Column(Text, nullable=True)
audio_url = Column(String(500), nullable=False)
duration = Column(Integer, nullable=True) # Duration in seconds
episode_number = Column(Integer, nullable=True, index=True)
published_at = Column(DateTime, nullable=True, index=True)
created_at = Column(DateTime, server_default=func.now(), index=True)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())

View file

@ -0,0 +1,29 @@
"""
Resources MVP model - Resource knowledge base.
Job ID: MTAD-IMPL-2025-11-18-CL
"""
from sqlalchemy import Column, String, Text, DateTime, ForeignKey, JSON
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class Resource(Base):
"""Resource - Knowledge base and documentation."""
__tablename__ = "resources"
id = Column(String(36), primary_key=True, index=True)
author_id = Column(String(36), ForeignKey("users.id"), index=True, nullable=True)
title = Column(String(500), nullable=False, index=True)
slug = Column(String(500), unique=True, index=True, nullable=False)
content = Column(Text, nullable=False)
access_tier = Column(String(20), default="public", index=True) # public, members
tags = Column(JSON, default=[], index=False) # Array of tags
created_at = Column(DateTime, server_default=func.now(), index=True)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), index=True)
# Relationships
author = relationship("User", back_populates="resources")

View file

@ -0,0 +1,27 @@
"""
Tribute MVP model - TributeEntry for memorials.
Job ID: MTAD-IMPL-2025-11-18-CL
"""
from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class TributeEntry(Base):
"""Tribute Entry - Memorial and tribute entries."""
__tablename__ = "tribute_entries"
id = Column(String(36), primary_key=True, index=True)
author_id = Column(String(36), ForeignKey("users.id"), index=True, nullable=False)
subject_name = Column(String(255), nullable=False, index=True)
memorial_text = Column(Text, nullable=False) # May contain PHI
published = Column(Boolean, default=False, index=True)
created_at = Column(DateTime, server_default=func.now(), index=True)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# Relationships
author = relationship("User", back_populates="tribute_entries")

119
backend/app/models/user.py Normal file
View file

@ -0,0 +1,119 @@
"""
User, Profile, Role, and Consent models.
Job ID: MTAD-IMPL-2025-11-18-CL
"""
from sqlalchemy import Column, String, Boolean, DateTime, Integer, ForeignKey, Text, JSON
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
from datetime import datetime
class User(Base):
"""User entity - Core authentication and identity."""
__tablename__ = "users"
id = Column(String(36), primary_key=True, index=True)
email = Column(String(255), unique=True, index=True, nullable=False)
password_hash = Column(String(255), nullable=False)
email_verified = Column(Boolean, default=False)
mfa_enabled = Column(Boolean, default=False)
mfa_secret = Column(String(255), nullable=True) # Encrypted
locked_until = Column(DateTime, nullable=True)
failed_login_attempts = Column(Integer, default=0)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
deleted_at = Column(DateTime, nullable=True)
# Relationships
profile = relationship("Profile", back_populates="user", uselist=False, cascade="all, delete-orphan")
roles = relationship("UserRole", back_populates="user", cascade="all, delete-orphan")
consents = relationship("Consent", back_populates="user", cascade="all, delete-orphan")
refresh_tokens = relationship("RefreshToken", back_populates="user", cascade="all, delete-orphan")
audit_logs = relationship("AuthAuditLog", back_populates="user", cascade="all, delete-orphan")
# Forum relationships
forum_threads = relationship("ForumThread", back_populates="author")
forum_posts = relationship("ForumPost", back_populates="author")
forum_reactions = relationship("ForumReaction", back_populates="user")
forum_reports = relationship("ForumReport", back_populates="reporter")
# Content relationships
blog_posts = relationship("BlogPost", back_populates="author")
resources = relationship("Resource", back_populates="author")
tribute_entries = relationship("TributeEntry", back_populates="author")
# Commerce relationships
orders = relationship("Order", back_populates="user", cascade="all, delete-orphan")
class Profile(Base):
"""User Profile - Extended user information and health journey."""
__tablename__ = "profiles"
id = Column(String(36), primary_key=True, index=True)
user_id = Column(String(36), ForeignKey("users.id"), unique=True, nullable=False, index=True)
display_name = Column(String(255), nullable=False)
pseudonym = Column(String(255), nullable=True, index=True)
pronouns = Column(String(50), nullable=True)
avatar_url = Column(String(500), nullable=True)
bio = Column(Text, nullable=True)
health_journey = Column(Text, nullable=True) # PHI - Encrypted at rest
consent_flags = Column(JSON, default={})
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# Relationships
user = relationship("User", back_populates="profile")
class Role(Base):
"""Role entity - RBAC roles."""
__tablename__ = "roles"
id = Column(String(36), primary_key=True, index=True)
name = Column(String(100), unique=True, index=True, nullable=False)
permissions = Column(JSON, default={})
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# Relationships
users = relationship("UserRole", back_populates="role", cascade="all, delete-orphan")
class UserRole(Base):
"""User-Role mapping for RBAC."""
__tablename__ = "user_roles"
id = Column(String(36), primary_key=True, index=True)
user_id = Column(String(36), ForeignKey("users.id"), index=True, nullable=False)
role_id = Column(String(36), ForeignKey("roles.id"), index=True, nullable=False)
created_at = Column(DateTime, server_default=func.now())
# Relationships
user = relationship("User", back_populates="roles")
role = relationship("Role", back_populates="users")
class Consent(Base):
"""Consent entity - User consent tracking for compliance."""
__tablename__ = "consents"
id = Column(String(36), primary_key=True, index=True)
user_id = Column(String(36), ForeignKey("users.id"), index=True, nullable=False)
consent_type = Column(String(100), nullable=False) # e.g., marketing, analytics, data_sharing
granted = Column(Boolean, default=False)
granted_at = Column(DateTime, nullable=True)
revoked_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# Relationships
user = relationship("User", back_populates="consents")

View file

@ -0,0 +1,86 @@
version: '3.8'
# Job ID: MTAD-IMPL-2025-11-18-CL
# Deployment: nexus-vector (production)
# Spec: openspec/specs/architecture.md
services:
postgres:
image: postgres:15-alpine
container_name: mtad-postgres
restart: unless-stopped
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: ${DB_PASSWORD:-change-me-in-production}
POSTGRES_DB: morethanadiagnosis
POSTGRES_INITDB_ARGS: "-c shared_preload_libraries=pg_stat_statements"
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- mtad-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -d morethanadiagnosis"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: mtad-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- mtad-network
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-change-me-in-production}
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
api:
build:
context: .
dockerfile: Dockerfile
container_name: mtad-api
restart: unless-stopped
environment:
ENV: production
DEBUG: "false"
DATABASE_URL: postgresql://admin:${DB_PASSWORD:-change-me-in-production}@postgres:5432/morethanadiagnosis
REDIS_URL: redis://:${REDIS_PASSWORD:-change-me-in-production}@redis:6379/0
SECRET_KEY: ${SECRET_KEY:-change-me-in-production}
CORS_ORIGINS: '["https://morethanadiagnosis.com", "http://localhost:3000"]'
LOG_LEVEL: INFO
ports:
- "8000:8000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- mtad-network
volumes:
- ./app:/app/app:ro
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
networks:
mtad-network:
driver: bridge
volumes:
postgres_data:
driver: local
redis_data:
driver: local

66
backend/requirements.txt Normal file
View file

@ -0,0 +1,66 @@
# Core Framework
fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic==2.5.0
pydantic-settings==2.1.0
python-multipart==0.0.6
# Database
sqlalchemy==2.0.23
alembic==1.12.1
psycopg2-binary==2.9.9
psycopg[binary]==3.9.10
# Authentication & Security
passlib[bcrypt]==1.7.4
python-jose[cryptography]==3.3.0
PyJWT==2.8.1
bcrypt==4.1.1
cryptography==41.0.7
argon2-cffi==23.1.0
# API Documentation
python-dateutil==2.8.2
# Validation & Serialization
email-validator==2.1.0
# Environment Variables
python-dotenv==1.0.0
# Logging & Observability
python-json-logger==2.0.7
opentelemetry-api==1.21.0
opentelemetry-sdk==1.21.0
opentelemetry-exporter-jaeger==1.21.0
opentelemetry-instrumentation-fastapi==0.42b0
opentelemetry-instrumentation-sqlalchemy==0.42b0
# Data Validation & Transformation
marshmallow==3.20.1
# Testing
pytest==7.4.3
pytest-asyncio==0.21.1
pytest-cov==4.1.0
httpx==0.25.2
# Linting & Type Checking
ruff==0.1.8
mypy==1.7.1
types-python-dateutil==2.8.20
# Development
black==23.12.0
isort==5.13.2
pre-commit==3.5.0
# CORS & Middleware
fastapi-cors==0.0.6
# Rate Limiting
slowapi==0.1.9
# Background Tasks
celery==5.3.4
redis==5.0.1