urban-json-random 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.
@@ -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,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: urban-json-random
3
+ Version: 0.1.0
4
+ Summary: Plugin urban-json : génération de données aléatoires (fonctions de template getrandom_*).
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,random,fake-data,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 :: Software Development :: Testing
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
+ Requires-Dist: faker>=20
27
+ Requires-Dist: rstr>=3.2
28
+ Dynamic: license-file
29
+
30
+ # urban-json-random
31
+
32
+ Plugin [urban-json](https://github.com/gaelgael5/Jslt) : **données aléatoires** dans
33
+ les templates, via les 25 fonctions `getrandom_*` du moteur .NET d'origine (port de
34
+ `Services.randoms.cs`). Utile pour produire des jeux de test réalistes.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install urban-json-random # tire urban-json, Faker et rstr
40
+ ```
41
+
42
+ Rien d'autre à faire : `urban-json` découvre le plugin automatiquement via les entry
43
+ points (`urban_json.functions`).
44
+
45
+ ## Utilisation
46
+
47
+ ```python
48
+ from urban_json import JsltTemplate
49
+
50
+ tpl = JsltTemplate.parse("""{
51
+ "id": getrandom_integer(1, false, 1000, false),
52
+ "client": getrandom_fullname(),
53
+ "email": getrandom_email(),
54
+ "iban": getrandom_iban("FR"),
55
+ "ref": getrandom_string(0, 0, "^[A-Z]{3}-\\\\d{4}$")
56
+ }""")
57
+ tpl.transform({})
58
+ # -> {'id': 417, 'client': 'Jane Doe', 'email': '…', 'iban': 'FR76…', 'ref': 'QZT-0381'}
59
+ ```
60
+
61
+ ## Fonctions
62
+
63
+ | Famille | Fonctions |
64
+ |---|---|
65
+ | Nombres | `getrandom_integer`, `getrandom_long`, `getrandom_short`, `getrandom_byte`, `getrandom_double`, `getrandom_float` — `(minimum, exclusiveMinimum, maximum, exclusiveMaximum)` |
66
+ | Texte | `getrandom_string(min_length, max_length, pattern)`, `getrandom_lipsum(paragraphs)`, `getrandom_password(token, length)`, `getrandom_naughtystrings()` |
67
+ | Personnes | `getrandom_firstname`, `getrandom_lastname`, `getrandom_fullname`, `getrandom_email` |
68
+ | Lieux | `getrandom_city`, `getrandom_country` |
69
+ | Réseau | `getrandom_ipv4`, `getrandom_ipv6`, `getrandom_macaddress(minimum, maximum)` |
70
+ | Banque | `getrandom_iban(pays, include_iban, include_bban)` |
71
+ | Temps | `getrandom_datatime()` (nom d'origine conservé), `getrandom_timespan()` |
72
+ | Divers | `getrandom_boolean()`, `getrandom_binary(min_length, max_length)`, `getrandom_in_list(items)` |
73
+
74
+ ## Écarts volontaires avec le .NET
75
+
76
+ - `exclusiveMinimum` / `exclusiveMaximum` excluent la borne (sens JSON Schema) ; le
77
+ .NET élargissait au contraire l'intervalle.
78
+ - Sans bornes : étendue du type (`integer` 0 à 2³¹−1, `double` dans [0, 1)…) ; le .NET
79
+ générait souvent toujours 0.
80
+ - Intervalle impossible : erreur `JsltError` explicite.
81
+ - `getrandom_boolean` et `getrandom_in_list` font un vrai tirage (le .NET prenait la
82
+ parité des millisecondes) ; liste vide → `null`.
83
+ - `getrandom_password` utilise un générateur cryptographique (`secrets`).
84
+ - Dates et durées en texte : ISO 8601 et `hh:mm:ss.fff`.
@@ -0,0 +1,55 @@
1
+ # urban-json-random
2
+
3
+ Plugin [urban-json](https://github.com/gaelgael5/Jslt) : **données aléatoires** dans
4
+ les templates, via les 25 fonctions `getrandom_*` du moteur .NET d'origine (port de
5
+ `Services.randoms.cs`). Utile pour produire des jeux de test réalistes.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install urban-json-random # tire urban-json, Faker et rstr
11
+ ```
12
+
13
+ Rien d'autre à faire : `urban-json` découvre le plugin automatiquement via les entry
14
+ points (`urban_json.functions`).
15
+
16
+ ## Utilisation
17
+
18
+ ```python
19
+ from urban_json import JsltTemplate
20
+
21
+ tpl = JsltTemplate.parse("""{
22
+ "id": getrandom_integer(1, false, 1000, false),
23
+ "client": getrandom_fullname(),
24
+ "email": getrandom_email(),
25
+ "iban": getrandom_iban("FR"),
26
+ "ref": getrandom_string(0, 0, "^[A-Z]{3}-\\\\d{4}$")
27
+ }""")
28
+ tpl.transform({})
29
+ # -> {'id': 417, 'client': 'Jane Doe', 'email': '…', 'iban': 'FR76…', 'ref': 'QZT-0381'}
30
+ ```
31
+
32
+ ## Fonctions
33
+
34
+ | Famille | Fonctions |
35
+ |---|---|
36
+ | Nombres | `getrandom_integer`, `getrandom_long`, `getrandom_short`, `getrandom_byte`, `getrandom_double`, `getrandom_float` — `(minimum, exclusiveMinimum, maximum, exclusiveMaximum)` |
37
+ | Texte | `getrandom_string(min_length, max_length, pattern)`, `getrandom_lipsum(paragraphs)`, `getrandom_password(token, length)`, `getrandom_naughtystrings()` |
38
+ | Personnes | `getrandom_firstname`, `getrandom_lastname`, `getrandom_fullname`, `getrandom_email` |
39
+ | Lieux | `getrandom_city`, `getrandom_country` |
40
+ | Réseau | `getrandom_ipv4`, `getrandom_ipv6`, `getrandom_macaddress(minimum, maximum)` |
41
+ | Banque | `getrandom_iban(pays, include_iban, include_bban)` |
42
+ | Temps | `getrandom_datatime()` (nom d'origine conservé), `getrandom_timespan()` |
43
+ | Divers | `getrandom_boolean()`, `getrandom_binary(min_length, max_length)`, `getrandom_in_list(items)` |
44
+
45
+ ## Écarts volontaires avec le .NET
46
+
47
+ - `exclusiveMinimum` / `exclusiveMaximum` excluent la borne (sens JSON Schema) ; le
48
+ .NET élargissait au contraire l'intervalle.
49
+ - Sans bornes : étendue du type (`integer` 0 à 2³¹−1, `double` dans [0, 1)…) ; le .NET
50
+ générait souvent toujours 0.
51
+ - Intervalle impossible : erreur `JsltError` explicite.
52
+ - `getrandom_boolean` et `getrandom_in_list` font un vrai tirage (le .NET prenait la
53
+ parité des millisecondes) ; liste vide → `null`.
54
+ - `getrandom_password` utilise un générateur cryptographique (`secrets`).
55
+ - Dates et durées en texte : ISO 8601 et `hh:mm:ss.fff`.
@@ -0,0 +1,62 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "urban-json-random"
7
+ dynamic = ["version"]
8
+ description = "Plugin urban-json : génération de données aléatoires (fonctions de template getrandom_*)."
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", "random", "fake-data", "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 :: Software Development :: Testing",
27
+ "Typing :: Typed",
28
+ ]
29
+ dependencies = [
30
+ "urban-json",
31
+ # Noms, villes, pays, e-mails, IBAN, adresses réseau, lorem ipsum (rôle tenu
32
+ # par RandomDataGenerator côté .NET). Borne basse large : Faker récent exige
33
+ # Python >= 3.10, pip choisit la version compatible avec chaque interpréteur.
34
+ "faker>=20",
35
+ # Chaîne aléatoire conforme à un motif regex (getrandom_string avec pattern).
36
+ "rstr>=3.2",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/gaelgael5/Jslt"
41
+ Repository = "https://github.com/gaelgael5/Jslt"
42
+
43
+ # Découverte automatique par urban-json : installer ce paquet suffit à exposer les getrandom_*.
44
+ [project.entry-points."urban_json.functions"]
45
+ random = "urban_json_random:register"
46
+
47
+ [tool.setuptools.dynamic]
48
+ version = { attr = "urban_json_random.__version__" }
49
+
50
+ [tool.setuptools.packages.find]
51
+ where = ["src"]
52
+
53
+ [tool.setuptools.package-data]
54
+ urban_json_random = ["py.typed"]
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+ pythonpath = ["src"]
59
+
60
+ [tool.ruff]
61
+ line-length = 100
62
+ src = ["src", "tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,310 @@
1
+ """Plugin urban-json : génération de données aléatoires (``getrandom_*``).
2
+
3
+ Port des 25 fonctions de ``Services.randoms.cs`` (.NET), qui s'appuyaient sur
4
+ ``RandomDataGenerator`` ; ici, Faker (noms, lieux, e-mails, IBAN, réseau,
5
+ lorem ipsum) et rstr (chaîne conforme à un motif regex). Les noms d'origine sont
6
+ conservés, y compris ``getrandom_datatime`` (faute de frappe du .NET), pour que
7
+ les templates existants fonctionnent tels quels.
8
+
9
+ Découvert automatiquement via l'entry point ``urban_json.functions`` ; aussi
10
+ enregistrable à la main : ``from urban_json_random import register``.
11
+
12
+ Écarts volontaires vs .NET (comportement voulu, pas les bugs) :
13
+
14
+ * ``exclusiveMinimum`` / ``exclusiveMaximum`` suivent JSON Schema (borne
15
+ exclue). Le .NET faisait ``min--`` / ``max++`` : il *élargissait* l'intervalle.
16
+ * Bornes absentes : étendue du type (``integer`` 0..2³¹−1, ``double`` [0, 1)…).
17
+ Le .NET générait souvent ``[0, 0]``, donc toujours 0.
18
+ * Intervalle impossible (min > max) : ``JsltError`` au lieu d'une exception .NET.
19
+ * ``getrandom_boolean`` / ``getrandom_in_list`` : vrai tirage aléatoire (le .NET
20
+ prenait la parité des millisecondes de l'horloge) ; liste vide -> ``null``.
21
+ * ``getrandom_password`` : générateur cryptographique (``secrets``), même
22
+ alphabet que le .NET.
23
+ * Dates et durées rendues en texte (le portage n'a pas de type date) :
24
+ ``getrandom_datatime`` en ISO 8601, ``getrandom_timespan`` en ``hh:mm:ss.fff``.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import base64
30
+ import random
31
+ import secrets
32
+ import string
33
+ from datetime import datetime, timedelta
34
+ from functools import cache
35
+ from typing import Any
36
+
37
+ from urban_json import JsltError
38
+
39
+ from ._naughty import NAUGHTY_STRINGS
40
+
41
+ __version__ = "0.1.0"
42
+
43
+ __all__ = ["NAUGHTY_STRINGS", "register"]
44
+
45
+ _RNG = random.SystemRandom()
46
+
47
+ # Étendue par défaut de chaque type entier (bornes .NET des types d'origine).
48
+ _INT_RANGES = {
49
+ "getrandom_byte": (0, 255),
50
+ "getrandom_short": (-(2**15), 2**15 - 1),
51
+ "getrandom_integer": (-(2**31), 2**31 - 1),
52
+ "getrandom_long": (-(2**63), 2**63 - 1),
53
+ }
54
+
55
+
56
+ @cache
57
+ def _faker(locale: str = "en_US") -> Any:
58
+ from faker import Faker # import différé : Faker charge beaucoup de données
59
+
60
+ return Faker(locale)
61
+
62
+
63
+ def _flag(value: Any) -> bool:
64
+ return value is True or (isinstance(value, str) and value.strip().lower() == "true")
65
+
66
+
67
+ def _int_bounds(name: str, minimum: Any, exclusive_min: Any, maximum: Any, exclusive_max: Any) -> tuple[int, int]:
68
+ type_low, type_high = _INT_RANGES[name]
69
+ low = 0 if minimum is None else int(minimum)
70
+ high = type_high if maximum is None else int(maximum)
71
+ if _flag(exclusive_min):
72
+ low += 1
73
+ if _flag(exclusive_max):
74
+ high -= 1
75
+ if not (type_low <= low <= type_high and type_low <= high <= type_high):
76
+ raise JsltError(f"{name} : bornes hors de l'étendue du type [{type_low}, {type_high}]")
77
+ if low > high:
78
+ raise JsltError(f"{name} : intervalle vide (minimum {low} > maximum {high})")
79
+ return low, high
80
+
81
+
82
+ def _make_int(name: str):
83
+ def generate(ctx: Any, minimum: Any = None, exclusive_minimum: Any = None,
84
+ maximum: Any = None, exclusive_maximum: Any = None) -> int:
85
+ low, high = _int_bounds(name, minimum, exclusive_minimum, maximum, exclusive_maximum)
86
+ return _RNG.randint(low, high)
87
+
88
+ return generate
89
+
90
+
91
+ def _make_real(name: str, single: bool):
92
+ def generate(ctx: Any, minimum: Any = None, exclusive_minimum: Any = None,
93
+ maximum: Any = None, exclusive_maximum: Any = None) -> float:
94
+ low = 0.0 if minimum is None else float(minimum)
95
+ high = 1.0 if maximum is None else float(maximum)
96
+ excl_low, excl_high = _flag(exclusive_minimum), _flag(exclusive_maximum)
97
+ if low > high or (low == high and (excl_low or excl_high)):
98
+ raise JsltError(f"{name} : intervalle vide (minimum {low}, maximum {high})")
99
+ while True:
100
+ value = _RNG.uniform(low, high)
101
+ if single:
102
+ value = float(f"{value:.7g}") # précision d'un float .NET (simple)
103
+ if (excl_low and value <= low) or (excl_high and value >= high):
104
+ continue
105
+ if low <= value <= high:
106
+ return value
107
+
108
+ return generate
109
+
110
+
111
+ def getrandom_string(ctx: Any, min_length: Any = None, max_length: Any = None, pattern: Any = None) -> str:
112
+ if pattern:
113
+ import re
114
+
115
+ import rstr
116
+
117
+ try:
118
+ re.compile(pattern)
119
+ except re.error as exc:
120
+ raise JsltError(f"getrandom_string : motif regex invalide {pattern!r} : {exc}") from exc
121
+ # Les ancres ^ $ délimitent déjà la chaîne produite : on les retire avant de générer.
122
+ body = pattern.removeprefix("^")
123
+ if body.endswith("$") and not body.endswith("\\$"):
124
+ body = body[:-1]
125
+ return rstr.xeger(body)
126
+ low = 1 if not min_length else int(min_length)
127
+ high = low + 5 if not max_length else int(max_length)
128
+ if low > high:
129
+ raise JsltError(f"getrandom_string : intervalle de longueur vide ({low} > {high})")
130
+ alphabet = string.ascii_letters + string.digits + " "
131
+ length = _RNG.randint(low, high)
132
+ # Pas d'espace en tête ni en fin : le texte reste lisible et sa longueur exacte.
133
+ inner = "".join(_RNG.choice(alphabet) for _ in range(max(0, length - 2)))
134
+ edge = string.ascii_letters + string.digits
135
+ if length == 1:
136
+ return _RNG.choice(edge)
137
+ return _RNG.choice(edge) + inner + _RNG.choice(edge)
138
+
139
+
140
+ def getrandom_datatime(ctx: Any) -> str:
141
+ # Sans fuseau, comme les dates « Unspecified » du .NET et `now()` du cœur.
142
+ start = datetime(1970, 1, 1) # noqa: DTZ001
143
+ span = (datetime(2050, 12, 31, 23, 59, 59) - start).total_seconds() # noqa: DTZ001
144
+ return (start + timedelta(seconds=_RNG.randint(0, int(span)))).isoformat()
145
+
146
+
147
+ def getrandom_timespan(ctx: Any) -> str:
148
+ total_ms = _RNG.randint(0, 24 * 3600 * 1000 - 1)
149
+ seconds, ms = divmod(total_ms, 1000)
150
+ minutes, sec = divmod(seconds, 60)
151
+ hours, minute = divmod(minutes, 60)
152
+ return f"{hours:02d}:{minute:02d}:{sec:02d}.{ms:03d}"
153
+
154
+
155
+ def getrandom_email(ctx: Any) -> str:
156
+ return _faker().email()
157
+
158
+
159
+ def getrandom_ipv4(ctx: Any) -> str:
160
+ return _faker().ipv4()
161
+
162
+
163
+ def getrandom_ipv6(ctx: Any) -> str:
164
+ return _faker().ipv6().upper()
165
+
166
+
167
+ def getrandom_binary(ctx: Any, min_length: Any = None, max_length: Any = None) -> str:
168
+ low = 1 if min_length is None else int(min_length)
169
+ high = low + 5 if max_length is None else int(max_length)
170
+ if low > high or low < 0:
171
+ raise JsltError(f"getrandom_binary : intervalle de longueur invalide ({low}, {high})")
172
+ return base64.b64encode(secrets.token_bytes(_RNG.randint(low, high))).decode("ascii")
173
+
174
+
175
+ def getrandom_boolean(ctx: Any) -> bool:
176
+ return _RNG.random() < 0.5
177
+
178
+
179
+ def getrandom_city(ctx: Any) -> str:
180
+ return _faker().city()
181
+
182
+
183
+ def getrandom_country(ctx: Any) -> str:
184
+ return _faker().country()
185
+
186
+
187
+ def getrandom_lastname(ctx: Any) -> str:
188
+ return _faker().last_name()
189
+
190
+
191
+ def getrandom_firstname(ctx: Any) -> str:
192
+ return _faker().first_name()
193
+
194
+
195
+ def getrandom_fullname(ctx: Any) -> str:
196
+ fake = _faker()
197
+ return f"{fake.first_name()} {fake.last_name()}"
198
+
199
+
200
+ # Pays ISO 3166-1 alpha-2 -> locale Faker qui sait produire son IBAN.
201
+ _IBAN_LOCALES = {
202
+ "AT": "de_AT", "BE": "nl_BE", "CH": "de_CH", "CZ": "cs_CZ", "DE": "de_DE", "DK": "da_DK",
203
+ "ES": "es_ES", "FI": "fi_FI", "FR": "fr_FR", "GB": "en_GB", "GR": "el_GR", "IE": "en_IE",
204
+ "IT": "it_IT", "NL": "nl_NL", "NO": "no_NO", "PL": "pl_PL", "PT": "pt_PT", "SE": "sv_SE",
205
+ }
206
+
207
+
208
+ def getrandom_iban(ctx: Any, country_code_iso2: Any = None, include_iban: Any = None, include_bban: Any = None) -> str:
209
+ country = (str(country_code_iso2).strip().upper() if country_code_iso2 else "GB")
210
+ locale = _IBAN_LOCALES.get(country)
211
+ if locale is None:
212
+ raise JsltError(
213
+ f"getrandom_iban : pays non pris en charge {country_code_iso2!r} "
214
+ f"(codes : {', '.join(sorted(_IBAN_LOCALES))})"
215
+ )
216
+ fake = _faker(locale)
217
+ if _flag(include_bban):
218
+ # .NET : BBAN seul si l'IBAN est explicitement exclu, sinon l'un ou l'autre.
219
+ if include_iban is not None and not _flag(include_iban):
220
+ return fake.bban()
221
+ return fake.iban() if _RNG.random() < 0.5 else fake.bban()
222
+ return fake.iban()
223
+
224
+
225
+ def _mac_int(text: Any, default: int) -> int:
226
+ if text is None or text == "":
227
+ return default
228
+ digits = str(text).replace(":", "").replace("-", "")
229
+ try:
230
+ value = int(digits, 16)
231
+ except ValueError as exc:
232
+ raise JsltError(f"getrandom_macaddress : adresse MAC invalide {text!r}") from exc
233
+ if len(digits) != 12:
234
+ raise JsltError(f"getrandom_macaddress : adresse MAC invalide {text!r}")
235
+ return value
236
+
237
+
238
+ def getrandom_macaddress(ctx: Any, minimum: Any = None, maximum: Any = None) -> str:
239
+ low, high = _mac_int(minimum, 0), _mac_int(maximum, 2**48 - 1)
240
+ if low > high:
241
+ raise JsltError("getrandom_macaddress : intervalle vide (minimum > maximum)")
242
+ raw = f"{_RNG.randint(low, high):012X}"
243
+ return ":".join(raw[i:i + 2] for i in range(0, 12, 2))
244
+
245
+
246
+ def getrandom_lipsum(ctx: Any, paragraphs: Any = None) -> str:
247
+ count = max(1, int(paragraphs or 0))
248
+ return "\n".join(_faker().paragraphs(nb=count))
249
+
250
+
251
+ def getrandom_naughtystrings(ctx: Any) -> str:
252
+ return _RNG.choice(NAUGHTY_STRINGS)
253
+
254
+
255
+ def getrandom_in_list(ctx: Any, items: Any = None) -> Any:
256
+ if not isinstance(items, list) or not items:
257
+ return None
258
+ return _RNG.choice(items)
259
+
260
+
261
+ # Alphabet du .NET (Services.randoms.cs), conservé pour des mots de passe comparables.
262
+ _PASSWORD_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890&é'(-è_çà)=$ù%*µ,?;:/.?'"
263
+
264
+
265
+ def getrandom_password(ctx: Any, token: Any = None, length: Any = None) -> str:
266
+ # `token` est ignoré, comme en .NET : il est gardé pour la compatibilité des templates.
267
+ size = int(length or 0) or 12
268
+ return "".join(secrets.choice(_PASSWORD_ALPHABET) for _ in range(size))
269
+
270
+
271
+ _RANGE = "(minimum, exclusiveMinimum, maximum, exclusiveMaximum ; bornes exclues si les drapeaux valent true)"
272
+
273
+ _FUNCTIONS: list[tuple[str, Any, str]] = [
274
+ ("getrandom_integer", _make_int("getrandom_integer"), f"Entier aléatoire 32 bits {_RANGE}"),
275
+ ("getrandom_long", _make_int("getrandom_long"), f"Entier aléatoire 64 bits {_RANGE}"),
276
+ ("getrandom_short", _make_int("getrandom_short"), f"Entier aléatoire 16 bits {_RANGE}"),
277
+ ("getrandom_byte", _make_int("getrandom_byte"), f"Entier aléatoire de 0 à 255 {_RANGE}"),
278
+ ("getrandom_double", _make_real("getrandom_double", single=False), f"Décimal aléatoire, [0, 1) par défaut {_RANGE}"),
279
+ ("getrandom_float", _make_real("getrandom_float", single=True), f"Décimal aléatoire en simple précision {_RANGE}"),
280
+ ("getrandom_string", getrandom_string,
281
+ "Texte aléatoire (lettres, chiffres, espaces) de min_length à max_length caractères, ou conforme au motif regex"),
282
+ ("getrandom_datatime", getrandom_datatime, "Date et heure aléatoires entre 1970 et 2050, en ISO 8601"),
283
+ ("getrandom_timespan", getrandom_timespan, "Durée aléatoire de moins d'un jour, au format hh:mm:ss.fff"),
284
+ ("getrandom_email", getrandom_email, "Adresse e-mail aléatoire"),
285
+ ("getrandom_ipv4", getrandom_ipv4, "Adresse IPv4 aléatoire"),
286
+ ("getrandom_ipv6", getrandom_ipv6, "Adresse IPv6 aléatoire, en majuscules"),
287
+ ("getrandom_binary", getrandom_binary, "Octets aléatoires de min_length à max_length, encodés en Base64"),
288
+ ("getrandom_boolean", getrandom_boolean, "Booléen aléatoire, true ou false"),
289
+ ("getrandom_city", getrandom_city, "Nom de ville aléatoire"),
290
+ ("getrandom_country", getrandom_country, "Nom de pays aléatoire"),
291
+ ("getrandom_lastname", getrandom_lastname, "Nom de famille aléatoire"),
292
+ ("getrandom_firstname", getrandom_firstname, "Prénom aléatoire"),
293
+ ("getrandom_fullname", getrandom_fullname, "Prénom et nom aléatoires"),
294
+ ("getrandom_iban", getrandom_iban,
295
+ "IBAN valide aléatoire pour un pays ISO (FR, DE, GB…) ; BBAN si include_bban vaut true et include_iban false"),
296
+ ("getrandom_macaddress", getrandom_macaddress,
297
+ "Adresse MAC aléatoire (AA:BB:…), éventuellement entre deux adresses minimum et maximum"),
298
+ ("getrandom_lipsum", getrandom_lipsum, "Texte lorem ipsum de n paragraphes (1 par défaut)"),
299
+ ("getrandom_naughtystrings", getrandom_naughtystrings,
300
+ "Chaîne piégeuse pour les tests (Unicode, injection, formats limites)"),
301
+ ("getrandom_in_list", getrandom_in_list, "Élément tiré au hasard dans un tableau (null s'il est vide)"),
302
+ ("getrandom_password", getrandom_password,
303
+ "Mot de passe aléatoire de length caractères (12 par défaut) ; le 1er argument est ignoré"),
304
+ ]
305
+
306
+
307
+ def register(registry: Any) -> None:
308
+ """Enregistre les 25 fonctions ``getrandom_*`` dans un registre jslt."""
309
+ for name, function, description in _FUNCTIONS:
310
+ registry.register(name, function, description=description)
@@ -0,0 +1,66 @@
1
+ """Chaînes piégeuses pour éprouver un traitement de texte (``getrandom_naughtystrings``).
2
+
3
+ Extrait de la « Big List of Naughty Strings » (Max Woolf, licence MIT,
4
+ https://github.com/minimaxir/big-list-of-naughty-strings), la source qu'utilise
5
+ ``RandomDataGenerator`` côté .NET. Une entrée par famille de piège, pas la liste
6
+ complète : l'objectif est la variété, pas l'exhaustivité.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ NAUGHTY_STRINGS: tuple[str, ...] = (
12
+ # vides et réservés
13
+ "",
14
+ " ",
15
+ "undefined",
16
+ "null",
17
+ "NULL",
18
+ "nil",
19
+ "None",
20
+ "true",
21
+ "false",
22
+ "NaN",
23
+ "Infinity",
24
+ "-Infinity",
25
+ "0",
26
+ "-0",
27
+ "0.0",
28
+ "1E+02",
29
+ "0xffffffff",
30
+ "999999999999999999999",
31
+ "1/0",
32
+ # espaces et contrôles
33
+ "\t",
34
+ "\u00a0",
35
+ "\u200b",
36
+ "\ufeff",
37
+ "\u2028",
38
+ # Unicode
39
+ "Ω≈ç√∫˜µ≤≥÷",
40
+ "Œ„´‰ˇÁ¨ˆØ∏”’",
41
+ "田中さんにあげて下さい",
42
+ "社會科學院語學研究所",
43
+ "울란바토르",
44
+ "👾 🙇 💁 🙅 🙆 🙋 🙎 🙍",
45
+ "👨‍👩‍👦",
46
+ "ثم نفس سقطت وبالتحديد",
47
+ "בְּרֵאשִׁית, בָּרָא אֱלֹהִים",
48
+ "Ṱ̺̺̕o͞ ̷i̲̬͇̪͙n̝̗͕v̟̜̘̦͟o̶̙̰̠kè͚̮̺̪̹̱̤ ̖t̝͕̳̣̻̪͞h̼͓̲̦̳̘̲e͇̣̰̦̬͎ ̢̼̻̱̘h͚͎͙̜̣̲ͅi̦̲̣̰̤v̻͍e̺̭̳̪̰-m̢iͅn̖̺̞̲̯̰d̵̼̟͙̩̼̘̳",
49
+ "゚・✿ヾ╲(。◕‿◕。)╱✿・゚",
50
+ # injections et formats
51
+ "<script>alert(123)</script>",
52
+ "<img src=x onerror=alert(1)>",
53
+ "javascript:alert(1)",
54
+ "' OR '1'='1",
55
+ "1; DROP TABLE users",
56
+ "${7*7}",
57
+ "{{7*7}}",
58
+ "%s%s%s%s%s",
59
+ "../../../../../../etc/passwd",
60
+ "C:\\Windows\\System32",
61
+ "$HOME",
62
+ "`ls -la`",
63
+ "\\\\",
64
+ '"',
65
+ "'",
66
+ )
File without changes
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: urban-json-random
3
+ Version: 0.1.0
4
+ Summary: Plugin urban-json : génération de données aléatoires (fonctions de template getrandom_*).
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,random,fake-data,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 :: Software Development :: Testing
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
+ Requires-Dist: faker>=20
27
+ Requires-Dist: rstr>=3.2
28
+ Dynamic: license-file
29
+
30
+ # urban-json-random
31
+
32
+ Plugin [urban-json](https://github.com/gaelgael5/Jslt) : **données aléatoires** dans
33
+ les templates, via les 25 fonctions `getrandom_*` du moteur .NET d'origine (port de
34
+ `Services.randoms.cs`). Utile pour produire des jeux de test réalistes.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install urban-json-random # tire urban-json, Faker et rstr
40
+ ```
41
+
42
+ Rien d'autre à faire : `urban-json` découvre le plugin automatiquement via les entry
43
+ points (`urban_json.functions`).
44
+
45
+ ## Utilisation
46
+
47
+ ```python
48
+ from urban_json import JsltTemplate
49
+
50
+ tpl = JsltTemplate.parse("""{
51
+ "id": getrandom_integer(1, false, 1000, false),
52
+ "client": getrandom_fullname(),
53
+ "email": getrandom_email(),
54
+ "iban": getrandom_iban("FR"),
55
+ "ref": getrandom_string(0, 0, "^[A-Z]{3}-\\\\d{4}$")
56
+ }""")
57
+ tpl.transform({})
58
+ # -> {'id': 417, 'client': 'Jane Doe', 'email': '…', 'iban': 'FR76…', 'ref': 'QZT-0381'}
59
+ ```
60
+
61
+ ## Fonctions
62
+
63
+ | Famille | Fonctions |
64
+ |---|---|
65
+ | Nombres | `getrandom_integer`, `getrandom_long`, `getrandom_short`, `getrandom_byte`, `getrandom_double`, `getrandom_float` — `(minimum, exclusiveMinimum, maximum, exclusiveMaximum)` |
66
+ | Texte | `getrandom_string(min_length, max_length, pattern)`, `getrandom_lipsum(paragraphs)`, `getrandom_password(token, length)`, `getrandom_naughtystrings()` |
67
+ | Personnes | `getrandom_firstname`, `getrandom_lastname`, `getrandom_fullname`, `getrandom_email` |
68
+ | Lieux | `getrandom_city`, `getrandom_country` |
69
+ | Réseau | `getrandom_ipv4`, `getrandom_ipv6`, `getrandom_macaddress(minimum, maximum)` |
70
+ | Banque | `getrandom_iban(pays, include_iban, include_bban)` |
71
+ | Temps | `getrandom_datatime()` (nom d'origine conservé), `getrandom_timespan()` |
72
+ | Divers | `getrandom_boolean()`, `getrandom_binary(min_length, max_length)`, `getrandom_in_list(items)` |
73
+
74
+ ## Écarts volontaires avec le .NET
75
+
76
+ - `exclusiveMinimum` / `exclusiveMaximum` excluent la borne (sens JSON Schema) ; le
77
+ .NET élargissait au contraire l'intervalle.
78
+ - Sans bornes : étendue du type (`integer` 0 à 2³¹−1, `double` dans [0, 1)…) ; le .NET
79
+ générait souvent toujours 0.
80
+ - Intervalle impossible : erreur `JsltError` explicite.
81
+ - `getrandom_boolean` et `getrandom_in_list` font un vrai tirage (le .NET prenait la
82
+ parité des millisecondes) ; liste vide → `null`.
83
+ - `getrandom_password` utilise un générateur cryptographique (`secrets`).
84
+ - Dates et durées en texte : ISO 8601 et `hh:mm:ss.fff`.
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/urban_json_random/__init__.py
5
+ src/urban_json_random/_naughty.py
6
+ src/urban_json_random/py.typed
7
+ src/urban_json_random.egg-info/PKG-INFO
8
+ src/urban_json_random.egg-info/SOURCES.txt
9
+ src/urban_json_random.egg-info/dependency_links.txt
10
+ src/urban_json_random.egg-info/entry_points.txt
11
+ src/urban_json_random.egg-info/requires.txt
12
+ src/urban_json_random.egg-info/top_level.txt
13
+ tests/test_random.py
@@ -0,0 +1,2 @@
1
+ [urban_json.functions]
2
+ random = urban_json_random:register
@@ -0,0 +1,3 @@
1
+ urban-json
2
+ faker>=20
3
+ rstr>=3.2
@@ -0,0 +1 @@
1
+ urban_json_random
@@ -0,0 +1,240 @@
1
+ """Fonctions getrandom_* : on vérifie formats, types et bornes (les valeurs sont
2
+ aléatoires), sur plusieurs tirages pour rendre visibles les bornes fausses."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import base64
7
+ import ipaddress
8
+ import re
9
+ from datetime import datetime
10
+
11
+ import pytest
12
+ from urban_json import FunctionRegistry, JsltError, JsltTemplate
13
+
14
+ from urban_json_random import NAUGHTY_STRINGS, register
15
+
16
+ DRAWS = 200
17
+
18
+
19
+ @pytest.fixture(scope="module")
20
+ def registry() -> FunctionRegistry:
21
+ reg = FunctionRegistry()
22
+ register(reg)
23
+ return reg
24
+
25
+
26
+ def run(registry, expression: str, source=None):
27
+ template = JsltTemplate.parse(f'{{ "r": {expression} }}')
28
+ return template.transform(source or {}, functions=registry)["r"]
29
+
30
+
31
+ def draws(registry, expression: str, source=None, n: int = DRAWS) -> list:
32
+ return [run(registry, expression, source) for _ in range(n)]
33
+
34
+
35
+ # --- nombres -----------------------------------------------------------------
36
+
37
+
38
+ @pytest.mark.parametrize(
39
+ ("expression", "low", "high", "kind"),
40
+ [
41
+ ("getrandom_integer(1, false, 6, false)", 1, 6, int),
42
+ ("getrandom_integer(1, true, 6, true)", 2, 5, int), # bornes exclues (JSON Schema)
43
+ ("getrandom_long(-5, false, 5, false)", -5, 5, int),
44
+ ("getrandom_short(10, false, 12, false)", 10, 12, int),
45
+ ("getrandom_byte(250, false, 255, false)", 250, 255, int),
46
+ ("getrandom_double(0.5, false, 0.75, false)", 0.5, 0.75, float),
47
+ ("getrandom_float(-1, false, 1, false)", -1.0, 1.0, float),
48
+ ],
49
+ )
50
+ def test_nombres_dans_les_bornes(registry, expression, low, high, kind) -> None:
51
+ values = draws(registry, expression)
52
+ assert all(isinstance(v, kind) and not isinstance(v, bool) for v in values)
53
+ assert all(low <= v <= high for v in values)
54
+ if kind is int:
55
+ assert set(values) == set(range(low, high + 1)), "toutes les valeurs de l'intervalle doivent sortir"
56
+
57
+
58
+ def test_double_bornes_exclues(registry) -> None:
59
+ values = draws(registry, "getrandom_double(0, true, 1, true)")
60
+ assert all(0 < v < 1 for v in values)
61
+
62
+
63
+ @pytest.mark.parametrize(
64
+ ("expression", "low", "high"),
65
+ [
66
+ ("getrandom_integer()", 0, 2**31 - 1),
67
+ ("getrandom_long()", 0, 2**63 - 1),
68
+ ("getrandom_short()", 0, 2**15 - 1),
69
+ ("getrandom_byte()", 0, 255),
70
+ ("getrandom_double()", 0.0, 1.0),
71
+ ],
72
+ )
73
+ def test_bornes_par_defaut(registry, expression, low, high) -> None:
74
+ values = draws(registry, expression, n=50)
75
+ assert all(low <= v <= high for v in values)
76
+ assert len(set(values)) > 1, "sans bornes, le résultat ne doit pas être constant"
77
+
78
+
79
+ @pytest.mark.parametrize(
80
+ "expression",
81
+ ["getrandom_integer(10, false, 1, false)", "getrandom_integer(3, true, 4, true)", "getrandom_byte(300)"],
82
+ )
83
+ def test_intervalle_impossible(registry, expression) -> None:
84
+ with pytest.raises(JsltError, match="getrandom"):
85
+ run(registry, expression)
86
+
87
+
88
+ # --- texte ---------------------------------------------------------------------
89
+
90
+
91
+ def test_string_longueur(registry) -> None:
92
+ values = draws(registry, "getrandom_string(3, 8)")
93
+ assert all(isinstance(v, str) and 3 <= len(v) <= 8 for v in values)
94
+ assert all(re.fullmatch(r"[A-Za-z0-9 ]+", v) for v in values)
95
+
96
+
97
+ def test_string_longueur_par_defaut(registry) -> None:
98
+ values = draws(registry, "getrandom_string()")
99
+ assert all(1 <= len(v) <= 6 for v in values) # min 1, max min + 5 (comme le .NET)
100
+
101
+
102
+ def test_string_motif_regex(registry) -> None:
103
+ values = draws(registry, 'getrandom_string(0, 0, "^[A-Z]{3}-\\\\d{4}$")')
104
+ assert all(re.fullmatch(r"[A-Z]{3}-\d{4}", v) for v in values)
105
+
106
+
107
+ def test_string_motif_invalide(registry) -> None:
108
+ with pytest.raises(JsltError, match="motif"):
109
+ run(registry, 'getrandom_string(1, 2, "(")')
110
+
111
+
112
+ def test_lipsum(registry) -> None:
113
+ text = run(registry, "getrandom_lipsum(3)")
114
+ assert len([p for p in text.split("\n") if p.strip()]) == 3
115
+ assert len(run(registry, "getrandom_lipsum(0)").split("\n")) == 1 # 0 -> 1 paragraphe
116
+
117
+
118
+ def test_password(registry) -> None:
119
+ values = draws(registry, 'getrandom_password(null, 20)', n=50)
120
+ assert all(len(v) == 20 for v in values)
121
+ assert len(set(values)) == 50
122
+ assert all(len(v) == 12 for v in draws(registry, "getrandom_password(null, 0)", n=10)) # 0 -> 12
123
+
124
+
125
+ def test_naughtystrings(registry) -> None:
126
+ values = draws(registry, "getrandom_naughtystrings()")
127
+ assert all(v in NAUGHTY_STRINGS for v in values)
128
+ assert len(set(values)) > 5
129
+
130
+
131
+ # --- personnes, lieux, réseau, banque --------------------------------------------
132
+
133
+
134
+ @pytest.mark.parametrize(
135
+ "name", ["getrandom_firstname", "getrandom_lastname", "getrandom_city", "getrandom_country"]
136
+ )
137
+ def test_textes_non_vides(registry, name) -> None:
138
+ values = draws(registry, f"{name}()", n=30)
139
+ assert all(isinstance(v, str) and v.strip() for v in values)
140
+ assert len(set(values)) > 3
141
+
142
+
143
+ def test_fullname(registry) -> None:
144
+ assert all(len(v.split()) >= 2 for v in draws(registry, "getrandom_fullname()", n=30))
145
+
146
+
147
+ def test_email(registry) -> None:
148
+ assert all(re.fullmatch(r"[^@\s]+@[^@\s]+\.[a-z]{2,}", v) for v in draws(registry, "getrandom_email()", n=30))
149
+
150
+
151
+ def test_ip(registry) -> None:
152
+ for v in draws(registry, "getrandom_ipv4()", n=30):
153
+ assert isinstance(ipaddress.ip_address(v), ipaddress.IPv4Address)
154
+ for v in draws(registry, "getrandom_ipv6()", n=30):
155
+ assert isinstance(ipaddress.ip_address(v), ipaddress.IPv6Address)
156
+ assert v == v.upper() # majuscules comme le .NET
157
+
158
+
159
+ def test_macaddress(registry) -> None:
160
+ values = draws(registry, "getrandom_macaddress()")
161
+ assert all(re.fullmatch(r"([0-9A-F]{2}:){5}[0-9A-F]{2}", v) for v in values)
162
+
163
+
164
+ def test_macaddress_bornes(registry) -> None:
165
+ values = draws(registry, 'getrandom_macaddress("00:00:00:00:00:10", "00:00:00:00:00:12")')
166
+ assert set(values) == {"00:00:00:00:00:10", "00:00:00:00:00:11", "00:00:00:00:00:12"}
167
+
168
+
169
+ def _iban_valide(iban: str) -> bool:
170
+ raw = iban.replace(" ", "")
171
+ moved = raw[4:] + raw[:4]
172
+ digits = "".join(str(int(c, 36)) for c in moved)
173
+ return int(digits) % 97 == 1
174
+
175
+
176
+ @pytest.mark.parametrize("country", ["FR", "DE", "GB", "ES", "IT", "NL", "BE"])
177
+ def test_iban_par_pays(registry, country) -> None:
178
+ for v in draws(registry, f'getrandom_iban("{country}")', n=10):
179
+ assert v.startswith(country) and _iban_valide(v)
180
+
181
+
182
+ def test_iban_bban_seul(registry) -> None:
183
+ values = draws(registry, 'getrandom_iban("FR", false, true)', n=10)
184
+ assert all(not v.startswith("FR") and v.isalnum() for v in values)
185
+
186
+
187
+ def test_iban_pays_inconnu(registry) -> None:
188
+ with pytest.raises(JsltError, match="pays"):
189
+ run(registry, 'getrandom_iban("ZZ")')
190
+
191
+
192
+ # --- dates, durées, binaire, booléen, liste -----------------------------------------
193
+
194
+
195
+ def test_datatime(registry) -> None:
196
+ for v in draws(registry, "getrandom_datatime()", n=50):
197
+ moment = datetime.fromisoformat(v)
198
+ assert 1970 <= moment.year <= 2050
199
+
200
+
201
+ def test_timespan(registry) -> None:
202
+ assert all(re.fullmatch(r"\d{2}:\d{2}:\d{2}\.\d{3}", v) for v in draws(registry, "getrandom_timespan()"))
203
+
204
+
205
+ def test_binary(registry) -> None:
206
+ for v in draws(registry, "getrandom_binary(2, 4)"):
207
+ assert 2 <= len(base64.b64decode(v)) <= 4
208
+
209
+
210
+ def test_boolean(registry) -> None:
211
+ assert set(draws(registry, "getrandom_boolean()")) == {True, False}
212
+
213
+
214
+ def test_in_list(registry) -> None:
215
+ values = draws(registry, "getrandom_in_list($.l)", {"l": ["a", 1, {"k": 2}]})
216
+ assert all(v in ["a", 1, {"k": 2}] for v in values)
217
+ assert len({repr(v) for v in values}) == 3
218
+
219
+
220
+ @pytest.mark.parametrize("source", [{"l": []}, {"l": None}, {"l": "abc"}])
221
+ def test_in_list_vide_ou_invalide(registry, source) -> None:
222
+ assert run(registry, "getrandom_in_list($.l)", source) is None
223
+
224
+
225
+ # --- intégration ------------------------------------------------------------------
226
+
227
+
228
+ def test_toutes_les_fonctions_sont_documentees(registry) -> None:
229
+ catalog = registry.catalog()
230
+ assert len(catalog) == 25
231
+ for entry in catalog:
232
+ assert entry["name"].startswith("getrandom_")
233
+ assert len((entry["description"] or "").strip()) >= 15, entry["name"]
234
+
235
+
236
+ def test_decouverte_par_entry_point() -> None:
237
+ from urban_json import default_registry, load_plugins
238
+
239
+ load_plugins()
240
+ assert "getrandom_email" in default_registry