xmlgenerator 0.1.0__tar.gz → 0.2.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.
- {xmlgenerator-0.1.0/xmlgenerator.egg-info → xmlgenerator-0.2.0}/PKG-INFO +1 -1
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0}/setup.py +1 -1
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0}/xmlgenerator/arguments.py +7 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0}/xmlgenerator/bootstrap.py +38 -10
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0}/xmlgenerator/configuration.py +24 -5
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0}/xmlgenerator/generator.py +22 -11
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0}/xmlgenerator/randomization.py +10 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0}/xmlgenerator/substitution.py +10 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0}/xmlgenerator/validation.py +6 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0/xmlgenerator.egg-info}/PKG-INFO +1 -1
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0}/LICENSE +0 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0}/README.md +0 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0}/setup.cfg +0 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0}/xmlgenerator/__init__.py +0 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0}/xmlgenerator.egg-info/SOURCES.txt +0 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0}/xmlgenerator.egg-info/dependency_links.txt +0 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0}/xmlgenerator.egg-info/entry_points.txt +0 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0}/xmlgenerator.egg-info/requires.txt +0 -0
- {xmlgenerator-0.1.0 → xmlgenerator-0.2.0}/xmlgenerator.egg-info/top_level.txt +0 -0
@@ -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.debug('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,5 +1,5 @@
|
|
1
|
+
import logging
|
1
2
|
import re
|
2
|
-
import sys
|
3
3
|
|
4
4
|
import rstr
|
5
5
|
import xmlschema
|
@@ -12,6 +12,8 @@ from xmlgenerator.configuration import GeneratorConfig
|
|
12
12
|
from xmlgenerator.randomization import Randomizer
|
13
13
|
from xmlgenerator.substitution import Substitutor
|
14
14
|
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
15
17
|
|
16
18
|
class XmlGenerator:
|
17
19
|
def __init__(self, randomizer: Randomizer, substitutor: Substitutor):
|
@@ -28,21 +30,27 @@ class XmlGenerator:
|
|
28
30
|
rnd = self.randomizer.rnd
|
29
31
|
|
30
32
|
xsd_element_type = getattr(xsd_element, 'type', None)
|
33
|
+
logger.debug('fill down element "%s" with type %s', xsd_element.name, type(xsd_element_type).__name__)
|
31
34
|
|
32
35
|
# Add attributes if they are
|
33
36
|
attributes = getattr(xsd_element, 'attributes', dict())
|
34
37
|
if len(attributes) > 0 and xsd_element_type.local_name != 'anyType':
|
38
|
+
logger.debug('add attributes to element %s', xsd_element.name)
|
35
39
|
for attr_name, attr in attributes.items():
|
40
|
+
logger.debug('attribute: %s', attr_name)
|
36
41
|
use = attr.use # optional | required | prohibited
|
37
42
|
if use == 'prohibited':
|
43
|
+
logger.debug('skipped')
|
38
44
|
continue
|
39
45
|
elif use == 'optional':
|
40
46
|
if rnd.random() > local_config.randomization.probability:
|
41
|
-
|
47
|
+
logger.debug('skipped')
|
48
|
+
continue # skip optional attribute
|
42
49
|
|
43
50
|
attr_value = self._generate_value(attr.type, attr_name, local_config)
|
44
51
|
if attr_value is not None:
|
45
52
|
xml_element.set(attr_name, str(attr_value))
|
53
|
+
logger.debug(f'attribute %s set with value %s', attr_name, attr_value)
|
46
54
|
|
47
55
|
# Process child elements --------------------------------------------------------------------------------------
|
48
56
|
if isinstance(xsd_element, XsdElement):
|
@@ -69,7 +77,7 @@ class XmlGenerator:
|
|
69
77
|
group_min_occurs = getattr(xsd_element, 'min_occurs', None)
|
70
78
|
group_max_occurs = getattr(xsd_element, 'max_occurs', None)
|
71
79
|
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
|
80
|
+
group_max_occurs = group_max_occurs if group_max_occurs is not None else 10 # TODO externalize
|
73
81
|
group_occurs = rnd.randint(group_min_occurs, group_max_occurs)
|
74
82
|
|
75
83
|
if model == 'all':
|
@@ -80,7 +88,7 @@ class XmlGenerator:
|
|
80
88
|
element_min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
|
81
89
|
element_max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
|
82
90
|
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
|
91
|
+
element_max_occurs = element_max_occurs if element_max_occurs is not None else 10 # TODO externalize
|
84
92
|
element_occurs = rnd.randint(element_min_occurs, element_max_occurs)
|
85
93
|
|
86
94
|
for _ in range(element_occurs):
|
@@ -96,7 +104,7 @@ class XmlGenerator:
|
|
96
104
|
element_min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
|
97
105
|
element_max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
|
98
106
|
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
|
107
|
+
element_max_occurs = element_max_occurs if element_max_occurs is not None else 10 # TODO externalize
|
100
108
|
element_occurs = rnd.randint(element_min_occurs, element_max_occurs)
|
101
109
|
|
102
110
|
if isinstance(xsd_child_element_type, XsdElement):
|
@@ -123,7 +131,7 @@ class XmlGenerator:
|
|
123
131
|
element_min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
|
124
132
|
element_max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
|
125
133
|
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
|
134
|
+
element_max_occurs = element_max_occurs if element_max_occurs is not None else 10 # TODO externalize
|
127
135
|
element_occurs = rnd.randint(element_min_occurs, element_max_occurs)
|
128
136
|
|
129
137
|
for _ in range(element_occurs):
|
@@ -232,7 +240,6 @@ class XmlGenerator:
|
|
232
240
|
|
233
241
|
raise RuntimeError(f"Can't generate value - unhandled type. Target name: {target_name}")
|
234
242
|
|
235
|
-
|
236
243
|
def _generate_value_by_type(self, xsd_type, target_name, patterns, min_length, max_length, min_value, max_value,
|
237
244
|
total_digits, fraction_digits) -> str | None:
|
238
245
|
|
@@ -295,11 +302,15 @@ class XmlGenerator:
|
|
295
302
|
xeger = rstr.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
|
# Иначе генерируем случайную строку
|
@@ -1,12 +1,22 @@
|
|
1
|
+
import logging
|
1
2
|
import random
|
2
3
|
import string
|
4
|
+
import sys
|
3
5
|
from datetime import datetime, timedelta
|
4
6
|
|
5
7
|
from faker import Faker
|
6
8
|
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
7
11
|
|
8
12
|
class Randomizer:
|
9
13
|
def __init__(self, seed=None):
|
14
|
+
if not seed:
|
15
|
+
seed = random.randrange(sys.maxsize)
|
16
|
+
logger.debug('initialize with random seed: %s', seed)
|
17
|
+
else:
|
18
|
+
logger.debug('initialize with provided seed: %s', seed)
|
19
|
+
|
10
20
|
self.rnd = random.Random(seed)
|
11
21
|
self.fake = Faker(locale='ru_RU')
|
12
22
|
self.fake.seed_instance(seed)
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import logging
|
1
2
|
import re
|
2
3
|
import uuid
|
3
4
|
|
@@ -9,6 +10,8 @@ __all__ = ['Substitutor']
|
|
9
10
|
|
10
11
|
_pattern = re.compile(pattern=r'\{\{\s*(?:(?P<function>\S*?)(?:\(\s*(?P<argument>[^)]*)\s*\))?\s*(?:\|\s*(?P<modifier>.*?))?)?\s*}}')
|
11
12
|
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
12
15
|
class Substitutor:
|
13
16
|
def __init__(self, randomizer: Randomizer):
|
14
17
|
fake = randomizer.fake
|
@@ -69,6 +72,11 @@ class Substitutor:
|
|
69
72
|
resolved_value = self._process_expression(output_filename)
|
70
73
|
self._local_context['output_filename'] = resolved_value
|
71
74
|
|
75
|
+
logger.debug('local_context reset')
|
76
|
+
logger.debug('local_context["source_filename"] = %s', xsd_filename)
|
77
|
+
logger.debug('local_context["source_extracted"] = %s (extracted with regexp %s)', source_extracted, source_filename)
|
78
|
+
logger.debug('local_context["output_filename"] = %s', resolved_value)
|
79
|
+
|
72
80
|
def get_output_filename(self):
|
73
81
|
return self._local_context.get("output_filename")
|
74
82
|
|
@@ -83,6 +91,7 @@ class Substitutor:
|
|
83
91
|
return False, None
|
84
92
|
|
85
93
|
def _process_expression(self, expression):
|
94
|
+
logger.debug('processing expression: %s', expression)
|
86
95
|
global_context = self._global_context
|
87
96
|
local_context = self._local_context
|
88
97
|
result_value: str = expression
|
@@ -115,4 +124,5 @@ class Substitutor:
|
|
115
124
|
for span, replacement in reversed(list(span_to_replacement.items())):
|
116
125
|
result_value = result_value[:span[0]] + replacement + result_value[span[1]:]
|
117
126
|
|
127
|
+
logger.debug('expression resolved to value: %s', result_value)
|
118
128
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|