xmlgenerator 0.3.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

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/arguments.py CHANGED
@@ -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)")
xmlgenerator/bootstrap.py CHANGED
@@ -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)
xmlgenerator/generator.py CHANGED
@@ -1,6 +1,8 @@
1
1
  import logging
2
+ from dataclasses import dataclass, replace
3
+ from decimal import Decimal
4
+ from typing import Optional, Any, Callable, Dict
2
5
 
3
- import xmlschema
4
6
  from lxml import etree
5
7
  from xmlschema.validators import XsdComplexType, XsdAtomicRestriction, XsdTotalDigitsFacet, XsdElement, \
6
8
  XsdGroup, XsdFractionDigitsFacet, XsdLengthFacet, XsdMaxLengthFacet, XsdMinExclusiveFacet, XsdMinInclusiveFacet, \
@@ -13,12 +15,78 @@ from xmlgenerator.substitution import Substitutor
13
15
  logger = logging.getLogger(__name__)
14
16
 
15
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
+
16
30
  class XmlGenerator:
17
31
  def __init__(self, randomizer: Randomizer, substitutor: Substitutor):
18
32
  self.randomizer = randomizer
19
33
  self.substitutor = substitutor
20
-
21
- def generate_xml(self, xsd_schema: xmlschema.XMLSchema, local_config: GeneratorConfig) -> etree.Element:
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...')
22
90
  ns_map = {None if k == '' else k: v for k, v in xsd_schema.namespaces.items() if v != ''}
23
91
  xsd_root_element = xsd_schema.root_elements[0]
24
92
  xml_root_element = etree.Element(xsd_root_element.name, nsmap=ns_map)
@@ -26,7 +94,11 @@ class XmlGenerator:
26
94
  self._add_elements(xml_tree, xml_root_element, xsd_root_element, local_config)
27
95
  return xml_root_element
28
96
 
29
- def _add_elements(self, xml_tree, xml_element: etree.Element, xsd_element, local_config: GeneratorConfig) -> None:
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
+
30
102
  # Process child elements --------------------------------------------------------------------------------------
31
103
  if isinstance(xsd_element, XsdElement):
32
104
  element_xpath = xml_tree.getpath(xml_element)
@@ -38,14 +110,14 @@ class XmlGenerator:
38
110
  attributes = getattr(xsd_element, 'attributes', dict())
39
111
  if len(attributes) > 0 and xsd_element_type.local_name != 'anyType':
40
112
  for attr_name, attr in attributes.items():
41
- logger.debug('element: %s; attribute "%s" [processing]', element_xpath, attr_name)
113
+ logger.debug('element: %s; attribute: "%s" - [processing]', element_xpath, attr_name)
42
114
  use = attr.use # optional | required | prohibited
43
115
  if use == 'prohibited':
44
- logger.debug('element: %s; attribute: "%s" [skipped]', element_xpath, attr_name)
116
+ logger.debug('element: %s; attribute: "%s" - [skipped]', element_xpath, attr_name)
45
117
  continue
46
118
  elif use == 'optional':
47
- if self.randomizer.random() > local_config.randomization.probability:
48
- logger.debug('element: %s; attribute: "%s" [skipped]', element_xpath, attr_name)
119
+ if self.randomizer.random() > rand_config.probability:
120
+ logger.debug('element: %s; attribute: "%s" - [skipped]', element_xpath, attr_name)
49
121
  continue
50
122
 
51
123
  attr_value = self._generate_value(attr.type, attr_name, local_config)
@@ -57,58 +129,82 @@ class XmlGenerator:
57
129
  text = self._generate_value(xsd_element_type, xsd_element.name, local_config)
58
130
  xml_element.text = text
59
131
  logger.debug('element: %s = "%s"', element_xpath, text)
60
- return
132
+
61
133
  elif isinstance(xsd_element_type, XsdAtomicRestriction):
62
134
  text = self._generate_value(xsd_element_type, xsd_element.name, local_config)
63
135
  xml_element.text = text
64
136
  logger.debug('element: %s = "%s"', element_xpath, text)
65
- return
137
+
66
138
  elif isinstance(xsd_element_type, XsdComplexType):
67
139
  xsd_element_type_content = xsd_element_type.content
