diff --git a/.coverage b/.coverage
index cad90cb..77c430a 100644
Binary files a/.coverage and b/.coverage differ
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index c202860..0272a24 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -152,4 +152,3 @@ jobs:
- name: Run mypy
run: mypy json2xml tests
-
diff --git a/HISTORY.rst b/HISTORY.rst
index 3f689cb..48ff349 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -450,4 +450,3 @@ v3.0.0 / 2019-02-26
* Cleanup Readme.md
* Update issue templates
* fix vulnerabilities in requests
-
diff --git a/Makefile b/Makefile
index 2834c67..106bd43 100644
--- a/Makefile
+++ b/Makefile
@@ -95,4 +95,3 @@ dist: clean ## builds source and wheel package
install: clean ## install the package to the active Python's site-packages
python setup.py install
-
diff --git a/README.rst b/README.rst
index f2bcc0e..ebe87d7 100644
--- a/README.rst
+++ b/README.rst
@@ -139,7 +139,9 @@ However, you can change this behavior using the item_wrap property like this:
from json2xml import json2xml
from json2xml.utils import readfromurl, readfromstring, readfromjson
- data = readfromstring('{"my_items":[{"my_item":{"id":1} },{"my_item":{"id":2} }],"my_str_items":["a","b"]}')
+ data = readfromstring(
+ '{"my_items":[{"my_item":{"id":1} },{"my_item":{"id":2} }],"my_str_items":["a","b"]}'
+ )
print(json2xml.Json2xml(data, item_wrap=False).to_xml())
Outputs this:
@@ -201,7 +203,7 @@ This project uses modern Python development practices. Here's how to set up a de
# Create and activate virtual environment (using uv - recommended)
uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
-
+
# Install dependencies
uv pip install -r requirements-dev.txt
uv pip install -e .
@@ -241,4 +243,3 @@ Help and Support to maintain this project
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- You can sponsor my work for this plugin here: https://github.com/sponsors/vinitkumar/
-
diff --git a/dev.py b/dev.py
index cf89b93..0744f49 100644
--- a/dev.py
+++ b/dev.py
@@ -31,10 +31,18 @@ def main() -> None:
success &= run_command(["ruff", "check", "json2xml", "tests"], "Linting")
if command in ("test", "all"):
- success &= run_command([
- "pytest", "--cov=json2xml", "--cov-report=term",
- "-xvs", "tests", "-n", "auto"
- ], "Tests")
+ success &= run_command(
+ [
+ "pytest",
+ "--cov=json2xml",
+ "--cov-report=term",
+ "-xvs",
+ "tests",
+ "-n",
+ "auto",
+ ],
+ "Tests",
+ )
if command in ("typecheck", "all"):
success &= run_command(["mypy", "json2xml", "tests"], "Type checking")
diff --git a/docs/conf.py b/docs/conf.py
index 139fc40..b87e9f7 100755
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -21,7 +21,7 @@
import os
import sys
-sys.path.insert(0, os.path.abspath('..'))
+sys.path.insert(0, os.path.abspath(".."))
import json2xml
@@ -35,23 +35,23 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
+extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"]
# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
-source_suffix = '.rst'
+source_suffix = ".rst"
# The master toctree document.
-master_doc = 'index'
+master_doc = "index"
# General information about the project.
-project = 'json2xml'
-copyright = f'{year}, Vinit Kumar'
+project = "json2xml"
+copyright = f"{year}, Vinit Kumar"
author = "Vinit Kumar"
# The version info for the project you're documenting, acts as replacement
@@ -73,10 +73,10 @@
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = "sphinx"
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
@@ -87,7 +87,7 @@
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
-html_theme = 'furo'
+html_theme = "furo"
# Theme options are theme-specific and customize the look and feel of a
# theme further. For a list of options available for each theme, see the
@@ -98,13 +98,13 @@
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
# -- Options for HTMLHelp output ---------------------------------------
# Output file base name for HTML help builder.
-htmlhelp_basename = 'json2xmldoc'
+htmlhelp_basename = "json2xmldoc"
# -- Options for LaTeX output ------------------------------------------
@@ -113,15 +113,12 @@
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
-
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
-
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
-
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
@@ -131,9 +128,7 @@
# (source start file, target name, title, author, documentclass
# [howto, manual, or own class]).
latex_documents = [
- (master_doc, 'json2xml.tex',
- 'json2xml Documentation',
- 'Vinit Kumar', 'manual'),
+ (master_doc, "json2xml.tex", "json2xml Documentation", "Vinit Kumar", "manual"),
]
@@ -141,11 +136,7 @@
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
-man_pages = [
- (master_doc, 'json2xml',
- 'json2xml Documentation',
- [author], 1)
-]
+man_pages = [(master_doc, "json2xml", "json2xml Documentation", [author], 1)]
# -- Options for Texinfo output ----------------------------------------
@@ -154,13 +145,13 @@
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
- (master_doc, 'json2xml',
- 'json2xml Documentation',
- author,
- 'json2xml',
- 'One line description of project.',
- 'Miscellaneous'),
+ (
+ master_doc,
+ "json2xml",
+ "json2xml Documentation",
+ author,
+ "json2xml",
+ "One line description of project.",
+ "Miscellaneous",
+ ),
]
-
-
-
diff --git a/examples/bigexample.json b/examples/bigexample.json
index 2f39e9d..f05bbf3 100644
--- a/examples/bigexample.json
+++ b/examples/bigexample.json
@@ -102,4 +102,4 @@
"entity_status": "",
"art_unit": ""
}
-]
\ No newline at end of file
+]
diff --git a/examples/wrongjson.json b/examples/wrongjson.json
index 8ac8cf5..87035ea 100644
--- a/examples/wrongjson.json
+++ b/examples/wrongjson.json
@@ -368,5 +368,5 @@
}
}
]
-
-
\ No newline at end of file
+
+
diff --git a/json2xml/__init__.py b/json2xml/__init__.py
index 53e5f5b..edf8979 100644
--- a/json2xml/__init__.py
+++ b/json2xml/__init__.py
@@ -3,4 +3,3 @@
__author__ = """Vinit Kumar"""
__email__ = "mail@vinitkumar.me"
__version__ = "5.2.1"
-
diff --git a/json2xml/dicttoxml.py b/json2xml/dicttoxml.py
index af32da4..2078d7c 100644
--- a/json2xml/dicttoxml.py
+++ b/json2xml/dicttoxml.py
@@ -5,7 +5,8 @@
import numbers
from collections.abc import Callable, Sequence
from random import SystemRandom
-from typing import Any, Union
+from typing import Any, SupportsFloat, Union
+from xml.parsers.expat import ExpatError
from defusedxml.minidom import parseString
@@ -14,6 +15,9 @@
# Set up logging
LOG = logging.getLogger("dicttoxml")
+# Module-level set for true uniqueness tracking
+_used_ids: set[str] = set()
+
def make_id(element: str, start: int = 100000, end: int = 999999) -> str:
"""
@@ -41,16 +45,11 @@ def get_unique_id(element: str) -> str:
Returns:
str: The unique ID.
"""
- ids: list[str] = [] # initialize list of unique ids
this_id = make_id(element)
- dup = True
- while dup:
- if this_id not in ids:
- dup = False
- ids.append(this_id)
- else:
- this_id = make_id(element)
- return ids[-1]
+ while this_id in _used_ids:
+ this_id = make_id(element)
+ _used_ids.add(this_id)
+ return this_id
ELEMENT = Union[
@@ -58,7 +57,7 @@ def get_unique_id(element: str) -> str:
int,
float,
bool,
- numbers.Number,
+ SupportsFloat,
Sequence[Any],
datetime.datetime,
datetime.date,
@@ -77,44 +76,43 @@ def get_xml_type(val: ELEMENT) -> str:
Returns:
str: The XML type.
"""
- if val is not None:
- if type(val).__name__ in ("str", "unicode"):
- return "str"
- if type(val).__name__ in ("int", "long"):
- return "int"
- if type(val).__name__ == "float":
- return "float"
- if type(val).__name__ == "bool":
- return "bool"
- if isinstance(val, numbers.Number):
- return "number"
- if isinstance(val, dict):
- return "dict"
- if isinstance(val, Sequence):
- return "list"
- else:
+ if val is None:
return "null"
+ if isinstance(val, bool): # Check bool before int (bool is subclass of int)
+ return "bool"
+ if isinstance(val, int):
+ return "int"
+ if isinstance(val, float):
+ return "float"
+ if isinstance(val, str):
+ return "str"
+ if isinstance(val, SupportsFloat):
+ return "number"
+ if isinstance(val, dict):
+ return "dict"
+ if isinstance(val, Sequence):
+ return "list"
return type(val).__name__
-def escape_xml(s: str | int | float | numbers.Number) -> str:
+def escape_xml(s: str | int | float | SupportsFloat) -> str:
"""
Escape a string for use in XML.
Args:
- s (str | numbers.Number): The string to escape.
+ s (str | int | float | SupportsFloat): The string to escape.
Returns:
str: The escaped string.
"""
+ s_str = str(s) # Convert to string once
if isinstance(s, str):
- s = str(s) # avoid UnicodeDecodeError
- s = s.replace("&", "&")
- s = s.replace('"', """)
- s = s.replace("'", "'")
- s = s.replace("<", "<")
- s = s.replace(">", ">")
- return str(s)
+ s_str = s_str.replace("&", "&")
+ s_str = s_str.replace('"', """)
+ s_str = s_str.replace("'", "'")
+ s_str = s_str.replace("<", "<")
+ s_str = s_str.replace(">", ">")
+ return s_str
def make_attrstring(attr: dict[str, Any]) -> str:
@@ -145,40 +143,44 @@ def key_is_valid_xml(key: str) -> bool:
try:
parseString(test_xml)
return True
- except Exception: # minidom does not implement exceptions well
+ except (ExpatError, ValueError) as e:
+ LOG.debug(f"Invalid XML name '{key}': {e}")
return False
-def make_valid_xml_name(key: str, attr: dict[str, Any]) -> tuple[str, dict[str, Any]]:
+def make_valid_xml_name(
+ key: str | int, attr: dict[str, Any]
+) -> tuple[str, dict[str, Any]]:
"""Tests an XML name and fixes it if invalid"""
- key = escape_xml(key)
+ key_str = str(key) # Ensure we're working with strings
+ key_str = escape_xml(key_str)
# nothing happens at escape_xml if attr is not a string, we don't
# need to pass it to the method at all.
# attr = escape_xml(attr)
# pass through if key is already valid
- if key_is_valid_xml(key):
- return key, attr
+ if key_is_valid_xml(key_str):
+ return key_str, attr
# prepend a lowercase n if the key is numeric
- if isinstance(key, int) or key.isdigit():
- return f"n{key}", attr
+ if key_str.isdigit():
+ return f"n{key_str}", attr
# replace spaces with underscores if that fixes the problem
- if key_is_valid_xml(key.replace(" ", "_")):
- return key.replace(" ", "_"), attr
+ if key_is_valid_xml(key_str.replace(" ", "_")):
+ return key_str.replace(" ", "_"), attr
# allow namespace prefixes + ignore @flat in key
- if key_is_valid_xml(key.replace(":", "").replace("@flat", "")):
- return key, attr
+ if key_is_valid_xml(key_str.replace(":", "").replace("@flat", "")):
+ return key_str, attr
# key is still invalid - move it into a name attribute
- attr["name"] = key
- key = "key"
- return key, attr
+ attr["name"] = key_str
+ key_str = "key"
+ return key_str, attr
-def wrap_cdata(s: str | int | float | numbers.Number) -> str:
+def wrap_cdata(s: str | int | float | SupportsFloat) -> str:
"""Wraps a string into CDATA sections"""
s = str(s).replace("]]>", "]]]]>")
return ""
@@ -188,6 +190,25 @@ def default_item_func(parent: str) -> str:
return "item"
+def _build_namespace_string(xml_namespaces: dict[str, Any]) -> str:
+ """Build XML namespace string from namespace dictionary."""
+ parts = []
+
+ for prefix, value in xml_namespaces.items():
+ if prefix == "xsi" and isinstance(value, dict):
+ for schema_att, ns in value.items():
+ if schema_att == "schemaInstance":
+ parts.append(f'xmlns:{prefix}="{ns}"')
+ elif schema_att == "schemaLocation":
+ parts.append(f'xsi:{schema_att}="{ns}"')
+ elif prefix == "xmlns":
+ parts.append(f'xmlns="{value}"')
+ else:
+ parts.append(f'xmlns:{prefix}="{value}"')
+
+ return " " + " ".join(parts) if parts else ""
+
+
def convert(
obj: ELEMENT,
ids: Any,
@@ -208,7 +229,7 @@ def convert(
if isinstance(obj, bool):
return convert_bool(key=item_name, val=obj, attr_type=attr_type, cdata=cdata)
- if isinstance(obj, numbers.Number):
+ if isinstance(obj, SupportsFloat):
return convert_kv(
key=item_name, val=obj, attr_type=attr_type, attr={}, cdata=cdata
)
@@ -233,10 +254,28 @@ def convert(
return convert_none(key=item_name, attr_type=attr_type, cdata=cdata)
if isinstance(obj, dict):
- return convert_dict(obj, ids, parent, attr_type, item_func, cdata, item_wrap, list_headers=list_headers)
+ return convert_dict(
+ obj,
+ ids,
+ parent,
+ attr_type,
+ item_func,
+ cdata,
+ item_wrap,
+ list_headers=list_headers,
+ )
if isinstance(obj, Sequence):
- return convert_list(obj, ids, parent, attr_type, item_func, cdata, item_wrap, list_headers=list_headers)
+ return convert_list(
+ obj,
+ ids,
+ parent,
+ attr_type,
+ item_func,
+ cdata,
+ item_wrap,
+ list_headers=list_headers,
+ )
raise TypeError(f"Unsupported data type: {obj} ({type(obj).__name__})")
@@ -262,12 +301,13 @@ def dict2xml_str(
parse dict2xml
"""
ids: list[str] = [] # initialize list of unique ids
- ", ".join(str(key) for key in item)
subtree = "" # Initialize subtree with default empty string
if attr_type:
attr["type"] = get_xml_type(item)
- val_attr: dict[str, str] = item.pop("@attrs", attr) # update attr with custom @attr if exists
+ val_attr: dict[str, str] = item.pop(
+ "@attrs", attr
+ ) # update attr with custom @attr if exists
rawitem = item["@val"] if "@val" in item else item
if is_primitive_type(rawitem):
if isinstance(rawitem, dict):
@@ -277,7 +317,14 @@ def dict2xml_str(
else:
# we can not use convert_dict, because rawitem could be non-dict
subtree = convert(
- rawitem, ids, attr_type, item_func, cdata, item_wrap, item_name, list_headers=list_headers
+ rawitem,
+ ids,
+ attr_type,
+ item_func,
+ cdata,
+ item_wrap,
+ item_name,
+ list_headers=list_headers,
)
if parentIsList and list_headers:
@@ -319,7 +366,7 @@ def list2xml_str(
item_func=item_func,
cdata=cdata,
item_wrap=item_wrap,
- list_headers=list_headers
+ list_headers=list_headers,
)
if flat or (len(item) > 0 and is_primitive_type(item[0]) and not item_wrap):
return subtree
@@ -337,7 +384,7 @@ def convert_dict(
item_func: Callable[[str], str],
cdata: bool,
item_wrap: bool,
- list_headers: bool = False
+ list_headers: bool = False,
) -> str:
"""Converts a dict into an XML string."""
output: list[str] = []
@@ -355,7 +402,7 @@ def convert_dict(
if isinstance(val, bool):
addline(convert_bool(key, val, attr_type, attr, cdata))
- elif isinstance(val, (numbers.Number, str)):
+ elif isinstance(val, (SupportsFloat, str)):
addline(
convert_kv(
key=key, val=val, attr_type=attr_type, attr=attr, cdata=cdata
@@ -376,9 +423,15 @@ def convert_dict(
elif isinstance(val, dict):
addline(
dict2xml_str(
- attr_type, attr, val, item_func, cdata, key, item_wrap,
+ attr_type,
+ attr,
+ val,
+ item_func,
+ cdata,
+ key,
+ item_wrap,
False,
- list_headers=list_headers
+ list_headers=list_headers,
)
)
@@ -392,7 +445,7 @@ def convert_dict(
cdata=cdata,
item_name=key,
item_wrap=item_wrap,
- list_headers=list_headers
+ list_headers=list_headers,
)
)
@@ -432,7 +485,7 @@ def convert_list(
if isinstance(item, bool):
addline(convert_bool(item_name, item, attr_type, attr, cdata))
- elif isinstance(item, (numbers.Number, str)):
+ elif isinstance(item, (SupportsFloat, str)):
if item_wrap:
addline(
convert_kv(
@@ -477,7 +530,7 @@ def convert_list(
item_wrap=item_wrap,
parentIsList=True,
parent=parent,
- list_headers=list_headers
+ list_headers=list_headers,
)
)
@@ -491,7 +544,7 @@ def convert_list(
cdata=cdata,
item_name=item_name,
item_wrap=item_wrap,
- list_headers=list_headers
+ list_headers=list_headers,
)
)
@@ -505,7 +558,7 @@ def convert_list(
def convert_kv(
key: str,
- val: str | int | float | numbers.Number | datetime.datetime | datetime.date,
+ val: str | int | float | SupportsFloat | datetime.datetime | datetime.date,
attr_type: bool,
attr: dict[str, Any] | None = None,
cdata: bool = False,
@@ -516,17 +569,25 @@ def convert_kv(
key, attr = make_valid_xml_name(key, attr)
# Convert datetime to isoformat string
- if hasattr(val, "isoformat") and isinstance(val, (datetime.datetime, datetime.date)):
+ if hasattr(val, "isoformat") and isinstance(
+ val, (datetime.datetime, datetime.date)
+ ):
val = val.isoformat()
if attr_type:
attr["type"] = get_xml_type(val)
attr_string = make_attrstring(attr)
- return f"<{key}{attr_string}>{wrap_cdata(val) if cdata else escape_xml(val)}{key}>"
+ return (
+ f"<{key}{attr_string}>{wrap_cdata(val) if cdata else escape_xml(val)}{key}>"
+ )
def convert_bool(
- key: str, val: bool, attr_type: bool, attr: dict[str, Any] | None = None, cdata: bool = False
+ key: str,
+ val: bool,
+ attr_type: bool,
+ attr: dict[str, Any] | None = None,
+ cdata: bool = False,
) -> str:
"""Converts a boolean into an XML element"""
if attr is None:
@@ -562,8 +623,8 @@ def dicttoxml(
item_wrap: bool = True,
item_func: Callable[[str], str] = default_item_func,
cdata: bool = False,
- xml_namespaces: dict[str, Any] = {},
- list_headers: bool = False
+ xml_namespaces: dict[str, Any] | None = None,
+ list_headers: bool = False,
) -> bytes:
"""
Converts a python object into XML.
@@ -681,35 +742,36 @@ def dicttoxml(
- 4
- 5
- 6
"""
- output = []
- namespace_str = ""
- for prefix in xml_namespaces:
- if prefix == 'xsi':
- for schema_att in xml_namespaces[prefix]:
- if schema_att == 'schemaInstance':
- ns = xml_namespaces[prefix]['schemaInstance']
- namespace_str += f' xmlns:{prefix}="{ns}"'
- elif schema_att == 'schemaLocation':
- ns = xml_namespaces[prefix][schema_att]
- namespace_str += f' xsi:{schema_att}="{ns}"'
-
- elif prefix == 'xmlns':
- # xmns needs no prefix
- ns = xml_namespaces[prefix]
- namespace_str += f' xmlns="{ns}"'
+ if xml_namespaces is None:
+ xml_namespaces = {}
- else:
- ns = xml_namespaces[prefix]
- namespace_str += f' xmlns:{prefix}="{ns}"'
+ output = []
+ namespace_str = _build_namespace_string(xml_namespaces)
if root:
output.append('')
output_elem = convert(
- obj, ids, attr_type, item_func, cdata, item_wrap, parent=custom_root, list_headers=list_headers
+ obj,
+ ids,
+ attr_type,
+ item_func,
+ cdata,
+ item_wrap,
+ parent=custom_root,
+ list_headers=list_headers,
)
output.append(f"<{custom_root}{namespace_str}>{output_elem}{custom_root}>")
else:
output.append(
- convert(obj, ids, attr_type, item_func, cdata, item_wrap, parent="", list_headers=list_headers)
+ convert(
+ obj,
+ ids,
+ attr_type,
+ item_func,
+ cdata,
+ item_wrap,
+ parent="",
+ list_headers=list_headers,
+ )
)
return "".join(output).encode("utf-8")
diff --git a/json2xml/json2xml.py b/json2xml/json2xml.py
index f3c7401..aa61486 100644
--- a/json2xml/json2xml.py
+++ b/json2xml/json2xml.py
@@ -1,7 +1,7 @@
-from pyexpat import ExpatError
from typing import Any
from defusedxml.minidom import parseString
+from pyexpat import ExpatError
from json2xml import dicttoxml
@@ -12,6 +12,7 @@ class Json2xml:
"""
Wrapper class to convert the data to xml
"""
+
def __init__(
self,
data: dict[str, Any] | None = None,
@@ -42,7 +43,9 @@ def to_xml(self) -> Any | None:
)
if self.pretty:
try:
- result = parseString(xml_data).toprettyxml(encoding="UTF-8").decode()
+ result = (
+ parseString(xml_data).toprettyxml(encoding="UTF-8").decode()
+ )
except ExpatError:
raise InvalidDataError
return result
diff --git a/json2xml/utils.py b/json2xml/utils.py
index 85954a4..a953f74 100644
--- a/json2xml/utils.py
+++ b/json2xml/utils.py
@@ -1,4 +1,5 @@
"""Utility methods for converting XML data to dictionary from various sources."""
+
import json
import urllib3
@@ -6,21 +7,25 @@
class JSONReadError(Exception):
"""Raised when there is an error reading JSON data."""
+
pass
class InvalidDataError(Exception):
"""Raised when the data is invalid."""
+
pass
class URLReadError(Exception):
"""Raised when there is an error reading from a URL."""
+
pass
class StringReadError(Exception):
"""Raised when there is an error reading from a string."""
+
pass
@@ -40,7 +45,7 @@ def readfromurl(url: str, params: dict[str, str] | None = None) -> dict[str, str
http = urllib3.PoolManager()
response = http.request("GET", url, fields=params)
if response.status == 200:
- return json.loads(response.data.decode('utf-8'))
+ return json.loads(response.data.decode("utf-8"))
raise URLReadError("URL is not returning correct response")
diff --git a/pyproject.toml b/pyproject.toml
index 19ab880..d8d1627 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,74 +1,79 @@
[build-system]
-requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
+requires = [ "setuptools>=42", "wheel" ]
+
[project]
name = "json2xml"
-version = "5.2.1" # Replace with the dynamic version if needed
+version = "5.2.1" # Replace with the dynamic version if needed
description = "Simple Python Library to convert JSON to XML"
readme = "README.rst"
-requires-python = ">=3.10"
+keywords = [ "json2xml" ]
license = { text = "Apache Software License 2.0" }
-keywords = ["json2xml"]
authors = [
- { name = "Vinit Kumar", email = "mail@vinitkumar.me" }
+ { name = "Vinit Kumar", email = "mail@vinitkumar.me" },
]
+requires-python = ">=3.10"
classifiers = [
- "Development Status :: 6 - Mature",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: Apache Software License",
- "Natural Language :: English",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Programming Language :: Python :: 3.12",
- "Programming Language :: Python :: 3.13",
- "Programming Language :: Python :: 3.14",
- "Programming Language :: Python :: Implementation :: CPython",
- "Programming Language :: Python :: Implementation :: PyPy",
- "Topic :: Software Development :: Libraries :: Python Modules"
+ "Development Status :: 6 - Mature",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "Natural Language :: English",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ "Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
- "defusedxml",
- "urllib3",
- "xmltodict>=0.12.0",
- "pytest",
- "pytest-cov",
- "coverage",
- "setuptools",
+ "coverage",
+ "defusedxml",
+ "pytest",
+ "pytest-cov",
+ "setuptools",
+ "urllib3",
+ "xmltodict>=0.12",
]
-[project.urls]
-Homepage = "https://github.com/vinitkumar/json2xml"
-
-[tool.setuptools.packages.find]
-include = ["json2xml"]
+optional-dependencies.test = [
+ "pytest==7.0.1",
+]
+urls.Homepage = "https://github.com/vinitkumar/json2xml"
-[project.optional-dependencies]
-test = [
- "pytest==7.0.1",
+[dependency-groups]
+dev = [
+ "mypy>=1.17.1",
+ "pre-commit>=4.3",
]
-[tool.pytest.ini_options]
-testpaths = ["tests"]
-python_files = ["test_*.py"]
-xvs = true
-addopts = "--cov=json2xml --cov-report=xml:coverage/reports/coverage.xml --cov-report=term"
+[tool.setuptools.packages.find]
+include = [ "json2xml" ]
+
[tool.ruff]
+line-length = 119
+
exclude = [
- ".env",
- ".venv",
- "**/migrations/**",
-]
-lint.ignore = [
- "E501", # line too long
- "F403", # 'from module import *' used; unable to detect undefined names
- "E701", # multiple statements on one line (colon)
- "F401", # module imported but unused
+ "**/migrations/**",
+ ".env",
+ ".venv",
]
-line-length = 119
lint.select = [
- "I",
- "E",
- "F",
- "W",
+ "E",
+ "F",
+ "I",
+ "W",
]
+lint.ignore = [
+ "E501", # line too long
+ "E701", # multiple statements on one line (colon)
+ "F403", # 'from module import *' used; unable to detect undefined names
+]
+
+[tool.pytest.ini_options]
+testpaths = [ "tests" ]
+python_files = [ "test_*.py" ]
+xvs = true
+addopts = "--cov=json2xml --cov-report=xml:coverage/reports/coverage.xml --cov-report=term"
diff --git a/pytest.ini b/pytest.ini
index 48a7eda..cd6079b 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,4 +1,3 @@
[pytest]
log_cli=True
log_cli_level=INFO
-
diff --git a/requirements.in b/requirements.in
index 9ed5639..eb52d54 100644
--- a/requirements.in
+++ b/requirements.in
@@ -1,3 +1,2 @@
defusedxml==0.7.1
urllib3==2.5.0
-
diff --git a/setup.py b/setup.py
index 58464f5..f00661b 100644
--- a/setup.py
+++ b/setup.py
@@ -5,4 +5,3 @@
from setuptools import setup
setup()
-
diff --git a/tests/conftest.py b/tests/conftest.py
index eedcc47..54b9c7b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,4 +1,5 @@
"""Pytest configuration for json2xml tests."""
+
from __future__ import annotations
import json
@@ -22,7 +23,7 @@ def sample_json_string() -> str:
@pytest.fixture
-def sample_json_dict() -> Dict[str, Any]:
+def sample_json_dict() -> dict[str, Any]:
"""Return a sample JSON dictionary for testing.
Returns:
@@ -36,7 +37,7 @@ def sample_json_dict() -> Dict[str, Any]:
@pytest.fixture
-def sample_json_list() -> List[Dict[str, Any]]:
+def sample_json_list() -> list[dict[str, Any]]:
"""Return a sample JSON list for testing.
Returns:
diff --git a/tests/test_dict2xml.py b/tests/test_dict2xml.py
index df770a9..ef2588c 100644
--- a/tests/test_dict2xml.py
+++ b/tests/test_dict2xml.py
@@ -1,18 +1,10 @@
import datetime
-import numbers
-from typing import TYPE_CHECKING, Any
+from typing import Any
import pytest
from json2xml import dicttoxml
-if TYPE_CHECKING:
- from _pytest.capture import CaptureFixture
- from _pytest.fixtures import FixtureRequest
- from _pytest.logging import LogCaptureFixture
- from _pytest.monkeypatch import MonkeyPatch
- from pytest_mock.plugin import MockerFixture
-
class TestDict2xml:
"""Test class for dicttoxml functionality."""
@@ -173,36 +165,32 @@ def test_dict2xml_with_ampsersand_and_attrs(self) -> None:
def dict_with_attrs(self) -> dict[str, Any]:
"""Fixture providing a dictionary with attributes for testing."""
return {
- 'transportation-mode': [
- {
- '@attrs': {'xml:lang': 'nl'},
- '@val': 'Fiets'
- },
- {
- '@attrs': {'xml:lang': 'nl'},
- '@val': 'Bus'
- },
- {
- '@attrs': {'xml:lang': 'en'},
- '@val': 'Bike'
- }
+ "transportation-mode": [
+ {"@attrs": {"xml:lang": "nl"}, "@val": "Fiets"},
+ {"@attrs": {"xml:lang": "nl"}, "@val": "Bus"},
+ {"@attrs": {"xml:lang": "en"}, "@val": "Bike"},
]
}
- def test_dict2xml_list_items_with_attrs(self, dict_with_attrs: dict[str, Any]) -> None:
+ def test_dict2xml_list_items_with_attrs(
+ self, dict_with_attrs: dict[str, Any]
+ ) -> None:
"""Test dicttoxml with list items containing attributes."""
- '''With list headers = True
- '''
+ """With list headers = True
+ """
- wanted_xml_result = b'Fiets' \
- b'Bus' \
- b'Bike'
+ wanted_xml_result = (
+ b'Fiets'
+ b'Bus'
+ b'Bike'
+ )
xml_result = dicttoxml.dicttoxml(
dict_with_attrs,
root=False,
attr_type=False,
item_wrap=False,
- list_headers=True)
+ list_headers=True,
+ )
assert xml_result == wanted_xml_result
@@ -409,34 +397,33 @@ def test_convert_datetime(self) -> None:
expected = '2023-02-15T12:30:45'
- assert dicttoxml.convert_kv(
- key='item_name',
- val=dt,
- attr_type=True,
- attr={},
- cdata=False
- ) == expected
+ assert (
+ dicttoxml.convert_kv(
+ key="item_name", val=dt, attr_type=True, attr={}, cdata=False
+ )
+ == expected
+ )
# write test for bool test
def test_basic_conversion(self) -> None:
"""Test basic boolean conversion."""
- xml = dicttoxml.convert_bool('key', True, False)
- assert xml == 'true'
+ xml = dicttoxml.convert_bool("key", True, False)
+ assert xml == "true"
def test_with_type_attribute(self) -> None:
"""Test boolean conversion with type attribute."""
- xml = dicttoxml.convert_bool('key', False, True)
+ xml = dicttoxml.convert_bool("key", False, True)
assert xml == 'false'
def test_with_custom_attributes(self) -> None:
"""Test boolean conversion with custom attributes."""
- xml = dicttoxml.convert_bool('key', True, False, {'id': '1'})
+ xml = dicttoxml.convert_bool("key", True, False, {"id": "1"})
assert xml == 'true'
def test_valid_key(self) -> None:
"""Test convert_bool with valid key."""
- xml = dicttoxml.convert_bool('valid_key', False, attr_type=False)
- assert xml == 'false'
+ xml = dicttoxml.convert_bool("valid_key", False, attr_type=False)
+ assert xml == "false"
def test_convert_kv_with_cdata(self) -> None:
"""Test convert_kv with CDATA wrapping."""
@@ -465,7 +452,6 @@ def test_convert_none_with_attr_type(self) -> None:
result = dicttoxml.convert_none("key", attr_type=True)
assert result == ''
-
def test_make_valid_xml_name_with_numeric_key(self) -> None:
"""Test make_valid_xml_name with numeric key."""
key, attr = dicttoxml.make_valid_xml_name("123", {})
@@ -474,8 +460,13 @@ def test_make_valid_xml_name_with_numeric_key(self) -> None:
def test_escape_xml_with_special_chars(self) -> None:
"""Test escape_xml with special characters."""
- result = dicttoxml.escape_xml('This & that < those > these "quotes" \'single quotes\'')
- assert result == "This & that < those > these "quotes" 'single quotes'"
+ result = dicttoxml.escape_xml(
+ "This & that < those > these \"quotes\" 'single quotes'"
+ )
+ assert (
+ result
+ == "This & that < those > these "quotes" 'single quotes'"
+ )
def test_get_xml_type_with_sequence(self) -> None:
"""Test get_xml_type with sequence."""
@@ -516,13 +507,19 @@ def test_list_to_xml_with_dict_items(self) -> None:
"""Test list to XML with dictionary items."""
data = {"items": [{"key1": "value1"}, {"key2": "value2"}]}
result = dicttoxml.dicttoxml(data, root=False, attr_type=False, item_wrap=True)
- assert result == b"- value1
- value2
"
+ assert (
+ result
+ == b"- value1
- value2
"
+ )
def test_list_to_xml_with_mixed_items(self) -> None:
"""Test list to XML with mixed item types."""
data = {"items": [1, "string", {"key": "value"}]}
result = dicttoxml.dicttoxml(data, root=False, attr_type=False, item_wrap=True)
- assert result == b"- 1
- string
- value
"
+ assert (
+ result
+ == b"- 1
- string
- value
"
+ )
def test_list_to_xml_with_empty_list(self) -> None:
"""Test list to XML with empty list."""
@@ -534,7 +531,10 @@ def test_list_to_xml_with_special_characters(self) -> None:
"""Test list to XML with special characters."""
data = {"items": ["", "&", '"quote"', "'single quote'"]}
result = dicttoxml.dicttoxml(data, root=False, attr_type=False, item_wrap=True)
- assert result == b"- <tag>
- &
- "quote"
- 'single quote'
"
+ assert (
+ result
+ == b"- <tag>
- &
- "quote"
- 'single quote'
"
+ )
def test_datetime_conversion_with_isoformat(self) -> None:
"""Test datetime conversion with isoformat."""
@@ -574,6 +574,7 @@ def test_date_conversion_with_custom_attributes(self) -> None:
def test_get_xml_type_unsupported(self) -> None:
"""Test get_xml_type with unsupported type."""
+
class CustomClass:
pass
@@ -592,6 +593,7 @@ def test_make_valid_xml_name_invalid_chars(self) -> None:
def test_dict2xml_str_invalid_type(self) -> None:
"""Test dict2xml_str with invalid type."""
+
class CustomClass:
pass
@@ -605,11 +607,12 @@ class CustomClass:
cdata=False,
item_name="test",
item_wrap=False,
- parentIsList=False
+ parentIsList=False,
)
def test_convert_dict_invalid_type(self) -> None:
"""Test convert_dict with invalid type."""
+
class CustomClass:
pass
@@ -622,11 +625,12 @@ class CustomClass:
attr_type=False,
item_func=lambda x: "item",
cdata=False,
- item_wrap=False
+ item_wrap=False,
)
def test_convert_list_invalid_type(self) -> None:
"""Test convert_list with invalid type."""
+
class CustomClass:
pass
@@ -639,7 +643,7 @@ class CustomClass:
attr_type=False,
item_func=lambda x: "item",
cdata=False,
- item_wrap=False
+ item_wrap=False,
)
def test_convert_list_with_none(self) -> None:
@@ -652,7 +656,7 @@ def test_convert_list_with_none(self) -> None:
attr_type=True,
item_func=lambda x: "item",
cdata=False,
- item_wrap=True
+ item_wrap=True,
)
assert result == ' '
@@ -666,13 +670,14 @@ def test_convert_list_with_custom_ids(self) -> None:
attr_type=False,
item_func=lambda x: "item",
cdata=False,
- item_wrap=True
+ item_wrap=True,
)
assert 'id="root_' in result
- assert '>test<' in result
+ assert ">test<" in result
def test_convert_list_mixed_types(self) -> None:
"""Test convert_list with a mix of valid and invalid types."""
+
class CustomClass:
pass
@@ -685,7 +690,7 @@ class CustomClass:
attr_type=False,
item_func=lambda x: "item",
cdata=False,
- item_wrap=False
+ item_wrap=False,
)
# Additional tests for better coverage
@@ -708,7 +713,7 @@ def test_convert_with_sequence_input(self) -> None:
attr_type=False,
item_func=lambda x: "item",
cdata=False,
- item_wrap=True
+ item_wrap=True,
)
assert "- 1
- 2
- 3
" == result
@@ -757,7 +762,7 @@ def test_convert_with_float(self) -> None:
attr_type=True,
item_func=lambda x: "item",
cdata=False,
- item_wrap=True
+ item_wrap=True,
)
assert result == '- 3.14
'
@@ -827,7 +832,7 @@ def test_convert_with_bool_direct(self) -> None:
attr_type=False,
item_func=lambda x: "item",
cdata=False,
- item_wrap=True
+ item_wrap=True,
)
assert result == "- true
"
@@ -839,7 +844,7 @@ def test_convert_with_string_direct(self) -> None:
attr_type=False,
item_func=lambda x: "item",
cdata=False,
- item_wrap=True
+ item_wrap=True,
)
assert result == "- test_string
"
@@ -852,7 +857,7 @@ def test_convert_with_datetime_direct(self) -> None:
attr_type=False,
item_func=lambda x: "item",
cdata=False,
- item_wrap=True
+ item_wrap=True,
)
assert result == "- 2023-02-15T12:30:45
"
@@ -864,12 +869,13 @@ def test_convert_with_none_direct(self) -> None:
attr_type=False,
item_func=lambda x: "item",
cdata=False,
- item_wrap=True
+ item_wrap=True,
)
assert result == " "
def test_convert_unsupported_type_direct(self) -> None:
"""Test convert function with unsupported type."""
+
class CustomClass:
pass
@@ -880,7 +886,7 @@ class CustomClass:
attr_type=False,
item_func=lambda x: "item",
cdata=False,
- item_wrap=True
+ item_wrap=True,
)
def test_dict2xml_str_with_attr_type(self) -> None:
@@ -894,7 +900,7 @@ def test_dict2xml_str_with_attr_type(self) -> None:
cdata=False,
item_name="test",
item_wrap=False,
- parentIsList=False
+ parentIsList=False,
)
assert 'type="dict"' in result
@@ -909,7 +915,7 @@ def test_dict2xml_str_with_primitive_dict(self) -> None:
cdata=False,
item_name="test",
item_wrap=False,
- parentIsList=False
+ parentIsList=False,
)
assert "nested" in result
@@ -923,7 +929,7 @@ def test_list2xml_str_with_attr_type(self) -> None:
item_func=lambda x: "item",
cdata=False,
item_name="test",
- item_wrap=True
+ item_wrap=True,
)
assert 'type="list"' in result
@@ -937,7 +943,7 @@ def test_convert_dict_with_bool_value(self) -> None:
attr_type=False,
item_func=lambda x: "item",
cdata=False,
- item_wrap=False
+ item_wrap=False,
)
assert "true" == result
@@ -951,7 +957,7 @@ def test_convert_dict_with_falsy_value(self) -> None:
attr_type=False,
item_func=lambda x: "item",
cdata=False,
- item_wrap=False
+ item_wrap=False,
)
assert "" == result
@@ -965,7 +971,7 @@ def test_convert_list_with_flat_item_name(self) -> None:
attr_type=False,
item_func=lambda x: x + "@flat",
cdata=False,
- item_wrap=True
+ item_wrap=True,
)
assert "test" == result
@@ -979,7 +985,7 @@ def test_convert_list_with_bool_item(self) -> None:
attr_type=False,
item_func=lambda x: "item",
cdata=False,
- item_wrap=True
+ item_wrap=True,
)
assert "- true
" == result
@@ -994,7 +1000,7 @@ def test_convert_list_with_datetime_item(self) -> None:
attr_type=False,
item_func=lambda x: "item",
cdata=False,
- item_wrap=True
+ item_wrap=True,
)
assert "- 2023-02-15T12:30:45
" == result
@@ -1008,7 +1014,7 @@ def test_convert_list_with_sequence_item(self) -> None:
attr_type=False,
item_func=lambda x: "item",
cdata=False,
- item_wrap=True
+ item_wrap=True,
)
assert "- nested
- list
" == result
@@ -1018,6 +1024,7 @@ def test_dict2xml_str_with_primitive_dict_rawitem(self) -> None:
# This is tricky because normally dicts are not primitive types
# We need to mock is_primitive_type to return True for a dict
import json2xml.dicttoxml as module
+
original_is_primitive = module.is_primitive_type
def mock_is_primitive(val: Any) -> bool:
@@ -1036,7 +1043,7 @@ def mock_is_primitive(val: Any) -> bool:
cdata=False,
item_name="test",
item_wrap=False,
- parentIsList=False
+ parentIsList=False,
)
assert "test" in result
finally:
@@ -1057,7 +1064,7 @@ def test_convert_dict_with_falsy_value_line_400(self) -> None:
attr_type=False,
item_func=lambda x: "item",
cdata=False,
- item_wrap=False
+ item_wrap=False,
)
# None should trigger the "elif not val:" branch and result in an empty element
@@ -1067,33 +1074,36 @@ def test_attrs_xml_escaping(self) -> None:
"""Test that @attrs values are properly XML-escaped."""
# Test the specific case from the user's bug report
info_dict = {
- 'Info': {
- "@attrs": {
- "Name": "systemSpec",
- "HelpText": "spec version "
- }
+ "Info": {
+ "@attrs": {"Name": "systemSpec", "HelpText": "spec version "}
}
}
- result = dicttoxml.dicttoxml(info_dict, attr_type=False, item_wrap=False, root=False).decode('utf-8')
- expected = ''
+ result = dicttoxml.dicttoxml(
+ info_dict, attr_type=False, item_wrap=False, root=False
+ ).decode("utf-8")
+ expected = (
+ ''
+ )
assert expected == result
def test_attrs_comprehensive_xml_escaping(self) -> None:
"""Test comprehensive XML escaping in attributes."""
data = {
- 'Element': {
+ "Element": {
"@attrs": {
"ampersand": "Tom & Jerry",
"less_than": "value < 10",
"greater_than": "value > 5",
"quotes": 'He said "Hello"',
"single_quotes": "It's working",
- "mixed": "Tom & Jerry < 10 > 5 \"quoted\" 'apostrophe'"
+ "mixed": "Tom & Jerry < 10 > 5 \"quoted\" 'apostrophe'",
},
- "@val": "content"
+ "@val": "content",
}
}
- result = dicttoxml.dicttoxml(data, attr_type=False, item_wrap=False, root=False).decode('utf-8')
+ result = dicttoxml.dicttoxml(
+ data, attr_type=False, item_wrap=False, root=False
+ ).decode("utf-8")
# Check that all special characters are properly escaped in attributes
assert 'ampersand="Tom & Jerry"' in result
@@ -1101,23 +1111,20 @@ def test_attrs_comprehensive_xml_escaping(self) -> None:
assert 'greater_than="value > 5"' in result
assert 'quotes="He said "Hello""' in result
assert 'single_quotes="It's working"' in result
- assert 'mixed="Tom & Jerry < 10 > 5 "quoted" 'apostrophe'"' in result
+ assert (
+ 'mixed="Tom & Jerry < 10 > 5 "quoted" 'apostrophe'"'
+ in result
+ )
# Verify the element content is also properly escaped
assert ">content<" in result
def test_attrs_empty_and_none_values(self) -> None:
"""Test attribute handling with empty and None values."""
- data = {
- 'Element': {
- "@attrs": {
- "empty": "",
- "zero": 0,
- "false": False
- }
- }
- }
- result = dicttoxml.dicttoxml(data, attr_type=False, item_wrap=False, root=False).decode('utf-8')
+ data = {"Element": {"@attrs": {"empty": "", "zero": 0, "false": False}}}
+ result = dicttoxml.dicttoxml(
+ data, attr_type=False, item_wrap=False, root=False
+ ).decode("utf-8")
assert 'empty=""' in result
assert 'zero="0"' in result
@@ -1131,7 +1138,7 @@ def test_make_attrstring_function_directly(self) -> None:
attrs = {
"test": "value ",
"ampersand": "Tom & Jerry",
- "quotes": 'Say "hello"'
+ "quotes": 'Say "hello"',
}
result = make_attrstring(attrs)
@@ -1143,3 +1150,59 @@ def test_make_attrstring_function_directly(self) -> None:
empty_attrs: dict[str, Any] = {}
result = make_attrstring(empty_attrs)
assert result == ""
+
+ def test_get_unique_id_collision_coverage(self) -> None:
+ """Test get_unique_id to cover line 50 - the collision case."""
+ import json2xml.dicttoxml as module
+
+ # Clear the global _used_ids set to start fresh
+ original_used_ids = module._used_ids.copy()
+ module._used_ids.clear()
+
+ # Mock make_id to return the same ID twice, then a different one
+ original_make_id = module.make_id
+ call_count = 0
+
+ def mock_make_id(element: str, start: int = 100000, end: int = 999999) -> str:
+ nonlocal call_count
+ call_count += 1
+ if call_count == 1:
+ return "test_123456" # First call - will be added to _used_ids
+ elif call_count == 2:
+ return "test_123456" # Second call - will collide, triggering line 50
+ else:
+ return "test_789012" # Third call - unique
+
+ module.make_id = mock_make_id
+
+ try:
+ # First call - adds "test_123456" to _used_ids
+ result1 = dicttoxml.get_unique_id("test")
+ assert result1 == "test_123456"
+
+ # Reset call count for second test
+ call_count = 0
+
+ # Second call - should trigger collision and regenerate
+ result2 = dicttoxml.get_unique_id("test")
+ assert result2 == "test_789012"
+ assert call_count == 3 # Should have called make_id 3 times
+ finally:
+ module.make_id = original_make_id
+ module._used_ids.clear()
+ module._used_ids.update(original_used_ids)
+
+ def test_get_xml_type_numbers_number_coverage(self) -> None:
+ """Test get_xml_type to cover line 90 - numbers.Number that's not int/float."""
+ import decimal
+ import fractions
+
+ # Test with Decimal (numbers.Number but not int/float)
+ decimal_val = decimal.Decimal("3.14159")
+ result = dicttoxml.get_xml_type(decimal_val)
+ assert result == "number"
+
+ # Test with Fraction (numbers.Number but not int/float)
+ fraction_val = fractions.Fraction(22, 7)
+ result = dicttoxml.get_xml_type(fraction_val)
+ assert result == "number"
diff --git a/tests/test_json2xml.py b/tests/test_json2xml.py
index f8858da..02c9a18 100644
--- a/tests/test_json2xml.py
+++ b/tests/test_json2xml.py
@@ -2,20 +2,13 @@
"""Tests for `json2xml` package."""
-import json
-from pyexpat import ExpatError
-
import pytest
import xmltodict
+from pyexpat import ExpatError
from json2xml import json2xml
-from json2xml.utils import (
- InvalidDataError,
- JSONReadError,
- StringReadError,
- readfromjson,
- readfromstring,
-)
+from json2xml.utils import (InvalidDataError, JSONReadError, StringReadError,
+ readfromjson, readfromstring)
class TestJson2xml:
@@ -111,8 +104,8 @@ def test_item_wrap(self) -> None:
xmldata = json2xml.Json2xml(data, pretty=False).to_xml()
old_dict = xmltodict.parse(xmldata)
# item must be present within my_items
- assert "item" in old_dict['all']['my_items']
- assert "item" in old_dict['all']['my_str_items']
+ assert "item" in old_dict["all"]["my_items"]
+ assert "item" in old_dict["all"]["my_str_items"]
def test_no_item_wrap(self) -> None:
data = readfromstring(
@@ -121,17 +114,15 @@ def test_no_item_wrap(self) -> None:
xmldata = json2xml.Json2xml(data, pretty=False, item_wrap=False).to_xml()
old_dict = xmltodict.parse(xmldata)
# my_item must be present within my_items
- assert "my_item" in old_dict['all']['my_items']
- assert "my_str_items" in old_dict['all']
+ assert "my_item" in old_dict["all"]["my_items"]
+ assert "my_str_items" in old_dict["all"]
def test_empty_array(self) -> None:
- data = readfromstring(
- '{"empty_list":[]}'
- )
+ data = readfromstring('{"empty_list":[]}')
xmldata = json2xml.Json2xml(data, pretty=False).to_xml()
old_dict = xmltodict.parse(xmldata)
# item empty_list be present within all
- assert "empty_list" in old_dict['all']
+ assert "empty_list" in old_dict["all"]
def test_attrs(self) -> None:
data = readfromstring(
@@ -140,29 +131,36 @@ def test_attrs(self) -> None:
xmldata = json2xml.Json2xml(data, pretty=False).to_xml()
old_dict = xmltodict.parse(xmldata)
# test all attrs
- assert "str" == old_dict['all']['my_string']['@type']
- assert "int" == old_dict['all']['my_int']['@type']
- assert "float" == old_dict['all']['my_float']['@type']
- assert "bool" == old_dict['all']['my_bool']['@type']
- assert "null" == old_dict['all']['my_null']['@type']
- assert "list" == old_dict['all']['empty_list']['@type']
- assert "dict" == old_dict['all']['empty_dict']['@type']
+ assert "str" == old_dict["all"]["my_string"]["@type"]
+ assert "int" == old_dict["all"]["my_int"]["@type"]
+ assert "float" == old_dict["all"]["my_float"]["@type"]
+ assert "bool" == old_dict["all"]["my_bool"]["@type"]
+ assert "null" == old_dict["all"]["my_null"]["@type"]
+ assert "list" == old_dict["all"]["empty_list"]["@type"]
+ assert "dict" == old_dict["all"]["empty_dict"]["@type"]
def test_dicttoxml_bug(self) -> None:
input_dict = {
- 'response': {
- 'results': {
- 'user': [{
- 'name': 'Ezequiel', 'age': '33', 'city': 'San Isidro'
- }, {
- 'name': 'Belén', 'age': '30', 'city': 'San Isidro'}]}}}
+ "response": {
+ "results": {
+ "user": [
+ {"name": "Ezequiel", "age": "33", "city": "San Isidro"},
+ {"name": "Belén", "age": "30", "city": "San Isidro"},
+ ]
+ }
+ }
+ }
xmldata = json2xml.Json2xml(
- input_dict, wrapper='response', pretty=False, attr_type=False, item_wrap=False
+ input_dict,
+ wrapper="response",
+ pretty=False,
+ attr_type=False,
+ item_wrap=False,
).to_xml()
old_dict = xmltodict.parse(xmldata)
- assert 'response' in old_dict.keys()
+ assert "response" in old_dict.keys()
def test_bad_data(self) -> None:
data = b"!\0a8f"
@@ -176,35 +174,42 @@ def test_read_boolean_data_from_json(self) -> None:
data = readfromjson("examples/booleanjson.json")
result = json2xml.Json2xml(data).to_xml()
dict_from_xml = xmltodict.parse(result)
- assert dict_from_xml["all"]["boolean"]["#text"] != 'True'
- assert dict_from_xml["all"]["boolean"]["#text"] == 'true'
- assert dict_from_xml["all"]["boolean_dict_list"]["item"][0]["boolean_dict"]["boolean"]["#text"] == 'true'
- assert dict_from_xml["all"]["boolean_dict_list"]["item"][1]["boolean_dict"]["boolean"]["#text"] == 'false'
- assert dict_from_xml["all"]["boolean_list"]["item"][0]["#text"] == 'true'
- assert dict_from_xml["all"]["boolean_list"]["item"][1]["#text"] == 'false'
+ assert dict_from_xml["all"]["boolean"]["#text"] != "True"
+ assert dict_from_xml["all"]["boolean"]["#text"] == "true"
+ assert (
+ dict_from_xml["all"]["boolean_dict_list"]["item"][0]["boolean_dict"][
+ "boolean"
+ ]["#text"]
+ == "true"
+ )
+ assert (
+ dict_from_xml["all"]["boolean_dict_list"]["item"][1]["boolean_dict"][
+ "boolean"
+ ]["#text"]
+ == "false"
+ )
+ assert dict_from_xml["all"]["boolean_list"]["item"][0]["#text"] == "true"
+ assert dict_from_xml["all"]["boolean_list"]["item"][1]["#text"] == "false"
def test_read_boolean_data_from_json2(self) -> None:
"""Test correct return for boolean types."""
data = readfromjson("examples/booleanjson2.json")
result = json2xml.Json2xml(data).to_xml()
dict_from_xml = xmltodict.parse(result)
- assert dict_from_xml["all"]["boolean_list"]["item"][0]["#text"] != 'True'
- assert dict_from_xml["all"]["boolean_list"]["item"][0]["#text"] == 'true'
- assert dict_from_xml["all"]["boolean_list"]["item"][1]["#text"] == 'false'
- assert dict_from_xml["all"]["number_array"]["item"][0]["#text"] == '1'
- assert dict_from_xml["all"]["number_array"]["item"][1]["#text"] == '2'
- assert dict_from_xml["all"]["number_array"]["item"][2]["#text"] == '3'
- assert dict_from_xml["all"]["string_array"]["item"][0]["#text"] == 'a'
- assert dict_from_xml["all"]["string_array"]["item"][1]["#text"] == 'b'
- assert dict_from_xml["all"]["string_array"]["item"][2]["#text"] == 'c'
+ assert dict_from_xml["all"]["boolean_list"]["item"][0]["#text"] != "True"
+ assert dict_from_xml["all"]["boolean_list"]["item"][0]["#text"] == "true"
+ assert dict_from_xml["all"]["boolean_list"]["item"][1]["#text"] == "false"
+ assert dict_from_xml["all"]["number_array"]["item"][0]["#text"] == "1"
+ assert dict_from_xml["all"]["number_array"]["item"][1]["#text"] == "2"
+ assert dict_from_xml["all"]["number_array"]["item"][2]["#text"] == "3"
+ assert dict_from_xml["all"]["string_array"]["item"][0]["#text"] == "a"
+ assert dict_from_xml["all"]["string_array"]["item"][1]["#text"] == "b"
+ assert dict_from_xml["all"]["string_array"]["item"][2]["#text"] == "c"
def test_dict_attr_crash(self) -> None:
data = {
"product": {
- "@attrs": {
- "attr_name": "attr_value",
- "a": "b"
- },
+ "@attrs": {"attr_name": "attr_value", "a": "b"},
"@val": [],
},
}
diff --git a/tests/test_utils.py b/tests/test_utils.py
index e98db99..eb9c1ad 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,27 +1,14 @@
"""Test module for json2xml.utils functionality."""
+
import json
import tempfile
-from typing import TYPE_CHECKING
from unittest.mock import Mock, patch
import pytest
-from json2xml.utils import (
- InvalidDataError,
- JSONReadError,
- StringReadError,
- URLReadError,
- readfromjson,
- readfromstring,
- readfromurl,
-)
-
-if TYPE_CHECKING:
- from _pytest.capture import CaptureFixture
- from _pytest.fixtures import FixtureRequest
- from _pytest.logging import LogCaptureFixture
- from _pytest.monkeypatch import MonkeyPatch
- from pytest_mock.plugin import MockerFixture
+from json2xml.utils import (InvalidDataError, JSONReadError, StringReadError,
+ URLReadError, readfromjson, readfromstring,
+ readfromurl)
class TestExceptions:
@@ -59,7 +46,7 @@ def test_readfromjson_valid_file(self) -> None:
"""Test reading a valid JSON file."""
test_data = {"key": "value", "number": 42}
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
json.dump(test_data, f)
temp_filename = f.name
@@ -68,11 +55,12 @@ def test_readfromjson_valid_file(self) -> None:
assert result == test_data
finally:
import os
+
os.unlink(temp_filename)
def test_readfromjson_invalid_json_content(self) -> None:
"""Test reading a file with invalid JSON content."""
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
f.write('{"invalid": json content}') # Invalid JSON
temp_filename = f.name
@@ -81,6 +69,7 @@ def test_readfromjson_invalid_json_content(self) -> None:
readfromjson(temp_filename)
finally:
import os
+
os.unlink(temp_filename)
def test_readfromjson_file_not_found(self) -> None:
@@ -88,7 +77,7 @@ def test_readfromjson_file_not_found(self) -> None:
with pytest.raises(JSONReadError, match="Invalid JSON File"):
readfromjson("non_existent_file.json")
- @patch('builtins.open')
+ @patch("builtins.open")
def test_readfromjson_permission_error(self, mock_open: Mock) -> None:
"""Test reading a file with permission issues."""
# Mock open to raise PermissionError
@@ -97,7 +86,7 @@ def test_readfromjson_permission_error(self, mock_open: Mock) -> None:
with pytest.raises(JSONReadError, match="Invalid JSON File"):
readfromjson("some_file.json")
- @patch('builtins.open')
+ @patch("builtins.open")
def test_readfromjson_os_error(self, mock_open: Mock) -> None:
"""Test reading a file with OS error."""
# Mock open to raise OSError (covers line 34-35 in utils.py)
@@ -110,7 +99,7 @@ def test_readfromjson_os_error(self, mock_open: Mock) -> None:
class TestReadFromUrl:
"""Test readfromurl function."""
- @patch('json2xml.utils.urllib3.PoolManager')
+ @patch("json2xml.utils.urllib3.PoolManager")
def test_readfromurl_success(self, mock_pool_manager: Mock) -> None:
"""Test successful URL reading."""
# Mock response
@@ -127,9 +116,11 @@ def test_readfromurl_success(self, mock_pool_manager: Mock) -> None:
assert result == {"key": "value", "number": 42}
mock_pool_manager.assert_called_once()
- mock_http.request.assert_called_once_with("GET", "http://example.com/data.json", fields=None)
+ mock_http.request.assert_called_once_with(
+ "GET", "http://example.com/data.json", fields=None
+ )
- @patch('json2xml.utils.urllib3.PoolManager')
+ @patch("json2xml.utils.urllib3.PoolManager")
def test_readfromurl_success_with_params(self, mock_pool_manager: Mock) -> None:
"""Test successful URL reading with parameters."""
# Mock response
@@ -146,9 +137,11 @@ def test_readfromurl_success_with_params(self, mock_pool_manager: Mock) -> None:
result = readfromurl("http://example.com/api", params=params)
assert result == {"result": "success"}
- mock_http.request.assert_called_once_with("GET", "http://example.com/api", fields=params)
+ mock_http.request.assert_called_once_with(
+ "GET", "http://example.com/api", fields=params
+ )
- @patch('json2xml.utils.urllib3.PoolManager')
+ @patch("json2xml.utils.urllib3.PoolManager")
def test_readfromurl_http_error(self, mock_pool_manager: Mock) -> None:
"""Test URL reading with HTTP error status."""
# Mock response with error status
@@ -163,7 +156,7 @@ def test_readfromurl_http_error(self, mock_pool_manager: Mock) -> None:
with pytest.raises(URLReadError, match="URL is not returning correct response"):
readfromurl("http://example.com/nonexistent.json")
- @patch('json2xml.utils.urllib3.PoolManager')
+ @patch("json2xml.utils.urllib3.PoolManager")
def test_readfromurl_server_error(self, mock_pool_manager: Mock) -> None:
"""Test URL reading with server error status."""
# Mock response with server error status
@@ -178,13 +171,13 @@ def test_readfromurl_server_error(self, mock_pool_manager: Mock) -> None:
with pytest.raises(URLReadError, match="URL is not returning correct response"):
readfromurl("http://example.com/error.json")
- @patch('json2xml.utils.urllib3.PoolManager')
+ @patch("json2xml.utils.urllib3.PoolManager")
def test_readfromurl_invalid_json_response(self, mock_pool_manager: Mock) -> None:
"""Test URL reading with invalid JSON response."""
# Mock response with invalid JSON
mock_response = Mock()
mock_response.status = 200
- mock_response.data = b'invalid json content'
+ mock_response.data = b"invalid json content"
# Mock PoolManager
mock_http = Mock()
@@ -206,7 +199,7 @@ def test_readfromstring_valid_json(self) -> None:
def test_readfromstring_empty_object(self) -> None:
"""Test reading empty JSON object."""
- json_string = '{}'
+ json_string = "{}"
result = readfromstring(json_string)
assert result == {}
@@ -215,11 +208,8 @@ def test_readfromstring_complex_object(self) -> None:
json_string = '{"users": [{"name": "John", "age": 30}, {"name": "Jane", "age": 25}], "total": 2}'
result = readfromstring(json_string)
expected = {
- "users": [
- {"name": "John", "age": 30},
- {"name": "Jane", "age": 25}
- ],
- "total": 2
+ "users": [{"name": "John", "age": 30}, {"name": "Jane", "age": 25}],
+ "total": 2,
}
assert result == expected
@@ -288,7 +278,7 @@ def test_readfromstring_then_convert_to_xml(self) -> None:
assert b"test" in xml_result
assert b"123" in xml_result
- @patch('json2xml.utils.urllib3.PoolManager')
+ @patch("json2xml.utils.urllib3.PoolManager")
def test_readfromurl_then_convert_to_xml(self, mock_pool_manager: Mock) -> None:
"""Test reading from URL and converting to XML."""
from json2xml import dicttoxml
diff --git a/uv.lock b/uv.lock
index 29559dd..7944ca7 100644
--- a/uv.lock
+++ b/uv.lock
@@ -17,6 +17,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152, upload-time = "2025-01-25T11:30:10.164Z" },
]
+[[package]]
+name = "cfgv"
+version = "3.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" },
+]
+
[[package]]
name = "colorama"
version = "0.4.6"
@@ -99,6 +108,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
]
+[[package]]
+name = "distlib"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.19.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" },
+]
+
+[[package]]
+name = "identify"
+version = "2.6.13"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/ca/ffbabe3635bb839aa36b3a893c91a9b0d368cb4d8073e03a12896970af82/identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32", size = 99243, upload-time = "2025-08-09T19:35:00.6Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/ce/461b60a3ee109518c055953729bf9ed089a04db895d47e95444071dcdef2/identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b", size = 99153, upload-time = "2025-08-09T19:34:59.1Z" },
+]
+
[[package]]
name = "iniconfig"
version = "2.0.0"
@@ -127,6 +163,12 @@ test = [
{ name = "pytest" },
]
+[package.dev-dependencies]
+dev = [
+ { name = "mypy" },
+ { name = "pre-commit" },
+]
+
[package.metadata]
requires-dist = [
{ name = "coverage" },
@@ -140,6 +182,75 @@ requires-dist = [
]
provides-extras = ["test"]
+[package.metadata.requires-dev]
+dev = [
+ { name = "mypy", specifier = ">=1.17.1" },
+ { name = "pre-commit", specifier = ">=4.3.0" },
+]
+
+[[package]]
+name = "mypy"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mypy-extensions" },
+ { name = "pathspec" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299, upload-time = "2025-07-31T07:54:06.425Z" },
+ { url = "https://files.pythonhosted.org/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451, upload-time = "2025-07-31T07:53:52.974Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211, upload-time = "2025-07-31T07:53:18.879Z" },
+ { url = "https://files.pythonhosted.org/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687, upload-time = "2025-07-31T07:53:30.544Z" },
+ { url = "https://files.pythonhosted.org/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322, upload-time = "2025-07-31T07:53:50.74Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/87/8e3e9c2c8bd0d7e071a89c71be28ad088aaecbadf0454f46a540bda7bca6/mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b", size = 9507962, upload-time = "2025-07-31T07:53:08.431Z" },
+ { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" },
+ { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" },
+ { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" },
+ { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" },
+ { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" },
+ { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" },
+ { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" },
+ { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" },
+ { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" },
+ { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" },
+ { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" },
+ { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.9.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
+]
+
[[package]]
name = "packaging"
version = "24.2"
@@ -149,6 +260,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" },
]
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" },
+]
+
[[package]]
name = "pluggy"
version = "1.5.0"
@@ -158,6 +287,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" },
]
+[[package]]
+name = "pre-commit"
+version = "4.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cfgv" },
+ { name = "identify" },
+ { name = "nodeenv" },
+ { name = "pyyaml" },
+ { name = "virtualenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" },
+]
+
[[package]]
name = "py"
version = "1.11.0"
@@ -199,6 +344,50 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949, upload-time = "2024-10-29T20:13:33.215Z" },
]
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" },
+ { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" },
+ { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" },
+ { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" },
+ { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" },
+ { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" },
+ { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" },
+ { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
+ { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
+ { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
+ { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
+ { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
+]
+
[[package]]
name = "setuptools"
version = "75.8.0"
@@ -247,6 +436,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
]
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
[[package]]
name = "urllib3"
version = "2.3.0"
@@ -256,6 +454,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369, upload-time = "2024-12-22T07:47:28.074Z" },
]
+[[package]]
+name = "virtualenv"
+version = "20.34.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "distlib" },
+ { name = "filelock" },
+ { name = "platformdirs" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" },
+]
+
[[package]]
name = "xmltodict"
version = "0.14.2"