pyRegRep 13__tar.gz → 14__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,15 +1,25 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyRegRep
3
- Version: 13
4
- Summary: Your package description
5
- Author: Andrey Shapovalov
6
- Author-email: mt.andrey@gmail.com
3
+ Version: 14
4
+ Summary: Бібліотека для роботи з реєстрами та репозитаріями в Україні
5
+ Author-email: Andrii Shapovalov <mt.andrey@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/AndreyShapovalovVN/pyRegRep
8
+ Project-URL: Repository, https://github.com/AndreyShapovalovVN/pyRegRep.git
9
+ Project-URL: Issues, https://github.com/AndreyShapovalovVN/pyRegRep/issues
10
+ Keywords: registry,repositories,ukraine
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
7
18
  Requires-Python: >=3.10
8
19
  Description-Content-Type: text/markdown
9
20
  License-File: LICENSE
10
21
  Requires-Dist: lxml>=5.2.1
11
22
  Requires-Dist: xmltodict>=0.13.0
12
- Dynamic: author-email
13
23
  Dynamic: license-file
14
24
 
15
25
  # pyRegRep
@@ -1,15 +1,25 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyRegRep
3
- Version: 13
4
- Summary: Your package description
5
- Author: Andrey Shapovalov
6
- Author-email: mt.andrey@gmail.com
3
+ Version: 14
4
+ Summary: Бібліотека для роботи з реєстрами та репозитаріями в Україні
5
+ Author-email: Andrii Shapovalov <mt.andrey@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/AndreyShapovalovVN/pyRegRep
8
+ Project-URL: Repository, https://github.com/AndreyShapovalovVN/pyRegRep.git
9
+ Project-URL: Issues, https://github.com/AndreyShapovalovVN/pyRegRep/issues
10
+ Keywords: registry,repositories,ukraine
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
7
18
  Requires-Python: >=3.10
8
19
  Description-Content-Type: text/markdown
9
20
  License-File: LICENSE
10
21
  Requires-Dist: lxml>=5.2.1
11
22
  Requires-Dist: xmltodict>=0.13.0
12
- Dynamic: author-email
13
23
  Dynamic: license-file
14
24
 
15
25
  # pyRegRep
@@ -1,7 +1,6 @@
1
1
  LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
- setup.py
5
4
  pyRegRep.egg-info/PKG-INFO
6
5
  pyRegRep.egg-info/SOURCES.txt
7
6
  pyRegRep.egg-info/dependency_links.txt
@@ -1,6 +1,5 @@
1
1
  """Namespace management for RIM/ebXML documents."""
2
2
 
3
- from typing import Dict
4
3
 
5
4
 
6
5
  class NS:
@@ -19,7 +18,7 @@ class NS:
19
18
 
20
19
  def __init__(self):
21
20
  """Initialize namespace handling."""
22
- self._ns: Dict[str, str] = self.DEFAULT_NAMESPACES.copy() # type: ignore
21
+ self._ns: dict[str, str] = self.DEFAULT_NAMESPACES.copy() # type: ignore
23
22
 
24
23
  def _tname(self, prefix: str, localname: str) -> str:
25
24
  """
@@ -37,7 +36,7 @@ class NS:
37
36
 
38
37
  return f"{{{self._ns[prefix]}}}{localname}"
39
38
 
40
- def _extract_namespaces(self) -> Dict[str, str]:
39
+ def _extract_namespaces(self) -> dict[str, str]:
41
40
  """
42
41
  Extract namespaces from the document.
43
42
  Override this method in subclasses if needed.
@@ -48,7 +47,7 @@ class NS:
48
47
  return self._ns.copy()
49
48
 
50
49
  @property
51
- def ns(self) -> Dict[str, str]:
50
+ def ns(self) -> dict[str, str]:
52
51
  """Get the namespace mappings."""
53
52
  return self._ns.copy()
54
53
 
@@ -1,7 +1,8 @@
1
1
  import datetime
2
2
  import logging
3
3
  from abc import ABC, abstractmethod
4
- from typing import Any, Callable
4
+ from collections.abc import Callable
5
+ from typing import Any, cast
5
6
 
6
7
  from lxml import etree
7
8
 
@@ -33,7 +34,7 @@ class _ElementContainer(NS):
33
34
 
34
35
  @property
35
36
  def text(self) -> bytes:
36
- return etree.tostring(self.element)
37
+ return cast(bytes, etree.tostring(self.element, encoding="utf-8"))
37
38
 
38
39
 
39
40
  class Xml(ABC, _ElementContainer):
@@ -163,7 +164,10 @@ class _InternationalStringValueType(Xml):
163
164
  if element is not None:
164
165
  v.append(element)
165
166
  else:
166
- _logger.warning(f"Unsupported item type in InternationalStringValueType: {type(item)}")
167
+ _logger.warning(
168
+ "Unsupported item type in InternationalStringValueType: %s",
169
+ type(item),
170
+ )
167
171
  else:
168
172
  if isinstance(self.value, etree._Element):
169
173
  v.append(self.value)
@@ -256,7 +260,10 @@ def get_slot(name: str, slot_type: str, value: Any) -> Xml:
256
260
 
257
261
  slot_class = slot_type_map.get(slot_type)
258
262
  if slot_class is None:
259
- raise ValueError(f"Невідомий тип слота: {slot_type}. Підтримувані типи: {list(slot_type_map.keys())}")
263
+ supported_types = list(slot_type_map.keys())
264
+ raise ValueError(
265
+ f"Невідомий тип слота: {slot_type}. Підтримувані типи: {supported_types}"
266
+ )
260
267
 
261
268
  slot = slot_class(value)
262
269
  slot.name = name