68
140
  if isinstance(xsd_element_type_content, XsdGroup):
69
141
  self._add_elements(xml_tree, xml_element, xsd_element_type_content, local_config)
70
142
  else:
71
143
  raise RuntimeError()
144
+
72
145
  else:
73
146
  raise RuntimeError()
74
147
 
75
148
  elif isinstance(xsd_element, XsdGroup):
76
149
  model = xsd_element.model
77
150
 
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)
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)
83
164
 
84
165
  if model == 'all':
85
166
  for _ in range(group_occurs):
86
167
  xsd_group_content = xsd_element.content
87
168
  for xsd_child_element_type in xsd_group_content:
88
169
 
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)
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)
94
183
 
95
184
  for _ in range(element_occurs):
96
185
  xml_child_element = etree.SubElement(xml_element, xsd_child_element_type.name)
97
186
  self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
98
- return
99
187
 
100
188
  elif model == 'sequence':
101
189
  for _ in range(group_occurs):
102
190
  xsd_group_content = xsd_element.content
103
191
  for xsd_child_element_type in xsd_group_content:
192
+ if isinstance(xsd_child_element_type, XsdElement):
104
193
 
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)
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)
110
207
 
111
- if isinstance(xsd_child_element_type, XsdElement):
112
208
  for _ in range(element_occurs):
113
209
  xml_child_element = etree.SubElement(xml_element, xsd_child_element_type.name)
114
210
  self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
@@ -123,22 +219,28 @@ class XmlGenerator:
123
219
 
124
220
  else:
125
221
  raise RuntimeError(xsd_child_element_type)
126
- return
127
222
 
128
223
  elif model == 'choice':
129
224
  for _ in range(group_occurs):
130
225
  xsd_child_element_type = self.randomizer.any(xsd_element)
131
226
 
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)
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)
137
240
 
138
241
  for _ in range(element_occurs):
139
242
  xml_child_element = etree.SubElement(xml_element, xsd_child_element_type.name)
140
243
  self._add_elements(xml_tree, xml_child_element, xsd_child_element_type, local_config)
141
- return
142
244
 
143
245
  else:
144
246
  raise RuntimeError()
@@ -176,71 +278,14 @@ class XmlGenerator:
176
278
  # -------------------------------------------------------------------------------------------------------------
177
279
  # Генерируем значения для стандартных типов и типов с ограничениями
178
280
  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}")
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)
217
288
 
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
289
  logger.debug('value generated: "%s"', generated_value)
245
290
  return generated_value
246
291
 
@@ -254,83 +299,27 @@ class XmlGenerator:
254
299
 
255
300
  raise RuntimeError(f"Can't generate value - unhandled type. Target name: {target_name}")
256
301
 
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:
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:
315
308
  # Генерация строки по regex
316
- random_enum = self.randomizer.any(patterns)
309
+ random_enum = self.randomizer.any(constraints.patterns)
317
310
  random_pattern = random_enum.attrib['value']
318
311
  return self.randomizer.regex(random_pattern)
319
312
 
320
313
  # Иначе генерируем случайную строку
321
- return self.randomizer.ascii_string(min_length, max_length)
322
-
323
- def _generate_boolean(self):
324
- return self.randomizer.any(['true', 'false'])
314
+ return self.randomizer.ascii_string(constraints.min_length, constraints.max_length)
325
315
 
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)
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
332
322
 
333
- def _generate_decimal(self, total_digits, fraction_digits, min_value, max_value):
334
323
  if fraction_digits is None:
335
324
  fraction_digits = self.randomizer.integer(1, 3)
336
325
 
@@ -345,112 +334,263 @@ class XmlGenerator:
345
334
 
346
335
  integer_digits = total_digits - fraction_digits
347
336
 
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}"
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}"
358
356
 
359
- def _generate_float(self, min_value, max_value):
360
- return self._generate_double(min_value, max_value)
357
+ def _generate_float(self, constraints: TypeConstraints):
358
+ decimal_constraints = replace(constraints, fraction_digits=2)
359
+ return self._generate_decimal(decimal_constraints)
361
360
 
