urban-json-csv 0.1.0__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.
- urban_json_csv-0.1.0/LICENSE +21 -0
- urban_json_csv-0.1.0/PKG-INFO +118 -0
- urban_json_csv-0.1.0/README.md +91 -0
- urban_json_csv-0.1.0/pyproject.toml +56 -0
- urban_json_csv-0.1.0/setup.cfg +4 -0
- urban_json_csv-0.1.0/src/urban_json_csv/__init__.py +202 -0
- urban_json_csv-0.1.0/src/urban_json_csv/mcsv.py +187 -0
- urban_json_csv-0.1.0/src/urban_json_csv/py.typed +0 -0
- urban_json_csv-0.1.0/src/urban_json_csv.egg-info/PKG-INFO +118 -0
- urban_json_csv-0.1.0/src/urban_json_csv.egg-info/SOURCES.txt +14 -0
- urban_json_csv-0.1.0/src/urban_json_csv.egg-info/dependency_links.txt +1 -0
- urban_json_csv-0.1.0/src/urban_json_csv.egg-info/entry_points.txt +2 -0
- urban_json_csv-0.1.0/src/urban_json_csv.egg-info/requires.txt +1 -0
- urban_json_csv-0.1.0/src/urban_json_csv.egg-info/top_level.txt +1 -0
- urban_json_csv-0.1.0/tests/test_loadcsv.py +96 -0
- urban_json_csv-0.1.0/tests/test_loadmcsv.py +101 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 black beard
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: urban-json-csv
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Plugin urban-json : lecture de fichiers CSV (fonction de template loadcsv).
|
|
5
|
+
Author-email: Gael Beard <gaelgael5@gmail.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/gaelgael5/Jslt
|
|
8
|
+
Project-URL: Repository, https://github.com/gaelgael5/Jslt
|
|
9
|
+
Keywords: json,transform,template,jslt,csv,plugin
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: urban-json
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# urban-json-csv
|
|
29
|
+
|
|
30
|
+
Plugin [urban-json](https://github.com/gaelgael5/Jslt) : lecture de fichiers **CSV**
|
|
31
|
+
dans les templates, via la fonction `loadcsv`. Port de
|
|
32
|
+
`Bb.CustomServices.ServiceCsv` (.NET).
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install urban-json-csv # tire urban-json en dépendance
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Rien d'autre à faire : `urban-json` découvre le plugin automatiquement via les entry
|
|
41
|
+
points (`urban_json.functions`). `loadcsv` devient utilisable dans les templates.
|
|
42
|
+
|
|
43
|
+
## Utilisation
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from urban_json import JsltTemplate
|
|
47
|
+
|
|
48
|
+
# people.csv :
|
|
49
|
+
# name;age;active
|
|
50
|
+
# Alice;30;true
|
|
51
|
+
# Bob;25;false
|
|
52
|
+
tpl = JsltTemplate.parse('[ { "$source": loadcsv("people.csv"), "n": $.name, "a": $.age } ]')
|
|
53
|
+
tpl.transform({})
|
|
54
|
+
# -> [{'n': 'Alice', 'a': 30}, {'n': 'Bob', 'a': 25}]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`loadcsv` renvoie un tableau d'objets (une entrée par ligne) ; il s'enchaîne donc
|
|
58
|
+
naturellement avec une boucle `$source`, un filtre `?(...)`, etc.
|
|
59
|
+
|
|
60
|
+
## Signature
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
loadcsv(source_path, has_header=true, separator=";", quote="\"", escape="\\",
|
|
64
|
+
excluded_columns=null, exclude_null_and_empty=false)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
| Paramètre | Rôle |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `source_path` | chemin du fichier CSV (absolu ou relatif au répertoire courant) |
|
|
70
|
+
| `has_header` | la 1re ligne porte les noms de colonnes (sinon `Column0`, `Column1`…) |
|
|
71
|
+
| `separator` | séparateur de champs (défaut `;`) |
|
|
72
|
+
| `quote` | caractère de citation (défaut `"`) |
|
|
73
|
+
| `escape` | caractère d'échappement (défaut `\`) |
|
|
74
|
+
| `excluded_columns` | noms de colonnes à exclure, séparés par `separator` |
|
|
75
|
+
| `exclude_null_and_empty` | omet les cellules nulles/vides |
|
|
76
|
+
|
|
77
|
+
### Typage des valeurs
|
|
78
|
+
|
|
79
|
+
Les cellules sont coercées comme en .NET : `true`/`false` → booléen,
|
|
80
|
+
entiers/décimaux → nombres, le reste → chaîne (les entêtes sont assainis :
|
|
81
|
+
accents → ASCII, espaces → `_`).
|
|
82
|
+
|
|
83
|
+
## `loadmcsv` — CSV multi-enregistrements hiérarchique
|
|
84
|
+
|
|
85
|
+
Port de `ServicesMultiCsv` : un fichier `;`-délimité où chaque ligne est préfixée
|
|
86
|
+
par un **type d'enregistrement**, les colonnes de chaque type étant déclarées par
|
|
87
|
+
des lignes `#DEF`. Un `rule_payload` décrit la **hiérarchie**.
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
#DEF;CATALOG;id;name
|
|
91
|
+
#DEF;BOOK;code;title
|
|
92
|
+
CATALOG;1;Librairie
|
|
93
|
+
BOOK;a;Alpha
|
|
94
|
+
BOOK;b;Beta
|
|
95
|
+
#END
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from urban_json import JsltTemplate
|
|
100
|
+
JsltTemplate.parse(
|
|
101
|
+
'[ { "$source": selectmany(loadmcsv("catalog.mcsv", "CATALOG:BOOK"), $.BOOK.*), "t": $.title } ]'
|
|
102
|
+
).transform({})
|
|
103
|
+
# -> [{'t': 'Alpha'}, {'t': 'Beta'}]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`loadmcsv(source_path, rule_payload, encoding, ignore_empty)` :
|
|
107
|
+
- `rule_payload` : règles `"PARENT:ENF1|ENF2; …"` (séparées par `;`). Un bloc dont
|
|
108
|
+
le nom est enfant du parent courant s'y imbrique, sinon il remonte.
|
|
109
|
+
- Les enfants homonymes deviennent un **tableau** ; chaque racine porte un objet
|
|
110
|
+
`_metadatas` (nom de fichier, encodage, métadonnées `#clé` du fichier).
|
|
111
|
+
- Lignes `#DEF;<schema>:<NOM>;col…` (colonnes), `#clé;…` (métadonnée), `#END` (fin).
|
|
112
|
+
|
|
113
|
+
Écarts assumés vs .NET : lecture `utf-8` ; mappage des colonnes par position
|
|
114
|
+
tolérant au `;` final (le .NET l'exige) ; noms de types comparés en majuscules.
|
|
115
|
+
|
|
116
|
+
## Licence
|
|
117
|
+
|
|
118
|
+
Apache-2.0.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# urban-json-csv
|
|
2
|
+
|
|
3
|
+
Plugin [urban-json](https://github.com/gaelgael5/Jslt) : lecture de fichiers **CSV**
|
|
4
|
+
dans les templates, via la fonction `loadcsv`. Port de
|
|
5
|
+
`Bb.CustomServices.ServiceCsv` (.NET).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install urban-json-csv # tire urban-json en dépendance
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Rien d'autre à faire : `urban-json` découvre le plugin automatiquement via les entry
|
|
14
|
+
points (`urban_json.functions`). `loadcsv` devient utilisable dans les templates.
|
|
15
|
+
|
|
16
|
+
## Utilisation
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from urban_json import JsltTemplate
|
|
20
|
+
|
|
21
|
+
# people.csv :
|
|
22
|
+
# name;age;active
|
|
23
|
+
# Alice;30;true
|
|
24
|
+
# Bob;25;false
|
|
25
|
+
tpl = JsltTemplate.parse('[ { "$source": loadcsv("people.csv"), "n": $.name, "a": $.age } ]')
|
|
26
|
+
tpl.transform({})
|
|
27
|
+
# -> [{'n': 'Alice', 'a': 30}, {'n': 'Bob', 'a': 25}]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`loadcsv` renvoie un tableau d'objets (une entrée par ligne) ; il s'enchaîne donc
|
|
31
|
+
naturellement avec une boucle `$source`, un filtre `?(...)`, etc.
|
|
32
|
+
|
|
33
|
+
## Signature
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
loadcsv(source_path, has_header=true, separator=";", quote="\"", escape="\\",
|
|
37
|
+
excluded_columns=null, exclude_null_and_empty=false)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| Paramètre | Rôle |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `source_path` | chemin du fichier CSV (absolu ou relatif au répertoire courant) |
|
|
43
|
+
| `has_header` | la 1re ligne porte les noms de colonnes (sinon `Column0`, `Column1`…) |
|
|
44
|
+
| `separator` | séparateur de champs (défaut `;`) |
|
|
45
|
+
| `quote` | caractère de citation (défaut `"`) |
|
|
46
|
+
| `escape` | caractère d'échappement (défaut `\`) |
|
|
47
|
+
| `excluded_columns` | noms de colonnes à exclure, séparés par `separator` |
|
|
48
|
+
| `exclude_null_and_empty` | omet les cellules nulles/vides |
|
|
49
|
+
|
|
50
|
+
### Typage des valeurs
|
|
51
|
+
|
|
52
|
+
Les cellules sont coercées comme en .NET : `true`/`false` → booléen,
|
|
53
|
+
entiers/décimaux → nombres, le reste → chaîne (les entêtes sont assainis :
|
|
54
|
+
accents → ASCII, espaces → `_`).
|
|
55
|
+
|
|
56
|
+
## `loadmcsv` — CSV multi-enregistrements hiérarchique
|
|
57
|
+
|
|
58
|
+
Port de `ServicesMultiCsv` : un fichier `;`-délimité où chaque ligne est préfixée
|
|
59
|
+
par un **type d'enregistrement**, les colonnes de chaque type étant déclarées par
|
|
60
|
+
des lignes `#DEF`. Un `rule_payload` décrit la **hiérarchie**.
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
#DEF;CATALOG;id;name
|
|
64
|
+
#DEF;BOOK;code;title
|
|
65
|
+
CATALOG;1;Librairie
|
|
66
|
+
BOOK;a;Alpha
|
|
67
|
+
BOOK;b;Beta
|
|
68
|
+
#END
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from urban_json import JsltTemplate
|
|
73
|
+
JsltTemplate.parse(
|
|
74
|
+
'[ { "$source": selectmany(loadmcsv("catalog.mcsv", "CATALOG:BOOK"), $.BOOK.*), "t": $.title } ]'
|
|
75
|
+
).transform({})
|
|
76
|
+
# -> [{'t': 'Alpha'}, {'t': 'Beta'}]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`loadmcsv(source_path, rule_payload, encoding, ignore_empty)` :
|
|
80
|
+
- `rule_payload` : règles `"PARENT:ENF1|ENF2; …"` (séparées par `;`). Un bloc dont
|
|
81
|
+
le nom est enfant du parent courant s'y imbrique, sinon il remonte.
|
|
82
|
+
- Les enfants homonymes deviennent un **tableau** ; chaque racine porte un objet
|
|
83
|
+
`_metadatas` (nom de fichier, encodage, métadonnées `#clé` du fichier).
|
|
84
|
+
- Lignes `#DEF;<schema>:<NOM>;col…` (colonnes), `#clé;…` (métadonnée), `#END` (fin).
|
|
85
|
+
|
|
86
|
+
Écarts assumés vs .NET : lecture `utf-8` ; mappage des colonnes par position
|
|
87
|
+
tolérant au `;` final (le .NET l'exige) ; noms de types comparés en majuscules.
|
|
88
|
+
|
|
89
|
+
## Licence
|
|
90
|
+
|
|
91
|
+
Apache-2.0.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "urban-json-csv"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Plugin urban-json : lecture de fichiers CSV (fonction de template loadcsv)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Gael Beard", email = "gaelgael5@gmail.com" }]
|
|
14
|
+
keywords = ["json", "transform", "template", "jslt", "csv", "plugin"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
"Topic :: Text Processing :: Markup",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"urban-json",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/gaelgael5/Jslt"
|
|
35
|
+
Repository = "https://github.com/gaelgael5/Jslt"
|
|
36
|
+
|
|
37
|
+
# Découverte automatique par urban-json : installer ce paquet suffit à exposer `loadcsv`.
|
|
38
|
+
[project.entry-points."urban_json.functions"]
|
|
39
|
+
csv = "urban_json_csv:register"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.dynamic]
|
|
42
|
+
version = { attr = "urban_json_csv.__version__" }
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.package-data]
|
|
48
|
+
urban_json_csv = ["py.typed"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|
|
52
|
+
pythonpath = ["src"]
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
line-length = 100
|
|
56
|
+
src = ["src", "tests"]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Plugin jslt : lecture de fichiers CSV (``loadcsv``, ``loadmcsv``).
|
|
2
|
+
|
|
3
|
+
Port de ``Bb.CustomServices.ServiceCsv.ExecuteLoadCsvSource`` du .NET. Fournit la
|
|
4
|
+
fonction de template ``loadcsv`` qui lit un fichier CSV et renvoie un tableau
|
|
5
|
+
d'objets JSON (une entrée par ligne), et ``loadmcsv`` (CSV multi-enregistrements
|
|
6
|
+
hiérarchique, port de ``ServicesMultiCsv`` — voir :mod:`urban_json_csv.mcsv`).
|
|
7
|
+
|
|
8
|
+
Ce paquet est découvert automatiquement par ``jslt`` via le groupe d'entry points
|
|
9
|
+
``urban_json.functions`` (il suffit de l'installer). Il peut aussi être enregistré
|
|
10
|
+
explicitement : ``from urban_json_csv import register; register(mon_registre)``.
|
|
11
|
+
|
|
12
|
+
Écarts volontaires vs .NET (documentés) :
|
|
13
|
+
|
|
14
|
+
* La résolution de chemin du .NET (``ctx.Configuration.ResolveFile``) n'existe pas
|
|
15
|
+
dans le portage cœur : ``source_path`` est ouvert tel quel (absolu ou relatif au
|
|
16
|
+
répertoire courant). Le BOM éventuel est retiré (``utf-8-sig``).
|
|
17
|
+
* Les types ``decimal``/``long`` du .NET deviennent ``float``/``int`` (types JSON
|
|
18
|
+
Python). La détection de type (bool / entier / décimal / chaîne) est fidèle.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import csv as _csv
|
|
24
|
+
import unicodedata
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from .mcsv import execute_loadmcsv
|
|
28
|
+
|
|
29
|
+
__version__ = "0.1.0"
|
|
30
|
+
|
|
31
|
+
__all__ = ["execute_loadcsv", "execute_loadmcsv", "register"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _sanitize_header(name: str) -> str:
|
|
35
|
+
"""Assainit un nom de colonne (port de ``ServiceCsv.GetLabel``).
|
|
36
|
+
|
|
37
|
+
Même intention que le .NET (alphanumérique conservé, accents -> ASCII,
|
|
38
|
+
espaces/quotes -> ``_``, underscores multiples réduits), mais via la
|
|
39
|
+
normalisation Unicode (NFKD) plutôt qu'une table figée : tous les accents sont
|
|
40
|
+
traités uniformément (le .NET avait une table incomplète, p.ex. ``Â`` manquant).
|
|
41
|
+
"""
|
|
42
|
+
decomposed = unicodedata.normalize("NFKD", name)
|
|
43
|
+
out: list[str] = []
|
|
44
|
+
for char in decomposed:
|
|
45
|
+
if unicodedata.combining(char):
|
|
46
|
+
continue # supprime les diacritiques (accents)
|
|
47
|
+
if char.isascii() and char.isalnum():
|
|
48
|
+
out.append(char)
|
|
49
|
+
elif char in (" ", "'", '"'):
|
|
50
|
+
out.append("_")
|
|
51
|
+
# tout autre caractère est supprimé
|
|
52
|
+
label = "".join(out)
|
|
53
|
+
while "___" in label:
|
|
54
|
+
label = label.replace("___", "_")
|
|
55
|
+
return label.replace("__", "_")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
#: Sentinelle interne : la cellule doit être omise de l'objet résultat.
|
|
59
|
+
_DROP = object()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _coerce(raw: Any, exclude_null_and_empty: bool) -> Any:
|
|
63
|
+
"""Coerce une cellule CSV en valeur JSON (port de ``ServiceCsv.GetValue``).
|
|
64
|
+
|
|
65
|
+
Retourne la sentinelle :data:`_DROP` si la valeur doit être omise (null/vide
|
|
66
|
+
avec ``exclude_null_and_empty``).
|
|
67
|
+
"""
|
|
68
|
+
if raw is None:
|
|
69
|
+
return _DROP if exclude_null_and_empty else None
|
|
70
|
+
|
|
71
|
+
text = raw.strip()
|
|
72
|
+
low = text.lower()
|
|
73
|
+
|
|
74
|
+
if low == "":
|
|
75
|
+
return _DROP if exclude_null_and_empty else ""
|
|
76
|
+
|
|
77
|
+
if low == "true":
|
|
78
|
+
return True
|
|
79
|
+
if low == "false":
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
# Détection numérique : commence par '-' ou un chiffre (fidèle au .NET).
|
|
83
|
+
first = low[0]
|
|
84
|
+
if first == "-" or first.isdigit():
|
|
85
|
+
is_int = False
|
|
86
|
+
is_decimal = False
|
|
87
|
+
for index, char in enumerate(low):
|
|
88
|
+
if index == 0 and char == "-":
|
|
89
|
+
continue
|
|
90
|
+
if char.isdigit():
|
|
91
|
+
is_int = True
|
|
92
|
+
elif is_int and char == ".":
|
|
93
|
+
is_decimal = True
|
|
94
|
+
else:
|
|
95
|
+
break
|
|
96
|
+
if is_decimal:
|
|
97
|
+
try:
|
|
98
|
+
return float(text)
|
|
99
|
+
except ValueError:
|
|
100
|
+
pass
|
|
101
|
+
elif is_int:
|
|
102
|
+
try:
|
|
103
|
+
value = int(text)
|
|
104
|
+
except ValueError:
|
|
105
|
+
value = None
|
|
106
|
+
# Garde-fou .NET (l.ToString() == v) : on ne coerce pas "007".
|
|
107
|
+
if value is not None and str(value) == text:
|
|
108
|
+
return value
|
|
109
|
+
|
|
110
|
+
# Chaîne : valeur d'origine (déjà rognée), casse conservée.
|
|
111
|
+
return text
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def execute_loadcsv(
|
|
115
|
+
ctx: Any,
|
|
116
|
+
source_path: str | None = None,
|
|
117
|
+
has_header: bool = True,
|
|
118
|
+
separator: str = ";",
|
|
119
|
+
quote: str = '"',
|
|
120
|
+
escape: str = "\\",
|
|
121
|
+
excluded_columns: str | None = None,
|
|
122
|
+
exclude_null_and_empty: bool = False,
|
|
123
|
+
) -> list:
|
|
124
|
+
"""Lit un fichier CSV et renvoie un tableau d'objets (port de ``loadcsv``).
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
source_path: chemin du fichier CSV (absolu ou relatif au répertoire courant).
|
|
128
|
+
has_header: la 1re ligne porte les noms de colonnes (sinon ``Column0``…).
|
|
129
|
+
separator: séparateur de champs (défaut ``;``).
|
|
130
|
+
quote: caractère de citation (défaut ``"``).
|
|
131
|
+
escape: caractère d'échappement (défaut ``\\``).
|
|
132
|
+
excluded_columns: noms de colonnes à exclure, séparés par ``separator``.
|
|
133
|
+
exclude_null_and_empty: omet les cellules nulles/vides (et les lignes qui
|
|
134
|
+
n'auraient plus aucune valeur).
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
La liste des lignes, chacune sous forme de dict colonne -> valeur typée.
|
|
138
|
+
"""
|
|
139
|
+
if not source_path:
|
|
140
|
+
raise ValueError("loadcsv: source_path est requis")
|
|
141
|
+
|
|
142
|
+
delimiter = separator[0] if separator else ";"
|
|
143
|
+
quotechar = quote[0] if quote else '"'
|
|
144
|
+
escapechar = escape[0] if escape else None
|
|
145
|
+
|
|
146
|
+
excluded: set[str] = set()
|
|
147
|
+
if excluded_columns:
|
|
148
|
+
excluded = {c for c in excluded_columns.split(delimiter) if c}
|
|
149
|
+
|
|
150
|
+
with open(source_path, newline="", encoding="utf-8-sig") as handle:
|
|
151
|
+
reader = _csv.reader(
|
|
152
|
+
handle, delimiter=delimiter, quotechar=quotechar, escapechar=escapechar
|
|
153
|
+
)
|
|
154
|
+
rows = list(reader)
|
|
155
|
+
|
|
156
|
+
if not rows:
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
if has_header:
|
|
160
|
+
raw_headers = rows[0]
|
|
161
|
+
data_rows = rows[1:]
|
|
162
|
+
else:
|
|
163
|
+
raw_headers = [f"Column{i}" for i in range(len(rows[0]))]
|
|
164
|
+
data_rows = rows
|
|
165
|
+
|
|
166
|
+
# (nom_brut, nom_assaini) par colonne ; l'exclusion compare le nom brut (.NET).
|
|
167
|
+
columns = [
|
|
168
|
+
(raw, raw if not has_header else _sanitize_header(raw)) for raw in raw_headers
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
result: list[dict] = []
|
|
172
|
+
for row in data_rows:
|
|
173
|
+
obj: dict[str, Any] = {}
|
|
174
|
+
for index, (raw_name, label) in enumerate(columns):
|
|
175
|
+
if raw_name in excluded:
|
|
176
|
+
continue
|
|
177
|
+
cell = row[index] if index < len(row) else None
|
|
178
|
+
value = _coerce(cell, exclude_null_and_empty)
|
|
179
|
+
if value is _DROP:
|
|
180
|
+
continue
|
|
181
|
+
obj[label] = value
|
|
182
|
+
if obj or not exclude_null_and_empty:
|
|
183
|
+
result.append(obj)
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def register(registry: Any) -> None:
|
|
188
|
+
"""Enregistre les fonctions du plugin dans un registre jslt.
|
|
189
|
+
|
|
190
|
+
Point d'entrée déclaré via l'entry point ``urban_json.functions`` (découverte
|
|
191
|
+
automatique) et utilisable manuellement.
|
|
192
|
+
"""
|
|
193
|
+
registry.register(
|
|
194
|
+
"loadcsv",
|
|
195
|
+
execute_loadcsv,
|
|
196
|
+
description="Lit un fichier CSV et renvoie un tableau d'objets.",
|
|
197
|
+
)
|
|
198
|
+
registry.register(
|
|
199
|
+
"loadmcsv",
|
|
200
|
+
execute_loadmcsv,
|
|
201
|
+
description="Lit un CSV multi-enregistrements hiérarchique et le convertit en arbre JSON.",
|
|
202
|
+
)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Lecture de CSV multi-enregistrements (``loadmcsv``).
|
|
2
|
+
|
|
3
|
+
Port de ``Bb.CustomServices.ServicesMultiCsv.LoadMultiCsv`` + la bibliothèque
|
|
4
|
+
``Bb.MultiCsv`` (lexer, règles d'indentation, blocs, visiteur JSON).
|
|
5
|
+
|
|
6
|
+
Format de fichier (``;``-délimité) :
|
|
7
|
+
|
|
8
|
+
* ``#DEF;<schema>:<NOM>;col1;col2;…`` — définit les colonnes du type ``NOM`` ;
|
|
9
|
+
* ``#END`` — fin du fichier ;
|
|
10
|
+
* ``#CLE;v1;v2`` — métadonnée ``CLE`` (valeurs jointes par ``;``) ;
|
|
11
|
+
* ligne de données : 1er champ = type d'enregistrement, puis les valeurs des
|
|
12
|
+
colonnes définies par le ``#DEF`` correspondant.
|
|
13
|
+
|
|
14
|
+
Un ``rule_payload`` ``"PARENT:ENF1|ENF2; AUTRE:…"`` (règles séparées par ``;``)
|
|
15
|
+
décrit la **hiérarchie** : un bloc dont le nom est enfant du parent courant s'y
|
|
16
|
+
imbrique, sinon il remonte. Le résultat JSON groupe les enfants homonymes en
|
|
17
|
+
tableaux ; ``loadmcsv`` ajoute un objet ``_metadatas`` (nom de fichier, encodage,
|
|
18
|
+
métadonnées du fichier).
|
|
19
|
+
|
|
20
|
+
Écarts volontaires vs .NET (documentés) :
|
|
21
|
+
|
|
22
|
+
* **Encodage** : lecture en ``utf-8`` (comme le .NET, qui lit en UTF-8 malgré sa
|
|
23
|
+
détection Ude) ; ``_encodingFile``/``_encodingFileBody`` rapportent ``utf-8``.
|
|
24
|
+
* **Dernier champ** : le .NET ignore le dernier champ de chaque ligne (il suppose
|
|
25
|
+
un ``;`` final). Le portage mappe les colonnes par position jusqu'au nombre de
|
|
26
|
+
colonnes déclarées — tolérant au ``;`` final comme à son absence.
|
|
27
|
+
* Noms de types et de règles comparés en **majuscules** (le .NET met les libellés
|
|
28
|
+
en majuscules ; on fait de même côté règles pour éviter les surprises de casse).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
__all__ = ["execute_loadmcsv"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _Block:
|
|
40
|
+
"""Un enregistrement : nom de type, champs (colonne→valeur), sous-blocs."""
|
|
41
|
+
|
|
42
|
+
__slots__ = ("fields", "name", "subs")
|
|
43
|
+
|
|
44
|
+
def __init__(self, name: str, fields: dict[str, str]) -> None:
|
|
45
|
+
self.name = name
|
|
46
|
+
self.fields = fields
|
|
47
|
+
self.subs: list[_Block] = []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _parse_rules(payload: str | None) -> dict[str, set[str]]:
|
|
51
|
+
"""Parse le DSL de règles ``"PARENT:ENF1|ENF2; …"`` (port d'``IndentationRules``)."""
|
|
52
|
+
rules: dict[str, set[str]] = {}
|
|
53
|
+
for rule in (payload or "").strip().split(";"):
|
|
54
|
+
rule = rule.strip()
|
|
55
|
+
if not rule or ":" not in rule:
|
|
56
|
+
continue
|
|
57
|
+
key, children = rule.split(":", 1)
|
|
58
|
+
kids = {c.strip().upper() for c in children.split("|") if c.strip()}
|
|
59
|
+
rules.setdefault(key.strip().upper(), set()).update(kids)
|
|
60
|
+
return rules
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _read_blocks(path: str, encoding: str) -> tuple[list[_Block], dict[str, str]]:
|
|
64
|
+
"""Lit le fichier en blocs de données + métadonnées (port du ``Lexer``)."""
|
|
65
|
+
headers: dict[str, list[str]] = {}
|
|
66
|
+
metadata: dict[str, str] = {}
|
|
67
|
+
blocks: list[_Block] = []
|
|
68
|
+
|
|
69
|
+
with open(path, encoding=encoding, newline="") as handle:
|
|
70
|
+
for raw in handle:
|
|
71
|
+
line = raw.rstrip("\r\n")
|
|
72
|
+
if line == "":
|
|
73
|
+
continue
|
|
74
|
+
fields = line.split(";")
|
|
75
|
+
tag = fields[0]
|
|
76
|
+
|
|
77
|
+
if tag.startswith("#"):
|
|
78
|
+
if tag == "#DEF":
|
|
79
|
+
# fields[1] = "<schema>:<NOM>" ; fields[2:] = colonnes
|
|
80
|
+
name = fields[1].split(":")[-1].strip().upper() if len(fields) > 1 else ""
|
|
81
|
+
headers[name] = [c.strip() for c in fields[2:] if c.strip()]
|
|
82
|
+
elif tag == "#END":
|
|
83
|
+
break
|
|
84
|
+
else:
|
|
85
|
+
metadata[tag[1:]] = ";".join(fields[1:]).strip(";")
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
name = tag.strip().upper()
|
|
89
|
+
columns = headers.get(name)
|
|
90
|
+
field_map: dict[str, str] = {}
|
|
91
|
+
if columns:
|
|
92
|
+
for index, column in enumerate(columns):
|
|
93
|
+
field_map[column] = fields[index + 1] if index + 1 < len(fields) else ""
|
|
94
|
+
blocks.append(_Block(name, field_map))
|
|
95
|
+
|
|
96
|
+
return blocks, metadata
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _build_tree(blocks: list[_Block], rules: dict[str, set[str]]) -> list[_Block]:
|
|
100
|
+
"""Construit l'arbre de blocs par pile (port de ``FileReader.Read``)."""
|
|
101
|
+
if not blocks:
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
def embedded(parent: _Block, child: _Block) -> bool:
|
|
105
|
+
return child.name in rules.get(parent.name, ())
|
|
106
|
+
|
|
107
|
+
stack: list[_Block] = [blocks[0]]
|
|
108
|
+
for current in blocks[1:]:
|
|
109
|
+
parent = stack[-1]
|
|
110
|
+
if embedded(parent, current):
|
|
111
|
+
parent.subs.append(current)
|
|
112
|
+
stack.append(current)
|
|
113
|
+
else:
|
|
114
|
+
parent = stack.pop()
|
|
115
|
+
while stack and not embedded(parent, current):
|
|
116
|
+
parent = stack.pop()
|
|
117
|
+
parent.subs.append(current)
|
|
118
|
+
stack.append(parent)
|
|
119
|
+
stack.append(current)
|
|
120
|
+
|
|
121
|
+
items: list[_Block] = []
|
|
122
|
+
while stack:
|
|
123
|
+
current = stack.pop()
|
|
124
|
+
if not stack or all(current is not sub for sub in stack[-1].subs):
|
|
125
|
+
items.append(current)
|
|
126
|
+
return items
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _to_json(block: _Block, seen: set[int], ignore_empty: bool) -> dict[str, Any]:
|
|
130
|
+
"""Convertit un bloc en objet JSON (port de ``JsonVisitor``)."""
|
|
131
|
+
result: dict[str, Any] = {}
|
|
132
|
+
for sub in block.subs:
|
|
133
|
+
array = result.setdefault(sub.name, [])
|
|
134
|
+
if id(sub) not in seen:
|
|
135
|
+
seen.add(id(sub))
|
|
136
|
+
array.append(_to_json(sub, seen, ignore_empty))
|
|
137
|
+
for key, value in block.fields.items():
|
|
138
|
+
if not ignore_empty or value != "":
|
|
139
|
+
result[key] = value
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def execute_loadmcsv(
|
|
144
|
+
ctx: Any,
|
|
145
|
+
source_path: str | None = None,
|
|
146
|
+
rule_payload: str | None = None,
|
|
147
|
+
encoding: str = "utf-8-sig",
|
|
148
|
+
ignore_empty: bool = False,
|
|
149
|
+
) -> Any:
|
|
150
|
+
"""Lit un CSV multi-enregistrements et le convertit en arbre JSON (port ``loadmcsv``).
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
source_path: chemin du fichier.
|
|
154
|
+
rule_payload: règles d'imbrication ``"PARENT:ENF1|ENF2; …"``.
|
|
155
|
+
encoding: encodage de lecture (défaut ``utf-8-sig``, retire le BOM).
|
|
156
|
+
ignore_empty: omet les propriétés vides.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
``None`` si aucun enregistrement, l'objet racine s'il y en a un seul,
|
|
160
|
+
sinon la liste des objets racines. Chaque racine porte ``_metadatas``.
|
|
161
|
+
"""
|
|
162
|
+
if not source_path:
|
|
163
|
+
raise ValueError("loadmcsv: source_path est requis")
|
|
164
|
+
|
|
165
|
+
blocks, metadata = _read_blocks(source_path, encoding)
|
|
166
|
+
rules = _parse_rules(rule_payload)
|
|
167
|
+
items = _build_tree(blocks, rules)
|
|
168
|
+
|
|
169
|
+
meta_obj: dict[str, Any] = {
|
|
170
|
+
"_filename": os.path.abspath(source_path),
|
|
171
|
+
"_encodingFile": "utf-8",
|
|
172
|
+
"_encodingFileBody": "utf-8",
|
|
173
|
+
}
|
|
174
|
+
meta_obj.update(metadata)
|
|
175
|
+
|
|
176
|
+
seen: set[int] = set()
|
|
177
|
+
datas: list[Any] = []
|
|
178
|
+
for block in items:
|
|
179
|
+
obj = _to_json(block, seen, ignore_empty)
|
|
180
|
+
obj["_metadatas"] = meta_obj
|
|
181
|
+
datas.append(obj)
|
|
182
|
+
|
|
183
|
+
if not datas:
|
|
184
|
+
return None
|
|
185
|
+
if len(datas) == 1:
|
|
186
|
+
return datas[0]
|
|
187
|
+
return datas
|
|
File without changes
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: urban-json-csv
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Plugin urban-json : lecture de fichiers CSV (fonction de template loadcsv).
|
|
5
|
+
Author-email: Gael Beard <gaelgael5@gmail.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/gaelgael5/Jslt
|
|
8
|
+
Project-URL: Repository, https://github.com/gaelgael5/Jslt
|
|
9
|
+
Keywords: json,transform,template,jslt,csv,plugin
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: urban-json
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# urban-json-csv
|
|
29
|
+
|
|
30
|
+
Plugin [urban-json](https://github.com/gaelgael5/Jslt) : lecture de fichiers **CSV**
|
|
31
|
+
dans les templates, via la fonction `loadcsv`. Port de
|
|
32
|
+
`Bb.CustomServices.ServiceCsv` (.NET).
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install urban-json-csv # tire urban-json en dépendance
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Rien d'autre à faire : `urban-json` découvre le plugin automatiquement via les entry
|
|
41
|
+
points (`urban_json.functions`). `loadcsv` devient utilisable dans les templates.
|
|
42
|
+
|
|
43
|
+
## Utilisation
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from urban_json import JsltTemplate
|
|
47
|
+
|
|
48
|
+
# people.csv :
|
|
49
|
+
# name;age;active
|
|
50
|
+
# Alice;30;true
|
|
51
|
+
# Bob;25;false
|
|
52
|
+
tpl = JsltTemplate.parse('[ { "$source": loadcsv("people.csv"), "n": $.name, "a": $.age } ]')
|
|
53
|
+
tpl.transform({})
|
|
54
|
+
# -> [{'n': 'Alice', 'a': 30}, {'n': 'Bob', 'a': 25}]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`loadcsv` renvoie un tableau d'objets (une entrée par ligne) ; il s'enchaîne donc
|
|
58
|
+
naturellement avec une boucle `$source`, un filtre `?(...)`, etc.
|
|
59
|
+
|
|
60
|
+
## Signature
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
loadcsv(source_path, has_header=true, separator=";", quote="\"", escape="\\",
|
|
64
|
+
excluded_columns=null, exclude_null_and_empty=false)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
| Paramètre | Rôle |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `source_path` | chemin du fichier CSV (absolu ou relatif au répertoire courant) |
|
|
70
|
+
| `has_header` | la 1re ligne porte les noms de colonnes (sinon `Column0`, `Column1`…) |
|
|
71
|
+
| `separator` | séparateur de champs (défaut `;`) |
|
|
72
|
+
| `quote` | caractère de citation (défaut `"`) |
|
|
73
|
+
| `escape` | caractère d'échappement (défaut `\`) |
|
|
74
|
+
| `excluded_columns` | noms de colonnes à exclure, séparés par `separator` |
|
|
75
|
+
| `exclude_null_and_empty` | omet les cellules nulles/vides |
|
|
76
|
+
|
|
77
|
+
### Typage des valeurs
|
|
78
|
+
|
|
79
|
+
Les cellules sont coercées comme en .NET : `true`/`false` → booléen,
|
|
80
|
+
entiers/décimaux → nombres, le reste → chaîne (les entêtes sont assainis :
|
|
81
|
+
accents → ASCII, espaces → `_`).
|
|
82
|
+
|
|
83
|
+
## `loadmcsv` — CSV multi-enregistrements hiérarchique
|
|
84
|
+
|
|
85
|
+
Port de `ServicesMultiCsv` : un fichier `;`-délimité où chaque ligne est préfixée
|
|
86
|
+
par un **type d'enregistrement**, les colonnes de chaque type étant déclarées par
|
|
87
|
+
des lignes `#DEF`. Un `rule_payload` décrit la **hiérarchie**.
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
#DEF;CATALOG;id;name
|
|
91
|
+
#DEF;BOOK;code;title
|
|
92
|
+
CATALOG;1;Librairie
|
|
93
|
+
BOOK;a;Alpha
|
|
94
|
+
BOOK;b;Beta
|
|
95
|
+
#END
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from urban_json import JsltTemplate
|
|
100
|
+
JsltTemplate.parse(
|
|
101
|
+
'[ { "$source": selectmany(loadmcsv("catalog.mcsv", "CATALOG:BOOK"), $.BOOK.*), "t": $.title } ]'
|
|
102
|
+
).transform({})
|
|
103
|
+
# -> [{'t': 'Alpha'}, {'t': 'Beta'}]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`loadmcsv(source_path, rule_payload, encoding, ignore_empty)` :
|
|
107
|
+
- `rule_payload` : règles `"PARENT:ENF1|ENF2; …"` (séparées par `;`). Un bloc dont
|
|
108
|
+
le nom est enfant du parent courant s'y imbrique, sinon il remonte.
|
|
109
|
+
- Les enfants homonymes deviennent un **tableau** ; chaque racine porte un objet
|
|
110
|
+
`_metadatas` (nom de fichier, encodage, métadonnées `#clé` du fichier).
|
|
111
|
+
- Lignes `#DEF;<schema>:<NOM>;col…` (colonnes), `#clé;…` (métadonnée), `#END` (fin).
|
|
112
|
+
|
|
113
|
+
Écarts assumés vs .NET : lecture `utf-8` ; mappage des colonnes par position
|
|
114
|
+
tolérant au `;` final (le .NET l'exige) ; noms de types comparés en majuscules.
|
|
115
|
+
|
|
116
|
+
## Licence
|
|
117
|
+
|
|
118
|
+
Apache-2.0.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/urban_json_csv/__init__.py
|
|
5
|
+
src/urban_json_csv/mcsv.py
|
|
6
|
+
src/urban_json_csv/py.typed
|
|
7
|
+
src/urban_json_csv.egg-info/PKG-INFO
|
|
8
|
+
src/urban_json_csv.egg-info/SOURCES.txt
|
|
9
|
+
src/urban_json_csv.egg-info/dependency_links.txt
|
|
10
|
+
src/urban_json_csv.egg-info/entry_points.txt
|
|
11
|
+
src/urban_json_csv.egg-info/requires.txt
|
|
12
|
+
src/urban_json_csv.egg-info/top_level.txt
|
|
13
|
+
tests/test_loadcsv.py
|
|
14
|
+
tests/test_loadmcsv.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
urban-json
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
urban_json_csv
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Tests du plugin urban-json-csv (fonction ``loadcsv``).
|
|
2
|
+
|
|
3
|
+
Couvre : lecture + typage des valeurs, séparateur par défaut ``;``, absence
|
|
4
|
+
d'entête, exclusions, assainissement des entêtes, découverte via entry point,
|
|
5
|
+
et usage dans un template (boucle ``$source``).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from urban_json import FunctionRegistry, JsltTemplate
|
|
13
|
+
|
|
14
|
+
from urban_json_csv import execute_loadcsv, register
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _write(tmp_path: Path, name: str, content: str) -> str:
|
|
18
|
+
path = tmp_path / name
|
|
19
|
+
path.write_text(content, encoding="utf-8")
|
|
20
|
+
return str(path)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_loadcsv_basic_typing(tmp_path: Path) -> None:
|
|
24
|
+
csv = "name;age;active\nAlice;30;true\nBob;25;false\n"
|
|
25
|
+
path = _write(tmp_path, "people.csv", csv)
|
|
26
|
+
rows = execute_loadcsv(None, path)
|
|
27
|
+
assert rows == [
|
|
28
|
+
{"name": "Alice", "age": 30, "active": True},
|
|
29
|
+
{"name": "Bob", "age": 25, "active": False},
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_loadcsv_decimal_and_leading_zero(tmp_path: Path) -> None:
|
|
34
|
+
csv = "ref;price\nA;1.50\nB;007\n"
|
|
35
|
+
path = _write(tmp_path, "p.csv", csv)
|
|
36
|
+
rows = execute_loadcsv(None, path)
|
|
37
|
+
assert rows[0]["price"] == 1.5
|
|
38
|
+
# "007" ne doit pas devenir un entier (garde-fou .NET) -> reste une chaîne.
|
|
39
|
+
assert rows[1]["price"] == "007"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_loadcsv_no_header(tmp_path: Path) -> None:
|
|
43
|
+
path = _write(tmp_path, "nh.csv", "Alice;30\nBob;25\n")
|
|
44
|
+
rows = execute_loadcsv(None, path, has_header=False)
|
|
45
|
+
assert rows == [
|
|
46
|
+
{"Column0": "Alice", "Column1": 30},
|
|
47
|
+
{"Column0": "Bob", "Column1": 25},
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_loadcsv_custom_separator(tmp_path: Path) -> None:
|
|
52
|
+
path = _write(tmp_path, "c.csv", "a,b\n1,2\n")
|
|
53
|
+
rows = execute_loadcsv(None, path, separator=",")
|
|
54
|
+
assert rows == [{"a": 1, "b": 2}]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_loadcsv_excluded_columns(tmp_path: Path) -> None:
|
|
58
|
+
path = _write(tmp_path, "e.csv", "name;secret\nAlice;x\n")
|
|
59
|
+
rows = execute_loadcsv(None, path, excluded_columns="secret")
|
|
60
|
+
assert rows == [{"name": "Alice"}]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_loadcsv_exclude_null_and_empty(tmp_path: Path) -> None:
|
|
64
|
+
path = _write(tmp_path, "n.csv", "a;b\n1;\n")
|
|
65
|
+
rows = execute_loadcsv(None, path, exclude_null_and_empty=True)
|
|
66
|
+
assert rows == [{"a": 1}] # cellule b vide -> omise
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_header_sanitized(tmp_path: Path) -> None:
|
|
70
|
+
path = _write(tmp_path, "h.csv", "Prénom Client;Âge\nAlice;30\n")
|
|
71
|
+
rows = execute_loadcsv(None, path)
|
|
72
|
+
assert rows == [{"Prenom_Client": "Alice", "Age": 30}]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_register_into_custom_registry(tmp_path: Path) -> None:
|
|
76
|
+
reg = FunctionRegistry()
|
|
77
|
+
register(reg)
|
|
78
|
+
assert "loadcsv" in reg
|
|
79
|
+
assert reg.describe("loadcsv").description
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_discovered_via_entry_point() -> None:
|
|
83
|
+
# Le plugin est installé : jslt doit le découvrir dans le registre par défaut.
|
|
84
|
+
from urban_json import default_registry, load_plugins
|
|
85
|
+
|
|
86
|
+
load_plugins(default_registry)
|
|
87
|
+
assert "loadcsv" in default_registry
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_loadcsv_in_template(tmp_path: Path) -> None:
|
|
91
|
+
path = _write(tmp_path, "t.csv", "name;age\nAlice;30\nBob;25\n")
|
|
92
|
+
template = f'[ {{ "$source": loadcsv("{path}"), "n": $.name, "a": $.age }} ]'
|
|
93
|
+
assert JsltTemplate.parse(template).transform({}) == [
|
|
94
|
+
{"n": "Alice", "a": 30},
|
|
95
|
+
{"n": "Bob", "a": 25},
|
|
96
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Tests de ``loadmcsv`` (CSV multi-enregistrements hiérarchique).
|
|
2
|
+
|
|
3
|
+
Couvre : #DEF/colonnes, hiérarchie par règles, enfants homonymes -> tableau,
|
|
4
|
+
métadonnées (#clé + _metadatas), fichier plat (sans règle), découverte entry
|
|
5
|
+
point, usage en template.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from urban_json import JsltTemplate
|
|
13
|
+
|
|
14
|
+
from urban_json_csv import execute_loadmcsv
|
|
15
|
+
|
|
16
|
+
CATALOG = (
|
|
17
|
+
"#SOURCE;demo\n"
|
|
18
|
+
"#DEF;CATALOG;id;name\n"
|
|
19
|
+
"#DEF;BOOK;code;title\n"
|
|
20
|
+
"CATALOG;1;Librairie\n"
|
|
21
|
+
"BOOK;a;Alpha\n"
|
|
22
|
+
"BOOK;b;Beta\n"
|
|
23
|
+
"#END\n"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _write(tmp_path: Path, name: str, content: str) -> str:
|
|
28
|
+
path = tmp_path / name
|
|
29
|
+
path.write_text(content, encoding="utf-8")
|
|
30
|
+
return str(path)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _strip_meta(obj):
|
|
34
|
+
"""Retire récursivement les _metadatas (chemin non déterministe) pour comparer."""
|
|
35
|
+
if isinstance(obj, dict):
|
|
36
|
+
return {k: _strip_meta(v) for k, v in obj.items() if k != "_metadatas"}
|
|
37
|
+
if isinstance(obj, list):
|
|
38
|
+
return [_strip_meta(v) for v in obj]
|
|
39
|
+
return obj
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_hierarchy_and_columns(tmp_path: Path) -> None:
|
|
43
|
+
path = _write(tmp_path, "catalog.mcsv", CATALOG)
|
|
44
|
+
result = execute_loadmcsv(None, path, "CATALOG:BOOK")
|
|
45
|
+
assert _strip_meta(result) == {
|
|
46
|
+
"BOOK": [
|
|
47
|
+
{"code": "a", "title": "Alpha"},
|
|
48
|
+
{"code": "b", "title": "Beta"},
|
|
49
|
+
],
|
|
50
|
+
"id": "1",
|
|
51
|
+
"name": "Librairie",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_metadatas_present(tmp_path: Path) -> None:
|
|
56
|
+
path = _write(tmp_path, "catalog.mcsv", CATALOG)
|
|
57
|
+
result = execute_loadmcsv(None, path, "CATALOG:BOOK")
|
|
58
|
+
meta = result["_metadatas"]
|
|
59
|
+
assert meta["SOURCE"] == "demo" # métadonnée #SOURCE
|
|
60
|
+
assert meta["_filename"].endswith("catalog.mcsv")
|
|
61
|
+
assert meta["_encodingFile"] == "utf-8"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_single_child_is_still_array(tmp_path: Path) -> None:
|
|
65
|
+
content = (
|
|
66
|
+
"#DEF;CATALOG;id\n#DEF;BOOK;code\nCATALOG;1\nBOOK;a\n#END\n"
|
|
67
|
+
)
|
|
68
|
+
path = _write(tmp_path, "one.mcsv", content)
|
|
69
|
+
result = execute_loadmcsv(None, path, "CATALOG:BOOK")
|
|
70
|
+
assert result["BOOK"] == [{"code": "a"}] # toujours un tableau
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_trailing_separator_tolerated(tmp_path: Path) -> None:
|
|
74
|
+
# Le .NET suppose un ';' final ; le portage tolère les deux formes.
|
|
75
|
+
content = "#DEF;R;a;b\nR;1;2;\n#END\n"
|
|
76
|
+
path = _write(tmp_path, "t.mcsv", content)
|
|
77
|
+
assert _strip_meta(execute_loadmcsv(None, path, "")) == {"a": "1", "b": "2"}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_flat_without_rule_nests_under_first(tmp_path: Path) -> None:
|
|
81
|
+
# Sans règle, tout s'imbrique sous le premier bloc (fidèle au .NET).
|
|
82
|
+
content = "#DEF;A;x\n#DEF;B;y\nA;1\nB;2\n#END\n"
|
|
83
|
+
path = _write(tmp_path, "flat.mcsv", content)
|
|
84
|
+
result = execute_loadmcsv(None, path, "")
|
|
85
|
+
assert _strip_meta(result) == {"x": "1", "B": [{"y": "2"}]}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_discovered_via_entry_point() -> None:
|
|
89
|
+
from urban_json import default_registry, load_plugins
|
|
90
|
+
|
|
91
|
+
load_plugins(default_registry)
|
|
92
|
+
assert "loadmcsv" in default_registry
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_in_template(tmp_path: Path) -> None:
|
|
96
|
+
path = _write(tmp_path, "catalog.mcsv", CATALOG)
|
|
97
|
+
template = (
|
|
98
|
+
f'[ {{ "$source": selectmany(loadmcsv("{path}", "CATALOG:BOOK"), $.BOOK.*),'
|
|
99
|
+
' "t": $.title } ]'
|
|
100
|
+
)
|
|
101
|
+
assert JsltTemplate.parse(template).transform({}) == [{"t": "Alpha"}, {"t": "Beta"}]
|