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.
Files changed (46) hide show
  1. {json2xml-6.0.6 → json2xml-6.0.7}/HISTORY.rst +7 -0
  2. {json2xml-6.0.6 → json2xml-6.0.7}/PKG-INFO +45 -45
  3. {json2xml-6.0.6 → json2xml-6.0.7}/README.rst +44 -44
  4. {json2xml-6.0.6 → json2xml-6.0.7}/json2xml/__init__.py +1 -1
  5. {json2xml-6.0.6 → json2xml-6.0.7}/json2xml/cli.py +16 -15
  6. {json2xml-6.0.6 → json2xml-6.0.7}/json2xml/dicttoxml.py +25 -24
  7. {json2xml-6.0.6 → json2xml-6.0.7}/json2xml/json2xml.py +3 -2
  8. json2xml-6.0.7/json2xml/types.py +8 -0
  9. json2xml-6.0.7/json2xml/utils.py +73 -0
  10. {json2xml-6.0.6 → json2xml-6.0.7}/json2xml.egg-info/PKG-INFO +45 -45
  11. {json2xml-6.0.6 → json2xml-6.0.7}/json2xml.egg-info/SOURCES.txt +1 -0
  12. {json2xml-6.0.6 → json2xml-6.0.7}/pyproject.toml +1 -2
  13. {json2xml-6.0.6 → json2xml-6.0.7}/setup.cfg +1 -1
  14. {json2xml-6.0.6 → json2xml-6.0.7}/tests/test_dict2xml.py +71 -1
  15. {json2xml-6.0.6 → json2xml-6.0.7}/tests/test_json2xml.py +17 -0
  16. {json2xml-6.0.6 → json2xml-6.0.7}/tests/test_rust_dicttoxml.py +21 -0
  17. {json2xml-6.0.6 → json2xml-6.0.7}/tests/test_utils.py +66 -79
  18. json2xml-6.0.6/json2xml/utils.py +0 -55
  19. {json2xml-6.0.6 → json2xml-6.0.7}/AUTHORS.rst +0 -0
  20. {json2xml-6.0.6 → json2xml-6.0.7}/CONTRIBUTING.rst +0 -0
  21. {json2xml-6.0.6 → json2xml-6.0.7}/LICENSE +0 -0
  22. {json2xml-6.0.6 → json2xml-6.0.7}/MANIFEST.in +0 -0
  23. {json2xml-6.0.6 → json2xml-6.0.7}/docs/Makefile +0 -0
  24. {json2xml-6.0.6 → json2xml-6.0.7}/docs/authors.rst +0 -0
  25. {json2xml-6.0.6 → json2xml-6.0.7}/docs/benchmarks.rst +0 -0
  26. {json2xml-6.0.6 → json2xml-6.0.7}/docs/conf.py +0 -0
  27. {json2xml-6.0.6 → json2xml-6.0.7}/docs/contributing.rst +0 -0
  28. {json2xml-6.0.6 → json2xml-6.0.7}/docs/history.rst +0 -0
  29. {json2xml-6.0.6 → json2xml-6.0.7}/docs/index.rst +0 -0
  30. {json2xml-6.0.6 → json2xml-6.0.7}/docs/installation.rst +0 -0
  31. {json2xml-6.0.6 → json2xml-6.0.7}/docs/json2xml.rst +0 -0
  32. {json2xml-6.0.6 → json2xml-6.0.7}/docs/make.bat +0 -0
  33. {json2xml-6.0.6 → json2xml-6.0.7}/docs/modules.rst +0 -0
  34. {json2xml-6.0.6 → json2xml-6.0.7}/docs/readme.rst +0 -0
  35. {json2xml-6.0.6 → json2xml-6.0.7}/docs/usage.rst +0 -0
  36. {json2xml-6.0.6 → json2xml-6.0.7}/json2xml/dicttoxml_fast.py +0 -0
  37. {json2xml-6.0.6 → json2xml-6.0.7}/json2xml.egg-info/dependency_links.txt +0 -0
  38. {json2xml-6.0.6 → json2xml-6.0.7}/json2xml.egg-info/entry_points.txt +0 -0
  39. {json2xml-6.0.6 → json2xml-6.0.7}/json2xml.egg-info/requires.txt +0 -0
  40. {json2xml-6.0.6 → json2xml-6.0.7}/json2xml.egg-info/top_level.txt +0 -0
  41. {json2xml-6.0.6 → json2xml-6.0.7}/requirements.in +0 -0
  42. {json2xml-6.0.6 → json2xml-6.0.7}/setup.py +0 -0
  43. {json2xml-6.0.6 → json2xml-6.0.7}/tests/__init__.py +0 -0
  44. {json2xml-6.0.6 → json2xml-6.0.7}/tests/conftest.py +0 -0
  45. {json2xml-6.0.6 → json2xml-6.0.7}/tests/test_cli.py +0 -0
  46. {json2xml-6.0.6 → json2xml-6.0.7}/tests/test_missing_coverage.py +0 -0
