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.
Files changed (20) hide show
  1. {xmlgenerator-0.3.0/xmlgenerator.egg-info → xmlgenerator-0.5.0}/PKG-INFO +4 -1
  2. {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/README.md +3 -0
  3. {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/setup.py +1 -1
  4. {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator/arguments.py +2 -1
  5. {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator/bootstrap.py +1 -1
  6. xmlgenerator-0.5.0/xmlgenerator/generator.py +596 -0
  7. {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator/randomization.py +13 -5
  8. {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator/substitution.py +2 -1
  9. {xmlgenerator-0.3.0 → xmlgenerator-0.5.0/xmlgenerator.egg-info}/PKG-INFO +4 -1
  10. xmlgenerator-0.3.0/xmlgenerator/generator.py +0 -456
  11. {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/LICENSE +0 -0
  12. {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/setup.cfg +0 -0
  13. {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator/__init__.py +0 -0
  14. {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator/configuration.py +0 -0
  15. {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator/validation.py +0 -0
  16. {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator.egg-info/SOURCES.txt +0 -0
  17. {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator.egg-info/dependency_links.txt +0 -0
  18. {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator.egg-info/entry_points.txt +0 -0
  19. {xmlgenerator-0.3.0 → xmlgenerator-0.5.0}/xmlgenerator.egg-info/requires.txt +0 -0
  20. {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.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
 
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='xmlgenerator',
5
- version='0.3.0',
5
+ version='0.5.0',
6
6
  packages=find_packages(exclude=("tests", "tests.*")),
7
7
  entry_points={
8
8
  'console_scripts': [
@@ -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('local_context reset')
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.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