genxai-framework 0.1.0__py3-none-any.whl

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.
Files changed (156) hide show
  1. cli/__init__.py +3 -0
  2. cli/commands/__init__.py +6 -0
  3. cli/commands/approval.py +85 -0
  4. cli/commands/audit.py +127 -0
  5. cli/commands/metrics.py +25 -0
  6. cli/commands/tool.py +389 -0
  7. cli/main.py +32 -0
  8. genxai/__init__.py +81 -0
  9. genxai/api/__init__.py +5 -0
  10. genxai/api/app.py +21 -0
  11. genxai/config/__init__.py +5 -0
  12. genxai/config/settings.py +37 -0
  13. genxai/connectors/__init__.py +19 -0
  14. genxai/connectors/base.py +122 -0
  15. genxai/connectors/kafka.py +92 -0
  16. genxai/connectors/postgres_cdc.py +95 -0
  17. genxai/connectors/registry.py +44 -0
  18. genxai/connectors/sqs.py +94 -0
  19. genxai/connectors/webhook.py +73 -0
  20. genxai/core/__init__.py +37 -0
  21. genxai/core/agent/__init__.py +32 -0
  22. genxai/core/agent/base.py +206 -0
  23. genxai/core/agent/config_io.py +59 -0
  24. genxai/core/agent/registry.py +98 -0
  25. genxai/core/agent/runtime.py +970 -0
  26. genxai/core/communication/__init__.py +6 -0
  27. genxai/core/communication/collaboration.py +44 -0
  28. genxai/core/communication/message_bus.py +192 -0
  29. genxai/core/communication/protocols.py +35 -0
  30. genxai/core/execution/__init__.py +22 -0
  31. genxai/core/execution/metadata.py +181 -0
  32. genxai/core/execution/queue.py +201 -0
  33. genxai/core/graph/__init__.py +30 -0
  34. genxai/core/graph/checkpoints.py +77 -0
  35. genxai/core/graph/edges.py +131 -0
  36. genxai/core/graph/engine.py +813 -0
  37. genxai/core/graph/executor.py +516 -0
  38. genxai/core/graph/nodes.py +161 -0
  39. genxai/core/graph/trigger_runner.py +40 -0
  40. genxai/core/memory/__init__.py +19 -0
  41. genxai/core/memory/base.py +72 -0
  42. genxai/core/memory/embedding.py +327 -0
  43. genxai/core/memory/episodic.py +448 -0
  44. genxai/core/memory/long_term.py +467 -0
  45. genxai/core/memory/manager.py +543 -0
  46. genxai/core/memory/persistence.py +297 -0
  47. genxai/core/memory/procedural.py +461 -0
  48. genxai/core/memory/semantic.py +526 -0
  49. genxai/core/memory/shared.py +62 -0
  50. genxai/core/memory/short_term.py +303 -0
  51. genxai/core/memory/vector_store.py +508 -0
  52. genxai/core/memory/working.py +211 -0
  53. genxai/core/state/__init__.py +6 -0
  54. genxai/core/state/manager.py +293 -0
  55. genxai/core/state/schema.py +115 -0
  56. genxai/llm/__init__.py +14 -0
  57. genxai/llm/base.py +150 -0
  58. genxai/llm/factory.py +329 -0
  59. genxai/llm/providers/__init__.py +1 -0
  60. genxai/llm/providers/anthropic.py +249 -0
  61. genxai/llm/providers/cohere.py +274 -0
  62. genxai/llm/providers/google.py +334 -0
  63. genxai/llm/providers/ollama.py +147 -0
  64. genxai/llm/providers/openai.py +257 -0
  65. genxai/llm/routing.py +83 -0
  66. genxai/observability/__init__.py +6 -0
  67. genxai/observability/logging.py +327 -0
  68. genxai/observability/metrics.py +494 -0
  69. genxai/observability/tracing.py +372 -0
  70. genxai/performance/__init__.py +39 -0
  71. genxai/performance/cache.py +256 -0
  72. genxai/performance/pooling.py +289 -0
  73. genxai/security/audit.py +304 -0
  74. genxai/security/auth.py +315 -0
  75. genxai/security/cost_control.py +528 -0
  76. genxai/security/default_policies.py +44 -0
  77. genxai/security/jwt.py +142 -0
  78. genxai/security/oauth.py +226 -0
  79. genxai/security/pii.py +366 -0
  80. genxai/security/policy_engine.py +82 -0
  81. genxai/security/rate_limit.py +341 -0
  82. genxai/security/rbac.py +247 -0
  83. genxai/security/validation.py +218 -0
  84. genxai/tools/__init__.py +21 -0
  85. genxai/tools/base.py +383 -0
  86. genxai/tools/builtin/__init__.py +131 -0
  87. genxai/tools/builtin/communication/__init__.py +15 -0
  88. genxai/tools/builtin/communication/email_sender.py +159 -0
  89. genxai/tools/builtin/communication/notification_manager.py +167 -0
  90. genxai/tools/builtin/communication/slack_notifier.py +118 -0
  91. genxai/tools/builtin/communication/sms_sender.py +118 -0
  92. genxai/tools/builtin/communication/webhook_caller.py +136 -0
  93. genxai/tools/builtin/computation/__init__.py +15 -0
  94. genxai/tools/builtin/computation/calculator.py +101 -0
  95. genxai/tools/builtin/computation/code_executor.py +183 -0
  96. genxai/tools/builtin/computation/data_validator.py +259 -0
  97. genxai/tools/builtin/computation/hash_generator.py +129 -0
  98. genxai/tools/builtin/computation/regex_matcher.py +201 -0
  99. genxai/tools/builtin/data/__init__.py +15 -0
  100. genxai/tools/builtin/data/csv_processor.py +213 -0
  101. genxai/tools/builtin/data/data_transformer.py +299 -0
  102. genxai/tools/builtin/data/json_processor.py +233 -0
  103. genxai/tools/builtin/data/text_analyzer.py +288 -0
  104. genxai/tools/builtin/data/xml_processor.py +175 -0
  105. genxai/tools/builtin/database/__init__.py +15 -0
  106. genxai/tools/builtin/database/database_inspector.py +157 -0
  107. genxai/tools/builtin/database/mongodb_query.py +196 -0
  108. genxai/tools/builtin/database/redis_cache.py +167 -0
  109. genxai/tools/builtin/database/sql_query.py +145 -0
  110. genxai/tools/builtin/database/vector_search.py +163 -0
  111. genxai/tools/builtin/file/__init__.py +17 -0
  112. genxai/tools/builtin/file/directory_scanner.py +214 -0
  113. genxai/tools/builtin/file/file_compressor.py +237 -0
  114. genxai/tools/builtin/file/file_reader.py +102 -0
  115. genxai/tools/builtin/file/file_writer.py +122 -0
  116. genxai/tools/builtin/file/image_processor.py +186 -0
  117. genxai/tools/builtin/file/pdf_parser.py +144 -0
  118. genxai/tools/builtin/test/__init__.py +15 -0
  119. genxai/tools/builtin/test/async_simulator.py +62 -0
  120. genxai/tools/builtin/test/data_transformer.py +99 -0
  121. genxai/tools/builtin/test/error_generator.py +82 -0
  122. genxai/tools/builtin/test/simple_math.py +94 -0
  123. genxai/tools/builtin/test/string_processor.py +72 -0
  124. genxai/tools/builtin/web/__init__.py +15 -0
  125. genxai/tools/builtin/web/api_caller.py +161 -0
  126. genxai/tools/builtin/web/html_parser.py +330 -0
  127. genxai/tools/builtin/web/http_client.py +187 -0
  128. genxai/tools/builtin/web/url_validator.py +162 -0
  129. genxai/tools/builtin/web/web_scraper.py +170 -0
  130. genxai/tools/custom/my_test_tool_2.py +9 -0
  131. genxai/tools/dynamic.py +105 -0
  132. genxai/tools/mcp_server.py +167 -0
  133. genxai/tools/persistence/__init__.py +6 -0
  134. genxai/tools/persistence/models.py +55 -0
  135. genxai/tools/persistence/service.py +322 -0
  136. genxai/tools/registry.py +227 -0
  137. genxai/tools/security/__init__.py +11 -0
  138. genxai/tools/security/limits.py +214 -0
  139. genxai/tools/security/policy.py +20 -0
  140. genxai/tools/security/sandbox.py +248 -0
  141. genxai/tools/templates.py +435 -0
  142. genxai/triggers/__init__.py +19 -0
  143. genxai/triggers/base.py +104 -0
  144. genxai/triggers/file_watcher.py +75 -0
  145. genxai/triggers/queue.py +68 -0
  146. genxai/triggers/registry.py +82 -0
  147. genxai/triggers/schedule.py +66 -0
  148. genxai/triggers/webhook.py +68 -0
  149. genxai/utils/__init__.py +1 -0
  150. genxai/utils/tokens.py +295 -0
  151. genxai_framework-0.1.0.dist-info/METADATA +495 -0
  152. genxai_framework-0.1.0.dist-info/RECORD +156 -0
  153. genxai_framework-0.1.0.dist-info/WHEEL +5 -0
  154. genxai_framework-0.1.0.dist-info/entry_points.txt +2 -0
  155. genxai_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
  156. genxai_framework-0.1.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,213 @@
