[build-system] # Sprint A9 (M-5) : setuptools_scm dérive la version du tag git le # plus proche. Le pipeline release.yml tag ``v1.2.3`` produit donc # un wheel ``picarones-1.2.3-py3-none-any.whl`` sans toucher à # pyproject.toml. Pour les builds non-tag (PR, dev) : version # pseudo ``1.2.4.dev3+g``. requires = ["setuptools>=68.0", "wheel", "setuptools_scm[toml]>=8.0"] build-backend = "setuptools.build_meta" [project] name = "picarones" # Sprint A9 (M-5) : ``version`` est désormais dynamique, dérivé du # tag git via setuptools_scm. Voir [tool.setuptools_scm] plus bas. dynamic = ["version"] description = "Plateforme de comparaison de moteurs OCR/HTR pour documents patrimoniaux" readme = "README.md" requires-python = ">=3.11" license = { text = "Apache-2.0" } authors = [{ name = "maribakulj" }] keywords = ["ocr", "htr", "patrimoine", "benchmark", "cer", "wer", "gallica", "escriptorium", "iiif"] classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Text Processing :: Linguistic", "Intended Audience :: Science/Research", "Natural Language :: French", ] dependencies = [ # Sprint S6.3 — toutes les dépendances core ont une borne # supérieure (caplock major) pour éviter qu'un ``pip install # picarones`` futur ramène une version 2 d'un paquet et casse # silencieusement l'API. Les majeures suivantes sont validées # sur le repo ; passage à une majeure supérieure = sprint # explicite (mise à jour des bornes ICI + tests d'équivalence). "click>=8.1.0,<9.0", "jiwer>=3.0.0,<5.0", "Pillow>=10.0.0,<13.0", "pyyaml>=6.0.0,<7.0", "pytesseract>=0.3.10,<0.4", "tqdm>=4.66.0,<5.0", "numpy>=1.24.0,<3.0", "jinja2>=3.1.0,<4.0", # XML parsing sécurisé contre les attaques XXE / Billion Laughs. # Utilisé par ``picarones.interfaces.web.corpus_utils`` pour le parsing # ALTO/PAGE quand un utilisateur uploade un corpus XML. "defusedxml>=0.7.1,<0.8", # Pydantic — types immutables de la couche 1 (``picarones.domain``). # Déclaré explicitement pour que les installs minimaux (sans # ``[web]`` qui le ramènerait via FastAPI) bénéficient des modèles # validés au moment de la déserialisation YAML. "pydantic>=2.0,<3.0", ] [project.urls] Homepage = "https://github.com/maribakulj/Picarones" Documentation = "https://github.com/maribakulj/Picarones/blob/main/docs/index.md" Repository = "https://github.com/maribakulj/Picarones" Changelog = "https://github.com/maribakulj/Picarones/blob/main/CHANGELOG.md" "Bug Tracker" = "https://github.com/maribakulj/Picarones/issues" [project.optional-dependencies] # Développement et tests. # pytest-timeout (Sprint A1) garantit qu'aucun test individuel ne hang la CI # au-delà de la limite définie dans [tool.pytest.ini_options]. # mypy (Sprint A1, M-4) : type-check strict sur picarones/domain/ + lax ailleurs. # bandit (Sprint A1, B-7) : scanner sécurité statique du code Python. # pip-audit (Sprint A1, B-7) : détection des CVE des dépendances installées. dev = [ "pytest>=7.4.0", "pytest-cov>=4.1.0", "pytest-timeout>=2.3.0", "httpx>=0.27.0", "fastapi>=0.111.0", "uvicorn[standard]>=0.29.0", "python-multipart>=0.0.9", "mypy>=1.10.0", "bandit>=1.7.0", "pip-audit>=2.7.0", ] # Interface web FastAPI web = ["fastapi>=0.111.0", "uvicorn[standard]>=0.29.0", "httpx>=0.27.0", "python-multipart>=0.0.9"] # Tests statistiques avancés (Wilcoxon exact, Friedman chi² exact, Nemenyi) # Sinon fallback pur Python (approximations normale / Wilson-Hilferty). stats = ["scipy>=1.11.0"] # Extracteurs d'entités nommées (Sprint 40 — A.II.1.a du plan d'évolution). # Sans cet extra, picarones.evaluation.metrics.ner_backends.SpacyEntityExtractor tombe # en mode dégradé silencieux et le runner saute le calcul NER. ner = ["spacy>=3.7.0"] # Import HuggingFace Datasets hf = ["datasets>=2.19.0"] # Site documentation auto-généré (mkdocstrings + mkdocs-material). # Build : ``mkdocs build`` ; serve : ``mkdocs serve``. La référence # d'API ``docs/api/`` est régénérée à chaque build depuis les # docstrings — pas de drift possible avec le code. docs = [ "mkdocs>=1.6.0", "mkdocs-material>=9.5.0", "mkdocstrings[python]>=0.25.0", ] # Moteurs OCR optionnels pero = ["pero-ocr>=0.1.0"] kraken = ["kraken>=4.0.0"] calamari = ["calamari-ocr>=2.0.0"] # Adaptateurs LLM llm = [ "openai>=1.0.0", "anthropic>=0.20.0", "mistralai>=1.0.0", ] # OCR cloud APIs ocr-cloud = [ "google-cloud-vision>=3.0.0", "boto3>=1.34.0", "azure-ai-formrecognizer>=3.3.0", ] # Sprint A9 (m-16) — les anciens placeholders ``[historical]`` et # ``[importers]`` (qui valaient ``[]`` et n'apportaient rien à # l'installation) sont retirés. La séparation future en packages PyPI # distincts (``picarones-historical``, ``picarones-importers``) est # documentée dans ``docs/developer/module-policy.md`` (Sprint 97) et # n'a plus besoin d'être réservée par un extra vide. # # Installation **vraiment complète** : tous les extras déclarés # ci-dessus, OCR cloud et docs inclus. Le nom ``all`` ne doit pas # tromper le contributeur — si un extra apparaît plus haut, il doit # apparaître ici. Phase 2.6 de l'audit code-quality (2026-05). all = [ "picarones[dev,docs,web,stats,ner,hf,pero,kraken,calamari,llm,ocr-cloud]", ] [project.scripts] picarones = "picarones.interfaces.cli:cli" # ────────────────────────────────────────────────────────────────── # Sprint A9 (M-5) — version dynamique via setuptools_scm. # # Comportement : # - sur un tag ``v1.2.3`` → version ``1.2.3`` # - hors tag (PR, main) → ``1.2.4.dev+g`` (PEP 440) # - le ``write_to`` injecte ``picarones/_version.py`` au build, lu # par ``picarones/__init__.py`` via ``__version__``. # ``fallback_version`` est utilisé si l'historique git est absent # (ex : tarball sdist) — doit être maintenu cohérent avec le dernier tag. # ────────────────────────────────────────────────────────────────── [tool.setuptools_scm] write_to = "picarones/_version.py" fallback_version = "1.0.0" version_scheme = "release-branch-semver" local_scheme = "no-local-version" [tool.setuptools.packages.find] where = ["."] include = ["picarones*"] [tool.setuptools.package-data] picarones = [ "prompts/*.txt", "data/*.yaml", "interfaces/web/static/*.css", "interfaces/web/static/*.js", "interfaces/web/templates/*.html", "interfaces/web/templates/*.j2", "reports/_helpers/vendor/*.js", "reports/glossary/*.yaml", "reports/html/templates/*.css", "reports/html/templates/*.html", "reports/html/templates/*.j2", "reports/html/templates/*.js", "reports/i18n/*.json", "reports/narrative/templates/*.yaml", ] [tool.pytest.ini_options] testpaths = ["tests"] # Le repo root dans ``sys.path`` pour que ``tests.fixtures.*`` soit # importable de manière déterministe sur tous les OS (Linux/macOS/ # Windows) — utilisé par les tests CLI E2E qui résolvent leurs mock # adapters via dotted path (``importlib.import_module("tests.fixtures.…")``). pythonpath = ["."] # Exclusion par défaut : markers ``network`` et ``live`` non # sélectionnés. Override en local via ``pytest -m network`` ou # ``pytest -m live`` (avec env vars / binaires correctement # configurés). ``-m ""`` pour tout exécuter. addopts = "-v --tb=short -m 'not network and not live'" # Sprint A1 (M-15) : aucun test individuel ne doit dépasser 5 minutes. # Mode "thread" car certains tests utilisent ProcessPoolExecutor qui est # incompatible avec le timeout en mode "signal" sur certaines plateformes. timeout = 300 timeout_method = "thread" # Marqueurs personnalisés. # - ``slow`` : tests longs (corpus de référence) ; désélectionnables # via ``pytest -m "not slow"`` pour les boucles de dev. # - ``network`` : tests qui font des requêtes HTTP réelles vers # l'extérieur (HTR-United GitHub, HuggingFace Hub, Gallica…). # Exclus du run local par défaut (sandbox sans accès réseau → # timeout urllib 30s × N tests = suite bloquée). La CI les exécute # explicitement via ``pytest -m network`` ou en levant l'exclusion # par défaut. markers = [ "slow: tests longs (corpus de référence, intégration cloud) ; non bloquants en dev local", "network: tests qui hit le réseau réel ; exclus par défaut", "live: tests d'intégration contre vraie API/binaire (Tesseract, Anthropic, OpenAI, Mistral) ; exclus par défaut, opt-in en local via 'pytest -m live'", ] # ────────────────────────────────────────────────────────────────── # Sprint A1 (B-8) — seuil minimal de couverture appliqué en CI. # Le baseline est mesuré en début de sprint puis le plancher est posé # 2 points en dessous, pour laisser une marge de manœuvre aux PR # tout en interdisant une dégradation franche. # ────────────────────────────────────────────────────────────────── [tool.coverage.run] source = ["picarones"] omit = [ "picarones/report/vendor/*", # Chart.js minifié vendoré "picarones/report/templates/*", # templates Jinja2 + JS, pas du code Python "*/tests/*", ] parallel = true [tool.coverage.report] # Le seuil est appliqué via la flag CLI ``--cov-fail-under=N`` dans la CI # (cf. .github/workflows/ci.yml) plutôt qu'ici, pour permettre aux # développeurs de lancer ``pytest --cov`` localement sans échec sur les # fichiers qu'ils ne touchent pas. exclude_lines = [ "pragma: no cover", "raise NotImplementedError", "if TYPE_CHECKING:", "if __name__ == .__main__.:", ] # ────────────────────────────────────────────────────────────────── # Sprint A1 (M-4) — type-checking gradient. # # Stratégie : ``picarones.domain`` est en mode ``strict`` car c'est la # couche 1 (types purs, Pydantic + stdlib only) — l'API publique stable # et la base de l'architecture concentrique. Les autres couches passent # en mode permissif (``ignore_missing_imports`` + pas de strict). # ────────────────────────────────────────────────────────────────── [tool.mypy] python_version = "3.11" ignore_missing_imports = true warn_unused_configs = true warn_redundant_casts = true warn_unused_ignores = true no_implicit_optional = true # Les imports vers les autres couches sont suivis silencieusement # pour éviter de propager les erreurs des couches non encore typées. follow_imports = "silent" # Sprint S3.6 — plugin Pydantic obligatoire pour que ``mypy --strict`` # sur ``picarones.domain`` reconnaisse les ``BaseModel`` correctement. # Sans plugin, ``BaseModel`` est traité comme ``Any`` et chaque # ``class X(BaseModel)`` génère un faux positif # ``Class cannot subclass "BaseModel"``. plugins = ["pydantic.mypy"] [[tool.mypy.overrides]] module = "picarones.domain.*" strict = true # A1 baseline : ces deux checks pré-existants génèrent ~70 % des erreurs # (annotations ``dict``/``tuple`` sans paramètres génériques, retours typés # ``Any``). Plutôt que de les fixer en bloc dans A1 et risquer une # régression, on les laisse explicitement désactivés et on les ré-active # en Sprint A11 (durcissement progressif du type-checking). disallow_any_generics = false warn_return_any = false # Ratchet audit prod P1.3 — ``picarones.formats`` promu strict COMPLET # (couche pure parsing/sérialisation, petite et sans état). Les 9 # erreurs (args génériques + retours ``Any``) ont été corrigées, donc # ``disallow_any_generics``/``warn_return_any`` restent actifs ici # (contrairement à domain qui les relâche encore). Prochaine cible du # ratchet : ``pipeline`` puis ``evaluation``. [[tool.mypy.overrides]] module = "picarones.formats.*" strict = true # ────────────────────────────────────────────────────────────────── # Sprint A1 (B-7) — configuration bandit (scan sécurité statique). # # Politique : on refuse tout finding HIGH/CRITICAL en CI. Les MEDIUM # documentés ci-dessous comme "accepté" font l'objet d'un suivi explicite # (sprint cible mentionné). # # Exclusions documentées : # - B101 (assert_used) : pytest utilise systématiquement ``assert`` ; # - B105/B106 (hardcoded_password) : nos fixtures utilisent des chaînes # ``"password"`` dans des contextes purement de test ; # - B310 (urllib_urlopen) : tous nos appels ``urllib.urlopen`` ciblent # des endpoints HTTPS connus (Mistral, Google Vision, Azure DI, # Gallica, HF Hub, eScriptorium, Ollama). Un audit ligne par ligne # est tracé dans docs/audits/security-urllib-audit.md ; # - B608 (hardcoded_sql_expressions) : deux occurrences en # ``measurements/history.py:341`` et ``web/jobs.py:235`` ; la seconde # est un faux positif vérifié (audit institutional-readiness §6 F-1), # la première utilise une whitelist de colonnes documentée ; # - B615 (huggingface_unsafe_download) : à corriger en pinant la # ``revision`` dans extras/importers/huggingface.py — Sprint A5 ; # - B701 (jinja2_autoescape_false) : décision de design pré-existante # (cf. report/generator.py:606-611) ; les variables injectées sont # pré-échappées par les modules de rendu via ``html.escape``. # Refactor à effectuer dans le scope a11y (Sprint A6 ou A7) en # passant à ``select_autoescape`` + marquage ``|safe`` explicite des # blocs JSON/SVG. # ────────────────────────────────────────────────────────────────── [tool.bandit] exclude_dirs = ["tests", "picarones/report/vendor"] skips = ["B101", "B105", "B106", "B310", "B608", "B615", "B701"] [tool.ruff] # Configuration centralisée pour que `ruff check`, `make lint` et le job CI # produisent exactement les mêmes résultats sans flags en ligne de commande. line-length = 100 target-version = "py311" [tool.ruff.lint] # E/W = pycodestyle, F = pyflakes. On conserve les mêmes règles que le CI # d'origine (avant Sprint 22), qui excluait les lignes longues (E501) et les # imports non-top (E402, parfois utiles pour imports conditionnels). # # Ratchet audit prod P1.4 — durcissement par famille, JAMAIS en bloc. # Famille activée uniquement si elle est déjà à ZÉRO violation (lock # sans churn) : # - ISC : implicit-str-concat — attrape le bug « virgule oubliée # dans une liste de chaînes → concaténation silencieuse ». # - FLY : static-join-to-fstring. # - G : logging-format (pas de f-string/format dans logging.*). # Backlog ratchet (NON activé — churn massif, à faire 1 famille/sprint # dédié, avec baseline) : UP≈736, RUF≈498, TRY≈368, I≈95, PERF≈47, # B≈34, ARG≈32, SIM≈25, C4≈15, RET≈11, PTH≈9. select = ["E", "W", "F", "ISC", "FLY", "G"] ignore = ["E501", "E402"]