362
- def _generate_double(self, min_value, max_value):
363
- return self._generate_decimal(None, 2, min_value, max_value)
361
+ def _generate_double(self, constraints: TypeConstraints):
362
+ decimal_constraints = replace(constraints, fraction_digits=2)
363
+ return self._generate_decimal(decimal_constraints)
364
364
 
365
- def _generate_duration(self):
365
+ def _generate_duration(self, constraints: TypeConstraints):
366
366
  raise RuntimeError("not yet implemented")
367
367
 
368
- def _generate_datetime(self):
368
+ # noinspection PyUnusedLocal
369
+ def _generate_datetime(self, constraints: TypeConstraints):
369
370
  random_datetime = self.randomizer.random_datetime()
370
371
  formatted = random_datetime.isoformat()
371
372
  return formatted
372
373
 
373
- def _generate_date(self):
374
+ # noinspection PyUnusedLocal
375
+ def _generate_date(self, constraints: TypeConstraints):
374
376
  random_date = self.randomizer.random_date()
375
377
  formatted = random_date.isoformat()
376
378
  return formatted
377
379
 
378
- def _generate_time(self):
380
+ # noinspection PyUnusedLocal
381
+ def _generate_time(self, constraints: TypeConstraints):
379
382
  random_time = self.randomizer.random_time()
380
383
  formatted = random_time.isoformat()
381
384
  return formatted
382
385
 
383
- def _generate_gyearmonth(self):
386
+ # noinspection PyUnusedLocal
387
+ def _generate_gyearmonth(self, constraints: TypeConstraints):
384
388
  random_date = self.randomizer.random_date()
385
389
  formatted = random_date.strftime('%Y-%m')
386
390
  return formatted
387
391
 
388
- def _generate_gyear(self):
392
+ # noinspection PyUnusedLocal
393
+ def _generate_gyear(self, constraints: TypeConstraints):
389
394
  return str(self.randomizer.integer(2000, 2050))
390
395
 
391
- def _generate_gmonthday(self):
396
+ # noinspection PyUnusedLocal
397
+ def _generate_gmonthday(self, constraints: TypeConstraints):
392
398
  random_date = self.randomizer.random_date()
393
399
  formatted = random_date.strftime('--%m-%d')
394
400
  return formatted
395
401
 
396
- def _generate_gday(self):
402
+ # noinspection PyUnusedLocal
403
+ def _generate_gday(self, constraints: TypeConstraints):
397
404
  random_date = self.randomizer.random_date()
398
405
  formatted = random_date.strftime('---%d')
399
406
  return formatted
400
407
 
401
- def _generate_gmonth(self):
408
+ # noinspection PyUnusedLocal
409
+ def _generate_gmonth(self, constraints: TypeConstraints):
402
410
  random_date = self.randomizer.random_date()
403
411
  formatted = random_date.strftime('--%m--')
404
412
  return formatted
405
413
 
406
- def _generate_hex_binary(self):
407
- raise RuntimeError("not yet implemented")
414
+ def _generate_hex_binary(self, constraints: TypeConstraints):
415
+ return self.randomizer.hex_string(constraints.min_length, constraints.max_length)
408
416
 
409
- def _generate_base64_binary(self):
417
+ # noinspection PyUnusedLocal
418
+ def _generate_base64_binary(self, constraints: TypeConstraints):
410
419
  raise RuntimeError("not yet implemented")
411
420
 
412
- def _generate_any_uri(self):
421
+ # noinspection PyUnusedLocal
422
+ def _generate_any_uri(self, constraints: TypeConstraints):
413
423
  raise RuntimeError("not yet implemented")
414
424
 
415
- def _generate_qname(self):
425
+ # noinspection PyUnusedLocal
426
+ def _generate_qname(self, constraints: TypeConstraints):
416
427
  raise RuntimeError("not yet implemented")
417
428
 
418
- def _generate_notation(self):
429
+ # noinspection PyUnusedLocal
430
+ def _generate_notation(self, constraints: TypeConstraints):
419
431
  raise RuntimeError("not yet implemented")
420
432
 
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
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
434
529
  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):
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
+ # Применяем конфигурационные ограничения с проверкой на конфликт
446
576
  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
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
450
582
 
451
583
  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
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
455
595
 
456
- return fact_min, fact_max
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
 
