From 8ffaaa9ca499222cfa2171259f3c6b7d6c377c6d Mon Sep 17 00:00:00 2001 From: Stefan Garlonta Date: Sat, 30 Dec 2023 13:28:24 +0100 Subject: [PATCH 01/11] :sparkles: Create data loader for XML --- README.md | 20 +++- pystreamapi/loaders/__init__.py | 4 +- pystreamapi/loaders/__xml/__init__.py | 0 pystreamapi/loaders/__xml/__xml_loader.py | 99 ++++++++++++++++++++ tests/test_xml_loader.py | 107 ++++++++++++++++++++++ 5 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 pystreamapi/loaders/__xml/__init__.py create mode 100644 pystreamapi/loaders/__xml/__xml_loader.py create mode 100644 tests/test_xml_loader.py diff --git a/README.md b/README.md index 0e0ee7a..f476017 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Now you might be wondering why another library when there are already a few impl * The implementation achieves 100% test coverage. * It follows Pythonic principles, resulting in clean and readable code. * It adds some cool innovative features such as conditions or error handling and an even more declarative look. -* It provides loaders for various data sources such as CSV +* It provides loaders for various data sources such as CSV, JSON and XML files. Let's take a look at a small example: @@ -213,14 +213,15 @@ Stream.concat(Stream.of([1, 2]), Stream.of([3, 4])) Creates a new Stream from multiple Streams. Order doesn't change. -## Use loaders: Load data from CSV and JSON files in just one line +## Use loaders: Load data from CSV, JSON and XML files in just one line -PyStreamAPI offers a convenient way to load data from CSV and JSON files. Like that you can start processing your files right away without having to worry about reading and parsing the files. +PyStreamAPI offers a convenient way to load data from CSV, JSON and XML files. Like that you can start processing your +files right away without having to worry about reading and parsing the files. You can import the loaders with: ```python -from pystreamapi.loaders import csv, json +from pystreamapi.loaders import csv, json, xml ``` Now you can use the loaders directly when creating your Stream: @@ -241,6 +242,17 @@ Stream.of(json("data.json")) \ You can access the attributes of the data structures directly like you would do with a normal object. +For XML: + +```python +Stream.of(xml("data.xml")) + .map(lambda x: x.attr1) + .for_each(print) +``` + +The access to the attributes is using a node path syntax. For more details on how to use the node path syntax, please +refer to the [documentation](https://pystreamapi.pickwicksoft.org/reference/data-loaders). + ## API Reference For a more detailed documentation view the docs on GitBook: [PyStreamAPI Docs](https://pystreamapi.pickwicksoft.org/) diff --git a/pystreamapi/loaders/__init__.py b/pystreamapi/loaders/__init__.py index 5d25db5..877f1a8 100644 --- a/pystreamapi/loaders/__init__.py +++ b/pystreamapi/loaders/__init__.py @@ -1,7 +1,9 @@ from pystreamapi.loaders.__csv.__csv_loader import csv from pystreamapi.loaders.__json.__json_loader import json +from pystreamapi.loaders.__xml.__xml_loader import xml __all__ = [ 'csv', - 'json' + 'json', + 'xml' ] diff --git a/pystreamapi/loaders/__xml/__init__.py b/pystreamapi/loaders/__xml/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pystreamapi/loaders/__xml/__xml_loader.py b/pystreamapi/loaders/__xml/__xml_loader.py new file mode 100644 index 0000000..9da3212 --- /dev/null +++ b/pystreamapi/loaders/__xml/__xml_loader.py @@ -0,0 +1,99 @@ +from xml.etree import ElementTree +from collections import namedtuple +from pystreamapi.loaders.__lazy_file_iterable import LazyFileIterable +from pystreamapi.loaders.__loader_utils import LoaderUtils + +__cast_types = True +__retrieve_children = True + + +def xml(src: str, read_from_src=False, retrieve_children=True, cast_types=True, + encoding="utf-8") -> LazyFileIterable: + """ + Loads XML data from either a path or a string and converts it into a list of namedtuples. + Warning: This method isn't safe against malicious XML trees. Parse only safe XML from sources + you trust. + + Returns: + LazyFileIterable: A list of namedtuples, where each namedtuple represents an XML element. + :param retrieve_children: If true, the children of the root element are used as stream elements. + :param encoding: The encoding of the XML file. + :param src: Either the path to an XML file or an XML string. + :param read_from_src: If True, src is treated as an XML string. If False, src is treated as + a path to an XML file. + :param cast_types: Set as False to disable casting of values to int, bool or float. + """ + global __cast_types, __retrieve_children + __cast_types = cast_types + __retrieve_children = retrieve_children + if read_from_src: + return LazyFileIterable(lambda: __load_xml_string(src)) + path = LoaderUtils.validate_path(src) + return LazyFileIterable(lambda: __load_xml_file(path, encoding)) + + +def __load_xml_file(file_path, encoding): + """Load an XML file and convert it into a list of namedtuples.""" + with open(file_path, mode='r', encoding=encoding) as xmlfile: + src = xmlfile.read() + if not src: + return [] + data = __parse_xml_string(src) + return data + + +def __load_xml_string(xml_string): + """Load XML data from a string and convert it into a list of namedtuples.""" + return __parse_xml_string(xml_string) + + +def __parse_xml_string(xml_string): + """Parse XML string and convert it into a list of namedtuples.""" + root = ElementTree.fromstring(xml_string) + parsed_xml = __parse_xml(root) + return __flatten(parsed_xml) if __retrieve_children else [parsed_xml] + + +def __parse_xml(element): + if len(element) == 0: + return __parse_empty_element(element) + elif len(element) == 1: + return __parse_single_element(element) + else: + return __parse_multiple_elements(element) + + +def __parse_empty_element(element): + return LoaderUtils.try_cast(element.text) if __cast_types else element.text + + +def __parse_single_element(element): + sub_element = element[0] + sub_item = __parse_xml(sub_element) + Item = namedtuple(element.tag, [sub_element.tag]) + return Item(sub_item) + + +def __parse_multiple_elements(element): + tag_dict = {} + for e in element: + if e.tag not in tag_dict: + tag_dict[e.tag] = [] + tag_dict[e.tag].append(__parse_xml(e)) + filtered_dict = __filter_single_items(tag_dict) + Item = namedtuple(element.tag, filtered_dict.keys()) + return Item(*filtered_dict.values()) + + +def __filter_single_items(tag_dict): + return {key: value[0] if len(value) == 1 else value for key, value in tag_dict.items()} + + +def __flatten(data): + res = [] + for item in data: + if isinstance(item, list): + res.extend(item) + else: + res.append(item) + return res diff --git a/tests/test_xml_loader.py b/tests/test_xml_loader.py new file mode 100644 index 0000000..81760ab --- /dev/null +++ b/tests/test_xml_loader.py @@ -0,0 +1,107 @@ +# pylint: disable=not-context-manager +from unittest import TestCase +from unittest.mock import patch, mock_open +from xml.etree.ElementTree import ParseError + +from file_test import OPEN, PATH_EXISTS, PATH_ISFILE +from pystreamapi.loaders import xml + +file_content = """ + + + John Doe + 80000 + + + Alice Smith + + Frank + + + + + Bugatti + Mercedes + + + +""" +file_path = 'path/to/data.xml' + + +class TestXmlLoader(TestCase): + + def test_xml_loader_from_file_children(self): + with (patch(OPEN, mock_open(read_data=file_content)), + patch(PATH_EXISTS, return_value=True), + patch(PATH_ISFILE, return_value=True)): + data = xml(file_path) + self.assertEqual(len(data), 3) + self.assertEqual(data[0].salary, 80000) + self.assertIsInstance(data[0].salary, int) + self.assertEqual(data[1].child.name, "Frank") + self.assertIsInstance(data[1].child.name, str) + self.assertEqual(data[2].cars.car[0], 'Bugatti') + self.assertIsInstance(data[2].cars.car[0], str) + + def test_xml_loader_from_file_no_children_false(self): + with (patch(OPEN, mock_open(read_data=file_content)), + patch(PATH_EXISTS, return_value=True), + patch(PATH_ISFILE, return_value=True)): + data = xml(file_path, retrieve_children=False) + self.assertEqual(len(data), 1) + self.assertEqual(data[0].employee[0].salary, 80000) + self.assertIsInstance(data[0].employee[0].salary, int) + self.assertEqual(data[0].employee[1].child.name, "Frank") + self.assertIsInstance(data[0].employee[1].child.name, str) + self.assertEqual(data[0].founder.cars.car[0], 'Bugatti') + self.assertIsInstance(data[0].founder.cars.car[0], str) + + def test_xml_loader_no_casting(self): + with (patch(OPEN, mock_open(read_data=file_content)), + patch(PATH_EXISTS, return_value=True), + patch(PATH_ISFILE, return_value=True)): + data = xml(file_path, cast_types=False) + self.assertEqual(len(data), 3) + self.assertEqual(data[0].salary, '80000') + self.assertIsInstance(data[0].salary, str) + self.assertEqual(data[1].child.name, "Frank") + self.assertIsInstance(data[1].child.name, str) + self.assertEqual(data[2].cars.car[0], 'Bugatti') + self.assertIsInstance(data[2].cars.car[0], str) + + def test_xml_loader_is_iterable(self): + with (patch(OPEN, mock_open(read_data=file_content)), + patch(PATH_EXISTS, return_value=True), + patch(PATH_ISFILE, return_value=True)): + data = xml(file_path) + self.assertEqual(len(list(iter(data))), 3) + + def test_xml_loader_with_empty_file(self): + with (patch(OPEN, mock_open(read_data="")), + patch(PATH_EXISTS, return_value=True), + patch(PATH_ISFILE, return_value=True)): + data = xml(file_path) + self.assertEqual(len(data), 0) + + def test_xml_loader_with_invalid_path(self): + with self.assertRaises(FileNotFoundError): + xml('path/to/invalid.xml') + + def test_xml_loader_with_no_file(self): + with self.assertRaises(ValueError): + xml('./') + + def test_xml_loader_from_string(self): + data = xml(file_content, read_from_src=True) + self.assertEqual(len(data), 3) + self.assertEqual(data[0].salary, 80000) + self.assertIsInstance(data[0].salary, int) + self.assertEqual(data[1].child.name, "Frank") + self.assertIsInstance(data[1].child.name, str) + self.assertEqual(data[2].cars.car[0], 'Bugatti') + self.assertIsInstance(data[2].cars.car[0], str) + + def test_xml_loader_from_empty_string(self): + with self.assertRaises(ParseError): + self.assertEqual(len(xml('', read_from_src=True)), 0) From 36b3fd0e416c66d4f42a0d197bf7e25d5b7fcd3f Mon Sep 17 00:00:00 2001 From: Stefan Garlonta Date: Sat, 30 Dec 2023 14:25:49 +0100 Subject: [PATCH 02/11] :adhesive_bandage: Fix issues --- pystreamapi/loaders/__xml/__xml_loader.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pystreamapi/loaders/__xml/__xml_loader.py b/pystreamapi/loaders/__xml/__xml_loader.py index 9da3212..237737c 100644 --- a/pystreamapi/loaders/__xml/__xml_loader.py +++ b/pystreamapi/loaders/__xml/__xml_loader.py @@ -16,7 +16,8 @@ def xml(src: str, read_from_src=False, retrieve_children=True, cast_types=True, Returns: LazyFileIterable: A list of namedtuples, where each namedtuple represents an XML element. - :param retrieve_children: If true, the children of the root element are used as stream elements. + :param retrieve_children: If true, the children of the root element are used as stream + elements. :param encoding: The encoding of the XML file. :param src: Either the path to an XML file or an XML string. :param read_from_src: If True, src is treated as an XML string. If False, src is treated as @@ -36,10 +37,9 @@ def __load_xml_file(file_path, encoding): """Load an XML file and convert it into a list of namedtuples.""" with open(file_path, mode='r', encoding=encoding) as xmlfile: src = xmlfile.read() - if not src: - return [] - data = __parse_xml_string(src) - return data + if src: + return __parse_xml_string(src) + return [] def __load_xml_string(xml_string): @@ -57,10 +57,9 @@ def __parse_xml_string(xml_string): def __parse_xml(element): if len(element) == 0: return __parse_empty_element(element) - elif len(element) == 1: + if len(element) == 1: return __parse_single_element(element) - else: - return __parse_multiple_elements(element) + return __parse_multiple_elements(element) def __parse_empty_element(element): From 782b5416c2d305af27957a91a3da1b551ab87b31 Mon Sep 17 00:00:00 2001 From: Stefan Garlonta Date: Sat, 30 Dec 2023 14:37:56 +0100 Subject: [PATCH 03/11] :adhesive_bandage: Fix code issues and smells --- pystreamapi/loaders/__xml/__xml_loader.py | 25 ++++++++++++++++------- tests/test_xml_loader.py | 2 +- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/pystreamapi/loaders/__xml/__xml_loader.py b/pystreamapi/loaders/__xml/__xml_loader.py index 237737c..2fa3a0d 100644 --- a/pystreamapi/loaders/__xml/__xml_loader.py +++ b/pystreamapi/loaders/__xml/__xml_loader.py @@ -3,8 +3,14 @@ from pystreamapi.loaders.__lazy_file_iterable import LazyFileIterable from pystreamapi.loaders.__loader_utils import LoaderUtils -__cast_types = True -__retrieve_children = True + +class __XmlLoaderUtil: + def __init__(self): + self.cast_types = True + self.retrieve_children = True + + +config = __XmlLoaderUtil() def xml(src: str, read_from_src=False, retrieve_children=True, cast_types=True, @@ -24,9 +30,8 @@ def xml(src: str, read_from_src=False, retrieve_children=True, cast_types=True, a path to an XML file. :param cast_types: Set as False to disable casting of values to int, bool or float. """ - global __cast_types, __retrieve_children - __cast_types = cast_types - __retrieve_children = retrieve_children + config.cast_types = cast_types + config.retrieve_children = retrieve_children if read_from_src: return LazyFileIterable(lambda: __load_xml_string(src)) path = LoaderUtils.validate_path(src) @@ -51,10 +56,11 @@ def __parse_xml_string(xml_string): """Parse XML string and convert it into a list of namedtuples.""" root = ElementTree.fromstring(xml_string) parsed_xml = __parse_xml(root) - return __flatten(parsed_xml) if __retrieve_children else [parsed_xml] + return __flatten(parsed_xml) if config.retrieve_children else [parsed_xml] def __parse_xml(element): + """Parse XML element and convert it into a namedtuple.""" if len(element) == 0: return __parse_empty_element(element) if len(element) == 1: @@ -63,10 +69,12 @@ def __parse_xml(element): def __parse_empty_element(element): - return LoaderUtils.try_cast(element.text) if __cast_types else element.text + """Parse XML element without children and convert it into a namedtuple.""" + return LoaderUtils.try_cast(element.text) if config.cast_types else element.text def __parse_single_element(element): + """Parse XML element with a single child and convert it into a namedtuple.""" sub_element = element[0] sub_item = __parse_xml(sub_element) Item = namedtuple(element.tag, [sub_element.tag]) @@ -74,6 +82,7 @@ def __parse_single_element(element): def __parse_multiple_elements(element): + """Parse XML element with multiple children and convert it into a namedtuple.""" tag_dict = {} for e in element: if e.tag not in tag_dict: @@ -85,10 +94,12 @@ def __parse_multiple_elements(element): def __filter_single_items(tag_dict): + """Filter out single-item lists from a dictionary.""" return {key: value[0] if len(value) == 1 else value for key, value in tag_dict.items()} def __flatten(data): + """Flatten a list of lists.""" res = [] for item in data: if isinstance(item, list): diff --git a/tests/test_xml_loader.py b/tests/test_xml_loader.py index 81760ab..d339c4a 100644 --- a/tests/test_xml_loader.py +++ b/tests/test_xml_loader.py @@ -104,4 +104,4 @@ def test_xml_loader_from_string(self): def test_xml_loader_from_empty_string(self): with self.assertRaises(ParseError): - self.assertEqual(len(xml('', read_from_src=True)), 0) + len(xml('', read_from_src=True)) From 46ff34bae18f7dceb77b201ed974f4858bdacfd7 Mon Sep 17 00:00:00 2001 From: Stefan Garlonta Date: Sat, 30 Dec 2023 15:33:08 +0100 Subject: [PATCH 04/11] :heavy_plus_sign: Add optional dependency defusedxml --- poetry.lock | 172 ++++++++++++---------- pyproject.toml | 6 +- pystreamapi/__init__.py | 2 +- pystreamapi/loaders/__xml/__xml_loader.py | 7 +- setup.cfg | 30 ---- 5 files changed, 103 insertions(+), 114 deletions(-) delete mode 100644 setup.cfg diff --git a/poetry.lock b/poetry.lock index 0b76d3a..e37e1e6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -104,6 +104,18 @@ files = [ [package.extras] toml = ["tomli"] +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + [[package]] name = "dill" version = "0.3.7" @@ -224,14 +236,14 @@ dev = ["jinja2"] [[package]] name = "platformdirs" -version = "3.10.0" +version = "4.0.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, - {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, + {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, + {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, ] [package.dependencies] @@ -360,90 +372,88 @@ files = [ [[package]] name = "wrapt" -version = "1.15.0" +version = "1.16.0" description = "Module for decorators, wrappers and monkey patching." category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" files = [ - {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, - {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, - {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, - {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, - {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, - {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, - {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, - {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, - {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, - {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, - {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, - {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, - {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, - {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, - {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, - {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, - {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, - {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, - {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] +[extras] +xml-loader = ["defusedxml"] + [metadata] lock-version = "2.0" python-versions = ">=3.7,<4.0" -content-hash = "8ac9e02a68abb5a7060f2bc96126a7e0364f97f0a66379b57eaa14ba8541d2e4" +content-hash = "07ef4e0b2abbb74f133d682d84bbd9e74e6af5561ad213f359408ec49b47d0ab" diff --git a/pyproject.toml b/pyproject.toml index 049bd58..def84d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "streams.py" -version = "1.1.0" +version = "1.2.0" authors = ["Stefan Garlonta "] description = "A stream library for Python inspired by Java Stream API" keywords = ["streams", "parallel", "data"] @@ -15,6 +15,10 @@ packages = [ [tool.poetry.dependencies] python = ">=3.7,<4.0" joblib = ">=1.2,<1.4" +defusedxml = { version = ">=0.7,<0.8", optional = true } + +[tool.poetry.extras] +xml_loader = ["defusedxml"] [tool.poetry.group.test.dependencies] parameterized = "*" diff --git a/pystreamapi/__init__.py b/pystreamapi/__init__.py index 171e82b..77d73a1 100644 --- a/pystreamapi/__init__.py +++ b/pystreamapi/__init__.py @@ -1,5 +1,5 @@ from pystreamapi.__stream import Stream from pystreamapi._streams.error.__levels import ErrorLevel -__version__ = "1.1.0" +__version__ = "1.2.0" __all__ = ["Stream", "ErrorLevel"] diff --git a/pystreamapi/loaders/__xml/__xml_loader.py b/pystreamapi/loaders/__xml/__xml_loader.py index 2fa3a0d..9760dc1 100644 --- a/pystreamapi/loaders/__xml/__xml_loader.py +++ b/pystreamapi/loaders/__xml/__xml_loader.py @@ -1,10 +1,14 @@ -from xml.etree import ElementTree +try: + from defusedxml import ElementTree +except ImportError: + import xml.etree.ElementTree as ElementTree from collections import namedtuple from pystreamapi.loaders.__lazy_file_iterable import LazyFileIterable from pystreamapi.loaders.__loader_utils import LoaderUtils class __XmlLoaderUtil: + """Utility class for the XML loader.""" def __init__(self): self.cast_types = True self.retrieve_children = True @@ -40,6 +44,7 @@ def xml(src: str, read_from_src=False, retrieve_children=True, cast_types=True, def __load_xml_file(file_path, encoding): """Load an XML file and convert it into a list of namedtuples.""" + # skipcq: PTC-W6004 with open(file_path, mode='r', encoding=encoding) as xmlfile: src = xmlfile.read() if src: diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 5cc4514..0000000 --- a/setup.cfg +++ /dev/null @@ -1,30 +0,0 @@ -[metadata] -name = streams.py -version = attr: pystreamapi.__version__ -author = Stefan Garlonta -author_email = stefan@pickwicksoft.org -url = https://github.com/PickwickSoft/pystreamapi -description = A stream library for Python inspired by Java Stream API -long_description = file: README.md -long_description_content_type = text/markdown -keywords = streams, parallel, data -license = GPL-3.0 license -classifiers = - License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) - Programming Language :: Python - Programming Language :: Python :: 3 - -[options] -packages = pystreamapi -zip_safe = True -include_package_data = True -install_requires = - optional.py>=1.3.2 - joblib~=1.2.0 - -[options.extras_require] -dev = - parameterized~=0.8.1 - -[options.package_data] -* = README.md From 0c3d304473c2c860c123ab8023ffb756695fbf7f Mon Sep 17 00:00:00 2001 From: Stefan Garlonta Date: Sat, 30 Dec 2023 15:34:10 +0100 Subject: [PATCH 05/11] :recycle: Change import --- pystreamapi/loaders/__xml/__xml_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pystreamapi/loaders/__xml/__xml_loader.py b/pystreamapi/loaders/__xml/__xml_loader.py index 9760dc1..5f9fa6b 100644 --- a/pystreamapi/loaders/__xml/__xml_loader.py +++ b/pystreamapi/loaders/__xml/__xml_loader.py @@ -1,7 +1,7 @@ try: from defusedxml import ElementTree except ImportError: - import xml.etree.ElementTree as ElementTree + from xml.etree import ElementTree from collections import namedtuple from pystreamapi.loaders.__lazy_file_iterable import LazyFileIterable from pystreamapi.loaders.__loader_utils import LoaderUtils From 443bbe15ddc96ebc208725b7b9b149b767e07b44 Mon Sep 17 00:00:00 2001 From: Stefan Garlonta Date: Sat, 30 Dec 2023 15:36:14 +0100 Subject: [PATCH 06/11] :art: Format class docstring --- pystreamapi/loaders/__xml/__xml_loader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pystreamapi/loaders/__xml/__xml_loader.py b/pystreamapi/loaders/__xml/__xml_loader.py index 5f9fa6b..4e59883 100644 --- a/pystreamapi/loaders/__xml/__xml_loader.py +++ b/pystreamapi/loaders/__xml/__xml_loader.py @@ -9,6 +9,7 @@ class __XmlLoaderUtil: """Utility class for the XML loader.""" + def __init__(self): self.cast_types = True self.retrieve_children = True From f07586bc9c8b80341d8121a59693490e895c32de Mon Sep 17 00:00:00 2001 From: Stefan Garlonta Date: Sat, 30 Dec 2023 15:44:55 +0100 Subject: [PATCH 07/11] :lock: Only use defusedxml module in xml loader --- README.md | 8 ++++++++ poetry.lock | 3 ++- pyproject.toml | 1 + pystreamapi/loaders/__xml/__xml_loader.py | 6 ++++-- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f476017..951a2a4 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,14 @@ You can access the attributes of the data structures directly like you would do For XML: +In order to use the XML loader, you need to install the optional xml dependency: + +```bash +pip install streams.py[xml_loader] +``` + +Afterward, you can use the XML loader like this: + ```python Stream.of(xml("data.xml")) .map(lambda x: x.attr1) diff --git a/poetry.lock b/poetry.lock index e37e1e6..6f59f04 100644 --- a/poetry.lock +++ b/poetry.lock @@ -451,9 +451,10 @@ files = [ ] [extras] +all = ["defusedxml"] xml-loader = ["defusedxml"] [metadata] lock-version = "2.0" python-versions = ">=3.7,<4.0" -content-hash = "07ef4e0b2abbb74f133d682d84bbd9e74e6af5561ad213f359408ec49b47d0ab" +content-hash = "6252674f8a6cc31e6860115894c00fd86d891059074bf6fdb77bbea533d9cf30" diff --git a/pyproject.toml b/pyproject.toml index def84d0..41025e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ defusedxml = { version = ">=0.7,<0.8", optional = true } [tool.poetry.extras] xml_loader = ["defusedxml"] +all = ["defusedxml"] [tool.poetry.group.test.dependencies] parameterized = "*" diff --git a/pystreamapi/loaders/__xml/__xml_loader.py b/pystreamapi/loaders/__xml/__xml_loader.py index 4e59883..98b551e 100644 --- a/pystreamapi/loaders/__xml/__xml_loader.py +++ b/pystreamapi/loaders/__xml/__xml_loader.py @@ -1,7 +1,9 @@ try: from defusedxml import ElementTree -except ImportError: - from xml.etree import ElementTree +except ImportError as exc: + raise ImportError( + "Please install the xml_loader extra dependency to use the xml loader." + ) from exc from collections import namedtuple from pystreamapi.loaders.__lazy_file_iterable import LazyFileIterable from pystreamapi.loaders.__loader_utils import LoaderUtils From 278b0159f42844ae60e338bec36362995bdbed82 Mon Sep 17 00:00:00 2001 From: Stefan Garlonta Date: Sat, 30 Dec 2023 16:12:26 +0100 Subject: [PATCH 08/11] :green_heart: Fix dependencies in CI/CD environments --- .github/workflows/unittests.yml | 2 +- tox.ini | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index f9d0ba4..f6b3fc6 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -64,7 +64,7 @@ jobs: # Install dependencies. `--no-root` means "install all dependencies but not the project # itself", which is what you want to avoid caching _your_ code. The `if` statement # ensures this only runs on a cache miss. - - run: poetry install --no-root + - run: poetry install --no-root --extras "xml_loader" if: steps.cache-deps.outputs.cache-hit != 'true' # Now install _your_ project. This isn't necessary for many types of projects -- particularly diff --git a/tox.ini b/tox.ini index c0a4aca..39d413d 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ deps = optional.py joblib parameterized + defusedxml commands = coverage run -m unittest discover -s tests -t tests --pattern 'test_*.py' coverage xml @@ -15,4 +16,4 @@ commands = [coverage:run] relative_files = True source = pystreamapi/ -branch = True \ No newline at end of file +branch = True From 42c520c3918c3adaba329a736bbc6d4b27a0732b Mon Sep 17 00:00:00 2001 From: Stefan Garlonta Date: Sat, 30 Dec 2023 16:17:16 +0100 Subject: [PATCH 09/11] :green_heart: Fix deps in unittest CI/CD --- .github/workflows/unittests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index f6b3fc6..7287cc9 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -64,14 +64,14 @@ jobs: # Install dependencies. `--no-root` means "install all dependencies but not the project # itself", which is what you want to avoid caching _your_ code. The `if` statement # ensures this only runs on a cache miss. - - run: poetry install --no-root --extras "xml_loader" + - run: poetry install --no-root --extras "all" if: steps.cache-deps.outputs.cache-hit != 'true' # Now install _your_ project. This isn't necessary for many types of projects -- particularly # things like Django apps don't need this. But it's a good idea since it fully-exercises the # pyproject.toml and makes that if you add things like console-scripts at some point that # they'll be installed and working. - - run: poetry install + - run: poetry install --extras "all" # Runs a single command using the runners shell - name: Run Unittests From 973038ce03d53e7cb9a30df696354b7ef9d8cca0 Mon Sep 17 00:00:00 2001 From: Stefan Garlonta Date: Sat, 30 Dec 2023 16:50:52 +0100 Subject: [PATCH 10/11] :white_check_mark: Add test testing import error --- tests/test_xml_loader.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_xml_loader.py b/tests/test_xml_loader.py index d339c4a..87a9400 100644 --- a/tests/test_xml_loader.py +++ b/tests/test_xml_loader.py @@ -105,3 +105,9 @@ def test_xml_loader_from_string(self): def test_xml_loader_from_empty_string(self): with self.assertRaises(ParseError): len(xml('', read_from_src=True)) + + @patch('builtins.__import__', side_effect=ImportError('Mocked ImportError')) + def test_defusedxml_not_installed(self, mock_import): + with self.assertRaises(ImportError): + from pystreamapi.loaders import xml + xml(file_path) From 9e76342abb9836fb846155004e8772e35654b67e Mon Sep 17 00:00:00 2001 From: Stefan Garlonta Date: Sat, 30 Dec 2023 16:54:00 +0100 Subject: [PATCH 11/11] :fire: Remove unnecessary test --- tests/test_xml_loader.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_xml_loader.py b/tests/test_xml_loader.py index 87a9400..d339c4a 100644 --- a/tests/test_xml_loader.py +++ b/tests/test_xml_loader.py @@ -105,9 +105,3 @@ def test_xml_loader_from_string(self): def test_xml_loader_from_empty_string(self): with self.assertRaises(ParseError): len(xml('', read_from_src=True)) - - @patch('builtins.__import__', side_effect=ImportError('Mocked ImportError')) - def test_defusedxml_not_installed(self, mock_import): - with self.assertRaises(ImportError): - from pystreamapi.loaders import xml - xml(file_path)