@@ -1,4 +1,11 @@
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
+
2
9
  6.0.6 / 2026-04-08
3
10
  ==================
4
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: json2xml
3
- Version: 6.0.6
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 149x faster)**
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 **75-149x faster** conversion compared to pure Python. It's automatically used when available, with seamless fallback to pure Python.
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 149x faster performance. To develop or build the Rust extension:
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
- - 78µs
468
- - 1.05µs
469
- - **75x**
467
+ - 31.49µs
468
+ - 0.55µs
469
+ - **56.8x**
470
470
  * - **Medium JSON** (3.2 KB)
471
- - 2.15ms
472
- - 15µs
473
- - **139x**
471
+ - 1.69ms
472
+ - 16.15µs
473
+ - **105.0x**
474
474
  * - **Large JSON** (32 KB)
475
- - 22ms
476
- - 151µs
477
- - **146x**
475
+ - 17.97ms
476
+ - 168.21µs
477
+ - **106.8x**
478
478
  * - **Very Large JSON** (323 KB)
479
- - 219ms
480
- - 1.47ms
481
- - **149x**
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, January 2026):
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
- - 40µs
525
- - 1.5µs
526
- - 4.6ms
527
- - 3.7ms
528
- - Rust (28x)
524
+ - 31.49µs
525
+ - 0.55µs
526
+ - 4.09ms
527
+ - 2.02ms
528
+ - Rust (56.8x)
529
529
  * - **Medium** (3.2KB)
530
- - 2.1ms
531
- - 71µs
532
- - 4.1ms
533
- - 3.3ms
534
- - Rust (30x)
530
+ - 1.69ms
531
+ - 16.15µs
532
+ - 4.07ms
533
+ - 2.09ms
534
+ - Rust (105.0x)
535
535
  * - **Large** (32KB)
536
- - 21ms
537
- - 740µs
538
- - 4ms
539
- - 6.1ms
540
- - Rust (28x)
536
+ - 17.97ms
537
+ - 168.21µs
538
+ - 4.10ms
539
+ - 2.42ms
540
+ - Rust (106.8x)
541
541
  * - **Very Large** (323KB)
542
- - 213ms
543
- - 7.5ms
544
- - 4.4ms
545
- - 33ms
546
- - Go (48x)
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**: ~28x faster than Python, zero overhead (best for Python users)
551
- - **Go CLI**: 48x faster for large files (300KB+), but has ~4ms startup overhead
552
- - **Zig CLI**: 3-6x faster for medium-large files
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, 28x faster)
557
- - **Large file CLI processing**: Use `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ (Go, 48x for 300KB+)
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/>`_ - 28x faster, Python extension via PyO3
569
- - **Go**: `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ - 48x faster for large files, native CLI
570
- - **Zig**: `json2xml-zig <https://github.com/vinitkumar/json2xml-zig>`_ - 6x faster, native CLI
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 149x faster)**
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 **75-149x faster** conversion compared to pure Python. It's automatically used when available, with seamless fallback to pure Python.
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 149x faster performance. To develop or build the Rust extension:
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
- - 78µs
432
- - 1.05µs
433
- - **75x**
431
+ - 31.49µs
432
+ - 0.55µs
433
+ - **56.8x**
434
434
  * - **Medium JSON** (3.2 KB)
435
- - 2.15ms
436
- - 15µs
437
- - **139x**
435
+ - 1.69ms
436
+ - 16.15µs
437
+ - **105.0x**
438
438
  * - **Large JSON** (32 KB)