@@ -0,0 +1,14 @@
1
+ xmlgenerator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ xmlgenerator/arguments.py,sha256=Pf7Bccan0FeO6v5INpBkhLlGfJg-FGMRX5pG8E2KVgo,4834
3
+ xmlgenerator/bootstrap.py,sha256=T_Xy5PElb75EuyKIwXUGkQ2mntt3v2RwC1ulFI-CZnM,3654
4
+ xmlgenerator/configuration.py,sha256=JYhz_lONxd0faUiZHG-TVEs6yocn0s__Ulwtcvq9eDs,5946
5
+ xmlgenerator/generator.py,sha256=vECUZZ5VlMq3Mpam3_ZUsWmzHr402nqxDRKzLNAqNiU,29237
6
+ xmlgenerator/randomization.py,sha256=azXW1SxKSA9_lw1IBQDPOwSUXFEXo8IGWFD0an-eVF0,4416
7
+ xmlgenerator/substitution.py,sha256=v4rzqnF1p1yN0VKRDFwQM5zQbpdg9ebbrh65cnh9qxw,6078
8
+ xmlgenerator/validation.py,sha256=uCJjS5YmRDlAp9C-5Rd4E2Brh6_3WOG2-dSGxDiaH14,2023
9
+ xmlgenerator-0.5.0.dist-info/licenses/LICENSE,sha256=QlXK8O3UcoAYUYwVJNgB9MSM7O94ogNo_1hd9GzznUQ,1070
10
+ xmlgenerator-0.5.0.dist-info/METADATA,sha256=-zApCcYPrg5A8nC2CLwBHWFEwMOwBDni21AnFhIEd1I,13083
11
+ xmlgenerator-0.5.0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
12
+ xmlgenerator-0.5.0.dist-info/entry_points.txt,sha256=ly9hKr3o4AzFUkelBZNRzyKYf-Ld4kfcffvBu1oHq54,61
13
+ xmlgenerator-0.5.0.dist-info/top_level.txt,sha256=jr7FbMBm8MQ6j8I_-nWzQQEseXzwSCZNXgrkWuk9P4E,13
14
+ xmlgenerator-0.5.0.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- xmlgenerator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- xmlgenerator/arguments.py,sha256=0WHKt7eOS7M3_R-rYdp_52Q8rgArCF9VlIDkPVP_8dk,4784
3
- xmlgenerator/bootstrap.py,sha256=7ONv9Eh46z6xd8w_V7FO1156FonAX-LHYpRrQ44PrBU,3668
4
- xmlgenerator/configuration.py,sha256=JYhz_lONxd0faUiZHG-TVEs6yocn0s__Ulwtcvq9eDs,5946
5
- xmlgenerator/generator.py,sha256=eBW8UlY8Bu8xh4oo5jp4_yo6OKz9T3xvQeZYC66lui4,20814
6
- xmlgenerator/randomization.py,sha256=ekNQJYgcmDCf6uCYiZnWat7u_9kO6TAQQ8qZFIpiB7o,4205
7
- xmlgenerator/substitution.py,sha256=1nvjQLSUS9Yo8r2T3f420Upbwm6iikUQG3lG5TQUSDU,6016
8
- xmlgenerator/validation.py,sha256=uCJjS5YmRDlAp9C-5Rd4E2Brh6_3WOG2-dSGxDiaH14,2023
9
- xmlgenerator-0.3.0.dist-info/licenses/LICENSE,sha256=QlXK8O3UcoAYUYwVJNgB9MSM7O94ogNo_1hd9GzznUQ,1070
10
- xmlgenerator-0.3.0.dist-info/METADATA,sha256=ONyx3zcbuX1zQTnt-5ORWcxU1shJDYbgsyFBlEWx8E4,12870
11
- xmlgenerator-0.3.0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
12
- xmlgenerator-0.3.0.dist-info/entry_points.txt,sha256=ly9hKr3o4AzFUkelBZNRzyKYf-Ld4kfcffvBu1oHq54,61
13
- xmlgenerator-0.3.0.dist-info/top_level.txt,sha256=jr7FbMBm8MQ6j8I_-nWzQQEseXzwSCZNXgrkWuk9P4E,13
14
- xmlgenerator-0.3.0.dist-info/RECORD,,