xmlgenerator 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xmlgenerator
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Generates XML documents from XSD schemas
5
5
  Home-page: https://github.com/lexakimov/xmlgenerator
6
6
  Author: Alexey Akimov
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='xmlgenerator',
5
- version='0.1.0',
5
+ version='0.2.0',
6
6
  packages=find_packages(exclude=("tests", "tests.*")),
7
7
  entry_points={
8
8
  'console_scripts': [
@@ -1,9 +1,12 @@
1
+ import logging
1
2
  import sys
2
3
  from argparse import ArgumentParser, HelpFormatter
3
4
  from pathlib import Path
4
5
 
5
6
  import shtab
6
7
 
8
+ logger = logging.getLogger(__name__)
9
+
7
10
 
8
11
  class MyParser(ArgumentParser):
9
12
  def error(self, message):
@@ -99,6 +102,10 @@ def parse_args():
99
102
  parser = _get_parser()
100
103
  args = parser.parse_args()
101
104
 
105
+ # setup logger
106
+ log_level = logging.DEBUG if args.debug else logging.INFO
107
+ logger.setLevel(log_level)
108
+
102
109
  if args.config_yaml:
103
110
  config_path = Path(args.config_yaml)
104
111
  if not config_path.exists() or not config_path.is_file():
@@ -1,39 +1,55 @@
1
+ import logging
2
+
1
3
  from lxml import etree
4
+ from xmlschema import XMLSchema
5
+
6
+ import xmlgenerator
7
+ from xmlgenerator import configuration, validation, randomization, substitution, generator
2
8
  from xmlgenerator.arguments import parse_args
3
9
  from xmlgenerator.configuration import load_config
4
10
  from xmlgenerator.generator import XmlGenerator
5
11
  from xmlgenerator.randomization import Randomizer
6
12
  from xmlgenerator.substitution import Substitutor
7
13
  from xmlgenerator.validation import XmlValidator
8
- from xmlschema import XMLSchema
9
-
10
14
 
11
15
  # TODO Generator - обработка стандартных xsd типов
12
16
  # TODO кастомные переменные для локального контекста
13
17
  # TODO валидация по Schematron
14
- # TODO debug logging
15
18
  # TODO типизировать
16
19
  # TODO Почистить и перевести комментарии
17
20
  # TODO Дописать тесты
18
- # TODO нативная сборка
19
- # TODO выкладка на github releases
20
- # TODO опубликовать https://pypi.org/
21
+
22
+ logging.basicConfig(level=logging.WARN, format='%(asctime)s [%(name)-26s] %(levelname)-6s - %(message)s')
23
+
24
+ logger = logging.getLogger('xmlgenerator.bootstrap')
21
25
 
22
26
 
23
27
  def main():
28
+ try:
29
+ _main()
30
+ except KeyboardInterrupt as ex:
31
+ logger.info('processing interrupted')
32
+
33
+
34
+ def _main():
24
35
  args, xsd_files, output_path = parse_args()
36
+ _setup_loggers(args)
25
37
 
26
- config = load_config(args.config_yaml)
38
+ if output_path:
39
+ logger.debug('specified output path: %s', output_path.absolute())
40
+ else:
41
+ logger.debug('output path is not specified. Generated xml will be written to stdout')
27
42
 
28
- print(f"Найдено схем: {len(xsd_files)}")
43
+ config = load_config(args.config_yaml)
29
44
 
30
45
  randomizer = Randomizer(args.seed)
31
46
  substitutor = Substitutor(randomizer)
32
47
  generator = XmlGenerator(randomizer, substitutor)
33
48
  validator = XmlValidator(args.validation, args.fail_fast)
34
49
 
50
+ logger.debug('found %s schemas', len(xsd_files))
35
51
  for xsd_file in xsd_files:
36
- print(f"Processing schema: {xsd_file.name}")
52
+ logger.debug('processing schema: %s', xsd_file.name)
37
53
 
38
54
  # get configuration override for current schema
39
55
  local_config = config.get_for_file(xsd_file.name)
@@ -52,6 +68,7 @@ def main():
52
68
 
53
69
  # Print out to console
54
70
  if not output_path:
71
+ logger.debug('print xml document to stdout')
55
72
  print(decoded)
56
73
 
57
74
  # Validation (if enabled)
@@ -65,9 +82,20 @@ def main():
65
82
  output_file = output_path
66
83
  if output_path.is_dir():
67
84
  output_file = output_path / f'{xml_filename}.xml'
85
+ logger.debug('save xml document as %s', output_file.absolute())
68
86
  with open(output_file, 'wb') as f:
69
87
  f.write(xml_str)
70
- print(f"Saved document: {output_file.name}")
88
+
89
+
90
+ def _setup_loggers(args):
91
+ log_level = logging.DEBUG if args.debug else logging.INFO
92
+ logger.setLevel(log_level)
93
+ configuration.logger.setLevel(log_level)
94
+ validation.logger.setLevel(log_level)
95
+ xmlgenerator.generator.logger.setLevel(log_level)
96
+ substitution.logger.setLevel(log_level)
97
+ randomization.logger.setLevel(log_level)
98
+
71
99
 
72
100
  if __name__ == "__main__":
73
101
  main()
@@ -1,4 +1,5 @@
1
1
  import dataclasses
2
+ import logging
2
3
  import re
3
4
  import sys
4
5
  from dataclasses import dataclass, field, Field
@@ -6,6 +7,8 @@ from typing import Dict, get_args, get_origin, Any
6
7
 
7
8
  import yaml
8
9
 
10
+ logger = logging.getLogger(__name__)
11
+
9
12
 
10
13
  @dataclass
11
14
  class RandomizationConfig:
@@ -45,6 +48,7 @@ class Config:
45
48
  def get_for_file(self, xsd_name):
46
49
  for pattern, conf in self.specific.items():
47
50
  if re.match(pattern, xsd_name):
51
+ logger.debug("resolved configration with pattern '%s'", pattern)
48
52
  base_dict = dataclasses.asdict(self.global_)
49
53
  override_dict = dataclasses.asdict(conf, dict_factory=lambda x: {k: v for (k, v) in x if v is not None})
50
54
  updated_dict = _recursive_update(base_dict, override_dict)
@@ -52,17 +56,23 @@ class Config:
52
56
  local_override = conf.value_override
53
57
  global_override = self.global_.value_override
54
58
  merged_config.value_override = _merge_dicts(local_override, global_override)
59
+ _log_configration('using specific configuration:', merged_config)
55
60
  return merged_config
56
61
 
62
+ _log_configration('using global configration:', self.global_)
57
63
  return self.global_
58
64
 
59
65
 
60
66
  def load_config(file_path: str | None) -> "Config":
61
67
  if not file_path:
62
- return Config()
68
+ config = Config()
69
+ _log_configration("created default configuration:", config)
70
+ return config
63
71
  with open(file_path, 'r') as file:
64
72
  config_data: dict[str, str] = yaml.safe_load(file) or {}
65
- return _map_to_class(config_data, Config, "")
73
+ config = _map_to_class(config_data, Config, "")
74
+ _log_configration(f"configuration loaded from {file_path}:", config)
75
+ return config
66
76
 
67
77
 
68
78
  def _map_to_class(data_dict: dict, cls, parent_path: str):
@@ -80,7 +90,7 @@ def _map_to_class(data_dict: dict, cls, parent_path: str):
80
90
  for yaml_name, value in data_dict.items():
81
91
  class_field_name = yaml_name if yaml_name != "global" else "global_"
82
92
  if class_field_name not in class_fields:
83
- print(f"YAML parse error: unexpected property: {parent_path}.{yaml_name}", file=sys.stderr)
93
+ logger.error('YAML parse error: unexpected property: %s.%s', parent_path, yaml_name)
84
94
  sys.exit(1)
85
95
 
86
96
  # Определяем тип поля
@@ -90,10 +100,10 @@ def _map_to_class(data_dict: dict, cls, parent_path: str):
90
100
  # Проверка на отсутствие обязательных полей
91
101
  missing_fields = required_fields - yaml_items.keys()
92
102
  if missing_fields:
93
- print(f"YAML parse error: missing required properties in {parent_path}:", file=sys.stderr)
103
+ logger.error('YAML parse error: missing required properties in %s:', parent_path)
94
104
  for missing_field in missing_fields:
95
105
  yaml_field_name = missing_field if missing_field != "global_" else "global"
96
- print(yaml_field_name, file=sys.stderr)
106
+ logger.error(yaml_field_name)
97
107
  sys.exit(1)
98
108
 
99
109
  return cls(**yaml_items)
@@ -133,3 +143,12 @@ def _merge_dicts(base_dict, extra_dict):
133
143
  if key not in merged_dict:
134
144
  merged_dict[key] = value
135
145
  return merged_dict
146
+
147
+
148
+ def _log_configration(message, config):
149
+ if logger.isEnabledFor(logging.DEBUG):
150
+ logger.debug(message)
151
+ as_dict = dataclasses.asdict(config)
152
+ dumped = yaml.safe_dump(as_dict, allow_unicode=True, width=float("inf"), sort_keys=False, indent=4)
153
+ for line in dumped.splitlines():
154
+ logger.debug('|\t%s', line)
@@ -1,5 +1,5 @@
1
+ import logging
1
2
  import re
2
- import sys
3
3
 
4
4
  import rstr
5
5
  import xmlschema
@@ -12,6 +12,8 @@ from xmlgenerator.configuration import GeneratorConfig
12
12
  from xmlgenerator.randomization import Randomizer
13
13
  from xmlgenerator.substitution import Substitutor
14
14
 
15
+ logger = logging.getLogger(__name__)
16
+
15
17
 
16
18
  class XmlGenerator:
17
19
  def __init__(self, randomizer: Randomizer, substitutor: Substitutor):
@@ -28,21 +30,27 @@ class XmlGenerator:
28
30
  rnd = self.randomizer.rnd
29
31
 
30
32
  xsd_element_type = getattr(xsd_element, 'type', None)
33
+ logger.debug('fill down element "%s" with type %s', xsd_element.name, type(xsd_element_type).__name__)
31
34
 
32
35
  # Add attributes if they are
33
36
  attributes = getattr(xsd_element, 'attributes', dict())
34
37
  if len(attributes) > 0 and xsd_element_type.local_name != 'anyType':
38
+ logger.debug('add attributes to element %s', xsd_element.name)
35
39
  for attr_name, attr in attributes.items():
40
+ logger.debug('attribute: %s', attr_name)
36
41
  use = attr.use # optional | required | prohibited
37
42
  if use == 'prohibited':
43
+ logger.debug('skipped')
38
44
  continue
39
45
  elif use == 'optional':
40
46
  if rnd.random() > local_config.randomization.probability:
41
- continue # skip optional attribute
47
+ logger.debug('skipped')
48
+ continue # skip optional attribute
42
49
 
43
50
  attr_value = self._generate_value(attr.type, attr_name, local_config)
44
51
  if attr_value is not None:
45
52
  xml_element.set(attr_name, str(attr_value))
53
+ logger.debug(f'attribute %s set with value %s', attr_name, attr_value)
46
54
 
47
55
  # Process child elements --------------------------------------------------------------------------------------
48
56
  if isinstance(xsd_element, XsdElement):
@@ -69,7 +77,7 @@ class XmlGenerator:
69
77
  group_min_occurs = getattr(xsd_element, 'min_occurs', None)
70
78
  group_max_occurs = getattr(xsd_element, 'max_occurs', None)
71
79
  group_min_occurs = group_min_occurs if group_min_occurs is not None else 0
72
- group_max_occurs = group_max_occurs if group_max_occurs is not None else 10 # TODO externalize
80
+ group_max_occurs = group_max_occurs if group_max_occurs is not None else 10 # TODO externalize
73
81
  group_occurs = rnd.randint(group_min_occurs, group_max_occurs)
74
82
 
75
83
  if model == 'all':
@@ -80,7 +88,7 @@ class XmlGenerator:
80
88
  element_min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
81
89
  element_max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
82
90
  element_min_occurs = element_min_occurs if element_min_occurs is not None else 0
83
- element_max_occurs = element_max_occurs if element_max_occurs is not None else 10 # TODO externalize
91
+ element_max_occurs = element_max_occurs if element_max_occurs is not None else 10 # TODO externalize
84
92
  element_occurs = rnd.randint(element_min_occurs, element_max_occurs)
85
93
 
86
94
  for _ in range(element_occurs):
@@ -96,7 +104,7 @@ class XmlGenerator:
96
104
  element_min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
97
105
  element_max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
98
106
  element_min_occurs = element_min_occurs if element_min_occurs is not None else 0
99
- element_max_occurs = element_max_occurs if element_max_occurs is not None else 10 # TODO externalize
107
+ element_max_occurs = element_max_occurs if element_max_occurs is not None else 10 # TODO externalize
100
108
  element_occurs = rnd.randint(element_min_occurs, element_max_occurs)
101
109
 
102
110
  if isinstance(xsd_child_element_type, XsdElement):
@@ -123,7 +131,7 @@ class XmlGenerator:
123
131
  element_min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
124
132
  element_max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
125
133
  element_min_occurs = element_min_occurs if element_min_occurs is not None else 0
126
- element_max_occurs = element_max_occurs if element_max_occurs is not None else 10 # TODO externalize
134
+ element_max_occurs = element_max_occurs if element_max_occurs is not None else 10 # TODO externalize
127
135
  element_occurs = rnd.randint(element_min_occurs, element_max_occurs)
128
136
 
129
137
  for _ in range(element_occurs):
@@ -232,7 +240,6 @@ class XmlGenerator:
232
240
 
233
241
  raise RuntimeError(f"Can't generate value - unhandled type. Target name: {target_name}")
234
242
 
235
-
236
243
  def _generate_value_by_type(self, xsd_type, target_name, patterns, min_length, max_length, min_value, max_value,
237
244
  total_digits, fraction_digits) -> str | None:
238
245
 
@@ -295,11 +302,15 @@ class XmlGenerator:
295
302
  xeger = rstr.xeger(random_pattern.attrib['value'])
296
303
  xeger = re.sub(r'\s', ' ', xeger)
297
304
  if min_length > -1 and len(xeger) < min_length:
298
- print(
299
- f"Possible mistake in schema: {target_name} generated value '{xeger}' can't be shorter than {min_length}",
300
- file=sys.stderr)
305
+ logger.warning(
306
+ "Possible mistake in schema: %s generated value '%s' can't be shorter than %s",
307
+ target_name, xeger, min_length
308
+ )
301
309
  if -1 < max_length < len(xeger):
302
- print(f"Possible mistake in schema: {target_name} generated value '{xeger}' can't be longer than {max_length}", file=sys.stderr)
310
+ logger.warning(
311
+ "Possible mistake in schema: %s generated value '%s' can't be longer than %s",
312
+ target_name, xeger, max_length
313
+ )
303
314
  return xeger
304
315
 
305
316
  # Иначе генерируем случайную строку
@@ -1,12 +1,22 @@
1
+ import logging
1
2
  import random
2
3
  import string
4
+ import sys
3
5
  from datetime import datetime, timedelta
4
6
 
5
7
  from faker import Faker
6
8
 
9
+ logger = logging.getLogger(__name__)
10
+
7
11
 
8
12
  class Randomizer:
9
13
  def __init__(self, seed=None):
14
+ if not seed:
15
+ seed = random.randrange(sys.maxsize)
16
+ logger.debug('initialize with random seed: %s', seed)
17
+ else:
18
+ logger.debug('initialize with provided seed: %s', seed)
19
+
10
20
  self.rnd = random.Random(seed)
11
21
  self.fake = Faker(locale='ru_RU')
12
22
  self.fake.seed_instance(seed)
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import re
2
3
  import uuid
3
4
 
@@ -9,6 +10,8 @@ __all__ = ['Substitutor']
9
10
 
10
11
  _pattern = re.compile(pattern=r'\{\{\s*(?:(?P<function>\S*?)(?:\(\s*(?P<argument>[^)]*)\s*\))?\s*(?:\|\s*(?P<modifier>.*?))?)?\s*}}')
11
12
 
13
+ logger = logging.getLogger(__name__)
14
+
12
15
  class Substitutor:
13
16
  def __init__(self, randomizer: Randomizer):
14
17
  fake = randomizer.fake
@@ -69,6 +72,11 @@ class Substitutor:
69
72
  resolved_value = self._process_expression(output_filename)
70
73
  self._local_context['output_filename'] = resolved_value
71
74
 
75
+ logger.debug('local_context reset')
76
+ logger.debug('local_context["source_filename"] = %s', xsd_filename)
77
+ logger.debug('local_context["source_extracted"] = %s (extracted with regexp %s)', source_extracted, source_filename)
78
+ logger.debug('local_context["output_filename"] = %s', resolved_value)
79
+
72
80
  def get_output_filename(self):
73
81
  return self._local_context.get("output_filename")
74
82
 
@@ -83,6 +91,7 @@ class Substitutor:
83
91
  return False, None
84
92
 
85
93
  def _process_expression(self, expression):
94
+ logger.debug('processing expression: %s', expression)
86
95
  global_context = self._global_context
87
96
  local_context = self._local_context
88
97
  result_value: str = expression
@@ -115,4 +124,5 @@ class Substitutor:
115
124
  for span, replacement in reversed(list(span_to_replacement.items())):
116
125
  result_value = result_value[:span[0]] + replacement + result_value[span[1]:]
117
126
 
127
+ logger.debug('expression resolved to value: %s', result_value)
118
128
  return result_value
@@ -1,7 +1,10 @@
1
+ import logging
1
2
  import sys
2
3
 
3
4
  from xmlschema import XMLSchemaValidationError
4
5
 
6
+ logger = logging.getLogger(__name__)
7
+
5
8
 
6
9
  class XmlValidator:
7
10
  def __init__(self, post_validate: str, fail_fast: bool):
@@ -11,11 +14,13 @@ class XmlValidator:
11
14
  self.validation_func = self._validate_with_schema
12
15
  case 'schematron':
13
16
  self.validation_func = self._validate_with_schematron
17
+ logger.debug("post validation: %s, fail fast: %s", post_validate, fail_fast)
14
18
 
15
19
  def validate(self, xsd_schema, document):
16
20
  self.validation_func(xsd_schema, document)
17
21
 
18
22
  def _validate_with_schema(self, xsd_schema, document):
23
+ logger.debug("validate generated xml with xsd schema")
19
24
  try:
20
25
  xsd_schema.validate(document)
21
26
  except XMLSchemaValidationError as err:
@@ -24,6 +29,7 @@ class XmlValidator:
24
29
  sys.exit(1)
25
30
 
26
31
  def _validate_with_schematron(self, xsd_schema, document):
32
+ logger.debug("validate generated xml with xsd schematron")
27
33
  raise RuntimeError("not yet implemented")
28
34
 
29
35
  # TODO
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xmlgenerator
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Generates XML documents from XSD schemas
5
5
  Home-page: https://github.com/lexakimov/xmlgenerator
6
6
  Author: Alexey Akimov
File without changes
File without changes
File without changes