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.
- urban_json_random-0.1.0/LICENSE +21 -0
- urban_json_random-0.1.0/PKG-INFO +84 -0
- urban_json_random-0.1.0/README.md +55 -0
- urban_json_random-0.1.0/pyproject.toml +62 -0
- urban_json_random-0.1.0/setup.cfg +4 -0
- urban_json_random-0.1.0/src/urban_json_random/__init__.py +310 -0
- urban_json_random-0.1.0/src/urban_json_random/_naughty.py +66 -0
- urban_json_random-0.1.0/src/urban_json_random/py.typed +0 -0
- urban_json_random-0.1.0/src/urban_json_random.egg-info/PKG-INFO +84 -0
- urban_json_random-0.1.0/src/urban_json_random.egg-info/SOURCES.txt +13 -0
- urban_json_random-0.1.0/src/urban_json_random.egg-info/dependency_links.txt +1 -0
- urban_json_random-0.1.0/src/urban_json_random.egg-info/entry_points.txt +2 -0
- urban_json_random-0.1.0/src/urban_json_random.egg-info/requires.txt +3 -0
- urban_json_random-0.1.0/src/urban_json_random.egg-info/top_level.txt +1 -0
- urban_json_random-0.1.0/tests/test_random.py +240 -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -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
|