Spaces:
Running
feat(ci): Sprint A1 — Hardening CI (B-7, B-8, M-4, M-15, m-7, m-8, m-9)
Browse filesPhase 0 du plan de remédiation institutional-readiness-2026-05.
Objectif : poser les garde-fous qui empêcheront les sprints suivants
de régresser sur ce qu'ils corrigeront.
pyproject.toml :
- [dev] : ajout pytest-timeout, mypy, bandit, pip-audit
- [tool.pytest.ini_options] : timeout=300 mode thread (M-15)
- [tool.coverage] : config explicite (omit vendor + templates)
- [tool.mypy] : strict sur picarones.core.*, lax ailleurs (M-4)
baseline pre-existante (~100 erreurs type-arg/no-any-return) gérée
via 2 checks désactivés à ré-activer en Sprint A11
- [tool.bandit] : skips documentés pour B310/B608/B615/B701 chacun
avec sprint cible explicite
- classifier Python 3.13 ajouté (m-8)
- Fix bug latéral package_data : core/narrative/templates → measurements/
picarones/py.typed (PEP 561) : marqueur de typage pour les consommateurs externes.
tests/core/test_public_api_signatures.py (m-9) :
63 tests qui verrouillent les défauts contractuels et le typage des
signatures de l'API publique. Échoue si un PR change un default value
(ex: corpus_lang="fr" → "en") ou supprime une annotation.
.github/workflows/ci.yml :
- Matrice étendue avec Python 3.13 (mode informationnel, continue-on-error 6 mois)
- pip/setuptools/wheel mis à jour en début de job (évite CVEs runner)
- --cov-fail-under=85 (baseline mesuré 87 %, marge 2 pts) (B-8)
- Job typecheck (mypy strict sur core/) (M-4)
- Job security (bandit + pip-audit + trivy image + trivy Dockerfile) (B-7)
trivy bloque sur HIGH/CRITICAL avec ignore-unfixed=true
.github/workflows/precommit.yml (nouveau) (m-7) :
Rejoue tous les hooks .pre-commit-config.yaml en CI, empêche le bypass
via git commit --no-verify. Cache pre-commit, run sur diff PR ou full
sur push, --show-diff-on-failure pour debug rapide.
Tests à la racine : 63/63 sur le nouveau test file. mypy core/ passe
exit 0. bandit 0 HIGH 0 MEDIUM. ruff inchangé (clean).
La suite complète (3356 baseline) en cours de revérif au prochain tour.
- .github/workflows/ci.yml +120 -7
- .github/workflows/precommit.yml +73 -0
- picarones/py.typed +0 -0
- pyproject.toml +121 -3
- tests/core/test_public_api_signatures.py +144 -0
|
@@ -1,9 +1,12 @@
|
|
| 1 |
# .github/workflows/ci.yml — Picarones CI/CD
|
| 2 |
#
|
| 3 |
-
# Pipeline GitHub Actions :
|
| 4 |
-
# - Tests sur Python 3.11
|
| 5 |
# - Linux, macOS, Windows
|
| 6 |
-
# -
|
|
|
|
|
|
|
|
|
|
| 7 |
# - Build de la distribution Python
|
| 8 |
# - Vérification de l'exécutable demo
|
| 9 |
|
|
@@ -31,7 +34,14 @@ jobs:
|
|
| 31 |
fail-fast: false
|
| 32 |
matrix:
|
| 33 |
os: [ubuntu-latest, macos-latest, windows-latest]
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
steps:
|
| 37 |
- name: Checkout
|
|
@@ -65,17 +75,25 @@ jobs:
|
|
| 65 |
shell: pwsh
|
| 66 |
|
| 67 |
# ── Dépendances Python ──────────────────────────────────────
|
|
|
|
|
|
|
| 68 |
- name: Install dependencies
|
| 69 |
run: |
|
| 70 |
-
python -m pip install --upgrade pip
|
| 71 |
pip install -e ".[dev,web]"
|
| 72 |
|
| 73 |
# ── Tests ───────────────────────────────────────────────────
|
|
|
|
|
|
|
| 74 |
- name: Run tests
|
|
|
|
|
|
|
|
|
|
| 75 |
shell: bash
|
| 76 |
run: |
|
| 77 |
pytest tests/ -q --tb=short --no-header \
|
| 78 |
-
--cov=picarones --cov-report=xml --cov-report=term-missing
|
|
|
|
| 79 |
env:
|
| 80 |
PYTHONIOENCODING: utf-8
|
| 81 |
PYTHONUTF8: "1"
|
|
@@ -201,7 +219,102 @@ jobs:
|
|
| 201 |
run: ruff check picarones/ tests/
|
| 202 |
|
| 203 |
# ──────────────────────────────────────────────────────────────────
|
| 204 |
-
# Job 5 :
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
# Commenté par défaut — activer si vous avez un corpus de référence
|
| 206 |
# ──────────────────────────────────────────────────────────────────
|
| 207 |
# regression-check:
|
|
|
|
| 1 |
# .github/workflows/ci.yml — Picarones CI/CD
|
| 2 |
#
|
| 3 |
+
# Pipeline GitHub Actions (mis à jour Sprint A1 — Hardening CI) :
|
| 4 |
+
# - Tests sur Python 3.11 / 3.12 / 3.13 (3.13 informationnel, 6 mois)
|
| 5 |
# - Linux, macOS, Windows
|
| 6 |
+
# - Couverture exigée >= 85 % (--cov-fail-under, plancher 2 pts sous baseline 87 %)
|
| 7 |
+
# - Timeout pytest 5 min par test individuel (pytest-timeout, mode thread)
|
| 8 |
+
# - Type-check mypy (strict sur picarones/core/, lax ailleurs — durci en A11)
|
| 9 |
+
# - Scanners sécurité : bandit (statique) + pip-audit (CVE deps) + trivy (image)
|
| 10 |
# - Build de la distribution Python
|
| 11 |
# - Vérification de l'exécutable demo
|
| 12 |
|
|
|
|
| 34 |
fail-fast: false
|
| 35 |
matrix:
|
| 36 |
os: [ubuntu-latest, macos-latest, windows-latest]
|
| 37 |
+
# 3.13 ajouté Sprint A1 (item m-8). Reste informationnel
|
| 38 |
+
# (continue-on-error sur Linux uniquement) pendant 6 mois pour
|
| 39 |
+
# tracker la compat sans bloquer.
|
| 40 |
+
python-version: ["3.11", "3.12", "3.13"]
|
| 41 |
+
include:
|
| 42 |
+
- python-version: "3.13"
|
| 43 |
+
os: ubuntu-latest
|
| 44 |
+
experimental: true
|
| 45 |
|
| 46 |
steps:
|
| 47 |
- name: Checkout
|
|
|
|
| 75 |
shell: pwsh
|
| 76 |
|
| 77 |
# ── Dépendances Python ──────────────────────────────────────
|
| 78 |
+
# Mise à jour pip/setuptools/wheel en début de job (Sprint A1) pour
|
| 79 |
+
# éviter les CVE qui dorment dans les images runner GitHub.
|
| 80 |
- name: Install dependencies
|
| 81 |
run: |
|
| 82 |
+
python -m pip install --upgrade pip setuptools wheel
|
| 83 |
pip install -e ".[dev,web]"
|
| 84 |
|
| 85 |
# ── Tests ───────────────────────────────────────────────────
|
| 86 |
+
# Sprint A1 : --cov-fail-under=85 (baseline mesuré 87 %, marge 2 pts).
|
| 87 |
+
# pytest-timeout est configuré dans pyproject.toml [tool.pytest.ini_options].
|
| 88 |
- name: Run tests
|
| 89 |
+
# Sur Python 3.13, on continue malgré une erreur pour ne pas bloquer
|
| 90 |
+
# le merge pendant la fenêtre informationnelle de 6 mois (m-8).
|
| 91 |
+
continue-on-error: ${{ matrix.python-version == '3.13' }}
|
| 92 |
shell: bash
|
| 93 |
run: |
|
| 94 |
pytest tests/ -q --tb=short --no-header \
|
| 95 |
+
--cov=picarones --cov-report=xml --cov-report=term-missing \
|
| 96 |
+
--cov-fail-under=85
|
| 97 |
env:
|
| 98 |
PYTHONIOENCODING: utf-8
|
| 99 |
PYTHONUTF8: "1"
|
|
|
|
| 219 |
run: ruff check picarones/ tests/
|
| 220 |
|
| 221 |
# ──────────────────────────────────────────────────────────────────
|
| 222 |
+
# Job 5 : Type-checking — Sprint A1 (item M-4)
|
| 223 |
+
#
|
| 224 |
+
# mypy est configuré dans pyproject.toml [tool.mypy] :
|
| 225 |
+
# - strict sur picarones.core.* (10 modules)
|
| 226 |
+
# - lax ailleurs (follow_imports=silent)
|
| 227 |
+
# Deux checks pré-existants désactivés (disallow_any_generics et
|
| 228 |
+
# warn_return_any), à ré-activer en Sprint A11 après fix des
|
| 229 |
+
# ~100 erreurs baseline.
|
| 230 |
+
# ──────────────────────────────────────────────────────────────────
|
| 231 |
+
typecheck:
|
| 232 |
+
name: Type checking (mypy)
|
| 233 |
+
runs-on: ubuntu-latest
|
| 234 |
+
|
| 235 |
+
steps:
|
| 236 |
+
- name: Checkout
|
| 237 |
+
uses: actions/checkout@v4
|
| 238 |
+
|
| 239 |
+
- name: Set up Python
|
| 240 |
+
uses: actions/setup-python@v5
|
| 241 |
+
with:
|
| 242 |
+
python-version: "3.11"
|
| 243 |
+
cache: pip
|
| 244 |
+
|
| 245 |
+
- name: Install dependencies
|
| 246 |
+
run: |
|
| 247 |
+
python -m pip install --upgrade pip setuptools wheel
|
| 248 |
+
pip install -e ".[dev,web,stats]"
|
| 249 |
+
|
| 250 |
+
- name: Run mypy on picarones/core (strict)
|
| 251 |
+
run: python -m mypy picarones/core/
|
| 252 |
+
|
| 253 |
+
# ──────────────────────────────────────────────────────────────────
|
| 254 |
+
# Job 6 : Sécurité — Sprint A1 (item B-7)
|
| 255 |
+
#
|
| 256 |
+
# bandit : scan statique du code Python (HIGH/MEDIUM bloquants).
|
| 257 |
+
# pip-audit : CVEs des dépendances installées.
|
| 258 |
+
# trivy : scan du Dockerfile + image résultante (HIGH/CRITICAL bloquants).
|
| 259 |
+
#
|
| 260 |
+
# Configuration bandit dans pyproject.toml [tool.bandit] avec exclusions
|
| 261 |
+
# documentées (B310, B608, B615, B701 — chacune avec sprint cible).
|
| 262 |
+
# ──────────────────────────────────────────────────────────────────
|
| 263 |
+
security:
|
| 264 |
+
name: Security scanners
|
| 265 |
+
runs-on: ubuntu-latest
|
| 266 |
+
|
| 267 |
+
steps:
|
| 268 |
+
- name: Checkout
|
| 269 |
+
uses: actions/checkout@v4
|
| 270 |
+
|
| 271 |
+
- name: Set up Python
|
| 272 |
+
uses: actions/setup-python@v5
|
| 273 |
+
with:
|
| 274 |
+
python-version: "3.11"
|
| 275 |
+
cache: pip
|
| 276 |
+
|
| 277 |
+
- name: Install scanners
|
| 278 |
+
run: |
|
| 279 |
+
python -m pip install --upgrade pip setuptools wheel
|
| 280 |
+
pip install -e ".[dev,web]"
|
| 281 |
+
|
| 282 |
+
# bandit : -ll = LOW + above. La config pyproject.toml exclut les
|
| 283 |
+
# informationnels acceptés ; tout HIGH/MEDIUM nouveau bloque.
|
| 284 |
+
- name: Run bandit
|
| 285 |
+
run: python -m bandit -r picarones/ -ll -c pyproject.toml
|
| 286 |
+
|
| 287 |
+
# pip-audit : --skip-editable car picarones n'est pas sur PyPI.
|
| 288 |
+
# En CI, l'env est frais (pip à jour grâce au step précédent) donc
|
| 289 |
+
# les CVEs trouvées sont des vraies vulnérabilités déclarées.
|
| 290 |
+
- name: Run pip-audit
|
| 291 |
+
run: python -m pip_audit --skip-editable --strict
|
| 292 |
+
|
| 293 |
+
# trivy : scan du Dockerfile + image résultante. Build local pour
|
| 294 |
+
# auditer ce qui sera réellement déployé sur HuggingFace Space.
|
| 295 |
+
- name: Build Docker image for scan
|
| 296 |
+
run: docker build -t picarones:ci-scan .
|
| 297 |
+
|
| 298 |
+
- name: Run Trivy vulnerability scanner (image)
|
| 299 |
+
uses: aquasecurity/trivy-action@0.28.0
|
| 300 |
+
with:
|
| 301 |
+
image-ref: 'picarones:ci-scan'
|
| 302 |
+
format: 'table'
|
| 303 |
+
exit-code: '1'
|
| 304 |
+
ignore-unfixed: true
|
| 305 |
+
severity: 'HIGH,CRITICAL'
|
| 306 |
+
vuln-type: 'os,library'
|
| 307 |
+
|
| 308 |
+
- name: Run Trivy on Dockerfile (config scan)
|
| 309 |
+
uses: aquasecurity/trivy-action@0.28.0
|
| 310 |
+
with:
|
| 311 |
+
scan-type: 'config'
|
| 312 |
+
scan-ref: 'Dockerfile'
|
| 313 |
+
exit-code: '1'
|
| 314 |
+
severity: 'HIGH,CRITICAL'
|
| 315 |
+
|
| 316 |
+
# ──────────────────────────────────────────────────────────────────
|
| 317 |
+
# Job 7 : CI/CD — Détection de régression CER (optionnel)
|
| 318 |
# Commenté par défaut — activer si vous avez un corpus de référence
|
| 319 |
# ──────────────────────────────────────────────────────────────────
|
| 320 |
# regression-check:
|
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# .github/workflows/precommit.yml — rejoue les hooks pre-commit en CI
|
| 2 |
+
#
|
| 3 |
+
# Sprint A1 (item m-7 de l'audit institutional-readiness-2026-05).
|
| 4 |
+
#
|
| 5 |
+
# Pourquoi : .pre-commit-config.yaml définit 12 hooks (ruff, trailing
|
| 6 |
+
# whitespace, YAML/JSON/TOML check, merge conflict marker, detect-private-key,
|
| 7 |
+
# check-added-large-files). Sans ce workflow, un développeur peut bypass
|
| 8 |
+
# les hooks via ``git commit --no-verify`` et la CI ne le détecte pas.
|
| 9 |
+
#
|
| 10 |
+
# Ce job rejoue *exactement* les hooks ``.pre-commit-config.yaml`` sur
|
| 11 |
+
# l'intégralité du diff de la PR. Si un hook échoue, la PR est bloquée.
|
| 12 |
+
#
|
| 13 |
+
# Le job ``lint`` de ci.yml reste en place pour valider ruff sur tout
|
| 14 |
+
# l'arbre (couverture inter-fichiers que pre-commit ne fait pas par défaut).
|
| 15 |
+
|
| 16 |
+
name: Pre-commit hooks
|
| 17 |
+
|
| 18 |
+
on:
|
| 19 |
+
push:
|
| 20 |
+
branches: [main, develop, "feature/**", "sprint/**", "claude/**"]
|
| 21 |
+
pull_request:
|
| 22 |
+
branches: [main, develop]
|
| 23 |
+
workflow_dispatch:
|
| 24 |
+
|
| 25 |
+
permissions:
|
| 26 |
+
contents: read
|
| 27 |
+
|
| 28 |
+
jobs:
|
| 29 |
+
precommit:
|
| 30 |
+
name: Replay pre-commit hooks
|
| 31 |
+
runs-on: ubuntu-latest
|
| 32 |
+
|
| 33 |
+
steps:
|
| 34 |
+
- name: Checkout
|
| 35 |
+
# fetch-depth: 0 nécessaire pour que pre-commit puisse comparer
|
| 36 |
+
# la PR à sa base et ne lance les hooks que sur les fichiers
|
| 37 |
+
# modifiés (rapide et conforme à l'usage local).
|
| 38 |
+
uses: actions/checkout@v4
|
| 39 |
+
with:
|
| 40 |
+
fetch-depth: 0
|
| 41 |
+
|
| 42 |
+
- name: Set up Python
|
| 43 |
+
uses: actions/setup-python@v5
|
| 44 |
+
with:
|
| 45 |
+
python-version: "3.11"
|
| 46 |
+
cache: pip
|
| 47 |
+
|
| 48 |
+
- name: Install pre-commit
|
| 49 |
+
run: |
|
| 50 |
+
python -m pip install --upgrade pip
|
| 51 |
+
pip install pre-commit
|
| 52 |
+
|
| 53 |
+
- name: Cache pre-commit hooks
|
| 54 |
+
uses: actions/cache@v4
|
| 55 |
+
with:
|
| 56 |
+
path: ~/.cache/pre-commit
|
| 57 |
+
key: precommit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
|
| 58 |
+
|
| 59 |
+
# Sur push : on rejoue les hooks sur tous les fichiers (mode --all-files)
|
| 60 |
+
# car on n'a pas de "base" naturelle. Sur PR : --from-ref / --to-ref
|
| 61 |
+
# pour ne lancer que sur le diff (rapide).
|
| 62 |
+
- name: Run pre-commit on PR diff
|
| 63 |
+
if: github.event_name == 'pull_request'
|
| 64 |
+
run: |
|
| 65 |
+
pre-commit run \
|
| 66 |
+
--from-ref ${{ github.event.pull_request.base.sha }} \
|
| 67 |
+
--to-ref ${{ github.event.pull_request.head.sha }} \
|
| 68 |
+
--show-diff-on-failure
|
| 69 |
+
|
| 70 |
+
- name: Run pre-commit on push (all files)
|
| 71 |
+
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
| 72 |
+
run: |
|
| 73 |
+
pre-commit run --all-files --show-diff-on-failure
|
|
File without changes
|
|
@@ -15,6 +15,7 @@ classifiers = [
|
|
| 15 |
"Development Status :: 4 - Beta",
|
| 16 |
"Programming Language :: Python :: 3.11",
|
| 17 |
"Programming Language :: Python :: 3.12",
|
|
|
|
| 18 |
"License :: OSI Approved :: Apache Software License",
|
| 19 |
"Operating System :: OS Independent",
|
| 20 |
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
@@ -45,8 +46,24 @@ Changelog = "https://github.com/maribakulj/Picarones/blob/main/CHANGELOG.md"
|
|
| 45 |
"Bug Tracker" = "https://github.com/maribakulj/Picarones/issues"
|
| 46 |
|
| 47 |
[project.optional-dependencies]
|
| 48 |
-
# Développement et tests
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
# Interface web FastAPI
|
| 51 |
web = ["fastapi>=0.111.0", "uvicorn[standard]>=0.29.0", "httpx>=0.27.0", "python-multipart>=0.0.9"]
|
| 52 |
# Tests statistiques avancés (Wilcoxon exact, Friedman chi² exact, Nemenyi)
|
|
@@ -115,7 +132,7 @@ picarones = [
|
|
| 115 |
"report/templates/*.css",
|
| 116 |
"report/templates/*.js",
|
| 117 |
"report/i18n/*.json",
|
| 118 |
-
"
|
| 119 |
"data/*.yaml",
|
| 120 |
"report/glossary/*.yaml",
|
| 121 |
]
|
|
@@ -123,6 +140,107 @@ picarones = [
|
|
| 123 |
[tool.pytest.ini_options]
|
| 124 |
testpaths = ["tests"]
|
| 125 |
addopts = "-v --tb=short"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
[tool.ruff]
|
| 128 |
# Configuration centralisée pour que `ruff check`, `make lint` et le job CI
|
|
|
|
| 15 |
"Development Status :: 4 - Beta",
|
| 16 |
"Programming Language :: Python :: 3.11",
|
| 17 |
"Programming Language :: Python :: 3.12",
|
| 18 |
+
"Programming Language :: Python :: 3.13",
|
| 19 |
"License :: OSI Approved :: Apache Software License",
|
| 20 |
"Operating System :: OS Independent",
|
| 21 |
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
|
|
| 46 |
"Bug Tracker" = "https://github.com/maribakulj/Picarones/issues"
|
| 47 |
|
| 48 |
[project.optional-dependencies]
|
| 49 |
+
# Développement et tests.
|
| 50 |
+
# pytest-timeout (Sprint A1) garantit qu'aucun test individuel ne hang la CI
|
| 51 |
+
# au-delà de la limite définie dans [tool.pytest.ini_options].
|
| 52 |
+
# mypy (Sprint A1, M-4) : type-check strict sur picarones/core/ + lax ailleurs.
|
| 53 |
+
# bandit (Sprint A1, B-7) : scanner sécurité statique du code Python.
|
| 54 |
+
# pip-audit (Sprint A1, B-7) : détection des CVE des dépendances installées.
|
| 55 |
+
dev = [
|
| 56 |
+
"pytest>=7.4.0",
|
| 57 |
+
"pytest-cov>=4.1.0",
|
| 58 |
+
"pytest-timeout>=2.3.0",
|
| 59 |
+
"httpx>=0.27.0",
|
| 60 |
+
"fastapi>=0.111.0",
|
| 61 |
+
"uvicorn[standard]>=0.29.0",
|
| 62 |
+
"python-multipart>=0.0.9",
|
| 63 |
+
"mypy>=1.10.0",
|
| 64 |
+
"bandit>=1.7.0",
|
| 65 |
+
"pip-audit>=2.7.0",
|
| 66 |
+
]
|
| 67 |
# Interface web FastAPI
|
| 68 |
web = ["fastapi>=0.111.0", "uvicorn[standard]>=0.29.0", "httpx>=0.27.0", "python-multipart>=0.0.9"]
|
| 69 |
# Tests statistiques avancés (Wilcoxon exact, Friedman chi² exact, Nemenyi)
|
|
|
|
| 132 |
"report/templates/*.css",
|
| 133 |
"report/templates/*.js",
|
| 134 |
"report/i18n/*.json",
|
| 135 |
+
"measurements/narrative/templates/*.yaml",
|
| 136 |
"data/*.yaml",
|
| 137 |
"report/glossary/*.yaml",
|
| 138 |
]
|
|
|
|
| 140 |
[tool.pytest.ini_options]
|
| 141 |
testpaths = ["tests"]
|
| 142 |
addopts = "-v --tb=short"
|
| 143 |
+
# Sprint A1 (M-15) : aucun test individuel ne doit dépasser 5 minutes.
|
| 144 |
+
# Mode "thread" car certains tests utilisent ProcessPoolExecutor qui est
|
| 145 |
+
# incompatible avec le timeout en mode "signal" sur certaines plateformes.
|
| 146 |
+
timeout = 300
|
| 147 |
+
timeout_method = "thread"
|
| 148 |
+
# Marqueurs personnalisés. ``slow`` peut être désélectionné via
|
| 149 |
+
# ``pytest -m "not slow"`` pour les boucles de dev.
|
| 150 |
+
markers = [
|
| 151 |
+
"slow: tests longs (corpus de référence, intégration cloud) ; non bloquants en dev local",
|
| 152 |
+
]
|
| 153 |
+
|
| 154 |
+
# ──────────────────────────────────────────────────────────────────
|
| 155 |
+
# Sprint A1 (B-8) — seuil minimal de couverture appliqué en CI.
|
| 156 |
+
# Le baseline est mesuré en début de sprint puis le plancher est posé
|
| 157 |
+
# 2 points en dessous, pour laisser une marge de manœuvre aux PR
|
| 158 |
+
# tout en interdisant une dégradation franche.
|
| 159 |
+
# ──────────────────────────────────────────────────────────────────
|
| 160 |
+
[tool.coverage.run]
|
| 161 |
+
source = ["picarones"]
|
| 162 |
+
omit = [
|
| 163 |
+
"picarones/report/vendor/*", # Chart.js minifié vendoré
|
| 164 |
+
"picarones/report/templates/*", # templates Jinja2 + JS, pas du code Python
|
| 165 |
+
"*/tests/*",
|
| 166 |
+
]
|
| 167 |
+
parallel = true
|
| 168 |
+
|
| 169 |
+
[tool.coverage.report]
|
| 170 |
+
# Le seuil est appliqué via la flag CLI ``--cov-fail-under=N`` dans la CI
|
| 171 |
+
# (cf. .github/workflows/ci.yml) plutôt qu'ici, pour permettre aux
|
| 172 |
+
# développeurs de lancer ``pytest --cov`` localement sans échec sur les
|
| 173 |
+
# fichiers qu'ils ne touchent pas.
|
| 174 |
+
exclude_lines = [
|
| 175 |
+
"pragma: no cover",
|
| 176 |
+
"raise NotImplementedError",
|
| 177 |
+
"if TYPE_CHECKING:",
|
| 178 |
+
"if __name__ == .__main__.:",
|
| 179 |
+
]
|
| 180 |
+
|
| 181 |
+
# ──────────────────────────────────────────────────────────────────
|
| 182 |
+
# Sprint A1 (M-4) — type-checking gradient.
|
| 183 |
+
#
|
| 184 |
+
# Stratégie : ``picarones.core`` est en mode ``strict`` car c'est la
|
| 185 |
+
# couche la plus stable et l'API publique. Les autres cercles passent
|
| 186 |
+
# en mode permissif (``ignore_missing_imports`` + pas de strict) — au
|
| 187 |
+
# fur et à mesure des sprints suivants, on monte le niveau (Sprint A11
|
| 188 |
+
# resserre `picarones.measurements`).
|
| 189 |
+
# ──────────────────────────────────────────────────────────────────
|
| 190 |
+
[tool.mypy]
|
| 191 |
+
python_version = "3.11"
|
| 192 |
+
ignore_missing_imports = true
|
| 193 |
+
warn_unused_configs = true
|
| 194 |
+
warn_redundant_casts = true
|
| 195 |
+
warn_unused_ignores = true
|
| 196 |
+
no_implicit_optional = true
|
| 197 |
+
# Les imports vers les autres cercles sont suivis silencieusement
|
| 198 |
+
# pour éviter de propager les erreurs des cercles non encore typés.
|
| 199 |
+
# Sprint A11 resserrera progressivement.
|
| 200 |
+
follow_imports = "silent"
|
| 201 |
+
|
| 202 |
+
[[tool.mypy.overrides]]
|
| 203 |
+
module = "picarones.core.*"
|
| 204 |
+
strict = true
|
| 205 |
+
# A1 baseline : ces deux checks pré-existants génèrent ~70 % des erreurs
|
| 206 |
+
# (annotations ``dict``/``tuple`` sans paramètres génériques, retours typés
|
| 207 |
+
# ``Any``). Plutôt que de les fixer en bloc dans A1 et risquer une
|
| 208 |
+
# régression, on les laisse explicitement désactivés et on les ré-active
|
| 209 |
+
# en Sprint A11 (durcissement progressif du type-checking).
|
| 210 |
+
disallow_any_generics = false
|
| 211 |
+
warn_return_any = false
|
| 212 |
+
|
| 213 |
+
# ──────────────────────────────────────────────────────────────────
|
| 214 |
+
# Sprint A1 (B-7) — configuration bandit (scan sécurité statique).
|
| 215 |
+
#
|
| 216 |
+
# Politique : on refuse tout finding HIGH/CRITICAL en CI. Les MEDIUM
|
| 217 |
+
# documentés ci-dessous comme "accepté" font l'objet d'un suivi explicite
|
| 218 |
+
# (sprint cible mentionné).
|
| 219 |
+
#
|
| 220 |
+
# Exclusions documentées :
|
| 221 |
+
# - B101 (assert_used) : pytest utilise systématiquement ``assert`` ;
|
| 222 |
+
# - B105/B106 (hardcoded_password) : nos fixtures utilisent des chaînes
|
| 223 |
+
# ``"password"`` dans des contextes purement de test ;
|
| 224 |
+
# - B310 (urllib_urlopen) : tous nos appels ``urllib.urlopen`` ciblent
|
| 225 |
+
# des endpoints HTTPS connus (Mistral, Google Vision, Azure DI,
|
| 226 |
+
# Gallica, HF Hub, eScriptorium, Ollama). Un audit ligne par ligne
|
| 227 |
+
# est tracé dans docs/audits/security-urllib-audit.md ;
|
| 228 |
+
# - B608 (hardcoded_sql_expressions) : deux occurrences en
|
| 229 |
+
# ``measurements/history.py:341`` et ``web/jobs.py:235`` ; la seconde
|
| 230 |
+
# est un faux positif vérifié (audit institutional-readiness §6 F-1),
|
| 231 |
+
# la première utilise une whitelist de colonnes documentée ;
|
| 232 |
+
# - B615 (huggingface_unsafe_download) : à corriger en pinant la
|
| 233 |
+
# ``revision`` dans extras/importers/huggingface.py — Sprint A5 ;
|
| 234 |
+
# - B701 (jinja2_autoescape_false) : décision de design pré-existante
|
| 235 |
+
# (cf. report/generator.py:606-611) ; les variables injectées sont
|
| 236 |
+
# pré-échappées par les modules de rendu via ``html.escape``.
|
| 237 |
+
# Refactor à effectuer dans le scope a11y (Sprint A6 ou A7) en
|
| 238 |
+
# passant à ``select_autoescape`` + marquage ``|safe`` explicite des
|
| 239 |
+
# blocs JSON/SVG.
|
| 240 |
+
# ──────────────────────────────────────────────────────────────────
|
| 241 |
+
[tool.bandit]
|
| 242 |
+
exclude_dirs = ["tests", "picarones/report/vendor"]
|
| 243 |
+
skips = ["B101", "B105", "B106", "B310", "B608", "B615", "B701"]
|
| 244 |
|
| 245 |
[tool.ruff]
|
| 246 |
# Configuration centralisée pour que `ruff check`, `make lint` et le job CI
|
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Garde-fou contractuel sur les signatures de l'API publique de ``picarones``.
|
| 2 |
+
|
| 3 |
+
Sprint A1 (item m-9 de l'audit institutional-readiness-2026-05).
|
| 4 |
+
|
| 5 |
+
Le module ``tests/core/test_public_api.py`` vérifie déjà *quels* symboles
|
| 6 |
+
sont exportés. Ce module-ci verrouille en plus les **valeurs par défaut**
|
| 7 |
+
des paramètres des fonctions publiques. Sans ce verrou, un PR peut
|
| 8 |
+
silencieusement changer un défaut documenté (ex : ``corpus_lang="fr"``
|
| 9 |
+
qui devient ``corpus_lang="en"``) et casser la rétrocompatibilité de
|
| 10 |
+
tous les consommateurs externes — y compris des notebooks de chercheurs
|
| 11 |
+
pinés sur une version mineure.
|
| 12 |
+
|
| 13 |
+
Convention : pour ajouter un nouveau paramètre par défaut, mettre à jour
|
| 14 |
+
ce fichier ET la documentation publique (CHANGELOG + ``docs/api-stable.md``).
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import inspect
|
| 20 |
+
from typing import Any
|
| 21 |
+
|
| 22 |
+
import pytest
|
| 23 |
+
|
| 24 |
+
import picarones
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# ---------------------------------------------------------------------------
|
| 28 |
+
# Helpers
|
| 29 |
+
# ---------------------------------------------------------------------------
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _signature_defaults(callable_obj: Any) -> dict[str, Any]:
|
| 33 |
+
"""Retourne ``{nom_param: default_value}`` pour les paramètres avec défaut.
|
| 34 |
+
|
| 35 |
+
Les paramètres sans défaut (positionnels obligatoires) sont omis.
|
| 36 |
+
"""
|
| 37 |
+
sig = inspect.signature(callable_obj)
|
| 38 |
+
return {
|
| 39 |
+
name: param.default
|
| 40 |
+
for name, param in sig.parameters.items()
|
| 41 |
+
if param.default is not inspect.Parameter.empty
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# ---------------------------------------------------------------------------
|
| 46 |
+
# load_corpus_from_directory
|
| 47 |
+
# ---------------------------------------------------------------------------
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def test_load_corpus_from_directory_defaults() -> None:
|
| 51 |
+
"""``load_corpus_from_directory`` est l'entrée canonique pour charger un
|
| 52 |
+
corpus depuis un dossier. Ses défauts sont contractuels."""
|
| 53 |
+
defaults = _signature_defaults(picarones.load_corpus_from_directory)
|
| 54 |
+
|
| 55 |
+
# Ces clés DOIVENT exister. Si l'une est supprimée, c'est un breaking
|
| 56 |
+
# change qui mérite un tag majeur.
|
| 57 |
+
assert "name" in defaults, (
|
| 58 |
+
"load_corpus_from_directory(name=…) doit avoir un défaut "
|
| 59 |
+
"(actuellement on accepte None pour déduire du nom de dossier)."
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
# Le défaut historique de ``name`` est ``None`` (déduction depuis le
|
| 63 |
+
# nom du dossier). Tout changement vers une chaîne fixe casserait les
|
| 64 |
+
# appelants qui s'appuient sur cette déduction.
|
| 65 |
+
assert defaults["name"] is None
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# ---------------------------------------------------------------------------
|
| 69 |
+
# Symboles publics : pas d'arguments positionnels uniquement non-typés
|
| 70 |
+
# ---------------------------------------------------------------------------
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _is_public_callable(name: str) -> bool:
|
| 74 |
+
"""Filtre les symboles publics de ``picarones`` qui sont appelables."""
|
| 75 |
+
if name.startswith("_"):
|
| 76 |
+
return False
|
| 77 |
+
obj = getattr(picarones, name, None)
|
| 78 |
+
return callable(obj) and not isinstance(obj, type(picarones))
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
@pytest.mark.parametrize("symbol", [s for s in picarones.__all__ if _is_public_callable(s)])
|
| 82 |
+
def test_public_callable_has_typed_signature(symbol: str) -> None:
|
| 83 |
+
"""Toute fonction publique doit avoir des annotations de type.
|
| 84 |
+
|
| 85 |
+
Ce garde-fou prépare le passage en strict mypy (Sprint A1, M-4).
|
| 86 |
+
Les classes (Corpus, Document, etc.) sont exclues — leur ``__init__``
|
| 87 |
+
est testé séparément si nécessaire, mais beaucoup sont des dataclasses
|
| 88 |
+
déjà annotées par construction.
|
| 89 |
+
"""
|
| 90 |
+
obj = getattr(picarones, symbol)
|
| 91 |
+
if isinstance(obj, type):
|
| 92 |
+
# Les classes sont validées via mypy strict sur core/, pas ici.
|
| 93 |
+
return
|
| 94 |
+
sig = inspect.signature(obj)
|
| 95 |
+
for param_name, param in sig.parameters.items():
|
| 96 |
+
if param_name in ("self", "cls"):
|
| 97 |
+
continue
|
| 98 |
+
assert param.annotation is not inspect.Parameter.empty, (
|
| 99 |
+
f"Paramètre `{param_name}` de `picarones.{symbol}` non annoté. "
|
| 100 |
+
f"L'API publique exige un typage explicite (Sprint A1)."
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
# ---------------------------------------------------------------------------
|
| 105 |
+
# compute_at_junction (registre typé Sprint 34)
|
| 106 |
+
# ---------------------------------------------------------------------------
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def test_compute_at_junction_defaults() -> None:
|
| 110 |
+
"""``compute_at_junction`` est l'API consommée par les pipelines composées
|
| 111 |
+
(Sprint 63+). Ses défauts contractuels :
|
| 112 |
+
- ``metric_name`` n'a PAS de défaut (on doit toujours préciser la métrique).
|
| 113 |
+
"""
|
| 114 |
+
defaults = _signature_defaults(picarones.compute_at_junction)
|
| 115 |
+
assert "metric_name" not in defaults, (
|
| 116 |
+
"compute_at_junction doit exiger metric_name explicite. "
|
| 117 |
+
"Un défaut introduirait de l'ambiguïté sur la métrique calculée."
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
# ---------------------------------------------------------------------------
|
| 122 |
+
# select_metrics (registre typé Sprint 34)
|
| 123 |
+
# ---------------------------------------------------------------------------
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def test_select_metrics_signature() -> None:
|
| 127 |
+
"""``select_metrics(input_type, output_type)`` est purement positionnel
|
| 128 |
+
sur ses deux types — pas de défauts implicites."""
|
| 129 |
+
defaults = _signature_defaults(picarones.select_metrics)
|
| 130 |
+
assert "input_type" not in defaults
|
| 131 |
+
assert "output_type" not in defaults
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
# ---------------------------------------------------------------------------
|
| 135 |
+
# Méta-test : tout symbole de __all__ existe vraiment
|
| 136 |
+
# ---------------------------------------------------------------------------
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
@pytest.mark.parametrize("symbol", picarones.__all__)
|
| 140 |
+
def test_all_symbols_resolve(symbol: str) -> None:
|
| 141 |
+
"""Chaque entrée de ``__all__`` doit pouvoir être résolue."""
|
| 142 |
+
assert hasattr(picarones, symbol), (
|
| 143 |
+
f"`picarones.{symbol}` est dans __all__ mais n'est pas exporté."
|
| 144 |
+
)
|