Picarones / pyproject.toml
Claude
chore(quality): ratchet ruff (ISC/FLY/G) + mypy strict formats/ + Protocol NER (P1.3/P1.4)
9efdce8 unverified
[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<sha>``.
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<N>+g<sha>`` (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"]