typed-ffmpeg-compatible 3.5.1__py3-none-any.whl → 3.6__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.
- typed_ffmpeg/__init__.py +4 -1
- typed_ffmpeg/_version.py +2 -2
- typed_ffmpeg/base.py +4 -1
- typed_ffmpeg/codecs/__init__.py +2 -0
- typed_ffmpeg/codecs/decoders.py +1852 -1853
- typed_ffmpeg/codecs/encoders.py +2001 -1782
- typed_ffmpeg/codecs/schema.py +6 -12
- typed_ffmpeg/common/__init__.py +1 -0
- typed_ffmpeg/common/cache.py +9 -6
- typed_ffmpeg/common/schema.py +11 -0
- typed_ffmpeg/common/serialize.py +13 -7
- typed_ffmpeg/compile/__init__.py +1 -0
- typed_ffmpeg/compile/compile_cli.py +55 -8
- typed_ffmpeg/compile/compile_json.py +4 -0
- typed_ffmpeg/compile/compile_python.py +15 -0
- typed_ffmpeg/compile/context.py +15 -4
- typed_ffmpeg/compile/validate.py +9 -8
- typed_ffmpeg/dag/factory.py +2 -0
- typed_ffmpeg/dag/global_runnable/__init__.py +1 -0
- typed_ffmpeg/dag/global_runnable/global_args.py +2 -2
- typed_ffmpeg/dag/global_runnable/runnable.py +51 -11
- typed_ffmpeg/dag/io/__init__.py +1 -0
- typed_ffmpeg/dag/io/_input.py +20 -5
- typed_ffmpeg/dag/io/_output.py +24 -9
- typed_ffmpeg/dag/io/output_args.py +21 -7
- typed_ffmpeg/dag/nodes.py +20 -0
- typed_ffmpeg/dag/schema.py +19 -6
- typed_ffmpeg/dag/utils.py +2 -2
- typed_ffmpeg/exceptions.py +2 -1
- typed_ffmpeg/expressions.py +884 -0
- typed_ffmpeg/ffprobe/__init__.py +1 -0
- typed_ffmpeg/ffprobe/parse.py +7 -1
- typed_ffmpeg/ffprobe/probe.py +3 -1
- typed_ffmpeg/ffprobe/schema.py +83 -1
- typed_ffmpeg/ffprobe/xml2json.py +8 -2
- typed_ffmpeg/filters.py +540 -631
- typed_ffmpeg/formats/__init__.py +2 -0
- typed_ffmpeg/formats/demuxers.py +1869 -1921
- typed_ffmpeg/formats/muxers.py +1382 -1107
- typed_ffmpeg/formats/schema.py +6 -12
- typed_ffmpeg/info.py +8 -0
- typed_ffmpeg/options/__init__.py +15 -0
- typed_ffmpeg/options/codec.py +711 -0
- typed_ffmpeg/options/format.py +196 -0
- typed_ffmpeg/options/framesync.py +43 -0
- typed_ffmpeg/options/timeline.py +22 -0
- typed_ffmpeg/schema.py +15 -0
- typed_ffmpeg/sources.py +392 -381
- typed_ffmpeg/streams/__init__.py +2 -0
- typed_ffmpeg/streams/audio.py +1071 -882
- typed_ffmpeg/streams/av.py +9 -3
- typed_ffmpeg/streams/subtitle.py +3 -3
- typed_ffmpeg/streams/video.py +1873 -1725
- typed_ffmpeg/types.py +3 -2
- typed_ffmpeg/utils/__init__.py +1 -0
- typed_ffmpeg/utils/escaping.py +8 -4
- typed_ffmpeg/utils/frozendict.py +31 -1
- typed_ffmpeg/utils/lazy_eval/__init__.py +1 -0
- typed_ffmpeg/utils/lazy_eval/operator.py +75 -27
- typed_ffmpeg/utils/lazy_eval/schema.py +176 -4
- typed_ffmpeg/utils/run.py +2 -0
- typed_ffmpeg/utils/snapshot.py +3 -2
- typed_ffmpeg/utils/typing.py +2 -1
- typed_ffmpeg/utils/view.py +2 -1
- {typed_ffmpeg_compatible-3.5.1.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/METADATA +1 -1
- typed_ffmpeg_compatible-3.6.dist-info/RECORD +73 -0
- typed_ffmpeg_compatible-3.5.1.dist-info/RECORD +0 -67
- {typed_ffmpeg_compatible-3.5.1.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/WHEEL +0 -0
- {typed_ffmpeg_compatible-3.5.1.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/entry_points.txt +0 -0
- {typed_ffmpeg_compatible-3.5.1.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/licenses/LICENSE +0 -0
- {typed_ffmpeg_compatible-3.5.1.dist-info → typed_ffmpeg_compatible-3.6.dist-info}/top_level.txt +0 -0
typed_ffmpeg/ffprobe/__init__.py
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
"""FFprobe utilities for media file analysis."""
|
typed_ffmpeg/ffprobe/parse.py
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
"""FFprobe XML parsing utilities."""
|
2
|
+
|
3
|
+
# !/usr/bin/env python3
|
2
4
|
|
3
5
|
import json
|
4
6
|
import types
|
@@ -29,6 +31,7 @@ def _get_actual_type(type_hint: Any) -> type[Any]:
|
|
29
31
|
|
30
32
|
Returns:
|
31
33
|
The actual type
|
34
|
+
|
32
35
|
"""
|
33
36
|
# If type_hint is a string, evaluate it in the schema's module context
|
34
37
|
if isinstance(type_hint, str):
|
@@ -57,6 +60,7 @@ def is_dataclass_type(obj: type[Any]) -> TypeGuard[type[T]]:
|
|
57
60
|
|
58
61
|
Returns:
|
59
62
|
True if the object is a dataclass type, False otherwise
|
63
|
+
|
60
64
|
"""
|
61
65
|
return is_dataclass(obj)
|
62
66
|
|
@@ -71,6 +75,7 @@ def _parse_obj_from_dict(data: Any, cls: type[T]) -> T | None:
|
|
71
75
|
|
72
76
|
Returns:
|
73
77
|
The parsed dataclass instance
|
78
|
+
|
74
79
|
"""
|
75
80
|
if data is None:
|
76
81
|
return None
|
@@ -125,6 +130,7 @@ def parse_ffprobe(xml_string: str) -> ffprobeType:
|
|
125
130
|
|
126
131
|
Returns:
|
127
132
|
The parsed ffprobeType instance
|
133
|
+
|
128
134
|
"""
|
129
135
|
json_str = xml_string_to_json(xml_string)
|
130
136
|
json_dict = json.loads(json_str)
|
typed_ffmpeg/ffprobe/probe.py
CHANGED
@@ -79,6 +79,7 @@ def _probe(
|
|
79
79
|
print(f"Duration: {float(info['format']['duration']):.2f} seconds")
|
80
80
|
print(f"Streams: {len(info['streams'])}")
|
81
81
|
```
|
82
|
+
|
82
83
|
"""
|
83
84
|
args = [
|
84
85
|
cmd,
|
@@ -172,6 +173,7 @@ def probe(
|
|
172
173
|
print(f"Duration: {float(info['format']['duration']):.2f} seconds")
|
173
174
|
print(f"Streams: {len(info['streams'])}")
|
174
175
|
```
|
176
|
+
|
175
177
|
"""
|
176
178
|
return json.loads(
|
177
179
|
_probe(
|
@@ -250,8 +252,8 @@ def probe_obj(
|
|
250
252
|
print(f"Duration: {float(info.format.duration):.2f} seconds")
|
251
253
|
print(f"Streams: {len(info.streams)}")
|
252
254
|
```
|
253
|
-
"""
|
254
255
|
|
256
|
+
"""
|
255
257
|
xml = _probe(
|
256
258
|
filename,
|
257
259
|
show_program_version=show_program_version,
|
typed_ffmpeg/ffprobe/schema.py
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
"""FFprobe XML schema definitions."""
|
2
|
+
|
3
|
+
# !/usr/bin/env python3
|
2
4
|
|
3
5
|
from dataclasses import dataclass
|
4
6
|
from typing import Optional
|
@@ -6,6 +8,8 @@ from typing import Optional
|
|
6
8
|
|
7
9
|
@dataclass(kw_only=True, frozen=True)
|
8
10
|
class ffprobeType:
|
11
|
+
"""Root type for FFprobe XML output."""
|
12
|
+
|
9
13
|
program_version: Optional["programVersionType"] = None
|
10
14
|
library_versions: Optional["libraryVersionsType"] = None
|
11
15
|
pixel_formats: Optional["pixelFormatsType"] = None
|
@@ -21,17 +25,23 @@ class ffprobeType:
|
|
21
25
|
|
22
26
|
@dataclass(kw_only=True, frozen=True)
|
23
27
|
class packetsType:
|
28
|
+
"""Container for packet information."""
|
29
|
+
|
24
30
|
packet: tuple["packetType", ...] | None = None
|
25
31
|
|
26
32
|
|
27
33
|
@dataclass(kw_only=True, frozen=True)
|
28
34
|
class framesType:
|
35
|
+
"""Container for frame information."""
|
36
|
+
|
29
37
|
frame: tuple["frameType", ...] | None = None
|
30
38
|
subtitle: tuple["subtitleType", ...] | None = None
|
31
39
|
|
32
40
|
|
33
41
|
@dataclass(kw_only=True, frozen=True)
|
34
42
|
class packetsAndFramesType:
|
43
|
+
"""Container for both packet and frame information."""
|
44
|
+
|
35
45
|
packet: tuple["packetType", ...] | None = None
|
36
46
|
frame: tuple["frameType", ...] | None = None
|
37
47
|
subtitle: tuple["subtitleType", ...] | None = None
|
@@ -39,11 +49,15 @@ class packetsAndFramesType:
|
|
39
49
|
|
40
50
|
@dataclass(kw_only=True, frozen=True)
|
41
51
|
class tagsType:
|
52
|
+
"""Container for tag information."""
|
53
|
+
|
42
54
|
tag: tuple["tagType", ...] | None = None
|
43
55
|
|
44
56
|
|
45
57
|
@dataclass(kw_only=True, frozen=True)
|
46
58
|
class packetType:
|
59
|
+
"""Information about a single packet."""
|
60
|
+
|
47
61
|
tags: Optional["tagsType"] = None
|
48
62
|
side_data_list: Optional["packetSideDataListType"] = None
|
49
63
|
codec_type: str | None = None
|
@@ -63,23 +77,31 @@ class packetType:
|
|
63
77
|
|
64
78
|
@dataclass(kw_only=True, frozen=True)
|
65
79
|
class packetSideDataListType:
|
80
|
+
"""Container for packet side data information."""
|
81
|
+
|
66
82
|
side_data: tuple["packetSideDataType", ...] | None = None
|
67
83
|
|
68
84
|
|
69
85
|
@dataclass(kw_only=True, frozen=True)
|
70
86
|
class packetSideDataType:
|
87
|
+
"""Information about packet side data."""
|
88
|
+
|
71
89
|
side_datum: tuple["packetSideDatumType", ...] | None = None
|
72
90
|
type: str | None = None
|
73
91
|
|
74
92
|
|
75
93
|
@dataclass(kw_only=True, frozen=True)
|
76
94
|
class packetSideDatumType:
|
95
|
+
"""Individual packet side data item."""
|
96
|
+
|
77
97
|
key: str | None = None
|
78
98
|
value: str | None = None
|
79
99
|
|
80
100
|
|
81
101
|
@dataclass(kw_only=True, frozen=True)
|
82
102
|
class frameType:
|
103
|
+
"""Information about a single frame."""
|
104
|
+
|
83
105
|
tags: Optional["tagsType"] = None
|
84
106
|
logs: Optional["logsType"] = None
|
85
107
|
side_data_list: Optional["frameSideDataListType"] = None
|
@@ -125,11 +147,15 @@ class frameType:
|
|
125
147
|
|
126
148
|
@dataclass(kw_only=True, frozen=True)
|
127
149
|
class logsType:
|
150
|
+
"""Container for log information."""
|
151
|
+
|
128
152
|
log: tuple["logType", ...] | None = None
|
129
153
|
|
130
154
|
|
131
155
|
@dataclass(kw_only=True, frozen=True)
|
132
156
|
class logType:
|
157
|
+
"""Information about a single log entry."""
|
158
|
+
|
133
159
|
context: str | None = None
|
134
160
|
level: int | None = None
|
135
161
|
category: int | None = None
|
@@ -140,11 +166,15 @@ class logType:
|
|
140
166
|
|
141
167
|
@dataclass(kw_only=True, frozen=True)
|
142
168
|
class frameSideDataListType:
|
169
|
+
"""Container for frame side data information."""
|
170
|
+
|
143
171
|
side_data: tuple["frameSideDataType", ...] | None = None
|
144
172
|
|
145
173
|
|
146
174
|
@dataclass(kw_only=True, frozen=True)
|
147
175
|
class frameSideDataType:
|
176
|
+
"""Information about frame side data."""
|
177
|
+
|
148
178
|
timecodes: Optional["frameSideDataTimecodeList"] = None
|
149
179
|
components: Optional["frameSideDataComponentList"] = None
|
150
180
|
side_datum: tuple["frameSideDatumType", ...] | None = None
|
@@ -155,43 +185,59 @@ class frameSideDataType:
|
|
155
185
|
|
156
186
|
@dataclass(kw_only=True, frozen=True)
|
157
187
|
class frameSideDatumType:
|
188
|
+
"""Individual frame side data item."""
|
189
|
+
|
158
190
|
key: str | None = None
|
159
191
|
value: str | None = None
|
160
192
|
|
161
193
|
|
162
194
|
@dataclass(kw_only=True, frozen=True)
|
163
195
|
class frameSideDataTimecodeList:
|
196
|
+
"""Container for frame side data timecode information."""
|
197
|
+
|
164
198
|
timecode: tuple["frameSideDataTimecodeType", ...] | None = None
|
165
199
|
|
166
200
|
|
167
201
|
@dataclass(kw_only=True, frozen=True)
|
168
202
|
class frameSideDataTimecodeType:
|
203
|
+
"""Information about frame side data timecode."""
|
204
|
+
|
169
205
|
value: str | None = None
|
170
206
|
|
171
207
|
|
172
208
|
@dataclass(kw_only=True, frozen=True)
|
173
209
|
class frameSideDataComponentList:
|
210
|
+
"""Container for frame side data component information."""
|
211
|
+
|
174
212
|
component: tuple["frameSideDataComponentType", ...] | None = None
|
175
213
|
|
176
214
|
|
177
215
|
@dataclass(kw_only=True, frozen=True)
|
178
216
|
class frameSideDataComponentType:
|
217
|
+
"""Information about frame side data component."""
|
218
|
+
|
179
219
|
pieces: Optional["frameSideDataPieceList"] = None
|
180
220
|
side_datum: tuple["frameSideDatumType", ...] | None = None
|
181
221
|
|
182
222
|
|
183
223
|
@dataclass(kw_only=True, frozen=True)
|
184
224
|
class frameSideDataPieceList:
|
225
|
+
"""Container for frame side data piece information."""
|
226
|
+
|
185
227
|
piece: tuple["frameSideDataPieceType", ...] | None = None
|
186
228
|
|
187
229
|
|
188
230
|
@dataclass(kw_only=True, frozen=True)
|
189
231
|
class frameSideDataPieceType:
|
232
|
+
"""Information about frame side data piece."""
|
233
|
+
|
190
234
|
side_datum: tuple["frameSideDatumType", ...] | None = None
|
191
235
|
|
192
236
|
|
193
237
|
@dataclass(kw_only=True, frozen=True)
|
194
238
|
class subtitleType:
|
239
|
+
"""Information about a subtitle frame."""
|
240
|
+
|
195
241
|
media_type: str | None = None
|
196
242
|
pts: int | None = None
|
197
243
|
pts_time: float | None = None
|
@@ -203,16 +249,22 @@ class subtitleType:
|
|
203
249
|
|
204
250
|
@dataclass(kw_only=True, frozen=True)
|
205
251
|
class streamsType:
|
252
|
+
"""Container for stream information."""
|
253
|
+
|
206
254
|
stream: tuple["streamType", ...] | None = None
|
207
255
|
|
208
256
|
|
209
257
|
@dataclass(kw_only=True, frozen=True)
|
210
258
|
class programsType:
|
259
|
+
"""Container for program information."""
|
260
|
+
|
211
261
|
program: tuple["programType", ...] | None = None
|
212
262
|
|
213
263
|
|
214
264
|
@dataclass(kw_only=True, frozen=True)
|
215
265
|
class streamDispositionType:
|
266
|
+
"""Information about stream disposition flags."""
|
267
|
+
|
216
268
|
default: int | None = None
|
217
269
|
dub: int | None = None
|
218
270
|
original: int | None = None
|
@@ -235,6 +287,8 @@ class streamDispositionType:
|
|
235
287
|
|
236
288
|
@dataclass(kw_only=True, frozen=True)
|
237
289
|
class streamType:
|
290
|
+
"""Information about a single stream."""
|
291
|
+
|
238
292
|
disposition: Optional["streamDispositionType"] = None
|
239
293
|
tags: Optional["tagsType"] = None
|
240
294
|
side_data_list: Optional["packetSideDataListType"] = None
|
@@ -290,6 +344,8 @@ class streamType:
|
|
290
344
|
|
291
345
|
@dataclass(kw_only=True, frozen=True)
|
292
346
|
class programType:
|
347
|
+
"""Information about a single program."""
|
348
|
+
|
293
349
|
tags: Optional["tagsType"] = None
|
294
350
|
streams: Optional["streamsType"] = None
|
295
351
|
program_id: int | None = None
|
@@ -301,6 +357,8 @@ class programType:
|
|
301
357
|
|
302
358
|
@dataclass(kw_only=True, frozen=True)
|
303
359
|
class formatType:
|
360
|
+
"""Information about the media format."""
|
361
|
+
|
304
362
|
tags: Optional["tagsType"] = None
|
305
363
|
filename: str | None = None
|
306
364
|
nb_streams: int | None = None
|
@@ -317,18 +375,24 @@ class formatType:
|
|
317
375
|
|
318
376
|
@dataclass(kw_only=True, frozen=True)
|
319
377
|
class tagType:
|
378
|
+
"""Information about a single tag."""
|
379
|
+
|
320
380
|
key: str | None = None
|
321
381
|
value: str | None = None
|
322
382
|
|
323
383
|
|
324
384
|
@dataclass(kw_only=True, frozen=True)
|
325
385
|
class errorType:
|
386
|
+
"""Information about an error."""
|
387
|
+
|
326
388
|
code: int | None = None
|
327
389
|
string: str | None = None
|
328
390
|
|
329
391
|
|
330
392
|
@dataclass(kw_only=True, frozen=True)
|
331
393
|
class programVersionType:
|
394
|
+
"""Information about the program version."""
|
395
|
+
|
332
396
|
version: str | None = None
|
333
397
|
copyright: str | None = None
|
334
398
|
build_date: str | None = None
|
@@ -339,11 +403,15 @@ class programVersionType:
|
|
339
403
|
|
340
404
|
@dataclass(kw_only=True, frozen=True)
|
341
405
|
class chaptersType:
|
406
|
+
"""Container for chapter information."""
|
407
|
+
|
342
408
|
chapter: tuple["chapterType", ...] | None = None
|
343
409
|
|
344
410
|
|
345
411
|
@dataclass(kw_only=True, frozen=True)
|
346
412
|
class chapterType:
|
413
|
+
"""Information about a single chapter."""
|
414
|
+
|
347
415
|
tags: tuple["tagsType", ...] | None = None
|
348
416
|
id: int | None = None
|
349
417
|
time_base: str | None = None
|
@@ -355,6 +423,8 @@ class chapterType:
|
|
355
423
|
|
356
424
|
@dataclass(kw_only=True, frozen=True)
|
357
425
|
class libraryVersionType:
|
426
|
+
"""Information about a library version."""
|
427
|
+
|
358
428
|
name: str | None = None
|
359
429
|
major: int | None = None
|
360
430
|
minor: int | None = None
|
@@ -365,11 +435,15 @@ class libraryVersionType:
|
|
365
435
|
|
366
436
|
@dataclass(kw_only=True, frozen=True)
|
367
437
|
class libraryVersionsType:
|
438
|
+
"""Container for library version information."""
|
439
|
+
|
368
440
|
library_version: tuple["libraryVersionType", ...] | None = None
|
369
441
|
|
370
442
|
|
371
443
|
@dataclass(kw_only=True, frozen=True)
|
372
444
|
class pixelFormatFlagsType:
|
445
|
+
"""Information about pixel format flags."""
|
446
|
+
|
373
447
|
big_endian: int | None = None
|
374
448
|
palette: int | None = None
|
375
449
|
bitstream: int | None = None
|
@@ -381,17 +455,23 @@ class pixelFormatFlagsType:
|
|
381
455
|
|
382
456
|
@dataclass(kw_only=True, frozen=True)
|
383
457
|
class pixelFormatComponentType:
|
458
|
+
"""Information about a pixel format component."""
|
459
|
+
|
384
460
|
index: int | None = None
|
385
461
|
bit_depth: int | None = None
|
386
462
|
|
387
463
|
|
388
464
|
@dataclass(kw_only=True, frozen=True)
|
389
465
|
class pixelFormatComponentsType:
|
466
|
+
"""Container for pixel format component information."""
|
467
|
+
|
390
468
|
component: tuple["pixelFormatComponentType", ...] | None = None
|
391
469
|
|
392
470
|
|
393
471
|
@dataclass(kw_only=True, frozen=True)
|
394
472
|
class pixelFormatType:
|
473
|
+
"""Information about a pixel format."""
|
474
|
+
|
395
475
|
flags: Optional["pixelFormatFlagsType"] = None
|
396
476
|
components: Optional["pixelFormatComponentsType"] = None
|
397
477
|
name: str | None = None
|
@@ -403,6 +483,8 @@ class pixelFormatType:
|
|
403
483
|
|
404
484
|
@dataclass(kw_only=True, frozen=True)
|
405
485
|
class pixelFormatsType:
|
486
|
+
"""Container for pixel format information."""
|
487
|
+
|
406
488
|
pixel_format: tuple["pixelFormatType", ...] | None = None
|
407
489
|
|
408
490
|
|
typed_ffmpeg/ffprobe/xml2json.py
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
+
"""XML to JSON conversion utilities for FFprobe output."""
|
2
|
+
|
1
3
|
import json
|
2
4
|
import xml.etree.ElementTree as ET
|
3
5
|
from typing import Any, cast
|
4
6
|
|
5
7
|
|
6
8
|
def xml_to_dict(element: ET.Element) -> dict[str, Any]:
|
7
|
-
"""
|
9
|
+
"""
|
10
|
+
Convert an XML Element to a dictionary representation.
|
8
11
|
|
9
12
|
This function recursively converts an XML Element and its children into a nested
|
10
13
|
dictionary structure. Attributes are preserved as dictionary keys, and text content
|
@@ -19,6 +22,7 @@ def xml_to_dict(element: ET.Element) -> dict[str, Any]:
|
|
19
22
|
- Child elements are stored as nested dictionaries
|
20
23
|
- Multiple elements with the same tag are stored as lists
|
21
24
|
- Text content is stored under the 'text' key
|
25
|
+
|
22
26
|
"""
|
23
27
|
node: dict[str, Any] = {}
|
24
28
|
if element.attrib:
|
@@ -43,7 +47,8 @@ def xml_to_dict(element: ET.Element) -> dict[str, Any]:
|
|
43
47
|
|
44
48
|
|
45
49
|
def xml_string_to_json(xml_string: str) -> str:
|
46
|
-
"""
|
50
|
+
"""
|
51
|
+
Convert an XML string to a JSON string.
|
47
52
|
|
48
53
|
This function takes an XML string, parses it into an ElementTree structure,
|
49
54
|
converts it to a dictionary using xml_to_dict, and then serializes it to a JSON string.
|
@@ -65,6 +70,7 @@ def xml_string_to_json(xml_string: str) -> str:
|
|
65
70
|
}
|
66
71
|
}
|
67
72
|
}'
|
73
|
+
|
68
74
|
"""
|
69
75
|
root = ET.fromstring(xml_string)
|
70
76
|
return json.dumps({root.tag: xml_to_dict(root)}, indent=2)
|