xmlgenerator 0.3.0__tar.gz → 0.5.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.3.0/xmlgenerator.egg-info → xmlgenerator-0.5.0}/PKG-INFO +4 -1
- {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/README.md +3 -0
- {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/setup.py +1 -1
- {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator/arguments.py +2 -1
- {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator/bootstrap.py +1 -1
- xmlgenerator-0.5.0/xmlgenerator/generator.py +596 -0
- {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator/randomization.py +13 -5
- {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator/substitution.py +2 -1
- {xmlgenerator-0.3.0 → xmlgenerator-0.5.0/xmlgenerator.egg-info}/PKG-INFO +4 -1
- xmlgenerator-0.3.0/xmlgenerator/generator.py +0 -456
- {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/LICENSE +0 -0
- {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/setup.cfg +0 -0
- {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator/__init__.py +0 -0
- {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator/configuration.py +0 -0
- {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator/validation.py +0 -0
- {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator.egg-info/SOURCES.txt +0 -0
- {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator.egg-info/dependency_links.txt +0 -0
- {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator.egg-info/entry_points.txt +0 -0
- {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator.egg-info/requires.txt +0 -0
- {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/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.
|
3
|
+
Version: 0.5.0
|
4
4
|
Summary: Generates XML documents from XSD schemas
|
5
5
|
Home-page: https://github.com/lexakimov/xmlgenerator
|
6
6
|
Author: Alexey Akimov
|
@@ -173,6 +173,8 @@ global:
|
|
173
173
|
# Probability of adding optional elements (0.0-1.0)
|
174
174
|
# Default value: 0.5
|
175
175
|
probability: 1
|
176
|
+
# Limit for the minimal number of elements
|
177
|
+
min_occurs: 0
|
176
178
|
# Limit for the maximum number of elements
|
177
179
|
max_occurs: 5
|
178
180
|
# Minimum string length
|
@@ -268,6 +270,7 @@ In the `value_override` sections, you can specify either a string value or speci
|
|
268
270
|
| `ogrn_fl` | Primary State Registration Number (Physical Person) |
|
269
271
|
| `kpp` | Reason Code for Registration |
|
270
272
|
| `snils_formatted` | SNILS (Personal Insurance Account Number) in the format `123-456-789 90` |
|
273
|
+
| `email` | Random email address |
|
271
274
|
|
272
275
|
**Configuration Examples:**
|
273
276
|
|
@@ -144,6 +144,8 @@ global:
|
|
144
144
|
# Probability of adding optional elements (0.0-1.0)
|
145
145
|
# Default value: 0.5
|
146
146
|
probability: 1
|
147
|
+
# Limit for the minimal number of elements
|
148
|
+
min_occurs: 0
|
147
149
|
# Limit for the maximum number of elements
|
148
150
|
max_occurs: 5
|
149
151
|
# Minimum string length
|
@@ -239,6 +241,7 @@ In the `value_override` sections, you can specify either a string value or speci
|
|
239
241
|
| `ogrn_fl` | Primary State Registration Number (Physical Person) |
|
240
242
|
| `kpp` | Reason Code for Registration |
|
241
243
|
| `snils_formatted` | SNILS (Personal Insurance Account Number) in the format `123-456-789 90` |
|
244
|
+
| `email` | Random email address |
|
242
245
|
|
243
246
|
**Configuration Examples:**
|
244
247
|
|
@@ -33,7 +33,7 @@ def _get_parser():
|
|
33
33
|
dest="source_paths",
|
34
34
|
help="paths to xsd schema(s) or directory with xsd schemas"
|
35
35
|
)
|
36
|
-
parser.add_argument(
|
36
|
+
config_arg = parser.add_argument(
|
37
37
|
"-c", "--config",
|
38
38
|
metavar="<config.yml>",
|
39
39
|
dest="config_yaml",
|
@@ -94,6 +94,7 @@ def _get_parser():
|
|
94
94
|
)
|
95
95
|
|
96
96
|
# add shell completions
|
97
|
+
config_arg.complete = shtab.FILE
|
97
98
|
source_arg.complete = shtab.FILE
|
98
99
|
output_arg.complete = shtab.FILE
|
99
100
|
shtab.add_argument_to(parser, ["-C", "--completion"], "print shell completion script (bash, zsh, tcsh)")
|
@@ -12,7 +12,6 @@ from xmlgenerator.randomization import Randomizer
|
|
12
12
|
from xmlgenerator.substitution import Substitutor
|
13
13
|
from xmlgenerator.validation import XmlValidator
|
14
14
|
|
15
|
-
# TODO конфигурация ограничений - occurs
|
16
15
|
# TODO Generator - обработка стандартных xsd типов
|
17
16
|
# TODO кастомные переменные для локального контекста
|
18
17
|
# TODO валидация по Schematron
|
@@ -90,6 +89,7 @@ def _main():
|
|
90
89
|
|
91
90
|
|
92
91
|
def _setup_loggers(args):
|
92
|
+
logging.addLevelName(logging.WARNING, 'WARN')
|
93
93
|
log_level = logging.DEBUG if args.debug else logging.INFO
|
94
94
|
logger.setLevel(log_level)
|
95
95
|
configuration.logger.setLevel(log_level)
|
@@ -0,0 +1,596 @@
|
|
1
|
+
import logging
|
2
|
+
from dataclasses import dataclass, replace
|
3
|
+
from decimal import Decimal
|
4
|
+
from typing import Optional, Any, Callable, Dict
|
5
|
+
|
6
|
+
from lxml import etree
|
7
|
+
from xmlschema.validators import XsdComplexType, XsdAtomicRestriction, XsdTotalDigitsFacet, XsdElement, \
|
8
|
+
XsdGroup, XsdFractionDigitsFacet, XsdLengthFacet, XsdMaxLengthFacet, XsdMinExclusiveFacet, XsdMinInclusiveFacet, \
|
9
|
+
XsdMinLengthFacet, XsdAnyElement, XsdAtomicBuiltin, XsdEnumerationFacets, XsdMaxExclusiveFacet, XsdMaxInclusiveFacet
|
10
|
+
|
11
|
+
from xmlgenerator.configuration import GeneratorConfig
|
12
|
+
from xmlgenerator.randomization import Randomizer
|
13
|
+
from xmlgenerator.substitution import Substitutor
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class TypeConstraints:
|
20
|
+
min_length: Optional[int] = None
|
21
|
+
max_length: Optional[int] = None
|
22
|
+
min_value: Optional[Any] = None
|
23
|
+
max_value: Optional[Any] = None
|
24
|
+
total_digits: Optional[int] = None
|
25
|
+
fraction_digits: Optional[int] = None
|
26
|
+
patterns: Optional[list] = None
|
27
|
+
rand_config: Optional[Any] = None
|
28
|
+
|
29
|
+
|
30
|
+
class XmlGenerator:
|
31
|
+
def __init__(self, randomizer: Randomizer, substitutor: Substitutor):
|
32
|
+
self.randomizer = randomizer
|
33
|
+
self.substitutor = substitutor
|
34
|
+
self.generators: Dict[str, Callable[[TypeConstraints], str]] = {
|
35
|
+
# primitive
|
36
|
+
'boolean': self._generate_boolean,
|
37
|
+
'string': self._generate_string,
|
38
|
+
'decimal': self._generate_decimal,
|
39
|
+
'float': self._generate_float,
|
40
|
+
'double': self._generate_double,
|
41
|
+
'duration': self._generate_duration,
|
42
|
+
'dateTime': self._generate_datetime,
|
43
|
+
'date': self._generate_date,
|
44
|
+
'time': self._generate_time,
|
45
|
+
'gYearMonth': self._generate_gyearmonth,
|
46
|
+
'gYear': self._generate_gyear,
|
47
|
+
'gMonthDay': self._generate_gmonthday,
|
48
|
+
'gDay': self._generate_gday,
|
49
|
+
'gMonth': self._generate_gmonth,
|
50
|
+
'hexBinary': self._generate_hex_binary,
|
51
|
+
'base64Binary': self._generate_base64_binary,
|
52
|
+
'anyURI': self._generate_any_uri,
|
53
|
+
'QName': self._generate_qname,
|
54
|
+
'NOTATION': self._generate_notation,
|
55
|
+
|
56
|
+
# derived - from decimal
|
57
|
+
'byte': self._generate_byte,
|
58
|
+
'short': self._generate_short,
|
59
|
+
'int': self._generate_int,
|
60
|
+
'integer': self._generate_integer,
|
61
|
+
'long': self._generate_long,
|
62
|
+
|
63
|
+
'unsignedByte': self._generate_unsigned_byte,
|
64
|
+
'unsignedShort': self._generate_unsigned_short,
|
65
|
+
'unsignedInt': self._generate_unsigned_int,
|
66
|
+
'unsignedLong': self._generate_unsigned_long,
|
67
|
+
|
68
|
+
'positiveInteger': self._generate_positive_integer,
|
69
|
+
'negativeInteger': self._generate_negative_integer,
|
70
|
+
'nonPositiveInteger': self._generate_non_positive_integer,
|
71
|
+
'nonNegativeInteger': self._generate_non_negative_integer,
|
72
|
+
|
73
|
+
# derived - from string
|
74
|
+
'language': lambda c: (_ for _ in ()).throw(Exception('not yet implemented')),
|
75
|
+
'Name': lambda c: (_ for _ in ()).throw(Exception('not yet implemented')),
|
76
|
+
'NCName': lambda c: (_ for _ in ()).throw(Exception('not yet implemented')),
|
77
|
+
'normalizedString': lambda c: (_ for _ in ()).throw(Exception('not yet implemented')),
|
78
|
+
'token': lambda c: (_ for _ in ()).throw(Exception('not yet implemented')),
|
79
|
+
'ID': lambda c: (_ for _ in ()).throw(Exception('not yet implemented')),
|
80
|
+
'IDREF': lambda c: (_ for _ in ()).throw(Exception('not yet implemented')),
|
81
|
+
'IDREFS': lambda c: (_ for _ in ()).throw(Exception('not yet implemented')),
|
82
|
+
'ENTITY': lambda c: (_ for _ in ()).throw(Exception('not yet implemented')),
|
83
|
+
'ENTITIES': lambda c: (_ for _ in ()).throw(Exception('not yet implemented')),
|
84
|
+
'NMTOKEN': lambda c: (_ for _ in ()).throw(Exception('not yet implemented')),
|
85
|
+
'NMTOKENS': lambda c: (_ for _ in ()).throw(Exception('not yet implemented')),
|
86
|
+
}
|
87
|
+
|
88
|
+
def generate_xml(self, xsd_schema, local_config: GeneratorConfig) -> etree.Element:
|
89
|
+
logger.debug('generate xml document...')
|
90
|
+
ns_map = {None if k == '' else k: v for k, v in xsd_schema.namespaces.items() if v != ''}
|
91
|
+
xsd_root_element = xsd_schema.root_elements[0]
|
92
|
+
xml_root_element = etree.Element(xsd_root_element.name, nsmap=ns_map)
|
93
|
+
xml_tree = etree.ElementTree(xml_root_element)
|
94
|
+
self._add_elements(xml_tree, xml_root_element, xsd_root_element, local_config)
|
95
|
+
return xml_root_element
|
96
|
+
|
97
|
+
def _add_elements(self, xml_tree, xml_element, xsd_element, local_config: GeneratorConfig) -> None:
|
98
|
+
rand_config = local_config.randomization
|
99
|
+
min_occurs_conf = rand_config.min_occurs
|
100
|
+
max_occurs_conf = rand_config.max_occurs
|
101
|
+
|
102
|
+
# Process child elements --------------------------------------------------------------------------------------
|
103
|
+
if isinstance(xsd_element, XsdElement):
|
104
|
+
element_xpath = xml_tree.getpath(xml_element)
|
105
|
+
logger.debug('element: %s [created]', element_xpath)
|
106
|
+
|
107
|
+
xsd_element_type = getattr(xsd_element, 'type', None)
|
108
|
+
|
109
|
+
# Add attributes if they are
|
110
|
+
attributes = getattr(xsd_element, 'attributes', dict())
|
111
|
+
if len(attributes) > 0 and xsd_element_type.local_name != 'anyType':
|
112
|
+
for attr_name, attr in attributes.items():
|
113
|
+
logger.debug('element: %s; attribute: "%s" - [processing]', element_xpath, attr_name)
|
114
|
+
use = attr.use # optional | required | prohibited
|
115
|
+
if use == 'prohibited':
|
116
|
+
logger.debug('element: %s; attribute: "%s" - [skipped]', element_xpath, attr_name)
|
117
|
+
continue
|
118
|
+
elif use == 'optional':
|
119
|
+
if self.randomizer.random() > rand_config.probability:
|
120
|
+
logger.debug('element: %s; attribute: "%s" - [skipped]', element_xpath, attr_name)
|
121
|
+
continue
|
122
|
+
|
123
|
+
attr_value = self._generate_value(attr.type, attr_name, local_config)
|
124
|
+
if attr_value is not None:
|
125
|
+
xml_element.set(attr_name, str(attr_value))
|
126
|
+
logger.debug('element: %s; attribute: "%s" = "%s"', element_xpath, attr_name, attr_value)
|
127
|
+
|
128
|
+
if isinstance(xsd_element_type, XsdAtomicBuiltin):
|
129
|
+
text = self._generate_value(xsd_element_type, xsd_element.name, local_config)
|
130
|
+
xml_element.text = text
|
131
|
+
logger.debug('element: %s = "%s"', element_xpath, text)
|
132
|
+
|
133
|
+
elif isinstance(xsd_element_type, XsdAtomicRestriction):
|
134
|
+
text = self._generate_value(xsd_element_type, xsd_element.name, local_config)
|
135
|
+
xml_element.text = text
|
136
|
+
logger.debug('element: %s = "%s"', element_xpath, text)
|
137
|
+
|
138
|
+
elif isinstance(xsd_element_type, XsdComplexType):
|
139
|
+
xsd_element_type_content = xsd_element_type.content
|
140
|
+
if isinstance(xsd_element_type_content, XsdGroup):
|
141
|
+
self._add_elements(xml_tree, xml_element, xsd_element_type_content, local_config)
|
142
|
+
else:
|
143
|
+
raise RuntimeError()
|
144
|
+
|
145
|
+
else:
|
146
|
+
raise RuntimeError()
|
147
|
+
|
148
|
+
elif isinstance(xsd_element, XsdGroup):
|
149
|
+
model = xsd_element.model
|
150
|
+
|
151
|
+
min_occurs = getattr(xsd_element, 'min_occurs', None)
|
152
|
+
max_occurs = getattr(xsd_element, 'max_occurs', None)
|
153
|
+
min_occurs, max_occurs = merge_constraints(
|
154
|
+
schema_min=min_occurs,
|
155
|
+
schema_max=max_occurs,
|
156
|
+
config_min=min_occurs_conf,
|
157
|
+
config_max=max_occurs_conf
|
158
|
+
)
|
159
|
+
if max_occurs is None:
|
160
|
+
max_occurs = 10
|
161
|
+
group_occurs = self.randomizer.integer(min_occurs, max_occurs)
|
162
|
+
logger.debug('add %s (random between %s and %s) groups of type "%s"',
|
163
|
+
group_occurs, min_occurs, max_occurs, model)
|
164
|
+
|
165
|
+
if model == 'all':
|
166
|
+
for _ in range(group_occurs):
|
167
|
+
xsd_group_content = xsd_element.content
|
168
|
+
for xsd_child_element_type in xsd_group_content:
|
169
|
+
|
170
|
+
min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
|
171
|
+
max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
|
172
|
+
min_occurs, max_occurs = merge_constraints(
|
173
|
+
schema_min=min_occurs,
|
174
|
+
schema_max=max_occurs,
|
175
|
+
config_min=min_occurs_conf,
|
176
|
+
config_max=max_occurs_conf
|
177
|
+
)
|
178
|
+
if max_occurs is None:
|
179
|
+
max_occurs = 10
|
180
|
+
element_occurs = self.randomizer.integer(min_occurs, max_occurs)
|
181
|
+
logger.debug('element_occurs: %s (random between %s and %s)', element_occurs, min_occurs,
|
182
|
+
max_occurs)
|
183
|
+
|
184
|
+
for _ in range(element_occurs):
|
185
|
+
xml_child_element = etree.SubElement(xml_element, xsd_child_element_type.name)
|
186
|
+
self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
|
187
|
+
|
188
|
+
elif model == 'sequence':
|
189
|
+
for _ in range(group_occurs):
|
190
|
+
xsd_group_content = xsd_element.content
|
191
|
+
for xsd_child_element_type in xsd_group_content:
|
192
|
+
if isinstance(xsd_child_element_type, XsdElement):
|
193
|
+
|
194
|
+
min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
|
195
|
+
max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
|
196
|
+
min_occurs, max_occurs = merge_constraints(
|
197
|
+
schema_min=min_occurs,
|
198
|
+
schema_max=max_occurs,
|
199
|
+
config_min=min_occurs_conf,
|
200
|
+
config_max=max_occurs_conf
|
201
|
+
)
|
202
|
+
if max_occurs is None:
|
203
|
+
max_occurs = 10
|
204
|
+
element_occurs = self.randomizer.integer(min_occurs, max_occurs)
|
205
|
+
logger.debug('element_occurs: %s (random between %s and %s)', element_occurs, min_occurs,
|
206
|
+
max_occurs)
|
207
|
+
|
208
|
+
for _ in range(element_occurs):
|
209
|
+
xml_child_element = etree.SubElement(xml_element, xsd_child_element_type.name)
|
210
|
+
self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
|
211
|
+
|
212
|
+
elif isinstance(xsd_child_element_type, XsdGroup):
|
213
|
+
xml_child_element = xml_element
|
214
|
+
self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
|
215
|
+
|
216
|
+
elif isinstance(xsd_child_element_type, XsdAnyElement):
|
217
|
+
xml_child_element = etree.SubElement(xml_element, "Any")
|
218
|
+
self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
|
219
|
+
|
220
|
+
else:
|
221
|
+
raise RuntimeError(xsd_child_element_type)
|
222
|
+
|
223
|
+
elif model == 'choice':
|
224
|
+
for _ in range(group_occurs):
|
225
|
+
xsd_child_element_type = self.randomizer.any(xsd_element)
|
226
|
+
|
227
|
+
min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
|
228
|
+
max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
|
229
|
+
min_occurs, max_occurs = merge_constraints(
|
230
|
+
schema_min=min_occurs,
|
231
|
+
schema_max=max_occurs,
|
232
|
+
config_min=min_occurs_conf,
|
233
|
+
config_max=max_occurs_conf
|
234
|
+
)
|
235
|
+
if max_occurs is None:
|
236
|
+
max_occurs = 10
|
237
|
+
element_occurs = self.randomizer.integer(min_occurs, max_occurs)
|
238
|
+
logger.debug('element_occurs: %s (random between %s and %s)', element_occurs, min_occurs,
|
239
|
+
max_occurs)
|
240
|
+
|
241
|
+
for _ in range(element_occurs):
|
242
|
+
xml_child_element = etree.SubElement(xml_element, xsd_child_element_type.name)
|
243
|
+
self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
|
244
|
+
|
245
|
+
else:
|
246
|
+
raise RuntimeError()
|
247
|
+
|
248
|
+
elif isinstance(xsd_element, XsdAnyElement):
|
249
|
+
# для any не добавляем никаких дочерних тегов и атрибутов
|
250
|
+
pass
|
251
|
+
|
252
|
+
else:
|
253
|
+
raise RuntimeError()
|
254
|
+
|
255
|
+
def _generate_value(self, xsd_type, target_name, local_config: GeneratorConfig) -> str | None:
|
256
|
+
if xsd_type is None:
|
257
|
+
raise RuntimeError(f"xsd_type is None. Target name: {target_name}")
|
258
|
+
|
259
|
+
if isinstance(xsd_type, XsdComplexType):
|
260
|
+
return None
|
261
|
+
|
262
|
+
# -------------------------------------------------------------------------------------------------------------
|
263
|
+
# Ищем переопределение значения в конфигурации
|
264
|
+
value_override = local_config.value_override
|
265
|
+
is_found, overridden_value = self.substitutor.substitute_value(target_name, value_override.items())
|
266
|
+
if is_found:
|
267
|
+
logger.debug('value resolved: "%s"', overridden_value)
|
268
|
+
return overridden_value
|
269
|
+
|
270
|
+
# -------------------------------------------------------------------------------------------------------------
|
271
|
+
# If there is an enumeration, select a random value from it
|
272
|
+
enumeration = getattr(xsd_type, 'enumeration', None)
|
273
|
+
if enumeration is not None:
|
274
|
+
random_enum = self.randomizer.any(enumeration)
|
275
|
+
logger.debug('use random value from enumeration: "%s" %s', random_enum, enumeration)
|
276
|
+
return str(random_enum)
|
277
|
+
|
278
|
+
# -------------------------------------------------------------------------------------------------------------
|
279
|
+
# Генерируем значения для стандартных типов и типов с ограничениями
|
280
|
+
if isinstance(xsd_type, XsdAtomicBuiltin) or isinstance(xsd_type, XsdAtomicRestriction):
|
281
|
+
constraints = extract_type_constraints(xsd_type, local_config)
|
282
|
+
type_id = xsd_type.id or xsd_type.base_type.id or xsd_type.root_type.id
|
283
|
+
logger.debug('generate value for type: "%s"', type_id)
|
284
|
+
generator = self.generators.get(type_id)
|
285
|
+
if generator is None:
|
286
|
+
raise RuntimeError(f"Generator not found for type: {type_id}")
|
287
|
+
generated_value = generator(constraints)
|
288
|
+
|
289
|
+
logger.debug('value generated: "%s"', generated_value)
|
290
|
+
return generated_value
|
291
|
+
|
292
|
+
# -------------------------------------------------------------------------------------------------------------
|
293
|
+
# Проверяем базовый тип
|
294
|
+
base_type = getattr(xsd_type, 'base_type', None)
|
295
|
+
|
296
|
+
# невозможный кейс (только если попался комплексный тип)
|
297
|
+
if base_type is None:
|
298
|
+
raise RuntimeError(f"base_type is None. Target name: {target_name}")
|
299
|
+
|
300
|
+
raise RuntimeError(f"Can't generate value - unhandled type. Target name: {target_name}")
|
301
|
+
|
302
|
+
# noinspection PyUnusedLocal
|
303
|
+
def _generate_boolean(self, constraints: TypeConstraints):
|
304
|
+
return self.randomizer.any(['true', 'false'])
|
305
|
+
|
306
|
+
def _generate_string(self, constraints: TypeConstraints):
|
307
|
+
if constraints.patterns is not None:
|
308
|
+
# Генерация строки по regex
|
309
|
+
random_enum = self.randomizer.any(constraints.patterns)
|
310
|
+
random_pattern = random_enum.attrib['value']
|
311
|
+
return self.randomizer.regex(random_pattern)
|
312
|
+
|
313
|
+
# Иначе генерируем случайную строку
|
314
|
+
return self.randomizer.ascii_string(constraints.min_length, constraints.max_length)
|
315
|
+
|
316
|
+
def _generate_decimal(self, constraints: TypeConstraints):
|
317
|
+
rand_config = constraints.rand_config
|
318
|
+
min_value = constraints.min_value
|
319
|
+
max_value = constraints.max_value
|
320
|
+
total_digits = constraints.total_digits
|
321
|
+
fraction_digits = constraints.fraction_digits
|
322
|
+
|
323
|
+
if fraction_digits is None:
|
324
|
+
fraction_digits = self.randomizer.integer(1, 3)
|
325
|
+
|
326
|
+
if fraction_digits > 4:
|
327
|
+
fraction_digits = self.randomizer.integer(1, 4)
|
328
|
+
|
329
|
+
if total_digits is None:
|
330
|
+
total_digits = 10 + fraction_digits
|
331
|
+
|
332
|
+
if total_digits > 10:
|
333
|
+
total_digits = self.randomizer.integer(6, total_digits - 2)
|
334
|
+
|
335
|
+
integer_digits = total_digits - fraction_digits
|
336
|
+
|
337
|
+
# negative bound
|
338
|
+
digit_min = -(10 ** integer_digits - 1)
|
339
|
+
# positive bound
|
340
|
+
digit_max = 10 ** integer_digits - 1
|
341
|
+
logger.debug("integer digits: %s; digit_min: %s; digit_max: %s", integer_digits, digit_min, digit_max)
|
342
|
+
|
343
|
+
logger.debug('bounds before adjust: min_value: %4s; max_value: %4s', min_value, max_value)
|
344
|
+
config_min = rand_config.min_inclusive
|
345
|
+
config_max = rand_config.max_inclusive
|
346
|
+
effective_min, effective_max \
|
347
|
+
= merge_constraints(digit_min, digit_max, min_value, max_value, config_min, config_max)
|
348
|
+
logger.debug('bounds after adjust: min_value: %4s; max_value: %4s', effective_min, effective_max)
|
349
|
+
|
350
|
+
if fraction_digits == 0:
|
351
|
+
random_int = self.randomizer.integer(min_value, max_value)
|
352
|
+
return str(random_int)
|
353
|
+
else:
|
354
|
+
random_float = self.randomizer.float(effective_min, effective_max)
|
355
|
+
return f"{random_float:.{fraction_digits}f}"
|
356
|
+
|
357
|
+
def _generate_float(self, constraints: TypeConstraints):
|
358
|
+
decimal_constraints = replace(constraints, fraction_digits=2)
|
359
|
+
return self._generate_decimal(decimal_constraints)
|
360
|
+
|
361
|
+
def _generate_double(self, constraints: TypeConstraints):
|
362
|
+
decimal_constraints = replace(constraints, fraction_digits=2)
|
363
|
+
return self._generate_decimal(decimal_constraints)
|
364
|
+
|
365
|
+
def _generate_duration(self, constraints: TypeConstraints):
|
366
|
+
raise RuntimeError("not yet implemented")
|
367
|
+
|
368
|
+
# noinspection PyUnusedLocal
|
369
|
+
def _generate_datetime(self, constraints: TypeConstraints):
|
370
|
+
random_datetime = self.randomizer.random_datetime()
|
371
|
+
formatted = random_datetime.isoformat()
|
372
|
+
return formatted
|
373
|
+
|
374
|
+
# noinspection PyUnusedLocal
|
375
|
+
def _generate_date(self, constraints: TypeConstraints):
|
376
|
+
random_date = self.randomizer.random_date()
|
377
|
+
formatted = random_date.isoformat()
|
378
|
+
return formatted
|
379
|
+
|
380
|
+
# noinspection PyUnusedLocal
|
381
|
+
def _generate_time(self, constraints: TypeConstraints):
|
382
|
+
random_time = self.randomizer.random_time()
|
383
|
+
formatted = random_time.isoformat()
|
384
|
+
return formatted
|
385
|
+
|
386
|
+
# noinspection PyUnusedLocal
|
387
|
+
def _generate_gyearmonth(self, constraints: TypeConstraints):
|
388
|
+
random_date = self.randomizer.random_date()
|
389
|
+
formatted = random_date.strftime('%Y-%m')
|
390
|
+
return formatted
|
391
|
+
|
392
|
+
# noinspection PyUnusedLocal
|
393
|
+
def _generate_gyear(self, constraints: TypeConstraints):
|
394
|
+
return str(self.randomizer.integer(2000, 2050))
|
395
|
+
|
396
|
+
# noinspection PyUnusedLocal
|
397
|
+
def _generate_gmonthday(self, constraints: TypeConstraints):
|
398
|
+
random_date = self.randomizer.random_date()
|
399
|
+
formatted = random_date.strftime('--%m-%d')
|
400
|
+
return formatted
|
401
|
+
|
402
|
+
# noinspection PyUnusedLocal
|
403
|
+
def _generate_gday(self, constraints: TypeConstraints):
|
404
|
+
random_date = self.randomizer.random_date()
|
405
|
+
formatted = random_date.strftime('---%d')
|
406
|
+
return formatted
|
407
|
+
|
408
|
+
# noinspection PyUnusedLocal
|
409
|
+
def _generate_gmonth(self, constraints: TypeConstraints):
|
410
|
+
random_date = self.randomizer.random_date()
|
411
|
+
formatted = random_date.strftime('--%m--')
|
412
|
+
return formatted
|
413
|
+
|
414
|
+
def _generate_hex_binary(self, constraints: TypeConstraints):
|
415
|
+
return self.randomizer.hex_string(constraints.min_length, constraints.max_length)
|
416
|
+
|
417
|
+
# noinspection PyUnusedLocal
|
418
|
+
def _generate_base64_binary(self, constraints: TypeConstraints):
|
419
|
+
raise RuntimeError("not yet implemented")
|
420
|
+
|
421
|
+
# noinspection PyUnusedLocal
|
422
|
+
def _generate_any_uri(self, constraints: TypeConstraints):
|
423
|
+
raise RuntimeError("not yet implemented")
|
424
|
+
|
425
|
+
# noinspection PyUnusedLocal
|
426
|
+
def _generate_qname(self, constraints: TypeConstraints):
|
427
|
+
raise RuntimeError("not yet implemented")
|
428
|
+
|
429
|
+
# noinspection PyUnusedLocal
|
430
|
+
def _generate_notation(self, constraints: TypeConstraints):
|
431
|
+
raise RuntimeError("not yet implemented")
|
432
|
+
|
433
|
+
def _generate_byte(self, constraints: TypeConstraints):
|
434
|
+
constraints = replace(constraints, fraction_digits=0)
|
435
|
+
return self._generate_decimal(constraints)
|
436
|
+
|
437
|
+
def _generate_short(self, constraints: TypeConstraints):
|
438
|
+
constraints = replace(constraints, fraction_digits=0)
|
439
|
+
return self._generate_decimal(constraints)
|
440
|
+
|
441
|
+
def _generate_int(self, constraints: TypeConstraints):
|
442
|
+
constraints = replace(constraints, fraction_digits=0)
|
443
|
+
return self._generate_decimal(constraints)
|
444
|
+
|
445
|
+
def _generate_integer(self, constraints: TypeConstraints):
|
446
|
+
min_value = constraints.min_value if constraints.min_value is not None else -2147483648
|
447
|
+
max_value = constraints.max_value if constraints.max_value is not None else 2147483647
|
448
|
+
constraints = replace(constraints, min_value=min_value, max_value=max_value, fraction_digits=0)
|
449
|
+
return self._generate_decimal(constraints)
|
450
|
+
|
451
|
+
def _generate_long(self, constraints: TypeConstraints):
|
452
|
+
constraints = replace(constraints, fraction_digits=0)
|
453
|
+
return self._generate_decimal(constraints)
|
454
|
+
|
455
|
+
def _generate_unsigned_byte(self, constraints: TypeConstraints):
|
456
|
+
constraints = replace(constraints, fraction_digits=0)
|
457
|
+
return self._generate_decimal(constraints)
|
458
|
+
|
459
|
+
def _generate_unsigned_short(self, constraints: TypeConstraints):
|
460
|
+
constraints = replace(constraints, fraction_digits=0)
|
461
|
+
return self._generate_decimal(constraints)
|
462
|
+
|
463
|
+
def _generate_unsigned_int(self, constraints: TypeConstraints):
|
464
|
+
constraints = replace(constraints, fraction_digits=0)
|
465
|
+
return self._generate_decimal(constraints)
|
466
|
+
|
467
|
+
def _generate_unsigned_long(self, constraints: TypeConstraints):
|
468
|
+
constraints = replace(constraints, fraction_digits=0)
|
469
|
+
return self._generate_decimal(constraints)
|
470
|
+
|
471
|
+
def _generate_positive_integer(self, constraints: TypeConstraints):
|
472
|
+
min_value = constraints.min_value if constraints.min_value is not None else 1
|
473
|
+
max_value = constraints.max_value if constraints.max_value is not None else 2 ** 31 - 1
|
474
|
+
constraints = replace(constraints, min_value=min_value, max_value=max_value, fraction_digits=0)
|
475
|
+
return self._generate_decimal(constraints)
|
476
|
+
|
477
|
+
def _generate_negative_integer(self, constraints: TypeConstraints):
|
478
|
+
min_value = constraints.min_value if constraints.min_value is not None else -2 ** 31
|
479
|
+
max_value = constraints.max_value if constraints.max_value is not None else -1
|
480
|
+
constraints = replace(constraints, min_value=min_value, max_value=max_value, fraction_digits=0)
|
481
|
+
return self._generate_decimal(constraints)
|
482
|
+
|
483
|
+
def _generate_non_positive_integer(self, constraints: TypeConstraints):
|
484
|
+
min_value = constraints.min_value if constraints.min_value is not None else -2 ** 31
|
485
|
+
max_value = constraints.max_value if constraints.max_value is not None else 0
|
486
|
+
constraints = replace(constraints, min_value=min_value, max_value=max_value, fraction_digits=0)
|
487
|
+
return self._generate_decimal(constraints)
|
488
|
+
|
489
|
+
def _generate_non_negative_integer(self, constraints: TypeConstraints):
|
490
|
+
min_value = constraints.min_value if constraints.min_value is not None else 0
|
491
|
+
max_value = constraints.max_value if constraints.max_value is not None else 2 ** 31 - 1
|
492
|
+
constraints = replace(constraints, min_value=min_value, max_value=max_value, fraction_digits=0)
|
493
|
+
return self._generate_decimal(constraints)
|
494
|
+
|
495
|
+
|
496
|
+
def extract_type_constraints(xsd_type, local_config: GeneratorConfig) -> TypeConstraints:
|
497
|
+
min_length = getattr(xsd_type, 'min_length', None)
|
498
|
+
max_length = getattr(xsd_type, 'max_length', None)
|
499
|
+
min_value = getattr(xsd_type, 'min_value', None)
|
500
|
+
max_value = getattr(xsd_type, 'max_value', None)
|
501
|
+
total_digits = None
|
502
|
+
fraction_digits = None
|
503
|
+
patterns = getattr(xsd_type, 'patterns', None)
|
504
|
+
validators = getattr(xsd_type, 'validators', None)
|
505
|
+
for validator in validators:
|
506
|
+
if isinstance(validator, XsdMinExclusiveFacet):
|
507
|
+
min_value = validator.value
|
508
|
+
elif isinstance(validator, XsdMinInclusiveFacet):
|
509
|
+
min_value = validator.value
|
510
|
+
elif isinstance(validator, XsdMaxExclusiveFacet):
|
511
|
+
max_value = validator.value
|
512
|
+
elif isinstance(validator, XsdMaxInclusiveFacet):
|
513
|
+
max_value = validator.value
|
514
|
+
elif isinstance(validator, XsdLengthFacet):
|
515
|
+
min_length = validator.value
|
516
|
+
max_length = validator.value
|
517
|
+
elif isinstance(validator, XsdMinLengthFacet):
|
518
|
+
min_length = validator.value
|
519
|
+
elif isinstance(validator, XsdMaxLengthFacet):
|
520
|
+
max_length = validator.value
|
521
|
+
elif isinstance(validator, XsdTotalDigitsFacet):
|
522
|
+
total_digits = validator.value
|
523
|
+
elif isinstance(validator, XsdFractionDigitsFacet):
|
524
|
+
fraction_digits = validator.value
|
525
|
+
elif isinstance(validator, XsdEnumerationFacets):
|
526
|
+
pass
|
527
|
+
elif callable(validator):
|
528
|
+
pass
|
529
|
+
else:
|
530
|
+
raise RuntimeError(f"Unhandled validator: {validator}")
|
531
|
+
|
532
|
+
if isinstance(min_value, Decimal):
|
533
|
+
min_value = float(min_value)
|
534
|
+
if isinstance(max_value, Decimal):
|
535
|
+
max_value = float(max_value)
|
536
|
+
|
537
|
+
rand_config = local_config.randomization
|
538
|
+
|
539
|
+
logger.debug('bounds before adjust: min_length: %4s; max_length: %4s', min_length, max_length)
|
540
|
+
min_length, max_length = merge_constraints(
|
541
|
+
schema_min=min_length,
|
542
|
+
schema_max=max_length,
|
543
|
+
config_min=rand_config.min_length,
|
544
|
+
config_max=rand_config.max_length
|
545
|
+
)
|
546
|
+
logger.debug('bounds after adjust: min_length: %4s; max_length: %4s', min_length, max_length)
|
547
|
+
|
548
|
+
return TypeConstraints(
|
549
|
+
min_length=min_length,
|
550
|
+
max_length=max_length,
|
551
|
+
min_value=min_value,
|
552
|
+
max_value=max_value,
|
553
|
+
total_digits=total_digits,
|
554
|
+
fraction_digits=fraction_digits,
|
555
|
+
patterns=patterns,
|
556
|
+
rand_config=rand_config
|
557
|
+
)
|
558
|
+
|
559
|
+
|
560
|
+
def merge_constraints(digit_min=None, digit_max=None, schema_min=None, schema_max=None, config_min=None, config_max=None):
|
561
|
+
logger.debug(
|
562
|
+
"merge numeric constraints: "
|
563
|
+
"digit_min: %s, digit_max: %s, schema_min: %s, schema_max: %s, config_min: %s, config_max: %s",
|
564
|
+
digit_min, digit_max, schema_min, schema_max, config_min, config_max)
|
565
|
+
|
566
|
+
# За основу берем цифровые ограничения (они самые нестрогие)
|
567
|
+
effective_min, effective_max = digit_min, digit_max
|
568
|
+
|
569
|
+
# Применяем схемные ограничения
|
570
|
+
if schema_min is not None:
|
571
|
+
effective_min = max(effective_min, schema_min) if effective_min is not None else schema_min
|
572
|
+
if schema_max is not None:
|
573
|
+
effective_max = min(effective_max, schema_max) if effective_max is not None else schema_max
|
574
|
+
|
575
|
+
# Применяем конфигурационные ограничения с проверкой на конфликт
|
576
|
+
if config_min is not None:
|
577
|
+
if effective_max is not None and config_min > effective_max:
|
578
|
+
logger.warning("can't apply bound from configuration: config_min (%s) > effective_max (%s)",
|
579
|
+
config_min, effective_max)
|
580
|
+
else:
|
581
|
+
effective_min = max(effective_min, config_min) if effective_min is not None else config_min
|
582
|
+
|
583
|
+
if config_max is not None:
|
584
|
+
if effective_min is not None and config_max < effective_min:
|
585
|
+
logger.warning("can't apply bound from configuration: config_max (%s) < effective_min (%s)",
|
586
|
+
config_max, effective_min)
|
587
|
+
else:
|
588
|
+
effective_max = min(effective_max, config_max) if effective_max is not None else config_max
|
589
|
+
|
590
|
+
# Проверяем на конфликт
|
591
|
+
if effective_min is not None and effective_max is not None and effective_min > effective_max:
|
592
|
+
logger.warning("constrains conflict: effective_min (%s) > effective_max (%s). Swap values.",
|
593
|
+
effective_min, effective_max)
|
594
|
+
effective_min, effective_max = effective_max, effective_min
|
595
|
+
|
596
|
+
return effective_min, effective_max
|
@@ -4,7 +4,6 @@ import re
|
|
4
4
|
import string
|
5
5
|
import sys
|
6
6
|
from datetime import datetime, date, time, timedelta
|
7
|
-
from decimal import Decimal
|
8
7
|
|
9
8
|
import rstr
|
10
9
|
from faker import Faker
|
@@ -42,10 +41,6 @@ class Randomizer:
|
|
42
41
|
return self._rnd.randint(min_value, max_value)
|
43
42
|
|
44
43
|
def float(self, min_value, max_value):
|
45
|
-
if isinstance(min_value, Decimal):
|
46
|
-
min_value = float(min_value)
|
47
|
-
if isinstance(max_value, Decimal):
|
48
|
-
max_value = float(max_value)
|
49
44
|
return self._rnd.uniform(min_value, max_value)
|
50
45
|
|
51
46
|
def ascii_string(self, min_length, max_length):
|
@@ -58,6 +53,16 @@ class Randomizer:
|
|
58
53
|
letters = string.ascii_lowercase
|
59
54
|
return ''.join(self._rnd.choice(letters) for _ in range(length)).capitalize()
|
60
55
|
|
56
|
+
def hex_string(self, min_length, max_length):
|
57
|
+
if min_length is None:
|
58
|
+
min_length = 1
|
59
|
+
if max_length is None:
|
60
|
+
max_length = 20
|
61
|
+
|
62
|
+
length = self._rnd.randint(min_length, max_length)
|
63
|
+
circumflexes = ''.join('^' for _ in range(length))
|
64
|
+
return self._fake.hexify(text=circumflexes, upper=True)
|
65
|
+
|
61
66
|
def random_date(self, start_date: str = '1990-01-01', end_date: str = '2025-12-31') -> date:
|
62
67
|
start = date.fromisoformat(start_date)
|
63
68
|
end = date.fromisoformat(end_date)
|
@@ -138,3 +143,6 @@ class Randomizer:
|
|
138
143
|
def snils_formatted(self):
|
139
144
|
snils = self._fake.snils()
|
140
145
|
return f"{snils[:3]}-{snils[3:6]}-{snils[6:9]} {snils[9:]}"
|
146
|
+
|
147
|
+
def email(self):
|
148
|
+
return self._fake.email()
|
@@ -46,6 +46,7 @@ class Substitutor:
|
|
46
46
|
'ogrn_fl': lambda args: self.randomizer.ogrn_fl(),
|
47
47
|
'kpp': lambda args: self.randomizer.kpp(),
|
48
48
|
'snils_formatted': lambda args: self.randomizer.snils_formatted(),
|
49
|
+
'email': lambda args: self.randomizer.email(),
|
49
50
|
}
|
50
51
|
|
51
52
|
def reset_context(self, xsd_filename, config_local):
|
@@ -61,7 +62,7 @@ class Substitutor:
|
|
61
62
|
resolved_value = self._process_expression(output_filename)
|
62
63
|
self._local_context['output_filename'] = resolved_value
|
63
64
|
|
64
|
-
logger.debug('
|
65
|
+
logger.debug('reset local context...')
|
65
66
|
logger.debug('local_context["source_filename"] = %s', xsd_filename)
|
66
67
|
logger.debug('local_context["source_extracted"] = %s (extracted with regexp %s)', source_extracted, source_filename)
|
67
68
|
logger.debug('local_context["output_filename"] = %s', resolved_value)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: xmlgenerator
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.5.0
|
4
4
|
Summary: Generates XML documents from XSD schemas
|
5
5
|
Home-page: https://github.com/lexakimov/xmlgenerator
|
6
6
|
Author: Alexey Akimov
|
@@ -173,6 +173,8 @@ global:
|
|
173
173
|
# Probability of adding optional elements (0.0-1.0)
|
174
174
|
# Default value: 0.5
|
175
175
|
probability: 1
|
176
|
+
# Limit for the minimal number of elements
|
177
|
+
min_occurs: 0
|
176
178
|
# Limit for the maximum number of elements
|
177
179
|
max_occurs: 5
|
178
180
|
# Minimum string length
|
@@ -268,6 +270,7 @@ In the `value_override` sections, you can specify either a string value or speci
|
|
268
270
|
| `ogrn_fl` | Primary State Registration Number (Physical Person) |
|
269
271
|
| `kpp` | Reason Code for Registration |
|
270
272
|
| `snils_formatted` | SNILS (Personal Insurance Account Number) in the format `123-456-789 90` |
|
273
|
+
| `email` | Random email address |
|
271
274
|
|
272
275
|
**Configuration Examples:**
|
273
276
|
|
@@ -1,456 +0,0 @@
|
|
1
|
-
import logging
|
2
|
-
|
3
|
-
import xmlschema
|
4
|
-
from lxml import etree
|
5
|
-
from xmlschema.validators import XsdComplexType, XsdAtomicRestriction, XsdTotalDigitsFacet, XsdElement, \
|
6
|
-
XsdGroup, XsdFractionDigitsFacet, XsdLengthFacet, XsdMaxLengthFacet, XsdMinExclusiveFacet, XsdMinInclusiveFacet, \
|
7
|
-
XsdMinLengthFacet, XsdAnyElement, XsdAtomicBuiltin, XsdEnumerationFacets, XsdMaxExclusiveFacet, XsdMaxInclusiveFacet
|
8
|
-
|
9
|
-
from xmlgenerator.configuration import GeneratorConfig
|
10
|
-
from xmlgenerator.randomization import Randomizer
|
11
|
-
from xmlgenerator.substitution import Substitutor
|
12
|
-
|
13
|
-
logger = logging.getLogger(__name__)
|
14
|
-
|
15
|
-
|
16
|
-
class XmlGenerator:
|
17
|
-
def __init__(self, randomizer: Randomizer, substitutor: Substitutor):
|
18
|
-
self.randomizer = randomizer
|
19
|
-
self.substitutor = substitutor
|
20
|
-
|
21
|
-
def generate_xml(self, xsd_schema: xmlschema.XMLSchema, local_config: GeneratorConfig) -> etree.Element:
|
22
|
-
ns_map = {None if k == '' else k: v for k, v in xsd_schema.namespaces.items() if v != ''}
|
23
|
-
xsd_root_element = xsd_schema.root_elements[0]
|
24
|
-
xml_root_element = etree.Element(xsd_root_element.name, nsmap=ns_map)
|
25
|
-
xml_tree = etree.ElementTree(xml_root_element)
|
26
|
-
self._add_elements(xml_tree, xml_root_element, xsd_root_element, local_config)
|
27
|
-
return xml_root_element
|
28
|
-
|
29
|
-
def _add_elements(self, xml_tree, xml_element: etree.Element, xsd_element, local_config: GeneratorConfig) -> None:
|
30
|
-
# Process child elements --------------------------------------------------------------------------------------
|
31
|
-
if isinstance(xsd_element, XsdElement):
|
32
|
-
element_xpath = xml_tree.getpath(xml_element)
|
33
|
-
logger.debug('element: %s [created]', element_xpath)
|
34
|
-
|
35
|
-
xsd_element_type = getattr(xsd_element, 'type', None)
|
36
|
-
|
37
|
-
# Add attributes if they are
|
38
|
-
attributes = getattr(xsd_element, 'attributes', dict())
|
39
|
-
if len(attributes) > 0 and xsd_element_type.local_name != 'anyType':
|
40
|
-
for attr_name, attr in attributes.items():
|
41
|
-
logger.debug('element: %s; attribute "%s" [processing]', element_xpath, attr_name)
|
42
|
-
use = attr.use # optional | required | prohibited
|
43
|
-
if use == 'prohibited':
|
44
|
-
logger.debug('element: %s; attribute: "%s" [skipped]', element_xpath, attr_name)
|
45
|
-
continue
|
46
|
-
elif use == 'optional':
|
47
|
-
if self.randomizer.random() > local_config.randomization.probability:
|
48
|
-
logger.debug('element: %s; attribute: "%s" [skipped]', element_xpath, attr_name)
|
49
|
-
continue
|
50
|
-
|
51
|
-
attr_value = self._generate_value(attr.type, attr_name, local_config)
|
52
|
-
if attr_value is not None:
|
53
|
-
xml_element.set(attr_name, str(attr_value))
|
54
|
-
logger.debug('element: %s; attribute: "%s" = "%s"', element_xpath, attr_name, attr_value)
|
55
|
-
|
56
|
-
if isinstance(xsd_element_type, XsdAtomicBuiltin):
|
57
|
-
text = self._generate_value(xsd_element_type, xsd_element.name, local_config)
|
58
|
-
xml_element.text = text
|
59
|
-
logger.debug('element: %s = "%s"', element_xpath, text)
|
60
|
-
return
|
61
|
-
elif isinstance(xsd_element_type, XsdAtomicRestriction):
|
62
|
-
text = self._generate_value(xsd_element_type, xsd_element.name, local_config)
|
63
|
-
xml_element.text = text
|
64
|
-
logger.debug('element: %s = "%s"', element_xpath, text)
|
65
|
-
return
|
66
|
-
elif isinstance(xsd_element_type, XsdComplexType):
|
67
|
-
xsd_element_type_content = xsd_element_type.content
|
68
|
-
if isinstance(xsd_element_type_content, XsdGroup):
|
69
|
-
self._add_elements(xml_tree, xml_element, xsd_element_type_content, local_config)
|
70
|
-
else:
|
71
|
-
raise RuntimeError()
|
72
|
-
else:
|
73
|
-
raise RuntimeError()
|
74
|
-
|
75
|
-
elif isinstance(xsd_element, XsdGroup):
|
76
|
-
model = xsd_element.model
|
77
|
-
|
78
|
-
group_min_occurs = getattr(xsd_element, 'min_occurs', None)
|
79
|
-
group_max_occurs = getattr(xsd_element, 'max_occurs', None)
|
80
|
-
group_min_occurs = group_min_occurs if group_min_occurs is not None else 0 # TODO externalize
|
81
|
-
group_max_occurs = group_max_occurs if group_max_occurs is not None else 10 # TODO externalize
|
82
|
-
group_occurs = self.randomizer.integer(group_min_occurs, group_max_occurs)
|
83
|
-
|
84
|
-
if model == 'all':
|
85
|
-
for _ in range(group_occurs):
|
86
|
-
xsd_group_content = xsd_element.content
|
87
|
-
for xsd_child_element_type in xsd_group_content:
|
88
|
-
|
89
|
-
element_min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
|
90
|
-
element_max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
|
91
|
-
element_min_occurs = element_min_occurs if element_min_occurs is not None else 0 # TODO externalize
|
92
|
-
element_max_occurs = element_max_occurs if element_max_occurs is not None else 10 # TODO externalize
|
93
|
-
element_occurs = self.randomizer.integer(element_min_occurs, element_max_occurs)
|
94
|
-
|
95
|
-
for _ in range(element_occurs):
|
96
|
-
xml_child_element = etree.SubElement(xml_element, xsd_child_element_type.name)
|
97
|
-
self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
|
98
|
-
return
|
99
|
-
|
100
|
-
elif model == 'sequence':
|
101
|
-
for _ in range(group_occurs):
|
102
|
-
xsd_group_content = xsd_element.content
|
103
|
-
for xsd_child_element_type in xsd_group_content:
|
104
|
-
|
105
|
-
element_min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
|
106
|
-
element_max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
|
107
|
-
element_min_occurs = element_min_occurs if element_min_occurs is not None else 0 # TODO externalize
|
108
|
-
element_max_occurs = element_max_occurs if element_max_occurs is not None else 10 # TODO externalize
|
109
|
-
element_occurs = self.randomizer.integer(element_min_occurs, element_max_occurs)
|
110
|
-
|
111
|
-
if isinstance(xsd_child_element_type, XsdElement):
|
112
|
-
for _ in range(element_occurs):
|
113
|
-
xml_child_element = etree.SubElement(xml_element, xsd_child_element_type.name)
|
114
|
-
self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
|
115
|
-
|
116
|
-
elif isinstance(xsd_child_element_type, XsdGroup):
|
117
|
-
xml_child_element = xml_element
|
118
|
-
self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
|
119
|
-
|
120
|
-
elif isinstance(xsd_child_element_type, XsdAnyElement):
|
121
|
-
xml_child_element = etree.SubElement(xml_element, "Any")
|
122
|
-
self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
|
123
|
-
|
124
|
-
else:
|
125
|
-
raise RuntimeError(xsd_child_element_type)
|
126
|
-
return
|
127
|
-
|
128
|
-
elif model == 'choice':
|
129
|
-
for _ in range(group_occurs):
|
130
|
-
xsd_child_element_type = self.randomizer.any(xsd_element)
|
131
|
-
|
132
|
-
element_min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
|
133
|
-
element_max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
|
134
|
-
element_min_occurs = element_min_occurs if element_min_occurs is not None else 0 # TODO externalize
|
135
|
-
element_max_occurs = element_max_occurs if element_max_occurs is not None else 10 # TODO externalize
|
136
|
-
element_occurs = self.randomizer.integer(element_min_occurs, element_max_occurs)
|
137
|
-
|
138
|
-
for _ in range(element_occurs):
|
139
|
-
xml_child_element = etree.SubElement(xml_element, xsd_child_element_type.name)
|
140
|
-
self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
|
141
|
-
return
|
142
|
-
|
143
|
-
else:
|
144
|
-
raise RuntimeError()
|
145
|
-
|
146
|
-
elif isinstance(xsd_element, XsdAnyElement):
|
147
|
-
# для any не добавляем никаких дочерних тегов и атрибутов
|
148
|
-
pass
|
149
|
-
|
150
|
-
else:
|
151
|
-
raise RuntimeError()
|
152
|
-
|
153
|
-
def _generate_value(self, xsd_type, target_name, local_config: GeneratorConfig) -> str | None:
|
154
|
-
if xsd_type is None:
|
155
|
-
raise RuntimeError(f"xsd_type is None. Target name: {target_name}")
|
156
|
-
|
157
|
-
if isinstance(xsd_type, XsdComplexType):
|
158
|
-
return None
|
159
|
-
|
160
|
-
# -------------------------------------------------------------------------------------------------------------
|
161
|
-
# Ищем переопределение значения в конфигурации
|
162
|
-
value_override = local_config.value_override
|
163
|
-
is_found, overridden_value = self.substitutor.substitute_value(target_name, value_override.items())
|
164
|
-
if is_found:
|
165
|
-
logger.debug('value resolved: "%s"', overridden_value)
|
166
|
-
return overridden_value
|
167
|
-
|
168
|
-
# -------------------------------------------------------------------------------------------------------------
|
169
|
-
# If there is an enumeration, select a random value from it
|
170
|
-
enumeration = getattr(xsd_type, 'enumeration', None)
|
171
|
-
if enumeration is not None:
|
172
|
-
random_enum = self.randomizer.any(enumeration)
|
173
|
-
logger.debug('use random value from enumeration: "%s" %s', random_enum, enumeration)
|
174
|
-
return str(random_enum)
|
175
|
-
|
176
|
-
# -------------------------------------------------------------------------------------------------------------
|
177
|
-
# Генерируем значения для стандартных типов и типов с ограничениями
|
178
|
-
if isinstance(xsd_type, XsdAtomicBuiltin) or isinstance(xsd_type, XsdAtomicRestriction):
|
179
|
-
# Выясняем ограничения
|
180
|
-
min_length = getattr(xsd_type, 'min_length', None) # None | int
|
181
|
-
max_length = getattr(xsd_type, 'max_length', None) # None | int
|
182
|
-
|
183
|
-
min_value = getattr(xsd_type, 'min_value', None) # None | int
|
184
|
-
max_value = getattr(xsd_type, 'max_value', None) # None
|
185
|
-
|
186
|
-
total_digits = None
|
187
|
-
fraction_digits = None
|
188
|
-
patterns = getattr(xsd_type, 'patterns', None)
|
189
|
-
|
190
|
-
validators = getattr(xsd_type, 'validators', None)
|
191
|
-
for validator in validators:
|
192
|
-
if isinstance(validator, XsdMinExclusiveFacet):
|
193
|
-
min_value = validator.value
|
194
|
-
elif isinstance(validator, XsdMinInclusiveFacet):
|
195
|
-
min_value = validator.value
|
196
|
-
elif isinstance(validator, XsdMaxExclusiveFacet):
|
197
|
-
max_value = validator.value
|
198
|
-
elif isinstance(validator, XsdMaxInclusiveFacet):
|
199
|
-
max_value = validator.value
|
200
|
-
elif isinstance(validator, XsdLengthFacet):
|
201
|
-
min_length = validator.value
|
202
|
-
max_length = validator.value
|
203
|
-
elif isinstance(validator, XsdMinLengthFacet):
|
204
|
-
min_length = validator.value
|
205
|
-
elif isinstance(validator, XsdMaxLengthFacet):
|
206
|
-
max_length = validator.value
|
207
|
-
elif isinstance(validator, XsdTotalDigitsFacet):
|
208
|
-
total_digits = validator.value
|
209
|
-
elif isinstance(validator, XsdFractionDigitsFacet):
|
210
|
-
fraction_digits = validator.value
|
211
|
-
elif isinstance(validator, XsdEnumerationFacets):
|
212
|
-
pass
|
213
|
-
elif callable(validator):
|
214
|
-
pass
|
215
|
-
else:
|
216
|
-
raise RuntimeError(f"Unhandled validator: {validator}")
|
217
|
-
|
218
|
-
rand_config = local_config.randomization
|
219
|
-
|
220
|
-
logger.debug(
|
221
|
-
'restrictions before override: min_length: %4s; max_length: %4s; min_value: %4s; max_value: %4s',
|
222
|
-
min_length, max_length, min_value, max_value
|
223
|
-
)
|
224
|
-
|
225
|
-
min_length, max_length = calculate_bounds_1(
|
226
|
-
min_length, max_length, rand_config.min_length, rand_config.max_length
|
227
|
-
)
|
228
|
-
|
229
|
-
min_value, max_value = calculate_bounds_1(
|
230
|
-
min_value, max_value, rand_config.min_inclusive, rand_config.max_inclusive
|
231
|
-
)
|
232
|
-
|
233
|
-
logger.debug(
|
234
|
-
'restrictions after override: min_length: %4s; max_length: %4s; min_value: %4s; max_value: %4s',
|
235
|
-
min_length, max_length, min_value, max_value
|
236
|
-
)
|
237
|
-
|
238
|
-
generated_value = self._generate_value_by_type(
|
239
|
-
xsd_type, patterns,
|
240
|
-
min_length, max_length,
|
241
|
-
min_value, max_value,
|
242
|
-
total_digits, fraction_digits
|
243
|
-
)
|
244
|
-
logger.debug('value generated: "%s"', generated_value)
|
245
|
-
return generated_value
|
246
|
-
|
247
|
-
# -------------------------------------------------------------------------------------------------------------
|
248
|
-
# Проверяем базовый тип
|
249
|
-
base_type = getattr(xsd_type, 'base_type', None)
|
250
|
-
|
251
|
-
# невозможный кейс (только если попался комплексный тип)
|
252
|
-
if base_type is None:
|
253
|
-
raise RuntimeError(f"base_type is None. Target name: {target_name}")
|
254
|
-
|
255
|
-
raise RuntimeError(f"Can't generate value - unhandled type. Target name: {target_name}")
|
256
|
-
|
257
|
-
def _generate_value_by_type(self, xsd_type, patterns, min_length, max_length, min_value, max_value,
|
258
|
-
total_digits, fraction_digits) -> str | None:
|
259
|
-
|
260
|
-
type_id = xsd_type.id
|
261
|
-
base_type = xsd_type.base_type
|
262
|
-
if not type_id:
|
263
|
-
type_id = base_type.id
|
264
|
-
if not type_id:
|
265
|
-
type_id = xsd_type.root_type.id
|
266
|
-
|
267
|
-
logger.debug('generate value for type: "%s"', type_id)
|
268
|
-
|
269
|
-
match type_id:
|
270
|
-
case 'string':
|
271
|
-
return self._generate_string(patterns, min_length, max_length)
|
272
|
-
case 'boolean':
|
273
|
-
return self._generate_boolean()
|
274
|
-
case 'integer':
|
275
|
-
return self._generate_integer(total_digits, min_value, max_value)
|
276
|
-
case 'decimal':
|
277
|
-
return self._generate_decimal(total_digits, fraction_digits, min_value, max_value)
|
278
|
-
case 'float':
|
279
|
-
return self._generate_float(min_value, max_value)
|
280
|
-
case 'double':
|
281
|
-
return self._generate_double(min_value, max_value)
|
282
|
-
case 'duration':
|
283
|
-
return self._generate_duration()
|
284
|
-
case 'dateTime':
|
285
|
-
return self._generate_datetime()
|
286
|
-
case 'date':
|
287
|
-
return self._generate_date()
|
288
|
-
case 'time':
|
289
|
-
return self._generate_time()
|
290
|
-
case 'gYearMonth':
|
291
|
-
return self._generate_gyearmonth()
|
292
|
-
case 'gYear':
|
293
|
-
return self._generate_gyear()
|
294
|
-
case 'gMonthDay':
|
295
|
-
return self._generate_gmonthday()
|
296
|
-
case 'gDay':
|
297
|
-
return self._generate_gday()
|
298
|
-
case 'gMonth':
|
299
|
-
return self._generate_gmonth()
|
300
|
-
case 'hexBinary':
|
301
|
-
return self._generate_hex_binary()
|
302
|
-
case 'base64Binary':
|
303
|
-
return self._generate_base64_binary()
|
304
|
-
case 'anyURI':
|
305
|
-
return self._generate_any_uri()
|
306
|
-
case 'QName':
|
307
|
-
return self._generate_qname()
|
308
|
-
case 'NOTATION':
|
309
|
-
return self._generate_notation()
|
310
|
-
case _:
|
311
|
-
raise RuntimeError(type_id)
|
312
|
-
|
313
|
-
def _generate_string(self, patterns, min_length, max_length):
|
314
|
-
if patterns is not None:
|
315
|
-
# Генерация строки по regex
|
316
|
-
random_enum = self.randomizer.any(patterns)
|
317
|
-
random_pattern = random_enum.attrib['value']
|
318
|
-
return self.randomizer.regex(random_pattern)
|
319
|
-
|
320
|
-
# Иначе генерируем случайную строку
|
321
|
-
return self.randomizer.ascii_string(min_length, max_length)
|
322
|
-
|
323
|
-
def _generate_boolean(self):
|
324
|
-
return self.randomizer.any(['true', 'false'])
|
325
|
-
|
326
|
-
def _generate_integer(self, total_digits, min_value, max_value):
|
327
|
-
if total_digits:
|
328
|
-
min_value = 10 ** (total_digits - 1)
|
329
|
-
max_value = (10 ** total_digits) - 1
|
330
|
-
rnd_int = self.randomizer.integer(min_value, max_value)
|
331
|
-
return str(rnd_int)
|
332
|
-
|
333
|
-
def _generate_decimal(self, total_digits, fraction_digits, min_value, max_value):
|
334
|
-
if fraction_digits is None:
|
335
|
-
fraction_digits = self.randomizer.integer(1, 3)
|
336
|
-
|
337
|
-
if fraction_digits > 4:
|
338
|
-
fraction_digits = self.randomizer.integer(1, 4)
|
339
|
-
|
340
|
-
if total_digits is None:
|
341
|
-
total_digits = 10 + fraction_digits
|
342
|
-
|
343
|
-
if total_digits > 10:
|
344
|
-
total_digits = self.randomizer.integer(6, total_digits - 2)
|
345
|
-
|
346
|
-
integer_digits = total_digits - fraction_digits
|
347
|
-
|
348
|
-
# negative
|
349
|
-
min_value_fact = -(10 ** integer_digits - 1)
|
350
|
-
|
351
|
-
# positive
|
352
|
-
max_value_fact = 10 ** integer_digits - 1
|
353
|
-
|
354
|
-
min_value_fact, max_value_fact = calculate_bounds_2(min_value_fact, max_value_fact, min_value, max_value)
|
355
|
-
|
356
|
-
random_float = self.randomizer.float(min_value_fact, max_value_fact)
|
357
|
-
return f"{random_float:.{fraction_digits}f}"
|
358
|
-
|
359
|
-
def _generate_float(self, min_value, max_value):
|
360
|
-
return self._generate_double(min_value, max_value)
|
361
|
-
|
362
|
-
def _generate_double(self, min_value, max_value):
|
363
|
-
return self._generate_decimal(None, 2, min_value, max_value)
|
364
|
-
|
365
|
-
def _generate_duration(self):
|
366
|
-
raise RuntimeError("not yet implemented")
|
367
|
-
|
368
|
-
def _generate_datetime(self):
|
369
|
-
random_datetime = self.randomizer.random_datetime()
|
370
|
-
formatted = random_datetime.isoformat()
|
371
|
-
return formatted
|
372
|
-
|
373
|
-
def _generate_date(self):
|
374
|
-
random_date = self.randomizer.random_date()
|
375
|
-
formatted = random_date.isoformat()
|
376
|
-
return formatted
|
377
|
-
|
378
|
-
def _generate_time(self):
|
379
|
-
random_time = self.randomizer.random_time()
|
380
|
-
formatted = random_time.isoformat()
|
381
|
-
return formatted
|
382
|
-
|
383
|
-
def _generate_gyearmonth(self):
|
384
|
-
random_date = self.randomizer.random_date()
|
385
|
-
formatted = random_date.strftime('%Y-%m')
|
386
|
-
return formatted
|
387
|
-
|
388
|
-
def _generate_gyear(self):
|
389
|
-
return str(self.randomizer.integer(2000, 2050))
|
390
|
-
|
391
|
-
def _generate_gmonthday(self):
|
392
|
-
random_date = self.randomizer.random_date()
|
393
|
-
formatted = random_date.strftime('--%m-%d')
|
394
|
-
return formatted
|
395
|
-
|
396
|
-
def _generate_gday(self):
|
397
|
-
random_date = self.randomizer.random_date()
|
398
|
-
formatted = random_date.strftime('---%d')
|
399
|
-
return formatted
|
400
|
-
|
401
|
-
def _generate_gmonth(self):
|
402
|
-
random_date = self.randomizer.random_date()
|
403
|
-
formatted = random_date.strftime('--%m--')
|
404
|
-
return formatted
|
405
|
-
|
406
|
-
def _generate_hex_binary(self):
|
407
|
-
raise RuntimeError("not yet implemented")
|
408
|
-
|
409
|
-
def _generate_base64_binary(self):
|
410
|
-
raise RuntimeError("not yet implemented")
|
411
|
-
|
412
|
-
def _generate_any_uri(self):
|
413
|
-
raise RuntimeError("not yet implemented")
|
414
|
-
|
415
|
-
def _generate_qname(self):
|
416
|
-
raise RuntimeError("not yet implemented")
|
417
|
-
|
418
|
-
def _generate_notation(self):
|
419
|
-
raise RuntimeError("not yet implemented")
|
420
|
-
|
421
|
-
|
422
|
-
def calculate_bounds_1(fact_min, fact_max, config_min, config_max):
|
423
|
-
if config_min:
|
424
|
-
if fact_min is None:
|
425
|
-
fact_min = config_min
|
426
|
-
else:
|
427
|
-
new_min = max(fact_min, config_min)
|
428
|
-
if fact_max and new_min <= fact_max:
|
429
|
-
fact_min = new_min
|
430
|
-
|
431
|
-
if config_max:
|
432
|
-
if fact_max is None:
|
433
|
-
fact_max = config_max
|
434
|
-
else:
|
435
|
-
new_max = min(fact_max, config_max)
|
436
|
-
if new_max >= fact_min:
|
437
|
-
fact_max = new_max
|
438
|
-
|
439
|
-
if fact_max and fact_min and fact_max < fact_min:
|
440
|
-
fact_max = fact_min = min(fact_max, fact_min)
|
441
|
-
|
442
|
-
return fact_min, fact_max
|
443
|
-
|
444
|
-
|
445
|
-
def calculate_bounds_2(fact_min, fact_max, config_min, config_max):
|
446
|
-
if config_min is not None:
|
447
|
-
new_min = max(fact_min, config_min)
|
448
|
-
if fact_max and new_min <= fact_max:
|
449
|
-
fact_min = new_min
|
450
|
-
|
451
|
-
if config_max is not None:
|
452
|
-
new_max = min(fact_max, config_max)
|
453
|
-
if new_max >= fact_min:
|
454
|
-
fact_max = new_max
|
455
|
-
|
456
|
-
return fact_min, fact_max
|
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
|
File without changes
|