Spaces:
Sleeping
refactor(domain): Sprint A14-S40 — PipelineSpec migré dans domain/
Browse filesPipelineSpec et PipelineStep sont des types purs (dataclasses pydantic
frozen sans dépendance d'exécution). Le module canonique migre du
cercle 2 (picarones/pipeline/) vers le cercle 1 (picarones/domain/) où
vivent les autres types purs (Artifact, EvaluationView, ProjectionSpec,
ProvenanceRecord).
picarones/domain/pipeline_spec.py
---------------------------------
Module canonique. Contient PipelineStep, PipelineSpec, INITIAL_STEP_ID
inchangés (copie verbatim).
picarones/pipeline/spec.py
--------------------------
Devient un alias de chemin pur :
from picarones.domain.pipeline_spec import (
INITIAL_STEP_ID, PipelineSpec, PipelineStep,
)
+ __all__ identique.
Pas un shim au sens architectural (adaptation d'API incompatible) —
c'est un re-export de convenance pour ne pas casser les 13 callsites
qui importent depuis picarones.pipeline.spec.
picarones/domain/__init__.py
----------------------------
Re-exporte PipelineSpec, PipelineStep, INITIAL_STEP_ID au top-level.
Permet :
from picarones.domain import PipelineSpec # canonique
from picarones.domain.pipeline_spec import PipelineSpec # canonique
from picarones.pipeline.spec import PipelineSpec # alias
from picarones.pipeline import PipelineSpec # convenience
Tests S40 dédiés (5 nouveaux)
-----------------------------
- test_canonical_path_in_domain : import depuis domain.pipeline_spec.
- test_domain_top_level_reexports : import depuis domain.
- test_legacy_pipeline_path_aliased : import depuis pipeline.spec.
- test_all_paths_resolve_to_same_classes : `is` strict — toutes les
classes sont LE MÊME objet (pas une copie).
- test_pipeline_module_init_reexports_too : pipeline/ continue d'exposer.
Tests existants (87 pipeline + 622 domain/integration) : 100 %
préservés.
Tests : 4831 passed, 11 skipped (vs 4826 avant : +5 S40).
Lint : ruff check picarones/ tests/ → All checks passed.
https://claude.ai/code/session_011XQZNitg1rCgia8ZD1a2hP
|
@@ -57,6 +57,11 @@ from picarones.domain.evaluation_spec import (
|
|
| 57 |
EvaluationView,
|
| 58 |
MetricSpec,
|
| 59 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
from picarones.domain.projection_spec import ProjectionSpec
|
| 61 |
from picarones.domain.provenance import ProvenanceRecord
|
| 62 |
from picarones.domain.run_manifest import RunManifest, utcnow
|
|
@@ -87,6 +92,10 @@ __all__ = [
|
|
| 87 |
"EvaluationView",
|
| 88 |
"EvaluationSpec",
|
| 89 |
"ProjectionSpec",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
# S17 — Run manifest (pure domain ; RunResult vit dans app/)
|
| 91 |
"RunManifest",
|
| 92 |
"utcnow",
|
|
|
|
| 57 |
EvaluationView,
|
| 58 |
MetricSpec,
|
| 59 |
)
|
| 60 |
+
from picarones.domain.pipeline_spec import (
|
| 61 |
+
INITIAL_STEP_ID,
|
| 62 |
+
PipelineSpec,
|
| 63 |
+
PipelineStep,
|
| 64 |
+
)
|
| 65 |
from picarones.domain.projection_spec import ProjectionSpec
|
| 66 |
from picarones.domain.provenance import ProvenanceRecord
|
| 67 |
from picarones.domain.run_manifest import RunManifest, utcnow
|
|
|
|
| 92 |
"EvaluationView",
|
| 93 |
"EvaluationSpec",
|
| 94 |
"ProjectionSpec",
|
| 95 |
+
# S6 + S40 — Pipeline spec (canonique en domain/ depuis S40)
|
| 96 |
+
"PipelineSpec",
|
| 97 |
+
"PipelineStep",
|
| 98 |
+
"INITIAL_STEP_ID",
|
| 99 |
# S17 — Run manifest (pure domain ; RunResult vit dans app/)
|
| 100 |
"RunManifest",
|
| 101 |
"utcnow",
|
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``PipelineStep`` et ``PipelineSpec`` — Sprints A14-S6 / S40.
|
| 2 |
+
|
| 3 |
+
Description **purement déclarative** d'un DAG de transformation
|
| 4 |
+
documentaire. Sérialisable en YAML, versionnable en git, valide
|
| 5 |
+
sans avoir besoin d'instancier les modules concrets.
|
| 6 |
+
|
| 7 |
+
Sprint S40 — migration depuis ``picarones.pipeline.spec``
|
| 8 |
+
---------------------------------------------------------
|
| 9 |
+
Le module canonique est désormais en cercle 1 (``picarones/domain/``)
|
| 10 |
+
— c'est un type pur qui n'a aucune dépendance d'exécution
|
| 11 |
+
(``picarones/pipeline/`` qui contient le runtime n'est en fait pas
|
| 12 |
+
nécessaire pour décrire la spec). ``picarones.pipeline.spec`` reste
|
| 13 |
+
exposé en re-export pour ne pas casser les callers existants — ce
|
| 14 |
+
n'est pas un shim au sens architectural (adaptation d'une API
|
| 15 |
+
incompatible) mais un alias de chemin.
|
| 16 |
+
|
| 17 |
+
Différence avec l'ancien ``picarones.core.pipeline`` (Sprint 63)
|
| 18 |
+
----------------------------------------------------------------
|
| 19 |
+
L'ancien ``PipelineStep`` portait un champ ``module: BaseModule``
|
| 20 |
+
— une **instance** d'objet exécutable. Conséquence : la spec
|
| 21 |
+
n'était pas sérialisable en YAML, et un test qui voulait juste
|
| 22 |
+
valider la cohérence des types devait instancier des stubs.
|
| 23 |
+
|
| 24 |
+
Ici, ``PipelineStep`` ne porte qu'un ``adapter_name: str``. Le
|
| 25 |
+
mapping ``nom → instance`` est maintenu par un service applicatif
|
| 26 |
+
(``picarones.app.services.adapter_registry`` au S19) et résolu au
|
| 27 |
+
moment de l'exécution, pas de la spec.
|
| 28 |
+
|
| 29 |
+
Bénéfices :
|
| 30 |
+
|
| 31 |
+
- Le YAML d'une pipeline composée est versionnable en git
|
| 32 |
+
indépendamment de l'environnement Python (BnF peut commit
|
| 33 |
+
``ocr_llm_alto_remap.yaml`` sans imposer aux contributeurs
|
| 34 |
+
d'avoir tous les SDK installés).
|
| 35 |
+
- ``validate_spec`` peut s'exécuter sans instancier aucun module
|
| 36 |
+
→ tests rapides et déterministes.
|
| 37 |
+
- Le rapport de reproductibilité peut citer le YAML exact, le
|
| 38 |
+
commit du code et la version des adapters utilisés —
|
| 39 |
+
séparation propre de la déclaration et de l'implémentation.
|
| 40 |
+
|
| 41 |
+
Anti-sur-ingénierie
|
| 42 |
+
-------------------
|
| 43 |
+
- Pas de typage des ``params`` par adapter ici (chaque adapter
|
| 44 |
+
validera ses propres params au moment de l'exécution).
|
| 45 |
+
- Pas de versioning de spec — un nouveau champ se traduit par un
|
| 46 |
+
rebump pydantic. Si on veut migrer entre versions de schéma,
|
| 47 |
+
on l'ajoutera quand le besoin sera concret.
|
| 48 |
+
- Pas d'``outputs_preferred`` (mapping logique "preferred_text =
|
| 49 |
+
step3.RAW_TEXT"). Reporté quand un caller en aura concrètement
|
| 50 |
+
besoin.
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
from __future__ import annotations
|
| 54 |
+
|
| 55 |
+
import re
|
| 56 |
+
|
| 57 |
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
| 58 |
+
|
| 59 |
+
from picarones.domain.artifacts import ArtifactType
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
#: Identifiant d'étape — alphanum + ``_-``. Doit être un nom court
|
| 63 |
+
#: lisible par un humain dans les logs et le rapport.
|
| 64 |
+
_STEP_ID_RE = re.compile(r"^[A-Za-z0-9_\-]+$")
|
| 65 |
+
|
| 66 |
+
#: Sentinel pour ``inputs_from`` qui désigne les artefacts initiaux
|
| 67 |
+
#: fournis au runner (typiquement ``IMAGE``).
|
| 68 |
+
INITIAL_STEP_ID = "__initial__"
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class PipelineStep(BaseModel):
|
| 72 |
+
"""Une étape déclarative dans un DAG de pipeline.
|
| 73 |
+
|
| 74 |
+
Attributs
|
| 75 |
+
---------
|
| 76 |
+
id:
|
| 77 |
+
Identifiant unique de l'étape dans la pipeline (alphanum +
|
| 78 |
+
``_-``). Sert dans les logs, le rapport, et comme cible
|
| 79 |
+
des références ``inputs_from`` des étapes en aval.
|
| 80 |
+
kind:
|
| 81 |
+
Catégorie informationnelle de l'étape (``"ocr"``,
|
| 82 |
+
``"post_correction"``, ``"alto_remapping"``,
|
| 83 |
+
``"alto_reconstruction"``, etc.). Pas de validation
|
| 84 |
+
d'enum — c'est un label libre que les services et le
|
| 85 |
+
rapport peuvent grouper. Par convention, en
|
| 86 |
+
``snake_case``.
|
| 87 |
+
adapter_name:
|
| 88 |
+
Nom de l'adapter dans le registre runtime (résolu par
|
| 89 |
+
``app/services`` au S19). Convention :
|
| 90 |
+
``"<provider>:<engine_or_model>"`` (ex : ``"tesseract"``,
|
| 91 |
+
``"openai:gpt-4o"``, ``"mistral:large"``,
|
| 92 |
+
``"<vendor>:<custom_module>"``).
|
| 93 |
+
params:
|
| 94 |
+
Paramètres passés à l'adapter au moment de l'exécution.
|
| 95 |
+
Format libre (chaque adapter valide les siens) — typage
|
| 96 |
+
scalaire pour rester sérialisable en YAML.
|
| 97 |
+
input_types:
|
| 98 |
+
Types d'artefacts consommés par l'étape. Validés par
|
| 99 |
+
``validate_spec`` contre les outputs des étapes antérieures.
|
| 100 |
+
output_types:
|
| 101 |
+
Types d'artefacts produits. Validés au runtime par
|
| 102 |
+
l'executor (qui vérifie que tous les types déclarés sont
|
| 103 |
+
bien dans le dict retourné par l'adapter).
|
| 104 |
+
inputs_from:
|
| 105 |
+
DAG branchant (héritage du Sprint 66). Pour chaque type
|
| 106 |
+
d'entrée, désigne explicitement l'étape source. La chaîne
|
| 107 |
+
spéciale ``"__initial__"`` désigne les entrées initiales
|
| 108 |
+
du runner. Si le dict est vide, l'executor prend la
|
| 109 |
+
version la plus récente de chaque type dans le bag.
|
| 110 |
+
"""
|
| 111 |
+
|
| 112 |
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
| 113 |
+
|
| 114 |
+
id: str = Field(min_length=1, max_length=128)
|
| 115 |
+
kind: str = Field(min_length=1, max_length=64)
|
| 116 |
+
adapter_name: str = Field(min_length=1, max_length=256)
|
| 117 |
+
params: dict[str, str | int | float | bool] = Field(default_factory=dict)
|
| 118 |
+
input_types: tuple[ArtifactType, ...] = Field(default_factory=tuple)
|
| 119 |
+
output_types: tuple[ArtifactType, ...] = Field(default_factory=tuple)
|
| 120 |
+
inputs_from: dict[ArtifactType, str] = Field(default_factory=dict)
|
| 121 |
+
|
| 122 |
+
@field_validator("id")
|
| 123 |
+
@classmethod
|
| 124 |
+
def _validate_step_id(cls, v: str) -> str:
|
| 125 |
+
if not _STEP_ID_RE.match(v):
|
| 126 |
+
from picarones.domain.errors import PicaronesError
|
| 127 |
+
raise PicaronesError(
|
| 128 |
+
f"step id invalide : {v!r}. "
|
| 129 |
+
f"Doit matcher {_STEP_ID_RE.pattern!r} (alphanum + _-)."
|
| 130 |
+
)
|
| 131 |
+
if v == INITIAL_STEP_ID:
|
| 132 |
+
from picarones.domain.errors import PicaronesError
|
| 133 |
+
raise PicaronesError(
|
| 134 |
+
f"step id réservé : {INITIAL_STEP_ID!r} désigne "
|
| 135 |
+
"les entrées initiales du runner."
|
| 136 |
+
)
|
| 137 |
+
return v
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class PipelineSpec(BaseModel):
|
| 141 |
+
"""DAG déclaratif d'une pipeline composée.
|
| 142 |
+
|
| 143 |
+
Sérialisable en YAML via ``model_dump()`` + ``yaml.safe_dump``,
|
| 144 |
+
chargeable via ``model_validate(yaml.safe_load(text))``. Le
|
| 145 |
+
round-trip est testé.
|
| 146 |
+
|
| 147 |
+
Attributs
|
| 148 |
+
---------
|
| 149 |
+
name:
|
| 150 |
+
Nom court de la pipeline (utilisé dans les logs, le cache,
|
| 151 |
+
le rapport). Convention ``snake_case``.
|
| 152 |
+
description:
|
| 153 |
+
Phrase courte d'introduction affichée dans le rapport.
|
| 154 |
+
initial_inputs:
|
| 155 |
+
Types d'artefacts qui doivent être fournis par le caller
|
| 156 |
+
au moment de l'exécution. Convention : ``(IMAGE,)`` pour
|
| 157 |
+
une pipeline OCR classique, ``(IMAGE, RAW_TEXT)`` pour
|
| 158 |
+
une post-correction qui part d'un OCR pré-calculé.
|
| 159 |
+
steps:
|
| 160 |
+
Étapes du DAG, ordonnées par dépendance topologique
|
| 161 |
+
d'exécution. Si une étape ``s2`` dépend de ``s1``, alors
|
| 162 |
+
``s1`` apparaît avant ``s2``. ``validate_spec`` détecte
|
| 163 |
+
les violations.
|
| 164 |
+
"""
|
| 165 |
+
|
| 166 |
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
| 167 |
+
|
| 168 |
+
name: str = Field(min_length=1, max_length=128)
|
| 169 |
+
description: str = ""
|
| 170 |
+
initial_inputs: tuple[ArtifactType, ...] = Field(default_factory=tuple)
|
| 171 |
+
steps: tuple[PipelineStep, ...] = Field(default_factory=tuple)
|
| 172 |
+
|
| 173 |
+
def step_by_id(self, step_id: str) -> PipelineStep | None:
|
| 174 |
+
for s in self.steps:
|
| 175 |
+
if s.id == step_id:
|
| 176 |
+
return s
|
| 177 |
+
return None
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
__all__ = ["PipelineStep", "PipelineSpec", "INITIAL_STEP_ID"]
|
|
@@ -1,170 +1,25 @@
|
|
| 1 |
-
"""``PipelineStep`` et ``PipelineSpec`` —
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
L'ancien ``PipelineStep`` portait un champ ``module: BaseModule``
|
| 10 |
-
— une **instance** d'objet exécutable. Conséquence : la spec
|
| 11 |
-
n'était pas sérialisable en YAML, et un test qui voulait juste
|
| 12 |
-
valider la cohérence des types devait instancier des stubs.
|
| 13 |
|
| 14 |
-
|
| 15 |
-
mapping ``nom → instance`` est maintenu par un service applicatif
|
| 16 |
-
(``picarones.app.services.adapter_registry`` au S19) et résolu au
|
| 17 |
-
moment de l'exécution, pas de la spec.
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
- Le YAML d'une pipeline composée est versionnable en git
|
| 22 |
-
indépendamment de l'environnement Python (BnF peut commit
|
| 23 |
-
``ocr_llm_alto_remap.yaml`` sans imposer aux contributeurs
|
| 24 |
-
d'avoir tous les SDK installés).
|
| 25 |
-
- ``validate_spec`` peut s'exécuter sans instancier aucun module
|
| 26 |
-
→ tests rapides et déterministes.
|
| 27 |
-
- Le rapport de reproductibilité peut citer le YAML exact, le
|
| 28 |
-
commit du code et la version des adapters utilisés —
|
| 29 |
-
séparation propre de la déclaration et de l'implémentation.
|
| 30 |
-
|
| 31 |
-
Anti-sur-ingénierie
|
| 32 |
-
-------------------
|
| 33 |
-
- Pas de typage des ``params`` par adapter ici (chaque adapter
|
| 34 |
-
validera ses propres params au moment de l'exécution).
|
| 35 |
-
- Pas de versioning de spec — un nouveau champ se traduit par un
|
| 36 |
-
rebump pydantic. Si on veut migrer entre versions de schéma,
|
| 37 |
-
on l'ajoutera quand le besoin sera concret.
|
| 38 |
-
- Pas d'``outputs_preferred`` (mapping logique "preferred_text =
|
| 39 |
-
step3.RAW_TEXT"). Reporté quand un caller en aura concrètement
|
| 40 |
-
besoin.
|
| 41 |
"""
|
| 42 |
|
| 43 |
from __future__ import annotations
|
| 44 |
|
| 45 |
-
import
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
#: Identifiant d'étape — alphanum + ``_-``. Doit être un nom court
|
| 53 |
-
#: lisible par un humain dans les logs et le rapport.
|
| 54 |
-
_STEP_ID_RE = re.compile(r"^[A-Za-z0-9_\-]+$")
|
| 55 |
-
|
| 56 |
-
#: Sentinel pour ``inputs_from`` qui désigne les artefacts initiaux
|
| 57 |
-
#: fournis au runner (typiquement ``IMAGE``).
|
| 58 |
-
INITIAL_STEP_ID = "__initial__"
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
class PipelineStep(BaseModel):
|
| 62 |
-
"""Une étape déclarative dans un DAG de pipeline.
|
| 63 |
-
|
| 64 |
-
Attributs
|
| 65 |
-
---------
|
| 66 |
-
id:
|
| 67 |
-
Identifiant unique de l'étape dans la pipeline (alphanum +
|
| 68 |
-
``_-``). Sert dans les logs, le rapport, et comme cible
|
| 69 |
-
des références ``inputs_from`` des étapes en aval.
|
| 70 |
-
kind:
|
| 71 |
-
Catégorie informationnelle de l'étape (``"ocr"``,
|
| 72 |
-
``"post_correction"``, ``"alto_remapping"``,
|
| 73 |
-
``"alto_reconstruction"``, etc.). Pas de validation
|
| 74 |
-
d'enum — c'est un label libre que les services et le
|
| 75 |
-
rapport peuvent grouper. Par convention, en
|
| 76 |
-
``snake_case``.
|
| 77 |
-
adapter_name:
|
| 78 |
-
Nom de l'adapter dans le registre runtime (résolu par
|
| 79 |
-
``app/services`` au S19). Convention :
|
| 80 |
-
``"<provider>:<engine_or_model>"`` (ex : ``"tesseract"``,
|
| 81 |
-
``"openai:gpt-4o"``, ``"mistral:large"``,
|
| 82 |
-
``"<vendor>:<custom_module>"``).
|
| 83 |
-
params:
|
| 84 |
-
Paramètres passés à l'adapter au moment de l'exécution.
|
| 85 |
-
Format libre (chaque adapter valide les siens) — typage
|
| 86 |
-
scalaire pour rester sérialisable en YAML.
|
| 87 |
-
input_types:
|
| 88 |
-
Types d'artefacts consommés par l'étape. Validés par
|
| 89 |
-
``validate_spec`` contre les outputs des étapes antérieures.
|
| 90 |
-
output_types:
|
| 91 |
-
Types d'artefacts produits. Validés au runtime par
|
| 92 |
-
l'executor (qui vérifie que tous les types déclarés sont
|
| 93 |
-
bien dans le dict retourné par l'adapter).
|
| 94 |
-
inputs_from:
|
| 95 |
-
DAG branchant (héritage du Sprint 66). Pour chaque type
|
| 96 |
-
d'entrée, désigne explicitement l'étape source. La chaîne
|
| 97 |
-
spéciale ``"__initial__"`` désigne les entrées initiales
|
| 98 |
-
du runner. Si le dict est vide, l'executor prend la
|
| 99 |
-
version la plus récente de chaque type dans le bag.
|
| 100 |
-
"""
|
| 101 |
-
|
| 102 |
-
model_config = ConfigDict(frozen=True, extra="forbid")
|
| 103 |
-
|
| 104 |
-
id: str = Field(min_length=1, max_length=128)
|
| 105 |
-
kind: str = Field(min_length=1, max_length=64)
|
| 106 |
-
adapter_name: str = Field(min_length=1, max_length=256)
|
| 107 |
-
params: dict[str, str | int | float | bool] = Field(default_factory=dict)
|
| 108 |
-
input_types: tuple[ArtifactType, ...] = Field(default_factory=tuple)
|
| 109 |
-
output_types: tuple[ArtifactType, ...] = Field(default_factory=tuple)
|
| 110 |
-
inputs_from: dict[ArtifactType, str] = Field(default_factory=dict)
|
| 111 |
-
|
| 112 |
-
@field_validator("id")
|
| 113 |
-
@classmethod
|
| 114 |
-
def _validate_step_id(cls, v: str) -> str:
|
| 115 |
-
if not _STEP_ID_RE.match(v):
|
| 116 |
-
from picarones.domain.errors import PicaronesError
|
| 117 |
-
raise PicaronesError(
|
| 118 |
-
f"step id invalide : {v!r}. "
|
| 119 |
-
f"Doit matcher {_STEP_ID_RE.pattern!r} (alphanum + _-)."
|
| 120 |
-
)
|
| 121 |
-
if v == INITIAL_STEP_ID:
|
| 122 |
-
from picarones.domain.errors import PicaronesError
|
| 123 |
-
raise PicaronesError(
|
| 124 |
-
f"step id réservé : {INITIAL_STEP_ID!r} désigne "
|
| 125 |
-
"les entrées initiales du runner."
|
| 126 |
-
)
|
| 127 |
-
return v
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
class PipelineSpec(BaseModel):
|
| 131 |
-
"""DAG déclaratif d'une pipeline composée.
|
| 132 |
-
|
| 133 |
-
Sérialisable en YAML via ``model_dump()`` + ``yaml.safe_dump``,
|
| 134 |
-
chargeable via ``model_validate(yaml.safe_load(text))``. Le
|
| 135 |
-
round-trip est testé.
|
| 136 |
-
|
| 137 |
-
Attributs
|
| 138 |
-
---------
|
| 139 |
-
name:
|
| 140 |
-
Nom court de la pipeline (utilisé dans les logs, le cache,
|
| 141 |
-
le rapport). Convention ``snake_case``.
|
| 142 |
-
description:
|
| 143 |
-
Phrase courte d'introduction affichée dans le rapport.
|
| 144 |
-
initial_inputs:
|
| 145 |
-
Types d'artefacts qui doivent être fournis par le caller
|
| 146 |
-
au moment de l'exécution. Convention : ``(IMAGE,)`` pour
|
| 147 |
-
une pipeline OCR classique, ``(IMAGE, RAW_TEXT)`` pour
|
| 148 |
-
une post-correction qui part d'un OCR pré-calculé.
|
| 149 |
-
steps:
|
| 150 |
-
Étapes du DAG, ordonnées par dépendance topologique
|
| 151 |
-
d'exécution. Si une étape ``s2`` dépend de ``s1``, alors
|
| 152 |
-
``s1`` apparaît avant ``s2``. ``validate_spec`` détecte
|
| 153 |
-
les violations.
|
| 154 |
-
"""
|
| 155 |
-
|
| 156 |
-
model_config = ConfigDict(frozen=True, extra="forbid")
|
| 157 |
-
|
| 158 |
-
name: str = Field(min_length=1, max_length=128)
|
| 159 |
-
description: str = ""
|
| 160 |
-
initial_inputs: tuple[ArtifactType, ...] = Field(default_factory=tuple)
|
| 161 |
-
steps: tuple[PipelineStep, ...] = Field(default_factory=tuple)
|
| 162 |
-
|
| 163 |
-
def step_by_id(self, step_id: str) -> PipelineStep | None:
|
| 164 |
-
for s in self.steps:
|
| 165 |
-
if s.id == step_id:
|
| 166 |
-
return s
|
| 167 |
-
return None
|
| 168 |
-
|
| 169 |
|
| 170 |
__all__ = ["PipelineStep", "PipelineSpec", "INITIAL_STEP_ID"]
|
|
|
|
| 1 |
+
"""``PipelineStep`` et ``PipelineSpec`` — re-export depuis ``domain``.
|
| 2 |
|
| 3 |
+
Sprint A14-S40 a migré le module canonique vers
|
| 4 |
+
``picarones.domain.pipeline_spec`` (cercle 1, types purs). Ce
|
| 5 |
+
module reste un alias de chemin pour ne pas casser les callers
|
| 6 |
+
existants — ce n'est pas un shim au sens architectural
|
| 7 |
+
(adaptation d'une API incompatible) mais une convenance de chemin.
|
| 8 |
|
| 9 |
+
Les nouveaux callers doivent importer directement depuis
|
| 10 |
+
``picarones.domain`` :
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
+
::
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
+
from picarones.domain import PipelineSpec, PipelineStep, INITIAL_STEP_ID
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
"""
|
| 16 |
|
| 17 |
from __future__ import annotations
|
| 18 |
|
| 19 |
+
from picarones.domain.pipeline_spec import (
|
| 20 |
+
INITIAL_STEP_ID,
|
| 21 |
+
PipelineSpec,
|
| 22 |
+
PipelineStep,
|
| 23 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
__all__ = ["PipelineStep", "PipelineSpec", "INITIAL_STEP_ID"]
|
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sprint A14-S40 — PipelineSpec migré dans domain/.
|
| 2 |
+
|
| 3 |
+
Vérifie que :
|
| 4 |
+
|
| 5 |
+
1. ``picarones.domain.pipeline_spec`` est le module canonique.
|
| 6 |
+
2. ``picarones.domain`` re-exporte ``PipelineSpec``, ``PipelineStep``,
|
| 7 |
+
``INITIAL_STEP_ID``.
|
| 8 |
+
3. ``picarones.pipeline.spec`` continue d'exposer les mêmes classes
|
| 9 |
+
(alias de chemin pour la rétrocompat).
|
| 10 |
+
4. Les deux chemins d'import retournent **la même classe**
|
| 11 |
+
(``is`` strict, pas seulement ``==``).
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def test_canonical_path_in_domain() -> None:
|
| 18 |
+
"""``picarones.domain.pipeline_spec`` expose les classes canoniques."""
|
| 19 |
+
from picarones.domain.pipeline_spec import (
|
| 20 |
+
INITIAL_STEP_ID,
|
| 21 |
+
PipelineSpec,
|
| 22 |
+
PipelineStep,
|
| 23 |
+
)
|
| 24 |
+
assert PipelineSpec is not None
|
| 25 |
+
assert PipelineStep is not None
|
| 26 |
+
assert INITIAL_STEP_ID == "__initial__"
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def test_domain_top_level_reexports() -> None:
|
| 30 |
+
"""``picarones.domain`` re-exporte au top-level."""
|
| 31 |
+
from picarones.domain import (
|
| 32 |
+
INITIAL_STEP_ID,
|
| 33 |
+
PipelineSpec,
|
| 34 |
+
PipelineStep,
|
| 35 |
+
)
|
| 36 |
+
assert PipelineSpec is not None
|
| 37 |
+
assert PipelineStep is not None
|
| 38 |
+
assert INITIAL_STEP_ID == "__initial__"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def test_legacy_pipeline_path_aliased() -> None:
|
| 42 |
+
"""``picarones.pipeline.spec`` reste un alias de chemin."""
|
| 43 |
+
from picarones.pipeline.spec import (
|
| 44 |
+
INITIAL_STEP_ID,
|
| 45 |
+
PipelineSpec,
|
| 46 |
+
PipelineStep,
|
| 47 |
+
)
|
| 48 |
+
assert PipelineSpec is not None
|
| 49 |
+
assert PipelineStep is not None
|
| 50 |
+
assert INITIAL_STEP_ID == "__initial__"
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def test_all_paths_resolve_to_same_classes() -> None:
|
| 54 |
+
"""Les imports depuis les 3 emplacements pointent vers le MÊME objet."""
|
| 55 |
+
from picarones.domain import PipelineSpec as DomainSpec
|
| 56 |
+
from picarones.domain import PipelineStep as DomainStep
|
| 57 |
+
from picarones.domain import INITIAL_STEP_ID as DomainInitial
|
| 58 |
+
from picarones.domain.pipeline_spec import PipelineSpec as CanonSpec
|
| 59 |
+
from picarones.domain.pipeline_spec import PipelineStep as CanonStep
|
| 60 |
+
from picarones.pipeline.spec import PipelineSpec as LegacySpec
|
| 61 |
+
from picarones.pipeline.spec import PipelineStep as LegacyStep
|
| 62 |
+
from picarones.pipeline.spec import INITIAL_STEP_ID as LegacyInitial
|
| 63 |
+
|
| 64 |
+
# is strict — toutes les classes pointent vers le même objet.
|
| 65 |
+
assert DomainSpec is CanonSpec
|
| 66 |
+
assert DomainSpec is LegacySpec
|
| 67 |
+
assert DomainStep is CanonStep
|
| 68 |
+
assert DomainStep is LegacyStep
|
| 69 |
+
assert DomainInitial == LegacyInitial
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def test_pipeline_module_init_reexports_too() -> None:
|
| 73 |
+
"""``picarones.pipeline`` continue d'exposer pour rétrocompat."""
|
| 74 |
+
from picarones.pipeline import PipelineSpec, PipelineStep, INITIAL_STEP_ID
|
| 75 |
+
from picarones.domain.pipeline_spec import (
|
| 76 |
+
PipelineSpec as CanonSpec,
|
| 77 |
+
PipelineStep as CanonStep,
|
| 78 |
+
)
|
| 79 |
+
assert PipelineSpec is CanonSpec
|
| 80 |
+
assert PipelineStep is CanonStep
|
| 81 |
+
assert INITIAL_STEP_ID == "__initial__"
|