xmlgenerator 0.1.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xmlgenerator-0.1.0/xmlgenerator.egg-info → xmlgenerator-0.2.1}/PKG-INFO +2 -1
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.1}/README.md +1 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.1}/setup.py +1 -1
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.1}/xmlgenerator/arguments.py +7 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.1}/xmlgenerator/bootstrap.py +38 -10
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.1}/xmlgenerator/configuration.py +24 -5
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.1}/xmlgenerator/generator.py +46 -21
- xmlgenerator-0.2.1/xmlgenerator/randomization.py +76 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.1}/xmlgenerator/substitution.py +38 -18
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.1}/xmlgenerator/validation.py +6 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.1/xmlgenerator.egg-info}/PKG-INFO +2 -1
- xmlgenerator-0.1.0/xmlgenerator/randomization.py +0 -40
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.1}/LICENSE +0 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.1}/setup.cfg +0 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.1}/xmlgenerator/__init__.py +0 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.1}/xmlgenerator.egg-info/SOURCES.txt +0 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.1}/xmlgenerator.egg-info/dependency_links.txt +0 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.1}/xmlgenerator.egg-info/entry_points.txt +0 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.1}/xmlgenerator.egg-info/requires.txt +0 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.1}/xmlgenerator.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: xmlgenerator
|
3
|
-
Version: 0.1
|
3
|
+
Version: 0.2.1
|
4
4
|
Summary: Generates XML documents from XSD schemas
|
5
5
|
Home-page: https://github.com/lexakimov/xmlgenerator
|
6
6
|
Author: Alexey Akimov
|
@@ -247,6 +247,7 @@ In the `value_override` sections, you can specify either a string value or speci
|
|
247
247
|
| `output_filename` | String described by the `output_filename_template` configuration parameter |
|
248
248
|
| `uuid` | Random UUIDv4 |
|
249
249
|
| `regex("pattern")` | Random string value matching the specified regular expression |
|
250
|
+
| `any('A', "B", C)` | Random value from enumeration |
|
250
251
|
| `number(A, B)` | Random number between A and B |
|
251
252
|
| `date("2010-01-01", "2025-01-01")` | Random date within the specified range |
|
252
253
|
| `last_name` | Last Name |
|
@@ -218,6 +218,7 @@ In the `value_override` sections, you can specify either a string value or speci
|
|
218
218
|
| `output_filename` | String described by the `output_filename_template` configuration parameter |
|
219
219
|
| `uuid` | Random UUIDv4 |
|
220
220
|
| `regex("pattern")` | Random string value matching the specified regular expression |
|
221
|
+
| `any('A', "B", C)` | Random value from enumeration |
|
221
222
|
| `number(A, B)` | Random number between A and B |
|
222
223
|
| `date("2010-01-01", "2025-01-01")` | Random date within the specified range |
|
223
224
|
| `last_name` | Last Name |
|
@@ -1,9 +1,12 @@
|
|
1
|
+
import logging
|
1
2
|
import sys
|
2
3
|
from argparse import ArgumentParser, HelpFormatter
|
3
4
|
from pathlib import Path
|
4
5
|
|
5
6
|
import shtab
|
6
7
|
|
8
|
+
logger = logging.getLogger(__name__)
|
9
|
+
|
7
10
|
|
8
11
|
class MyParser(ArgumentParser):
|
9
12
|
def error(self, message):
|
@@ -99,6 +102,10 @@ def parse_args():
|
|
99
102
|
parser = _get_parser()
|
100
103
|
args = parser.parse_args()
|
101
104
|
|
105
|
+
# setup logger
|
106
|
+
log_level = logging.DEBUG if args.debug else logging.INFO
|
107
|
+
logger.setLevel(log_level)
|
108
|
+
|
102
109
|
if args.config_yaml:
|
103
110
|
config_path = Path(args.config_yaml)
|
104
111
|
if not config_path.exists() or not config_path.is_file():
|
@@ -1,39 +1,55 @@
|
|
1
|
+
import logging
|
2
|
+
|
1
3
|
from lxml import etree
|
4
|
+
from xmlschema import XMLSchema
|
5
|
+
|
6
|
+
import xmlgenerator
|
7
|
+
from xmlgenerator import configuration, validation, randomization, substitution, generator
|
2
8
|
from xmlgenerator.arguments import parse_args
|
3
9
|
from xmlgenerator.configuration import load_config
|
4
10
|
from xmlgenerator.generator import XmlGenerator
|
5
11
|
from xmlgenerator.randomization import Randomizer
|
6
12
|
from xmlgenerator.substitution import Substitutor
|
7
13
|
from xmlgenerator.validation import XmlValidator
|
8
|
-
from xmlschema import XMLSchema
|
9
|
-
|
10
14
|
|
11
15
|
# TODO Generator - обработка стандартных xsd типов
|
12
16
|
# TODO кастомные переменные для локального контекста
|
13
17
|
# TODO валидация по Schematron
|
14
|
-
# TODO debug logging
|
15
18
|
# TODO типизировать
|
16
19
|
# TODO Почистить и перевести комментарии
|
17
20
|
# TODO Дописать тесты
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
+
|
22
|
+
logging.basicConfig(level=logging.WARN, format='%(asctime)s [%(name)-26s] %(levelname)-6s - %(message)s')
|
23
|
+
|
24
|
+
logger = logging.getLogger('xmlgenerator.bootstrap')
|
21
25
|
|
22
26
|
|
23
27
|
def main():
|
28
|
+
try:
|
29
|
+
_main()
|
30
|
+
except KeyboardInterrupt as ex:
|
31
|
+
logger.info('processing interrupted')
|
32
|
+
|
33
|
+
|
34
|
+
def _main():
|
24
35
|
args, xsd_files, output_path = parse_args()
|
36
|
+
_setup_loggers(args)
|
25
37
|
|
26
|
-
|
38
|
+
if output_path:
|
39
|
+
logger.debug('specified output path: %s', output_path.absolute())
|
40
|
+
else:
|
41
|
+
logger.debug('output path is not specified. Generated xml will be written to stdout')
|
27
42
|
|
28
|
-
|
43
|
+
config = load_config(args.config_yaml)
|
29
44
|
|
30
45
|
randomizer = Randomizer(args.seed)
|
31
46
|
substitutor = Substitutor(randomizer)
|
32
47
|
generator = XmlGenerator(randomizer, substitutor)
|
33
48
|
validator = XmlValidator(args.validation, args.fail_fast)
|
34
49
|
|
50
|
+
logger.debug('found %s schemas', len(xsd_files))
|
35
51
|
for xsd_file in xsd_files:
|
36
|
-
|
52
|
+
logger.info('processing schema: %s', xsd_file.name)
|
37
53
|
|
38
54
|
# get configuration override for current schema
|
39
55
|
local_config = config.get_for_file(xsd_file.name)
|
@@ -52,6 +68,7 @@ def main():
|
|
52
68
|
|
53
69
|
# Print out to console
|
54
70
|
if not output_path:
|
71
|
+
logger.debug('print xml document to stdout')
|
55
72
|
print(decoded)
|
56
73
|
|
57
74
|
# Validation (if enabled)
|
@@ -65,9 +82,20 @@ def main():
|
|
65
82
|
output_file = output_path
|
66
83
|
if output_path.is_dir():
|
67
84
|
output_file = output_path / f'{xml_filename}.xml'
|
85
|
+
logger.debug('save xml document as %s', output_file.absolute())
|
68
86
|
with open(output_file, 'wb') as f:
|
69
87
|
f.write(xml_str)
|
70
|
-
|
88
|
+
|
89
|
+
|
90
|
+
def _setup_loggers(args):
|
91
|
+
log_level = logging.DEBUG if args.debug else logging.INFO
|
92
|
+
logger.setLevel(log_level)
|
93
|
+
configuration.logger.setLevel(log_level)
|
94
|
+
validation.logger.setLevel(log_level)
|
95
|
+
xmlgenerator.generator.logger.setLevel(log_level)
|
96
|
+
substitution.logger.setLevel(log_level)
|
97
|
+
randomization.logger.setLevel(log_level)
|
98
|
+
|
71
99
|
|
72
100
|
if __name__ == "__main__":
|
73
101
|
main()
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import dataclasses
|
2
|
+
import logging
|
2
3
|
import re
|
3
4
|
import sys
|
4
5
|
from dataclasses import dataclass, field, Field
|
@@ -6,6 +7,8 @@ from typing import Dict, get_args, get_origin, Any
|
|
6
7
|
|
7
8
|
import yaml
|
8
9
|
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
9
12
|
|
10
13
|
@dataclass
|
11
14
|
class RandomizationConfig:
|
@@ -45,6 +48,7 @@ class Config:
|
|
45
48
|
def get_for_file(self, xsd_name):
|
46
49
|
for pattern, conf in self.specific.items():
|
47
50
|
if re.match(pattern, xsd_name):
|
51
|
+
logger.debug("resolved configration with pattern '%s'", pattern)
|
48
52
|
base_dict = dataclasses.asdict(self.global_)
|
49
53
|
override_dict = dataclasses.asdict(conf, dict_factory=lambda x: {k: v for (k, v) in x if v is not None})
|
50
54
|
updated_dict = _recursive_update(base_dict, override_dict)
|
@@ -52,17 +56,23 @@ class Config:
|
|
52
56
|
local_override = conf.value_override
|
53
57
|
global_override = self.global_.value_override
|
54
58
|
merged_config.value_override = _merge_dicts(local_override, global_override)
|
59
|
+
_log_configration('using specific configuration:', merged_config)
|
55
60
|
return merged_config
|
56
61
|
|
62
|
+
_log_configration('using global configration:', self.global_)
|
57
63
|
return self.global_
|
58
64
|
|
59
65
|
|
60
66
|
def load_config(file_path: str | None) -> "Config":
|
61
67
|
if not file_path:
|
62
|
-
|
68
|
+
config = Config()
|
69
|
+
_log_configration("created default configuration:", config)
|
70
|
+
return config
|
63
71
|
with open(file_path, 'r') as file:
|
64
72
|
config_data: dict[str, str] = yaml.safe_load(file) or {}
|
65
|
-
|
73
|
+
config = _map_to_class(config_data, Config, "")
|
74
|
+
_log_configration(f"configuration loaded from {file_path}:", config)
|
75
|
+
return config
|
66
76
|
|
67
77
|
|
68
78
|
def _map_to_class(data_dict: dict, cls, parent_path: str):
|
@@ -80,7 +90,7 @@ def _map_to_class(data_dict: dict, cls, parent_path: str):
|
|
80
90
|
for yaml_name, value in data_dict.items():
|
81
91
|
class_field_name = yaml_name if yaml_name != "global" else "global_"
|
82
92
|
if class_field_name not in class_fields:
|
83
|
-
|
93
|
+
logger.error('YAML parse error: unexpected property: %s.%s', parent_path, yaml_name)
|
84
94
|
sys.exit(1)
|
85
95
|
|
86
96
|
# Определяем тип поля
|
@@ -90,10 +100,10 @@ def _map_to_class(data_dict: dict, cls, parent_path: str):
|
|
90
100
|
# Проверка на отсутствие обязательных полей
|
91
101
|
missing_fields = required_fields - yaml_items.keys()
|
92
102
|
if missing_fields:
|
93
|
-
|
103
|
+
logger.error('YAML parse error: missing required properties in %s:', parent_path)
|
94
104
|
for missing_field in missing_fields:
|
95
105
|
yaml_field_name = missing_field if missing_field != "global_" else "global"
|
96
|
-
|
106
|
+
logger.error(yaml_field_name)
|
97
107
|
sys.exit(1)
|
98
108
|
|
99
109
|
return cls(**yaml_items)
|
@@ -133,3 +143,12 @@ def _merge_dicts(base_dict, extra_dict):
|
|
133
143
|
if key not in merged_dict:
|
134
144
|
merged_dict[key] = value
|
135
145
|
return merged_dict
|
146
|
+
|
147
|
+
|
148
|
+
def _log_configration(message, config):
|
149
|
+
if logger.isEnabledFor(logging.DEBUG):
|
150
|
+
logger.debug(message)
|
151
|
+
as_dict = dataclasses.asdict(config)
|
152
|
+
dumped = yaml.safe_dump(as_dict, allow_unicode=True, width=float("inf"), sort_keys=False, indent=4)
|
153
|
+
for line in dumped.splitlines():
|
154
|
+
logger.debug('|\t%s', line)
|
@@ -1,7 +1,6 @@
|
|
1
|
+
import logging
|
1
2
|
import re
|
2
|
-
import sys
|
3
3
|
|
4
|
-
import rstr
|
5
4
|
import xmlschema
|
6
5
|
from lxml import etree
|
7
6
|
from xmlschema.validators import XsdComplexType, XsdAtomicRestriction, XsdTotalDigitsFacet, XsdElement, \
|
@@ -12,6 +11,8 @@ from xmlgenerator.configuration import GeneratorConfig
|
|
12
11
|
from xmlgenerator.randomization import Randomizer
|
13
12
|
from xmlgenerator.substitution import Substitutor
|
14
13
|
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
15
16
|
|
16
17
|
class XmlGenerator:
|
17
18
|
def __init__(self, randomizer: Randomizer, substitutor: Substitutor):
|
@@ -28,21 +29,27 @@ class XmlGenerator:
|
|
28
29
|
rnd = self.randomizer.rnd
|
29
30
|
|
30
31
|
xsd_element_type = getattr(xsd_element, 'type', None)
|
32
|
+
logger.debug('fill down element "%s" with type %s', xsd_element.name, type(xsd_element_type).__name__)
|
31
33
|
|
32
34
|
# Add attributes if they are
|
33
35
|
attributes = getattr(xsd_element, 'attributes', dict())
|
34
36
|
if len(attributes) > 0 and xsd_element_type.local_name != 'anyType':
|
37
|
+
logger.debug('add attributes to element %s', xsd_element.name)
|
35
38
|
for attr_name, attr in attributes.items():
|
39
|
+
logger.debug('attribute: %s', attr_name)
|
36
40
|
use = attr.use # optional | required | prohibited
|
37
41
|
if use == 'prohibited':
|
42
|
+
logger.debug('skipped')
|
38
43
|
continue
|
39
44
|
elif use == 'optional':
|
40
45
|
if rnd.random() > local_config.randomization.probability:
|
41
|
-
|
46
|
+
logger.debug('skipped')
|
47
|
+
continue # skip optional attribute
|
42
48
|
|
43
49
|
attr_value = self._generate_value(attr.type, attr_name, local_config)
|
44
50
|
if attr_value is not None:
|
45
51
|
xml_element.set(attr_name, str(attr_value))
|
52
|
+
logger.debug(f'attribute %s set with value %s', attr_name, attr_value)
|
46
53
|
|
47
54
|
# Process child elements --------------------------------------------------------------------------------------
|
48
55
|
if isinstance(xsd_element, XsdElement):
|
@@ -69,7 +76,7 @@ class XmlGenerator:
|
|
69
76
|
group_min_occurs = getattr(xsd_element, 'min_occurs', None)
|
70
77
|
group_max_occurs = getattr(xsd_element, 'max_occurs', None)
|
71
78
|
group_min_occurs = group_min_occurs if group_min_occurs is not None else 0
|
72
|
-
group_max_occurs = group_max_occurs if group_max_occurs is not None else 10
|
79
|
+
group_max_occurs = group_max_occurs if group_max_occurs is not None else 10 # TODO externalize
|
73
80
|
group_occurs = rnd.randint(group_min_occurs, group_max_occurs)
|
74
81
|
|
75
82
|
if model == 'all':
|
@@ -80,7 +87,7 @@ class XmlGenerator:
|
|
80
87
|
element_min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
|
81
88
|
element_max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
|
82
89
|
element_min_occurs = element_min_occurs if element_min_occurs is not None else 0
|
83
|
-
element_max_occurs = element_max_occurs if element_max_occurs is not None else 10
|
90
|
+
element_max_occurs = element_max_occurs if element_max_occurs is not None else 10 # TODO externalize
|
84
91
|
element_occurs = rnd.randint(element_min_occurs, element_max_occurs)
|
85
92
|
|
86
93
|
for _ in range(element_occurs):
|
@@ -96,7 +103,7 @@ class XmlGenerator:
|
|
96
103
|
element_min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
|
97
104
|
element_max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
|
98
105
|
element_min_occurs = element_min_occurs if element_min_occurs is not None else 0
|
99
|
-
element_max_occurs = element_max_occurs if element_max_occurs is not None else 10
|
106
|
+
element_max_occurs = element_max_occurs if element_max_occurs is not None else 10 # TODO externalize
|
100
107
|
element_occurs = rnd.randint(element_min_occurs, element_max_occurs)
|
101
108
|
|
102
109
|
if isinstance(xsd_child_element_type, XsdElement):
|
@@ -123,7 +130,7 @@ class XmlGenerator:
|
|
123
130
|
element_min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
|
124
131
|
element_max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
|
125
132
|
element_min_occurs = element_min_occurs if element_min_occurs is not None else 0
|
126
|
-
element_max_occurs = element_max_occurs if element_max_occurs is not None else 10
|
133
|
+
element_max_occurs = element_max_occurs if element_max_occurs is not None else 10 # TODO externalize
|
127
134
|
element_occurs = rnd.randint(element_min_occurs, element_max_occurs)
|
128
135
|
|
129
136
|
for _ in range(element_occurs):
|
@@ -232,7 +239,6 @@ class XmlGenerator:
|
|
232
239
|
|
233
240
|
raise RuntimeError(f"Can't generate value - unhandled type. Target name: {target_name}")
|
234
241
|
|
235
|
-
|
236
242
|
def _generate_value_by_type(self, xsd_type, target_name, patterns, min_length, max_length, min_value, max_value,
|
237
243
|
total_digits, fraction_digits) -> str | None:
|
238
244
|
|
@@ -289,17 +295,22 @@ class XmlGenerator:
|
|
289
295
|
|
290
296
|
def _generate_string(self, target_name, patterns, min_length, max_length):
|
291
297
|
rnd = self.randomizer.rnd
|
298
|
+
re_gen = self.randomizer.re_gen
|
292
299
|
if patterns is not None:
|
293
300
|
# Генерация строки по regex
|
294
301
|
random_pattern = rnd.choice(patterns)
|
295
|
-
xeger =
|
302
|
+
xeger = re_gen.xeger(random_pattern.attrib['value'])
|
296
303
|
xeger = re.sub(r'\s', ' ', xeger)
|
297
304
|
if min_length > -1 and len(xeger) < min_length:
|
298
|
-
|
299
|
-
|
300
|
-
|
305
|
+
logger.warning(
|
306
|
+
"Possible mistake in schema: %s generated value '%s' can't be shorter than %s",
|
307
|
+
target_name, xeger, min_length
|
308
|
+
)
|
301
309
|
if -1 < max_length < len(xeger):
|
302
|
-
|
310
|
+
logger.warning(
|
311
|
+
"Possible mistake in schema: %s generated value '%s' can't be longer than %s",
|
312
|
+
target_name, xeger, max_length
|
313
|
+
)
|
303
314
|
return xeger
|
304
315
|
|
305
316
|
# Иначе генерируем случайную строку
|
@@ -347,29 +358,43 @@ class XmlGenerator:
|
|
347
358
|
raise RuntimeError("not yet implemented")
|
348
359
|
|
349
360
|
def _generate_datetime(self):
|
350
|
-
|
361
|
+
random_datetime = self.randomizer.random_datetime()
|
362
|
+
formatted = random_datetime.isoformat()
|
363
|
+
return formatted
|
351
364
|
|
352
365
|
def _generate_date(self):
|
353
|
-
|
366
|
+
random_date = self.randomizer.random_date()
|
367
|
+
formatted = random_date.isoformat()
|
368
|
+
return formatted
|
354
369
|
|
355
370
|
def _generate_time(self):
|
356
|
-
|
371
|
+
random_time = self.randomizer.random_time()
|
372
|
+
formatted = random_time.isoformat()
|
373
|
+
return formatted
|
357
374
|
|
358
375
|
def _generate_gyearmonth(self):
|
359
|
-
|
376
|
+
random_date = self.randomizer.random_date()
|
377
|
+
formatted = random_date.strftime('%Y-%m')
|
378
|
+
return formatted
|
360
379
|
|
361
380
|
def _generate_gyear(self):
|
362
381
|
rnd = self.randomizer.rnd
|
363
|
-
return rnd.randint(2000, 2050)
|
382
|
+
return str(rnd.randint(2000, 2050))
|
364
383
|
|
365
384
|
def _generate_gmonthday(self):
|
366
|
-
|
385
|
+
random_date = self.randomizer.random_date()
|
386
|
+
formatted = random_date.strftime('--%m-%d')
|
387
|
+
return formatted
|
367
388
|
|
368
389
|
def _generate_gday(self):
|
369
|
-
|
390
|
+
random_date = self.randomizer.random_date()
|
391
|
+
formatted = random_date.strftime('---%d')
|
392
|
+
return formatted
|
370
393
|
|
371
394
|
def _generate_gmonth(self):
|
372
|
-
|
395
|
+
random_date = self.randomizer.random_date()
|
396
|
+
formatted = random_date.strftime('--%m--')
|
397
|
+
return formatted
|
373
398
|
|
374
399
|
def _generate_hex_binary(self):
|
375
400
|
raise RuntimeError("not yet implemented")
|
@@ -0,0 +1,76 @@
|
|
1
|
+
import logging
|
2
|
+
import random
|
3
|
+
import string
|
4
|
+
import sys
|
5
|
+
from datetime import datetime, date, time, timedelta
|
6
|
+
|
7
|
+
import rstr
|
8
|
+
from faker import Faker
|
9
|
+
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
class Randomizer:
|
14
|
+
def __init__(self, seed=None):
|
15
|
+
if not seed:
|
16
|
+
seed = random.randrange(sys.maxsize)
|
17
|
+
logger.debug('initialize with random seed: %s', seed)
|
18
|
+
else:
|
19
|
+
logger.debug('initialize with provided seed: %s', seed)
|
20
|
+
|
21
|
+
self.rnd = random.Random(seed)
|
22
|
+
self.fake = Faker(locale='ru_RU')
|
23
|
+
self.fake.seed_instance(seed)
|
24
|
+
self.re_gen = rstr.Rstr(self.rnd)
|
25
|
+
|
26
|
+
def ascii_string(self, min_length=-1, max_length=-1):
|
27
|
+
min_length = min_length if min_length and min_length > -1 else 1
|
28
|
+
max_length = max_length if max_length and max_length >= min_length else 20
|
29
|
+
if max_length > 50:
|
30
|
+
max_length = 50
|
31
|
+
length = self.rnd.randint(min_length, max_length)
|
32
|
+
# Генерация случайной строки из букв латиницы
|
33
|
+
letters = string.ascii_letters # Все буквы латиницы (a-z, A-Z)
|
34
|
+
return ''.join(self.rnd.choice(letters) for _ in range(length))
|
35
|
+
|
36
|
+
def random_date(self, start_date: str = '1990-01-01', end_date: str = '2025-12-31') -> date:
|
37
|
+
# Преобразуем строки в объекты datetime
|
38
|
+
start = date.fromisoformat(start_date)
|
39
|
+
end = date.fromisoformat(end_date)
|
40
|
+
|
41
|
+
# Вычисляем разницу в днях между начальной и конечной датой
|
42
|
+
delta = (end - start).days
|
43
|
+
|
44
|
+
# Генерируем случайное количество дней в пределах delta
|
45
|
+
random_days = self.rnd.randint(0, delta)
|
46
|
+
|
47
|
+
# Добавляем случайное количество дней к начальной дате
|
48
|
+
return start + timedelta(days=random_days)
|
49
|
+
|
50
|
+
def random_time(self, start_time: str = '00:00:00', end_time: str = '23:59:59') -> time:
|
51
|
+
start = time.fromisoformat(start_time)
|
52
|
+
end = time.fromisoformat(end_time)
|
53
|
+
|
54
|
+
random_h = self.rnd.randint(start.hour, end.hour)
|
55
|
+
random_m = self.rnd.randint(start.minute, end.minute)
|
56
|
+
random_s = self.rnd.randint(start.second, end.second)
|
57
|
+
|
58
|
+
return time(hour=random_h, minute=random_m, second=random_s)
|
59
|
+
|
60
|
+
def random_datetime(self, start_date: str = '1990-01-01', end_date: str = '2025-12-31') -> datetime:
|
61
|
+
# Преобразуем строки в объекты datetime
|
62
|
+
start = datetime.strptime(start_date, "%Y-%m-%d")
|
63
|
+
end = datetime.strptime(end_date, "%Y-%m-%d")
|
64
|
+
|
65
|
+
# Вычисляем разницу в днях между начальной и конечной датой
|
66
|
+
delta = (end - start).days
|
67
|
+
|
68
|
+
# Генерируем случайное количество дней в пределах delta
|
69
|
+
random_days = self.rnd.randint(0, delta)
|
70
|
+
|
71
|
+
# Добавляем случайное количество дней к начальной дате
|
72
|
+
return start + timedelta(days=random_days)
|
73
|
+
|
74
|
+
def snils_formatted(self):
|
75
|
+
snils = self.fake.snils()
|
76
|
+
return f"{snils[:3]}-{snils[3:6]}-{snils[6:9]} {snils[9:]}"
|
@@ -1,13 +1,15 @@
|
|
1
|
+
import logging
|
1
2
|
import re
|
2
|
-
import uuid
|
3
|
-
|
4
|
-
import rstr
|
5
3
|
|
6
4
|
from xmlgenerator.randomization import Randomizer
|
7
5
|
|
8
6
|
__all__ = ['Substitutor']
|
9
7
|
|
10
|
-
_pattern = re.compile(
|
8
|
+
_pattern = re.compile(
|
9
|
+
r'\{\{\s*(?:(?P<function>\S*?)(?:\(\s*(?P<argument>[^)]*)\s*\))?\s*(?:\|\s*(?P<modifier>.*?))?)?\s*}}')
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
11
13
|
|
12
14
|
class Substitutor:
|
13
15
|
def __init__(self, randomizer: Randomizer):
|
@@ -17,18 +19,19 @@ class Substitutor:
|
|
17
19
|
self._global_context = {}
|
18
20
|
self.providers_dict = {
|
19
21
|
# Функции локального контекста
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
'uuid': lambda:
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
22
|
+
'source_filename': lambda: self._local_context["source_filename"],
|
23
|
+
'source_extracted': lambda: self._local_context["source_extracted"],
|
24
|
+
'output_filename': lambda: self.get_output_filename(),
|
25
|
+
|
26
|
+
'uuid': lambda: fake.uuid4(),
|
27
|
+
'regex': self._rand_regex,
|
28
|
+
'any': self._rand_any,
|
29
|
+
'number': self._rand_int,
|
30
|
+
'date': self._rand_date,
|
31
|
+
|
32
|
+
'last_name': fake.last_name_male,
|
33
|
+
'first_name': fake.first_name_male,
|
34
|
+
'middle_name': fake.middle_name_male,
|
32
35
|
'address_text': fake.address,
|
33
36
|
'administrative_unit': fake.administrative_unit,
|
34
37
|
'house_number': fake.building_number,
|
@@ -45,6 +48,16 @@ class Substitutor:
|
|
45
48
|
'snils_formatted': randomizer.snils_formatted,
|
46
49
|
}
|
47
50
|
|
51
|
+
def _rand_regex(self, a):
|
52
|
+
pattern = a.strip("'").strip('"')
|
53
|
+
return self.randomizer.re_gen.xeger(pattern)
|
54
|
+
|
55
|
+
def _rand_any(self, a):
|
56
|
+
args = str(a).split(sep=",")
|
57
|
+
value = self.randomizer.rnd.choice(args)
|
58
|
+
value = value.strip(' ').strip("'").strip('"')
|
59
|
+
return value
|
60
|
+
|
48
61
|
def _rand_int(self, a):
|
49
62
|
args = str(a).split(sep=",")
|
50
63
|
return str(self.randomizer.rnd.randint(int(args[0]), int(args[1])))
|
@@ -53,8 +66,8 @@ class Substitutor:
|
|
53
66
|
args = str(a).split(sep=",")
|
54
67
|
date_from = args[0].strip(' ').strip("'").strip('"')
|
55
68
|
date_until = args[1].strip(' ').strip("'").strip('"')
|
56
|
-
random_date = self.randomizer.
|
57
|
-
return random_date.strftime('%Y%m%d')
|
69
|
+
random_date = self.randomizer.random_datetime(date_from, date_until)
|
70
|
+
return random_date.strftime('%Y%m%d') # TODO externalize pattern
|
58
71
|
|
59
72
|
def reset_context(self, xsd_filename, config_local):
|
60
73
|
self._local_context.clear()
|
@@ -69,6 +82,11 @@ class Substitutor:
|
|
69
82
|
resolved_value = self._process_expression(output_filename)
|
70
83
|
self._local_context['output_filename'] = resolved_value
|
71
84
|
|
85
|
+
logger.debug('local_context reset')
|
86
|
+
logger.debug('local_context["source_filename"] = %s', xsd_filename)
|
87
|
+
logger.debug('local_context["source_extracted"] = %s (extracted with regexp %s)', source_extracted, source_filename)
|
88
|
+
logger.debug('local_context["output_filename"] = %s', resolved_value)
|
89
|
+
|
72
90
|
def get_output_filename(self):
|
73
91
|
return self._local_context.get("output_filename")
|
74
92
|
|
@@ -83,6 +101,7 @@ class Substitutor:
|
|
83
101
|
return False, None
|
84
102
|
|
85
103
|
def _process_expression(self, expression):
|
104
|
+
logger.debug('processing expression: %s', expression)
|
86
105
|
global_context = self._global_context
|
87
106
|
local_context = self._local_context
|
88
107
|
result_value: str = expression
|
@@ -115,4 +134,5 @@ class Substitutor:
|
|
115
134
|
for span, replacement in reversed(list(span_to_replacement.items())):
|
116
135
|
result_value = result_value[:span[0]] + replacement + result_value[span[1]:]
|
117
136
|
|
137
|
+
logger.debug('expression resolved to value: %s', result_value)
|
118
138
|
return result_value
|
@@ -1,7 +1,10 @@
|
|
1
|
+
import logging
|
1
2
|
import sys
|
2
3
|
|
3
4
|
from xmlschema import XMLSchemaValidationError
|
4
5
|
|
6
|
+
logger = logging.getLogger(__name__)
|
7
|
+
|
5
8
|
|
6
9
|
class XmlValidator:
|
7
10
|
def __init__(self, post_validate: str, fail_fast: bool):
|
@@ -11,11 +14,13 @@ class XmlValidator:
|
|
11
14
|
self.validation_func = self._validate_with_schema
|
12
15
|
case 'schematron':
|
13
16
|
self.validation_func = self._validate_with_schematron
|
17
|
+
logger.debug("post validation: %s, fail fast: %s", post_validate, fail_fast)
|
14
18
|
|
15
19
|
def validate(self, xsd_schema, document):
|
16
20
|
self.validation_func(xsd_schema, document)
|
17
21
|
|
18
22
|
def _validate_with_schema(self, xsd_schema, document):
|
23
|
+
logger.debug("validate generated xml with xsd schema")
|
19
24
|
try:
|
20
25
|
xsd_schema.validate(document)
|
21
26
|
except XMLSchemaValidationError as err:
|
@@ -24,6 +29,7 @@ class XmlValidator:
|
|
24
29
|
sys.exit(1)
|
25
30
|
|
26
31
|
def _validate_with_schematron(self, xsd_schema, document):
|
32
|
+
logger.debug("validate generated xml with xsd schematron")
|
27
33
|
raise RuntimeError("not yet implemented")
|
28
34
|
|
29
35
|
# TODO
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: xmlgenerator
|
3
|
-
Version: 0.1
|
3
|
+
Version: 0.2.1
|
4
4
|
Summary: Generates XML documents from XSD schemas
|
5
5
|
Home-page: https://github.com/lexakimov/xmlgenerator
|
6
6
|
Author: Alexey Akimov
|
@@ -247,6 +247,7 @@ In the `value_override` sections, you can specify either a string value or speci
|
|
247
247
|
| `output_filename` | String described by the `output_filename_template` configuration parameter |
|
248
248
|
| `uuid` | Random UUIDv4 |
|
249
249
|
| `regex("pattern")` | Random string value matching the specified regular expression |
|
250
|
+
| `any('A', "B", C)` | Random value from enumeration |
|
250
251
|
| `number(A, B)` | Random number between A and B |
|
251
252
|
| `date("2010-01-01", "2025-01-01")` | Random date within the specified range |
|
252
253
|
| `last_name` | Last Name |
|
@@ -1,40 +0,0 @@
|
|
1
|
-
import random
|
2
|
-
import string
|
3
|
-
from datetime import datetime, timedelta
|
4
|
-
|
5
|
-
from faker import Faker
|
6
|
-
|
7
|
-
|
8
|
-
class Randomizer:
|
9
|
-
def __init__(self, seed=None):
|
10
|
-
self.rnd = random.Random(seed)
|
11
|
-
self.fake = Faker(locale='ru_RU')
|
12
|
-
self.fake.seed_instance(seed)
|
13
|
-
|
14
|
-
def ascii_string(self, min_length=-1, max_length=-1):
|
15
|
-
min_length = min_length if min_length and min_length > -1 else 1
|
16
|
-
max_length = max_length if max_length and max_length >= min_length else 20
|
17
|
-
if max_length > 50:
|
18
|
-
max_length = 50
|
19
|
-
length = self.rnd.randint(min_length, max_length)
|
20
|
-
# Генерация случайной строки из букв латиницы
|
21
|
-
letters = string.ascii_letters # Все буквы латиницы (a-z, A-Z)
|
22
|
-
return ''.join(self.rnd.choice(letters) for _ in range(length))
|
23
|
-
|
24
|
-
def random_date(self, start_date: str, end_date: str) -> datetime:
|
25
|
-
# Преобразуем строки в объекты datetime
|
26
|
-
start = datetime.strptime(start_date, "%Y-%m-%d")
|
27
|
-
end = datetime.strptime(end_date, "%Y-%m-%d")
|
28
|
-
|
29
|
-
# Вычисляем разницу в днях между начальной и конечной датой
|
30
|
-
delta = (end - start).days
|
31
|
-
|
32
|
-
# Генерируем случайное количество дней в пределах delta
|
33
|
-
random_days = self.rnd.randint(0, delta)
|
34
|
-
|
35
|
-
# Добавляем случайное количество дней к начальной дате
|
36
|
-
return start + timedelta(days=random_days)
|
37
|
-
|
38
|
-
def snils_formatted(self):
|
39
|
-
snils = self.fake.snils()
|
40
|
-
return f"{snils[:3]}-{snils[3:6]}-{snils[6:9]} {snils[9:]}"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|