[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/INSTALL.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/core/ + 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.core.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"] # 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.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", "web/static/*.css", "web/static/*.js", "web/templates/*.j2", "web/templates/*.html", "report/templates/*.j2", "report/templates/*.html", "report/templates/*.css", "report/templates/*.js", "report/i18n/*.json", "measurements/narrative/templates/*.yaml", "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 : marker network non sélectionné. Override via # ``pytest -m network`` (CI réseau-friendly) ou ``pytest -m ""``. addopts = "-v --tb=short -m 'not network'" # 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", ] # ────────────────────────────────────────────────────────────────── # 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.core`` est en mode ``strict`` car c'est la # couche la plus stable et l'API publique. Les autres cercles 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`). # ────────────────────────────────────────────────────────────────── [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.core.*" 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"]