From a5a2d02180b69a1468520d83648c55236a25b19e Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Fri, 29 Aug 2025 13:36:40 +0530 Subject: [PATCH 1/6] fix: cleanup code --- pyproject.toml | 1 - tests/test_dict2xml.py | 7 +------ tests/test_json2xml.py | 1 - tests/test_utils.py | 6 +----- 4 files changed, 2 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 19ab880..f98e6e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,6 @@ 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 ] line-length = 119 lint.select = [ diff --git a/tests/test_dict2xml.py b/tests/test_dict2xml.py index df770a9..47e7cb5 100644 --- a/tests/test_dict2xml.py +++ b/tests/test_dict2xml.py @@ -1,5 +1,4 @@ import datetime -import numbers from typing import TYPE_CHECKING, Any import pytest @@ -7,11 +6,7 @@ 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 + pass class TestDict2xml: diff --git a/tests/test_json2xml.py b/tests/test_json2xml.py index f8858da..b57f733 100644 --- a/tests/test_json2xml.py +++ b/tests/test_json2xml.py @@ -2,7 +2,6 @@ """Tests for `json2xml` package.""" -import json from pyexpat import ExpatError import pytest diff --git a/tests/test_utils.py b/tests/test_utils.py index e98db99..b0768d2 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -17,11 +17,7 @@ ) 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 + pass class TestExceptions: From 6fb1bdc97f3fc994dd9a49ff1d99e3ccdf645d84 Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Fri, 29 Aug 2025 20:56:55 +0530 Subject: [PATCH 2/6] fix: remove shit AI code --- tests/test_dict2xml.py | 3 --- tests/test_utils.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/tests/test_dict2xml.py b/tests/test_dict2xml.py index 47e7cb5..255d09b 100644 --- a/tests/test_dict2xml.py +++ b/tests/test_dict2xml.py @@ -5,9 +5,6 @@ from json2xml import dicttoxml -if TYPE_CHECKING: - pass - class TestDict2xml: """Test class for dicttoxml functionality.""" diff --git a/tests/test_utils.py b/tests/test_utils.py index b0768d2..0761c24 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -16,9 +16,6 @@ readfromurl, ) -if TYPE_CHECKING: - pass - class TestExceptions: """Test custom exception classes.""" From ea0b743d2fe3e2351dba3122139faa9bea4fbc8f Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Fri, 29 Aug 2025 21:20:17 +0530 Subject: [PATCH 3/6] fix: some more improvements --- json2xml/dicttoxml.py | 139 +++++++++++++++++++++--------------------- 1 file changed, 71 insertions(+), 68 deletions(-) diff --git a/json2xml/dicttoxml.py b/json2xml/dicttoxml.py index af32da4..366f9c7 100644 --- a/json2xml/dicttoxml.py +++ b/json2xml/dicttoxml.py @@ -8,12 +8,16 @@ from typing import Any, Union from defusedxml.minidom import parseString +from xml.parsers.expat import ExpatError # Create a safe random number generator # 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[ @@ -77,23 +76,22 @@ 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, numbers.Number): + return "number" + if isinstance(val, dict): + return "dict" + if isinstance(val, Sequence): + return "list" return type(val).__name__ @@ -102,19 +100,19 @@ def escape_xml(s: str | int | float | numbers.Number) -> str: Escape a string for use in XML. Args: - s (str | numbers.Number): The string to escape. + s (str | int | float | numbers.Number): 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,37 +143,39 @@ 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: @@ -188,6 +188,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, @@ -262,7 +281,6 @@ 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: @@ -562,7 +580,7 @@ def dicttoxml( item_wrap: bool = True, item_func: Callable[[str], str] = default_item_func, cdata: bool = False, - xml_namespaces: dict[str, Any] = {}, + xml_namespaces: dict[str, Any] | None = None, list_headers: bool = False ) -> bytes: """ @@ -681,26 +699,11 @@ def dicttoxml( 456 """ + if xml_namespaces is None: + xml_namespaces = {} + 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}"' - - else: - ns = xml_namespaces[prefix] - namespace_str += f' xmlns:{prefix}="{ns}"' + namespace_str = _build_namespace_string(xml_namespaces) if root: output.append('') output_elem = convert( From 299baa056954acc43e6800583cd2daecfb5aaf07 Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Sat, 30 Aug 2025 02:23:26 +0530 Subject: [PATCH 4/6] fix: get the dicttoxml test improved --- .coverage | Bin 53248 -> 53248 bytes tests/test_dict2xml.py | 56 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/.coverage b/.coverage index cad90cb0823aeb6c1b6446407d356ed31be0f21a..77c430a54fe3045bcafef983d408cf05335bfaf5 100644 GIT binary patch delta 433 zcmZozz}&Ead4e<}=R_H2R!#=JE|HBXi{#lJGVs6Wf4Est;41&*2l{-1{EW=f;$^8t z#hLke=6Ys&mYWQ?1z6AWZ{eQ@RMf|BD8jx(k$Gs3jMO=OW^ zHbscSq=6f_(U0JR&fTsyqz0U7Rj?cVBmkp|6sGAz!iQ49wug4{*1)p_{5^* zVgmt2W@*W?)S}|d{5*3#Gd)9tO$OWotmpW5@XrIP@8f5ZV3~a0UxG=5dGZ2(QAZ|b z2GgwK{5+$I+#LP*_{_Y_lK6PNf=Y2lWC@rGpad5K0|N{HdAGoSaf&Gwb&M_|2YR#>&Xq=)m%;mE{1-2lfB%4F9Wz57;yMFxayi z8!w;vpVK{D@&#vcWGZh2c&c|d0@#j+p{?GhR`QPyW1iI}!zakq@l935Qnt~~23s#^5 N8O^~Q7PCbQ3;=u(jI96w diff --git a/tests/test_dict2xml.py b/tests/test_dict2xml.py index 255d09b..f40d043 100644 --- a/tests/test_dict2xml.py +++ b/tests/test_dict2xml.py @@ -1135,3 +1135,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" From e371a9c9d23b73b595c92b26ac1440ffa205cc66 Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Sat, 30 Aug 2025 02:25:51 +0530 Subject: [PATCH 5/6] fix: some more checks --- json2xml/dicttoxml.py | 8 ++++---- tests/test_dict2xml.py | 2 +- tests/test_utils.py | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/json2xml/dicttoxml.py b/json2xml/dicttoxml.py index 366f9c7..42c7648 100644 --- a/json2xml/dicttoxml.py +++ b/json2xml/dicttoxml.py @@ -6,9 +6,9 @@ from collections.abc import Callable, Sequence from random import SystemRandom from typing import Any, Union +from xml.parsers.expat import ExpatError from defusedxml.minidom import parseString -from xml.parsers.expat import ExpatError # Create a safe random number generator @@ -191,7 +191,7 @@ def default_item_func(parent: str) -> str: 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(): @@ -203,7 +203,7 @@ def _build_namespace_string(xml_namespaces: dict[str, Any]) -> str: parts.append(f'xmlns="{value}"') else: parts.append(f'xmlns:{prefix}="{value}"') - + return ' ' + ' '.join(parts) if parts else '' @@ -701,7 +701,7 @@ def dicttoxml( """ if xml_namespaces is None: xml_namespaces = {} - + output = [] namespace_str = _build_namespace_string(xml_namespaces) if root: diff --git a/tests/test_dict2xml.py b/tests/test_dict2xml.py index f40d043..6eb6e42 100644 --- a/tests/test_dict2xml.py +++ b/tests/test_dict2xml.py @@ -1,5 +1,5 @@ import datetime -from typing import TYPE_CHECKING, Any +from typing import Any import pytest diff --git a/tests/test_utils.py b/tests/test_utils.py index 0761c24..52d516d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,6 @@ """Test module for json2xml.utils functionality.""" import json import tempfile -from typing import TYPE_CHECKING from unittest.mock import Mock, patch import pytest From d73781190ba70c192919f88b64f8058be4c31cea Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Sat, 30 Aug 2025 02:39:56 +0530 Subject: [PATCH 6/6] fix: some more fixes --- .github/workflows/pythonpackage.yml | 1 - HISTORY.rst | 1 - Makefile | 1 - README.rst | 7 +- dev.py | 16 ++- docs/conf.py | 55 +++---- examples/bigexample.json | 2 +- examples/wrongjson.json | 4 +- json2xml/__init__.py | 1 - json2xml/dicttoxml.py | 125 +++++++++++----- json2xml/json2xml.py | 7 +- json2xml/utils.py | 7 +- pyproject.toml | 106 +++++++------- pytest.ini | 1 - requirements.in | 1 - setup.py | 1 - tests/conftest.py | 5 +- tests/test_dict2xml.py | 191 +++++++++++++------------ tests/test_json2xml.py | 108 +++++++------- tests/test_utils.py | 54 ++++--- uv.lock | 213 ++++++++++++++++++++++++++++ 21 files changed, 603 insertions(+), 304 deletions(-) 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 42c7648..2078d7c 100644 --- a/json2xml/dicttoxml.py +++ b/json2xml/dicttoxml.py @@ -5,7 +5,7 @@ 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 @@ -57,7 +57,7 @@ def get_unique_id(element: str) -> str: int, float, bool, - numbers.Number, + SupportsFloat, Sequence[Any], datetime.datetime, datetime.date, @@ -86,7 +86,7 @@ def get_xml_type(val: ELEMENT) -> str: return "float" if isinstance(val, str): return "str" - if isinstance(val, numbers.Number): + if isinstance(val, SupportsFloat): return "number" if isinstance(val, dict): return "dict" @@ -95,12 +95,12 @@ def get_xml_type(val: ELEMENT) -> str: 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 | int | float | numbers.Number): The string to escape. + s (str | int | float | SupportsFloat): The string to escape. Returns: str: The escaped string. @@ -148,7 +148,9 @@ def key_is_valid_xml(key: str) -> bool: return False -def make_valid_xml_name(key: str | int, 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_str = str(key) # Ensure we're working with strings key_str = escape_xml(key_str) @@ -178,7 +180,7 @@ def make_valid_xml_name(key: str | int, attr: dict[str, Any]) -> tuple[str, dict 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 "" @@ -193,18 +195,18 @@ def _build_namespace_string(xml_namespaces: dict[str, Any]) -> str: parts = [] for prefix, value in xml_namespaces.items(): - if prefix == 'xsi' and isinstance(value, dict): + if prefix == "xsi" and isinstance(value, dict): for schema_att, ns in value.items(): - if schema_att == 'schemaInstance': + if schema_att == "schemaInstance": parts.append(f'xmlns:{prefix}="{ns}"') - elif schema_att == 'schemaLocation': + elif schema_att == "schemaLocation": parts.append(f'xsi:{schema_att}="{ns}"') - elif prefix == 'xmlns': + elif prefix == "xmlns": parts.append(f'xmlns="{value}"') else: parts.append(f'xmlns:{prefix}="{value}"') - return ' ' + ' '.join(parts) if parts else '' + return " " + " ".join(parts) if parts else "" def convert( @@ -227,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 ) @@ -252,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__})") @@ -285,7 +305,9 @@ def dict2xml_str( 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): @@ -295,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: @@ -337,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 @@ -355,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] = [] @@ -373,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 @@ -394,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, ) ) @@ -410,7 +445,7 @@ def convert_dict( cdata=cdata, item_name=key, item_wrap=item_wrap, - list_headers=list_headers + list_headers=list_headers, ) ) @@ -450,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( @@ -495,7 +530,7 @@ def convert_list( item_wrap=item_wrap, parentIsList=True, parent=parent, - list_headers=list_headers + list_headers=list_headers, ) ) @@ -509,7 +544,7 @@ def convert_list( cdata=cdata, item_name=item_name, item_wrap=item_wrap, - list_headers=list_headers + list_headers=list_headers, ) ) @@ -523,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, @@ -534,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)}" + return ( + f"<{key}{attr_string}>{wrap_cdata(val) if cdata else escape_xml(val)}" + ) 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: @@ -581,7 +624,7 @@ def dicttoxml( item_func: Callable[[str], str] = default_item_func, cdata: bool = False, xml_namespaces: dict[str, Any] | None = None, - list_headers: bool = False + list_headers: bool = False, ) -> bytes: """ Converts a python object into XML. @@ -707,12 +750,28 @@ def dicttoxml( 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}") 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 f98e6e1..d8d1627 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,73 +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) + "**/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 6eb6e42..ef2588c 100644 --- a/tests/test_dict2xml.py +++ b/tests/test_dict2xml.py @@ -165,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 @@ -401,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.""" @@ -457,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", {}) @@ -466,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.""" @@ -508,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"value1value2" + assert ( + result + == b"value1value2" + ) 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"1stringvalue" + assert ( + result + == b"1stringvalue" + ) def test_list_to_xml_with_empty_list(self) -> None: """Test list to XML with empty list.""" @@ -526,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.""" @@ -566,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 @@ -584,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 @@ -597,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 @@ -614,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 @@ -631,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: @@ -644,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 == '' @@ -658,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 @@ -677,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 @@ -700,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 "123" == result @@ -749,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' @@ -819,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" @@ -831,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" @@ -844,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" @@ -856,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 @@ -872,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: @@ -886,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 @@ -901,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 @@ -915,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 @@ -929,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 @@ -943,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 @@ -957,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 @@ -971,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 @@ -986,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 @@ -1000,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 "nestedlist" == result @@ -1010,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: @@ -1028,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: @@ -1049,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 @@ -1059,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 @@ -1093,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 @@ -1123,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) @@ -1183,7 +1198,7 @@ def test_get_xml_type_numbers_number_coverage(self) -> None: import fractions # Test with Decimal (numbers.Number but not int/float) - decimal_val = decimal.Decimal('3.14159') + decimal_val = decimal.Decimal("3.14159") result = dicttoxml.get_xml_type(decimal_val) assert result == "number" diff --git a/tests/test_json2xml.py b/tests/test_json2xml.py index b57f733..02c9a18 100644 --- a/tests/test_json2xml.py +++ b/tests/test_json2xml.py @@ -2,19 +2,13 @@ """Tests for `json2xml` package.""" -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: @@ -110,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( @@ -120,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( @@ -139,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" @@ -175,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 52d516d..eb9c1ad 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,19 +1,14 @@ """Test module for json2xml.utils functionality.""" + import json import tempfile from unittest.mock import Mock, patch import pytest -from json2xml.utils import ( - InvalidDataError, - JSONReadError, - StringReadError, - URLReadError, - readfromjson, - readfromstring, - readfromurl, -) +from json2xml.utils import (InvalidDataError, JSONReadError, StringReadError, + URLReadError, readfromjson, readfromstring, + readfromurl) class TestExceptions: @@ -51,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 @@ -60,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 @@ -73,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: @@ -80,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 @@ -89,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) @@ -102,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 @@ -119,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 @@ -138,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 @@ -155,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 @@ -170,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() @@ -198,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 == {} @@ -207,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 @@ -280,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"