@@ -0,0 +1,377 @@
1
+ import logging
2
+ from enum import Enum
3
+ from typing import Any
4
+
5
+ import xmltodict
6
+ from lxml import etree
7
+
8
+ _logger = logging.getLogger(__name__)
9
+
10
+
11
+ def serialize_any_value_type(value: Any) -> Any:
12
+ """Convert XML elements from AnyValueType into plain dictionaries."""
13
+ if isinstance(value, etree._Element):
14
+ try:
15
+ xml_bytes = etree.tostring(value, encoding="utf-8")
16
+ return xmltodict.parse(xml_bytes)
17
+ except Exception as e:
18
+ _logger.warning(f"Не вдалося серіалізувати AnyValueType через xmltodict: {e}")
19
+ return etree.tostring(value, encoding="unicode")
20
+ return value
21
+
22
+
23
+ class ValueTypes(str, Enum):
24
+ """Типи значень у RIM документах."""
25
+ STRING = "StringValueType"
26
+ BOOLEAN = "BooleanValueType"
27
+ INTEGER = "IntegerValueType"
28
+ DATETIME = "DateTimeValueType"
29
+ INTERNATIONAL_STRING = "InternationalStringValueType"
30
+ COLLECTION = "CollectionValueType"
31
+ ANY = "AnyValueType"
32
+
33
+
34
+ class ParsingError(Exception):
35
+ """Базовий клас для помилок парсування."""
36
+ pass
37
+
38
+
39
+ class Parsing:
40
+ """
41
+ Парсер RIM/ebXML документів.
42
+
43
+ Автоматично обробляє namespace, витягує запити, винятки та об'єкти реєстру.
44
+
45
+ Attributes:
46
+ xml: Декодований XML як рядок
47
+ doc: Распарсений XML документ (etree._Element)
48
+ query: Розташування Query елемента у документі або None
49
+ exception: Розташування Exception елемента у документі або None
50
+ objects: Список RegistryObject елементів
51
+ slots: Кешований словник усіх слотів з документу
52
+ """
53
+
54
+ def __init__(self, doc: bytes | str):
55
+
56
+ if isinstance(doc, bytes):
57
+ self.xml = doc.decode("utf-8")
58
+ elif isinstance(doc, str):
59
+ self.xml = doc
60
+ doc = doc.encode("utf-8") # Конвертуємо назад в bytes для lxml
61
+ else:
62
+ raise ParsingError(
63
+ f"Невалідний тип даних для парсування: {type(doc)}. "
64
+ "Очікується bytes або str."
65
+ )
66
+
67
+ try:
68
+ parser = etree.XMLParser(remove_comments=True)
69
+ self.doc = etree.fromstring(doc, parser)
70
+ except etree.XMLSyntaxError as e:
71
+ raise ParsingError(f"Помилка парсування XML: {e}") from e
72
+
73
+ # Обробка namespace
74
+ self._ns = self._extract_namespaces()
75
+
76
+ # Витягування основних елементів
77
+ try:
78
+ self.query = self.doc.find(".//query:Query", namespaces=self._ns)
79
+ except Exception as e:
80
+ _logger.warning(f"Namespace не знайдено: {e}")
81
+ self.query = None
82
+
83
+ try:
84
+ self.exception = self.doc.find(".//rs:Exception", namespaces=self._ns)
85
+ except Exception as e:
86
+ _logger.warning(f"Namespace не знайдено: {e}")
87
+ self.exception = None
88
+
89
+ try:
90
+ self.objects = self.doc.findall(".//rim:RegistryObject", namespaces=self._ns)
91
+ except Exception as e:
92
+ _logger.warning(f"Namespace не знайдено: {e}")
93
+ self.objects = []
94
+
95
+ # Кешований результат
96
+ self._slots_cache: dict[str, Any] | None = None
97
+ self.slots = self.__list_slots()
98
+
99
+ def _extract_namespaces(self) -> dict[str, str]:
100
+ """
101
+ Витягти всі namespace з кореневого елемента XML.
102
+
103
+ Returns:
104
+ Словник у форматі {префікс: URI}
105
+ """
106
+ ns = {}
107
+ for k, v in self.doc.nsmap.items():
108
+ if k is not None and v is not None:
109
+ ns[k] = v
110
+ _logger.debug(f"Знайдено namespace: {k} -> {v}")
111
+ else:
112
+ _logger.warning(f"Пропущено некоректний namespace: {k} -> {v}")
113
+ return ns
114
+
115
+ def __safe_add_slot(
116
+ self,
117
+ target: dict[str, tuple[str, Any]],
118
+ slot: etree._Element,
119
+ context: str,
120
+ ) -> None:
121
+ name = slot.get("name")
122
+ if not name:
123
+ return
124
+
125
+ try:
126
+ target[name] = self.__value(slot)
127
+ except ParsingError as e:
128
+ _logger.exception(f"Помилка парсування слота '{context}/{name}': {e}")
129
+
130
+ def __tname(self, ns: str, tag: str) -> str:
131
+ """
132
+ Побудувати повне ім'я тега з namespace.
133
+
134
+ Args:
135
+ ns: Префікс namespace
136
+ tag: Локальне ім'я тега
137
+
138
+ Returns:
139
+ Повне ім'я тега у форматі {namespace_uri}tag
140
+ """
141
+ if not ns or ns not in self._ns:
142
+ _logger.warning(f"Namespace '{ns}' не знайдено, повертаємо просто тег '{tag}'")
143
+ return tag
144
+ return f"{{{self._ns[ns]}}}{tag}"
145
+
146
+ def __extract_type(self, type_attr: str) -> str:
147
+ """
148
+ Витягти тип значення з атрибута xsi:type.
149
+
150
+ Args:
151
+ type_attr: Атрибут у форматі "префікс:Тип"
152
+
153
+ Returns:
154
+ Само значення типу без префіксу
155
+
156
+ Raises:
157
+ ParsingError: Якщо формат типу невалідний
158
+ """
159
+ if not type_attr:
160
+ raise ParsingError("Атрибут xsi:type не знайдено")
161
+
162
+ if ':' not in type_attr:
163
+ raise ParsingError(f"Невалідний формат типу: {type_attr}. Очікується 'префікс:Тип'")
164
+
165
+ type_value = type_attr.split(":")[1].strip()
166
+ if not type_value:
167
+ raise ParsingError(f"Тип значення не можна витягти з: {type_attr}")
168
+
169
+ return type_value
170
+
171
+ def __get_slot_value(self, slot: etree._Element) -> etree._Element:
172
+ if len(slot) == 0:
173
+ raise ParsingError(f"Слот '{slot.get('name')}' не має дочірніх елементів")
174
+
175
+ return slot[0]
176
+
177
+ def __get_slot_value_type(self, slot_value: etree._Element) -> str:
178
+ type_attr = slot_value.get(self.__tname("xsi", "type"), "")
179
+
180
+ try:
181
+ return self.__extract_type(type_attr)
182
+ except ParsingError as e:
183
+ _logger.exception(f"Помилка витягування типу для слота: {e}")
184
+ raise
185
+
186
+ def __parse_slot_value(self, type_: str, slot_value: etree._Element) -> Any:
187
+ if ValueTypes.ANY.value in type_:
188
+ return self.__parse_any_value(slot_value)
189
+
190
+ if ValueTypes.INTERNATIONAL_STRING.value in type_:
191
+ return self.__parse_international_string(slot_value)
192
+
193
+ if ValueTypes.COLLECTION.value in type_:
194
+ return self.__parse_collection(slot_value)
195
+
196
+ return self.__parse_simple_value(slot_value)
197
+
198
+ def __parse_any_value(self, slot_value: etree._Element) -> Any:
199
+ return slot_value[0] if len(slot_value) > 0 else None
200
+
201
+ def __parse_international_string(self, slot_value: etree._Element) -> list[dict[str, Any]]:
202
+ values: list[dict[str, Any]] = []
203
+
204
+ if len(slot_value) == 0:
205
+ return values
206
+
207
+ for item in slot_value[0]:
208
+ values.append(
209
+ {
210
+ "lang": item.get(self.__tname("xsi", "lang")),
211
+ "value": item.get("value"),
212
+ }
213
+ )
214
+
215
+ return values
216
+
217
+ def __parse_collection(self, slot_value: etree._Element) -> list[tuple[str, Any]]:
218
+ values = []
219
+
220
+ for item in slot_value:
221
+ wrapper = etree.Element("wrapper")
222
+ wrapper.append(item)
223
+ values.append(self.__value(wrapper))
224
+
225
+ return values
226
+
227
+ def __parse_simple_value(self, slot_value: etree._Element) -> Any:
228
+ if len(slot_value) > 0 and len(slot_value[0]) > 0:
229
+ return slot_value[0][0].text
230
+
231
+ if len(slot_value) > 0:
232
+ return slot_value[0].text
233
+
234
+ return None
235
+
236
+ def __value(self, slot: etree._Element) -> tuple[str, Any]:
237
+ slot_value = self.__get_slot_value(slot)
238
+ type_ = self.__get_slot_value_type(slot_value)
239
+
240
+ try:
241
+ return type_, self.__parse_slot_value(type_, slot_value)
242
+ except (IndexError, AttributeError) as e:
243
+ _logger.exception(f"Помилка обробки значення типу {type_}: {e}")
244
+ raise ParsingError(f"Помилка обробки значення: {e}") from e
245
+
246
+ def __process_root_element(
247
+ self,
248
+ element: etree._Element,
249
+ slots: dict[str, dict[str, Any]],
250
+ ) -> None:
251
+ if element.tag == self.__tname("rim", "Slot"):
252
+ self.__safe_add_slot(slots["doc"], element, "doc")
253
+ return
254
+
255
+ if element.tag == self.__tname("query", "Query"):
256
+ self.__process_slot_container(element, slots["query"], "query")
257
+ return
258
+
259
+ if element.tag == self.__tname("rs", "Exception"):
260
+ self.__process_slot_container(element, slots["exception"], "exception")
261
+ return
262
+
263
+ if element.tag == self.__tname("rim", "RegistryObjectList"):
264
+ self.__process_registry_object_list(element, slots["object"])
265
+
266
+ def __process_slot_container(
267
+ self,
268
+ container: etree._Element,
269
+ target: dict[str, Any],
270
+ context: str,
271
+ ) -> None:
272
+ for item in container:
273
+ self.__safe_add_slot(target, item, context)
274
+
275
+ def __process_registry_object_list(
276
+ self,
277
+ registry_object_list: etree._Element,
278
+ target: dict[str, Any],
279
+ ) -> None:
280
+ for registry_object in registry_object_list:
281
+ obj_data = self.__process_registry_object(registry_object)
282
+
283
+ if obj_data:
284
+ target.update(obj_data)
285
+
286
+ def __process_registry_object(
287
+ self,
288
+ registry_object: etree._Element,
289
+ ) -> dict[str, Any]:
290
+ obj_data: dict[str, Any] = {}
291
+
292
+ for child in registry_object:
293
+ if child.tag == self.__tname("rim", "Slot"):
294
+ self.__safe_add_slot(obj_data, child, "object")
295
+
296
+ elif child.tag == self.__tname("rim", "RepositoryItemRef"):
297
+ obj_data["RepositoryItemRef"] = self.__repository_item_ref(child)
298
+
299
+ return obj_data
300
+
301
+ def __repository_item_ref(self, element: etree._Element) -> dict[str, str | None]:
302
+ return {
303
+ "xlink": element.get(self.__tname("xlink", "href")),
304
+ "title": element.get(self.__tname("xlink", "title")),
305
+ }
306
+
307
+ def __list_slots(self) -> dict[str, dict[str, Any]]:
308
+ slots: dict[str, dict[str, Any]] = {
309
+ "doc": {},
310
+ "query": {},
311
+ "exception": {},
312
+ "object": {},
313
+ }
314
+
315
+ for element in self.doc:
316
+ try:
317
+ self.__process_root_element(element, slots)
318
+ except Exception as e:
319
+ _logger.exception(f"Неочікувана помилка при обробці елемента: {e}")
320
+
321
+ return slots
322
+
323
+ def __transform_serialized_value(self, data: Any, any_type: bool) -> Any:
324
+ if isinstance(data, list):
325
+ return [self.__transform_serialized_value(item, any_type) for item in data]
326
+
327
+ if isinstance(data, dict):
328
+ return {
329
+ key: self.__transform_serialized_value(value, any_type)
330
+ for key, value in data.items()
331
+ }
332
+
333
+ if not isinstance(data, tuple) or len(data) != 2:
334
+ return data
335
+
336
+ data_type, value = data
337
+ return self.__transform_value_by_type(data_type, value, any_type)
338
+
339
+ def __transform_value_by_type(
340
+ self,
341
+ data_type: str,
342
+ value: Any,
343
+ any_type: bool,
344
+ ) -> Any:
345
+ if data_type == ValueTypes.BOOLEAN.value:
346
+ return value.lower() == "true" if isinstance(value, str) else bool(value)
347
+
348
+ if data_type in {
349
+ ValueTypes.STRING.value,
350
+ ValueTypes.DATETIME.value,
351
+ }:
352
+ return value.strip() if isinstance(value, str) else value
353
+
354
+ if data_type == ValueTypes.INTEGER.value:
355
+ return int(value) if value else None
356
+
357
+ if data_type == ValueTypes.COLLECTION.value:
358
+ return self.__transform_serialized_value(value, any_type)
359
+
360
+ if data_type == ValueTypes.ANY.value:
361
+ return serialize_any_value_type(value) if any_type else value
362
+
363
+ if data_type == ValueTypes.INTERNATIONAL_STRING.value:
364
+ return self.__transform_serialized_value(value, any_type)
365
+
366
+ _logger.warning(f"Невідомий тип значення: {data_type}")
367
+ return (
368
+ self.__transform_serialized_value(value, any_type)
369
+ if isinstance(value, (list, dict))
370
+ else value
371
+ )
372
+
373
+ def serialize(self, any_type: bool = False) -> dict[str, Any]:
374
+ serialized = self.__transform_serialized_value(self.slots, any_type)
375
+ if isinstance(serialized, dict):
376
+ return serialized
377
+ raise ParsingError("Очікувався словник після серіалізації слотів")
@@ -9,7 +9,8 @@ def deep_get(data: dict[Any, Any], *keys: str, default: Any = None) -> Any:
9
9
  Args:
