feat: init plesna-gerance - extracteur de comptes rendus de gerance
- Backend Python (uv + click + FastAPI) - CLI: plesna-gerance extract <pdf> pour extraire les donnees - CLI: plesna-gerance serve pour lancer le serveur API - API REST: POST /api/extract pour upload et extraction de PDF - Parsers modulaires: metadata, locataires, operations - Utilise pdftotext (poppler-utils) pour l'extraction de texte - Frontend Vue.js + Tailwind CSS - Interface split-screen: PDF a gauche, donnees a droite - Preview PDF avec zoom et navigation pages (pdf.js) - Visualisation structuree des donnees extraites - Sections depliables: metadata, locataires, operations - Drag & drop pour upload de PDF - Extraction automatique a la selection du fichier
This commit is contained in:
5
src/plesna_gerance/api/__init__.py
Normal file
5
src/plesna_gerance/api/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""API FastAPI pour l'extraction de comptes rendus de gérance."""
|
||||
|
||||
from .app import app
|
||||
|
||||
__all__ = ["app"]
|
||||
122
src/plesna_gerance/api/app.py
Normal file
122
src/plesna_gerance/api/app.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""Application FastAPI pour l'extraction de comptes rendus de gérance."""
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, File, HTTPException, UploadFile
|
||||
from fastapi.responses import JSONResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from .. import __version__
|
||||
from ..extractor import extract_compte_rendu
|
||||
|
||||
app = FastAPI(
|
||||
title="Plesna Gérance API",
|
||||
description="API pour extraire les informations structurées des PDFs de comptes rendus de gérance Oralia/ICS.",
|
||||
version=__version__,
|
||||
docs_url="/api/docs",
|
||||
redoc_url="/api/redoc",
|
||||
openapi_url="/api/openapi.json",
|
||||
)
|
||||
|
||||
|
||||
# Determine the frontend dist path
|
||||
FRONTEND_DIST = Path(__file__).parent.parent.parent.parent / "frontend" / "dist"
|
||||
|
||||
|
||||
@app.get("/api", tags=["health"])
|
||||
async def api_root() -> dict:
|
||||
"""Endpoint racine de l'API - informations."""
|
||||
return {
|
||||
"name": "Plesna Gérance API",
|
||||
"version": __version__,
|
||||
"docs": "/api/docs",
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/health", tags=["health"])
|
||||
async def health() -> dict:
|
||||
"""Vérification de l'état du serveur."""
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.post("/api/extract", tags=["extraction"])
|
||||
async def extract_pdf(
|
||||
file: UploadFile = File(..., description="Fichier PDF de compte rendu de gérance"),
|
||||
) -> JSONResponse:
|
||||
"""Extrait les données d'un PDF de compte rendu de gérance.
|
||||
|
||||
Upload un fichier PDF et retourne les données structurées en JSON.
|
||||
|
||||
- **file**: Fichier PDF à analyser (Content-Type: multipart/form-data)
|
||||
|
||||
Retourne un objet JSON contenant:
|
||||
- **source_file**: Nom du fichier uploadé
|
||||
- **data**: Données extraites (metadata, situation_locataires, recapitulatif_operations)
|
||||
"""
|
||||
# Validation du type de fichier
|
||||
if not file.filename:
|
||||
raise HTTPException(status_code=400, detail="Nom de fichier manquant")
|
||||
|
||||
if not file.filename.lower().endswith(".pdf"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Le fichier doit être un PDF. Reçu: {file.filename}",
|
||||
)
|
||||
|
||||
# Validation du content-type (si fourni)
|
||||
if file.content_type and file.content_type not in (
|
||||
"application/pdf",
|
||||
"application/octet-stream",
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Content-Type invalide. Attendu: application/pdf, reçu: {file.content_type}",
|
||||
)
|
||||
|
||||
tmp_path: Path | None = None
|
||||
|
||||
# Sauvegarde temporaire du fichier uploadé
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:
|
||||
content = await file.read()
|
||||
tmp_file.write(content)
|
||||
tmp_path = Path(tmp_file.name)
|
||||
|
||||
# Extraction des données
|
||||
try:
|
||||
data = extract_compte_rendu(str(tmp_path))
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"Erreur lors de l'extraction du PDF: {str(e)}",
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"source_file": file.filename,
|
||||
"data": data,
|
||||
}
|
||||
)
|
||||
|
||||
finally:
|
||||
# Nettoyage du fichier temporaire
|
||||
if tmp_path and tmp_path.exists():
|
||||
tmp_path.unlink()
|
||||
|
||||
|
||||
# Mount static files for production (if dist exists)
|
||||
if FRONTEND_DIST.exists():
|
||||
# Serve static assets
|
||||
app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets")
|
||||
|
||||
# Catch-all route for SPA - must be last
|
||||
@app.get("/{full_path:path}", include_in_schema=False)
|
||||
async def serve_spa(full_path: str):
|
||||
"""Serve the SPA for all non-API routes."""
|
||||
# If requesting a file that exists, serve it
|
||||
file_path = FRONTEND_DIST / full_path
|
||||
if file_path.is_file():
|
||||
return FileResponse(file_path)
|
||||
# Otherwise serve index.html for SPA routing
|
||||
return FileResponse(FRONTEND_DIST / "index.html")
|
||||
Reference in New Issue
Block a user