BODtoJSON 1.5.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bodtojson-1.5.0/src/BODtoJSON.egg-info → bodtojson-2.0.0}/PKG-INFO +12 -13
- {bodtojson-1.5.0 → bodtojson-2.0.0}/README.md +10 -11
- {bodtojson-1.5.0 → bodtojson-2.0.0}/pyproject.toml +2 -2
- bodtojson-2.0.0/src/BODtoJSON/__init__.py +9 -0
- {bodtojson-1.5.0 → bodtojson-2.0.0}/src/BODtoJSON/api.py +3 -3
- bodtojson-2.0.0/src/BODtoJSON/mapper.py +204 -0
- {bodtojson-1.5.0 → bodtojson-2.0.0/src/BODtoJSON.egg-info}/PKG-INFO +12 -13
- bodtojson-1.5.0/src/BODtoJSON/__init__.py +0 -10
- bodtojson-1.5.0/src/BODtoJSON/mapper.py +0 -108
- {bodtojson-1.5.0 → bodtojson-2.0.0}/LICENSE +0 -0
- {bodtojson-1.5.0 → bodtojson-2.0.0}/setup.cfg +0 -0
- {bodtojson-1.5.0 → bodtojson-2.0.0}/src/BODtoJSON.egg-info/SOURCES.txt +0 -0
- {bodtojson-1.5.0 → bodtojson-2.0.0}/src/BODtoJSON.egg-info/dependency_links.txt +0 -0
- {bodtojson-1.5.0 → bodtojson-2.0.0}/src/BODtoJSON.egg-info/requires.txt +0 -0
- {bodtojson-1.5.0 → bodtojson-2.0.0}/src/BODtoJSON.egg-info/top_level.txt +0 -0
- {bodtojson-1.5.0 → bodtojson-2.0.0}/tests/test_api.py +0 -0
- {bodtojson-1.5.0 → bodtojson-2.0.0}/tests/test_mapper.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: BODtoJSON
|
|
3
|
-
Version:
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Zero-dependency OAGIS BOD to Flattened JSON and NDJSON converter
|
|
5
5
|
Author: Niraj Kakodkar
|
|
6
6
|
Project-URL: Homepage, https://github.com/thekakodkar/BODtoJSON
|
|
7
7
|
Classifier: Development Status :: 5 - Production/Stable
|
|
@@ -14,24 +14,23 @@ License-File: LICENSE
|
|
|
14
14
|
Requires-Dist: xmltodict
|
|
15
15
|
Dynamic: license-file
|
|
16
16
|
|
|
17
|
-
# BODtoJSON
|
|
17
|
+
# BODtoJSON v2.0.0
|
|
18
|
+
|
|
19
|
+
**BODtoJSON** is a zero-dependency, enterprise-grade Python data liquidity engine designed to transform complex Infor OAGIS (XML) Business Object Documents into modern, AI-ready flattened JSON and NDJSON structures.
|
|
18
20
|
|
|
19
|
-
**BODtoJSON** is an enterprise-grade Python library designed to transform complex Infor OAGIS (XML) Business Object Documents into modern, AI-ready flattened JSON structures.
|
|
20
21
|
<center><img width="769" height="336" alt="BOD2JSon" src="https://github.com/user-attachments/assets/0b089179-f137-4d56-b717-85cffb20d622" /></center>
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
v2.0.0 marks a major architectural milestone: eliminating all external core dependencies, introducing native single-pass ElementTree traversal, and establishing a dual-pipeline streaming standard for Data Lakehouses and Vector Databases.
|
|
23
24
|
|
|
24
25
|
---
|
|
25
26
|
|
|
26
|
-
## 🚀 What’s New in
|
|
27
|
-
|
|
28
|
-
The transition to v1.5.0 introduces explicit processing pipelines and an enterprise API tier while safeguarding legacy system investments:
|
|
27
|
+
## 🚀 What’s New in v2.0.0 (vs. v1.0.0)
|
|
29
28
|
|
|
30
|
-
* **
|
|
31
|
-
* **
|
|
32
|
-
* **
|
|
33
|
-
* **Enterprise REST API Tier (`api.py`):** Added
|
|
34
|
-
* **Pytest
|
|
29
|
+
* **Zero-Dependency Core Engine:** Removed `xmltodict` in favor of a single-pass recursive traversal built purely on Python’s native `xml.etree.ElementTree`. This eliminates external supply-chain risks, shrinks deployment footprints for air-gapped or serverless environments, and halves intermediate memory allocations.
|
|
30
|
+
* **Explicit Dual-Pipeline Architecture:** Replaced parameter-driven routing with dedicated top-level API methods—`to_json` and `to_ndjson`—improving interface predictability and self-documentation.
|
|
31
|
+
* **Strict NDJSON Linearization:** Features compact serialization via `separators=(',', ':')` and forced line termination (`\n`). Multi-noun batch payloads inside `DataArea` are automatically unpacked and emitted as individual, single-line text records.
|
|
32
|
+
* **Enterprise REST API Tier (`api.py`):** Added an optional, high-performance FastAPI microservice layer featuring zero-overhead streaming using raw `Response` classes, eliminating middleware double-serialization bottlenecks.
|
|
33
|
+
* **Pytest Suite:** Standardized automated quality assurance on **`pytest`**, including regression assertions for deprecation warnings.
|
|
35
34
|
|
|
36
35
|
---
|
|
37
36
|
|
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
# BODtoJSON
|
|
1
|
+
# BODtoJSON v2.0.0
|
|
2
|
+
|
|
3
|
+
**BODtoJSON** is a zero-dependency, enterprise-grade Python data liquidity engine designed to transform complex Infor OAGIS (XML) Business Object Documents into modern, AI-ready flattened JSON and NDJSON structures.
|
|
2
4
|
|
|
3
|
-
**BODtoJSON** is an enterprise-grade Python library designed to transform complex Infor OAGIS (XML) Business Object Documents into modern, AI-ready flattened JSON structures.
|
|
4
5
|
<center><img width="769" height="336" alt="BOD2JSon" src="https://github.com/user-attachments/assets/0b089179-f137-4d56-b717-85cffb20d622" /></center>
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
v2.0.0 marks a major architectural milestone: eliminating all external core dependencies, introducing native single-pass ElementTree traversal, and establishing a dual-pipeline streaming standard for Data Lakehouses and Vector Databases.
|
|
7
8
|
|
|
8
9
|
---
|
|
9
10
|
|
|
10
|
-
## 🚀 What’s New in
|
|
11
|
-
|
|
12
|
-
The transition to v1.5.0 introduces explicit processing pipelines and an enterprise API tier while safeguarding legacy system investments:
|
|
11
|
+
## 🚀 What’s New in v2.0.0 (vs. v1.0.0)
|
|
13
12
|
|
|
14
|
-
* **
|
|
15
|
-
* **
|
|
16
|
-
* **
|
|
17
|
-
* **Enterprise REST API Tier (`api.py`):** Added
|
|
18
|
-
* **Pytest
|
|
13
|
+
* **Zero-Dependency Core Engine:** Removed `xmltodict` in favor of a single-pass recursive traversal built purely on Python’s native `xml.etree.ElementTree`. This eliminates external supply-chain risks, shrinks deployment footprints for air-gapped or serverless environments, and halves intermediate memory allocations.
|
|
14
|
+
* **Explicit Dual-Pipeline Architecture:** Replaced parameter-driven routing with dedicated top-level API methods—`to_json` and `to_ndjson`—improving interface predictability and self-documentation.
|
|
15
|
+
* **Strict NDJSON Linearization:** Features compact serialization via `separators=(',', ':')` and forced line termination (`\n`). Multi-noun batch payloads inside `DataArea` are automatically unpacked and emitted as individual, single-line text records.
|
|
16
|
+
* **Enterprise REST API Tier (`api.py`):** Added an optional, high-performance FastAPI microservice layer featuring zero-overhead streaming using raw `Response` classes, eliminating middleware double-serialization bottlenecks.
|
|
17
|
+
* **Pytest Suite:** Standardized automated quality assurance on **`pytest`**, including regression assertions for deprecation warnings.
|
|
19
18
|
|
|
20
19
|
---
|
|
21
20
|
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "BODtoJSON"
|
|
7
|
-
version = "
|
|
7
|
+
version = "2.0.0"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Niraj Kakodkar" },
|
|
10
10
|
]
|
|
11
|
-
description = "
|
|
11
|
+
description = "Zero-dependency OAGIS BOD to Flattened JSON and NDJSON converter"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.8"
|
|
14
14
|
dependencies = [
|
|
@@ -4,8 +4,8 @@ from BODtoJSON import to_json, to_ndjson, convert
|
|
|
4
4
|
|
|
5
5
|
app = FastAPI(
|
|
6
6
|
title="BODtoJSON API",
|
|
7
|
-
version="
|
|
8
|
-
description="Enterprise OAGIS Integration Data Fabric Engine"
|
|
7
|
+
version="2.0.0",
|
|
8
|
+
description="Zero-Dependency Enterprise OAGIS Integration Data Fabric Engine"
|
|
9
9
|
)
|
|
10
10
|
|
|
11
11
|
# -------------------------------------------------------------------
|
|
@@ -24,7 +24,7 @@ async def root():
|
|
|
24
24
|
return {
|
|
25
25
|
"service": "BODtoJSON",
|
|
26
26
|
"status": "running",
|
|
27
|
-
"version": "
|
|
27
|
+
"version": "2.0.0"
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import xml.etree.ElementTree as ET
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import warnings
|
|
5
|
+
from typing import Dict, Any, List, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BODConverter:
|
|
9
|
+
def __init__(self, pnode: str = 'DataArea', separator: str = '_'):
|
|
10
|
+
self.pnode = pnode
|
|
11
|
+
self.separator = separator
|
|
12
|
+
|
|
13
|
+
def _prepare_noun_element(self, bod_xml: str, verb: str) -> ET.Element:
|
|
14
|
+
"""Cleans XML, strips the administrative Verb, and extracts the primary Noun element."""
|
|
15
|
+
clean_xml = bod_xml.strip().encode('utf-8').decode('utf-8-sig')
|
|
16
|
+
sanitized_xml = re.sub(r'\sxmlns(?::\w+)?="[^"]+"', '', clean_xml, count=1)
|
|
17
|
+
|
|
18
|
+
root = ET.fromstring(sanitized_xml)
|
|
19
|
+
data_area = root.find(f".//{self.pnode}")
|
|
20
|
+
|
|
21
|
+
if data_area is None:
|
|
22
|
+
raise ValueError(f"Target node '{self.pnode}' not found.")
|
|
23
|
+
|
|
24
|
+
verb_node = data_area.find(verb)
|
|
25
|
+
if verb_node is not None:
|
|
26
|
+
data_area.remove(verb_node)
|
|
27
|
+
|
|
28
|
+
children = list(data_area)
|
|
29
|
+
if not children:
|
|
30
|
+
raise ValueError(f"No Noun element found inside '{self.pnode}'.")
|
|
31
|
+
|
|
32
|
+
return children[0]
|
|
33
|
+
|
|
34
|
+
def _strip_ns(self, tag: str) -> str:
|
|
35
|
+
"""Strips any residual {namespace} URI prefixes from ElementTree tags."""
|
|
36
|
+
return tag.split('}', 1)[-1] if '}' in tag else tag
|
|
37
|
+
|
|
38
|
+
def _flatten_element(self, elem: ET.Element, prefix: str = "") -> Dict[str, Any]:
|
|
39
|
+
"""
|
|
40
|
+
Recursively traverses an ElementTree node directly into a flattened dictionary,
|
|
41
|
+
preserving exact xmltodict attribute (@) and text (#text) key parity.
|
|
42
|
+
"""
|
|
43
|
+
flat_dict: Dict[str, Any] = {}
|
|
44
|
+
tag_name = self._strip_ns(elem.tag)
|
|
45
|
+
current_prefix = f"{prefix}{self.separator}{tag_name}" if prefix else tag_name
|
|
46
|
+
|
|
47
|
+
# 1. Extract XML Attributes (e.g., _@accountingEntity)
|
|
48
|
+
for attr_k, attr_v in elem.attrib.items():
|
|
49
|
+
clean_attr = self._strip_ns(attr_k)
|
|
50
|
+
flat_dict[f"{current_prefix}{self.separator}@{clean_attr}"] = attr_v
|
|
51
|
+
|
|
52
|
+
children = list(elem)
|
|
53
|
+
text_val = elem.text.strip() if elem.text and elem.text.strip() else None
|
|
54
|
+
|
|
55
|
+
# 2. Leaf Node Handling
|
|
56
|
+
if not children:
|
|
57
|
+
if elem.attrib:
|
|
58
|
+
# If node has both attributes and text, assign text to _#text
|
|
59
|
+
if text_val is not None:
|
|
60
|
+
flat_dict[f"{current_prefix}{self.separator}#text"] = text_val
|
|
61
|
+
else:
|
|
62
|
+
# Pure text leaf node (maps to None if empty, matching xmltodict parity)
|
|
63
|
+
flat_dict[current_prefix] = text_val
|
|
64
|
+
return flat_dict
|
|
65
|
+
|
|
66
|
+
# 3. Mixed Content Handling (Parent node has children AND direct text)
|
|
67
|
+
if text_val is not None:
|
|
68
|
+
flat_dict[f"{current_prefix}{self.separator}#text"] = text_val
|
|
69
|
+
|
|
70
|
+
# 4. Group Child Elements by Tag Name to Detect Repeating Arrays
|
|
71
|
+
grouped_children: Dict[str, List[ET.Element]] = {}
|
|
72
|
+
for child in children:
|
|
73
|
+
c_tag = self._strip_ns(child.tag)
|
|
74
|
+
grouped_children.setdefault(c_tag, []).append(child)
|
|
75
|
+
|
|
76
|
+
for c_tag, c_list in grouped_children.items():
|
|
77
|
+
if len(c_list) == 1:
|
|
78
|
+
flat_dict.update(self._flatten_element(c_list[0], current_prefix))
|
|
79
|
+
else:
|
|
80
|
+
# Repeating sibling tags get indexed (e.g., _PurchaseOrderLine_0_...)
|
|
81
|
+
for idx, child_elem in enumerate(c_list):
|
|
82
|
+
indexed_prefix = f"{current_prefix}{self.separator}{c_tag}{self.separator}{idx}"
|
|
83
|
+
# Process attributes and children of the indexed element directly
|
|
84
|
+
flat_dict.update(self._flatten_indexed_child(child_elem, indexed_prefix))
|
|
85
|
+
|
|
86
|
+
return flat_dict
|
|
87
|
+
|
|
88
|
+
def _flatten_indexed_child(self, elem: ET.Element, indexed_prefix: str) -> Dict[str, Any]:
|
|
89
|
+
"""Helper to flatten elements inside a repeating sibling list without duplicating the tag name."""
|
|
90
|
+
flat_dict: Dict[str, Any] = {}
|
|
91
|
+
|
|
92
|
+
for attr_k, attr_v in elem.attrib.items():
|
|
93
|
+
clean_attr = self._strip_ns(attr_k)
|
|
94
|
+
flat_dict[f"{indexed_prefix}{self.separator}@{clean_attr}"] = attr_v
|
|
95
|
+
|
|
96
|
+
children = list(elem)
|
|
97
|
+
text_val = elem.text.strip() if elem.text and elem.text.strip() else None
|
|
98
|
+
|
|
99
|
+
if not children:
|
|
100
|
+
if elem.attrib:
|
|
101
|
+
if text_val is not None:
|
|
102
|
+
flat_dict[f"{indexed_prefix}{self.separator}#text"] = text_val
|
|
103
|
+
else:
|
|
104
|
+
flat_dict[indexed_prefix] = text_val
|
|
105
|
+
return flat_dict
|
|
106
|
+
|
|
107
|
+
if text_val is not None:
|
|
108
|
+
flat_dict[f"{indexed_prefix}{self.separator}#text"] = text_val
|
|
109
|
+
|
|
110
|
+
grouped_children: Dict[str, List[ET.Element]] = {}
|
|
111
|
+
for child in children:
|
|
112
|
+
c_tag = self._strip_ns(child.tag)
|
|
113
|
+
grouped_children.setdefault(c_tag, []).append(child)
|
|
114
|
+
|
|
115
|
+
for c_tag, c_list in grouped_children.items():
|
|
116
|
+
if len(c_list) == 1:
|
|
117
|
+
flat_dict.update(self._flatten_element(c_list[0], indexed_prefix))
|
|
118
|
+
else:
|
|
119
|
+
for idx, child_elem in enumerate(c_list):
|
|
120
|
+
sub_prefix = f"{indexed_prefix}{self.separator}{c_tag}{self.separator}{idx}"
|
|
121
|
+
flat_dict.update(self._flatten_indexed_child(child_elem, sub_prefix))
|
|
122
|
+
|
|
123
|
+
return flat_dict
|
|
124
|
+
|
|
125
|
+
def to_json(self, bod_xml: str, verb: str) -> str:
|
|
126
|
+
"""Converts OAGIS XML to a single, standard flat JSON string."""
|
|
127
|
+
try:
|
|
128
|
+
noun_elem = self._prepare_noun_element(bod_xml, verb)
|
|
129
|
+
flattened = self._flatten_element(noun_elem)
|
|
130
|
+
return json.dumps(flattened)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
return json.dumps({"status": "error", "message": str(e)})
|
|
133
|
+
|
|
134
|
+
def to_ndjson(self, bod_xml: str, verb: str) -> str:
|
|
135
|
+
"""
|
|
136
|
+
Converts OAGIS XML into strict NDJSON format.
|
|
137
|
+
Unpacks repeating sibling elements under DataArea into single-line records.
|
|
138
|
+
"""
|
|
139
|
+
try:
|
|
140
|
+
clean_xml = bod_xml.strip().encode('utf-8').decode('utf-8-sig')
|
|
141
|
+
sanitized_xml = re.sub(r'\sxmlns(?::\w+)?="[^"]+"', '', clean_xml, count=1)
|
|
142
|
+
|
|
143
|
+
root = ET.fromstring(sanitized_xml)
|
|
144
|
+
data_area = root.find(f".//{self.pnode}")
|
|
145
|
+
|
|
146
|
+
if data_area is None:
|
|
147
|
+
raise ValueError(f"Target node '{self.pnode}' not found.")
|
|
148
|
+
|
|
149
|
+
verb_node = data_area.find(verb)
|
|
150
|
+
if verb_node is not None:
|
|
151
|
+
data_area.remove(verb_node)
|
|
152
|
+
|
|
153
|
+
noun_nodes = list(data_area)
|
|
154
|
+
if not noun_nodes:
|
|
155
|
+
raise ValueError(f"No Noun element found inside '{self.pnode}'.")
|
|
156
|
+
|
|
157
|
+
lines = []
|
|
158
|
+
for noun_elem in noun_nodes:
|
|
159
|
+
flat_record = self._flatten_element(noun_elem)
|
|
160
|
+
compact_line = json.dumps(flat_record, separators=(',', ':')).strip()
|
|
161
|
+
lines.append(compact_line)
|
|
162
|
+
|
|
163
|
+
return "\n".join(lines) + "\n"
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
return json.dumps({"status": "error", "message": str(e)}) + "\n"
|
|
167
|
+
|
|
168
|
+
def convert(bod_xml: str, verb: str) -> str:
|
|
169
|
+
"""
|
|
170
|
+
LEGACY FUNCTION: Maintained to prevent breaking production changes.
|
|
171
|
+
Maps directly to to_json() behind the scenes.
|
|
172
|
+
"""
|
|
173
|
+
warnings.warn(
|
|
174
|
+
"The 'convert' function is deprecated and will be removed in v3.0.0. Use 'to_json' or 'to_ndjson' instead.",
|
|
175
|
+
DeprecationWarning,
|
|
176
|
+
stacklevel=2
|
|
177
|
+
)
|
|
178
|
+
return to_json(bod_xml, verb)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# -------------------------------------------------------------------
|
|
182
|
+
# Top-Level Facade Functions (Global Access Shorthand)
|
|
183
|
+
# -------------------------------------------------------------------
|
|
184
|
+
def to_json(bod_xml: str, verb: str) -> str:
|
|
185
|
+
"""Primary API for Standard JSON conversion"""
|
|
186
|
+
return BODConverter().to_json(bod_xml, verb)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def to_ndjson(bod_xml: str, verb: str) -> str:
|
|
190
|
+
"""Primary API for Newline Delimited JSON (NDJSON) conversion"""
|
|
191
|
+
return BODConverter().to_ndjson(bod_xml, verb)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def convert(bod_xml: str, verb: str) -> str:
|
|
195
|
+
"""
|
|
196
|
+
LEGACY FUNCTION: Maintained to prevent breaking production changes.
|
|
197
|
+
Maps directly to to_json() behind the scenes.
|
|
198
|
+
"""
|
|
199
|
+
warnings.warn(
|
|
200
|
+
"The 'convert' function is deprecated and will be removed in v2.0.0. Use 'to_json' or 'to_ndjson' instead.",
|
|
201
|
+
DeprecationWarning,
|
|
202
|
+
stacklevel=2
|
|
203
|
+
)
|
|
204
|
+
return to_json(bod_xml, verb)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: BODtoJSON
|
|
3
|
-
Version:
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Zero-dependency OAGIS BOD to Flattened JSON and NDJSON converter
|
|
5
5
|
Author: Niraj Kakodkar
|
|
6
6
|
Project-URL: Homepage, https://github.com/thekakodkar/BODtoJSON
|
|
7
7
|
Classifier: Development Status :: 5 - Production/Stable
|
|
@@ -14,24 +14,23 @@ License-File: LICENSE
|
|
|
14
14
|
Requires-Dist: xmltodict
|
|
15
15
|
Dynamic: license-file
|
|
16
16
|
|
|
17
|
-
# BODtoJSON
|
|
17
|
+
# BODtoJSON v2.0.0
|
|
18
|
+
|
|
19
|
+
**BODtoJSON** is a zero-dependency, enterprise-grade Python data liquidity engine designed to transform complex Infor OAGIS (XML) Business Object Documents into modern, AI-ready flattened JSON and NDJSON structures.
|
|
18
20
|
|
|
19
|
-
**BODtoJSON** is an enterprise-grade Python library designed to transform complex Infor OAGIS (XML) Business Object Documents into modern, AI-ready flattened JSON structures.
|
|
20
21
|
<center><img width="769" height="336" alt="BOD2JSon" src="https://github.com/user-attachments/assets/0b089179-f137-4d56-b717-85cffb20d622" /></center>
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
v2.0.0 marks a major architectural milestone: eliminating all external core dependencies, introducing native single-pass ElementTree traversal, and establishing a dual-pipeline streaming standard for Data Lakehouses and Vector Databases.
|
|
23
24
|
|
|
24
25
|
---
|
|
25
26
|
|
|
26
|
-
## 🚀 What’s New in
|
|
27
|
-
|
|
28
|
-
The transition to v1.5.0 introduces explicit processing pipelines and an enterprise API tier while safeguarding legacy system investments:
|
|
27
|
+
## 🚀 What’s New in v2.0.0 (vs. v1.0.0)
|
|
29
28
|
|
|
30
|
-
* **
|
|
31
|
-
* **
|
|
32
|
-
* **
|
|
33
|
-
* **Enterprise REST API Tier (`api.py`):** Added
|
|
34
|
-
* **Pytest
|
|
29
|
+
* **Zero-Dependency Core Engine:** Removed `xmltodict` in favor of a single-pass recursive traversal built purely on Python’s native `xml.etree.ElementTree`. This eliminates external supply-chain risks, shrinks deployment footprints for air-gapped or serverless environments, and halves intermediate memory allocations.
|
|
30
|
+
* **Explicit Dual-Pipeline Architecture:** Replaced parameter-driven routing with dedicated top-level API methods—`to_json` and `to_ndjson`—improving interface predictability and self-documentation.
|
|
31
|
+
* **Strict NDJSON Linearization:** Features compact serialization via `separators=(',', ':')` and forced line termination (`\n`). Multi-noun batch payloads inside `DataArea` are automatically unpacked and emitted as individual, single-line text records.
|
|
32
|
+
* **Enterprise REST API Tier (`api.py`):** Added an optional, high-performance FastAPI microservice layer featuring zero-overhead streaming using raw `Response` classes, eliminating middleware double-serialization bottlenecks.
|
|
33
|
+
* **Pytest Suite:** Standardized automated quality assurance on **`pytest`**, including regression assertions for deprecation warnings.
|
|
35
34
|
|
|
36
35
|
---
|
|
37
36
|
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
BODtoJSON: A high-performance OAGIS BOD to Flattened JSON/NDJSON converter.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
__version__ = "1.1.0"
|
|
6
|
-
|
|
7
|
-
# Expose both legacy and modern functionality at the top level
|
|
8
|
-
from .mapper import to_json, to_ndjson, convert, BODConverter
|
|
9
|
-
|
|
10
|
-
__all__ = ["to_json", "to_ndjson", "convert", "BODConverter"]
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import xml.etree.ElementTree as ET
|
|
2
|
-
import xmltodict
|
|
3
|
-
import json
|
|
4
|
-
import re
|
|
5
|
-
import warnings
|
|
6
|
-
from typing import Dict, Any
|
|
7
|
-
|
|
8
|
-
class BODConverter:
|
|
9
|
-
def __init__(self, pnode: str = 'DataArea', separator: str = '_'):
|
|
10
|
-
self.pnode = pnode
|
|
11
|
-
self.separator = separator
|
|
12
|
-
|
|
13
|
-
def _prepare_raw_dict(self, bod_xml: str, verb: str) -> Dict[str, Any]:
|
|
14
|
-
"""Internal helper to clean XML, strip verbs, and extract the raw dictionary."""
|
|
15
|
-
clean_xml = bod_xml.strip().encode('utf-8').decode('utf-8-sig')
|
|
16
|
-
sanitized_xml = re.sub(r'\sxmlns(?::\w+)?="[^"]+"', '', clean_xml, count=1)
|
|
17
|
-
|
|
18
|
-
root = ET.fromstring(sanitized_xml)
|
|
19
|
-
data_area = root.find(f".//{self.pnode}")
|
|
20
|
-
|
|
21
|
-
if data_area is None:
|
|
22
|
-
raise ValueError(f"Target node '{self.pnode}' not found.")
|
|
23
|
-
|
|
24
|
-
verb_node = data_area.find(verb)
|
|
25
|
-
if verb_node is not None:
|
|
26
|
-
data_area.remove(verb_node)
|
|
27
|
-
|
|
28
|
-
noun_node = list(data_area)[0]
|
|
29
|
-
xml_fragment = ET.tostring(noun_node, encoding='utf-8').decode('utf-8')
|
|
30
|
-
|
|
31
|
-
return xmltodict.parse(xml_fragment)
|
|
32
|
-
|
|
33
|
-
def _flatify(self, obj: Any, prefix: str = "") -> Dict[str, Any]:
|
|
34
|
-
"""Recursively flattens deeply nested structures into single-level key-values."""
|
|
35
|
-
flat_dict = {}
|
|
36
|
-
if isinstance(obj, dict):
|
|
37
|
-
for k, v in obj.items():
|
|
38
|
-
new_key = f"{prefix}{self.separator}{k}" if prefix else k
|
|
39
|
-
flat_dict.update(self._flatify(v, new_key))
|
|
40
|
-
elif isinstance(obj, (list, tuple)):
|
|
41
|
-
for i, v in enumerate(obj):
|
|
42
|
-
new_key = f"{prefix}{self.separator}{i}"
|
|
43
|
-
flat_dict.update(self._flatify(v, new_key))
|
|
44
|
-
else:
|
|
45
|
-
flat_dict[prefix] = obj
|
|
46
|
-
return flat_dict
|
|
47
|
-
|
|
48
|
-
def to_json(self, bod_xml: str, verb: str) -> str:
|
|
49
|
-
"""Converts OAGIS XML to a single, standard flat JSON string."""
|
|
50
|
-
try:
|
|
51
|
-
raw_dict = self._prepare_raw_dict(bod_xml, verb)
|
|
52
|
-
flattened = self._flatify(raw_dict)
|
|
53
|
-
return json.dumps(flattened)
|
|
54
|
-
except Exception as e:
|
|
55
|
-
return json.dumps({"status": "error", "message": str(e)})
|
|
56
|
-
|
|
57
|
-
def to_ndjson(self, bod_xml: str, verb: str) -> str:
|
|
58
|
-
"""
|
|
59
|
-
Converts OAGIS XML into strict NDJSON format.
|
|
60
|
-
Flattens and compresses individual objects onto single lines terminated by a newline.
|
|
61
|
-
"""
|
|
62
|
-
try:
|
|
63
|
-
raw_dict = self._prepare_raw_dict(bod_xml, verb)
|
|
64
|
-
noun_key = list(raw_dict.keys())[0]
|
|
65
|
-
noun_content = raw_dict[noun_key]
|
|
66
|
-
|
|
67
|
-
if isinstance(noun_content, list):
|
|
68
|
-
lines = []
|
|
69
|
-
for record in noun_content:
|
|
70
|
-
flat_record = self._flatify({noun_key: record})
|
|
71
|
-
compact_line = json.dumps(flat_record, separators=(',', ':')).strip()
|
|
72
|
-
lines.append(compact_line)
|
|
73
|
-
return "\n".join(lines) + "\n"
|
|
74
|
-
|
|
75
|
-
flat_record = self._flatify(raw_dict)
|
|
76
|
-
compact_line = json.dumps(flat_record, separators=(',', ':')).strip()
|
|
77
|
-
return compact_line + "\n"
|
|
78
|
-
|
|
79
|
-
except Exception as e:
|
|
80
|
-
return json.dumps({"status": "error", "message": str(e)}) + "\n"
|
|
81
|
-
|
|
82
|
-
def convert(self, bod_xml: str, verb: str) -> str:
|
|
83
|
-
"""Legacy class method kept for 100% backward compatibility."""
|
|
84
|
-
return self.to_json(bod_xml, verb)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
# -------------------------------------------------------------------
|
|
88
|
-
# Top-Level Facade Functions (Global Access Shorthand)
|
|
89
|
-
# -------------------------------------------------------------------
|
|
90
|
-
def to_json(bod_xml: str, verb: str) -> str:
|
|
91
|
-
"""Primary API for Standard JSON conversion"""
|
|
92
|
-
return BODConverter().to_json(bod_xml, verb)
|
|
93
|
-
|
|
94
|
-
def to_ndjson(bod_xml: str, verb: str) -> str:
|
|
95
|
-
"""Primary API for Newline Delimited JSON (NDJSON) conversion"""
|
|
96
|
-
return BODConverter().to_ndjson(bod_xml, verb)
|
|
97
|
-
|
|
98
|
-
def convert(bod_xml: str, verb: str) -> str:
|
|
99
|
-
"""
|
|
100
|
-
LEGACY FUNCTION: Maintained to prevent breaking production changes.
|
|
101
|
-
Maps directly to to_json() behind the scenes.
|
|
102
|
-
"""
|
|
103
|
-
warnings.warn(
|
|
104
|
-
"The 'convert' function is deprecated and will be removed in v2.0.0. Use 'to_json' or 'to_ndjson' instead.",
|
|
105
|
-
DeprecationWarning,
|
|
106
|
-
stacklevel=2
|
|
107
|
-
)
|
|
108
|
-
return to_json(bod_xml, verb)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|