File size: 12,799 Bytes
49cc409
628d92a
 
 
 
 
 
49cc409
 
 
 
628d92a
 
 
bff1348
49cc409
 
 
cecde1f
bff1348
49cc409
b283975
49cc409
 
89d5b21
49cc409
bff1348
49cc409
bff1348
 
 
49cc409
 
 
 
 
 
 
 
 
a0dc23e
de46be0
 
 
 
49cc409
 
bff1348
cecde1f
 
 
 
 
bff1348
49cc409
89d5b21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bff1348
7afb12e
35001ff
 
 
d733b48
 
 
 
bff1348
b4cc1c5
bff1348
 
 
 
 
 
 
001e605
bff1348
 
 
 
 
 
 
628d92a
 
 
 
 
 
bff1348
 
628d92a
bff1348
49cc409
 
 
 
628d92a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49cc409
 
 
 
8c509eb
a0dc23e
 
 
b914841
 
 
a0dc23e
 
 
 
 
89d5b21
b6bdecc
76e79a0
a0dc23e
8c509eb
49cc409
 
4255304
 
 
 
 
563a0f0
 
 
89d5b21
 
 
 
 
563a0f0
 
 
 
 
 
 
 
 
89d5b21
 
563a0f0
89d5b21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a76711b
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
[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 = [
    "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<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",
    "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"]