generated from coulomb/repo-seed
init: documentation and prototypes
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
# Using SQLite for easy local testing without Docker.
|
||||
# For Production, switch to PostgreSQL as per ADR-007.
|
||||
DATABASE_URL = "sqlite+aiosqlite:///./dvd_local.db"
|
||||
# DATABASE_URL = "postgresql+asyncpg://user:password@localhost/dvd_db"
|
||||
|
||||
engine = create_async_engine(DATABASE_URL, echo=True)
|
||||
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
async def get_db():
|
||||
async with AsyncSessionLocal() as session:
|
||||
yield session
|
||||
108
prototype-geminiNbt3pro/direktvermittlung_backend/app/main.py
Normal file
108
prototype-geminiNbt3pro/direktvermittlung_backend/app/main.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
import time
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import FastAPI, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.database import engine, Base, get_db
|
||||
from app.models import Document, InteractionThread, Message
|
||||
from app.schemas import DocumentCreate, DocumentResponse, ThreadCreate, MessageListResponse, MessageResponse
|
||||
|
||||
# --- Hybrid Concurrency Config (ADR-007) ---
|
||||
process_pool = ProcessPoolExecutor(max_workers=4)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Initialize DB (Auto-create tables for demo)
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield
|
||||
process_pool.shutdown()
|
||||
await engine.dispose()
|
||||
|
||||
app = FastAPI(title="DirektVermittlungDe API", lifespan=lifespan)
|
||||
|
||||
# --- Dependencies ---
|
||||
async def get_current_user():
|
||||
return {"id": "citizen-123", "role": "citizen"}
|
||||
|
||||
# --- CPU Bound Logic (Isolated) ---
|
||||
def cpu_bound_validation(payload_base64: str) -> bool:
|
||||
'''Simulates heavy crypto validation away from the Event Loop'''
|
||||
time.sleep(0.1) # Simulate CPU work
|
||||
return True
|
||||
|
||||
# --- Routes ---
|
||||
|
||||
@app.post("/documents", response_model=DocumentResponse, status_code=201)
|
||||
async def upload_document(
|
||||
doc_in: DocumentCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user = Depends(get_current_user)
|
||||
):
|
||||
# 1. Offload CPU heavy validation
|
||||
loop = asyncio.get_running_loop()
|
||||
is_valid = await loop.run_in_executor(
|
||||
process_pool,
|
||||
cpu_bound_validation,
|
||||
doc_in.encryptedPayload
|
||||
)
|
||||
if not is_valid:
|
||||
raise HTTPException(status_code=400, detail="Invalid payload")
|
||||
|
||||
# 2. Mock S3 Upload
|
||||
s3_key = f"s3://bucket/{uuid.uuid4()}.enc"
|
||||
|
||||
# 3. Create Record
|
||||
new_doc = Document(
|
||||
authority_id=doc_in.metadata.authorityId,
|
||||
reference_number=doc_in.metadata.referenceNumber,
|
||||
doc_type=doc_in.metadata.docType.value,
|
||||
storage_path=s3_key,
|
||||
status="ROUTED"
|
||||
)
|
||||
db.add(new_doc)
|
||||
await db.commit()
|
||||
await db.refresh(new_doc)
|
||||
|
||||
return DocumentResponse(
|
||||
id=new_doc.id,
|
||||
status=new_doc.status,
|
||||
assignedUnit="Tax-Team-A"
|
||||
)
|
||||
|
||||
@app.get("/threads/{thread_id}/messages", response_model=MessageListResponse)
|
||||
async def get_messages(
|
||||
thread_id: str,
|
||||
limit: int = 20,
|
||||
before: Optional[datetime] = Query(None),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
# Cursor Pagination Logic
|
||||
query = select(Message).where(Message.thread_id == uuid.UUID(thread_id))
|
||||
if before:
|
||||
query = query.where(Message.timestamp < before)
|
||||
|
||||
query = query.order_by(Message.timestamp.desc()).limit(limit)
|
||||
result = await db.execute(query)
|
||||
messages = result.scalars().all()
|
||||
|
||||
next_cursor = messages[-1].timestamp if messages else None
|
||||
|
||||
return MessageListResponse(
|
||||
data=[
|
||||
MessageResponse(
|
||||
id=m.id,
|
||||
senderRole=m.sender_role,
|
||||
content=m.content_blob,
|
||||
timestamp=m.timestamp
|
||||
) for m in messages
|
||||
],
|
||||
nextCursor=next_cursor
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from sqlalchemy import String, DateTime, ForeignKey, Index
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.database import Base
|
||||
|
||||
class Document(Base):
|
||||
'''
|
||||
Represents the DocumentEnvelope.
|
||||
Implements ADR-001: Separation of Metadata and Payload.
|
||||
'''
|
||||
__tablename__ = "documents"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
||||
authority_id: Mapped[str] = mapped_column(String(50), index=True)
|
||||
reference_number: Mapped[str] = mapped_column(String(50))
|
||||
doc_type: Mapped[str] = mapped_column(String(20))
|
||||
storage_path: Mapped[str] = mapped_column(String(255))
|
||||
status: Mapped[str] = mapped_column(String(20), default="RECEIVED")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
retention_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
threads = relationship("InteractionThread", back_populates="document")
|
||||
|
||||
class InteractionThread(Base):
|
||||
__tablename__ = "threads"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
||||
document_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("documents.id"))
|
||||
type: Mapped[str] = mapped_column(String(20))
|
||||
status: Mapped[str] = mapped_column(String(20), default="OPEN")
|
||||
|
||||
document = relationship("Document", back_populates="threads")
|
||||
messages = relationship("Message", back_populates="thread")
|
||||
|
||||
class Message(Base):
|
||||
'''
|
||||
Optimized for Cursor-Based Pagination (NFR-1).
|
||||
'''
|
||||
__tablename__ = "messages"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
||||
thread_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("threads.id"))
|
||||
sender_role: Mapped[str] = mapped_column(String(20))
|
||||
content_blob: Mapped[str] = mapped_column(String)
|
||||
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
thread = relationship("InteractionThread", back_populates="messages")
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_msgs_thread_time', 'thread_id', 'timestamp'),
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
from enum import Enum
|
||||
|
||||
class DocType(str, Enum):
|
||||
NOTICE = "NOTICE"
|
||||
COURT_ORDER = "COURT_ORDER"
|
||||
GENERAL_INQUIRY = "GENERAL_INQUIRY"
|
||||
|
||||
class ThreadType(str, Enum):
|
||||
TEXT_CHAT = "TEXT_CHAT"
|
||||
CALLBACK_REQUEST = "CALLBACK_REQUEST"
|
||||
APPOINTMENT = "APPOINTMENT"
|
||||
|
||||
class MetadataInput(BaseModel):
|
||||
authorityId: str = Field(..., example="Finanzamt-Muenchen-I")
|
||||
referenceNumber: str = Field(..., example="123/456/789")
|
||||
docType: DocType
|
||||
issuedAt: datetime
|
||||
|
||||
class DocumentCreate(BaseModel):
|
||||
metadata: MetadataInput
|
||||
encryptedPayload: str
|
||||
|
||||
class ThreadCreate(BaseModel):
|
||||
type: ThreadType
|
||||
initialMessage: str
|
||||
preferredTimeSlot: Optional[datetime] = None
|
||||
|
||||
class DocumentResponse(BaseModel):
|
||||
id: UUID
|
||||
status: str
|
||||
assignedUnit: Optional[str] = None
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
id: UUID
|
||||
senderRole: str
|
||||
content: str
|
||||
timestamp: datetime
|
||||
|
||||
class MessageListResponse(BaseModel):
|
||||
data: List[MessageResponse]
|
||||
nextCursor: Optional[datetime] = None
|
||||
@@ -0,0 +1,7 @@
|
||||
fastapi==0.109.0
|
||||
uvicorn==0.27.0
|
||||
sqlalchemy==2.0.25
|
||||
asyncpg==0.29.0
|
||||
pydantic==2.6.0
|
||||
pydantic-settings==2.1.0
|
||||
python-multipart==0.0.9
|
||||
Reference in New Issue
Block a user