Spaces:
Running
chore(audit): Phase 0 quick wins — entry point + numérotation couches
Browse filesTrois corrections triviales identifiées par l'audit code-quality :
1. **app.py** : ``picarones.web.app:app`` (paquet supprimé v2.0) →
``picarones.interfaces.web.app:app``. Le Dockerfile masquait le
bug en lançant ``picarones serve`` ; quiconque exécutait
``python app.py`` localement obtenait ``ModuleNotFoundError``.
2. **picarones/{8 couches}/__init__.py** : renumérotation
``Cercle N`` (incohérent : 5 max, doublons) → ``Couche N``
(1 à 8, ordre du manifeste CLAUDE.md). domain=1, formats=2,
evaluation=3, pipeline=4, adapters=5, app=6, reports=7,
interfaces=8.
3. **.env.example** : commenter ``PICARONES_PORT`` comme variable
docker-compose uniquement (le code Python ne la lit pas — le
port serveur passe par ``picarones serve --port`` ou le CMD
Dockerfile).
**Test ajouté** : ``tests/release/test_entry_points.py`` (3 tests)
- ``app.py`` ``uvicorn.run('module:attr', ...)`` est importable
- ``[project.scripts]`` de pyproject.toml résolvent
- Aucune référence résiduelle à ``picarones.web`` dans les
fichiers de déploiement (app.py, Dockerfile, docker-compose,
picarones.spec).
Suite : 4 689 passed, 14 skipped, 8 deselected, 2 xfailed (+3 vs
baseline 4 686). Ruff propre.
- .env.example +7 -1
- app.py +1 -1
- picarones/adapters/__init__.py +1 -1
- picarones/app/__init__.py +1 -1
- picarones/domain/__init__.py +1 -1
- picarones/evaluation/__init__.py +1 -1
- picarones/formats/__init__.py +1 -1
- picarones/interfaces/__init__.py +1 -1
- picarones/pipeline/__init__.py +1 -1
- picarones/reports/__init__.py +1 -1
- tests/release/test_entry_points.py +137 -0
|
@@ -88,7 +88,13 @@ AZURE_DOC_INTEL_ENDPOINT=
|
|
| 88 |
AZURE_DOC_INTEL_KEY=
|
| 89 |
|
| 90 |
# ──────────────────────────────────────────────────────────────────
|
| 91 |
-
# Réseau / port d'exposition
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
# ──────────────────────────────────────────────────────────────────
|
| 93 |
PICARONES_PORT=7860
|
| 94 |
|
|
|
|
| 88 |
AZURE_DOC_INTEL_KEY=
|
| 89 |
|
| 90 |
# ──────────────────────────────────────────────────────────────────
|
| 91 |
+
# Réseau / port d'exposition.
|
| 92 |
+
#
|
| 93 |
+
# NB : variable utilisée UNIQUEMENT par ``docker-compose.yml`` pour
|
| 94 |
+
# mapper le port hôte (cf. ligne ``${PICARONES_PORT:-7860}:7860``).
|
| 95 |
+
# Le code Python ne la lit pas — pour changer le port serveur,
|
| 96 |
+
# utiliser ``picarones serve --port <N>`` ou modifier le CMD du
|
| 97 |
+
# Dockerfile.
|
| 98 |
# ──────────────────────────────────────────────────────────────────
|
| 99 |
PICARONES_PORT=7860
|
| 100 |
|
|
@@ -2,4 +2,4 @@
|
|
| 2 |
import uvicorn
|
| 3 |
|
| 4 |
if __name__ == "__main__":
|
| 5 |
-
uvicorn.run("picarones.web.app:app", host="0.0.0.0", port=7860)
|
|
|
|
| 2 |
import uvicorn
|
| 3 |
|
| 4 |
if __name__ == "__main__":
|
| 5 |
+
uvicorn.run("picarones.interfaces.web.app:app", host="0.0.0.0", port=7860)
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
Implémentations concrètes des contrats du domain. C'est ici que
|
| 4 |
vivent les dépendances externes lourdes (pytesseract, pero_ocr,
|
|
|
|
| 1 |
+
"""Couche 5 — Adapters.
|
| 2 |
|
| 3 |
Implémentations concrètes des contrats du domain. C'est ici que
|
| 4 |
vivent les dépendances externes lourdes (pytesseract, pero_ocr,
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
Couche d'orchestration : reçoit des requêtes (DTO Pydantic) depuis
|
| 4 |
``interfaces/``, valide tout (chemins sandboxés, quotas, mode
|
|
|
|
| 1 |
+
"""Couche 6 — Application services.
|
| 2 |
|
| 3 |
Couche d'orchestration : reçoit des requêtes (DTO Pydantic) depuis
|
| 4 |
``interfaces/``, valide tout (chemins sandboxés, quotas, mode
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
Types purs et abstractions du modèle métier de Picarones.
|
| 4 |
|
|
|
|
| 1 |
+
"""Couche 1 — Domain.
|
| 2 |
|
| 3 |
Types purs et abstractions du modèle métier de Picarones.
|
| 4 |
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
Vues d'évaluation, projecteurs et calculs de métriques.
|
| 4 |
|
|
|
|
| 1 |
+
"""Couche 3 — Evaluation.
|
| 2 |
|
| 3 |
Vues d'évaluation, projecteurs et calculs de métriques.
|
| 4 |
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
Parsers, writers et validateurs pour les formats d'entrée/sortie
|
| 4 |
patrimoniaux. Tout le code XML / namespaces / parsing vit ici, à
|
|
|
|
| 1 |
+
"""Couche 2 — Formats documentaires.
|
| 2 |
|
| 3 |
Parsers, writers et validateurs pour les formats d'entrée/sortie
|
| 4 |
patrimoniaux. Tout le code XML / namespaces / parsing vit ici, à
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
Couches de transport. Code mince qui parse des arguments / des
|
| 4 |
requêtes HTTP, appelle un service applicatif, retourne une réponse.
|
|
|
|
| 1 |
+
"""Couche 8 — Interfaces (CLI, web).
|
| 2 |
|
| 3 |
Couches de transport. Code mince qui parse des arguments / des
|
| 4 |
requêtes HTTP, appelle un service applicatif, retourne une réponse.
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
Exécution séquentielle ou DAG-branchante d'une chaîne de modules
|
| 4 |
tiers (``StepExecutor``). Picarones ne fournit **aucun module
|
|
|
|
| 1 |
+
"""Couche 4 — Pipeline execution.
|
| 2 |
|
| 3 |
Exécution séquentielle ou DAG-branchante d'une chaîne de modules
|
| 4 |
tiers (``StepExecutor``). Picarones ne fournit **aucun module
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
Sortie en différents formats à partir d'un ``RunResult`` persisté.
|
| 4 |
Le rapport est une **vue** des artefacts et des résultats
|
|
|
|
| 1 |
+
"""Couche 7 — Reports.
|
| 2 |
|
| 3 |
Sortie en différents formats à partir d'un ``RunResult`` persisté.
|
| 4 |
Le rapport est une **vue** des artefacts et des résultats
|
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Garde-fou : tout entry point déclaré doit pointer vers un module
|
| 2 |
+
réellement importable.
|
| 3 |
+
|
| 4 |
+
Audit de mai 2026 — ``app.py`` (entry point HuggingFace Spaces)
|
| 5 |
+
référençait ``picarones.web.app:app``, paquet supprimé au sprint H.4
|
| 6 |
+
mais jamais rebranché. ``python app.py`` produisait un
|
| 7 |
+
``ModuleNotFoundError`` ; le Dockerfile masquait le bug en lançant
|
| 8 |
+
``picarones serve`` à la place.
|
| 9 |
+
|
| 10 |
+
Ce test vérifie qu'au fil du temps, **chaque entry point déclaré**
|
| 11 |
+
(``app.py`` racine, ``[project.scripts]`` dans ``pyproject.toml``,
|
| 12 |
+
``huggingface``, ``uvicorn``...) pointe vers un module qui
|
| 13 |
+
``importlib.import_module()`` accepte.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import ast
|
| 19 |
+
import importlib
|
| 20 |
+
import re
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
|
| 23 |
+
import pytest
|
| 24 |
+
|
| 25 |
+
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _extract_uvicorn_targets(path: Path) -> list[str]:
|
| 29 |
+
"""Extrait les chaînes ``module:attr`` passées à ``uvicorn.run(...)``.
|
| 30 |
+
|
| 31 |
+
Plutôt que de regex à la louche, on parse l'AST pour ne capturer
|
| 32 |
+
que les littéraux passés au premier argument positionnel d'un
|
| 33 |
+
appel ``uvicorn.run(...)``.
|
| 34 |
+
"""
|
| 35 |
+
tree = ast.parse(path.read_text(encoding="utf-8"))
|
| 36 |
+
targets: list[str] = []
|
| 37 |
+
for node in ast.walk(tree):
|
| 38 |
+
if not isinstance(node, ast.Call):
|
| 39 |
+
continue
|
| 40 |
+
func = node.func
|
| 41 |
+
is_uvicorn_run = (
|
| 42 |
+
isinstance(func, ast.Attribute)
|
| 43 |
+
and func.attr == "run"
|
| 44 |
+
and isinstance(func.value, ast.Name)
|
| 45 |
+
and func.value.id == "uvicorn"
|
| 46 |
+
)
|
| 47 |
+
if not is_uvicorn_run or not node.args:
|
| 48 |
+
continue
|
| 49 |
+
first = node.args[0]
|
| 50 |
+
if isinstance(first, ast.Constant) and isinstance(first.value, str):
|
| 51 |
+
targets.append(first.value)
|
| 52 |
+
return targets
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _check_target_importable(target: str) -> None:
|
| 56 |
+
"""Vérifie que ``module:attr`` est résolvable."""
|
| 57 |
+
if ":" in target:
|
| 58 |
+
module_name, attr = target.split(":", 1)
|
| 59 |
+
else:
|
| 60 |
+
module_name, attr = target, None
|
| 61 |
+
module = importlib.import_module(module_name)
|
| 62 |
+
if attr is not None:
|
| 63 |
+
assert hasattr(module, attr), (
|
| 64 |
+
f"Entry point '{target}' : le module '{module_name}' "
|
| 65 |
+
f"existe mais n'expose pas l'attribut '{attr}'."
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def test_hf_spaces_app_py_resolves() -> None:
|
| 70 |
+
"""``app.py`` à la racine doit pointer vers un module importable.
|
| 71 |
+
|
| 72 |
+
Régression : ``picarones.web.app:app`` (legacy supprimé v2.0)
|
| 73 |
+
→ ``picarones.interfaces.web.app:app`` (Phase 0.1 audit code-quality).
|
| 74 |
+
"""
|
| 75 |
+
app_py = REPO_ROOT / "app.py"
|
| 76 |
+
if not app_py.exists():
|
| 77 |
+
pytest.skip("app.py absent (déploiement non-HF)")
|
| 78 |
+
|
| 79 |
+
targets = _extract_uvicorn_targets(app_py)
|
| 80 |
+
assert targets, (
|
| 81 |
+
f"{app_py} ne contient aucun appel ``uvicorn.run('module:attr', ...)`` "
|
| 82 |
+
f"— le test ne peut pas vérifier la cible."
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
for target in targets:
|
| 86 |
+
_check_target_importable(target)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def test_pyproject_console_scripts_resolve() -> None:
|
| 90 |
+
"""Chaque ``[project.scripts]`` doit pointer vers un module importable.
|
| 91 |
+
|
| 92 |
+
Format pyproject : ``picarones = "picarones.interfaces.cli:cli"``.
|
| 93 |
+
"""
|
| 94 |
+
pyproject = REPO_ROOT / "pyproject.toml"
|
| 95 |
+
content = pyproject.read_text(encoding="utf-8")
|
| 96 |
+
|
| 97 |
+
# Sous-section [project.scripts] — capture tout jusqu'à la
|
| 98 |
+
# prochaine section [...] ou fin de fichier.
|
| 99 |
+
match = re.search(
|
| 100 |
+
r"\[project\.scripts\]\s*\n(.*?)(?=^\[|\Z)",
|
| 101 |
+
content,
|
| 102 |
+
re.DOTALL | re.MULTILINE,
|
| 103 |
+
)
|
| 104 |
+
if not match:
|
| 105 |
+
pytest.skip("Aucune section [project.scripts] dans pyproject.toml")
|
| 106 |
+
|
| 107 |
+
# Chaque ligne ``key = "module:attr"``.
|
| 108 |
+
script_re = re.compile(r'^\s*[\w_-]+\s*=\s*"([^"]+)"\s*$', re.MULTILINE)
|
| 109 |
+
targets = script_re.findall(match.group(1))
|
| 110 |
+
assert targets, "Section [project.scripts] vide ou mal formatée"
|
| 111 |
+
|
| 112 |
+
for target in targets:
|
| 113 |
+
_check_target_importable(target)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def test_no_legacy_picarones_web_references() -> None:
|
| 117 |
+
"""``picarones.web`` a été supprimé en v2.0 ; aucune chaîne
|
| 118 |
+
``picarones.web.app`` ne doit subsister dans les entry points
|
| 119 |
+
ou fichiers de déploiement.
|
| 120 |
+
"""
|
| 121 |
+
suspicious_paths = [
|
| 122 |
+
REPO_ROOT / "app.py",
|
| 123 |
+
REPO_ROOT / "Dockerfile",
|
| 124 |
+
REPO_ROOT / "docker-compose.yml",
|
| 125 |
+
REPO_ROOT / "picarones.spec",
|
| 126 |
+
]
|
| 127 |
+
pattern = re.compile(r"\bpicarones\.web(?:\.|\b)")
|
| 128 |
+
for path in suspicious_paths:
|
| 129 |
+
if not path.exists():
|
| 130 |
+
continue
|
| 131 |
+
text = path.read_text(encoding="utf-8")
|
| 132 |
+
match = pattern.search(text)
|
| 133 |
+
assert match is None, (
|
| 134 |
+
f"{path.name} référence encore le paquet legacy 'picarones.web' "
|
| 135 |
+
f"(supprimé v2.0) à la position {match.start()}. "
|
| 136 |
+
f"Remplacer par 'picarones.interfaces.web'."
|
| 137 |
+
)
|