json2xml 6.0.6__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.6 → json2xml-6.0.7}/HISTORY.rst +7 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/PKG-INFO +45 -45
- {json2xml-6.0.6 → json2xml-6.0.7}/README.rst +44 -44
- {json2xml-6.0.6 → json2xml-6.0.7}/json2xml/__init__.py +1 -1
- {json2xml-6.0.6 → json2xml-6.0.7}/json2xml/cli.py +16 -15
- {json2xml-6.0.6 → json2xml-6.0.7}/json2xml/dicttoxml.py +25 -24
- {json2xml-6.0.6 → json2xml-6.0.7}/json2xml/json2xml.py +3 -2
- json2xml-6.0.7/json2xml/types.py +8 -0
- json2xml-6.0.7/json2xml/utils.py +73 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/json2xml.egg-info/PKG-INFO +45 -45
- {json2xml-6.0.6 → json2xml-6.0.7}/json2xml.egg-info/SOURCES.txt +1 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/pyproject.toml +1 -2
- {json2xml-6.0.6 → json2xml-6.0.7}/setup.cfg +1 -1
- {json2xml-6.0.6 → json2xml-6.0.7}/tests/test_dict2xml.py +71 -1
- {json2xml-6.0.6 → json2xml-6.0.7}/tests/test_json2xml.py +17 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/tests/test_rust_dicttoxml.py +21 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/tests/test_utils.py +66 -79
- json2xml-6.0.6/json2xml/utils.py +0 -55
- {json2xml-6.0.6 → json2xml-6.0.7}/AUTHORS.rst +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/CONTRIBUTING.rst +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/LICENSE +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/MANIFEST.in +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/docs/Makefile +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/docs/authors.rst +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/docs/benchmarks.rst +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/docs/conf.py +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/docs/contributing.rst +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/docs/history.rst +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/docs/index.rst +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/docs/installation.rst +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/docs/json2xml.rst +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/docs/make.bat +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/docs/modules.rst +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/docs/readme.rst +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/docs/usage.rst +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/json2xml/dicttoxml_fast.py +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/json2xml.egg-info/dependency_links.txt +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/json2xml.egg-info/entry_points.txt +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/json2xml.egg-info/requires.txt +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/json2xml.egg-info/top_level.txt +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/requirements.in +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/setup.py +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/tests/__init__.py +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/tests/conftest.py +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/tests/test_cli.py +0 -0
- {json2xml-6.0.6 → json2xml-6.0.7}/tests/test_missing_coverage.py +0 -0
|
@@ -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,12 @@ 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
|
+
|
|
63
70
|
# @lat: [[architecture#CLI entrypoint]]
|
|
64
71
|
def create_parser() -> argparse.ArgumentParser:
|
|
65
72
|
"""Create and configure the argument parser."""
|
|
@@ -230,7 +237,7 @@ Examples:
|
|
|
230
237
|
|
|
231
238
|
|
|
232
239
|
# @lat: [[behavior#Input readers]]
|
|
233
|
-
def read_input(args: argparse.Namespace) ->
|
|
240
|
+
def read_input(args: argparse.Namespace) -> JSONValue:
|
|
234
241
|
"""
|
|
235
242
|
Read JSON input from the specified source.
|
|
236
243
|
|
|
@@ -250,15 +257,13 @@ def read_input(args: argparse.Namespace) -> dict[str, Any] | list[Any]:
|
|
|
250
257
|
try:
|
|
251
258
|
return readfromurl(args.url)
|
|
252
259
|
except URLReadError as e:
|
|
253
|
-
|
|
254
|
-
sys.exit(1)
|
|
260
|
+
exit_with_error(f"Error reading from URL: {e}")
|
|
255
261
|
|
|
256
262
|
if args.string:
|
|
257
263
|
try:
|
|
258
264
|
return readfromstring(args.string)
|
|
259
265
|
except StringReadError as e:
|
|
260
|
-
|
|
261
|
-
sys.exit(1)
|
|
266
|
+
exit_with_error(f"Error parsing JSON string: {e}")
|
|
262
267
|
|
|
263
268
|
if args.input_file:
|
|
264
269
|
if args.input_file == "-":
|
|
@@ -267,18 +272,16 @@ def read_input(args: argparse.Namespace) -> dict[str, Any] | list[Any]:
|
|
|
267
272
|
try:
|
|
268
273
|
return readfromjson(args.input_file)
|
|
269
274
|
except JSONReadError as e:
|
|
270
|
-
|
|
271
|
-
sys.exit(1)
|
|
275
|
+
exit_with_error(f"Error reading JSON file: {e}")
|
|
272
276
|
|
|
273
277
|
# Check if there's data on stdin
|
|
274
278
|
if not sys.stdin.isatty():
|
|
275
279
|
return read_from_stdin()
|
|
276
280
|
|
|
277
|
-
|
|
278
|
-
sys.exit(1)
|
|
281
|
+
exit_with_error("Error: No input provided. Use -h for help.")
|
|
279
282
|
|
|
280
283
|
|
|
281
|
-
def read_from_stdin() ->
|
|
284
|
+
def read_from_stdin() -> JSONValue:
|
|
282
285
|
"""
|
|
283
286
|
Read JSON from standard input.
|
|
284
287
|
|
|
@@ -291,12 +294,10 @@ def read_from_stdin() -> dict[str, Any] | list[Any]:
|
|
|
291
294
|
try:
|
|
292
295
|
json_str = sys.stdin.read().strip()
|
|
293
296
|
if not json_str:
|
|
294
|
-
|
|
295
|
-
sys.exit(1)
|
|
297
|
+
exit_with_error("Error: Empty input")
|
|
296
298
|
return readfromstring(json_str)
|
|
297
299
|
except StringReadError as e:
|
|
298
|
-
|
|
299
|
-
sys.exit(1)
|
|
300
|
+
exit_with_error(f"Error parsing JSON from stdin: {e}")
|
|
300
301
|
|
|
301
302
|
|
|
302
303
|
def write_output(output: str | bytes, output_file: str | None) -> None:
|
|
@@ -102,7 +102,7 @@ def get_xml_type(val: Any) -> 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:
|
|
@@ -345,13 +337,20 @@ def dict2xml_str(
|
|
|
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
|
)
|
|
@@ -5,6 +5,7 @@ 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
|
|
|
@@ -15,7 +16,7 @@ class Json2xml:
|
|
|
15
16
|
"""
|
|
16
17
|
def __init__(
|
|
17
18
|
self,
|
|
18
|
-
data:
|
|
19
|
+
data: JSONValue = None,
|
|
19
20
|
wrapper: str = "all",
|
|
20
21
|
root: bool = True,
|
|
21
22
|
pretty: bool = True,
|
|
@@ -41,7 +42,7 @@ class Json2xml:
|
|
|
41
42
|
"""
|
|
42
43
|
Convert to xml using dicttoxml.dicttoxml and then pretty print it.
|
|
43
44
|
"""
|
|
44
|
-
if self.data:
|
|
45
|
+
if self.data is not None:
|
|
45
46
|
xml_data = dicttoxml.dicttoxml(
|
|
46
47
|
self.data,
|
|
47
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"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Utility methods for reading JSON data from various sources."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
import urllib3
|
|
7
|
+
|
|
8
|
+
from .types import JSONValue
|
|
9
|
+
|
|
10
|
+
DEFAULT_URL_TIMEOUT = urllib3.Timeout(connect=5.0, read=30.0)
|
|
11
|
+
_HTTP = urllib3.PoolManager()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class JSONReadError(Exception):
|
|
15
|
+
"""Raised when there is an error reading JSON data."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InvalidDataError(Exception):
|
|
20
|
+
"""Raised when the data is invalid."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class URLReadError(Exception):
|
|
25
|
+
"""Raised when there is an error reading from a URL."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class StringReadError(Exception):
|
|
30
|
+
"""Raised when there is an error reading from a string."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# @lat: [[behavior#Input readers]]
|
|
35
|
+
def readfromjson(filename: str) -> JSONValue:
|
|
36
|
+
"""Read JSON data from a file."""
|
|
37
|
+
try:
|
|
38
|
+
with open(filename, encoding="utf-8") as jsondata:
|
|
39
|
+
return json.load(jsondata)
|
|
40
|
+
except (ValueError, OSError) as error:
|
|
41
|
+
raise JSONReadError("Invalid JSON File") from error
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def readfromurl(url: str, params: dict[str, str] | None = None) -> JSONValue:
|
|
45
|
+
"""Load JSON data from a URL."""
|
|
46
|
+
try:
|
|
47
|
+
response = _HTTP.request(
|
|
48
|
+
"GET",
|
|
49
|
+
url,
|
|
50
|
+
fields=params,
|
|
51
|
+
timeout=DEFAULT_URL_TIMEOUT,
|
|
52
|
+
retries=False,
|
|
53
|
+
)
|
|
54
|
+
except urllib3.exceptions.HTTPError as error:
|
|
55
|
+
raise URLReadError("URL could not be read") from error
|
|
56
|
+
|
|
57
|
+
if response.status != 200:
|
|
58
|
+
raise URLReadError("URL is not returning correct response")
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
return json.loads(response.data.decode("utf-8"))
|
|
62
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
63
|
+
raise URLReadError("URL did not return valid JSON") from error
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def readfromstring(jsondata: object) -> JSONValue:
|
|
67
|
+
"""Load JSON data from a string."""
|
|
68
|
+
if not isinstance(jsondata, str):
|
|
69
|
+
raise StringReadError("Input is not a proper JSON string")
|
|
70
|
+
try:
|
|
71
|
+
return json.loads(jsondata)
|
|
72
|
+
except ValueError as error:
|
|
73
|
+
raise StringReadError("Input is not a proper JSON string") from error
|