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.
- {pyregrep-13 → pyregrep-14}/PKG-INFO +15 -5
- {pyregrep-13 → pyregrep-14}/pyRegRep.egg-info/PKG-INFO +15 -5
- {pyregrep-13 → pyregrep-14}/pyRegRep.egg-info/SOURCES.txt +0 -1
- {pyregrep-13 → pyregrep-14}/pyRegRep4/NS.py +3 -4
- {pyregrep-13 → pyregrep-14}/pyRegRep4/RIMElement.py +11 -4
- pyregrep-14/pyRegRep4/RIMParsing.py +377 -0
- {pyregrep-13 → pyregrep-14}/pyRegRep4/utils.py +2 -2
- pyregrep-14/pyproject.toml +76 -0
- {pyregrep-13 → pyregrep-14}/tests/test_get_slot_simple.py +4 -2
- {pyregrep-13 → pyregrep-14}/tests/test_rim_element.py +10 -8
- {pyregrep-13 → pyregrep-14}/tests/test_rim_parsing.py +44 -17
- pyregrep-13/pyRegRep4/RIMParsing.py +0 -350
- pyregrep-13/pyproject.toml +0 -18
- pyregrep-13/setup.py +0 -16
- {pyregrep-13 → pyregrep-14}/LICENSE +0 -0
- {pyregrep-13 → pyregrep-14}/README.md +0 -0
- {pyregrep-13 → pyregrep-14}/pyRegRep.egg-info/dependency_links.txt +0 -0
- {pyregrep-13 → pyregrep-14}/pyRegRep.egg-info/requires.txt +0 -0
- {pyregrep-13 → pyregrep-14}/pyRegRep.egg-info/top_level.txt +0 -0
- {pyregrep-13 → pyregrep-14}/pyRegRep4/__init__.py +1 -1
- {pyregrep-13 → pyregrep-14}/setup.cfg +0 -0
- {pyregrep-13 → pyregrep-14}/tests/test_utils.py +0 -0
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyRegRep
|
|
3
|
-
Version:
|
|
4
|
-
Summary:
|
|
5
|
-
Author:
|
|
6
|
-
|
|
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:
|
|
4
|
-
Summary:
|
|
5
|
-
Author:
|
|
6
|
-
|
|
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,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:
|
|
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) ->
|
|
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) ->
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
+
Classification,
|
|
17
|
+
QueryResponse,
|
|
18
|
+
RegistryObject,
|
|
19
|
+
RepositoryItemRef,
|
|
20
|
+
_AnyValueType,
|
|
16
21
|
_BooleanValueType,
|
|
17
|
-
_TimeStamp,
|
|
18
22
|
_CollectionValueType,
|
|
19
|
-
_AnyValueType,
|
|
20
23
|
_InternationalStringValueType,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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(
|
|
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,
|
|
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 (
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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)
|
pyregrep-13/pyproject.toml
DELETED
|
@@ -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
|
|
File without changes
|
|
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
|