xmlgenerator 0.2.1__tar.gz → 0.4.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 (21) hide show
  1. {xmlgenerator-0.2.1/xmlgenerator.egg-info → xmlgenerator-0.4.0}/PKG-INFO +6 -3
  2. {xmlgenerator-0.2.1 → xmlgenerator-0.4.0}/README.md +5 -2
  3. {xmlgenerator-0.2.1 → xmlgenerator-0.4.0}/setup.py +1 -1
  4. {xmlgenerator-0.2.1 → xmlgenerator-0.4.0}/xmlgenerator/arguments.py +7 -1
  5. {xmlgenerator-0.2.1 → xmlgenerator-0.4.0}/xmlgenerator/bootstrap.py +6 -4
  6. {xmlgenerator-0.2.1 → xmlgenerator-0.4.0}/xmlgenerator/configuration.py +2 -1
  7. xmlgenerator-0.4.0/xmlgenerator/generator.py +480 -0
  8. xmlgenerator-0.4.0/xmlgenerator/randomization.py +135 -0
  9. {xmlgenerator-0.2.1 → xmlgenerator-0.4.0}/xmlgenerator/substitution.py +49 -52
  10. {xmlgenerator-0.2.1 → xmlgenerator-0.4.0/xmlgenerator.egg-info}/PKG-INFO +6 -3
  11. xmlgenerator-0.2.1/xmlgenerator/generator.py +0 -412
  12. xmlgenerator-0.2.1/xmlgenerator/randomization.py +0 -76
  13. {xmlgenerator-0.2.1 → xmlgenerator-0.4.0}/LICENSE +0 -0
  14. {xmlgenerator-0.2.1 → xmlgenerator-0.4.0}/setup.cfg +0 -0
  15. {xmlgenerator-0.2.1 → xmlgenerator-0.4.0}/xmlgenerator/__init__.py +0 -0
  16. {xmlgenerator-0.2.1 → xmlgenerator-0.4.0}/xmlgenerator/validation.py +0 -0
  17. {xmlgenerator-0.2.1 → xmlgenerator-0.4.0}/xmlgenerator.egg-info/SOURCES.txt +0 -0
  18. {xmlgenerator-0.2.1 → xmlgenerator-0.4.0}/xmlgenerator.egg-info/dependency_links.txt +0 -0
  19. {xmlgenerator-0.2.1 → xmlgenerator-0.4.0}/xmlgenerator.egg-info/entry_points.txt +0 -0
  20. {xmlgenerator-0.2.1 → xmlgenerator-0.4.0}/xmlgenerator.egg-info/requires.txt +0 -0
  21. {xmlgenerator-0.2.1 → xmlgenerator-0.4.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.2.1
3
+ Version: 0.4.0
4
4
  Summary: Generates XML documents from XSD schemas
5
5
  Home-page: https://github.com/lexakimov/xmlgenerator
6
6
  Author: Alexey Akimov
@@ -136,12 +136,13 @@ positional arguments:
136
136
  options:
137
137
  -h, --help show this help message and exit
138
138
  -c, --config <config.yml> pass yaml configuration file
139
+ -l, --locale <locale> randomizer locale (default: en_US)
139
140
  -o, --output <output.xml> save output to dir or file
140
141
  -p, --pretty prettify output XML
141
142
  -v, --validation <validation> validate generated XML document (none, schema, schematron, default is schema)
142
143
  -ff, --fail-fast terminate execution on validation error (default is true)
143
144
  -e, --encoding <encoding> output XML encoding (utf-8, windows-1251, default is utf-8)
144
- --seed <seed> set randomization seed
145
+ -s, --seed <seed> set randomization seed
145
146
  -d, --debug enable debug mode
146
147
  -V, --version shows current version
147
148
  -C, --completion <shell> print shell completion script (bash, zsh, tcsh)
@@ -164,7 +165,7 @@ global:
164
165
  source_filename: ...
165
166
 
166
167
  # Filename template for saving the generated document.
167
- # Default value: `{{ source_filename }}_{{ uuid }}` (xsd schema filename + random UUID)
168
+ # Default value: `{{ source_extracted }}_{{ uuid }}` (xsd schema filename + random UUID)
168
169
  output_filename: ...
169
170
 
170
171
  # Random value generator settings
@@ -172,6 +173,8 @@ global:
172
173
  # Probability of adding optional elements (0.0-1.0)
173
174
  # Default value: 0.5
174
175
  probability: 1
176
+ # Limit for the minimal number of elements
177
+ min_occurs: 0
175
178
  # Limit for the maximum number of elements
176
179
  max_occurs: 5
177
180
  # Minimum string length
@@ -107,12 +107,13 @@ positional arguments:
107
107
  options:
108
108
  -h, --help show this help message and exit
109
109
  -c, --config <config.yml> pass yaml configuration file
110
+ -l, --locale <locale> randomizer locale (default: en_US)
110
111
  -o, --output <output.xml> save output to dir or file
111
112
  -p, --pretty prettify output XML
112
113
  -v, --validation <validation> validate generated XML document (none, schema, schematron, default is schema)
113
114
  -ff, --fail-fast terminate execution on validation error (default is true)
114
115
  -e, --encoding <encoding> output XML encoding (utf-8, windows-1251, default is utf-8)
115
- --seed <seed> set randomization seed
116
+ -s, --seed <seed> set randomization seed
116
117
  -d, --debug enable debug mode
117
118
  -V, --version shows current version
118
119
  -C, --completion <shell> print shell completion script (bash, zsh, tcsh)
@@ -135,7 +136,7 @@ global:
135
136
  source_filename: ...
136
137
 
137
138
  # Filename template for saving the generated document.
138
- # Default value: `{{ source_filename }}_{{ uuid }}` (xsd schema filename + random UUID)
139
+ # Default value: `{{ source_extracted }}_{{ uuid }}` (xsd schema filename + random UUID)
139
140
  output_filename: ...
140
141
 
141
142
  # Random value generator settings
@@ -143,6 +144,8 @@ global:
143
144
  # Probability of adding optional elements (0.0-1.0)
144
145
  # Default value: 0.5
145
146
  probability: 1
147
+ # Limit for the minimal number of elements
148
+ min_occurs: 0
146
149
  # Limit for the maximum number of elements
147
150
  max_occurs: 5
148
151
  # Minimum string length
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='xmlgenerator',
5
- version='0.2.1',
5
+ version='0.4.0',
6
6
  packages=find_packages(exclude=("tests", "tests.*")),
7
7
  entry_points={
8
8
  'console_scripts': [
@@ -39,6 +39,12 @@ def _get_parser():
39
39
  dest="config_yaml",
40
40
  help="pass yaml configuration file"
41
41
  )
42
+ parser.add_argument(
43
+ "-l", "--locale",
44
+ metavar="<locale>",
45
+ default="en_US",
46
+ help="randomizer locale (default: %(default)s)"
47
+ )
42
48
  output_arg = parser.add_argument(
43
49
  "-o", "--output",
44
50
  metavar="<output.xml>",
@@ -71,7 +77,7 @@ def _get_parser():
71
77
  help="output XML encoding (utf-8, windows-1251, default is utf-8)"
72
78
  )
73
79
  parser.add_argument(
74
- "--seed",
80
+ "-s", "--seed",
75
81
  metavar="<seed>",
76
82
  help="set randomization seed"
77
83
  )
@@ -42,14 +42,15 @@ def _main():
42
42
 
43
43
  config = load_config(args.config_yaml)
44
44
 
45
- randomizer = Randomizer(args.seed)
45
+ randomizer = Randomizer(args.seed, args.locale)
46
46
  substitutor = Substitutor(randomizer)
47
47
  generator = XmlGenerator(randomizer, substitutor)
48
48
  validator = XmlValidator(args.validation, args.fail_fast)
49
49
 
50
- logger.debug('found %s schemas', len(xsd_files))
51
- for xsd_file in xsd_files:
52
- logger.info('processing schema: %s', xsd_file.name)
50
+ total_count = len(xsd_files)
51
+ logger.debug('found %s schemas', total_count)
52
+ for index, xsd_file in enumerate(xsd_files):
53
+ logger.info('processing schema %s of %s: %s', index + 1, total_count, xsd_file.name)
53
54
 
54
55
  # get configuration override for current schema
55
56
  local_config = config.get_for_file(xsd_file.name)
@@ -88,6 +89,7 @@ def _main():
88
89
 
89
90
 
90
91
  def _setup_loggers(args):
92
+ logging.addLevelName(logging.WARNING, 'WARN')
91
93
  log_level = logging.DEBUG if args.debug else logging.INFO
92
94
  logger.setLevel(log_level)
93
95
  configuration.logger.setLevel(log_level)
@@ -13,6 +13,7 @@ logger = logging.getLogger(__name__)
13
13
  @dataclass
14
14
  class RandomizationConfig:
15
15
  probability: float = field(default=None)
16
+ min_occurs: int = field(default=None)
16
17
  max_occurs: int = field(default=None)
17
18
  min_length: int = field(default=None)
18
19
  max_length: int = field(default=None)
@@ -36,7 +37,7 @@ class GeneratorConfig:
36
37
  @dataclass
37
38
  class GlobalGeneratorConfig(GeneratorConfig):
38
39
  source_filename: str = field(default='(?P<extracted>.*).(xsd|XSD)')
39
- output_filename: str = field(default='{{ source_filename }}_{{ uuid }}')
40
+ output_filename: str = field(default='{{ source_extracted }}_{{ uuid }}')
40
41
  randomization: GlobalRandomizationConfig = field(default_factory=lambda: GlobalRandomizationConfig())
41
42
 
42
43
 
@@ -0,0 +1,480 @@
1
+ import logging
2
+ from decimal import Decimal
3
+
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, local_config: GeneratorConfig) -> etree.Element:
22
+ logger.debug('generate xml document...')
23
+ ns_map = {None if k == '' else k: v for k, v in xsd_schema.namespaces.items() if v != ''}
24
+ xsd_root_element = xsd_schema.root_elements[0]
25
+ xml_root_element = etree.Element(xsd_root_element.name, nsmap=ns_map)
26
+ xml_tree = etree.ElementTree(xml_root_element)
27
+ self._add_elements(xml_tree, xml_root_element, xsd_root_element, local_config)
28
+ return xml_root_element
29
+
30
+ def _add_elements(self, xml_tree, xml_element, xsd_element, local_config: GeneratorConfig) -> None:
31
+ rand_config = local_config.randomization
32
+ min_occurs_conf = rand_config.min_occurs
33
+ max_occurs_conf = rand_config.max_occurs
34
+
35
+ # Process child elements --------------------------------------------------------------------------------------
36
+ if isinstance(xsd_element, XsdElement):
37
+ element_xpath = xml_tree.getpath(xml_element)
38
+ logger.debug('element: %s [created]', element_xpath)
39
+
40
+ xsd_element_type = getattr(xsd_element, 'type', None)
41
+
42
+ # Add attributes if they are
43
+ attributes = getattr(xsd_element, 'attributes', dict())
44
+ if len(attributes) > 0 and xsd_element_type.local_name != 'anyType':
45
+ for attr_name, attr in attributes.items():
46
+ logger.debug('element: %s; attribute: "%s" - [processing]', element_xpath, attr_name)
47
+ use = attr.use # optional | required | prohibited
48
+ if use == 'prohibited':
49
+ logger.debug('element: %s; attribute: "%s" - [skipped]', element_xpath, attr_name)
50
+ continue
51
+ elif use == 'optional':
52
+ if self.randomizer.random() > rand_config.probability:
53
+ logger.debug('element: %s; attribute: "%s" - [skipped]', element_xpath, attr_name)
54
+ continue
55
+
56
+ attr_value = self._generate_value(attr.type, attr_name, local_config)
57
+ if attr_value is not None:
58
+ xml_element.set(attr_name, str(attr_value))
59
+ logger.debug('element: %s; attribute: "%s" = "%s"', element_xpath, attr_name, attr_value)
60
+
61
+ if isinstance(xsd_element_type, XsdAtomicBuiltin):
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
+
66
+ elif isinstance(xsd_element_type, XsdAtomicRestriction):
67
+ text = self._generate_value(xsd_element_type, xsd_element.name, local_config)
68
+ xml_element.text = text
69
+ logger.debug('element: %s = "%s"', element_xpath, text)
70
+
71
+ elif isinstance(xsd_element_type, XsdComplexType):
72
+ xsd_element_type_content = xsd_element_type.content
73
+ if isinstance(xsd_element_type_content, XsdGroup):
74
+ self._add_elements(xml_tree, xml_element, xsd_element_type_content, local_config)
75
+ else:
76
+ raise RuntimeError()
77
+
78
+ else:
79
+ raise RuntimeError()
80
+
81
+ elif isinstance(xsd_element, XsdGroup):
82
+ model = xsd_element.model
83
+
84
+ min_occurs = getattr(xsd_element, 'min_occurs', None)
85
+ max_occurs = getattr(xsd_element, 'max_occurs', None)
86
+ min_occurs, max_occurs = merge_constraints(
87
+ schema_min=min_occurs,
88
+ schema_max=max_occurs,
89
+ config_min=min_occurs_conf,
90
+ config_max=max_occurs_conf
91
+ )
92
+ if max_occurs is None:
93
+ max_occurs = 10
94
+ group_occurs = self.randomizer.integer(min_occurs, max_occurs)
95
+ logger.debug('add %s (random between %s and %s) groups of type "%s"',
96
+ group_occurs, min_occurs, max_occurs, model)
97
+
98
+ if model == 'all':
99
+ for _ in range(group_occurs):
100
+ xsd_group_content = xsd_element.content
101
+ for xsd_child_element_type in xsd_group_content:
102
+
103
+ min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
104
+ max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
105
+ min_occurs, max_occurs = merge_constraints(
106
+ schema_min=min_occurs,
107
+ schema_max=max_occurs,
108
+ config_min=min_occurs_conf,
109
+ config_max=max_occurs_conf
110
+ )
111
+ if max_occurs is None:
112
+ max_occurs = 10
113
+ element_occurs = self.randomizer.integer(min_occurs, max_occurs)
114
+ logger.debug('element_occurs: %s (random between %s and %s)', element_occurs, min_occurs,
115
+ max_occurs)
116
+
117
+ for _ in range(element_occurs):
118
+ xml_child_element = etree.SubElement(xml_element, xsd_child_element_type.name)
119
+ self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
120
+
121
+ elif model == 'sequence':
122
+ for _ in range(group_occurs):
123
+ xsd_group_content = xsd_element.content
124
+ for xsd_child_element_type in xsd_group_content:
125
+ if isinstance(xsd_child_element_type, XsdElement):
126
+
127
+ min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
128
+ max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
129
+ min_occurs, max_occurs = merge_constraints(
130
+ schema_min=min_occurs,
131
+ schema_max=max_occurs,
132
+ config_min=min_occurs_conf,
133
+ config_max=max_occurs_conf
134
+ )
135
+ if max_occurs is None:
136
+ max_occurs = 10
137
+ element_occurs = self.randomizer.integer(min_occurs, max_occurs)
138
+ logger.debug('element_occurs: %s (random between %s and %s)', element_occurs, min_occurs,
139
+ max_occurs)
140
+
141
+ for _ in range(element_occurs):
142
+ xml_child_element = etree.SubElement(xml_element, xsd_child_element_type.name)
143
+ self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
144
+
145
+ elif isinstance(xsd_child_element_type, XsdGroup):
146
+ xml_child_element = xml_element
147
+ self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
148
+
149
+ elif isinstance(xsd_child_element_type, XsdAnyElement):
150
+ xml_child_element = etree.SubElement(xml_element, "Any")
151
+ self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
152
+
153
+ else:
154
+ raise RuntimeError(xsd_child_element_type)
155
+
156
+ elif model == 'choice':
157
+ for _ in range(group_occurs):
158
+ xsd_child_element_type = self.randomizer.any(xsd_element)
159
+
160
+ min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
161
+ max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
162
+ min_occurs, max_occurs = merge_constraints(
163
+ schema_min=min_occurs,
164
+ schema_max=max_occurs,
165
+ config_min=min_occurs_conf,
166
+ config_max=max_occurs_conf
167
+ )
168
+ if max_occurs is None:
169
+ max_occurs = 10
170
+ element_occurs = self.randomizer.integer(min_occurs, max_occurs)
171
+ logger.debug('element_occurs: %s (random between %s and %s)', element_occurs, min_occurs,
172
+ max_occurs)
173
+
174
+ for _ in range(element_occurs):
175
+ xml_child_element = etree.SubElement(xml_element, xsd_child_element_type.name)
176
+ self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
177
+
178
+ else:
179
+ raise RuntimeError()
180
+
181
+ elif isinstance(xsd_element, XsdAnyElement):
182
+ # для any не добавляем никаких дочерних тегов и атрибутов
183
+ pass
184
+
185
+ else:
186
+ raise RuntimeError()
187
+
188
+ def _generate_value(self, xsd_type, target_name, local_config: GeneratorConfig) -> str | None:
189
+ if xsd_type is None:
190
+ raise RuntimeError(f"xsd_type is None. Target name: {target_name}")
191
+
192
+ if isinstance(xsd_type, XsdComplexType):
193
+ return None
194
+
195
+ # -------------------------------------------------------------------------------------------------------------
196
+ # Ищем переопределение значения в конфигурации
197
+ value_override = local_config.value_override
198
+ is_found, overridden_value = self.substitutor.substitute_value(target_name, value_override.items())
199
+ if is_found:
200
+ logger.debug('value resolved: "%s"', overridden_value)
201
+ return overridden_value
202
+
203
+ # -------------------------------------------------------------------------------------------------------------
204
+ # If there is an enumeration, select a random value from it
205
+ enumeration = getattr(xsd_type, 'enumeration', None)
206
+ if enumeration is not None:
207
+ random_enum = self.randomizer.any(enumeration)
208
+ logger.debug('use random value from enumeration: "%s" %s', random_enum, enumeration)
209
+ return str(random_enum)
210
+
211
+ # -------------------------------------------------------------------------------------------------------------
212
+ # Генерируем значения для стандартных типов и типов с ограничениями
213
+ if isinstance(xsd_type, XsdAtomicBuiltin) or isinstance(xsd_type, XsdAtomicRestriction):
214
+ # Выясняем ограничения
215
+ min_length = getattr(xsd_type, 'min_length', None) # None | int
216
+ max_length = getattr(xsd_type, 'max_length', None) # None | int
217
+
218
+ min_value = getattr(xsd_type, 'min_value', None) # None | int
219
+ max_value = getattr(xsd_type, 'max_value', None) # None
220
+
221
+ total_digits = None
222
+ fraction_digits = None
223
+ patterns = getattr(xsd_type, 'patterns', None)
224
+
225
+ validators = getattr(xsd_type, 'validators', None)
226
+ for validator in validators:
227
+ if isinstance(validator, XsdMinExclusiveFacet):
228
+ min_value = validator.value
229
+ elif isinstance(validator, XsdMinInclusiveFacet):
230
+ min_value = validator.value
231
+ elif isinstance(validator, XsdMaxExclusiveFacet):
232
+ max_value = validator.value
233
+ elif isinstance(validator, XsdMaxInclusiveFacet):
234
+ max_value = validator.value
235
+ elif isinstance(validator, XsdLengthFacet):
236
+ min_length = validator.value
237
+ max_length = validator.value
238
+ elif isinstance(validator, XsdMinLengthFacet):
239
+ min_length = validator.value
240
+ elif isinstance(validator, XsdMaxLengthFacet):
241
+ max_length = validator.value
242
+ elif isinstance(validator, XsdTotalDigitsFacet):
243
+ total_digits = validator.value
244
+ elif isinstance(validator, XsdFractionDigitsFacet):
245
+ fraction_digits = validator.value
246
+ elif isinstance(validator, XsdEnumerationFacets):
247
+ pass
248
+ elif callable(validator):
249
+ pass
250
+ else:
251
+ raise RuntimeError(f"Unhandled validator: {validator}")
252
+
253
+ if isinstance(min_value, Decimal):
254
+ min_value = float(min_value)
255
+ if isinstance(max_value, Decimal):
256
+ max_value = float(max_value)
257
+
258
+ rand_config = local_config.randomization
259
+
260
+ logger.debug('bounds before adjust: min_length: %4s; max_length: %4s', min_length, max_length)
261
+ min_length, max_length = merge_constraints(
262
+ schema_min=min_length,
263
+ schema_max=max_length,
264
+ config_min=rand_config.min_length,
265
+ config_max=rand_config.max_length
266
+ )
267
+ logger.debug('bounds after adjust: min_length: %4s; max_length: %4s', min_length, max_length)
268
+
269
+ type_id = xsd_type.id or xsd_type.base_type.id or xsd_type.root_type.id
270
+ logger.debug('generate value for type: "%s"', type_id)
271
+
272
+ match type_id:
273
+ case 'boolean':
274
+ result = self._generate_boolean()
275
+ case 'string':
276
+ result = self._generate_string(min_length, max_length, patterns)
277
+ case 'integer':
278
+ result = self._generate_integer(min_value, max_value, total_digits)
279
+ case 'decimal':
280
+ result = self._generate_decimal(rand_config, min_value, max_value, total_digits, fraction_digits)
281
+ case 'float':
282
+ result = self._generate_float(rand_config, min_value, max_value)
283
+ case 'double':
284
+ result = self._generate_double(rand_config, min_value, max_value)
285
+ case 'duration':
286
+ result = self._generate_duration()
287
+ case 'dateTime':
288
+ result = self._generate_datetime()
289
+ case 'date':
290
+ result = self._generate_date()
291
+ case 'time':
292
+ result = self._generate_time()
293
+ case 'gYearMonth':
294
+ result = self._generate_gyearmonth()
295
+ case 'gYear':
296
+ result = self._generate_gyear()
297
+ case 'gMonthDay':
298
+ result = self._generate_gmonthday()
299
+ case 'gDay':
300
+ result = self._generate_gday()
301
+ case 'gMonth':
302
+ result = self._generate_gmonth()
303
+ case 'hexBinary':
304
+ result = self._generate_hex_binary()
305
+ case 'base64Binary':
306
+ result = self._generate_base64_binary()
307
+ case 'anyURI':
308
+ result = self._generate_any_uri()
309
+ case 'QName':
310
+ result = self._generate_qname()
311
+ case 'NOTATION':
312
+ result = self._generate_notation()
313
+ case _:
314
+ raise RuntimeError(type_id)
315
+ generated_value = result
316
+ logger.debug('value generated: "%s"', generated_value)
317
+ return generated_value
318
+
319
+ # -------------------------------------------------------------------------------------------------------------
320
+ # Проверяем базовый тип
321
+ base_type = getattr(xsd_type, 'base_type', None)
322
+
323
+ # невозможный кейс (только если попался комплексный тип)
324
+ if base_type is None:
325
+ raise RuntimeError(f"base_type is None. Target name: {target_name}")
326
+
327
+ raise RuntimeError(f"Can't generate value - unhandled type. Target name: {target_name}")
328
+
329
+ def _generate_boolean(self):
330
+ return self.randomizer.any(['true', 'false'])
331
+
332
+ def _generate_string(self, min_length, max_length, patterns):
333
+ if patterns is not None:
334
+ # Генерация строки по regex
335
+ random_enum = self.randomizer.any(patterns)
336
+ random_pattern = random_enum.attrib['value']
337
+ return self.randomizer.regex(random_pattern)
338
+
339
+ # Иначе генерируем случайную строку
340
+ return self.randomizer.ascii_string(min_length, max_length)
341
+
342
+ def _generate_integer(self, min_value, max_value, total_digits):
343
+ if total_digits:
344
+ min_value = 10 ** (total_digits - 1)
345
+ max_value = (10 ** total_digits) - 1
346
+ rnd_int = self.randomizer.integer(min_value, max_value)
347
+ return str(rnd_int)
348
+
349
+ def _generate_decimal(self, rand_config, schema_min, schema_max, total_digits, fraction_digits):
350
+ if fraction_digits is None:
351
+ fraction_digits = self.randomizer.integer(1, 3)
352
+
353
+ if fraction_digits > 4:
354
+ fraction_digits = self.randomizer.integer(1, 4)
355
+
356
+ if total_digits is None:
357
+ total_digits = 10 + fraction_digits
358
+
359
+ if total_digits > 10:
360
+ total_digits = self.randomizer.integer(6, total_digits - 2)
361
+
362
+ integer_digits = total_digits - fraction_digits
363
+
364
+ # negative bound
365
+ digit_min = -(10 ** integer_digits - 1)
366
+ # positive bound
367
+ digit_max = 10 ** integer_digits - 1
368
+ logger.debug("integer digits: %s; digit_min: %s; digit_max: %s", integer_digits, digit_min, digit_max)
369
+
370
+ logger.debug('bounds before adjust: min_value: %4s; max_value: %4s', schema_min, schema_max)
371
+ config_min = rand_config.min_inclusive
372
+ config_max = rand_config.max_inclusive
373
+ effective_min, effective_max \
374
+ = merge_constraints(digit_min, digit_max, schema_min, schema_max, config_min, config_max)
375
+ logger.debug('bounds after adjust: min_value: %4s; max_value: %4s', effective_min, effective_max)
376
+
377
+ random_float = self.randomizer.float(effective_min, effective_max)
378
+ return f"{random_float:.{fraction_digits}f}"
379
+
380
+ def _generate_float(self, rand_config, min_value, max_value):
381
+ return self._generate_decimal(rand_config, min_value, max_value, None, 2)
382
+
383
+ def _generate_double(self, rand_config, min_value, max_value):
384
+ return self._generate_decimal(rand_config, min_value, max_value, None, 2)
385
+
386
+ def _generate_duration(self):
387
+ raise RuntimeError("not yet implemented")
388
+
389
+ def _generate_datetime(self):
390
+ random_datetime = self.randomizer.random_datetime()
391
+ formatted = random_datetime.isoformat()
392
+ return formatted
393
+
394
+ def _generate_date(self):
395
+ random_date = self.randomizer.random_date()
396
+ formatted = random_date.isoformat()
397
+ return formatted
398
+
399
+ def _generate_time(self):
400
+ random_time = self.randomizer.random_time()
401
+ formatted = random_time.isoformat()
402
+ return formatted
403
+
404
+ def _generate_gyearmonth(self):
405
+ random_date = self.randomizer.random_date()
406
+ formatted = random_date.strftime('%Y-%m')
407
+ return formatted
408
+
409
+ def _generate_gyear(self):
410
+ return str(self.randomizer.integer(2000, 2050))
411
+
412
+ def _generate_gmonthday(self):
413
+ random_date = self.randomizer.random_date()
414
+ formatted = random_date.strftime('--%m-%d')
415
+ return formatted
416
+
417
+ def _generate_gday(self):
418
+ random_date = self.randomizer.random_date()
419
+ formatted = random_date.strftime('---%d')
420
+ return formatted
421
+
422
+ def _generate_gmonth(self):
423
+ random_date = self.randomizer.random_date()
424
+ formatted = random_date.strftime('--%m--')
425
+ return formatted
426
+
427
+ def _generate_hex_binary(self):
428
+ raise RuntimeError("not yet implemented")
429
+
430
+ def _generate_base64_binary(self):
431
+ raise RuntimeError("not yet implemented")
432
+
433
+ def _generate_any_uri(self):
434
+ raise RuntimeError("not yet implemented")
435
+
436
+ def _generate_qname(self):
437
+ raise RuntimeError("not yet implemented")
438
+
439
+ def _generate_notation(self):
440
+ raise RuntimeError("not yet implemented")
441
+
442
+
443
+ def merge_constraints(digit_min=None, digit_max=None, schema_min=None, schema_max=None, config_min=None,
444
+ config_max=None):
445
+ logger.debug(
446
+ "merge numeric constraints: "
447
+ "digit_min: %s, digit_max: %s, schema_min: %s, schema_max: %s, config_min: %s, config_max: %s",
448
+ digit_min, digit_max, schema_min, schema_max, config_min, config_max)
449
+
450
+ # За основу берем цифровые ограничения (они самые нестрогие)
451
+ effective_min, effective_max = digit_min, digit_max
452
+
453
+ # Применяем схемные ограничения
454
+ if schema_min is not None:
455
+ effective_min = max(effective_min, schema_min) if effective_min is not None else schema_min
456
+ if schema_max is not None:
457
+ effective_max = min(effective_max, schema_max) if effective_max is not None else schema_max
458
+
459
+ # Применяем конфигурационные ограничения с проверкой на конфликт
460
+ if config_min is not None:
461
+ if effective_max is not None and config_min > effective_max:
462
+ logger.warning("can't apply bound from configuration: config_min (%s) > effective_max (%s)",
463
+ config_min, effective_max)
464
+ else:
465
+ effective_min = max(effective_min, config_min) if effective_min is not None else config_min
466
+
467
+ if config_max is not None:
468
+ if effective_min is not None and config_max < effective_min:
469
+ logger.warning("can't apply bound from configuration: config_max (%s) < effective_min (%s)",
470
+ config_max, effective_min)
471
+ else:
472
+ effective_max = min(effective_max, config_max) if effective_max is not None else config_max
473
+
474
+ # Проверяем на конфликт
475
+ if effective_min is not None and effective_max is not None and effective_min > effective_max:
476
+ logger.warning("constrains conflict: effective_min (%s) > effective_max (%s). Swap values.",
477
+ effective_min, effective_max)
478
+ effective_min, effective_max = effective_max, effective_min
479
+
480
+ return effective_min, effective_max