Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
155 lines
4.0 KiB
Python
155 lines
4.0 KiB
Python
"""FastAPI application for ZUGFeRD invoice processing."""
|
|
|
|
import base64
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
|
|
import uvicorn
|
|
from fastapi import FastAPI, HTTPException, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from src.extractor import ExtractionError, extract_zugferd
|
|
from src.models import (
|
|
ExtractRequest,
|
|
ExtractResponse,
|
|
HealthResponse,
|
|
ValidateRequest,
|
|
ValidateResponse,
|
|
)
|
|
from src.validator import validate_invoice
|
|
|
|
|
|
class JSONFormatter(logging.Formatter):
|
|
def format(self, record):
|
|
log_data = {
|
|
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
"level": record.levelname,
|
|
"message": record.getMessage(),
|
|
}
|
|
if hasattr(record, "data"):
|
|
log_data["data"] = record.data
|
|
return json.dumps(log_data)
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
if not logger.handlers:
|
|
handler = logging.StreamHandler()
|
|
handler.setFormatter(JSONFormatter())
|
|
logger.addHandler(handler)
|
|
logger.setLevel(logging.INFO)
|
|
|
|
|
|
app = FastAPI(
|
|
title="ZUGFeRD Service",
|
|
version="1.0.0",
|
|
description="REST API for ZUGFeRD invoice extraction and validation",
|
|
)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
@app.exception_handler(ExtractionError)
|
|
async def extraction_error_handler(request: Request, exc: ExtractionError):
|
|
return JSONResponse(
|
|
status_code=400,
|
|
content={
|
|
"error": exc.error_code,
|
|
"message": exc.message,
|
|
"details": exc.details,
|
|
},
|
|
)
|
|
|
|
|
|
@app.exception_handler(HTTPException)
|
|
async def http_exception_handler(request: Request, exc: HTTPException):
|
|
if isinstance(exc.detail, dict) and "error" in exc.detail:
|
|
return JSONResponse(
|
|
status_code=exc.status_code,
|
|
content={
|
|
"error": exc.detail.get("error"),
|
|
"message": exc.detail.get("message"),
|
|
},
|
|
)
|
|
return JSONResponse(
|
|
status_code=exc.status_code,
|
|
content={
|
|
"error": "http_error",
|
|
"message": str(exc.detail),
|
|
},
|
|
)
|
|
|
|
|
|
@app.exception_handler(Exception)
|
|
async def generic_error_handler(request: Request, exc: Exception):
|
|
logger.error(f"Internal error: {exc}")
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={
|
|
"error": "internal_error",
|
|
"message": "An internal error occurred",
|
|
},
|
|
)
|
|
|
|
|
|
@app.get("/health", response_model=HealthResponse)
|
|
async def health_check() -> HealthResponse:
|
|
"""Health check endpoint.
|
|
|
|
Returns:
|
|
HealthResponse with status and version.
|
|
"""
|
|
return HealthResponse(status="healthy", version="1.0.0")
|
|
|
|
|
|
@app.post("/extract", response_model=ExtractResponse)
|
|
async def extract_pdf(request: ExtractRequest) -> ExtractResponse:
|
|
"""Extract ZUGFeRD data from PDF.
|
|
|
|
Args:
|
|
request: ExtractRequest with pdf_base64 field
|
|
|
|
Returns:
|
|
ExtractResponse with extraction results
|
|
"""
|
|
try:
|
|
pdf_bytes = base64.b64decode(request.pdf_base64)
|
|
except Exception:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={"error": "invalid_base64", "message": "Invalid base64 encoding"},
|
|
)
|
|
|
|
return extract_zugferd(pdf_bytes)
|
|
|
|
|
|
@app.post("/validate", response_model=ValidateResponse)
|
|
async def validate_invoice_endpoint(request: ValidateRequest) -> ValidateResponse:
|
|
"""Validate ZUGFeRD invoice data.
|
|
|
|
Args:
|
|
request: ValidateRequest with xml_data, pdf_text, checks
|
|
|
|
Returns:
|
|
ValidateResponse with validation results
|
|
"""
|
|
result = validate_invoice(request)
|
|
return ValidateResponse(result=result)
|
|
|
|
|
|
def run(host: str = "0.0.0.0", port: int = 5000) -> None:
|
|
"""Run the FastAPI application.
|
|
|
|
Args:
|
|
host: Host to bind to.
|
|
port: Port to listen on.
|
|
"""
|
|
uvicorn.run(app, host=host, port=port)
|