smartXML 1.0.1__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.
- smartxml-1.0.1/PKG-INFO +45 -0
- smartxml-1.0.1/README.md +24 -0
- smartxml-1.0.1/pyproject.toml +42 -0
- smartxml-1.0.1/setup.cfg +4 -0
- smartxml-1.0.1/src/smartXML/__init__.py +0 -0
- smartxml-1.0.1/src/smartXML/_elements_utils.py +75 -0
- smartxml-1.0.1/src/smartXML/element.py +239 -0
- smartxml-1.0.1/src/smartXML/xmltree.py +302 -0
- smartxml-1.0.1/src/smartXML.egg-info/PKG-INFO +45 -0
- smartxml-1.0.1/src/smartXML.egg-info/SOURCES.txt +12 -0
- smartxml-1.0.1/src/smartXML.egg-info/dependency_links.txt +1 -0
- smartxml-1.0.1/src/smartXML.egg-info/requires.txt +6 -0
- smartxml-1.0.1/src/smartXML.egg-info/top_level.txt +1 -0
- smartxml-1.0.1/tests/test.py +1967 -0
smartxml-1.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smartXML
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: smartXML package enables you to read, search, manipulate, and write XML files with ease
|
|
5
|
+
Author-email: Dudu Arbel <duduarbel@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: python,example
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: requests>=2.31
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
19
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
20
|
+
Requires-Dist: ruff>=0.3; extra == "dev"
|
|
21
|
+
|
|
22
|
+
# smartXML
|
|
23
|
+
|
|
24
|
+
The **smartXML** package enables you to read, search, manipulate, and write XML files with ease.
|
|
25
|
+
|
|
26
|
+
The API is designed to be as simple as possible, but it will be enhanced according to usage and requests.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Usage Example
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from smartxml import SmartXML, TextOnlyComment
|
|
35
|
+
|
|
36
|
+
input_file = Path('./example.xml')
|
|
37
|
+
xml = SmartXML(input_file)
|
|
38
|
+
|
|
39
|
+
firstName = xml.find('students|student|firstName', with_content='Bob')
|
|
40
|
+
bob = firstName.parent
|
|
41
|
+
bob.comment_out()
|
|
42
|
+
header = TextOnlyComment('Bob is out')
|
|
43
|
+
header.add_before(bob)
|
|
44
|
+
|
|
45
|
+
xml.write()
|
smartxml-1.0.1/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# smartXML
|
|
2
|
+
|
|
3
|
+
The **smartXML** package enables you to read, search, manipulate, and write XML files with ease.
|
|
4
|
+
|
|
5
|
+
The API is designed to be as simple as possible, but it will be enhanced according to usage and requests.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Usage Example
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from smartxml import SmartXML, TextOnlyComment
|
|
14
|
+
|
|
15
|
+
input_file = Path('./example.xml')
|
|
16
|
+
xml = SmartXML(input_file)
|
|
17
|
+
|
|
18
|
+
firstName = xml.find('students|student|firstName', with_content='Bob')
|
|
19
|
+
bob = firstName.parent
|
|
20
|
+
bob.comment_out()
|
|
21
|
+
header = TextOnlyComment('Bob is out')
|
|
22
|
+
header.add_before(bob)
|
|
23
|
+
|
|
24
|
+
xml.write()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "smartXML"
|
|
7
|
+
version = "1.0.1"
|
|
8
|
+
description = "smartXML package enables you to read, search, manipulate, and write XML files with ease"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Dudu Arbel", email = "duduarbel@gmail.com" }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
keywords = ["python", "example"]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
"requests>=2.31",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=8.0",
|
|
34
|
+
"mypy>=1.8",
|
|
35
|
+
"ruff>=0.3",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools]
|
|
39
|
+
package-dir = { "" = "src" }
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
smartxml-1.0.1/setup.cfg
ADDED
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
def _find_one_in_sons(
|
|
2
|
+
element: "Element",
|
|
3
|
+
names_list: list[str],
|
|
4
|
+
with_content: str = None,
|
|
5
|
+
) -> "Element":
|
|
6
|
+
if not names_list:
|
|
7
|
+
return element
|
|
8
|
+
for name in names_list:
|
|
9
|
+
for son in element._sons:
|
|
10
|
+
if _check_match(son, name):
|
|
11
|
+
found = _find_one_in_sons(son, names_list[1:], with_content)
|
|
12
|
+
if found:
|
|
13
|
+
if with_content is None or found.content == with_content:
|
|
14
|
+
return found
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _check_match(element: "Element", names: str) -> bool:
|
|
19
|
+
if names and element.name != names:
|
|
20
|
+
return False
|
|
21
|
+
return True
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _find_one(element: "Element", names: str, with_content: str) -> "Element":
|
|
25
|
+
|
|
26
|
+
if _check_match(element, names):
|
|
27
|
+
if with_content is None or element.content == with_content:
|
|
28
|
+
return element
|
|
29
|
+
|
|
30
|
+
names_list = names.split("|")
|
|
31
|
+
|
|
32
|
+
if len(names_list) > 1:
|
|
33
|
+
if element.name == names_list[0]:
|
|
34
|
+
found = _find_one_in_sons(element, names_list[1:], with_content)
|
|
35
|
+
if found:
|
|
36
|
+
return found
|
|
37
|
+
|
|
38
|
+
for son in element._sons:
|
|
39
|
+
found = _find_one(son, names, with_content)
|
|
40
|
+
if found:
|
|
41
|
+
return found
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _find_all(element: "Element", names: str, with_content: str) -> list["Element"]:
|
|
46
|
+
results = []
|
|
47
|
+
if _check_match(element, names=names):
|
|
48
|
+
if with_content is None or element.content == with_content:
|
|
49
|
+
results.extend([element])
|
|
50
|
+
for son in element._sons:
|
|
51
|
+
results.extend(_find_all(son, names, with_content))
|
|
52
|
+
return results
|
|
53
|
+
|
|
54
|
+
names_list = names.split("|")
|
|
55
|
+
|
|
56
|
+
if _check_match(element, names_list[0]):
|
|
57
|
+
if with_content is None or element.content == with_content:
|
|
58
|
+
sons = []
|
|
59
|
+
sons.extend(element._sons)
|
|
60
|
+
match = []
|
|
61
|
+
for index, name in enumerate(names_list[1:]):
|
|
62
|
+
for son in sons:
|
|
63
|
+
if son.name == name:
|
|
64
|
+
if index == len(names_list) - 2:
|
|
65
|
+
results.append(son)
|
|
66
|
+
else:
|
|
67
|
+
match.extend(son._sons)
|
|
68
|
+
sons.clear()
|
|
69
|
+
sons.extend(match)
|
|
70
|
+
match.clear()
|
|
71
|
+
|
|
72
|
+
for son in element._sons:
|
|
73
|
+
results.extend(_find_all(son, names, with_content))
|
|
74
|
+
|
|
75
|
+
return results
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
from ._elements_utils import (
|
|
4
|
+
_find_one,
|
|
5
|
+
_find_all,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
class IllegalOperation(Exception):
|
|
9
|
+
def __init__(self, message: str):
|
|
10
|
+
self.message = message
|
|
11
|
+
super().__init__(self.message)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ElementBase:
|
|
16
|
+
def __init__(self, name: str):
|
|
17
|
+
self._name = name
|
|
18
|
+
self._sons = []
|
|
19
|
+
self._parent = None
|
|
20
|
+
|
|
21
|
+
def is_comment(self) -> bool:
|
|
22
|
+
"""Check if the element is a comment."""
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def parent(self):
|
|
27
|
+
"""Get the parent of the element."""
|
|
28
|
+
return self._parent
|
|
29
|
+
@property
|
|
30
|
+
def name(self) -> str:
|
|
31
|
+
"""Get the name of the element."""
|
|
32
|
+
return self._name
|
|
33
|
+
|
|
34
|
+
@name.setter
|
|
35
|
+
def name(self, new_name: str):
|
|
36
|
+
"""Set the name of the element."""
|
|
37
|
+
if not new_name or new_name[0].isdigit():
|
|
38
|
+
raise ValueError(f"Invalid tag name '{new_name}'")
|
|
39
|
+
self._name = new_name
|
|
40
|
+
|
|
41
|
+
def to_string(self, indentation: str = "\t") -> str:
|
|
42
|
+
"""
|
|
43
|
+
Convert the XML tree to a string.
|
|
44
|
+
:param indentation: string used for indentation, default is tab character
|
|
45
|
+
:return: XML string
|
|
46
|
+
"""
|
|
47
|
+
return self._to_string(0, indentation)
|
|
48
|
+
|
|
49
|
+
def _to_string(self, index: int, indentation: str) -> str:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
def get_path(self) -> str:
|
|
53
|
+
""" Get the full path of the element
|
|
54
|
+
returns: the path as a string from the root of the XML tree, separated by |.
|
|
55
|
+
"""
|
|
56
|
+
elements = []
|
|
57
|
+
current = self
|
|
58
|
+
while current is not None:
|
|
59
|
+
elements.append(current._name)
|
|
60
|
+
current = current._parent
|
|
61
|
+
return "|".join(reversed(elements))
|
|
62
|
+
|
|
63
|
+
def add_before(self, sibling: "Element"):
|
|
64
|
+
"""Add this element before the given sibling element."""
|
|
65
|
+
parent = sibling._parent
|
|
66
|
+
if parent is None:
|
|
67
|
+
raise ValueError(f"Element {sibling.name} has no parent")
|
|
68
|
+
index = parent._sons.index(sibling)
|
|
69
|
+
parent._sons.insert(index, self)
|
|
70
|
+
self._parent = parent
|
|
71
|
+
|
|
72
|
+
def add_after(self, sibling: "Element"):
|
|
73
|
+
"""Add this element after the given sibling element."""
|
|
74
|
+
parent = sibling._parent
|
|
75
|
+
if parent is None:
|
|
76
|
+
raise ValueError(f"Element {sibling.name} has no parent")
|
|
77
|
+
index = parent._sons.index(sibling)
|
|
78
|
+
parent._sons.insert(index + 1, self)
|
|
79
|
+
self._parent = parent
|
|
80
|
+
|
|
81
|
+
def add_as_son_of(self, parent: "Element"):
|
|
82
|
+
"""Add this element as a son of the given parent element."""
|
|
83
|
+
parent._sons.append(self)
|
|
84
|
+
self._parent = parent
|
|
85
|
+
|
|
86
|
+
def set_as_parent_of(self, son: "Element"):
|
|
87
|
+
"""Set this element as the parent of the given son element."""
|
|
88
|
+
self._sons.append(son)
|
|
89
|
+
son._parent = self
|
|
90
|
+
|
|
91
|
+
def remove(self):
|
|
92
|
+
"""Remove this element from its parent's sons."""
|
|
93
|
+
self._parent._sons.remove(self)
|
|
94
|
+
self._parent = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TextOnlyComment(ElementBase):
|
|
98
|
+
"""A comment that only contains text, not other elements."""
|
|
99
|
+
def __init__(self, text: str):
|
|
100
|
+
super().__init__("")
|
|
101
|
+
self._text = text
|
|
102
|
+
|
|
103
|
+
def is_comment(self) -> bool:
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
def _to_string(self, index: int, indentation: str) -> str:
|
|
107
|
+
indent = indentation * index
|
|
108
|
+
return f"{indent}<!-- {self._text} -->\n"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class CData(ElementBase):
|
|
112
|
+
"""A CDATA section that contains text."""
|
|
113
|
+
def __init__(self, text: str):
|
|
114
|
+
super().__init__("")
|
|
115
|
+
self._text = text
|
|
116
|
+
|
|
117
|
+
def _to_string(self, index: int, indentation: str) -> str:
|
|
118
|
+
indent = indentation * index
|
|
119
|
+
return f"{indent}<![CDATA[{self._text}]]>\n"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class Doctype(ElementBase):
|
|
123
|
+
"""A DOCTYPE declaration."""
|
|
124
|
+
def __init__(self, text: str):
|
|
125
|
+
super().__init__("")
|
|
126
|
+
self._text = text
|
|
127
|
+
|
|
128
|
+
def _to_string(self, index: int, indentation: str) -> str:
|
|
129
|
+
indent = indentation * index
|
|
130
|
+
sons_indent = indentation * (index + 1)
|
|
131
|
+
children_str = ""
|
|
132
|
+
for son in self._sons:
|
|
133
|
+
if isinstance(son, TextOnlyComment):
|
|
134
|
+
children_str = children_str + son._to_string(index + 1, indentation)
|
|
135
|
+
else:
|
|
136
|
+
children_str = children_str + sons_indent + "<" + son.name + ">\n"
|
|
137
|
+
if children_str:
|
|
138
|
+
return f"{indent}<{self._text}[\n{children_str}{indent}]>\n"
|
|
139
|
+
else:
|
|
140
|
+
return f"{indent}<![CDATA[{self._text}]]>\n"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class Element(ElementBase):
|
|
144
|
+
"""An XML element that can contain attributes, content, and child elements."""
|
|
145
|
+
def __init__(self, name: str):
|
|
146
|
+
super().__init__(name)
|
|
147
|
+
self.content = ""
|
|
148
|
+
self.attributes = {}
|
|
149
|
+
self._is_empty = False # whether the element is self-closing
|
|
150
|
+
|
|
151
|
+
def comment_out(self):
|
|
152
|
+
"""Convert this element into a comment.
|
|
153
|
+
raises IllegalOperation, if any parent or any descended is a comment
|
|
154
|
+
"""
|
|
155
|
+
def find_comment_son(element: "Element") -> bool:
|
|
156
|
+
if element.is_comment():
|
|
157
|
+
return True
|
|
158
|
+
for son in element._sons:
|
|
159
|
+
if find_comment_son(son):
|
|
160
|
+
return True
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
parent = self.parent
|
|
164
|
+
while parent:
|
|
165
|
+
if parent.is_comment():
|
|
166
|
+
raise IllegalOperation("Cannot comment out an element whose parent is a comment")
|
|
167
|
+
parent = parent.parent
|
|
168
|
+
|
|
169
|
+
for son in self._sons:
|
|
170
|
+
if find_comment_son(son):
|
|
171
|
+
raise IllegalOperation("Cannot comment out an element whose descended is a comment")
|
|
172
|
+
|
|
173
|
+
self.__class__ = Comment
|
|
174
|
+
|
|
175
|
+
def _to_string(self, index: int, indentation: str, with_endl=True) -> str:
|
|
176
|
+
indent = indentation * index
|
|
177
|
+
|
|
178
|
+
attributes_str = " ".join(
|
|
179
|
+
f'{key}="{value}"' for key, value in self.attributes.items() # f-string formats the pair as key="value"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
attributes_part = f" {attributes_str}" if attributes_str else ""
|
|
183
|
+
|
|
184
|
+
if self._is_empty:
|
|
185
|
+
result = f"{indent}<{self.name}{attributes_part}/>"
|
|
186
|
+
else:
|
|
187
|
+
opening_tag = f"<{self.name}{attributes_part}>"
|
|
188
|
+
closing_tag = f"</{self.name}>"
|
|
189
|
+
|
|
190
|
+
children_str = "".join(son._to_string(index + 1, indentation) for son in self._sons)
|
|
191
|
+
|
|
192
|
+
if children_str:
|
|
193
|
+
result = f"{indent}{opening_tag}" f"{self.content}" f"{"\n"}" f"{children_str}{indent}{closing_tag}"
|
|
194
|
+
else:
|
|
195
|
+
result = f"{indent}{opening_tag}{self.content}{closing_tag}"
|
|
196
|
+
|
|
197
|
+
if with_endl:
|
|
198
|
+
result += "\n"
|
|
199
|
+
return result
|
|
200
|
+
|
|
201
|
+
def find(
|
|
202
|
+
self,
|
|
203
|
+
name: str = None,
|
|
204
|
+
only_one: bool = True,
|
|
205
|
+
with_content: str = None,
|
|
206
|
+
) -> Union["Element", list["Element"], None]:
|
|
207
|
+
"""
|
|
208
|
+
Find element(s) by name or content or both
|
|
209
|
+
:param name: name of the element to find, can be nested using |, e.g. "parent|child|subchild"
|
|
210
|
+
:param only_one: stop at first find or return all found elements
|
|
211
|
+
:param with_content: filter by content
|
|
212
|
+
:return: the elements found,
|
|
213
|
+
if found, return the elements that match the last name in the path,
|
|
214
|
+
if not found, return None if only_one is True, else return empty list
|
|
215
|
+
"""
|
|
216
|
+
if only_one:
|
|
217
|
+
return _find_one(self, name, with_content=with_content)
|
|
218
|
+
else:
|
|
219
|
+
return _find_all(self, name, with_content=with_content)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class Comment(Element):
|
|
223
|
+
"""An XML comment that can contain other elements."""
|
|
224
|
+
def __init__(self, name: str):
|
|
225
|
+
super().__init__(name)
|
|
226
|
+
|
|
227
|
+
def is_comment(self) -> bool:
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
def uncomment(self):
|
|
231
|
+
"""Convert this comment back into a normal element."""
|
|
232
|
+
self.__class__ = Element
|
|
233
|
+
|
|
234
|
+
def _to_string(self, index: int, indentation: str) -> str:
|
|
235
|
+
indent = indentation * index
|
|
236
|
+
if len(self._sons) == 0:
|
|
237
|
+
return f"{indent}<!-- {super()._to_string(0, indentation, False)} -->\n"
|
|
238
|
+
else:
|
|
239
|
+
return f"{indent}<!--\n{super()._to_string(index +1, indentation, False)}\n{indent}-->\n"
|