[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 = [ "click>=8.1.0", "jiwer>=3.0.0", "Pillow>=10.0.0", "pyyaml>=6.0.0", "pytesseract>=0.3.10", "tqdm>=4.66.0", "numpy>=1.24.0", "jinja2>=3.1.0", # XML parsing sécurisé contre les attaques XXE / Billion Laughs. # Utilisé par ``picarones.web.corpus_utils`` pour le parsing ALTO/PAGE # quand un utilisateur uploade un corpus XML. "defusedxml>=0.7.1", ] [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"] # 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 complète (tous les extras sauf les OCR cloud) all = [ "picarones[web,hf,llm,dev]", ] [project.scripts] picarones = "picarones.interfaces.cli._legacy: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", "interfaces/web/_legacy/static/*.css", "interfaces/web/_legacy/static/*.js", "interfaces/web/_legacy/templates/*.j2", "interfaces/web/_legacy/templates/*.html", "report/templates/*.j2", "report/templates/*.html", "report/templates/*.css", "report/templates/*.js", "report/i18n/*.json", "data/*.yaml", "report/glossary/*.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``, ``live`` et # ``regression`` non sélectionnés. Override en local via # ``pytest -m network`` ou ``pytest -m live`` (avec env vars / # binaires correctement configurés). Le marker ``regression`` # (harness legacy ↔ rewrite) est lent ; opt-in via # ``pytest -m regression`` ou run dédié en CI. ``-m ""`` pour # tout exécuter. addopts = "-v --tb=short -m 'not network and not live and not regression'" # 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'", "regression: harness de régression legacy ↔ rewrite (tests/regression/legacy_vs_rewrite/) ; exclus par défaut, opt-in via 'pytest -m regression' ou job CI dédié", ] # ────────────────────────────────────────────────────────────────── # 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 du rewrite (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) — au fur et à mesure des sprints suivants, on monte le # niveau (Sprint A11 resserre `picarones.measurements`). # Note : avant les Lots A-G de la migration legacy → rewrite, ce strict # ciblait ``picarones.core`` ; ce paquet a été entièrement supprimé, # son contenu est désormais dans ``domain/``, ``evaluation/`` et ``formats/``. # ────────────────────────────────────────────────────────────────── [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 cercles sont suivis silencieusement # pour éviter de propager les erreurs des cercles non encore typés. # Sprint A11 resserrera progressivement. follow_imports = "silent" [[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 # ────────────────────────────────────────────────────────────────── # 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). select = ["E", "W", "F"] ignore = ["E501", "E402"]