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.
@@ -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()
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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"