10
10
  data: Source dictionary.
11
11
  *keys: Sequence of keys to traverse.
12
- default: Value returned when the key path is missing or the intermediate value is not a dict.
12
+ default: Value returned when the key path is missing or the
13
+ intermediate value is not a dict.
13
14
 
14
15
  Returns:
15
16
  Value by key path or "default" when the path cannot be resolved.
@@ -20,4 +21,3 @@ def deep_get(data: dict[Any, Any], *keys: str, default: Any = None) -> Any:
20
21
  return default
21
22
  current = current.get(key)
22
23
  return current if current is not None else default
23
-
@@ -0,0 +1,76 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyRegRep"
7
+ version = "14"
8
+ description = "Бібліотека для роботи з реєстрами та репозитаріями в Україні"
9
+ authors = [
10
+ { name = "Andrii Shapovalov", email = "mt.andrey@gmail.com" }
11
+ ]
12
+ readme = "README.md"
13
+ license = { text = "MIT" }
14
+ requires-python = ">=3.10"
15
+ dependencies = [
16
+ "lxml>=5.2.1",
17
+ "xmltodict>=0.13.0",
18
+ ]
19
+ keywords = ["registry", "repositories", "ukraine"]
20
+ classifiers = [
21
+ "Development Status :: 4 - Beta",
22
+ "Intended Audience :: Developers",
23
+ "License :: OSI Approved :: MIT License",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/AndreyShapovalovVN/pyRegRep"
32
+ Repository = "https://github.com/AndreyShapovalovVN/pyRegRep.git"
33
+ Issues = "https://github.com/AndreyShapovalovVN/pyRegRep/issues"
34
+
35
+ [tool.setuptools]
36
+ packages = ["pyRegRep4"]
37
+ include-package-data = true
38
+
39
+ [tool.pytest.ini_options]
40
+ pythonpath = ["."]
41
+ testpaths = ["tests"]
42
+ addopts = "-v"
43
+
44
+ [tool.coverage.run]
45
+ source = ["pyRegRep4"]
46
+ omit = ["*/tests/*", "*/test_*.py"]
47
+
48
+ [tool.coverage.report]
49
+ exclude_lines = [
50
+ "pragma: no cover",
51
+ "def __repr__",
52
+ "if __name__ == .__main__.:",
53
+ "raise AssertionError",
54
+ "raise NotImplementedError",
55
+ ]
56
+
57
+ [tool.mypy]
58
+ python_version = "3.10"
59
+ warn_return_any = true
60
+ warn_unused_configs = true
61
+ disallow_untyped_defs = false
62
+ disallow_incomplete_defs = false
63
+
64
+ [tool.ruff]
65
+ line-length = 100
66
+ target-version = "py310"
67
+
68
+ [tool.ruff.lint]
69
+ select = ["E", "F", "W", "I", "UP"]
70
+
71
+ [dependency-groups]
72
+ dev = [
73
+ "mypy>=1.10.0",
74
+ "pytest>=8.2.0",
75
+ "ruff>=0.4.0",
76
+ ]
@@ -3,14 +3,16 @@
3
3
  Simple test script to verify get_slot() functionality.
4
4
  """
5
5
 
6
- import sys
7
6
  import datetime
7
+ import sys
8
+
8
9
  from lxml import etree
9
10
 
10
11
  # Add the project root to the path
11
12
  sys.path.insert(0, '/home/andrey/OOTS/pyRegRep')
12
13
 
13
- from pyRegRep4.RIMElement import get_slot, _StringValueType, _AnyValueType
14
+ from pyRegRep4.RIMElement import _AnyValueType, _StringValueType, get_slot
15
+
14
16
 
15
17
  def test_basic_functionality():
16
18
  """Test basic get_slot() functionality."""
@@ -8,20 +8,22 @@ Tests cover:
8
8
  """
9
9
 
10
10
  import datetime
11
+
11
12
  import pytest
12
13
  from lxml import etree
14
+
13
15
  from pyRegRep4.RIMElement import (
14
- get_slot,
15
- _StringValueType,
16
+ Classification,
17
+ QueryResponse,
18
+ RegistryObject,
19
+ RepositoryItemRef,
20
+ _AnyValueType,
16
21
  _BooleanValueType,
17
- _TimeStamp,
18
22
  _CollectionValueType,
19
- _AnyValueType,
20
23
  _InternationalStringValueType,
21
- RegistryObject,
22
- RepositoryItemRef,
23
- QueryResponse,
24
- Classification,
24
+ _StringValueType,
25
+ _TimeStamp,
26
+ get_slot,
25
27
  )
26
28
 
27
29
 
@@ -9,11 +9,12 @@ Tests cover:
9
9
  """
10
10
 
11
11
  import inspect
12
+ from pathlib import Path
13
+
12
14
  import pytest
13
15
  from lxml import etree
14
- from pathlib import Path
15
- from pyRegRep4.RIMParsing import Parsing
16
16
 
17
+ from pyRegRep4.RIMParsing import Parsing
17
18
 
18
19
  # Test data directory
19
20
  TEST_DATA_DIR = Path(__file__).parent
@@ -53,7 +54,11 @@ class TestParsingBasics:
53
54
  assert first_request._ns is not None
54
55
  assert isinstance(first_request._ns, dict)
55
56
  # Common namespaces in RIM documents
56
- assert any(ns for ns in first_request._ns.values() if "rim" in ns.lower() or "regrep" in ns.lower())
57
+ assert any(
58
+ ns
59
+ for ns in first_request._ns.values()
60
+ if "rim" in ns.lower() or "regrep" in ns.lower()
61
+ )
57
62
 
58
63
  def test_slots_structure(self, first_request):
59
64
  """Test that slots dictionary has expected structure."""
@@ -140,7 +145,7 @@ class TestValueTypeParsing:
140
145
  preview = doc_slots.get("PossibilityForPreview")
141
146
 
142
147
  assert preview is not None
143
- type_name, value = preview
148
+ type_name, _ = preview
144
149
  assert "BooleanValueType" in type_name
145
150
 
146
151
  def test_international_string_value_type(self, request_doc):
@@ -194,7 +199,11 @@ class TestSerialization:
194
199
  for key, value in doc_slots.items():
195
200
  if value is not None:
196
201
  # Should not be (type, value) tuples anymore
197
- assert not (isinstance(value, tuple) and len(value) == 2 and isinstance(value[0], str))
202
+ assert not (
203
+ isinstance(value, tuple)
204
+ and len(value) == 2
205
+ and isinstance(value[0], str)
206
+ )
198
207
 
199
208
  def test_serialize_string_value(self, request_doc):
200
209
  """Test serialization of string values."""
@@ -238,7 +247,10 @@ class TestSerialization:
238
247
  def test_serialize_any_value_type_to_dict(self, request_doc):
239
248
  """AnyValueType with any_type=True should be serialized to a plain dictionary."""
240
249
  if not _supports_any_type_argument(request_doc):
241
- pytest.skip("serialize(any_type=...) is not supported in the current Parsing implementation")
250
+ pytest.skip(
251
+ "serialize(any_type=...) is not supported "
252
+ "in the current Parsing implementation"
253
+ )
242
254
 
243
255
  serialized = request_doc.serialize(any_type=True)
244
256
  evidence_provider = serialized.get("doc", {}).get("EvidenceProvider")
@@ -262,7 +274,10 @@ class TestSerialization:
262
274
  def test_serialize_collection_with_any_value_type(self, request_doc):
263
275
  """CollectionValueType with AnyValueType items (any_type=True) should serialize to dicts."""
264
276
  if not _supports_any_type_argument(request_doc):
265
- pytest.skip("serialize(any_type=...) is not supported in the current Parsing implementation")
277
+ pytest.skip(
278
+ "serialize(any_type=...) is not supported "
279
+ "in the current Parsing implementation"
280
+ )
266
281
 
267
282
  serialized = request_doc.serialize(any_type=True)
268
283
  requirements = serialized.get("doc", {}).get("Requirements")
@@ -290,7 +305,6 @@ class TestMultipleFiles:
290
305
  with open(xml_file, "rb") as f:
291
306
  parsing = Parsing(f.read())
292
307
 
293
- assert parsing is not None
294
308
  assert parsing.slots is not None
295
309
  assert isinstance(parsing.slots, dict)
296
310
 
@@ -329,7 +343,6 @@ class TestEdgeCases:
329
343
  xmlns:rs="urn:oasis:names:tc:ebxml-regrep:xsd:rs:4.0">
330
344
  </query:QueryRequest>'''
331
345
  parsing = Parsing(minimal_xml)
332
- assert parsing is not None
333
346
  assert isinstance(parsing.slots, dict)
334
347
 
335
348
  @pytest.fixture
@@ -375,9 +388,12 @@ class TestAnyValueTypeSerialization:
375
388
  assert isinstance(evidence_requester, etree._Element)
376
389
 
377
390
  def test_any_value_type_single_element_serialized(self, second_response_doc):
378
- """Single AnyValueType element (any_type=True) should be dict with namespace-aware keys."""
391
+ """Single AnyValueType element (any_type=True) should be namespace-aware dict."""
379
392
  if not _supports_any_type_argument(second_response_doc):
380
- pytest.skip("serialize(any_type=...) is not supported in the current Parsing implementation")
393
+ pytest.skip(
394
+ "serialize(any_type=...) is not supported "
395
+ "in the current Parsing implementation"
396
+ )
381
397
 
382
398
  serialized = second_response_doc.serialize(any_type=True)
383
399
  evidence_requester = serialized.get("doc", {}).get("EvidenceRequester")
@@ -389,7 +405,10 @@ class TestAnyValueTypeSerialization:
389
405
  def test_any_value_type_preserves_xml_structure(self, second_response_doc):
390
406
  """Serialized AnyValueType should preserve XML structure and content."""
391
407
  if not _supports_any_type_argument(second_response_doc):
392
- pytest.skip("serialize(any_type=...) is not supported in the current Parsing implementation")
408
+ pytest.skip(
409
+ "serialize(any_type=...) is not supported "
410
+ "in the current Parsing implementation"
411
+ )
393
412
 
394
413
  serialized = second_response_doc.serialize(any_type=True)
395
414
  evidence_requester = serialized.get("doc", {}).get("EvidenceRequester")
@@ -405,7 +424,10 @@ class TestAnyValueTypeSerialization:
405
424
  def test_any_value_type_in_objects_section(self, second_response_doc):
406
425
  """AnyValueType in RegistryObject slots should serialize correctly."""
407
426
  if not _supports_any_type_argument(second_response_doc):
408
- pytest.skip("serialize(any_type=...) is not supported in the current Parsing implementation")
427
+ pytest.skip(
428
+ "serialize(any_type=...) is not supported "
429
+ "in the current Parsing implementation"
430
+ )
409
431
 
410
432
  serialized = second_response_doc.serialize(any_type=True)
411
433
  object_slots = serialized.get("object", {})
@@ -415,9 +437,12 @@ class TestAnyValueTypeSerialization:
415
437
  assert isinstance(evidence_metadata, dict)
416
438
 
417
439
  def test_serialize_consistency_across_calls(self, second_response_doc):
418
- """Multiple serialize(any_type=True) calls should produce identical results."""
440
+ """Multiple serialize(any_type=True) calls should return identical results."""
419
441
  if not _supports_any_type_argument(second_response_doc):
420
- pytest.skip("serialize(any_type=...) is not supported in the current Parsing implementation")
442
+ pytest.skip(
443
+ "serialize(any_type=...) is not supported "
444
+ "in the current Parsing implementation"
445
+ )
421
446
 
422
447
  result1 = second_response_doc.serialize(any_type=True)
423
448
  result2 = second_response_doc.serialize(any_type=True)
@@ -427,7 +452,10 @@ class TestAnyValueTypeSerialization:
427
452
  def test_any_type_parameter_does_not_affect_other_types(self, second_response_doc):
428
453
  """Setting any_type=True should not affect serialization of other types."""
429
454
  if not _supports_any_type_argument(second_response_doc):
430
- pytest.skip("serialize(any_type=...) is not supported in the current Parsing implementation")
455
+ pytest.skip(
456
+ "serialize(any_type=...) is not supported "
457
+ "in the current Parsing implementation"
458
+ )
431
459
 
432
460
  serialized_with_any = second_response_doc.serialize(any_type=True)
433
461
  serialized_without_any = second_response_doc.serialize(any_type=False)
@@ -436,4 +464,3 @@ class TestAnyValueTypeSerialization:
436
464
  for key, val in serialized_with_any.get(section, {}).items():
437
465
  if key not in ["EvidenceRequester", "EvidenceProvider", "EvidenceMetadata"]:
438
466
  assert key in serialized_without_any.get(section, {})
439
-
@@ -1,350 +0,0 @@
1
- import logging
2
- from enum import Enum
3
- from typing import Any, Dict, Optional, Tuple
4
-
5
- import xmltodict
6
- from lxml import etree
7
-
8
- _logger = logging.getLogger(__name__)
9
-
10
-
11
- def serialize_any_value_type(value: Any) -> Any:
12
- """Convert XML elements from AnyValueType into plain dictionaries."""
13
- if isinstance(value, etree._Element):
14
- try:
15
- xml_bytes = etree.tostring(value, encoding="utf-8")
16
- return xmltodict.parse(xml_bytes)
17
- except Exception as e:
18
- _logger.warning(f"Не вдалося серіалізувати AnyValueType через xmltodict: {e}")
19
- return etree.tostring(value, encoding="unicode")
20
- return value
21
-
22
-
23
- class ValueTypes(str, Enum):
24
- """Типи значень у RIM документах."""
25
- STRING = "StringValueType"
26
- BOOLEAN = "BooleanValueType"
27
- INTEGER = "IntegerValueType"
28
- DATETIME = "DateTimeValueType"
29
- INTERNATIONAL_STRING = "InternationalStringValueType"
30
- COLLECTION = "CollectionValueType"
31
- ANY = "AnyValueType"
32
-
33
-
34
- class ParsingError(Exception):
35
- """Базовий клас для помилок парсування."""
36
- pass
37
-
38
-
39
- class Parsing:
40
- """
41
- Парсер RIM/ebXML документів.
42
-
43
- Автоматично обробляє namespace, витягує запити, винятки та об'єкти реєстру.
44
-
45
- Attributes:
46
- xml: Декодований XML як рядок
47
- doc: Распарсений XML документ (etree._Element)
48
- query: Розташування Query елемента у документі або None
49
- exception: Розташування Exception елемента у документі або None
50
- objects: Список RegistryObject елементів
51
- slots: Кешований словник усіх слотів з документу
52
- """
53
-
54
- def __init__(self, doc: bytes | str):
55
-
56
- if isinstance(doc, bytes):
57
- self.xml = doc.decode("utf-8")
58
- elif isinstance(doc, str):
59
- self.xml = doc
60
- doc = doc.encode("utf-8") # Конвертуємо назад в bytes для lxml
61
- else:
62
- raise ParsingError(f"Невалідний тип даних для парсування: {type(doc)}. Очікується bytes або str.")
63
-
64
- try:
65
- parser = etree.XMLParser(remove_comments=True)
66
- self.doc = etree.fromstring(doc, parser)
67
- except etree.XMLSyntaxError as e:
68
- raise ParsingError(f"Помилка парсування XML: {e}") from e
69
-
70
- # Обробка namespace
71
- self._ns = self._extract_namespaces()
72
-
73
- # Витягування основних елементів
74
- try:
75
- self.query = self.doc.find(".//query:Query", namespaces=self._ns)
76
- except Exception as e:
77
- _logger.warning(f"Namespace не знайдено: {e}")
78
- self.query = None
79
-
80
- try:
81
- self.exception = self.doc.find(".//rs:Exception", namespaces=self._ns)
82
- except Exception as e:
83
- _logger.warning(f"Namespace не знайдено: {e}")
84
- self.exception = None
85
-
86
- try:
87
- self.objects = self.doc.findall(".//rim:RegistryObject", namespaces=self._ns)
88
- except Exception as e:
89
- _logger.warning(f"Namespace не знайдено: {e}")
90
- self.objects = []
91
-
92
- # Кешований результат
93
- self._slots_cache: Optional[Dict[str, Any]] = None
94
- self.slots = self.__list_slots()
95
-
96
- def _extract_namespaces(self) -> Dict[str, str]:
97
- """
98
- Витягти всі namespace з кореневого елемента XML.
99
-
100
- Returns:
101
- Словник у форматі {префікс: URI}
102
- """
103
- ns = {}
104
- for k, v in self.doc.nsmap.items():
105
- if k is not None and v is not None:
106
- ns[k] = v
107
- _logger.debug(f"Знайдено namespace: {k} -> {v}")
108
- else:
109
- _logger.warning(f"Пропущено некоректний namespace: {k} -> {v}")
110
- return ns
111
-
112
- def __tname(self, ns: str, tag: str) -> str:
113
- """
114
- Побудувати повне ім'я тега з namespace.
115
-
116
- Args:
117
- ns: Префікс namespace
118
- tag: Локальне ім'я тега
119
-
120
- Returns:
121
- Повне ім'я тега у форматі {namespace_uri}tag
122
- """
123
- if not ns or ns not in self._ns:
124
- _logger.warning(f"Namespace '{ns}' не знайдено, повертаємо просто тег '{tag}'")
125
- return tag
126
- return f"{{{self._ns[ns]}}}{tag}"
127
-
128
- def __extract_type(self, type_attr: str) -> str:
129
- """
130
- Витягти тип значення з атрибута xsi:type.
131
-
132
- Args:
133
- type_attr: Атрибут у форматі "префікс:Тип"
134
-
135
- Returns:
136
- Само значення типу без префіксу
137
-
138
- Raises:
139
- ParsingError: Якщо формат типу невалідний
140
- """
141
- if not type_attr:
142
- raise ParsingError("Атрибут xsi:type не знайдено")
143
-
144
- if ':' not in type_attr:
145
- raise ParsingError(f"Невалідний формат типу: {type_attr}. Очікується 'префікс:Тип'")
146
-
147
- type_value = type_attr.split(":")[1].strip()
148
- if not type_value:
149
- raise ParsingError(f"Тип значення не можна витягти з: {type_attr}")
150
-
151
- return type_value
152
-
153
- def __value(self, slot: etree._Element) -> Tuple[str, Any]:
154
- """
155
- Витягти значення зі слота з його типом.
156
-
157
- Args:
158
- slot: XML елемент слота
159
-
160
- Returns:
161
- Кортеж (тип_значення, значення)
162
-
163
- Raises:
164
- ParsingError: Якщо слот має неправильну структуру
165
- """
166
- if len(slot) == 0:
167
- raise ParsingError(f"Слот '{slot.get('name')}' не має дочірніх елементів")
168
-
169
- slot_value = slot[0] # Отримати SlotValue елемент
170
- type_attr = slot_value.get(self.__tname("xsi", "type"), '')
171
-
172
- try:
173
- type_ = self.__extract_type(type_attr)
174
- except ParsingError as e:
175
- _logger.error(f"Помилка витягування типу для слота: {e}")
176
- raise
177
-
178
- # Обробка різних типів значень
179
- try:
180
- if ValueTypes.ANY.value in type_:
181
- return type_, slot_value[0] if len(slot_value) > 0 else None
182
-
183
- elif ValueTypes.INTERNATIONAL_STRING.value in type_:
184
- values = []
185
- for item in slot_value[0]:
186
- value = {
187
- "lang": item.get(self.__tname("xsi", "lang")),
188
- "value": item.get("value"),
189
- }
190
- values.append(value)
191
- return type_, values
192
-
193
- elif ValueTypes.COLLECTION.value in type_:
194
- values = []
195
- for item in slot_value:
196
- wrapper = etree.Element("wrapper")
197
- wrapper.append(item)
198
- values.append(self.__value(wrapper)) # type: ignore
199
- return type_, values
200
-
201
- else:
202
- # Прості типи: String, Boolean, DateTime тощо
203
- if len(slot_value) > 0 and len(slot_value[0]) > 0:
204
- return type_, slot_value[0][0].text
205
- elif len(slot_value) > 0:
206
- return type_, slot_value[0].text
207
- else:
208
- return type_, None
209
- except (IndexError, AttributeError) as e:
210
- _logger.error(f"Помилка обробки значення типу {type_}: {e}")
211
- raise ParsingError(f"Помилка обробки значення: {e}") from e
212
-
213
- def __list_slots(self) -> Dict[str, Dict[str, Tuple[str, Any]]]:
214
- """
215
- Витягти всі слоти з документу та організувати їх за категоріями.
216
-
217
- Returns:
218
- Словник структури: {категорія: {ім'я_слота: (тип, значення)}}
219
- """
220
- slots: Dict[str, Dict[str, Tuple[str, Any]]] = {
221
- "doc": {},
222
- "query": {},
223
- "exception": {},
224
- "object": {}
225
- }
226
-
227
- for element in self.doc:
228
- try:
229
- # Слоти на рівні документу
230
- if element.tag == self.__tname("rim", "Slot"):
231
- name = element.get("name")
232
- if name:
233
- try:
234
- slots["doc"][name] = self.__value(element)
235
- except ParsingError as e:
236
- _logger.error(f"Помилка парсування слота 'doc/{name}': {e}")
237
-
238
- # Query елементи
239
- elif element.tag == self.__tname("query", "Query"):
240
- for query_item in element:
241
- name = query_item.get("name")
242
- if name:
243
- try:
244
- slots["query"][name] = self.__value(query_item)
245
- except ParsingError as e:
246
- _logger.error(f"Помилка парсування слота 'query/{name}': {e}")
247
-
248
- # Exception елементи
249
- elif element.tag == self.__tname("rs", "Exception"):
250
- for exc_item in element:
251
- name = exc_item.get("name")
252
- if name:
253
- try:
254
- slots["exception"][name] = self.__value(exc_item)
255
- except ParsingError as e:
256
- _logger.error(f"Помилка парсування слота 'exception/{name}': {e}")
257
-
258
- # RegistryObject елементи
259
- elif element.tag == self.__tname("rim", "RegistryObjectList"):
260
- for registry_object in element:
261
- obj_data: Dict[str, Any] = {}
262
- for child_element in registry_object:
263
- if child_element.tag == self.__tname("rim", "Slot"):
264
- name = child_element.get("name")
265
- if name:
266
- try:
267
- obj_data[name] = self.__value(child_element)
268
- except ParsingError as e:
269
- _logger.error(f"Помилка парсування слота 'object/{name}': {e}")
270
-
271
- elif child_element.tag == self.__tname("rim", "RepositoryItemRef"):
272
- obj_data["RepositoryItemRef"] = {
273
- "xlink": child_element.get(self.__tname("xlink", "href")),
274
- "title": child_element.get(self.__tname("xlink", "title")),
275
- }
276
-
277
- if obj_data:
278
- slots["object"].update(obj_data)
279
-
280
- except Exception as e:
281
- _logger.error(f"Неочікувана помилка при обробці елемента: {e}")
282
- continue
283
-
284
- return slots
285
-
286
- def serialize(self, any_type: bool = False) -> Dict[str, Any]:
287
- """
288
- Серіалізувати слоти, перетворюючи типізовані значення на Python об'єкти.
289
-
290
- Перетворює:
291
- - BooleanValueType -> bool
292
- - StringValueType -> str
293
- - DateTimeValueType -> str (ISO format)
294
- - InternationalStringValueType -> list[dict]
295
- - CollectionValueType -> list
296
- - AnyValueType -> Element (default) або dict (якщо any_type=True)
297
-
298
- Args:
299
- any_type: Якщо True, перетворює AnyValueType елементи в dict через xmltodict.
300
- Якщо False (за замовчуванням), повертає lxml Element.
301
-
302
- Returns:
303
- Серіалізований словник без типової інформації
304
- """
305
-
306
- def transform_data(data: Any) -> Any:
307
- """Рекурсивно трансформувати дані, видаляючи тип-інформацію."""
308
- if isinstance(data, list):
309
- return [transform_data(item) for item in data]
310
-
311
- elif isinstance(data, dict):
312
- return {k: transform_data(v) for k, v in data.items()}
313
-
314
- elif isinstance(data, tuple) and len(data) == 2:
315
- data_type, value = data
316
-
317
- # Перетворення типів значень
318
- if data_type == ValueTypes.BOOLEAN.value:
319
- if isinstance(value, str):
320
- return value.lower() == 'true'
321
- return bool(value)
322
-
323
- elif data_type == ValueTypes.STRING.value:
324
- return value.strip() if isinstance(value, str) else value
325
-
326
- elif data_type == ValueTypes.DATETIME.value:
327
- return value.strip() if isinstance(value, str) else value
328
-
329
- elif data_type == ValueTypes.COLLECTION.value:
330
- return transform_data(value)
331
-
332
- elif data_type == ValueTypes.ANY.value:
333
- if any_type:
334
- return serialize_any_value_type(value)
335
- return value
336
-
337
- elif data_type == ValueTypes.INTERNATIONAL_STRING.value:
338
- return transform_data(value)
339
-
340
- elif data_type == ValueTypes.INTEGER.value:
341
- return int(value) if value else None
342
-
343
- # Невідомий тип - повернути значення як є
344
- else:
345
- _logger.warning(f"Невідомий тип значення: {data_type}")
346
- return transform_data(value) if isinstance(value, (list, dict)) else value
347
-
348
- return data
349
-
350
- return transform_data(self.slots)
@@ -1,18 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools>=61.0"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "pyRegRep"
7
- version = "13"
8
- description = "Your package description"
9
- authors = [{ name="Andrey Shapovalov" }]
10
- readme = "README.md"
11
- requires-python = ">=3.10"
12
- dependencies = [
13
- "lxml>=5.2.1",
14
- "xmltodict>=0.13.0",
15
- ]
16
-
17
- [tool.pytest.ini_options]
18
- pythonpath = ["."]
pyregrep-13/setup.py DELETED
@@ -1,16 +0,0 @@
1
- from setuptools import setup, find_packages
2
-
3
- setup(
4
- name="pyRegRep",
5
- version="13",
6
- description="Бібліотека для роботи з реєстрами та репозитаріями в Україні",
7
- packages=find_packages(),
8
- license="MIT",
9
- author="Andrii Shapovalov",
10
- author_email="mt.andrey@gmail.com",
11
- include_package_data=True,
12
- install_requires=[
13
- "lxml>=5.2.1",
14
- "xmltodict>=0.13.0",
15
- ],
16
- )
File without changes
File without changes
@@ -16,8 +16,8 @@ from pyRegRep4.RIMElement import (
16
16
  RepositoryItemRef,
17
17
  get_slot,
18
18
  )
19
- from pyRegRep4.utils import deep_get
20
19
  from pyRegRep4.RIMParsing import Parsing, serialize_any_value_type
20
+ from pyRegRep4.utils import deep_get
21
21
 
22
22
  __all__ = [
23
23
  "get_slot",
File without changes
File without changes