pyannotators-entityfishing 1.6.87__tar.gz → 1.6.90__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/PKG-INFO +1 -1
  2. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/src/pyannotators_entityfishing/__init__.py +1 -1
  3. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/submodules/python-archetype/.claude-plugin/plugin.json +1 -1
  4. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/submodules/python-archetype/resources/Taskfile.yml +7 -4
  5. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/submodules/python-archetype/skills/new-plugin/SKILL.md +9 -1
  6. pyannotators_entityfishing-1.6.90/submodules/python-archetype/skills/new-plugin/references/converter.md +66 -0
  7. pyannotators_entityfishing-1.6.90/submodules/python-archetype/skills/new-plugin/references/formatter.md +54 -0
  8. pyannotators_entityfishing-1.6.90/submodules/python-archetype/skills/new-plugin/references/segmenter.md +55 -0
  9. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/submodules/python-archetype/skills/new-plugin/references/types.md +39 -14
  10. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/submodules/python-archetype/vars/pythonPipeline.groovy +43 -3
  11. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/.claude/skills/ef-mapping/SKILL.md +0 -0
  12. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/.gitignore +0 -0
  13. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/.gitmodules +0 -0
  14. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/.python-version +0 -0
  15. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/README.md +0 -0
  16. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/Taskfile.yml +0 -0
  17. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/pyproject.toml +0 -0
  18. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/src/pyannotators_entityfishing/candidates.py +0 -0
  19. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/src/pyannotators_entityfishing/ef_client.py +0 -0
  20. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/src/pyannotators_entityfishing/entityfishing.py +0 -0
  21. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/submodules/python-archetype/.claude-plugin/marketplace.json +0 -0
  22. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/submodules/python-archetype/HOWTO.md +0 -0
  23. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/submodules/python-archetype/README.md +0 -0
  24. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/submodules/python-archetype/resources/Stages.yml +0 -0
  25. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/submodules/python-archetype/skills/new-plugin/references/annotator.md +0 -0
  26. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/submodules/python-archetype/skills/new-plugin/references/importer.md +0 -0
  27. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/submodules/python-archetype/skills/new-plugin/references/processor.md +0 -0
  28. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/submodules/python-archetype/tools/check-marketplace.py +0 -0
  29. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/tools/build_rerank_data.py +0 -0
  30. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/tools/check_rerank_endpoint.py +0 -0
  31. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/tools/el_jev.py +0 -0
  32. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/tools/eval_linking.py +0 -0
  33. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/tools/eval_mappings.py +0 -0
  34. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/tools/eval_rerank.py +0 -0
  35. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/tools/finetune_rerank.py +0 -0
  36. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/tools/merge_rerank.py +0 -0
  37. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/tools/recalibrate_linking.py +0 -0
  38. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/tools/review_linking.html +0 -0
  39. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/tools/review_linking.py +0 -0
  40. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/tools/sherpa_plan_project.py +0 -0
  41. {pyannotators_entityfishing-1.6.87 → pyannotators_entityfishing-1.6.90}/tools/wdfilter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyannotators-entityfishing
3
- Version: 1.6.87
3
+ Version: 1.6.90
4
4
  Summary: Annotator based on entity-fishing
5
5
  Project-URL: Homepage, https://github.com/oterrier/pyannotators_entityfishing/
6
6
  Author-email: Olivier Terrier <olivier.terrier@kairntech.com>
@@ -1,3 +1,3 @@
1
1
  """Annotator based on entity-fishing"""
2
2
 
3
- __version__ = "1.6.87"
3
+ __version__ = "1.6.90"
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "python-archetype",
4
4
  "displayName": "Kairntech python-archetype",
5
- "version": "1.1.0",
5
+ "version": "1.2.0",
6
6
  "description": "Cree et entretient les depots Python de Kairntech qui consomment python-archetype : creation d'un plugin a partir de sa graine, et mesure de la derive d'un depot par rapport a elle.",