439
- - 22ms
440
- - 151µs
441
- - **146x**
439
+ - 17.97ms
440
+ - 168.21µs
441
+ - **106.8x**
442
442
  * - **Very Large JSON** (323 KB)
443
- - 219ms
444
- - 1.47ms
445
- - **149x**
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, January 2026):
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
- - 40µs
489
- - 1.5µs
490
- - 4.6ms
491
- - 3.7ms
492
- - Rust (28x)
488
+ - 31.49µs
489
+ - 0.55µs
490
+ - 4.09ms
491
+ - 2.02ms
492
+ - Rust (56.8x)
493
493
  * - **Medium** (3.2KB)
494
- - 2.1ms
495
- - 71µs
496
- - 4.1ms
497
- - 3.3ms
498
- - Rust (30x)
494
+ - 1.69ms
495
+ - 16.15µs
496
+ - 4.07ms
497
+ - 2.09ms
498
+ - Rust (105.0x)
499
499
  * - **Large** (32KB)
500
- - 21ms
501
- - 740µs
502
- - 4ms
503
- - 6.1ms
504
- - Rust (28x)
500
+ - 17.97ms
501
+ - 168.21µs
502
+ - 4.10ms
503
+ - 2.42ms
504
+ - Rust (106.8x)
505
505
  * - **Very Large** (323KB)
506
- - 213ms
507
- - 7.5ms
508
- - 4.4ms
509
- - 33ms
510
- - Go (48x)
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**: ~28x faster than Python, zero overhead (best for Python users)
515
- - **Go CLI**: 48x faster for large files (300KB+), but has ~4ms startup overhead
516
- - **Zig CLI**: 3-6x faster for medium-large files
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, 28x faster)
521
- - **Large file CLI processing**: Use `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ (Go, 48x for 300KB+)
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/>`_ - 28x faster, Python extension via PyO3
533
- - **Go**: `json2xml-go <https://github.com/vinitkumar/json2xml-go>`_ - 48x faster for large files, native CLI
534
- - **Zig**: `json2xml-zig <https://github.com/vinitkumar/json2xml-zig>`_ - 6x faster, native CLI
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
@@ -2,4 +2,4 @@
2
2
 
3
3
  __author__ = """Vinit Kumar"""
4
4
  __email__ = "mail@vinitkumar.me"
5
- __version__ = "6.0.6"
5
+ __version__ = "6.0.7"
@@ -43,10 +43,11 @@ from __future__ import annotations
43
43
 
44
44
  import argparse
45
45
  import sys
46
- from typing import Any
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) -> dict[str, Any] | list[Any]:
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
- print(f"Error reading from URL: {e}", file=sys.stderr)
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
- print(f"Error parsing JSON string: {e}", file=sys.stderr)
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
- print(f"Error reading JSON file: {e}", file=sys.stderr)
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
- print("Error: No input provided. Use -h for help.", file=sys.stderr)
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() -> dict[str, Any] | list[Any]:
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
- print("Error: Empty input", file=sys.stderr)
295
- sys.exit(1)
297
+ exit_with_error("Error: Empty input")
296
298
  return readfromstring(json_str)
297
299
  except StringReadError as e:
298
- print(f"Error parsing JSON from stdin: {e}", file=sys.stderr)
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
- """Tests an XML name and fixes it if invalid"""
159
- key = escape_xml(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
- # prepend a lowercase n if the key is numeric
169
- if isinstance(key, int) or key.isdigit():
164
+ if key.isdigit():
170
165
  return f"n{key}", attr
171
166
 
172
- # replace spaces with underscores if that fixes the problem
173
- if key_is_valid_xml(key.replace(" ", "_")):
174
- return key.replace(" ", "_"), attr
167
+ key_with_underscores = key.replace(" ", "_")
168
+ if key_is_valid_xml(key_with_underscores):
169
+ return key_with_underscores, attr
175
170
 
176
- # allow namespace prefixes + ignore @flat in key
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
- key = "key"
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: dict[str, str] = item.pop("@attrs", attr) # update attr with custom @attr if exists
349
- rawitem = item["@val"] if "@val" in item else item
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 isinstance(rawitem, dict):
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(key, attr)
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: dict[str, Any] | list[Any] | None = None,
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