Skip to content

Commit 030c593

Browse files
authored
debug logging (#2)
1 parent bb2468c commit 030c593

File tree

8 files changed

+118
-27
lines changed

8 files changed

+118
-27
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
setup(
44
name='xmlgenerator',
5-
version='0.1.0',
5+
version='0.2.0',
66
packages=find_packages(exclude=("tests", "tests.*")),
77
entry_points={
88
'console_scripts': [

xmlgenerator/arguments.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import logging
12
import sys
23
from argparse import ArgumentParser, HelpFormatter
34
from pathlib import Path
45

56
import shtab
67

8+
logger = logging.getLogger(__name__)
9+
710

811
class MyParser(ArgumentParser):
912
def error(self, message):
@@ -99,6 +102,10 @@ def parse_args():
99102
parser = _get_parser()
100103
args = parser.parse_args()
101104

105+
# setup logger
106+
log_level = logging.DEBUG if args.debug else logging.INFO
107+
logger.setLevel(log_level)
108+
102109
if args.config_yaml:
103110
config_path = Path(args.config_yaml)
104111
if not config_path.exists() or not config_path.is_file():

xmlgenerator/bootstrap.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,55 @@
1+
import logging
2+
13
from lxml import etree
4+
from xmlschema import XMLSchema
5+
6+
import xmlgenerator
7+
from xmlgenerator import configuration, validation, randomization, substitution, generator
28
from xmlgenerator.arguments import parse_args
39
from xmlgenerator.configuration import load_config
410
from xmlgenerator.generator import XmlGenerator
511
from xmlgenerator.randomization import Randomizer
612
from xmlgenerator.substitution import Substitutor
713
from xmlgenerator.validation import XmlValidator
8-
from xmlschema import XMLSchema
9-
1014

1115
# TODO Generator - обработка стандартных xsd типов
1216
# TODO кастомные переменные для локального контекста
1317
# TODO валидация по Schematron
14-
# TODO debug logging
1518
# TODO типизировать
1619
# TODO Почистить и перевести комментарии
1720
# 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')
2125

2226

2327
def main():
28+
try:
29+
_main()
30+
except KeyboardInterrupt as ex:
31+
logger.info('processing interrupted')
32+
33+
34+
def _main():
2435
args, xsd_files, output_path = parse_args()
36+
_setup_loggers(args)
2537

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')
2742

28-
print(f"Найдено схем: {len(xsd_files)}")
43+
config = load_config(args.config_yaml)
2944

3045
randomizer = Randomizer(args.seed)
3146
substitutor = Substitutor(randomizer)
3247
generator = XmlGenerator(randomizer, substitutor)
3348
validator = XmlValidator(args.validation, args.fail_fast)
3449

50+
logger.debug('found %s schemas', len(xsd_files))
3551
for xsd_file in xsd_files:
36-
print(f"Processing schema: {xsd_file.name}")
52+
logger.debug('processing schema: %s', xsd_file.name)
3753

3854
# get configuration override for current schema
3955
local_config = config.get_for_file(xsd_file.name)
@@ -52,6 +68,7 @@ def main():
5268

5369
# Print out to console
5470
if not output_path:
71+
logger.debug('print xml document to stdout')
5572
print(decoded)
5673

5774
# Validation (if enabled)
@@ -65,9 +82,20 @@ def main():
6582
output_file = output_path
6683
if output_path.is_dir():
6784
output_file = output_path / f'{xml_filename}.xml'
85+
logger.debug('save xml document as %s', output_file.absolute())
6886
with open(output_file, 'wb') as f:
6987
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+
7199

72100
if __name__ == "__main__":
73101
main()

xmlgenerator/configuration.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import dataclasses
2+
import logging
23
import re
34
import sys
45
from dataclasses import dataclass, field, Field
56
from typing import Dict, get_args, get_origin, Any
67

78
import yaml
89

10+
logger = logging.getLogger(__name__)
11+
912

1013
@dataclass
1114
class RandomizationConfig:
@@ -45,24 +48,31 @@ class Config:
4548
def get_for_file(self, xsd_name):
4649
for pattern, conf in self.specific.items():
4750
if re.match(pattern, xsd_name):
51+
logger.debug("resolved configration with pattern '%s'", pattern)
4852
base_dict = dataclasses.asdict(self.global_)
4953
override_dict = dataclasses.asdict(conf, dict_factory=lambda x: {k: v for (k, v) in x if v is not None})
5054
updated_dict = _recursive_update(base_dict, override_dict)
5155
merged_config = _map_to_class(updated_dict, GeneratorConfig, "")
5256
local_override = conf.value_override
5357
global_override = self.global_.value_override
5458
merged_config.value_override = _merge_dicts(local_override, global_override)
59+
_log_configration('using specific configuration:', merged_config)
5560
return merged_config
5661

62+
_log_configration('using global configration:', self.global_)
5763
return self.global_
5864

5965

6066
def load_config(file_path: str | None) -> "Config":
6167
if not file_path:
62-
return Config()
68+
config = Config()
69+
_log_configration("created default configuration:", config)
70+
return config
6371
with open(file_path, 'r') as file:
6472
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
6676

6777

6878
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):
8090
for yaml_name, value in data_dict.items():
8191
class_field_name = yaml_name if yaml_name != "global" else "global_"
8292
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)
8494
sys.exit(1)
8595

8696
# Определяем тип поля
@@ -90,10 +100,10 @@ def _map_to_class(data_dict: dict, cls, parent_path: str):
90100
# Проверка на отсутствие обязательных полей
91101
missing_fields = required_fields - yaml_items.keys()
92102
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)
94104
for missing_field in missing_fields:
95105
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)
97107
sys.exit(1)
98108