7
7
  "author": {
8
8
  "name": "Kairntech",
@@ -169,11 +169,14 @@ tasks:
169
169
  cmds:
170
170
  - task: sync
171
171
  vars: { EXTRAS: '{{.SYNC_EXTRAS}} sbom' }
172
- # '|| true' : pip-audit sort non-zéro dès qu'il trouve quelque chose, et cette
173
- # task ne rend pas le verdict. Sans ça, le rapport serait écrit puis la task
174
- # échouerait, empêchant le stage suivant d'exister. Même principe que le
172
+ # pip-audit sort non-zéro dès qu'il trouve quelque chose, et cette task ne rend
173
+ # pas le verdict : son échec ne doit pas l'arrêter. Même principe que le
175
174
  # vuln-report.py de docker-image-pipeline, qui n'utilise pas le --exit-code de
176
175
  # trivy pour garantir que le rapport est écrit AVANT l'échec.
176
+ # '&& echo 0 || echo $?' et PAS '; echo $?' : Task exécute les cmds sous set -e,
177
+ # donc avec ';' le shell s'arrête sur l'échec de pip-audit avant d'écrire le code
178
+ # -- la task échouait, sans rapport, dans le seul cas où il sert. Une liste && ||
179
+ # n'est pas interrompue par set -e, et $? y est encore celui de pip-audit.
177
180
  # --skip-editable : les paquets installes en editable sont les membres du
178
181
  # workspace, qui ne peuvent figurer dans aucune base d'avis. pip-audit les
179
182
  # ignorait DEJA, faute de les trouver sur PyPI, mais en le disant -- le rapport
@@ -182,7 +185,7 @@ tasks:
182
185
  # Le code de sortie est mémorisé : c'est le verdict, et la sortie markdown seule
183
186
  # ne le porte pas.
184
187
  - task: uv
185
- vars: { ARGS: 'run pip-audit {{.AUDIT_ARGS}} --skip-editable --format markdown --output {{.AUDIT_REPORT}}.tmp; echo $? > {{.AUDIT_REPORT}}.rc' }
188
+ vars: { ARGS: 'run pip-audit {{.AUDIT_ARGS}} --skip-editable --format markdown --output {{.AUDIT_REPORT}}.tmp && echo 0 > {{.AUDIT_REPORT}}.rc || echo $? > {{.AUDIT_REPORT}}.rc' }
186
189
  # Le verdict est ECRIT par nous, parce que pip-audit ne le dit jamais en markdown :
187
190
  # sans trouvaille il n'écrit aucun fichier, et sans trouvaille mais avec des paquets
188
191
  # ignorés il n'écrit QUE leur table « Skip Reason ». Dans les deux cas le lecteur ne
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: new-plugin
3
- description: Créer un plugin Sherpa — importeur (pyimporters), processeur ou annotateur (pymultirole), et plus tard convertisseur, formateur ou segmenteur — à partir d'un format ou d'un besoin : clone la graine du contrat, écrit la classe et ses paramètres, les tests et leurs fixtures, la table du README, et câble le plugin dans le paquet de flavor du serveur. À utiliser quand on demande un nouvel importeur, un nouveau format de thésaurus, un nouveau processeur ou un nouvel annotateur, ou un nouveau plugin pyimporters / pymultirole.
3
+ description: Créer un plugin Sherpa — importeur (pyimporters), ou processeur, annotateur, convertisseur, formateur ou segmenteur (pymultirole) — à partir d'un format ou d'un besoin : clone la graine du contrat, écrit la classe et ses paramètres, les tests et leurs fixtures, la table du README, et câble le plugin dans le paquet de flavor du serveur. À utiliser quand on demande un nouvel importeur, un nouveau format de thésaurus, un nouveau processeur, annotateur, convertisseur (un format de fichier d'entrée), formateur (un format de sortie) ou segmenteur, ou un nouveau plugin pyimporters / pymultirole.
4
4
  ---
5
5
 
6
6
  # Créer un plugin Sherpa
@@ -65,6 +65,9 @@ d'un type à l'autre.
65
65
  - [`references/importer.md`](references/importer.md)
66
66
  - [`references/processor.md`](references/processor.md)
67
67
  - [`references/annotator.md`](references/annotator.md)
68
+ - [`references/converter.md`](references/converter.md)
69
+ - [`references/formatter.md`](references/formatter.md)
70
+ - [`references/segmenter.md`](references/segmenter.md)
68
71
 
69
72
  Deux règles valent pour tous les types :
70
73
 
@@ -84,6 +87,11 @@ paquet de flavor. Un plugin non câblé ne remonte jamais dans l'API.
84
87
  (`references/<type>.md` dit lequel) ;
85
88
  - aligner la borne de `pyimporters_plugins` ou `pymultirole-plugins` sur celle des
86
89
  dépôts frères, ne pas l'élargir ;
90
+ - **un plugin destiné à un client peut-il aller sur PyPI, qui est public ?**
91
+ Demander, ne pas supposer. Si non : `publish: false` et `archiveWheel: true` dans
92
+ le Jenkinsfile, et la roue rejoint `pymultirole_server/deps/` au lieu des
93
+ `dependencies` du paquet de flavor (`references/types.md`, « Un plugin propre à
94
+ un client ») ;
87
95
  - le dépôt Bitbucket `kairntech/<prefixe>_<nom>` (**underscore** : seul le nom de
88
96
  distribution prend le tiret) est créé par une personne, avec le script de
89
97
  création de dépôt, qui crée aussi le webhook et le job Jenkins. Il demande des
@@ -0,0 +1,66 @@
1
+ # Converter — le contenu à écrire
2
+
3
+ Graine `kairntech/pyconverters_seed`, layout `src/pyconverters_<nom>/`, fichier
4
+ `src/pyconverters_<nom>/<nom>.py`. Modèles vivants : `pyconverters_inscriptis`
5
+ (HTML → texte et phrases), `pyconverters_newsml` (un fichier, un document par `newsItem`).
6
+
7
+ ## À demander en plus
8
+
9
+ - **le format d'entrée** et **un fichier d'exemple réel** : c'est la fixture des
10
+ tests, dans `tests/data/`. Un convertisseur testé sur un fichier fabriqué pour
11
+ l'occasion passe, puis casse sur le premier vrai document ;
12
+ - **un document ou plusieurs par fichier** : une archive, un classeur, un flux
13
+ NewsML en donnent plusieurs ; que devient alors `identifier` ;
14
+ - **ce qui doit sortir en plus du texte** : `title`, `metadata` (langue, dates),
15
+ `properties`, `sentences` ou `boundaries` si le format les porte déjà ;
16
+ - **un service externe ?** (OCR, API de transcription) Son URL par défaut, la
17
+ variable d'environnement qui la surcharge, la clé et l'endroit où elle vit
18
+ (`tests/.env`, jamais versionné).
19
+
20
+ ## Le convertisseur
21
+
22
+ Remplacer le corps de `src/pyconverters_<nom>/<nom>.py`, **garder la forme** :
23
+
24
+ - les paramètres dérivent de `ConverterParameters`, qui est un `FormDataModel` :
25
+ le serveur les reçoit en JSON dans un champ de formulaire multipart, et le
26
+ modèle les décode lui-même. **Ne pas changer de classe de base** ;
27
+ `Field(défaut, description=...)` et `json_schema_extra={"extra": ...}` comme
28
+ pour un processeur (`advanced`, `internal`…) ;
29
+ - `convert(source, parameters)` reçoit un `UploadFile` de Starlette et rend une
30
+ **liste** de `Document`, même pour un seul. `source.file` est un flux binaire
31
+ côté serveur : décoder soi-même, avec l'encodage des paramètres ou celui que le
32
+ format déclare ; `source.filename` donne le nom ;
33
+ - `identifier` porte le nom du fichier (suffixé pour plusieurs documents), et
34
+ `properties["fileName"]` aussi, comme les convertisseurs frères : c'est ce qui
35
+ rattache le document à sa source dans Sherpa ;
36
+ - `ConverterBase` a pour métaclasse `ABCSingleton` : un modèle lourd (OCR…) se
37
+ charge une fois et se met en cache par paramètres, pas dans `self` à chaque
38
+ appel ;
39
+ - `get_model()` renvoie la classe de paramètres.
40
+
41
+ ## Les tests
42
+
43
+ - le test du modèle, le test des paramètres reçus en formulaire et le test de
44
+ découverte par entry-point de la graine, gardés ;
45
+ - un test par fixture de `tests/data/`, qui vérifie le **texte produit** (contre un
46
+ `.txt` attendu à côté quand il est long), `identifier` et ce que le format
47
+ apporte en plus (titre, phrases, métadonnées) ;
48
+ - un fichier dégradé ou vide, qui doit donner un document vide ou une erreur
49
+ claire, pas une trace illisible ;
50
+ - si le convertisseur appelle un service : aucun appel réseau par défaut, tests
51
+ live derrière `skipif` sur la clé ou un marqueur `integration`. Un dépôt dont
52
+ tous les tests sont sautés sans clé est **vert sans rien tester** : garder au
53
+ moins un test hors ligne.
54
+
55
+ ## Le README
56
+
57
+ Compléter la table des paramètres, et dire quels formats et quels types MIME le
58
+ convertisseur accepte.
59
+
60
+ ## Le câblage
61
+
62
+ `"pyconverters-<nom>>=1.6.0,<1.7.0"` dans les `dependencies` du paquet de flavor de
63
+ `pymultirole_server/packages/` : `pymultirole-builtins` pour un format générique,
64
+ la flavor du client sinon (`references/types.md`). Un paquet système (`ffmpeg`,
65
+ `libicu-dev`…) doit aussi être présent sur l'agent Jenkins, qui n'a pas de
66
+ conteneur : le dire à qui gère l'agent.
@@ -0,0 +1,54 @@
1
+ # Formatter — le contenu à écrire
2
+
3
+ Graine `kairntech/pyformatters_seed`, layout `src/pyformatters_<nom>/`, fichier
4
+ `src/pyformatters_<nom>/<nom>.py`. Modèles vivants : `pyformatters_json` (JSON),
5
+ `pyformatters_tabular` (CSV ou Excel selon un paramètre).
6
+
7
+ ## À demander en plus
8
+
9
+ - **le format de sortie** et son type MIME, et **un exemple du fichier attendu** :
10
+ c'est la fixture des tests ;
11
+ - **un document d'exemple réaliste**, annotations et catégories comprises : un
12
+ formateur rend ce que les plugins précédents ont produit, et un document nu ne
13
+ teste rien ;
14
+ - **ce qui entre dans la sortie** : le texte, les annotations (et leurs `terms`),
15
+ les catégories, les phrases, les métadonnées — et dans quel ordre.
16
+
17
+ ## Le formateur
18
+
19
+ Remplacer le corps de `src/pyformatters_<nom>/<nom>.py`, **garder la forme** :
20
+
21
+ - les paramètres dérivent de `FormatterParameters` et se déclarent en
22
+ `Field(défaut, description=...)` ; `json_schema_extra={"extra": ...}` comme pour
23
+ un processeur ;
24
+ - `format(document, parameters)` reçoit **un** document, pas un lot, et rend une
25
+ `Response` de Starlette, que le serveur transmet telle quelle au client. Poser le
26
+ `media_type`, et un `Content-Disposition: attachment; filename=...` comme les
27
+ formateurs frères, pour que le navigateur enregistre un fichier au bon nom ;
28
+ - lire les offsets tels quels : `document.text[a.start:a.end]` est le texte d'une
29
+ annotation (fin exclue). Ne rien recalculer, ne rien modifier du document ;
30
+ - `FormatterBase` a pour métaclasse `ABCSingleton` : rien dans `self` qui dépende
31
+ d'un appel ;
32
+ - `get_model()` renvoie la classe de paramètres.
33
+
34
+ ## Les tests
35
+
36
+ - le test du modèle et le test de découverte par entry-point de la graine, gardés ;
37
+ - un test de formatage qui relit la sortie (`json.loads(resp.body)`, un lecteur CSV,
38
+ `openpyxl` pour un classeur) et vérifie son **contenu**, pas seulement le code
39
+ 200 : `status_code`, `media_type` et `Content-Disposition` compris ;
40
+ - un document sans annotation, qui doit donner une sortie valide et vide ;
41
+ - attention au renommage de `task init` : il remplace le mot `seed` **partout**, y
42
+ compris dans les textes des tests. Un offset écrit en dur sur un texte qui
43
+ contient ce mot se décale après l'`init`.
44
+
45
+ ## Le README
46
+
47
+ Compléter la table des paramètres, et décrire la sortie : type MIME, structure,
48
+ ce qui y figure.
49
+
50
+ ## Le câblage
51
+
52
+ `"pyformatters-<nom>>=1.6.0,<1.7.0"` dans les `dependencies` du paquet de flavor de
53
+ `pymultirole_server/packages/` : `pymultirole-builtins` pour un format générique,
54
+ la flavor du client sinon (`references/types.md`).
@@ -0,0 +1,55 @@
1
+ # Segmenter — le contenu à écrire
2
+
3
+ Graine `kairntech/pysegmenters_seed`, layout `src/pysegmenters_<nom>/`, fichier
4
+ `src/pysegmenters_<nom>/<nom>.py`. Modèles vivants : `pysegmenters_syntok` et
5
+ `pysegmenters_blingfire` (phrases, selon la langue), `pysegmenters_md_splitter`
6
+ (sections d'un texte Markdown).
7
+
8
+ ## À demander en plus
9
+
10
+ - **l'unité produite** : phrases, paragraphes, sections, fenêtres de N caractères ;
11
+ - **les langues** à couvrir, et d'où vient la langue d'un document
12
+ (`document.metadata["language"]`, un paramètre, une détection) ;
13
+ - **des textes d'exemple réels** et le découpage attendu, avec ses cas difficiles :
14
+ abréviations, listes, titres, lignes vides, texte sans ponctuation.
15
+
16
+ ## Le segmenteur
17
+
18
+ Remplacer le corps de `src/pysegmenters_<nom>/<nom>.py`, **garder la forme** :
19
+
20
+ - les paramètres dérivent de `SegmenterParameters` et se déclarent en
21
+ `Field(défaut, description=...)` ; `json_schema_extra={"extra": ...}` comme pour
22
+ un processeur ;
23
+ - `segment(documents, parameters)` reçoit et rend **tout le lot**, sans perdre ni
24
+ réordonner de document ;
25
+ - il **remplace** `document.sentences` : un segmenteur définit le découpage, il ne
26
+ l'enrichit pas, et segmenter deux fois doit donner le même résultat ;
27
+ - une `Sentence` a `start` et `end` en **offsets de caractères dans
28
+ `document.text`, fin exclue**, sans les blancs de bord ; `metadata` et
29
+ `categories` sont optionnels ;
30
+ - les phrases couvrent le texte **sans trou ni chevauchement** hors blancs : un
31
+ annotateur ou un processeur en aval travaille phrase par phrase, et un morceau
32
+ de texte hors de toute phrase n'est jamais vu ;
33
+ - `SegmenterBase` a pour métaclasse `ABCSingleton` : un modèle de langue se charge
34
+ une fois, pas à chaque appel ;
35
+ - `get_model()` renvoie la classe de paramètres.
36
+
37
+ ## Les tests
38
+
39
+ - le test du modèle et le test de découverte par entry-point de la graine, gardés ;
40
+ - un test de segmentation qui vérifie `text[s.start:s.end]` pour chaque phrase,
41
+ contre la liste attendue : c'est ce qui attrape un décalage d'un caractère ou une
42
+ confusion octets/caractères ;
43
+ - un test d'idempotence (segmenter deux fois), et un document au texte vide ;
44
+ - un test par langue couverte, sur un texte réel.
45
+
46
+ ## Le README
47
+
48
+ Compléter la table des paramètres, et dire quelle unité le segmenteur produit et
49
+ pour quelles langues.
50
+
51
+ ## Le câblage
52
+
53
+ `"pysegmenters-<nom>>=1.6.0,<1.7.0"` dans les `dependencies` du paquet de flavor de
54
+ `pymultirole_server/packages/` : `pymultirole-builtins` pour un segmenteur
55
+ générique, la flavor du client sinon (`references/types.md`).
@@ -8,11 +8,11 @@ skill.
8
8
  | type | graine | groupe d'entry-points | classe de base | paquet de flavor | graine prête ? |
9
9
  |------|--------|-----------------------|----------------|------------------|----------------|
10
10
  | `importer` | `kairntech/pyimporters_seed` | `pyimporters.plugins` | `KnowledgeParserBase` (`pyimporters_plugins.base`) | `pyimporters_server/packages/pyimporters-builtins` | **oui** |
11
- | `converter` | `kairntech/pyconverters_seed` | `pyconverters.plugins` | `ConverterBase` (`pymultirole_plugins.v1.converter`) | `pymultirole_server/packages/pymultirole-builtins` | non |
11
+ | `converter` | `kairntech/pyconverters_seed` | `pyconverters.plugins` | `ConverterBase` (`pymultirole_plugins.v1.converter`) | `pymultirole_server/packages/pymultirole-builtins` | **oui** |
12
12
  | `processor` | `kairntech/pyprocessors_seed` | `pyprocessors.plugins` | `ProcessorBase` (`pymultirole_plugins.v1.processor`) | `pymultirole-builtins` ou une flavor dédiée | **oui** |
13
13
  | `annotator` | `kairntech/pyannotators_seed` | `pyannotators.plugins` | `AnnotatorBase` (`pymultirole_plugins.v1.annotator`) | `pymultirole-builtins` ou une flavor dédiée | **oui** |
14
- | `formatter` | `kairntech/pyformatters_seed` | `pyformatters.plugins` | `FormatterBase` (`pymultirole_plugins.v1.formatter`) | `pymultirole-builtins` | non |
15
- | `segmenter` | `kairntech/pysegmenters_seed` | `pysegmenters.plugins` | `SegmenterBase` (`pymultirole_plugins.v1.segmenter`) | `pymultirole-builtins` | non |
14
+ | `formatter` | `kairntech/pyformatters_seed` | `pyformatters.plugins` | `FormatterBase` (`pymultirole_plugins.v1.formatter`) | `pymultirole-builtins` | **oui** |
15
+ | `segmenter` | `kairntech/pysegmenters_seed` | `pysegmenters.plugins` | `SegmenterBase` (`pymultirole_plugins.v1.segmenter`) | `pymultirole-builtins` | **oui** |
16
16
 
17
17
  ## Ce que chaque graine prête produit
18
18
 
@@ -21,13 +21,19 @@ skill.
21
21
  | `importer` | à plat : `pyimporters_<nom>/` | `pyimporters_<nom>` | `CSV` → `CSVKnowledgeParser`, `CSVOptions` | « Sherpa FOO knowledge import plugin » | [`importer.md`](importer.md) |
22
22
  | `processor` | `src/pyprocessors_<nom>/` | `src/pyprocessors_<nom>` | `Foo` → `FooProcessor`, `FooParameters` | « Processor based on foo » | [`processor.md`](processor.md) |
23
23
  | `annotator` | `src/pyannotators_<nom>/` | `src/pyannotators_<nom>` | `Foo` → `FooAnnotator`, `FooParameters` | « Annotator based on foo » | [`annotator.md`](annotator.md) |
24
+ | `converter` | `src/pyconverters_<nom>/` | `src/pyconverters_<nom>` | `Foo` → `FooConverter`, `FooParameters` | « Converter based on foo » | [`converter.md`](converter.md) |
25
+ | `formatter` | `src/pyformatters_<nom>/` | `src/pyformatters_<nom>` | `Foo` → `FooFormatter`, `FooParameters` | « Formatter based on foo » | [`formatter.md`](formatter.md) |
26
+ | `segmenter` | `src/pysegmenters_<nom>/` | `src/pysegmenters_<nom>` | `Foo` → `FooSegmenter`, `FooParameters` | « Segmenter based on foo » | [`segmenter.md`](segmenter.md) |
24
27
 
25
28
  Le layout suit la famille, pas une préférence : les dépôts `pyimporters_*` ont leur
26
29
  paquet à plat, les dépôts `pymultirole` sont en `src/` — dont `pyprocessors_jev` et
27
30
  `pyannotators_entityfishing`, déjà passés sous `python-archetype`, sur lesquels les
28
- graines `pymultirole` sont calquées (ruff à 256 colonnes compris). Après `task init`,
29
- le Taskfile d'un processeur est identique octet pour octet à celui de
30
- `pyprocessors_jev`, celui d'un annotateur à celui de `pyannotators_entityfishing`.
31
+ graines `pymultirole` sont calquées (ruff à 256 colonnes compris). Les cinq graines
32
+ `pymultirole` partagent la même enveloppe au type près : après `task init`, le Taskfile
33
+ d'un plugin est identique octet pour octet à celui des dépôts frères de sa famille
34
+ (`pyprocessors_jev`, `pyannotators_entityfishing`, et les dépôts migrés de chaque
35
+ famille). Quelques dépôts frères plus anciens ont leur paquet à plat
36
+ (`pyformatters_json`, `pyprocessors_deepl`…) : un plugin neuf est en `src/`.
31
37
 
32
38
  Les paquets de flavor de `pymultirole_server/packages/` : `pymultirole-builtins`,
33
39
  `pymultirole-afp`, `pymultirole-cairn`, `pymultirole-escrim`, `pymultirole-grouperf`,
@@ -35,19 +41,38 @@ Les paquets de flavor de `pymultirole_server/packages/` : `pymultirole-builtins`
35
41
  serveur, pas une flavor). Un plugin propre à un client va dans la flavor de ce
36
42
  client ; demander, ne pas deviner.
37
43
 
38
- ## Trois graines existent aujourd'hui
44
+ ## Les six graines existent
39
45
 
40
- `pyimporters_seed`, `pyprocessors_seed` et `pyannotators_seed` sont poussées, et ne
41
- publient rien (`publish: false`). **Les trois autres n'existent pas encore.** La
42
- famille `pymultirole` passe progressivement sous `python-archetype` : quelques dépôts
43
- sont migrés, la plupart portent encore la sortie de `cookiecutter-pymultirole`
46
+ `pyimporters_seed`, et pour `pymultirole` `pyprocessors_seed`, `pyannotators_seed`,
47
+ `pyconverters_seed`, `pyformatters_seed` et `pysegmenters_seed`. Elles ne publient
48
+ rien (`publish: false`). La famille `pymultirole` est passée sous `python-archetype` ;
49
+ les dépôts restés en flit portent encore la sortie de `cookiecutter-pymultirole`
44
50
  (`bumpversion.py`, `AUTHORS.md`, `CHANGELOG.md`, `RELEASE.md`, `mypy.ini`, `docs/`).
45
51
 
46
- Si on demande un type dont la graine n'existe pas : **le dire et s'arrêter.**
52
+ Si on demande un type qui n'est pas dans la table : **le dire et s'arrêter.**
47
53
  Improviser l'enveloppe reviendrait à recréer à la main ce que la graine garantit,
48
54
  sans la garantie — et le dépôt produit devrait être migré comme les autres. La
49
- bonne réponse est de créer d'abord la graine du type, sur le modèle des trois
50
- existantes.
55
+ bonne réponse est de créer d'abord la graine du type, sur le modèle des existantes.
56
+
57
+ ## Un plugin propre à un client : construit, jamais publié
58
+
59
+ PyPI est public. Un plugin écrit pour un client (`pyprocessors_escrim_reconciliation`,
60
+ `pyformatters_json_rem`) ne doit pas y aller : **demander**, pour tout plugin destiné à
61
+ une flavor client, s'il peut être publié. Si non, remplacer dans le Jenkinsfile le
62
+ `publishBranches` seul par :
63
+
64
+ ```groovy
65
+ publish: false,
66
+ archiveWheel: true,
67
+ publishBranches: '^master$',
68
+ ```
69
+
70
+ `publish: false` est la garantie, et elle ne dépend de rien d'autre. `archiveWheel`
71
+ demande en plus le stage `version` et l'archivage de la roue dans le build Jenkins,
72
+ d'où elle rejoint `pymultirole_server/deps/` ; le Dockerfile de la flavor l'y prend
73
+ (`find deps -name '<distribution>-*.whl' | sort -V | tail -n 1`). Aucun index ne la
74
+ servant, **ne pas** l'ajouter aux `dependencies` du paquet de flavor : la résolution y
75
+ chercherait une version publiée, qui n'existe pas ou qui est ancienne.
51
76
 
52
77
  ## Pourquoi une graine par contrat, et un seul skill
53
78
 
@@ -33,6 +33,12 @@
33
33
  * @param throttleCategory Throttle category shared with the sibling repositories. Omit for none.
34
34
  * @param publish Publish the wheel to PyPI. Default true; never on a pull request,
35
35
  * and never off a publishBranches branch.
36
+ * @param archiveWheel Archive the wheel in the build, with the version bump that goes with
37
+ * it. Default false. For a client-specific plugin that must never reach
38
+ * PyPI, which is public: 'publish: false, archiveWheel: true' versions,
39
+ * builds and archives, and sends nothing out. 'publish: false' alone is
40
+ * the guarantee -- an older copy of this file ignores archiveWheel and
41
+ * still publishes nothing. Same branch and pull-request rules as publish.
36
42
  * @param publishBranches Regex the branch must FULLY match for 'version' and 'py:publish'
37
43
  * to run, e.g. '^(master|py38)$'. Falls back to the
38
44
  * CI_PUBLISH_BRANCHES global environment variable, then '^master$'.
@@ -90,6 +96,9 @@ def call(Map config = [:]) {
90
96
  env._TIMEOUT_TIME = buildTimeout.time as String
91
97
  env._TIMEOUT_UNIT = buildTimeout.unit ?: 'HOURS'
92
98
  env._PUBLISH = config.get('publish', true) ? 'true' : 'false'
99
+ // Versionner et archiver la roue sans la publier : un plugin propre a un client est
100
+ // construit ici, mais ne doit jamais partir sur PyPI, qui est public.
101
+ env._ARCHIVE_WHEEL = config.get('archiveWheel', false) ? 'true' : 'false'
93
102
  // Les branches depuis lesquelles publier. Une branche de feature ne doit RIEN
94
103
  // publier : PyPI est immuable et son espace de noms est commun a toutes les
95
104
  // branches, donc une branche neuve dont le compteur de build repasse sur un numero
@@ -168,7 +177,8 @@ def call(Map config = [:]) {
168
177
  checkStageDrift()
169
178
  // Dire pourquoi 'version' et 'py:publish' seront joues ou non :
170
179
  // un stage grise sans explication laisse chercher.
171
- String state = publishAllowed() ? 'activee' : 'desactivee'
180
+ String state = publishAllowed() ? 'activee' :
181
+ (releaseAllowed() ? 'desactivee, roue archivee sans publication' : 'desactivee')
172
182
  // Le '+' DOIT rester en fin de ligne. Place en tete de la ligne
173
183
  // suivante, Groovy termine l'instruction 'echo' et lit '+ "..."'
174
184
  // comme un plus UNAIRE sur une String : String.positive(), qui
@@ -280,7 +290,7 @@ def call(Map config = [:]) {
280
290
  when {
281
291
  allOf {
282
292
  not { changeRequest() }
283
- expression { return publishAllowed() }
293
+ expression { return releaseAllowed() }
284
294
  }
285
295
  }
286
296
  steps {
@@ -297,11 +307,13 @@ def call(Map config = [:]) {
297
307
  }
298
308
  }
299
309
 
310
+ // Publie sur PyPI, ou archive seulement la roue d'un plugin confidentiel
311
+ // (archiveWheel) : les deux partagent la garde de version de publishWheel().
300
312
  stage('py:publish') {
301
313
  when {
302
314
  allOf {
303
315
  not { changeRequest() }
304
- expression { return publishAllowed() }
316
+ expression { return releaseAllowed() }
305
317
  }
306
318
  }
307
319
  steps {
@@ -379,6 +391,22 @@ private boolean publishAllowed() {
379
391
  if (env._PUBLISH != 'true') {
380
392
  return false
381
393
  }
394
+ return onPublishBranch()
395
+ }
396
+
397
+ /**
398
+ * True when this build produces a release: a version commit and a wheel that leaves the build,
399
+ * whether on PyPI (publish) or only as a build artifact (archiveWheel). Same branch rule as
400
+ * publishAllowed(): a feature branch must not push version commits either.
401
+ */
402
+ private boolean releaseAllowed() {
403
+ if (env._PUBLISH != 'true' && env._ARCHIVE_WHEEL != 'true') {
404
+ return false
405
+ }
406
+ return onPublishBranch()
407
+ }
408
+
409
+ private boolean onPublishBranch() {
382
410
  String branch = currentBranch()
383
411
  return branch && (branch ==~ env._PUBLISH_BRANCHES)
384
412
  }
@@ -617,6 +645,18 @@ private void publishWheel() {
617
645
  return
618
646
  }
619
647
 
648
+ if (env._ARCHIVE_WHEEL == 'true') {
649
+ // Le nom EXACT de la roue de ce build, pas 'dist/*.whl' : dist/ vit dans le workspace
650
+ // persistant et garde les roues des builds precedents. Aucune roue a ce nom est une
651
+ // erreur -- un build qui annonce une roue archivee et n'en archive aucune ment.
652
+ archiveArtifacts(artifacts: "dist/*-${version}-*.whl", fingerprint: true)
653
+ }
654
+ // Test STRICT de 'true' : la publication n'a lieu que si elle est demandee, jamais par
655
+ // defaut d'une valeur inattendue.
656
+ if (env._PUBLISH != 'true') {
657
+ echo "publish: false -- nothing sent to PyPI."
658
+ return
659
+ }
620
660
  withEnv([
621
661
  "UV_PUBLISH_USERNAME=${credentialFrom(env._PYPI_FILE, 'FLIT_USERNAME')}",
622
662
  "UV_PUBLISH_PASSWORD=${credentialFrom(env._PYPI_FILE, 'FLIT_PASSWORD')}",