1
+ """CSV processor tool for parsing and manipulating CSV data."""
2
+
3
+ from typing import Any, Dict, List, Optional
4
+ import logging
5
+ import csv
6
+ import io
7
+
8
+ from genxai.tools.base import Tool, ToolMetadata, ToolParameter, ToolCategory
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class CSVProcessorTool(Tool):
14
+ """Process, parse, and manipulate CSV data."""
15
+
16
+ def __init__(self) -> None:
17
+ """Initialize CSV processor tool."""
18
+ metadata = ToolMetadata(
19
+ name="csv_processor",
20
+ description="Parse, validate, filter, and transform CSV data",
21
+ category=ToolCategory.DATA,
22
+ tags=["csv", "data", "parsing", "tabular", "spreadsheet"],
23
+ version="1.0.0",
24
+ )
25
+
26
+ parameters = [
27
+ ToolParameter(
28
+ name="data",
29
+ type="string",
30
+ description="CSV string to process",
31
+ required=True,
32
+ ),
33
+ ToolParameter(
34
+ name="operation",
35
+ type="string",
36
+ description="Operation to perform",
37
+ required=True,
38
+ enum=["parse", "filter", "transform", "aggregate", "validate"],
39
+ ),
40
+ ToolParameter(
41
+ name="delimiter",
42
+ type="string",
43
+ description="CSV delimiter character",
44
+ required=False,
45
+ default=",",
46
+ ),
47
+ ToolParameter(
48
+ name="has_header",
49
+ type="boolean",
50
+ description="Whether CSV has header row",
51
+ required=False,
52
+ default=True,
53
+ ),
54
+ ToolParameter(
55
+ name="filter_column",
56
+ type="string",
57
+ description="Column name to filter by (for filter operation)",
58
+ required=False,
59
+ ),
60
+ ToolParameter(
61
+ name="filter_value",
62
+ type="string",
63
+ description="Value to filter for (for filter operation)",
64
+ required=False,
65
+ ),
66
+ ]
67
+
68
+ super().__init__(metadata, parameters)
69
+
70
+ async def _execute(
71
+ self,
72
+ data: str,
73
+ operation: str,
74
+ delimiter: str = ",",
75
+ has_header: bool = True,
76
+ filter_column: Optional[str] = None,
77
+ filter_value: Optional[str] = None,
78
+ ) -> Dict[str, Any]:
79
+ """Execute CSV processing.
80
+
81
+ Args:
82
+ data: CSV string
83
+ operation: Operation to perform
84
+ delimiter: CSV delimiter
85
+ has_header: Has header flag
86
+ filter_column: Column to filter
87
+ filter_value: Value to filter
88
+
89
+ Returns:
90
+ Dictionary containing processed data
91
+ """
92
+ result: Dict[str, Any] = {
93
+ "operation": operation,
94
+ "success": False,
95
+ }
96
+
97
+ try:
98
+ # Parse CSV
99
+ csv_file = io.StringIO(data)
100
+ reader = csv.reader(csv_file, delimiter=delimiter)
101
+ rows = list(reader)
102
+
103
+ if not rows:
104
+ raise ValueError("Empty CSV data")
105
+
106
+ headers = rows[0] if has_header else [f"col_{i}" for i in range(len(rows[0]))]
107
+ data_rows = rows[1:] if has_header else rows
108
+
109
+ if operation == "parse":
110
+ # Convert to list of dictionaries
111
+ parsed_data = []
112
+ for row in data_rows:
113
+ if len(row) == len(headers):
114
+ parsed_data.append(dict(zip(headers, row)))
115
+
116
+ result["data"] = parsed_data
117
+ result["headers"] = headers
118
+ result["row_count"] = len(parsed_data)
119
+ result["column_count"] = len(headers)
120
+ result["success"] = True
121
+
122
+ elif operation == "filter":
123
+ if not filter_column or filter_value is None:
124
+ raise ValueError("filter_column and filter_value required for filter operation")
125
+
126
+ if filter_column not in headers:
127
+ raise ValueError(f"Column '{filter_column}' not found in headers")
128
+
129
+ col_index = headers.index(filter_column)
130
+ filtered_rows = [
131
+ row for row in data_rows
132
+ if len(row) > col_index and row[col_index] == filter_value
133
+ ]
134
+
135
+ filtered_data = [dict(zip(headers, row)) for row in filtered_rows]
136
+ result["data"] = filtered_data
137
+ result["filtered_count"] = len(filtered_data)
138
+ result["original_count"] = len(data_rows)
139
+ result["success"] = True
140
+
141
+ elif operation == "transform":
142
+ # Convert to structured format
143
+ transformed = {
144
+ "headers": headers,
145
+ "rows": data_rows,
146
+ "metadata": {
147
+ "row_count": len(data_rows),
148
+ "column_count": len(headers),
149
+ "delimiter": delimiter,
150
+ }
151
+ }
152
+ result["data"] = transformed
153
+ result["success"] = True
154
+
155
+ elif operation == "aggregate":
156
+ # Calculate basic statistics for numeric columns
157
+ aggregates = {}
158
+
159
+ for col_idx, header in enumerate(headers):
160
+ column_values = [row[col_idx] for row in data_rows if len(row) > col_idx]
161
+
162
+ # Try to convert to numbers
163
+ numeric_values = []
164
+ for val in column_values:
165
+ try:
166
+ numeric_values.append(float(val))
167
+ except (ValueError, TypeError):
168
+ pass
169
+
170
+ if numeric_values:
171
+ aggregates[header] = {
172
+ "count": len(numeric_values),
173
+ "sum": sum(numeric_values),
174
+ "mean": sum(numeric_values) / len(numeric_values),
175
+ "min": min(numeric_values),
176
+ "max": max(numeric_values),
177
+ }
178
+ else:
179
+ # For non-numeric columns, count unique values
180
+ unique_values = set(column_values)
181
+ aggregates[header] = {
182
+ "count": len(column_values),
183
+ "unique_count": len(unique_values),
184
+ "sample_values": list(unique_values)[:5],
185
+ }
186
+
187
+ result["data"] = aggregates
188
+ result["success"] = True
189
+
190
+ elif operation == "validate":
191
+ # Validate CSV structure
192
+ issues = []
193
+
194
+ # Check for consistent column count
195
+ expected_cols = len(headers)
196
+ for idx, row in enumerate(data_rows):
197
+ if len(row) != expected_cols:
198
+ issues.append({
199
+ "row": idx + (2 if has_header else 1),
200
+ "issue": f"Expected {expected_cols} columns, found {len(row)}",
201
+ })
202
+
203
+ result["valid"] = len(issues) == 0
204
+ result["issues"] = issues
205
+ result["row_count"] = len(data_rows)
206
+ result["column_count"] = len(headers)
207
+ result["success"] = True
208
+
209
+ except Exception as e:
210
+ result["error"] = str(e)
211
+
212
+ logger.info(f"CSV {operation} operation completed: success={result['success']}")
213
+ return result
@@ -0,0 +1,299 @@
1
+ """Data transformer tool for converting between data formats."""
2
+
3
+ from typing import Any, Dict, Optional
4
+ import logging
5
+ import json
6
+ import csv
7
+ import io
8
+
9
+ from genxai.tools.base import Tool, ToolMetadata, ToolParameter, ToolCategory
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class DataTransformerTool(Tool):
15
+ """Transform data between different formats (JSON, CSV, XML, etc.)."""
16
+
17
+ def __init__(self) -> None:
18
+ """Initialize data transformer tool."""
19
+ metadata = ToolMetadata(
20
+ name="data_transformer",
21
+ description="Convert data between different formats (JSON, CSV, XML, YAML)",
22
+ category=ToolCategory.DATA,
23
+ tags=["transformation", "conversion", "format", "data", "json", "csv", "xml"],
24
+ version="1.0.0",
25
+ )
26
+
27
+ parameters = [
28
+ ToolParameter(
29
+ name="data",
30
+ type="string",
31
+ description="Input data to transform",
32
+ required=True,
33
+ ),
34
+ ToolParameter(
35
+ name="operation",
36
+ type="string",
37
+ description="Simple text transformation operation (used by unit tests)",
38
+ required=False,
39
+ default="uppercase",
40
+ enum=["uppercase", "lowercase", "trim", "convert"],
41
+ ),
42
+ ToolParameter(
43
+ name="source_format",
44
+ type="string",
45
+ description="Source data format (for convert operation)",
46
+ required=False,
47
+ default="json",
48
+ enum=["json", "csv", "xml", "yaml"],
49
+ ),
50
+ ToolParameter(
51
+ name="target_format",
52
+ type="string",
53
+ description="Target data format (for convert operation)",
54
+ required=False,
55
+ default="json",
56
+ enum=["json", "csv", "xml", "yaml"],
57
+ ),
58
+ ToolParameter(
59
+ name="csv_delimiter",
60
+ type="string",
61
+ description="CSV delimiter (for CSV operations)",
62
+ required=False,
63
+ default=",",
64
+ ),
65
+ ]
66
+
67
+ super().__init__(metadata, parameters)
68
+
69
+ async def _execute(
70
+ self,
71
+ data: str,
72
+ operation: str = "convert",
73
+ source_format: str = "json",
74
+ target_format: str = "json",
75
+ csv_delimiter: str = ",",
76
+ ) -> Dict[str, Any]:
77
+ """Execute data transformation.
78
+
79
+ Args:
80
+ data: Input data
81
+ source_format: Source format
82
+ target_format: Target format
83
+ csv_delimiter: CSV delimiter
84
+
85
+ Returns:
86
+ Dictionary containing transformed data
87
+ """
88
+ result: Dict[str, Any] = {
89
+ "operation": operation,
90
+ "success": False,
91
+ }
92
+
93
+ try:
94
+ if operation == "uppercase":
95
+ result["result"] = data.upper()
96
+ result["success"] = True
97
+ return result
98
+ if operation == "lowercase":
99
+ result["result"] = data.lower()
100
+ result["success"] = True
101
+ return result
102
+ if operation == "trim":
103
+ result["result"] = data.strip()
104
+ result["success"] = True
105
+ return result
106
+
107
+ if operation not in {"convert"}:
108
+ raise ValueError(f"Unsupported operation: {operation}")
109
+
110
+ # Parse source data
111
+ parsed_data = self._parse_data(data, source_format, csv_delimiter)
112
+
113
+ # Convert to target format
114
+ transformed_data = self._convert_data(parsed_data, target_format, csv_delimiter)
115
+
116
+ result["data"] = transformed_data
117
+ result["source_format"] = source_format
118
+ result["target_format"] = target_format
119
+ result["success"] = True
120
+
121
+ except Exception as e:
122
+ result["error"] = str(e)
123
+
124
+ logger.info(
125
+ f"Data transformer ({operation}) completed: success={result['success']}"
126
+ )
127
+ return result
128
+
129
+ def _parse_data(self, data: str, format: str, delimiter: str) -> Any:
130
+ """Parse data from source format.
131
+
132
+ Args:
133
+ data: Input data
134
+ format: Data format
135
+ delimiter: CSV delimiter
136
+
137
+ Returns:
138
+ Parsed data
139
+ """
140
+ if format == "json":
141
+ return json.loads(data)
142
+
143
+ elif format == "csv":
144
+ csv_file = io.StringIO(data)
145
+ reader = csv.DictReader(csv_file, delimiter=delimiter)
146
+ return list(reader)
147
+
148
+ elif format == "xml":
149
+ import xml.etree.ElementTree as ET
150
+ root = ET.fromstring(data)
151
+ return self._xml_to_dict(root)
152
+
153
+ elif format == "yaml":
154
+ try:
155
+ import yaml
156
+ return yaml.safe_load(data)
157
+ except ImportError:
158
+ raise ImportError(
159
+ "pyyaml package not installed. Install with: pip install pyyaml"
160
+ )
161
+
162
+ else:
163
+ raise ValueError(f"Unsupported source format: {format}")
164
+
165
+ def _convert_data(self, data: Any, format: str, delimiter: str) -> str:
166
+ """Convert data to target format.
167
+
168
+ Args:
169
+ data: Parsed data
170
+ format: Target format
171
+ delimiter: CSV delimiter
172
+
173
+ Returns:
174
+ Converted data string
175
+ """
176
+ if format == "json":
177
+ return json.dumps(data, indent=2)
178
+
179
+ elif format == "csv":
180
+ if not isinstance(data, list):
181
+ raise ValueError("CSV conversion requires list of dictionaries")
182
+
183
+ if not data:
184
+ return ""
185
+
186
+ output = io.StringIO()
187
+ if isinstance(data[0], dict):
188
+ writer = csv.DictWriter(output, fieldnames=data[0].keys(), delimiter=delimiter)
189
+ writer.writeheader()
190
+ writer.writerows(data)
191
+ else:
192
+ writer = csv.writer(output, delimiter=delimiter)
193
+ writer.writerows(data)
194
+
195
+ return output.getvalue()
196
+
197
+ elif format == "xml":
198
+ import xml.etree.ElementTree as ET
199
+ root = self._dict_to_xml(data, "root")
200
+ self._indent_xml(root)
201
+ return ET.tostring(root, encoding="unicode")
202
+
203
+ elif format == "yaml":
204
+ try:
205
+ import yaml
206
+ return yaml.dump(data, default_flow_style=False, sort_keys=False)
207
+ except ImportError:
208
+ raise ImportError(
209
+ "pyyaml package not installed. Install with: pip install pyyaml"
210
+ )
211
+
212
+ else:
213
+ raise ValueError(f"Unsupported target format: {format}")
214
+
215
+ def _xml_to_dict(self, element: Any) -> Dict[str, Any]:
216
+ """Convert XML element to dictionary.
217
+
218
+ Args:
219
+ element: XML element
220
+
221
+ Returns:
222
+ Dictionary representation
223
+ """
224
+ result: Dict[str, Any] = {}
225
+
226
+ # Add attributes
227
+ if element.attrib:
228
+ result["@attributes"] = element.attrib
229
+
230
+ # Add text content
231
+ if element.text and element.text.strip():
232
+ result["#text"] = element.text.strip()
233
+
234
+ # Add children
235
+ for child in element:
236
+ child_data = self._xml_to_dict(child)
237
+ if child.tag in result:
238
+ # Multiple children with same tag - convert to list
239
+ if not isinstance(result[child.tag], list):
240
+ result[child.tag] = [result[child.tag]]
241
+ result[child.tag].append(child_data)
242
+ else:
243
+ result[child.tag] = child_data
244
+
245
+ return {element.tag: result} if result else {element.tag: element.text}
246
+
247
+ def _dict_to_xml(self, data: Any, root_tag: str = "root") -> Any:
248
+ """Convert dictionary to XML element.
249
+
250
+ Args:
251
+ data: Dictionary data
252
+ root_tag: Root element tag
253
+
254
+ Returns:
255
+ XML element
256
+ """
257
+ import xml.etree.ElementTree as ET
258
+
259
+ root = ET.Element(root_tag)
260
+
261
+ if isinstance(data, dict):
262
+ for key, value in data.items():
263
+ if key == "@attributes":
264
+ root.attrib.update(value)
265
+ elif key == "#text":
266
+ root.text = str(value)
267
+ else:
268
+ if isinstance(value, list):
269
+ for item in value:
270
+ child = self._dict_to_xml(item, key)
271
+ root.append(child)
272
+ else:
273
+ child = self._dict_to_xml(value, key)
274
+ root.append(child)
275
+ else:
276
+ root.text = str(data)
277
+
278
+ return root
279
+
280
+ def _indent_xml(self, elem: Any, level: int = 0) -> None:
281
+ """Add indentation to XML element.
282
+
283
+ Args:
284
+ elem: XML element
285
+ level: Indentation level
286
+ """
287
+ indent = "\n" + " " * level
288
+ if len(elem):
289
+ if not elem.text or not elem.text.strip():
290
+ elem.text = indent + " "
291
+ if not elem.tail or not elem.tail.strip():
292
+ elem.tail = indent
293
+ for child in elem:
294
+ self._indent_xml(child, level + 1)
295
+ if not child.tail or not child.tail.strip():
296
+ child.tail = indent
297
+ else:
298
+ if level and (not elem.tail or not elem.tail.strip()):
299
+ elem.tail = indent