# -*- coding: utf-8 -*- """ app.py — service d'extraction de texte pour les CV de Doobito (tourne sur le VPS). POST /extraire (multipart, champ « fichier », en-tête X-API-Key) -> {"ok": true, "texte": "...", "methode": "..."} GET /sante -> {"ok": true} Le service ne conserve rien : le fichier reçu est écrit dans un dossier temporaire, lu par extracteur.py dans un processus séparé (temps et mémoire bornés), puis supprimé. Rien n'est journalisé du contenu des CV. """ import hmac import json import os import subprocess import sys import tempfile import threading from fastapi import FastAPI, File, Header, HTTPException, UploadFile from fastapi.responses import JSONResponse CLE_API = os.environ.get("CV_API_KEY", "") if len(CLE_API) < 32: raise RuntimeError("CV_API_KEY manquante ou trop courte (32 caractères minimum).") TAILLE_MAX = 3 * 1024 * 1024 DELAI_SANS_OCR = 25 DELAI_AVEC_OCR = 70 SIMULTANES = threading.BoundedSemaphore(int(os.environ.get("CV_SIMULTANES", "3"))) DOSSIER = os.path.dirname(os.path.abspath(__file__)) app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None) def verifier_cle(cle): if not cle or not hmac.compare_digest(cle.encode("utf-8"), CLE_API.encode("utf-8")): raise HTTPException(status_code=401, detail="Clé invalide.") @app.get("/sante") def sante(): return {"ok": True} @app.post("/extraire") def extraire(fichier: UploadFile = File(...), x_api_key: str = Header(default="")): verifier_cle(x_api_key) contenu = fichier.file.read(TAILLE_MAX + 1) if len(contenu) > TAILLE_MAX: raise HTTPException(status_code=413, detail="Fichier trop volumineux.") if not contenu: raise HTTPException(status_code=400, detail="Fichier vide.") if not SIMULTANES.acquire(blocking=False): raise HTTPException(status_code=503, detail="Service occupé, réessayez.") try: with tempfile.TemporaryDirectory(prefix="cv_") as dossier: chemin = os.path.join(dossier, "cv.bin") # nom fixe : le nom envoyé par le client n'est jamais utilisé with open(chemin, "wb") as f: f.write(contenu) reponse = None for avec_ocr in (False, True): commande = [sys.executable, "-I", os.path.join(DOSSIER, "extracteur.py"), chemin] + (["--ocr"] if avec_ocr else []) try: resultat = subprocess.run(commande, capture_output=True, timeout=DELAI_AVEC_OCR if avec_ocr else DELAI_SANS_OCR) except subprocess.TimeoutExpired: raise HTTPException(status_code=504, detail="Lecture trop longue.") if resultat.returncode != 0: raise HTTPException(status_code=422, detail="Fichier illisible.") reponse = json.loads(resultat.stdout.decode("utf-8")) # OCR seulement si la lecture normale n'a rien donné d'exploitable (CV scanné). lettres = sum(1 for c in reponse["texte"] if c.isalpha()) if lettres >= 60 or avec_ocr or reponse["methode"] not in ("pypdf", "pdfminer", "aucune"): break return JSONResponse({"ok": True, "texte": reponse["texte"], "methode": reponse["methode"]}) finally: SIMULTANES.release()