xml-cmpr 0.2.0__tar.gz → 0.2.1__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.
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/PKG-INFO +49 -138
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/README.md +48 -137
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/pyproject.toml +1 -1
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/uv.lock +23 -23
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/.gitignore +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/LICENSE +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/docs/README.md +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/docs/analyses/README.md +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/src/xml_cmpr/__init__.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/src/xml_cmpr/archive.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/src/xml_cmpr/campaign.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/src/xml_cmpr/canonical.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/src/xml_cmpr/cli.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/src/xml_cmpr/diff.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/src/xml_cmpr/discovery.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/src/xml_cmpr/model.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/src/xml_cmpr/py.typed +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/src/xml_cmpr/report.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/src/xml_cmpr/resources/__init__.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/src/xml_cmpr/resources/normalize.xsl +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/conftest.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/fixtures/a/absent_de_b.xml +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/fixtures/a/attributs.xml +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/fixtures/a/factures/facture_001.xml +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/fixtures/a/identique.xml +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/fixtures/a/malforme.xml +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/fixtures/a/mixte.xml +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/fixtures/a/notes.txt +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/fixtures/a/texte.xml +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/fixtures/b/attributs.xml +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/fixtures/b/factures/facture_001.xml +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/fixtures/b/identique.xml +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/fixtures/b/malforme.xml +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/fixtures/b/mixte.xml +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/fixtures/b/notes.txt +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/fixtures/b/seulement_en_b.xml +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/fixtures/b/texte.xml +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/test_archive.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/test_campaign.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/test_canonical.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/test_cli.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/test_diff.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/test_discovery.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/test_docs.py +0 -0
- {xml_cmpr-0.2.0 → xml_cmpr-0.2.1}/tests/test_report.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: xml-cmpr
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Compare des documents XML issus de deux sources et produit un rapport des différences
|
|
5
5
|
Project-URL: Homepage, https://framagit.org/opikanoba/xml-compare
|
|
6
6
|
Project-URL: Repository, https://framagit.org/opikanoba/xml-compare.git
|
|
@@ -30,7 +30,7 @@ Compare des documents XML produits par **deux chaînes de production différente
|
|
|
30
30
|
produit un rapport des différences, fichier par fichier.
|
|
31
31
|
|
|
32
32
|
L'outil parcourt la source A, cherche dans la source B le fichier de même chemin
|
|
33
|
-
relatif (les fichiers sans
|
|
33
|
+
relatif (les fichiers sans correspondance sont ignorés), compare les deux documents sur leur
|
|
34
34
|
**forme canonique**, et écrit un rapport Markdown et/ou JSON dans le répertoire de
|
|
35
35
|
sortie.
|
|
36
36
|
|
|
@@ -49,25 +49,15 @@ xml-cmpr --help
|
|
|
49
49
|
|
|
50
50
|
Python 3.13 ou 3.14 est requis (le socle de `saxonche`, qui apporte le moteur XSLT).
|
|
51
51
|
|
|
52
|
-
Le paquet lui-même est en Python pur, mais `saxonche`
|
|
53
|
-
sans source à compiler. L'installation n'aboutit donc que sur les plateformes pour
|
|
54
|
-
lesquelles il existe une roue :
|
|
52
|
+
Le paquet lui-même est en Python pur, mais `saxonche` est publié sans source.
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
|---|---|
|
|
58
|
-
| Linux (glibc 2.24 ou plus, « manylinux ») | x86_64, aarch64 |
|
|
59
|
-
| macOS (11 ou plus) | x86_64, arm64 |
|
|
60
|
-
| Windows | x86_64 |
|
|
61
|
-
|
|
62
|
-
Ailleurs — Alpine et autres Linux musl, processeurs 32 bits, PyPy, Python 3.15 —,
|
|
63
|
-
l'installation échoue en résolvant `saxonche` (pip : « No matching distribution found
|
|
64
|
-
for saxonche » ; uv : aucune roue pour la plateforme courante) : ce message signale une
|
|
65
|
-
plateforme non prise en charge, pas un défaut du paquet.
|
|
66
|
-
Pour travailler sur le dépôt lui-même :
|
|
54
|
+
L'installation n'aboutit donc que sur les plateformes pour lesquelles il existe la librairie a été publiée :
|
|
67
55
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
56
|
+
| Système | Architectures |
|
|
57
|
+
| ----------------------------------------- | --------------- |
|
|
58
|
+
| Linux (glibc 2.24 ou plus, « manylinux ») | x86_64, aarch64 |
|
|
59
|
+
| macOS (11 ou plus) | x86_64, arm64 |
|
|
60
|
+
| Windows | x86_64 |
|
|
71
61
|
|
|
72
62
|
## Utilisation
|
|
73
63
|
|
|
@@ -106,18 +96,18 @@ Produit `REPERTOIRE_SORTIE/rapport.md` et `REPERTOIRE_SORTIE/rapport.json`
|
|
|
106
96
|
|
|
107
97
|
### Options
|
|
108
98
|
|
|
109
|
-
| Option
|
|
110
|
-
|
|
111
|
-
| `--pattern GLOB`
|
|
112
|
-
| `--no-recursive`
|
|
113
|
-
| `--keep-whitespace`
|
|
114
|
-
| `--exclude FICHIER`
|
|
115
|
-
| `--max-diffs-per-file N` | différences détaillées par fichier (défaut : 50, `0` = illimité)
|
|
116
|
-
| `--no-size-limit`
|
|
117
|
-
| `--format FORMATS`
|
|
118
|
-
| `--fail-on-diff`
|
|
119
|
-
| `--fail-on-error`
|
|
120
|
-
| `-v`, `--verbose`
|
|
99
|
+
| Option | Effet |
|
|
100
|
+
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
101
|
+
| `--pattern GLOB` | motif des fichiers de A (défaut : `*.xml`) |
|
|
102
|
+
| `--no-recursive` | ne pas descendre dans les sous-répertoires |
|
|
103
|
+
| `--keep-whitespace` | ne pas normaliser les espaces des nœuds texte |
|
|
104
|
+
| `--exclude FICHIER` | fichier contenant des expressions XPath à exclure de la comparaison |
|
|
105
|
+
| `--max-diffs-per-file N` | différences détaillées par fichier (défaut : 50, `0` = illimité) |
|
|
106
|
+
| `--no-size-limit` | extraire une archive ZIP quels que soient sa taille et son nombre d'entrées. Par défaut les limites sont fixées à 1 Go ou de 100 000 fichiers. |
|
|
107
|
+
| `--format FORMATS` | formats du rapport : `md`, `json` (défaut : `md,json`) |
|
|
108
|
+
| `--fail-on-diff` | code de sortie 1 si au moins une différence |
|
|
109
|
+
| `--fail-on-error` | code de sortie 3 si au moins un fichier est illisible |
|
|
110
|
+
| `-v`, `--verbose` | progression sur stderr |
|
|
121
111
|
|
|
122
112
|
Codes de sortie : `0` succès · `1` différences détectées (avec `--fail-on-diff`) ·
|
|
123
113
|
`2` erreur d'usage (source introuvable ou illisible, sortie inutilisable, XPath
|
|
@@ -125,10 +115,7 @@ d'exclusion invalide, format inconnu) · `3` fichiers en erreur (avec
|
|
|
125
115
|
`--fail-on-error`).
|
|
126
116
|
|
|
127
117
|
Un fichier XML malformé n'interrompt pas la campagne : il est reporté comme fichier en
|
|
128
|
-
erreur, et **sans `--fail-on-error` la
|
|
129
|
-
intégration continue, poser les deux drapeaux : `--fail-on-diff` seul laisse passer un
|
|
130
|
-
corpus entièrement illisible. Quand les deux conditions sont réunies, `3` l'emporte —
|
|
131
|
-
une campagne incomplète est un résultat moins sûr qu'une campagne qui a tout comparé.
|
|
118
|
+
erreur, et **sans `--fail-on-error` la comparaison se termine, avec un code retour `0`**.
|
|
132
119
|
|
|
133
120
|
### Le rapport JSON
|
|
134
121
|
|
|
@@ -189,24 +176,17 @@ consommable par un autre traitement :
|
|
|
189
176
|
|
|
190
177
|
Une source ZIP est extraite dans un répertoire temporaire, supprimé à la fin de la
|
|
191
178
|
campagne ; la comparaison, les options et le rapport sont ensuite exactement les mêmes
|
|
192
|
-
que pour un répertoire (le rapport nomme l'archive, pas le temporaire).
|
|
193
|
-
|
|
194
|
-
Si l'archive enveloppe tout son contenu dans un unique répertoire de tête, celui-ci est
|
|
195
|
-
traversé : deux archives dont les répertoires de tête s'appellent `chaine_a/` et
|
|
196
|
-
`chaine_b/` s'apparient quand même sur le chemin relatif interne.
|
|
179
|
+
que pour un répertoire (le rapport nomme l'archive, pas le répertoire temporaire).
|
|
197
180
|
|
|
198
181
|
Une archive dont le contenu **décompressé** dépasse **1 Gio**, ou qui compte plus de
|
|
199
182
|
**100 000 entrées** (fichiers et répertoires), est refusée (code `2`), avant toute
|
|
200
183
|
extraction : les bornes valent pour chaque archive séparément, et ne concernent pas une
|
|
201
|
-
source répertoire, qui n'est pas copiée. `--no-size-limit`
|
|
202
|
-
explicitement — en connaissance de cause, le répertoire temporaire devant alors pouvoir
|
|
203
|
-
accueillir l'archive entière.
|
|
184
|
+
source répertoire, qui n'est pas copiée. `--no-size-limit` lève les deux limites.
|
|
204
185
|
|
|
205
186
|
## Utiliser la comparaison depuis Python
|
|
206
187
|
|
|
207
188
|
Le pipeline se décompose en trois étapes indépendantes : **analyse**, **formatage**,
|
|
208
|
-
**écriture**. Le formatage retourne une chaîne
|
|
209
|
-
elle peut partir directement dans une réponse HTTP ou vers le traitement suivant.
|
|
189
|
+
**écriture**. Le formatage retourne une chaîne qui n'est pas directement écrite sur le disque, elle peut donc être utilisée dans une réponse HTTP ou vers un autre traitement.
|
|
210
190
|
|
|
211
191
|
```python
|
|
212
192
|
from pathlib import Path
|
|
@@ -220,13 +200,13 @@ corps = render(summary, ReportFormat.JSON) # formatage -> str
|
|
|
220
200
|
write_report(summary, Path("rapport"), "json") # écriture -> Path (facultatif)
|
|
221
201
|
```
|
|
222
202
|
|
|
223
|
-
| Étape
|
|
224
|
-
|
|
225
|
-
| analyse
|
|
226
|
-
| formatage | `render(summary, "md"\|"json")`
|
|
227
|
-
|
|
|
228
|
-
| écriture
|
|
229
|
-
|
|
|
203
|
+
| Étape | Fonction | Rend |
|
|
204
|
+
| --------- | ----------------------------------------------- | --------------------------------------- |
|
|
205
|
+
| analyse | `run_comparison(source_a, source_b, **options)` | `RunSummary` |
|
|
206
|
+
| formatage | `render(summary, "md"\|"json")` | `str` |
|
|
207
|
+
| | `summary.as_dict()` | `dict` (JSON sans passer par la chaîne) |
|
|
208
|
+
| écriture | `write_report(summary, dossier, format)` | `Path` du fichier écrit |
|
|
209
|
+
| | `write_reports(summary, dossier, formats)` | `list[Path]` |
|
|
230
210
|
|
|
231
211
|
`run_comparison` accepte les mêmes options que le CLI (`pattern`, `recursive`,
|
|
232
212
|
`keep_whitespace`, `exclude_xpaths`, `max_diffs_per_file`), et
|
|
@@ -247,21 +227,14 @@ with as_directory(Path("chaine_a.zip")) as dir_a:
|
|
|
247
227
|
|
|
248
228
|
### Les exceptions
|
|
249
229
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
| Exception | Levée par | Cause |
|
|
253
|
-
|---|---|---|
|
|
254
|
-
| `ExcludeError` | `run_comparison`, `Canonicalizer` | un XPath d'exclusion est invalide |
|
|
255
|
-
| `ArchiveError` | `as_directory` | source introuvable, archive illisible, chemin d'entrée suspect |
|
|
256
|
-
| `ArchiveSizeError` | `as_directory` | sous-classe d'`ArchiveError` : archive au-delà de `size_limit` ou d'`entry_limit` (attributs `size`, `limit`, et `measure` : `"bytes"` ou `"entries"`) |
|
|
257
|
-
| `CanonicalError` | `Canonicalizer.load` | un document est illisible ou malformé |
|
|
230
|
+
Les exceptions de `xml_cmpr` sont dérivées d'`Exception` :
|
|
258
231
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
`
|
|
263
|
-
|
|
264
|
-
|
|
232
|
+
| Exception | Levée par | Cause |
|
|
233
|
+
| ------------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
234
|
+
| `ExcludeError` | `run_comparison`, `Canonicalizer` | un XPath d'exclusion est invalide |
|
|
235
|
+
| `ArchiveError` | `as_directory` | source introuvable, archive illisible, chemin d'entrée suspect |
|
|
236
|
+
| `ArchiveSizeError` | `as_directory` | sous-classe d'`ArchiveError` : archive au-delà de `size_limit` ou d'`entry_limit` (attributs `size`, `limit`, et `measure` : `"bytes"` ou `"entries"`) |
|
|
237
|
+
| `CanonicalError` | `Canonicalizer.load` | un document est illisible ou malformé |
|
|
265
238
|
|
|
266
239
|
### Exclure des éléments ou attributs de la comparaison
|
|
267
240
|
|
|
@@ -279,10 +252,10 @@ les URI de namespace terminées par `#` restent écrivables.
|
|
|
279
252
|
|
|
280
253
|
Chaque élément ou attribut sélectionné par l'un de ces XPath, dans A comme dans B, est
|
|
281
254
|
retiré de la forme canonique **avant** le calcul de l'empreinte : il ne compte ni pour
|
|
282
|
-
le test d'identité, ni dans le diff. Un élément
|
|
255
|
+
le test d'identité, ni dans le diff. Un élément trouvé dans 1 XPath exclut tout son sous-arbre.
|
|
283
256
|
|
|
284
257
|
Le XPath est évalué par Saxon directement sur le document source, sans les préfixes de
|
|
285
|
-
namespace déclarés dans le document
|
|
258
|
+
namespace déclarés dans le document. Pour un nom
|
|
286
259
|
sans espace de noms, écrire `//DateGeneration` ; pour un nom avec espace de noms,
|
|
287
260
|
utiliser la notation `Q{uri}local`, par ex. `//Q{urn:facture}DateGeneration`.
|
|
288
261
|
|
|
@@ -302,25 +275,7 @@ différences :
|
|
|
302
275
|
|
|
303
276
|
Ils ne sont **jamais** comparés, et aucune option ne les réintègre. Ni ceux placés dans
|
|
304
277
|
un élément, ni ceux du prologue — la bannière de génération qu'une chaîne écrit avant
|
|
305
|
-
l'élément racine, la PI `<?xml-stylesheet?>` qui désigne une feuille de style.
|
|
306
|
-
documents qui ne diffèrent que par là sont `identique`.
|
|
307
|
-
|
|
308
|
-
C'est un choix de périmètre, pas une limite d'implémentation : l'outil compare ce que
|
|
309
|
-
les documents *disent* — éléments, attributs, texte — et un commentaire ne dit rien du
|
|
310
|
-
contenu. Deux chaînes de production qui datent et signent différemment leurs sorties ne
|
|
311
|
-
doivent pas produire un rapport plein de différences pour autant. Pour comparer une
|
|
312
|
-
bannière de génération, comparer les fichiers en texte, pas en XML.
|
|
313
|
-
|
|
314
|
-
Sont des différences : l'ordre des éléments frères, la présence ou la valeur d'un
|
|
315
|
-
attribut, le texte d'un élément, la présence d'un élément, et **la position du texte en
|
|
316
|
-
contenu mixte**.
|
|
317
|
-
|
|
318
|
-
Les éléments frères sont appariés d'abord par empreinte de sous-arbre, puis par nom :
|
|
319
|
-
insérer un `<Ligne>` au milieu d'une liste coûte **une** différence, pas un décalage en
|
|
320
|
-
cascade sur tous les suivants. Enfin, deux éléments qui ne diffèrent **que** par leur nom
|
|
321
|
-
(ou leur espace de noms) sont appariés et rapportés en `element_renamed`, à n'importe
|
|
322
|
-
quelle profondeur ; un élément à la fois renommé et modifié reste un `element_removed`
|
|
323
|
-
suivi d'un `element_added`.
|
|
278
|
+
l'élément racine, la PI `<?xml-stylesheet?>` qui désigne une feuille de style.
|
|
324
279
|
|
|
325
280
|
### Le contenu mixte
|
|
326
281
|
|
|
@@ -333,57 +288,13 @@ A : <p>Voir la <ref>note</ref> ci-dessous.</p>
|
|
|
333
288
|
B : <p>Voir la ci-dessous.<ref>note</ref></p>
|
|
334
289
|
```
|
|
335
290
|
|
|
336
|
-
| Chemin
|
|
337
|
-
|
|
338
|
-
| `/p/#text[1]` | texte modifié
|
|
339
|
-
| `/p/#text[2]` | texte absent de B | `ci-dessous.` | —
|
|
291
|
+
| Chemin | Type | A | B |
|
|
292
|
+
| ------------- | ----------------- | ------------- | --------------------- |
|
|
293
|
+
| `/p/#text[1]` | texte modifié | `Voir la` | `Voir la ci-dessous.` |
|
|
294
|
+
| `/p/#text[2]` | texte absent de B | `ci-dessous.` | — |
|
|
340
295
|
|
|
341
|
-
Les deux documents
|
|
342
|
-
qui ne contient que du texte, lui, n'est pas concerné — c'est le cas courant, et son
|
|
343
|
-
texte reste comparé d'un bloc (`text_changed` sur le chemin de l'élément).
|
|
296
|
+
Les deux documents sont considérés comme **différents**.
|
|
344
297
|
|
|
345
|
-
##
|
|
346
|
-
|
|
347
|
-
```sh
|
|
348
|
-
uv run pytest # tests
|
|
349
|
-
uv run ruff check . && uv run ruff format .
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
## Publication
|
|
353
|
-
|
|
354
|
-
`publish.sh` enchaîne les étapes d'une livraison sur PyPI : contrôle du verrou, `ruff`,
|
|
355
|
-
`pytest`, construction des distributions, contrôle des métadonnées (`twine check`),
|
|
356
|
-
vérification que la roue embarque bien `normalize.xsl`, puis téléversement. Il refuse un
|
|
357
|
-
arbre de travail modifié ou une version déjà publiée, et ne pose aucun tag lui-même — il
|
|
358
|
-
rappelle la commande à la fin.
|
|
359
|
-
|
|
360
|
-
```sh
|
|
361
|
-
uv version --bump patch # 0.2.0 -> 0.2.1 (ou minor / major)
|
|
362
|
-
./publish.sh --test # répétition générale sur TestPyPI
|
|
363
|
-
./publish.sh # publication réelle sur PyPI
|
|
364
|
-
```
|
|
298
|
+
## Licence
|
|
365
299
|
|
|
366
|
-
|
|
367
|
-
|---|---|
|
|
368
|
-
| `--test` | publier sur TestPyPI (index `testpypi` déclaré dans `pyproject.toml`) |
|
|
369
|
-
| `--dry-run` | tout exécuter sauf le téléversement |
|
|
370
|
-
| `--skip-checks` | ne pas rejouer `ruff` et `pytest` |
|
|
371
|
-
| `--allow-dirty` | autoriser un arbre de travail modifié |
|
|
372
|
-
|
|
373
|
-
Le jeton d'API s'obtient, par ordre de préférence :
|
|
374
|
-
|
|
375
|
-
1. par une commande, `PUBLISH_TOKEN_CMD='pass show pypi/xml-cmpr' ./publish.sh` — le
|
|
376
|
-
jeton ne transite ni par l'historique du shell ni par l'environnement ;
|
|
377
|
-
2. par saisie au clavier, sans écho, si rien n'est fourni et que le script tourne dans
|
|
378
|
-
un terminal ;
|
|
379
|
-
3. par `UV_PUBLISH_TOKEN`, toléré pour la compatibilité mais déconseillé : exporté dans
|
|
380
|
-
le profil, il est lisible par tous les processus de la session.
|
|
381
|
-
|
|
382
|
-
Quelle que soit sa source, le script retire le jeton de l'environnement dès son
|
|
383
|
-
démarrage et ne le remet qu'à `uv publish` : `pytest`, `uv build` et `twine` — donc les
|
|
384
|
-
dépendances de dev et les hooks de build — ne le voient jamais. Après la première
|
|
385
|
-
publication, remplacer le jeton global du compte par un jeton limité au projet.
|
|
386
|
-
|
|
387
|
-
Le jeton est celui du dépôt visé — un jeton PyPI ne vaut pas sur TestPyPI, et
|
|
388
|
-
réciproquement. Après un `--test`, l'installation se vérifie par
|
|
389
|
-
`uv tool install --index https://test.pypi.org/simple xml-cmpr`.
|
|
300
|
+
Distribué sous licence [MIT](LICENSE) — © 2026 Frédéric Laurent.
|
|
@@ -4,7 +4,7 @@ Compare des documents XML produits par **deux chaînes de production différente
|
|
|
4
4
|
produit un rapport des différences, fichier par fichier.
|
|
5
5
|
|
|
6
6
|
L'outil parcourt la source A, cherche dans la source B le fichier de même chemin
|
|
7
|
-
relatif (les fichiers sans
|
|
7
|
+
relatif (les fichiers sans correspondance sont ignorés), compare les deux documents sur leur
|
|
8
8
|
**forme canonique**, et écrit un rapport Markdown et/ou JSON dans le répertoire de
|
|
9
9
|
sortie.
|
|
10
10
|
|
|
@@ -23,25 +23,15 @@ xml-cmpr --help
|
|
|
23
23
|
|
|
24
24
|
Python 3.13 ou 3.14 est requis (le socle de `saxonche`, qui apporte le moteur XSLT).
|
|
25
25
|
|
|
26
|
-
Le paquet lui-même est en Python pur, mais `saxonche`
|
|
27
|
-
sans source à compiler. L'installation n'aboutit donc que sur les plateformes pour
|
|
28
|
-
lesquelles il existe une roue :
|
|
26
|
+
Le paquet lui-même est en Python pur, mais `saxonche` est publié sans source.
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|---|---|
|
|
32
|
-
| Linux (glibc 2.24 ou plus, « manylinux ») | x86_64, aarch64 |
|
|
33
|
-
| macOS (11 ou plus) | x86_64, arm64 |
|
|
34
|
-
| Windows | x86_64 |
|
|
35
|
-
|
|
36
|
-
Ailleurs — Alpine et autres Linux musl, processeurs 32 bits, PyPy, Python 3.15 —,
|
|
37
|
-
l'installation échoue en résolvant `saxonche` (pip : « No matching distribution found
|
|
38
|
-
for saxonche » ; uv : aucune roue pour la plateforme courante) : ce message signale une
|
|
39
|
-
plateforme non prise en charge, pas un défaut du paquet.
|
|
40
|
-
Pour travailler sur le dépôt lui-même :
|
|
28
|
+
L'installation n'aboutit donc que sur les plateformes pour lesquelles il existe la librairie a été publiée :
|
|
41
29
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
30
|
+
| Système | Architectures |
|
|
31
|
+
| ----------------------------------------- | --------------- |
|
|
32
|
+
| Linux (glibc 2.24 ou plus, « manylinux ») | x86_64, aarch64 |
|
|
33
|
+
| macOS (11 ou plus) | x86_64, arm64 |
|
|
34
|
+
| Windows | x86_64 |
|
|
45
35
|
|
|
46
36
|
## Utilisation
|
|
47
37
|
|
|
@@ -80,18 +70,18 @@ Produit `REPERTOIRE_SORTIE/rapport.md` et `REPERTOIRE_SORTIE/rapport.json`
|
|
|
80
70
|
|
|
81
71
|
### Options
|
|
82
72
|
|
|
83
|
-
| Option
|
|
84
|
-
|
|
85
|
-
| `--pattern GLOB`
|
|
86
|
-
| `--no-recursive`
|
|
87
|
-
| `--keep-whitespace`
|
|
88
|
-
| `--exclude FICHIER`
|
|
89
|
-
| `--max-diffs-per-file N` | différences détaillées par fichier (défaut : 50, `0` = illimité)
|
|
90
|
-
| `--no-size-limit`
|
|
91
|
-
| `--format FORMATS`
|
|
92
|
-
| `--fail-on-diff`
|
|
93
|
-
| `--fail-on-error`
|
|
94
|
-
| `-v`, `--verbose`
|
|
73
|
+
| Option | Effet |
|
|
74
|
+
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
75
|
+
| `--pattern GLOB` | motif des fichiers de A (défaut : `*.xml`) |
|
|
76
|
+
| `--no-recursive` | ne pas descendre dans les sous-répertoires |
|
|
77
|
+
| `--keep-whitespace` | ne pas normaliser les espaces des nœuds texte |
|
|
78
|
+
| `--exclude FICHIER` | fichier contenant des expressions XPath à exclure de la comparaison |
|
|
79
|
+
| `--max-diffs-per-file N` | différences détaillées par fichier (défaut : 50, `0` = illimité) |
|
|
80
|
+
| `--no-size-limit` | extraire une archive ZIP quels que soient sa taille et son nombre d'entrées. Par défaut les limites sont fixées à 1 Go ou de 100 000 fichiers. |
|
|
81
|
+
| `--format FORMATS` | formats du rapport : `md`, `json` (défaut : `md,json`) |
|
|
82
|
+
| `--fail-on-diff` | code de sortie 1 si au moins une différence |
|
|
83
|
+
| `--fail-on-error` | code de sortie 3 si au moins un fichier est illisible |
|
|
84
|
+
| `-v`, `--verbose` | progression sur stderr |
|
|
95
85
|
|
|
96
86
|
Codes de sortie : `0` succès · `1` différences détectées (avec `--fail-on-diff`) ·
|
|
97
87
|
`2` erreur d'usage (source introuvable ou illisible, sortie inutilisable, XPath
|
|
@@ -99,10 +89,7 @@ d'exclusion invalide, format inconnu) · `3` fichiers en erreur (avec
|
|
|
99
89
|
`--fail-on-error`).
|
|
100
90
|
|
|
101
91
|
Un fichier XML malformé n'interrompt pas la campagne : il est reporté comme fichier en
|
|
102
|
-
erreur, et **sans `--fail-on-error` la
|
|
103
|
-
intégration continue, poser les deux drapeaux : `--fail-on-diff` seul laisse passer un
|
|
104
|
-
corpus entièrement illisible. Quand les deux conditions sont réunies, `3` l'emporte —
|
|
105
|
-
une campagne incomplète est un résultat moins sûr qu'une campagne qui a tout comparé.
|
|
92
|
+
erreur, et **sans `--fail-on-error` la comparaison se termine, avec un code retour `0`**.
|
|
106
93
|
|
|
107
94
|
### Le rapport JSON
|
|
108
95
|
|
|
@@ -163,24 +150,17 @@ consommable par un autre traitement :
|
|
|
163
150
|
|
|
164
151
|
Une source ZIP est extraite dans un répertoire temporaire, supprimé à la fin de la
|
|
165
152
|
campagne ; la comparaison, les options et le rapport sont ensuite exactement les mêmes
|
|
166
|
-
que pour un répertoire (le rapport nomme l'archive, pas le temporaire).
|
|
167
|
-
|
|
168
|
-
Si l'archive enveloppe tout son contenu dans un unique répertoire de tête, celui-ci est
|
|
169
|
-
traversé : deux archives dont les répertoires de tête s'appellent `chaine_a/` et
|
|
170
|
-
`chaine_b/` s'apparient quand même sur le chemin relatif interne.
|
|
153
|
+
que pour un répertoire (le rapport nomme l'archive, pas le répertoire temporaire).
|
|
171
154
|
|
|
172
155
|
Une archive dont le contenu **décompressé** dépasse **1 Gio**, ou qui compte plus de
|
|
173
156
|
**100 000 entrées** (fichiers et répertoires), est refusée (code `2`), avant toute
|
|
174
157
|
extraction : les bornes valent pour chaque archive séparément, et ne concernent pas une
|
|
175
|
-
source répertoire, qui n'est pas copiée. `--no-size-limit`
|
|
176
|
-
explicitement — en connaissance de cause, le répertoire temporaire devant alors pouvoir
|
|
177
|
-
accueillir l'archive entière.
|
|
158
|
+
source répertoire, qui n'est pas copiée. `--no-size-limit` lève les deux limites.
|
|
178
159
|
|
|
179
160
|
## Utiliser la comparaison depuis Python
|
|
180
161
|
|
|
181
162
|
Le pipeline se décompose en trois étapes indépendantes : **analyse**, **formatage**,
|
|
182
|
-
**écriture**. Le formatage retourne une chaîne
|
|
183
|
-
elle peut partir directement dans une réponse HTTP ou vers le traitement suivant.
|
|
163
|
+
**écriture**. Le formatage retourne une chaîne qui n'est pas directement écrite sur le disque, elle peut donc être utilisée dans une réponse HTTP ou vers un autre traitement.
|
|
184
164
|
|
|
185
165
|
```python
|
|
186
166
|
from pathlib import Path
|
|
@@ -194,13 +174,13 @@ corps = render(summary, ReportFormat.JSON) # formatage -> str
|
|
|
194
174
|
write_report(summary, Path("rapport"), "json") # écriture -> Path (facultatif)
|
|
195
175
|
```
|
|
196
176
|
|
|
197
|
-
| Étape
|
|
198
|
-
|
|
199
|
-
| analyse
|
|
200
|
-
| formatage | `render(summary, "md"\|"json")`
|
|
201
|
-
|
|
|
202
|
-
| écriture
|
|
203
|
-
|
|
|
177
|
+
| Étape | Fonction | Rend |
|
|
178
|
+
| --------- | ----------------------------------------------- | --------------------------------------- |
|
|
179
|
+
| analyse | `run_comparison(source_a, source_b, **options)` | `RunSummary` |
|
|
180
|
+
| formatage | `render(summary, "md"\|"json")` | `str` |
|
|
181
|
+
| | `summary.as_dict()` | `dict` (JSON sans passer par la chaîne) |
|
|
182
|
+
| écriture | `write_report(summary, dossier, format)` | `Path` du fichier écrit |
|
|
183
|
+
| | `write_reports(summary, dossier, formats)` | `list[Path]` |
|
|
204
184
|
|
|
205
185
|
`run_comparison` accepte les mêmes options que le CLI (`pattern`, `recursive`,
|
|
206
186
|
`keep_whitespace`, `exclude_xpaths`, `max_diffs_per_file`), et
|
|
@@ -221,21 +201,14 @@ with as_directory(Path("chaine_a.zip")) as dir_a:
|
|
|
221
201
|
|
|
222
202
|
### Les exceptions
|
|
223
203
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
| Exception | Levée par | Cause |
|
|
227
|
-
|---|---|---|
|
|
228
|
-
| `ExcludeError` | `run_comparison`, `Canonicalizer` | un XPath d'exclusion est invalide |
|
|
229
|
-
| `ArchiveError` | `as_directory` | source introuvable, archive illisible, chemin d'entrée suspect |
|
|
230
|
-
| `ArchiveSizeError` | `as_directory` | sous-classe d'`ArchiveError` : archive au-delà de `size_limit` ou d'`entry_limit` (attributs `size`, `limit`, et `measure` : `"bytes"` ou `"entries"`) |
|
|
231
|
-
| `CanonicalError` | `Canonicalizer.load` | un document est illisible ou malformé |
|
|
204
|
+
Les exceptions de `xml_cmpr` sont dérivées d'`Exception` :
|
|
232
205
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
`
|
|
237
|
-
|
|
238
|
-
|
|
206
|
+
| Exception | Levée par | Cause |
|
|
207
|
+
| ------------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
208
|
+
| `ExcludeError` | `run_comparison`, `Canonicalizer` | un XPath d'exclusion est invalide |
|
|
209
|
+
| `ArchiveError` | `as_directory` | source introuvable, archive illisible, chemin d'entrée suspect |
|
|
210
|
+
| `ArchiveSizeError` | `as_directory` | sous-classe d'`ArchiveError` : archive au-delà de `size_limit` ou d'`entry_limit` (attributs `size`, `limit`, et `measure` : `"bytes"` ou `"entries"`) |
|
|
211
|
+
| `CanonicalError` | `Canonicalizer.load` | un document est illisible ou malformé |
|
|
239
212
|
|
|
240
213
|
### Exclure des éléments ou attributs de la comparaison
|
|
241
214
|
|
|
@@ -253,10 +226,10 @@ les URI de namespace terminées par `#` restent écrivables.
|
|
|
253
226
|
|
|
254
227
|
Chaque élément ou attribut sélectionné par l'un de ces XPath, dans A comme dans B, est
|
|
255
228
|
retiré de la forme canonique **avant** le calcul de l'empreinte : il ne compte ni pour
|
|
256
|
-
le test d'identité, ni dans le diff. Un élément
|
|
229
|
+
le test d'identité, ni dans le diff. Un élément trouvé dans 1 XPath exclut tout son sous-arbre.
|
|
257
230
|
|
|
258
231
|
Le XPath est évalué par Saxon directement sur le document source, sans les préfixes de
|
|
259
|
-
namespace déclarés dans le document
|
|
232
|
+
namespace déclarés dans le document. Pour un nom
|
|
260
233
|
sans espace de noms, écrire `//DateGeneration` ; pour un nom avec espace de noms,
|
|
261
234
|
utiliser la notation `Q{uri}local`, par ex. `//Q{urn:facture}DateGeneration`.
|
|
262
235
|
|
|
@@ -276,25 +249,7 @@ différences :
|
|
|
276
249
|
|
|
277
250
|
Ils ne sont **jamais** comparés, et aucune option ne les réintègre. Ni ceux placés dans
|
|
278
251
|
un élément, ni ceux du prologue — la bannière de génération qu'une chaîne écrit avant
|
|
279
|
-
l'élément racine, la PI `<?xml-stylesheet?>` qui désigne une feuille de style.
|
|
280
|
-
documents qui ne diffèrent que par là sont `identique`.
|
|
281
|
-
|
|
282
|
-
C'est un choix de périmètre, pas une limite d'implémentation : l'outil compare ce que
|
|
283
|
-
les documents *disent* — éléments, attributs, texte — et un commentaire ne dit rien du
|
|
284
|
-
contenu. Deux chaînes de production qui datent et signent différemment leurs sorties ne
|
|
285
|
-
doivent pas produire un rapport plein de différences pour autant. Pour comparer une
|
|
286
|
-
bannière de génération, comparer les fichiers en texte, pas en XML.
|
|
287
|
-
|
|
288
|
-
Sont des différences : l'ordre des éléments frères, la présence ou la valeur d'un
|
|
289
|
-
attribut, le texte d'un élément, la présence d'un élément, et **la position du texte en
|
|
290
|
-
contenu mixte**.
|
|
291
|
-
|
|
292
|
-
Les éléments frères sont appariés d'abord par empreinte de sous-arbre, puis par nom :
|
|
293
|
-
insérer un `<Ligne>` au milieu d'une liste coûte **une** différence, pas un décalage en
|
|
294
|
-
cascade sur tous les suivants. Enfin, deux éléments qui ne diffèrent **que** par leur nom
|
|
295
|
-
(ou leur espace de noms) sont appariés et rapportés en `element_renamed`, à n'importe
|
|
296
|
-
quelle profondeur ; un élément à la fois renommé et modifié reste un `element_removed`
|
|
297
|
-
suivi d'un `element_added`.
|
|
252
|
+
l'élément racine, la PI `<?xml-stylesheet?>` qui désigne une feuille de style.
|
|
298
253
|
|
|
299
254
|
### Le contenu mixte
|
|
300
255
|
|
|
@@ -307,57 +262,13 @@ A : <p>Voir la <ref>note</ref> ci-dessous.</p>
|
|
|
307
262
|
B : <p>Voir la ci-dessous.<ref>note</ref></p>
|
|
308
263
|
```
|
|
309
264
|
|
|
310
|
-
| Chemin
|
|
311
|
-
|
|
312
|
-
| `/p/#text[1]` | texte modifié
|
|
313
|
-
| `/p/#text[2]` | texte absent de B | `ci-dessous.` | —
|
|
265
|
+
| Chemin | Type | A | B |
|
|
266
|
+
| ------------- | ----------------- | ------------- | --------------------- |
|
|
267
|
+
| `/p/#text[1]` | texte modifié | `Voir la` | `Voir la ci-dessous.` |
|
|
268
|
+
| `/p/#text[2]` | texte absent de B | `ci-dessous.` | — |
|
|
314
269
|
|
|
315
|
-
Les deux documents
|
|
316
|
-
qui ne contient que du texte, lui, n'est pas concerné — c'est le cas courant, et son
|
|
317
|
-
texte reste comparé d'un bloc (`text_changed` sur le chemin de l'élément).
|
|
270
|
+
Les deux documents sont considérés comme **différents**.
|
|
318
271
|
|
|
319
|
-
##
|
|
320
|
-
|
|
321
|
-
```sh
|
|
322
|
-
uv run pytest # tests
|
|
323
|
-
uv run ruff check . && uv run ruff format .
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
## Publication
|
|
327
|
-
|
|
328
|
-
`publish.sh` enchaîne les étapes d'une livraison sur PyPI : contrôle du verrou, `ruff`,
|
|
329
|
-
`pytest`, construction des distributions, contrôle des métadonnées (`twine check`),
|
|
330
|
-
vérification que la roue embarque bien `normalize.xsl`, puis téléversement. Il refuse un
|
|
331
|
-
arbre de travail modifié ou une version déjà publiée, et ne pose aucun tag lui-même — il
|
|
332
|
-
rappelle la commande à la fin.
|
|
333
|
-
|
|
334
|
-
```sh
|
|
335
|
-
uv version --bump patch # 0.2.0 -> 0.2.1 (ou minor / major)
|
|
336
|
-
./publish.sh --test # répétition générale sur TestPyPI
|
|
337
|
-
./publish.sh # publication réelle sur PyPI
|
|
338
|
-
```
|
|
272
|
+
## Licence
|
|
339
273
|
|
|
340
|
-
|
|
341
|
-
|---|---|
|
|
342
|
-
| `--test` | publier sur TestPyPI (index `testpypi` déclaré dans `pyproject.toml`) |
|
|
343
|
-
| `--dry-run` | tout exécuter sauf le téléversement |
|
|
344
|
-
| `--skip-checks` | ne pas rejouer `ruff` et `pytest` |
|
|
345
|
-
| `--allow-dirty` | autoriser un arbre de travail modifié |
|
|
346
|
-
|
|
347
|
-
Le jeton d'API s'obtient, par ordre de préférence :
|
|
348
|
-
|
|
349
|
-
1. par une commande, `PUBLISH_TOKEN_CMD='pass show pypi/xml-cmpr' ./publish.sh` — le
|
|
350
|
-
jeton ne transite ni par l'historique du shell ni par l'environnement ;
|
|
351
|
-
2. par saisie au clavier, sans écho, si rien n'est fourni et que le script tourne dans
|
|
352
|
-
un terminal ;
|
|
353
|
-
3. par `UV_PUBLISH_TOKEN`, toléré pour la compatibilité mais déconseillé : exporté dans
|
|
354
|
-
le profil, il est lisible par tous les processus de la session.
|
|
355
|
-
|
|
356
|
-
Quelle que soit sa source, le script retire le jeton de l'environnement dès son
|
|
357
|
-
démarrage et ne le remet qu'à `uv publish` : `pytest`, `uv build` et `twine` — donc les
|
|
358
|
-
dépendances de dev et les hooks de build — ne le voient jamais. Après la première
|
|
359
|
-
publication, remplacer le jeton global du compte par un jeton limité au projet.
|
|
360
|
-
|
|
361
|
-
Le jeton est celui du dépôt visé — un jeton PyPI ne vaut pas sur TestPyPI, et
|
|
362
|
-
réciproquement. Après un `--test`, l'installation se vérifie par
|
|
363
|
-
`uv tool install --index https://test.pypi.org/simple xml-cmpr`.
|
|
274
|
+
Distribué sous licence [MIT](LICENSE) — © 2026 Frédéric Laurent.
|
|
@@ -13,11 +13,11 @@ wheels = [
|
|
|
13
13
|
|
|
14
14
|
[[package]]
|
|
15
15
|
name = "iniconfig"
|
|
16
|
-
version = "2.3.
|
|
16
|
+
version = "2.3.1"
|
|
17
17
|
source = { registry = "https://pypi.org/simple" }
|
|
18
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
18
|
+
sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" }
|
|
19
19
|
wheels = [
|
|
20
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
20
|
+
{ url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" },
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
[[package]]
|
|
@@ -65,27 +65,27 @@ wheels = [
|
|
|
65
65
|
|
|
66
66
|
[[package]]
|
|
67
67
|
name = "ruff"
|
|
68
|
-
version = "0.
|
|
68
|
+
version = "0.17.0"
|
|
69
69
|
source = { registry = "https://pypi.org/simple" }
|
|
70
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
70
|
+
sdist = { url = "https://files.pythonhosted.org/packages/e9/a7/70debb024dfacda67b8e560cc7511f52b34b9348a8cc8c1ec23036dcd51d/ruff-0.17.0.tar.gz", hash = "sha256:5cd03240d8208a557c2a9655a5cb07ebe36aa6bb35065f97d48c1f6adef5a322", size = 5320554, upload-time = "2026-10-09T19:47:29.248Z" }
|
|
71
71
|
wheels = [
|
|
72
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
73
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
74
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
75
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
76
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
77
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
78
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
79
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
80
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
81
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
82
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
83
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
84
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
85
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
86
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
87
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
88
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
72
|
+
{ url = "https://files.pythonhosted.org/packages/bc/f8/ee5ab9da6089eae2a33e6008b01deb1eda19992c1c8e10661e98cee1640f/ruff-0.17.0-py3-none-linux_armv6l.whl", hash = "sha256:0e271826af9a20d18c6cfae8c51e82959167c24859686ddd3eb9a7f0842ce81e", size = 10148907, upload-time = "2026-10-09T19:46:38.695Z" },
|
|
73
|
+
{ url = "https://files.pythonhosted.org/packages/9f/d9/2f81fb5a9d580afbb11b1c8ff915233a11f2a1b27405d7991f183c5e1976/ruff-0.17.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5f0ca4a40f81403689c04f12966e22f44e329ae362072d8f1587b7bda87f603b", size = 10224208, upload-time = "2026-10-09T19:46:41.711Z" },
|
|
74
|
+
{ url = "https://files.pythonhosted.org/packages/a7/20/643f3c8f75594f937b2bf74801241c56a2e2b8e139d24dff8b66b28cdd7f/ruff-0.17.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cbf7149e0927dc3295d5d64679a4765576eef71b00782b2ae969ef82274d6bb9", size = 9273324, upload-time = "2026-10-09T19:46:44.323Z" },
|
|
75
|
+
{ url = "https://files.pythonhosted.org/packages/ec/91/627700b233d367736cb274f1bd0b47d1f2b12f68878192812bd875adadc3/ruff-0.17.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ee90156522998c3037059d8f66885c8adeeaf7643bdce2caceee196ecd23e0", size = 9953190, upload-time = "2026-10-09T19:46:47.155Z" },
|
|
76
|
+
{ url = "https://files.pythonhosted.org/packages/cd/92/91f7b5ed39490f89d6cbf56e1f543c383667a725efa8e2c2dee0f01f5591/ruff-0.17.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d8e4a002a94cd9d0dc48b51dc69d807a172b5b9bf2b668e656424dc5b55ead1", size = 9961746, upload-time = "2026-10-09T19:46:50.098Z" },
|
|
77
|
+
{ url = "https://files.pythonhosted.org/packages/87/c5/7310f9fc63ce11ff6394edbd5e85433dfb0e14c9fbf6ccc97f1538491bc7/ruff-0.17.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0b8a60c06a218c337e1161638d34757f83449243e2db161483ddf948e53ad14", size = 10625483, upload-time = "2026-10-09T19:46:53.379Z" },
|
|
78
|
+
{ url = "https://files.pythonhosted.org/packages/a5/8d/97443f0dca4a03a0bc7629fd396fd494a1cb6666121e38c5075acb217d8f/ruff-0.17.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a330178bdffc4205dbf3bda11d93e059e388fd6546f8cdd304501a9160363c0d", size = 11350129, upload-time = "2026-10-09T19:46:56.486Z" },
|
|
79
|
+
{ url = "https://files.pythonhosted.org/packages/9c/0a/c525efd9777be4b6b012e6969a3012648468e7e6c4b3e5b46af69f46e8eb/ruff-0.17.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7bb08489e234876fa2da67ae3ea938e9a2156da80293e0e4365abd6973d98329", size = 10906223, upload-time = "2026-10-09T19:47:00.263Z" },
|
|
80
|
+
{ url = "https://files.pythonhosted.org/packages/2c/3c/4a01195d93420cad1175bedad13a515dc8a56f95a6e39789b92e582682f5/ruff-0.17.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc73e7c133e82d55b5f15897b2a442d72c0cb4a0c886c46801ce3c247150b60c", size = 10386314, upload-time = "2026-10-09T19:47:03.057Z" },
|
|
81
|
+
{ url = "https://files.pythonhosted.org/packages/c7/72/1a3951665485a921f6375f91e754a1854d5a645d41acc3642668064ff64d/ruff-0.17.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:db4f74c533403ab70fe4007873f6ae0c9f94a8b03158cf48d78788e47cdbe399", size = 10565464, upload-time = "2026-10-09T19:47:05.831Z" },
|
|
82
|
+
{ url = "https://files.pythonhosted.org/packages/90/8c/539b4d8c082f57e18db8ae2be85a460d77861c79dcd5798e32b536a6a06f/ruff-0.17.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3d8cc360e666d1914e47b0777c6906d70cf18891a55532bd0a16844195d70859", size = 10079468, upload-time = "2026-10-09T19:47:08.617Z" },
|
|
83
|
+
{ url = "https://files.pythonhosted.org/packages/e0/b8/84286966db79434e8c26b585b0a0f6897cb3ab1c51a4aa4df10c28488b62/ruff-0.17.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d66de796b726c4801e05fa99a2a8d7a780e107be222486c304ab61765561e866", size = 9958556, upload-time = "2026-10-09T19:47:11.324Z" },
|
|
84
|
+
{ url = "https://files.pythonhosted.org/packages/69/50/27b6eed27b83fcdd5bfa0d52b83231e29094754374698da404d094487ae3/ruff-0.17.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c3f268baf004aea944f040623327119527ea231af15f7fb7890e82cea0679589", size = 10352173, upload-time = "2026-10-09T19:47:14.188Z" },
|
|
85
|
+
{ url = "https://files.pythonhosted.org/packages/2a/fa/955399fd13044cd827862044117d784a59e3196f6cce7424908ac9a7f914/ruff-0.17.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:864b6c1acb6b0bccf94b5a3938a1531fd09aaca5e5659a2e7bf0f3cf2a685540", size = 10791761, upload-time = "2026-10-09T19:47:16.931Z" },
|
|
86
|
+
{ url = "https://files.pythonhosted.org/packages/ae/bf/024e01e1f6aec87768696725e648ed5b438941341eea8f8100beb681961f/ruff-0.17.0-py3-none-win32.whl", hash = "sha256:5e50aa5b84decd9fe5b0bb0e6f71c3b592f1767ed09faa4b7207d933961e35cd", size = 9904112, upload-time = "2026-10-09T19:47:19.75Z" },
|
|
87
|
+
{ url = "https://files.pythonhosted.org/packages/cc/77/1ee73df41dcc8d1cdb686ee4bc46ea29704ea175feb6b95c78420f631ab8/ruff-0.17.0-py3-none-win_amd64.whl", hash = "sha256:8ab76bcda86dfd28e13776cb5de3c7bcdcf1ae3d37ed761113d1a5a415dc134c", size = 10115585, upload-time = "2026-10-09T19:47:22.698Z" },
|
|
88
|
+
{ url = "https://files.pythonhosted.org/packages/fd/71/eb4f0ccc844aece56963e8578df9c95d4c00f580547d52329d4035d3af18/ruff-0.17.0-py3-none-win_arm64.whl", hash = "sha256:c154c73ff43f9854395e24cac507af13078962e53d2b511605058d22af1fdb88", size = 9872035, upload-time = "2026-10-09T19:47:26.306Z" },
|
|
89
89
|
]
|
|
90
90
|
|
|
91
91
|
[[package]]
|
|
@@ -112,7 +112,7 @@ wheels = [
|
|
|
112
112
|
|
|
113
113
|
[[package]]
|
|
114
114
|
name = "xml-cmpr"
|
|
115
|
-
version = "0.2.
|
|
115
|
+
version = "0.2.1"
|
|
116
116
|
source = { editable = "." }
|
|
117
117
|
dependencies = [
|
|
118
118
|
{ name = "saxonche" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|