json2xml 6.0.5__tar.gz → 6.0.7__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.
- {json2xml-6.0.5 → json2xml-6.0.7}/HISTORY.rst +17 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/PKG-INFO +45 -45
- {json2xml-6.0.5 → json2xml-6.0.7}/README.rst +44 -44
- {json2xml-6.0.5 → json2xml-6.0.7}/json2xml/__init__.py +1 -2
- {json2xml-6.0.5 → json2xml-6.0.7}/json2xml/cli.py +18 -15
- {json2xml-6.0.5 → json2xml-6.0.7}/json2xml/dicttoxml.py +34 -30
- {json2xml-6.0.5 → json2xml-6.0.7}/json2xml/dicttoxml_fast.py +9 -6
- {json2xml-6.0.5 → json2xml-6.0.7}/json2xml/json2xml.py +6 -2
- json2xml-6.0.7/json2xml/types.py +8 -0
- json2xml-6.0.7/json2xml/utils.py +73 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/json2xml.egg-info/PKG-INFO +45 -45
- {json2xml-6.0.5 → json2xml-6.0.7}/json2xml.egg-info/SOURCES.txt +1 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/pyproject.toml +1 -2
- {json2xml-6.0.5 → json2xml-6.0.7}/setup.cfg +1 -1
- {json2xml-6.0.5 → json2xml-6.0.7}/tests/conftest.py +1 -1
- {json2xml-6.0.5 → json2xml-6.0.7}/tests/test_cli.py +2 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/tests/test_dict2xml.py +183 -44
- {json2xml-6.0.5 → json2xml-6.0.7}/tests/test_json2xml.py +18 -6
- {json2xml-6.0.5 → json2xml-6.0.7}/tests/test_rust_dicttoxml.py +23 -4
- {json2xml-6.0.5 → json2xml-6.0.7}/tests/test_utils.py +66 -79
- json2xml-6.0.5/json2xml/utils.py +0 -54
- {json2xml-6.0.5 → json2xml-6.0.7}/AUTHORS.rst +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/CONTRIBUTING.rst +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/LICENSE +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/MANIFEST.in +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/docs/Makefile +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/docs/authors.rst +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/docs/benchmarks.rst +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/docs/conf.py +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/docs/contributing.rst +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/docs/history.rst +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/docs/index.rst +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/docs/installation.rst +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/docs/json2xml.rst +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/docs/make.bat +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/docs/modules.rst +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/docs/readme.rst +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/docs/usage.rst +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/json2xml.egg-info/dependency_links.txt +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/json2xml.egg-info/entry_points.txt +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/json2xml.egg-info/requires.txt +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/json2xml.egg-info/top_level.txt +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/requirements.in +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/setup.py +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/tests/__init__.py +0 -0
- {json2xml-6.0.5 → json2xml-6.0.7}/tests/test_missing_coverage.py +0 -0
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
|
|
2
|
+
6.0.7 / 2026-04-26
|
|
3
|
+
==================
|
|
4
|
+
|
|
5
|
+
* fix: handle JSON conversion edge cases and improve URL reader robustness (#284)
|
|
6
|
+
* chore(deps-dev): bump pytest from 9.0.1 to 9.0.3 (#282)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
6.0.6 / 2026-04-08
|
|
10
|
+
==================
|
|
11
|
+
|
|
12
|
+
* fix: review cleanup (#281)
|
|
13
|
+
* fix: remove needless line (#280)
|
|
14
|
+
* docs: add lat.md support to the codebase (#279)
|
|
15
|
+
* chore(deps): bump pygments from 2.19.1 to 2.20.0 in /docs (#278)
|
|
16
|
+
* fix: upgrade Python workflow to 3.12
|
|
17
|
+
|
|
18
|
+
|
|
2
19
|
6.0.5 / 2026-03-12
|
|
3
20
|
==================
|
|
4
21
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: json2xml
|
|
3
|
-
Version: 6.0.
|
|
3
|
+
Version: 6.0.7
|
|
4
4
|
Summary: Simple Python Library to convert JSON to XML
|
|
5
5
|
Author-email: Vinit Kumar <mail@vinitkumar.me>
|
|
6
6
|
License: Apache Software License 2.0
|
|
@@ -79,7 +79,7 @@ Installation
|
|
|
79
79
|
|
|
80
80
|
pip install json2xml
|
|
81
81
|
|
|
82
|
-
**With Native Rust Acceleration (up to
|
|
82
|
+
**With Native Rust Acceleration (up to 129x faster)**
|
|
83
83
|
|
|
84
84
|
For maximum performance, install the optional Rust extension:
|
|
85
85
|
|
|
@@ -91,7 +91,7 @@ For maximum performance, install the optional Rust extension:
|
|
|
91
91
|
# Or install the Rust extension separately
|
|
92
92
|
pip install json2xml-rs
|
|
93
93
|
|
|
94
|
-
The Rust extension provides **
|
|
94
|
+
The Rust extension provides **57-129x faster** conversion compared to pure Python in the latest benchmark. It's automatically used when available, with seamless fallback to pure Python.
|
|
95
95
|
|
|
96
96
|
**As a CLI Tool**
|
|
97
97
|
|
|
@@ -337,7 +337,7 @@ Using tools directly:
|
|
|
337
337
|
|
|
338
338
|
**Rust Extension Development**
|
|
339
339
|
|
|
340
|
-
The optional Rust extension (``json2xml-rs``) provides up to
|
|
340
|
+
The optional Rust extension (``json2xml-rs``) provides up to 129x faster performance in the latest benchmark. To develop or build the Rust extension:
|
|
341
341
|
|
|
342
342
|
Prerequisites:
|
|
343
343
|
|
|
@@ -464,21 +464,21 @@ For users who need maximum performance within Python, json2xml includes an optio
|
|
|
464
464
|
- Rust Extension
|
|
465
465
|
- Speedup
|
|
466
466
|
* - **Small JSON** (47 bytes)
|
|
467
|
-
-
|
|
468
|
-
-
|
|
469
|
-
- **
|
|
467
|
+
- 31.49µs
|
|
468
|
+
- 0.55µs
|
|
469
|
+
- **56.8x**
|
|
470
470
|
* - **Medium JSON** (3.2 KB)
|
|
471
|
-
-
|
|
472
|
-
- 15µs
|
|
473
|
-
- **
|
|
471
|
+
- 1.69ms
|
|
472
|
+
- 16.15µs
|
|
473
|
+
- **105.0x**
|
|
474
474
|
* - **Large JSON** (32 KB)
|
|
475
|
-
-
|
|
476
|
-
-
|
|
477
|
-
- **
|
|
475
|
+
- 17.97ms
|
|
476
|
+
- 168.21µs
|
|
477
|
+
- **106.8x**
|
|
478
478
|
* - **Very Large JSON** (323 KB)
|
|
479
|
-
-
|
|
480
|
-
- 1.
|
|
481
|
-
- **
|
|
479
|
+
- 183.33ms
|
|
480
|
+
- 1.42ms
|
|
481
|
+
- **129.0x**
|
|
482
482
|
|
|
483
483
|
**Usage with Rust Extension:**
|
|
484
484
|
|
|
@@ -508,7 +508,7 @@ For other platforms, the pure Python version is used automatically.
|
|
|
508
508
|
Performance Benchmarks
|
|
509
509
|
^^^^^^^^^^^^^^^^^^^^^^
|
|
510
510
|
|
|
511
|
-
Comprehensive benchmarks comparing all implementations (Apple Silicon,
|
|
511
|
+
Comprehensive benchmarks comparing all implementations (Apple Silicon, macOS 26.4.1, Python 3.14.4, April 2026):
|
|
512
512
|
|
|
513
513
|
.. list-table::
|
|
514
514
|
:header-rows: 1
|
|
@@ -521,40 +521,40 @@ Comprehensive benchmarks comparing all implementations (Apple Silicon, January 2
|
|
|
521
521
|
- Zig
|
|
522
522
|
- Best
|
|
523
523
|
* - **Small** (47B)
|
|
524
|
-
-
|
|
525
|
-
-
|
|
526
|
-
- 4.
|
|
527
|
-
-
|
|
528
|
-
- Rust (
|
|
524
|
+
- 31.49µs
|
|
525
|
+
- 0.55µs
|
|
526
|
+
- 4.09ms
|
|
527
|
+
- 2.02ms
|
|
528
|
+
- Rust (56.8x)
|
|
529
529
|
* - **Medium** (3.2KB)
|
|
530
|
-
-
|
|
531
|
-
-
|
|
532
|
-
- 4.
|
|
533
|
-
-
|
|
534
|
-
- Rust (
|
|
530
|
+
- 1.69ms
|
|
531
|
+
- 16.15µs
|
|
532
|
+
- 4.07ms
|
|
533
|
+
- 2.09ms
|
|
534
|
+
- Rust (105.0x)
|
|
535
535
|
* - **Large** (32KB)
|
|
536
|
-
-
|
|
537
|
-
-
|
|
538
|
-
-
|
|
539
|
-
-
|
|
540
|
-
- Rust (
|
|
536
|
+
- 17.97ms
|
|
537
|
+
- 168.21µs
|
|
538
|
+
- 4.10ms
|
|
539
|
+
- 2.42ms
|
|
540
|
+
- Rust (106.8x)
|
|
541
541
|
* - **Very Large** (323KB)
|
|
542
|
-
-
|
|
543
|
-
-
|
|
544
|
-
- 4.
|
|
545
|
-
-
|
|
546
|
-
-
|
|
542
|
+
- 183.33ms
|
|
543
|
+
- 1.42ms
|
|
544
|
+
- 4.20ms
|
|
545
|
+
- 5.12ms
|
|
546
|
+
- Rust (129.0x)
|
|
547
547
|
|
|
548
548
|
**Key Findings:**
|
|
549
549
|
|
|
550
|
-
- **Rust extension**:
|
|
551
|
-
- **Go CLI**:
|
|
552
|
-
- **Zig CLI**:
|
|
550
|
+
- **Rust extension**: 57-129x faster than Python, zero process overhead (best for Python users)
|
|
551
|
+
- **Go CLI**: 43.6x faster for very large files (300KB+), but has ~4ms startup overhead
|
|
552
|
+
- **Zig CLI**: 7.4x faster for large files and 35.8x faster for very large files, with ~2ms startup overhead
|
|
553
553
|
|
|
554
554
|
**Recommendation by Use Case:**
|
|
555
555
|
|
|
556
|
-
- **Python library calls**: Use ``pip install json2xml[fast]`` (Rust,
|
|
557
|
-
- **Large file CLI processing**: Use `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_
|
|
556
|
+
- **Python library calls**: Use ``pip install json2xml[fast]`` (Rust, up to 129x faster)
|
|
557
|
+
- **Large file CLI processing**: Use `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ or `json2xml-zig <https://github.com/vinitkumar/json2xml-zig>`_ depending on your workload
|
|
558
558
|
- **Pure Python required**: Use ``pip install json2xml``
|
|
559
559
|
|
|
560
560
|
For detailed benchmarks, see `BENCHMARKS.md <https://github.com/vinitkumar/json2xml/blob/master/BENCHMARKS.md>`_.
|
|
@@ -565,9 +565,9 @@ Other Implementations
|
|
|
565
565
|
|
|
566
566
|
This library is also available in other languages:
|
|
567
567
|
|
|
568
|
-
- **Rust**: `json2xml-rs <https://pypi.org/project/json2xml-rs/>`_ -
|
|
569
|
-
- **Go**: `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ -
|
|
570
|
-
- **Zig**: `json2xml-zig <https://github.com/vinitkumar/json2xml-zig>`_ -
|
|
568
|
+
- **Rust**: `json2xml-rs <https://pypi.org/project/json2xml-rs/>`_ - up to 129x faster, Python extension via PyO3
|
|
569
|
+
- **Go**: `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ - 43.6x faster for very large files, native CLI
|
|
570
|
+
- **Zig**: `json2xml-zig <https://github.com/vinitkumar/json2xml-zig>`_ - 35.8x faster for very large files, native CLI
|
|
571
571
|
|
|
572
572
|
|
|
573
573
|
Help and Support to maintain this project
|
|
@@ -43,7 +43,7 @@ Installation
|
|
|
43
43
|
|
|
44
44
|
pip install json2xml
|
|
45
45
|
|
|
46
|
-
**With Native Rust Acceleration (up to
|
|
46
|
+
**With Native Rust Acceleration (up to 129x faster)**
|
|
47
47
|
|
|
48
48
|
For maximum performance, install the optional Rust extension:
|
|
49
49
|
|
|
@@ -55,7 +55,7 @@ For maximum performance, install the optional Rust extension:
|
|
|
55
55
|
# Or install the Rust extension separately
|
|
56
56
|
pip install json2xml-rs
|
|
57
57
|
|
|
58
|
-
The Rust extension provides **
|
|
58
|
+
The Rust extension provides **57-129x faster** conversion compared to pure Python in the latest benchmark. It's automatically used when available, with seamless fallback to pure Python.
|
|
59
59
|
|
|
60
60
|
**As a CLI Tool**
|
|
61
61
|
|
|
@@ -301,7 +301,7 @@ Using tools directly:
|
|
|
301
301
|
|
|
302
302
|
**Rust Extension Development**
|
|
303
303
|
|
|
304
|
-
The optional Rust extension (``json2xml-rs``) provides up to
|
|
304
|
+
The optional Rust extension (``json2xml-rs``) provides up to 129x faster performance in the latest benchmark. To develop or build the Rust extension:
|
|
305
305
|
|
|
306
306
|
Prerequisites:
|
|
307
307
|
|
|
@@ -428,21 +428,21 @@ For users who need maximum performance within Python, json2xml includes an optio
|
|
|
428
428
|
- Rust Extension
|
|
429
429
|
- Speedup
|
|
430
430
|
* - **Small JSON** (47 bytes)
|
|
431
|
-
-
|
|
432
|
-
-
|
|
433
|
-
- **
|
|
431
|
+
- 31.49µs
|
|
432
|
+
- 0.55µs
|
|
433
|
+
- **56.8x**
|
|
434
434
|
* - **Medium JSON** (3.2 KB)
|
|
435
|
-
-
|
|
436
|
-
- 15µs
|
|
437
|
-
- **
|
|
435
|
+
- 1.69ms
|
|
436
|
+
- 16.15µs
|
|
437
|
+
- **105.0x**
|
|
438
438
|
* - **Large JSON** (32 KB)
|
|
439
|
-
-
|
|
440
|
-
-
|
|
441
|
-
- **
|
|
439
|
+
- 17.97ms
|
|
440
|
+
- 168.21µs
|
|
441
|
+
- **106.8x**
|
|
442
442
|
* - **Very Large JSON** (323 KB)
|
|
443
|
-
-
|
|
444
|
-
- 1.
|
|
445
|
-
- **
|
|
443
|
+
- 183.33ms
|
|
444
|
+
- 1.42ms
|
|
445
|
+
- **129.0x**
|
|
446
446
|
|
|
447
447
|
**Usage with Rust Extension:**
|
|
448
448
|
|
|
@@ -472,7 +472,7 @@ For other platforms, the pure Python version is used automatically.
|
|
|
472
472
|
Performance Benchmarks
|
|
473
473
|
^^^^^^^^^^^^^^^^^^^^^^
|
|
474
474
|
|
|
475
|
-
Comprehensive benchmarks comparing all implementations (Apple Silicon,
|
|
475
|
+
Comprehensive benchmarks comparing all implementations (Apple Silicon, macOS 26.4.1, Python 3.14.4, April 2026):
|
|
476
476
|
|
|
477
477
|
.. list-table::
|
|
478
478
|
:header-rows: 1
|
|
@@ -485,40 +485,40 @@ Comprehensive benchmarks comparing all implementations (Apple Silicon, January 2
|
|
|
485
485
|
- Zig
|
|
486
486
|
- Best
|
|
487
487
|
* - **Small** (47B)
|
|
488
|
-
-
|
|
489
|
-
-
|
|
490
|
-
- 4.
|
|
491
|
-
-
|
|
492
|
-
- Rust (
|
|
488
|
+
- 31.49µs
|
|
489
|
+
- 0.55µs
|
|
490
|
+
- 4.09ms
|
|
491
|
+
- 2.02ms
|
|
492
|
+
- Rust (56.8x)
|
|
493
493
|
* - **Medium** (3.2KB)
|
|
494
|
-
-
|
|
495
|
-
-
|
|
496
|
-
- 4.
|
|
497
|
-
-
|
|
498
|
-
- Rust (
|
|
494
|
+
- 1.69ms
|
|
495
|
+
- 16.15µs
|
|
496
|
+
- 4.07ms
|
|
497
|
+
- 2.09ms
|
|
498
|
+
- Rust (105.0x)
|
|
499
499
|
* - **Large** (32KB)
|
|
500
|
-
-
|
|
501
|
-
-
|
|
502
|
-
-
|
|
503
|
-
-
|
|
504
|
-
- Rust (
|
|
500
|
+
- 17.97ms
|
|
501
|
+
- 168.21µs
|
|
502
|
+
- 4.10ms
|
|
503
|
+
- 2.42ms
|
|
504
|
+
- Rust (106.8x)
|
|
505
505
|
* - **Very Large** (323KB)
|
|
506
|
-
-
|
|
507
|
-
-
|
|
508
|
-
- 4.
|
|
509
|
-
-
|
|
510
|
-
-
|
|
506
|
+
- 183.33ms
|
|
507
|
+
- 1.42ms
|
|
508
|
+
- 4.20ms
|
|
509
|
+
- 5.12ms
|
|
510
|
+
- Rust (129.0x)
|
|
511
511
|
|
|
512
512
|
**Key Findings:**
|
|
513
513
|
|
|
514
|
-
- **Rust extension**:
|
|
515
|
-
- **Go CLI**:
|
|
516
|
-
- **Zig CLI**:
|
|
514
|
+
- **Rust extension**: 57-129x faster than Python, zero process overhead (best for Python users)
|
|
515
|
+
- **Go CLI**: 43.6x faster for very large files (300KB+), but has ~4ms startup overhead
|
|
516
|
+
- **Zig CLI**: 7.4x faster for large files and 35.8x faster for very large files, with ~2ms startup overhead
|
|
517
517
|
|
|
518
518
|
**Recommendation by Use Case:**
|
|
519
519
|
|
|
520
|
-
- **Python library calls**: Use ``pip install json2xml[fast]`` (Rust,
|
|
521
|
-
- **Large file CLI processing**: Use `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_
|
|
520
|
+
- **Python library calls**: Use ``pip install json2xml[fast]`` (Rust, up to 129x faster)
|
|
521
|
+
- **Large file CLI processing**: Use `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ or `json2xml-zig <https://github.com/vinitkumar/json2xml-zig>`_ depending on your workload
|
|
522
522
|
- **Pure Python required**: Use ``pip install json2xml``
|
|
523
523
|
|
|
524
524
|
For detailed benchmarks, see `BENCHMARKS.md <https://github.com/vinitkumar/json2xml/blob/master/BENCHMARKS.md>`_.
|
|
@@ -529,9 +529,9 @@ Other Implementations
|
|
|
529
529
|
|
|
530
530
|
This library is also available in other languages:
|
|
531
531
|
|
|
532
|
-
- **Rust**: `json2xml-rs <https://pypi.org/project/json2xml-rs/>`_ -
|
|
533
|
-
- **Go**: `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ -
|
|
534
|
-
- **Zig**: `json2xml-zig <https://github.com/vinitkumar/json2xml-zig>`_ -
|
|
532
|
+
- **Rust**: `json2xml-rs <https://pypi.org/project/json2xml-rs/>`_ - up to 129x faster, Python extension via PyO3
|
|
533
|
+
- **Go**: `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ - 43.6x faster for very large files, native CLI
|
|
534
|
+
- **Zig**: `json2xml-zig <https://github.com/vinitkumar/json2xml-zig>`_ - 35.8x faster for very large files, native CLI
|
|
535
535
|
|
|
536
536
|
|
|
537
537
|
Help and Support to maintain this project
|
|
@@ -43,10 +43,11 @@ from __future__ import annotations
|
|
|
43
43
|
|
|
44
44
|
import argparse
|
|
45
45
|
import sys
|
|
46
|
-
from typing import
|
|
46
|
+
from typing import NoReturn
|
|
47
47
|
|
|
48
48
|
from json2xml import __version__
|
|
49
49
|
from json2xml.json2xml import Json2xml
|
|
50
|
+
from json2xml.types import JSONValue
|
|
50
51
|
from json2xml.utils import (
|
|
51
52
|
JSONReadError,
|
|
52
53
|
StringReadError,
|
|
@@ -60,6 +61,13 @@ AUTHOR = "Vinit Kumar"
|
|
|
60
61
|
EMAIL = "mail@vinitkumar.me"
|
|
61
62
|
|
|
62
63
|
|
|
64
|
+
def exit_with_error(message: str) -> NoReturn:
|
|
65
|
+
"""Print an error message and terminate CLI processing."""
|
|
66
|
+
print(message, file=sys.stderr)
|
|
67
|
+
raise SystemExit(1)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# @lat: [[architecture#CLI entrypoint]]
|
|
63
71
|
def create_parser() -> argparse.ArgumentParser:
|
|
64
72
|
"""Create and configure the argument parser."""
|
|
65
73
|
parser = argparse.ArgumentParser(
|
|
@@ -228,7 +236,8 @@ Examples:
|
|
|
228
236
|
return parser
|
|
229
237
|
|
|
230
238
|
|
|
231
|
-
|
|
239
|
+
# @lat: [[behavior#Input readers]]
|
|
240
|
+
def read_input(args: argparse.Namespace) -> JSONValue:
|
|
232
241
|
"""
|
|
233
242
|
Read JSON input from the specified source.
|
|
234
243
|
|
|
@@ -248,15 +257,13 @@ def read_input(args: argparse.Namespace) -> dict[str, Any] | list[Any]:
|
|
|
248
257
|
try:
|
|
249
258
|
return readfromurl(args.url)
|
|
250
259
|
except URLReadError as e:
|
|
251
|
-
|
|
252
|
-
sys.exit(1)
|
|
260
|
+
exit_with_error(f"Error reading from URL: {e}")
|
|
253
261
|
|
|
254
262
|
if args.string:
|
|
255
263
|
try:
|
|
256
264
|
return readfromstring(args.string)
|
|
257
265
|
except StringReadError as e:
|
|
258
|
-
|
|
259
|
-
sys.exit(1)
|
|
266
|
+
exit_with_error(f"Error parsing JSON string: {e}")
|
|
260
267
|
|
|
261
268
|
if args.input_file:
|
|
262
269
|
if args.input_file == "-":
|
|
@@ -265,18 +272,16 @@ def read_input(args: argparse.Namespace) -> dict[str, Any] | list[Any]:
|
|
|
265
272
|
try:
|
|
266
273
|
return readfromjson(args.input_file)
|
|
267
274
|
except JSONReadError as e:
|
|
268
|
-
|
|
269
|
-
sys.exit(1)
|
|
275
|
+
exit_with_error(f"Error reading JSON file: {e}")
|
|
270
276
|
|
|
271
277
|
# Check if there's data on stdin
|
|
272
278
|
if not sys.stdin.isatty():
|
|
273
279
|
return read_from_stdin()
|
|
274
280
|
|
|
275
|
-
|
|
276
|
-
sys.exit(1)
|
|
281
|
+
exit_with_error("Error: No input provided. Use -h for help.")
|
|
277
282
|
|
|
278
283
|
|
|
279
|
-
def read_from_stdin() ->
|
|
284
|
+
def read_from_stdin() -> JSONValue:
|
|
280
285
|
"""
|
|
281
286
|
Read JSON from standard input.
|
|
282
287
|
|
|
@@ -289,12 +294,10 @@ def read_from_stdin() -> dict[str, Any] | list[Any]:
|
|
|
289
294
|
try:
|
|
290
295
|
json_str = sys.stdin.read().strip()
|
|
291
296
|
if not json_str:
|
|
292
|
-
|
|
293
|
-
sys.exit(1)
|
|
297
|
+
exit_with_error("Error: Empty input")
|
|
294
298
|
return readfromstring(json_str)
|
|
295
299
|
except StringReadError as e:
|
|
296
|
-
|
|
297
|
-
sys.exit(1)
|
|
300
|
+
exit_with_error(f"Error parsing JSON from stdin: {e}")
|
|
298
301
|
|
|
299
302
|
|
|
300
303
|
def write_output(output: str | bytes, output_file: str | None) -> None:
|
|
@@ -12,6 +12,7 @@ from typing import Any, Union, cast
|
|
|
12
12
|
from defusedxml.minidom import parseString
|
|
13
13
|
|
|
14
14
|
# Create a safe random number generator
|
|
15
|
+
_SAFE_RANDOM = SystemRandom()
|
|
15
16
|
|
|
16
17
|
# Set up logging
|
|
17
18
|
LOG = logging.getLogger("dicttoxml")
|
|
@@ -29,8 +30,7 @@ def make_id(element: str, start: int = 100000, end: int = 999999) -> str:
|
|
|
29
30
|
Returns:
|
|
30
31
|
str: The generated ID.
|
|
31
32
|
"""
|
|
32
|
-
|
|
33
|
-
return f"{element}_{safe_random.randint(start, end)}"
|
|
33
|
+
return f"{element}_{_SAFE_RANDOM.randint(start, end)}"
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def get_unique_id(element: str) -> str:
|
|
@@ -72,7 +72,7 @@ ELEMENT = Union[
|
|
|
72
72
|
]
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
def get_xml_type(val:
|
|
75
|
+
def get_xml_type(val: Any) -> str:
|
|
76
76
|
"""
|
|
77
77
|
Get the XML type of a given value.
|
|
78
78
|
|
|
@@ -102,7 +102,7 @@ def get_xml_type(val: ELEMENT) -> str:
|
|
|
102
102
|
return type(val).__name__
|
|
103
103
|
|
|
104
104
|
|
|
105
|
-
def escape_xml(s: str | int | float | numbers.Number) -> str:
|
|
105
|
+
def escape_xml(s: str | int | float | numbers.Number | None) -> str:
|
|
106
106
|
"""
|
|
107
107
|
Escape a string for use in XML.
|
|
108
108
|
|
|
@@ -155,32 +155,24 @@ def key_is_valid_xml(key: str) -> bool:
|
|
|
155
155
|
|
|
156
156
|
|
|
157
157
|
def make_valid_xml_name(key: str, attr: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
|
158
|
-
"""
|
|
159
|
-
key =
|
|
160
|
-
# nothing happens at escape_xml if attr is not a string, we don't
|
|
161
|
-
# need to pass it to the method at all.
|
|
162
|
-
# attr = escape_xml(attr)
|
|
158
|
+
"""Return a valid XML element name and carry the original key as metadata when needed."""
|
|
159
|
+
key = str(key)
|
|
163
160
|
|
|
164
|
-
# pass through if key is already valid
|
|
165
161
|
if key_is_valid_xml(key):
|
|
166
162
|
return key, attr
|
|
167
163
|
|
|
168
|
-
|
|
169
|
-
if isinstance(key, int) or key.isdigit():
|
|
164
|
+
if key.isdigit():
|
|
170
165
|
return f"n{key}", attr
|
|
171
166
|
|
|
172
|
-
|
|
173
|
-
if key_is_valid_xml(
|
|
174
|
-
return
|
|
167
|
+
key_with_underscores = key.replace(" ", "_")
|
|
168
|
+
if key_is_valid_xml(key_with_underscores):
|
|
169
|
+
return key_with_underscores, attr
|
|
175
170
|
|
|
176
|
-
|
|
177
|
-
if key_is_valid_xml(key.replace(":", "").replace("@flat", "")):
|
|
171
|
+
if ":" in key and key_is_valid_xml(key.replace(":", "")):
|
|
178
172
|
return key, attr
|
|
179
173
|
|
|
180
|
-
# key is still invalid - move it into a name attribute
|
|
181
174
|
attr["name"] = key
|
|
182
|
-
|
|
183
|
-
return key, attr
|
|
175
|
+
return "key", attr
|
|
184
176
|
|
|
185
177
|
|
|
186
178
|
def wrap_cdata(s: str | int | float | numbers.Number) -> str:
|
|
@@ -227,6 +219,7 @@ def get_xpath31_tag_name(val: Any) -> str:
|
|
|
227
219
|
return "string"
|
|
228
220
|
|
|
229
221
|
|
|
222
|
+
# @lat: [[behavior#XPath 3.1 format]]
|
|
230
223
|
def convert_to_xpath31(obj: Any, parent_key: str | None = None) -> str:
|
|
231
224
|
"""
|
|
232
225
|
Convert a Python object to XPath 3.1 json-to-xml format.
|
|
@@ -267,7 +260,7 @@ def convert_to_xpath31(obj: Any, parent_key: str | None = None) -> str:
|
|
|
267
260
|
|
|
268
261
|
|
|
269
262
|
def convert(
|
|
270
|
-
obj:
|
|
263
|
+
obj: Any,
|
|
271
264
|
ids: Any,
|
|
272
265
|
attr_type: bool,
|
|
273
266
|
item_func: Callable[[str], str],
|
|
@@ -340,18 +333,24 @@ def dict2xml_str(
|
|
|
340
333
|
parse dict2xml
|
|
341
334
|
"""
|
|
342
335
|
ids: list[str] = [] # initialize list of unique ids
|
|
343
|
-
", ".join(str(key) for key in item)
|
|
344
336
|
subtree = "" # Initialize subtree with default empty string
|
|
345
337
|
|
|
346
338
|
if attr_type:
|
|
347
339
|
attr["type"] = get_xml_type(item)
|
|
348
|
-
val_attr
|
|
349
|
-
|
|
340
|
+
val_attr = dict(item["@attrs"]) if "@attrs" in item else dict(attr)
|
|
341
|
+
if "@val" in item:
|
|
342
|
+
rawitem = item["@val"]
|
|
343
|
+
elif "@attrs" in item:
|
|
344
|
+
rawitem = {key: value for key, value in item.items() if key != "@attrs"}
|
|
345
|
+
else:
|
|
346
|
+
rawitem = item
|
|
350
347
|
if is_primitive_type(rawitem):
|
|
351
|
-
if
|
|
348
|
+
if rawitem is None:
|
|
349
|
+
subtree = ""
|
|
350
|
+
elif isinstance(rawitem, bool):
|
|
351
|
+
subtree = str(rawitem).lower()
|
|
352
|
+
else:
|
|
352
353
|
subtree = escape_xml(str(rawitem))
|
|
353
|
-
if isinstance(rawitem, str):
|
|
354
|
-
subtree = escape_xml(rawitem)
|
|
355
354
|
else:
|
|
356
355
|
# we can not use convert_dict, because rawitem could be non-dict
|
|
357
356
|
subtree = convert(
|
|
@@ -423,8 +422,10 @@ def convert_dict(
|
|
|
423
422
|
|
|
424
423
|
for key, val in obj.items():
|
|
425
424
|
attr = {} if not ids else {"id": f"{get_unique_id(parent)}"}
|
|
425
|
+
key_is_flat = isinstance(key, str) and key.endswith("@flat")
|
|
426
|
+
xml_key = key[:-5] if key_is_flat else key
|
|
426
427
|
|
|
427
|
-
key, attr = make_valid_xml_name(
|
|
428
|
+
key, attr = make_valid_xml_name(xml_key, attr)
|
|
428
429
|
|
|
429
430
|
# since bool is also a subtype of number.Number and int, the check for bool
|
|
430
431
|
# never comes and hence we get wrong value for the xml type bool
|
|
@@ -468,7 +469,7 @@ def convert_dict(
|
|
|
468
469
|
item=val,
|
|
469
470
|
item_func=item_func,
|
|
470
471
|
cdata=cdata,
|
|
471
|
-
item_name=key,
|
|
472
|
+
item_name=f"{key}@flat" if key_is_flat else key,
|
|
472
473
|
item_wrap=item_wrap,
|
|
473
474
|
list_headers=list_headers
|
|
474
475
|
)
|
|
@@ -631,6 +632,7 @@ def convert_none(
|
|
|
631
632
|
return f"<{key}{attr_string}></{key}>"
|
|
632
633
|
|
|
633
634
|
|
|
635
|
+
# @lat: [[architecture#Conversion engine]]
|
|
634
636
|
def dicttoxml(
|
|
635
637
|
obj: ELEMENT,
|
|
636
638
|
root: bool = True,
|
|
@@ -640,7 +642,7 @@ def dicttoxml(
|
|
|
640
642
|
item_wrap: bool = True,
|
|
641
643
|
item_func: Callable[[str], str] = default_item_func,
|
|
642
644
|
cdata: bool = False,
|
|
643
|
-
xml_namespaces: dict[str, Any] =
|
|
645
|
+
xml_namespaces: dict[str, Any] | None = None,
|
|
644
646
|
list_headers: bool = False,
|
|
645
647
|
xpath_format: bool = False,
|
|
646
648
|
) -> bytes:
|
|
@@ -796,6 +798,8 @@ def dicttoxml(
|
|
|
796
798
|
|
|
797
799
|
output = []
|
|
798
800
|
namespace_str = ""
|
|
801
|
+
if xml_namespaces is None:
|
|
802
|
+
xml_namespaces = {}
|
|
799
803
|
for prefix in xml_namespaces:
|
|
800
804
|
if prefix == 'xsi':
|
|
801
805
|
for schema_att in xml_namespaces[prefix]:
|
|
@@ -17,22 +17,24 @@ import logging
|
|
|
17
17
|
from collections.abc import Callable
|
|
18
18
|
from typing import Any
|
|
19
19
|
|
|
20
|
+
RustStringTransform = Callable[[str], str]
|
|
21
|
+
|
|
20
22
|
LOG = logging.getLogger("dicttoxml_fast")
|
|
21
23
|
|
|
22
24
|
# Try to import the Rust implementation
|
|
23
25
|
_USE_RUST = False
|
|
24
|
-
_rust_dicttoxml = None
|
|
26
|
+
_rust_dicttoxml: Callable[..., bytes] | None = None
|
|
27
|
+
rust_escape_xml: RustStringTransform | None = None
|
|
28
|
+
rust_wrap_cdata: RustStringTransform | None = None
|
|
25
29
|
|
|
26
30
|
try:
|
|
27
|
-
from json2xml_rs import dicttoxml as _rust_dicttoxml #
|
|
28
|
-
from json2xml_rs import escape_xml_py as rust_escape_xml #
|
|
29
|
-
from json2xml_rs import wrap_cdata_py as rust_wrap_cdata #
|
|
31
|
+
from json2xml_rs import dicttoxml as _rust_dicttoxml # pragma: no cover
|
|
32
|
+
from json2xml_rs import escape_xml_py as rust_escape_xml # pragma: no cover
|
|
33
|
+
from json2xml_rs import wrap_cdata_py as rust_wrap_cdata # pragma: no cover
|
|
30
34
|
_USE_RUST = True # pragma: no cover
|
|
31
35
|
LOG.debug("Using Rust backend for dicttoxml") # pragma: no cover
|
|
32
36
|
except ImportError: # pragma: no cover
|
|
33
37
|
LOG.debug("Rust backend not available, using pure Python")
|
|
34
|
-
rust_escape_xml = None
|
|
35
|
-
rust_wrap_cdata = None
|
|
36
38
|
|
|
37
39
|
# Import the pure Python implementation as fallback
|
|
38
40
|
from json2xml import dicttoxml as _py_dicttoxml # noqa: E402
|
|
@@ -48,6 +50,7 @@ def get_backend() -> str:
|
|
|
48
50
|
return "rust" if _USE_RUST else "python"
|
|
49
51
|
|
|
50
52
|
|
|
53
|
+
# @lat: [[architecture#Backend selection]]
|
|
51
54
|
def dicttoxml(
|
|
52
55
|
obj: Any,
|
|
53
56
|
root: bool = True,
|
|
@@ -5,16 +5,18 @@ from defusedxml.minidom import parseString
|
|
|
5
5
|
|
|
6
6
|
from json2xml import dicttoxml
|
|
7
7
|
|
|
8
|
+
from .types import JSONValue
|
|
8
9
|
from .utils import InvalidDataError
|
|
9
10
|
|
|
10
11
|
|
|
12
|
+
# @lat: [[architecture#Core pipeline]]
|
|
11
13
|
class Json2xml:
|
|
12
14
|
"""
|
|
13
15
|
Wrapper class to convert the data to xml
|
|
14
16
|
"""
|
|
15
17
|
def __init__(
|
|
16
18
|
self,
|
|
17
|
-
data:
|
|
19
|
+
data: JSONValue = None,
|
|
18
20
|
wrapper: str = "all",
|
|
19
21
|
root: bool = True,
|
|
20
22
|
pretty: bool = True,
|
|
@@ -34,11 +36,13 @@ class Json2xml:
|
|
|
34
36
|
self.cdata = cdata
|
|
35
37
|
self.list_headers = list_headers
|
|
36
38
|
|
|
39
|
+
# @lat: [[behavior#Conversion output]]
|
|
40
|
+
# @lat: [[behavior#Invalid XML payloads]]
|
|
37
41
|
def to_xml(self) -> Any | None:
|
|
38
42
|
"""
|
|
39
43
|
Convert to xml using dicttoxml.dicttoxml and then pretty print it.
|
|
40
44
|
"""
|
|
41
|
-
if self.data:
|
|
45
|
+
if self.data is not None:
|
|
42
46
|
xml_data = dicttoxml.dicttoxml(
|
|
43
47
|
self.data,
|
|
44
48
|
root=self.root,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Shared JSON type aliases used by reader and converter APIs."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TypeAlias
|
|
5
|
+
|
|
6
|
+
JSONValue: TypeAlias = None | bool | int | float | str | list["JSONValue"] | dict[str, "JSONValue"]
|
|
7
|
+
|
|
8
|
+
__all__ = ["JSONValue"]
|