99109
return cls(**yaml_items)
@@ -133,3 +143,12 @@ def _merge_dicts(base_dict, extra_dict):
133143
if key not in merged_dict:
134144
merged_dict[key] = value
135145
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)

xmlgenerator/generator.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import logging
12
import re
2-
import sys
33

44
import rstr
55
import xmlschema
@@ -12,6 +12,8 @@
1212
from xmlgenerator.randomization import Randomizer
1313
from xmlgenerator.substitution import Substitutor
1414

15+
logger = logging.getLogger(__name__)
16+
1517

1618
class XmlGenerator:
1719
def __init__(self, randomizer: Randomizer, substitutor: Substitutor):
@@ -28,21 +30,27 @@ def _add_elements(self, xml_element: etree.Element, xsd_element, local_config: G
2830
rnd = self.randomizer.rnd
2931

3032
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__)
3134

3235
# Add attributes if they are
3336
attributes = getattr(xsd_element, 'attributes', dict())
3437
if len(attributes) > 0 and xsd_element_type.local_name != 'anyType':
38+
logger.debug('add attributes to element %s', xsd_element.name)
3539
for attr_name, attr in attributes.items():
40+
logger.debug('attribute: %s', attr_name)
3641
use = attr.use # optional | required | prohibited
3742
if use == 'prohibited':
43+
logger.debug('skipped')
3844
continue
3945
elif use == 'optional':
4046
if rnd.random() > local_config.randomization.probability:
41-
continue # skip optional attribute
47+
logger.debug('skipped')
48+
continue # skip optional attribute
4249

4350
attr_value = self._generate_value(attr.type, attr_name, local_config)
4451
if attr_value is not None:
4552
xml_element.set(attr_name, str(attr_value))
53+
logger.debug(f'attribute %s set with value %s', attr_name, attr_value)
4654

4755
# Process child elements --------------------------------------------------------------------------------------
4856
if isinstance(xsd_element, XsdElement):
@@ -69,7 +77,7 @@ def _add_elements(self, xml_element: etree.Element, xsd_element, local_config: G
6977
group_min_occurs = getattr(xsd_element, 'min_occurs', None)
7078
group_max_occurs = getattr(xsd_element, 'max_occurs', None)
7179
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
7381
group_occurs = rnd.randint(group_min_occurs, group_max_occurs)
7482

7583
if model == 'all':
@@ -80,7 +88,7 @@ def _add_elements(self, xml_element: etree.Element, xsd_element, local_config: G
8088
element_min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
8189
element_max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
8290
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
8492
element_occurs = rnd.randint(element_min_occurs, element_max_occurs)
8593

8694
for _ in range(element_occurs):
@@ -96,7 +104,7 @@ def _add_elements(self, xml_element: etree.Element, xsd_element, local_config: G
96104
element_min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
97105
element_max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
98106
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
100108
element_occurs = rnd.randint(element_min_occurs, element_max_occurs)
101109

102110
if isinstance(xsd_child_element_type, XsdElement):
@@ -123,7 +131,7 @@ def _add_elements(self, xml_element: etree.Element, xsd_element, local_config: G
123131
element_min_occurs = getattr(xsd_child_element_type, 'min_occurs', None)
124132
element_max_occurs = getattr(xsd_child_element_type, 'max_occurs', None)
125133
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
127135
element_occurs = rnd.randint(element_min_occurs, element_max_occurs)
128136

129137
for _ in range(element_occurs):
@@ -232,7 +240,6 @@ def _generate_value(self, xsd_type, target_name, local_config: GeneratorConfig)
232240

233241
raise RuntimeError(f"Can't generate value - unhandled type. Target name: {target_name}")
234242

235-
236243
def _generate_value_by_type(self, xsd_type, target_name, patterns, min_length, max_length, min_value, max_value,
237244
total_digits, fraction_digits) -> str | None:
238245

@@ -295,11 +302,15 @@ def _generate_string(self, target_name, patterns, min_length, max_length):
295302
xeger = rstr.xeger(random_pattern.attrib['value'])
296303
xeger = re.sub(r'\s', ' ', xeger)
297304
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+
)
301309
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+
)
303314
return xeger
304315

305316
# Иначе генерируем случайную строку

xmlgenerator/randomization.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
1+
import logging
12
import random
23
import string
4+
import sys
35
from datetime import datetime, timedelta
46

57
from faker import Faker
68

9+
logger = logging.getLogger(__name__)
10+
711

812
class Randomizer:
913
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+
1020
self.rnd = random.Random(seed)
1121
self.fake = Faker(locale='ru_RU')
1222
self.fake.seed_instance(seed)

xmlgenerator/substitution.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import re
23
import uuid
34

@@ -9,6 +10,8 @@
910

1011
_pattern = re.compile(pattern=r'\{\{\s*(?:(?P<function>\S*?)(?:\(\s*(?P<argument>[^)]*)\s*\))?\s*(?:\|\s*(?P<modifier>.*?))?)?\s*}}')
1112

13+
logger = logging.getLogger(__name__)
14+
1215
class Substitutor:
1316
def __init__(self, randomizer: Randomizer):
1417
fake = randomizer.fake
@@ -69,6 +72,11 @@ def reset_context(self, xsd_filename, config_local):
6972
resolved_value = self._process_expression(output_filename)
7073
self._local_context['output_filename'] = resolved_value
7174

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+
7280
def get_output_filename(self):
7381
return self._local_context.get("output_filename")
7482

@@ -83,6 +91,7 @@ def substitute_value(self, target_name, items):
8391
return False, None
8492

8593
def _process_expression(self, expression):
94+
logger.debug('processing expression: %s', expression)
8695
global_context = self._global_context
8796
local_context = self._local_context
8897
result_value: str = expression
@@ -115,4 +124,5 @@ def _process_expression(self, expression):
115124
for span, replacement in reversed(list(span_to_replacement.items())):
116125
result_value = result_value[:span[0]] + replacement + result_value[span[1]:]
117126

127+
logger.debug('expression resolved to value: %s', result_value)
118128
return result_value

0 commit comments

Comments
 (0)