From 878a57d142dfc94086bd82b5cdb65986efa9402e Mon Sep 17 00:00:00 2001 From: Kian-Meng Ang Date: Tue, 7 Feb 2023 15:13:35 +0800 Subject: [PATCH 01/94] Fix typos (#53) --- multipart/exceptions.py | 2 +- multipart/multipart.py | 28 ++++++++++++++-------------- multipart/tests/test_multipart.py | 7 +++---- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/multipart/exceptions.py b/multipart/exceptions.py index 3264b82..016e7f7 100644 --- a/multipart/exceptions.py +++ b/multipart/exceptions.py @@ -9,7 +9,7 @@ class ParseError(FormParserError): """ #: This is the offset in the input data chunk (*NOT* the overall stream) in - #: which the parse error occured. It will be -1 if not specified. + #: which the parse error occurred. It will be -1 if not specified. offset = -1 diff --git a/multipart/multipart.py b/multipart/multipart.py index df5d374..a9f1f9f 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -34,7 +34,7 @@ STATES = [ "START", - "START_BOUNDARY", "HEADER_FEILD_START", "HEADER_FIELD", "HEADER_VALUE_START", "HEADER_VALUE", + "START_BOUNDARY", "HEADER_FIELD_START", "HEADER_FIELD", "HEADER_VALUE_START", "HEADER_VALUE", "HEADER_VALUE_ALMOST_DONE", "HEADRES_ALMOST_DONE", "PART_DATA_START", "PART_DATA", "PART_DATA_END", "END" ] @@ -789,9 +789,9 @@ def _internal_write(self, data, length): # Depending on our state... if state == STATE_BEFORE_FIELD: # If the 'found_sep' flag is set, we've already encountered - # and skipped a single seperator. If so, we check our strict + # and skipped a single separator. If so, we check our strict # parsing flag and decide what to do. Otherwise, we haven't - # yet reached a seperator, and thus, if we do, we need to skip + # yet reached a separator, and thus, if we do, we need to skip # it as it will be the boundary between fields that's supposed # to be there. if ch == AMPERSAND or ch == SEMICOLON: @@ -809,7 +809,7 @@ def _internal_write(self, data, length): "semicolon at %d", i) else: # This case is when we're skipping the (first) - # seperator between fields, so we just set our flag + # separator between fields, so we just set our flag # and continue on. found_sep = True else: @@ -822,14 +822,14 @@ def _internal_write(self, data, length): found_sep = False elif state == STATE_FIELD_NAME: - # Try and find a seperator - we ensure that, if we do, we only + # Try and find a separator - we ensure that, if we do, we only # look for the equal sign before it. sep_pos = data.find(b'&', i) if sep_pos == -1: sep_pos = data.find(b';', i) # See if we can find an equals sign in the remaining data. If - # so, we can immedately emit the field name and jump to the + # so, we can immediately emit the field name and jump to the # data state. if sep_pos != -1: equals_pos = data.find(b'=', i, sep_pos) @@ -849,7 +849,7 @@ def _internal_write(self, data, length): # No equals sign found. if not strict_parsing: # See also comments in the STATE_FIELD_DATA case below. - # If we found the seperator, we emit the name and just + # If we found the separator, we emit the name and just # end - there's no data callback at all (not even with # a blank value). if sep_pos != -1: @@ -859,13 +859,13 @@ def _internal_write(self, data, length): i = sep_pos - 1 state = STATE_BEFORE_FIELD else: - # Otherwise, no seperator in this block, so the + # Otherwise, no separator in this block, so the # rest of this chunk must be a name. self.callback('field_name', data, i, length) i = length else: - # We're parsing strictly. If we find a seperator, + # We're parsing strictly. If we find a separator, # this is an error - we require an equals sign. if sep_pos != -1: e = QuerystringParseError( @@ -877,7 +877,7 @@ def _internal_write(self, data, length): e.offset = i raise e - # No seperator in the rest of this chunk, so it's just + # No separator in the rest of this chunk, so it's just # a field name. self.callback('field_name', data, i, length) i = length @@ -895,7 +895,7 @@ def _internal_write(self, data, length): self.callback('field_data', data, i, sep_pos) self.callback('field_end') - # Note that we go to the seperator, which brings us to the + # Note that we go to the separator, which brings us to the # "before field" state. This allows us to properly emit # "field_start" events only when we actually have data for # a field of some sort. @@ -1006,7 +1006,7 @@ def __init__(self, boundary, callbacks={}, max_size=float('inf')): self.max_size = max_size self._current_size = 0 - # Setup marks. These are used to track the state of data recieved. + # Setup marks. These are used to track the state of data received. self.marks = {} # TODO: Actually use this rather than the dumb version we currently use @@ -1500,7 +1500,7 @@ class FormParser: :class:`File`, but you can provide your own class if you wish to customize behaviour. The class will be instantiated as FileClass(file_name, field_name), and it - must provide the folllowing functions:: + must provide the following functions:: file_instance.write(data) file_instance.finalize() file_instance.close() @@ -1509,7 +1509,7 @@ class FormParser: :class:`Field`, but you can provide your own class if you wish to customize behaviour. The class will be instantiated as FieldClass(field_name), and it must - provide the folllowing functions:: + provide the following functions:: field_instance.write(data) field_instance.finalize() field_instance.close() diff --git a/multipart/tests/test_multipart.py b/multipart/tests/test_multipart.py index 8525227..089f451 100644 --- a/multipart/tests/test_multipart.py +++ b/multipart/tests/test_multipart.py @@ -391,7 +391,7 @@ def test_streaming_break(self): (b'asdf', b'baz') ) - def test_semicolon_seperator(self): + def test_semicolon_separator(self): self.p.write(b'foo=bar;asdf=baz') self.assert_fields( @@ -1070,7 +1070,7 @@ def on_file(f): f.write(b'1234') f.finalize() - # Assert that we only recieved a single file, with the right data, and that we're done. + # Assert that we only received a single file, with the right data, and that we're done. self.assertFalse(on_field.called) self.assertEqual(len(files), 1) self.assert_file_data(files[0], b'test1234') @@ -1094,7 +1094,7 @@ def simple_test(f): f.write(b'&test=asdf') f.finalize() - # Assert we only recieved 2 fields... + # Assert we only received 2 fields... self.assertFalse(on_file.called) self.assertEqual(len(fields), 2) @@ -1303,4 +1303,3 @@ def suite(): suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestHelperFunctions)) return suite - From a4f2f4089f8a90c01c07796f72562910105acb2d Mon Sep 17 00:00:00 2001 From: Nahuel Ambrosini Date: Mon, 27 Feb 2023 13:21:05 -0300 Subject: [PATCH 02/94] =?UTF-8?q?=E2=9C=A8=20Setup=20automated=20releases?= =?UTF-8?q?=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marcelo Trylesinski --- .github/workflows/publish.yaml | 44 ++++++++++++++++++++++++++++++++++ .github/workflows/test.yaml | 5 ++-- 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/publish.yaml diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..d35bfa7 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,44 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+.*' # Run on every git tag with semantic versioning. i.e: 1.5.0 or 1.5.0rc1 + +permissions: + contents: read + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + + - name: Build package + run: python -m build + + - name: Publish package + uses: pypa/gh-action-pypi-publish@v1.6.4 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5fd0bcc..21428c5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -25,8 +25,9 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install .[dev] - name: Test with pytest run: | + # This should be inv test but invoke does not have python3.11 support yet. + # See https://github.com/pyinvoke/invoke/issues/891 for details pytest From 4ccfb3a0f20990c1ed682727262b2560707b81df Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 27 Feb 2023 17:37:18 +0100 Subject: [PATCH 03/94] Version 0.0.6 (#60) --- CHANGELOG.md | 9 +++++++++ multipart/__init__.py | 4 +--- 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7373025 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## 0.0.6 (2023-02-27) + +* Migrate package installation to `pyproject.toml` (PEP 621) [#54](https://github.com/andrew-d/python-multipart/pull/54). +* Use yaml.safe_load instead of yaml.load [#46](https://github.com/andrew-d/python-multipart/pull/46). +* Add support for Python 3.11, drop EOL 3.6 [#51](https://github.com/andrew-d/python-multipart/pull/51). +* Add support for Python 3.8-3.10, drop EOL 2.7-3.5 [#42](https://github.com/andrew-d/python-multipart/pull/42). +* `QuerystringParser`: don't raise an AttributeError in `__repr__` [#30](https://github.com/andrew-d/python-multipart/pull/30). diff --git a/multipart/__init__.py b/multipart/__init__.py index 39e5605..309d698 100644 --- a/multipart/__init__.py +++ b/multipart/__init__.py @@ -1,10 +1,8 @@ -import sys - # This is the canonical package information. __author__ = 'Andrew Dunham' __license__ = 'Apache' __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" -__version__ = "0.0.5" +__version__ = "0.0.6" from .multipart import ( From 3929f8e5018c3c0ab60737734d0a0e64a1c2536d Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Tue, 28 Feb 2023 09:32:44 +0100 Subject: [PATCH 04/94] Move tests folder to root folder (#61) --- .coveragerc | 2 +- .github/workflows/publish.yaml | 2 +- MANIFEST.in | 4 ---- multipart/__init__.py | 8 ++++---- pyproject.toml | 13 ++++--------- tasks.py | 2 +- {multipart/tests => tests}/__init__.py | 0 {multipart/tests => tests}/compat.py | 0 .../test_data/http/CR_in_header.http | 0 .../test_data/http/CR_in_header.yaml | 0 .../test_data/http/CR_in_header_value.http | 0 .../test_data/http/CR_in_header_value.yaml | 0 .../test_data/http/almost_match_boundary.http | 0 .../test_data/http/almost_match_boundary.yaml | 0 .../http/almost_match_boundary_without_CR.http | 0 .../http/almost_match_boundary_without_CR.yaml | 0 .../http/almost_match_boundary_without_LF.http | 0 .../http/almost_match_boundary_without_LF.yaml | 0 .../almost_match_boundary_without_final_hyphen.http | 0 .../almost_match_boundary_without_final_hyphen.yaml | 0 .../test_data/http/bad_end_of_headers.http | 0 .../test_data/http/bad_end_of_headers.yaml | 0 .../test_data/http/bad_header_char.http | 0 .../test_data/http/bad_header_char.yaml | 0 .../test_data/http/bad_initial_boundary.http | 0 .../test_data/http/bad_initial_boundary.yaml | 0 .../test_data/http/base64_encoding.http | 0 .../test_data/http/base64_encoding.yaml | 0 .../test_data/http/empty_header.http | 0 .../test_data/http/empty_header.yaml | 0 .../test_data/http/multiple_fields.http | 0 .../test_data/http/multiple_fields.yaml | 0 .../test_data/http/multiple_files.http | 0 .../test_data/http/multiple_files.yaml | 0 .../test_data/http/quoted_printable_encoding.http | 0 .../test_data/http/quoted_printable_encoding.yaml | 0 .../test_data/http/single_field.http | 0 .../test_data/http/single_field.yaml | 0 .../test_data/http/single_field_blocks.http | 0 .../test_data/http/single_field_blocks.yaml | 0 .../test_data/http/single_field_longer.http | 0 .../test_data/http/single_field_longer.yaml | 0 .../test_data/http/single_field_single_file.http | 0 .../test_data/http/single_field_single_file.yaml | 0 .../http/single_field_with_leading_newlines.http | 0 .../http/single_field_with_leading_newlines.yaml | 0 .../tests => tests}/test_data/http/single_file.http | 0 .../tests => tests}/test_data/http/single_file.yaml | 0 .../test_data/http/utf8_filename.http | 0 .../test_data/http/utf8_filename.yaml | 0 {multipart/tests => tests}/test_multipart.py | 6 ++---- tox.ini | 2 +- 52 files changed, 14 insertions(+), 25 deletions(-) delete mode 100644 MANIFEST.in rename {multipart/tests => tests}/__init__.py (100%) rename {multipart/tests => tests}/compat.py (100%) rename {multipart/tests => tests}/test_data/http/CR_in_header.http (100%) rename {multipart/tests => tests}/test_data/http/CR_in_header.yaml (100%) rename {multipart/tests => tests}/test_data/http/CR_in_header_value.http (100%) rename {multipart/tests => tests}/test_data/http/CR_in_header_value.yaml (100%) rename {multipart/tests => tests}/test_data/http/almost_match_boundary.http (100%) rename {multipart/tests => tests}/test_data/http/almost_match_boundary.yaml (100%) rename {multipart/tests => tests}/test_data/http/almost_match_boundary_without_CR.http (100%) rename {multipart/tests => tests}/test_data/http/almost_match_boundary_without_CR.yaml (100%) rename {multipart/tests => tests}/test_data/http/almost_match_boundary_without_LF.http (100%) rename {multipart/tests => tests}/test_data/http/almost_match_boundary_without_LF.yaml (100%) rename {multipart/tests => tests}/test_data/http/almost_match_boundary_without_final_hyphen.http (100%) rename {multipart/tests => tests}/test_data/http/almost_match_boundary_without_final_hyphen.yaml (100%) rename {multipart/tests => tests}/test_data/http/bad_end_of_headers.http (100%) rename {multipart/tests => tests}/test_data/http/bad_end_of_headers.yaml (100%) rename {multipart/tests => tests}/test_data/http/bad_header_char.http (100%) rename {multipart/tests => tests}/test_data/http/bad_header_char.yaml (100%) rename {multipart/tests => tests}/test_data/http/bad_initial_boundary.http (100%) rename {multipart/tests => tests}/test_data/http/bad_initial_boundary.yaml (100%) rename {multipart/tests => tests}/test_data/http/base64_encoding.http (100%) rename {multipart/tests => tests}/test_data/http/base64_encoding.yaml (100%) rename {multipart/tests => tests}/test_data/http/empty_header.http (100%) rename {multipart/tests => tests}/test_data/http/empty_header.yaml (100%) rename {multipart/tests => tests}/test_data/http/multiple_fields.http (100%) rename {multipart/tests => tests}/test_data/http/multiple_fields.yaml (100%) rename {multipart/tests => tests}/test_data/http/multiple_files.http (100%) rename {multipart/tests => tests}/test_data/http/multiple_files.yaml (100%) rename {multipart/tests => tests}/test_data/http/quoted_printable_encoding.http (100%) rename {multipart/tests => tests}/test_data/http/quoted_printable_encoding.yaml (100%) rename {multipart/tests => tests}/test_data/http/single_field.http (100%) rename {multipart/tests => tests}/test_data/http/single_field.yaml (100%) rename {multipart/tests => tests}/test_data/http/single_field_blocks.http (100%) rename {multipart/tests => tests}/test_data/http/single_field_blocks.yaml (100%) rename {multipart/tests => tests}/test_data/http/single_field_longer.http (100%) rename {multipart/tests => tests}/test_data/http/single_field_longer.yaml (100%) rename {multipart/tests => tests}/test_data/http/single_field_single_file.http (100%) rename {multipart/tests => tests}/test_data/http/single_field_single_file.yaml (100%) rename {multipart/tests => tests}/test_data/http/single_field_with_leading_newlines.http (100%) rename {multipart/tests => tests}/test_data/http/single_field_with_leading_newlines.yaml (100%) rename {multipart/tests => tests}/test_data/http/single_file.http (100%) rename {multipart/tests => tests}/test_data/http/single_file.yaml (100%) rename {multipart/tests => tests}/test_data/http/utf8_filename.http (100%) rename {multipart/tests => tests}/test_data/http/utf8_filename.yaml (100%) rename {multipart/tests => tests}/test_multipart.py (99%) diff --git a/.coveragerc b/.coveragerc index 0f1b93d..5724e30 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,7 +5,7 @@ branch = False # branch = True omit = - multipart/tests/* + tests/* [report] # Regexes for lines to exclude from consideration diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index d35bfa7..63078f7 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -10,7 +10,7 @@ name: Upload Python Package on: push: - tags: + tags: - '[0-9]+.[0-9]+.[0-9]+.*' # Run on every git tag with semantic versioning. i.e: 1.5.0 or 1.5.0rc1 permissions: diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 864fc99..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include requirements.txt README.md LICENSE.txt -recursive-include multipart *.py *.yaml *.bare *.http LICENSE*.* -recursive-exclude multipart *.pyc *.pyo *.pyd - diff --git a/multipart/__init__.py b/multipart/__init__.py index 309d698..b49d100 100644 --- a/multipart/__init__.py +++ b/multipart/__init__.py @@ -1,15 +1,15 @@ # This is the canonical package information. -__author__ = 'Andrew Dunham' -__license__ = 'Apache' +__author__ = "Andrew Dunham" +__license__ = "Apache" __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" -__version__ = "0.0.6" +__version__ = "0.0.6" from .multipart import ( FormParser, MultipartParser, - QuerystringParser, OctetStreamParser, + QuerystringParser, create_form_parser, parse_form, ) diff --git a/pyproject.toml b/pyproject.toml index 2220432..77deb1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,9 +9,7 @@ description = "A streaming multipart parser for Python" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.7" -authors = [ - { name = "Andrew Dunham", email = "andrew@du.nham.ca" }, -] +authors = [{ name = "Andrew Dunham", email = "andrew@du.nham.ca" }] classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', @@ -56,10 +54,7 @@ Source = "https://github.com/andrew-d/python-multipart" path = "multipart/__init__.py" [tool.hatch.build.targets.wheel] -packages = [ - "multipart", -] +packages = ["multipart"] + [tool.hatch.build.targets.sdist] -include = [ - "/multipart", -] +include = ["/multipart", "/tests"] diff --git a/tasks.py b/tasks.py index 3ac7419..f2cec11 100644 --- a/tasks.py +++ b/tasks.py @@ -28,7 +28,7 @@ def test(ctx, all=False): test_cmd.append('-m "not slow_test"') # Test in this directory - test_cmd.append(os.path.join("multipart", "tests")) + test_cmd.append("tests") # Run the command. # TODO: why does this fail with pty=True? diff --git a/multipart/tests/__init__.py b/tests/__init__.py similarity index 100% rename from multipart/tests/__init__.py rename to tests/__init__.py diff --git a/multipart/tests/compat.py b/tests/compat.py similarity index 100% rename from multipart/tests/compat.py rename to tests/compat.py diff --git a/multipart/tests/test_data/http/CR_in_header.http b/tests/test_data/http/CR_in_header.http similarity index 100% rename from multipart/tests/test_data/http/CR_in_header.http rename to tests/test_data/http/CR_in_header.http diff --git a/multipart/tests/test_data/http/CR_in_header.yaml b/tests/test_data/http/CR_in_header.yaml similarity index 100% rename from multipart/tests/test_data/http/CR_in_header.yaml rename to tests/test_data/http/CR_in_header.yaml diff --git a/multipart/tests/test_data/http/CR_in_header_value.http b/tests/test_data/http/CR_in_header_value.http similarity index 100% rename from multipart/tests/test_data/http/CR_in_header_value.http rename to tests/test_data/http/CR_in_header_value.http diff --git a/multipart/tests/test_data/http/CR_in_header_value.yaml b/tests/test_data/http/CR_in_header_value.yaml similarity index 100% rename from multipart/tests/test_data/http/CR_in_header_value.yaml rename to tests/test_data/http/CR_in_header_value.yaml diff --git a/multipart/tests/test_data/http/almost_match_boundary.http b/tests/test_data/http/almost_match_boundary.http similarity index 100% rename from multipart/tests/test_data/http/almost_match_boundary.http rename to tests/test_data/http/almost_match_boundary.http diff --git a/multipart/tests/test_data/http/almost_match_boundary.yaml b/tests/test_data/http/almost_match_boundary.yaml similarity index 100% rename from multipart/tests/test_data/http/almost_match_boundary.yaml rename to tests/test_data/http/almost_match_boundary.yaml diff --git a/multipart/tests/test_data/http/almost_match_boundary_without_CR.http b/tests/test_data/http/almost_match_boundary_without_CR.http similarity index 100% rename from multipart/tests/test_data/http/almost_match_boundary_without_CR.http rename to tests/test_data/http/almost_match_boundary_without_CR.http diff --git a/multipart/tests/test_data/http/almost_match_boundary_without_CR.yaml b/tests/test_data/http/almost_match_boundary_without_CR.yaml similarity index 100% rename from multipart/tests/test_data/http/almost_match_boundary_without_CR.yaml rename to tests/test_data/http/almost_match_boundary_without_CR.yaml diff --git a/multipart/tests/test_data/http/almost_match_boundary_without_LF.http b/tests/test_data/http/almost_match_boundary_without_LF.http similarity index 100% rename from multipart/tests/test_data/http/almost_match_boundary_without_LF.http rename to tests/test_data/http/almost_match_boundary_without_LF.http diff --git a/multipart/tests/test_data/http/almost_match_boundary_without_LF.yaml b/tests/test_data/http/almost_match_boundary_without_LF.yaml similarity index 100% rename from multipart/tests/test_data/http/almost_match_boundary_without_LF.yaml rename to tests/test_data/http/almost_match_boundary_without_LF.yaml diff --git a/multipart/tests/test_data/http/almost_match_boundary_without_final_hyphen.http b/tests/test_data/http/almost_match_boundary_without_final_hyphen.http similarity index 100% rename from multipart/tests/test_data/http/almost_match_boundary_without_final_hyphen.http rename to tests/test_data/http/almost_match_boundary_without_final_hyphen.http diff --git a/multipart/tests/test_data/http/almost_match_boundary_without_final_hyphen.yaml b/tests/test_data/http/almost_match_boundary_without_final_hyphen.yaml similarity index 100% rename from multipart/tests/test_data/http/almost_match_boundary_without_final_hyphen.yaml rename to tests/test_data/http/almost_match_boundary_without_final_hyphen.yaml diff --git a/multipart/tests/test_data/http/bad_end_of_headers.http b/tests/test_data/http/bad_end_of_headers.http similarity index 100% rename from multipart/tests/test_data/http/bad_end_of_headers.http rename to tests/test_data/http/bad_end_of_headers.http diff --git a/multipart/tests/test_data/http/bad_end_of_headers.yaml b/tests/test_data/http/bad_end_of_headers.yaml similarity index 100% rename from multipart/tests/test_data/http/bad_end_of_headers.yaml rename to tests/test_data/http/bad_end_of_headers.yaml diff --git a/multipart/tests/test_data/http/bad_header_char.http b/tests/test_data/http/bad_header_char.http similarity index 100% rename from multipart/tests/test_data/http/bad_header_char.http rename to tests/test_data/http/bad_header_char.http diff --git a/multipart/tests/test_data/http/bad_header_char.yaml b/tests/test_data/http/bad_header_char.yaml similarity index 100% rename from multipart/tests/test_data/http/bad_header_char.yaml rename to tests/test_data/http/bad_header_char.yaml diff --git a/multipart/tests/test_data/http/bad_initial_boundary.http b/tests/test_data/http/bad_initial_boundary.http similarity index 100% rename from multipart/tests/test_data/http/bad_initial_boundary.http rename to tests/test_data/http/bad_initial_boundary.http diff --git a/multipart/tests/test_data/http/bad_initial_boundary.yaml b/tests/test_data/http/bad_initial_boundary.yaml similarity index 100% rename from multipart/tests/test_data/http/bad_initial_boundary.yaml rename to tests/test_data/http/bad_initial_boundary.yaml diff --git a/multipart/tests/test_data/http/base64_encoding.http b/tests/test_data/http/base64_encoding.http similarity index 100% rename from multipart/tests/test_data/http/base64_encoding.http rename to tests/test_data/http/base64_encoding.http diff --git a/multipart/tests/test_data/http/base64_encoding.yaml b/tests/test_data/http/base64_encoding.yaml similarity index 100% rename from multipart/tests/test_data/http/base64_encoding.yaml rename to tests/test_data/http/base64_encoding.yaml diff --git a/multipart/tests/test_data/http/empty_header.http b/tests/test_data/http/empty_header.http similarity index 100% rename from multipart/tests/test_data/http/empty_header.http rename to tests/test_data/http/empty_header.http diff --git a/multipart/tests/test_data/http/empty_header.yaml b/tests/test_data/http/empty_header.yaml similarity index 100% rename from multipart/tests/test_data/http/empty_header.yaml rename to tests/test_data/http/empty_header.yaml diff --git a/multipart/tests/test_data/http/multiple_fields.http b/tests/test_data/http/multiple_fields.http similarity index 100% rename from multipart/tests/test_data/http/multiple_fields.http rename to tests/test_data/http/multiple_fields.http diff --git a/multipart/tests/test_data/http/multiple_fields.yaml b/tests/test_data/http/multiple_fields.yaml similarity index 100% rename from multipart/tests/test_data/http/multiple_fields.yaml rename to tests/test_data/http/multiple_fields.yaml diff --git a/multipart/tests/test_data/http/multiple_files.http b/tests/test_data/http/multiple_files.http similarity index 100% rename from multipart/tests/test_data/http/multiple_files.http rename to tests/test_data/http/multiple_files.http diff --git a/multipart/tests/test_data/http/multiple_files.yaml b/tests/test_data/http/multiple_files.yaml similarity index 100% rename from multipart/tests/test_data/http/multiple_files.yaml rename to tests/test_data/http/multiple_files.yaml diff --git a/multipart/tests/test_data/http/quoted_printable_encoding.http b/tests/test_data/http/quoted_printable_encoding.http similarity index 100% rename from multipart/tests/test_data/http/quoted_printable_encoding.http rename to tests/test_data/http/quoted_printable_encoding.http diff --git a/multipart/tests/test_data/http/quoted_printable_encoding.yaml b/tests/test_data/http/quoted_printable_encoding.yaml similarity index 100% rename from multipart/tests/test_data/http/quoted_printable_encoding.yaml rename to tests/test_data/http/quoted_printable_encoding.yaml diff --git a/multipart/tests/test_data/http/single_field.http b/tests/test_data/http/single_field.http similarity index 100% rename from multipart/tests/test_data/http/single_field.http rename to tests/test_data/http/single_field.http diff --git a/multipart/tests/test_data/http/single_field.yaml b/tests/test_data/http/single_field.yaml similarity index 100% rename from multipart/tests/test_data/http/single_field.yaml rename to tests/test_data/http/single_field.yaml diff --git a/multipart/tests/test_data/http/single_field_blocks.http b/tests/test_data/http/single_field_blocks.http similarity index 100% rename from multipart/tests/test_data/http/single_field_blocks.http rename to tests/test_data/http/single_field_blocks.http diff --git a/multipart/tests/test_data/http/single_field_blocks.yaml b/tests/test_data/http/single_field_blocks.yaml similarity index 100% rename from multipart/tests/test_data/http/single_field_blocks.yaml rename to tests/test_data/http/single_field_blocks.yaml diff --git a/multipart/tests/test_data/http/single_field_longer.http b/tests/test_data/http/single_field_longer.http similarity index 100% rename from multipart/tests/test_data/http/single_field_longer.http rename to tests/test_data/http/single_field_longer.http diff --git a/multipart/tests/test_data/http/single_field_longer.yaml b/tests/test_data/http/single_field_longer.yaml similarity index 100% rename from multipart/tests/test_data/http/single_field_longer.yaml rename to tests/test_data/http/single_field_longer.yaml diff --git a/multipart/tests/test_data/http/single_field_single_file.http b/tests/test_data/http/single_field_single_file.http similarity index 100% rename from multipart/tests/test_data/http/single_field_single_file.http rename to tests/test_data/http/single_field_single_file.http diff --git a/multipart/tests/test_data/http/single_field_single_file.yaml b/tests/test_data/http/single_field_single_file.yaml similarity index 100% rename from multipart/tests/test_data/http/single_field_single_file.yaml rename to tests/test_data/http/single_field_single_file.yaml diff --git a/multipart/tests/test_data/http/single_field_with_leading_newlines.http b/tests/test_data/http/single_field_with_leading_newlines.http similarity index 100% rename from multipart/tests/test_data/http/single_field_with_leading_newlines.http rename to tests/test_data/http/single_field_with_leading_newlines.http diff --git a/multipart/tests/test_data/http/single_field_with_leading_newlines.yaml b/tests/test_data/http/single_field_with_leading_newlines.yaml similarity index 100% rename from multipart/tests/test_data/http/single_field_with_leading_newlines.yaml rename to tests/test_data/http/single_field_with_leading_newlines.yaml diff --git a/multipart/tests/test_data/http/single_file.http b/tests/test_data/http/single_file.http similarity index 100% rename from multipart/tests/test_data/http/single_file.http rename to tests/test_data/http/single_file.http diff --git a/multipart/tests/test_data/http/single_file.yaml b/tests/test_data/http/single_file.yaml similarity index 100% rename from multipart/tests/test_data/http/single_file.yaml rename to tests/test_data/http/single_file.yaml diff --git a/multipart/tests/test_data/http/utf8_filename.http b/tests/test_data/http/utf8_filename.http similarity index 100% rename from multipart/tests/test_data/http/utf8_filename.http rename to tests/test_data/http/utf8_filename.http diff --git a/multipart/tests/test_data/http/utf8_filename.yaml b/tests/test_data/http/utf8_filename.yaml similarity index 100% rename from multipart/tests/test_data/http/utf8_filename.yaml rename to tests/test_data/http/utf8_filename.yaml diff --git a/multipart/tests/test_multipart.py b/tests/test_multipart.py similarity index 99% rename from multipart/tests/test_multipart.py rename to tests/test_multipart.py index 089f451..031515b 100644 --- a/multipart/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -1,8 +1,6 @@ import os import sys -import glob import yaml -import base64 import random import tempfile import unittest @@ -12,9 +10,9 @@ slow_test, ) from io import BytesIO -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import Mock -from ..multipart import * +from multipart.multipart import * # Get the current directory for our later test cases. diff --git a/tox.ini b/tox.ini index 85d1b54..16f7b05 100644 --- a/tox.ini +++ b/tox.ini @@ -8,4 +8,4 @@ deps= pytest-timeout PyYAML commands= - pytest --cov-report term-missing --cov-config .coveragerc --cov multipart --timeout=30 multipart/tests + pytest --cov-report term-missing --cov-config .coveragerc --cov multipart --timeout=30 tests From 86d422ca6f070f5af8c7ba2cafd218408fa6c58b Mon Sep 17 00:00:00 2001 From: jvstme <36324149+jvstme@users.noreply.github.com> Date: Sun, 1 Oct 2023 07:01:12 +0000 Subject: [PATCH 05/94] Update changelog URL (#68) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 77deb1e..1ad20d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dev = [ [project.urls] Homepage = "https://github.com/andrew-d/python-multipart" Documentation = "https://andrew-d.github.io/python-multipart/" -Changelog = "https://github.com/andrew-d/python-multipart/tags" +Changelog = "https://github.com/andrew-d/python-multipart/blob/master/CHANGELOG.md" Source = "https://github.com/andrew-d/python-multipart" [tool.hatch.version] From 8e59febf3ad42f2b09a42bb06b19cfdb05dd1656 Mon Sep 17 00:00:00 2001 From: eltbus <33374178+eltbus@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:08:24 +0100 Subject: [PATCH 06/94] Use single quotes to avoid special zsh chars '[' and ']' (#71) --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0a7dac4..7bdc864 100644 --- a/README.rst +++ b/README.rst @@ -24,5 +24,5 @@ If you want to test: .. code-block:: bash - $ pip install .[dev] + $ pip install '.[dev]' $ inv test From d3d16dae4b061c34fe9d3c9081d9800c49fc1f7a Mon Sep 17 00:00:00 2001 From: eltbus <33374178+eltbus@users.noreply.github.com> Date: Mon, 22 Jan 2024 11:51:59 +0100 Subject: [PATCH 07/94] Use latest invoke version (2.2.0) (#73) * Use latest invoke version (2.2.0) * Use 'inv test' --- .github/workflows/test.yaml | 4 +--- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 21428c5..21452ec 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -28,6 +28,4 @@ jobs: pip install .[dev] - name: Test with pytest run: | - # This should be inv test but invoke does not have python3.11 support yet. - # See https://github.com/pyinvoke/invoke/issues/891 for details - pytest + inv test diff --git a/pyproject.toml b/pyproject.toml index 1ad20d2..4eea5b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ dev = [ "pytest==7.2.0", "pytest-cov==4.0.0", "PyYAML==5.1", - "invoke==1.7.3", + "invoke==2.2.0", "pytest-timeout==2.1.0", "hatch", ] From 20f0ef6b4e4caf7d69a667c54dff57fe467109a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 3 Feb 2024 12:54:23 +0100 Subject: [PATCH 08/94] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20header=20?= =?UTF-8?q?option=20parser=20to=20use=20the=20standard=20library=20instead?= =?UTF-8?q?=20of=20a=20custom=20RegEx=20(#75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- multipart/multipart.py | 50 ++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/multipart/multipart.py b/multipart/multipart.py index a9f1f9f..e1d10fc 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -9,6 +9,8 @@ import tempfile from io import BytesIO from numbers import Number +from email.message import Message +from typing import Dict, Union, Tuple # Unique missing object. _missing = object() @@ -76,44 +78,44 @@ QUOTE = b'"'[0] -def parse_options_header(value): +def parse_options_header(value: Union[str, bytes]) -> Tuple[bytes, Dict[bytes, bytes]]: """ Parses a Content-Type header into a value in the following format: (content_type, {parameters}) """ + # Uses email.message.Message to parse the header as described in PEP 594. + # Ref: https://peps.python.org/pep-0594/#cgi if not value: return (b'', {}) - # If we are passed a string, we assume that it conforms to WSGI and does - # not contain any code point that's not in latin-1. - if isinstance(value, str): # pragma: no cover - value = value.encode('latin-1') + # If we are passed bytes, we assume that it conforms to WSGI, encoding in latin-1. + if isinstance(value, bytes): # pragma: no cover + value = value.decode('latin-1') + + # For types + assert isinstance(value, str), 'Value should be a string by now' # If we have no options, return the string as-is. - if b';' not in value: - return (value.lower().strip(), {}) + if ";" not in value: + return (value.lower().strip().encode('latin-1'), {}) # Split at the first semicolon, to get our value and then options. - ctype, rest = value.split(b';', 1) + # ctype, rest = value.split(b';', 1) + message = Message() + message['content-type'] = value + params = message.get_params() + # If there were no parameters, this would have already returned above + assert params, 'At least the content type value should be present' + ctype = params.pop(0)[0].encode('latin-1') options = {} - - # Parse the options. - for match in OPTION_RE.finditer(rest): - key = match.group(1).lower() - value = match.group(2) - if value[0] == QUOTE and value[-1] == QUOTE: - # Unquote the value. - value = value[1:-1] - value = value.replace(b'\\\\', b'\\').replace(b'\\"', b'"') - + for param in params: + key, value = param # If the value is a filename, we need to fix a bug on IE6 that sends # the full file path instead of the filename. - if key == b'filename': - if value[1:3] == b':\\' or value[:2] == b'\\\\': - value = value.split(b'\\')[-1] - - options[key] = value - + if key == 'filename': + if value[1:3] == ':\\' or value[:2] == '\\\\': + value = value.split('\\')[-1] + options[key.encode('latin-1')] = value.encode('latin-1') return ctype, options From fb7d3c92dd07d63e6f02a2a2d0350cebdf356e3a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 3 Feb 2024 05:02:34 -0700 Subject: [PATCH 09/94] Bump pygments from 2.7.4 to 2.15.0 (#66) Bumps [pygments](https://github.com/pygments/pygments) from 2.7.4 to 2.15.0. - [Release notes](https://github.com/pygments/pygments/releases) - [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES) - [Commits](https://github.com/pygments/pygments/compare/2.7.4...2.15.0) --- updated-dependencies: - dependency-name: pygments dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs_requirements.txt b/docs_requirements.txt index ef0e956..44993e5 100644 --- a/docs_requirements.txt +++ b/docs_requirements.txt @@ -1,6 +1,6 @@ Jinja2==2.11.3 PyYAML==5.4 -Pygments==2.7.4 +Pygments==2.15.0 Sphinx==1.2b1 cov-core==1.7 coverage==3.6 From c83e6da1a3a6ed002ebb22138baa1664134d540c Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 3 Feb 2024 13:03:47 +0100 Subject: [PATCH 10/94] Version 0.0.7 (#77) --- CHANGELOG.md | 4 ++++ multipart/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7373025..3c77841 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.0.7 (2024-02-03) + +* Refactor header option parser to use the standard library instead of a custom RegEx [#75](https://github.com/andrew-d/python-multipart/pull/75). + ## 0.0.6 (2023-02-27) * Migrate package installation to `pyproject.toml` (PEP 621) [#54](https://github.com/andrew-d/python-multipart/pull/54). diff --git a/multipart/__init__.py b/multipart/__init__.py index b49d100..e8b163a 100644 --- a/multipart/__init__.py +++ b/multipart/__init__.py @@ -2,7 +2,7 @@ __author__ = "Andrew Dunham" __license__ = "Apache" __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" -__version__ = "0.0.6" +__version__ = "0.0.7" from .multipart import ( From 0776bf0428c3cfeca3528c3bb03941034c83f34c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 3 Feb 2024 05:24:02 -0700 Subject: [PATCH 11/94] Bump jinja2 from 2.11.3 to 3.1.3 (#76) Bumps [jinja2](https://github.com/pallets/jinja) from 2.11.3 to 3.1.3. - [Release notes](https://github.com/pallets/jinja/releases) - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/jinja/compare/2.11.3...3.1.3) --- updated-dependencies: - dependency-name: jinja2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs_requirements.txt b/docs_requirements.txt index 44993e5..fccc00d 100644 --- a/docs_requirements.txt +++ b/docs_requirements.txt @@ -1,4 +1,4 @@ -Jinja2==2.11.3 +Jinja2==3.1.3 PyYAML==5.4 Pygments==2.15.0 Sphinx==1.2b1 From c2eea36cc8fc3fc4b53e3b4f60dbc433601c4e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 9 Feb 2024 17:25:22 +0100 Subject: [PATCH 12/94] =?UTF-8?q?=E2=9C=85=20Add=20ReDos=20check=20test=20?= =?UTF-8?q?(#81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_multipart.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 031515b..2ceecab 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -270,6 +270,11 @@ def test_handles_ie6_bug(self): t, p = parse_options_header(b'text/plain; filename="C:\\this\\is\\a\\path\\file.txt"') self.assertEqual(p[b'filename'], b'file.txt') + + def test_redos_attack_header(self): + t, p = parse_options_header(b'application/x-www-form-urlencoded; !="\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\') + # If vulnerable, this test wouldn't finish, the line above would hang + self.assertIn(b'"\\', p[b'!']) class TestBaseParser(unittest.TestCase): From cb0b7cf60e3e3c9f5854686d7725960ff77b7301 Mon Sep 17 00:00:00 2001 From: Konstantinos Tselepakis Date: Fri, 9 Feb 2024 23:44:31 +0200 Subject: [PATCH 13/94] Cleanup unused regex patterns (#82) --- multipart/multipart.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/multipart/multipart.py b/multipart/multipart.py index e1d10fc..6ec1364 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -2,7 +2,6 @@ from .exceptions import * import os -import re import sys import shutil import logging @@ -67,16 +66,6 @@ ord_char = lambda c: c join_bytes = lambda b: bytes(list(b)) -# These are regexes for parsing header values. -SPECIAL_CHARS = re.escape(b'()<>@,;:\\"/[]?={} \t') -QUOTED_STR = br'"(?:\\.|[^"])*"' -VALUE_STR = br'(?:[^' + SPECIAL_CHARS + br']+|' + QUOTED_STR + br')' -OPTION_RE_STR = ( - br'(?:;|^)\s*([^' + SPECIAL_CHARS + br']+)\s*=\s*(' + VALUE_STR + br')' -) -OPTION_RE = re.compile(OPTION_RE_STR) -QUOTE = b'"'[0] - def parse_options_header(value: Union[str, bytes]) -> Tuple[bytes, Dict[bytes, bytes]]: """ From 26d664f277a330bb63ee03820d8c2f6f1bbf951a Mon Sep 17 00:00:00 2001 From: lorenpike <55057781+lorenpike@users.noreply.github.com> Date: Fri, 9 Feb 2024 16:45:33 -0500 Subject: [PATCH 14/94] Check if `Message.get_params` return 3-`tuple` instead of `str` on `parse_options_header` (#79) * Fixing issue #78 * Added test for parse_options_header * Added a comment clarifying a condition in parse_options_header * Fixed a typo --------- Co-authored-by: lorenpike --- multipart/multipart.py | 5 +++++ tests/test_multipart.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/multipart/multipart.py b/multipart/multipart.py index 6ec1364..73910da 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -99,6 +99,11 @@ def parse_options_header(value: Union[str, bytes]) -> Tuple[bytes, Dict[bytes, b options = {} for param in params: key, value = param + # If the value returned from get_params() is a 3-tuple, the last + # element corresponds to the value. + # See: https://docs.python.org/3/library/email.compat32-message.html + if isinstance(value, tuple): + value = value[-1] # If the value is a filename, we need to fix a bug on IE6 that sends # the full file path instead of the filename. if key == 'filename': diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 2ceecab..5cfacf4 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -276,6 +276,11 @@ def test_redos_attack_header(self): # If vulnerable, this test wouldn't finish, the line above would hang self.assertIn(b'"\\', p[b'!']) + def test_handles_rfc_2231(self): + t, p = parse_options_header(b'text/plain; param*=us-ascii\'en-us\'encoded%20message') + + self.assertEqual(p[b'param'], b'encoded message') + class TestBaseParser(unittest.TestCase): def setUp(self): From 8ce342cd9ac03fe238c24d68cffaf25a7ea0371a Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Fri, 9 Feb 2024 22:52:41 +0100 Subject: [PATCH 15/94] Version 0.0.8 (#83) --- CHANGELOG.md | 5 +++++ multipart/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c77841..3d6f080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.0.8 (2024-02-09) + +* Check if Message.get_params return 3-tuple instead of str on parse_options_header [#79](https://github.com/Kludex/python-multipart/pull/79). +* Cleanup unused regex patterns [#82](https://github.com/Kludex/python-multipart/pull/82). + ## 0.0.7 (2024-02-03) * Refactor header option parser to use the standard library instead of a custom RegEx [#75](https://github.com/andrew-d/python-multipart/pull/75). diff --git a/multipart/__init__.py b/multipart/__init__.py index e8b163a..28c7ad6 100644 --- a/multipart/__init__.py +++ b/multipart/__init__.py @@ -2,7 +2,7 @@ __author__ = "Andrew Dunham" __license__ = "Apache" __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" -__version__ = "0.0.7" +__version__ = "0.0.8" from .multipart import ( From 1fa5843bea591acc7e41e790ffbac5e1f9f48fa2 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Feb 2024 12:13:27 +0100 Subject: [PATCH 16/94] Add support for Python 3.12 (#85) --- .github/workflows/test.yaml | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 21452ec..49bb7fa 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 diff --git a/pyproject.toml b/pyproject.toml index 4eea5b6..76cbb1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ classifiers = [ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Topic :: Software Development :: Libraries :: Python Modules', ] dependencies = [] From c83b0aaf67905a9c9ca5d1eb3063c58802e25a24 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Feb 2024 12:20:49 +0100 Subject: [PATCH 17/94] Add missing py312 environment to tox (#86) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 16f7b05..8dd469d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,py310,py311 +envlist = py37,py38,py39,py310,py311,py312 [testenv] deps= From f2bc0a1dd16f7bf499fa6a5f7e5e5c2e0d6202d2 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Feb 2024 12:22:23 +0100 Subject: [PATCH 18/94] Run publish on every tag pushed (#87) --- .github/workflows/publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 63078f7..f070a71 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -11,7 +11,7 @@ name: Upload Python Package on: push: tags: - - '[0-9]+.[0-9]+.[0-9]+.*' # Run on every git tag with semantic versioning. i.e: 1.5.0 or 1.5.0rc1 + - '*' permissions: contents: read From b8a35417b53c8b82b53cec5ad525a1e24a72e881 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Feb 2024 12:47:13 +0100 Subject: [PATCH 19/94] Add dependabot (#88) --- .github/dependabot.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2e58137 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" + groups: + python-packages: + patterns: + - "*" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: monthly From 3a13f0bff778457c179a827a2cf1f458c610a35a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 10 Feb 2024 04:48:27 -0700 Subject: [PATCH 20/94] Bump actions/setup-python from 4 to 5 (#91) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yaml | 2 +- .github/workflows/test.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index f070a71..3a9c54b 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 49bb7fa..f85d042 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 26e2de2b254193524261d5922168a334ca5c43bc Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Feb 2024 12:51:50 +0100 Subject: [PATCH 21/94] Group GitHub acitons on dependabot (#93) --- .github/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2e58137..cf07f01 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,3 +12,7 @@ updates: directory: "/" schedule: interval: monthly + groups: + github-actions: + patterns: + - "*" From 66aa2d58d5c4a52cb6c1c7c5f18068ae6193cd56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 10 Feb 2024 04:53:31 -0700 Subject: [PATCH 22/94] Bump the github-actions group with 2 updates (#94) Bumps the github-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish). Updates `actions/checkout` from 3 to 4 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) Updates `pypa/gh-action-pypi-publish` from 1.6.4 to 1.8.11 - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.6.4...v1.8.11) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yaml | 4 ++-- .github/workflows/test.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 3a9c54b..221b67f 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python 3.10 uses: actions/setup-python@v5 @@ -38,7 +38,7 @@ jobs: run: python -m build - name: Publish package - uses: pypa/gh-action-pypi-publish@v1.6.4 + uses: pypa/gh-action-pypi-publish@v1.8.11 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f85d042..377762f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -17,7 +17,7 @@ jobs: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: From d1ddb052d9341a19da4b8e066f9b5ab42085c3e3 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Feb 2024 12:53:51 +0100 Subject: [PATCH 23/94] Drop support for Python 3.7 (#95) --- .github/workflows/test.yaml | 2 +- pyproject.toml | 3 +-- tox.ini | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 377762f..7448d91 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 diff --git a/pyproject.toml b/pyproject.toml index 76cbb1d..62db4ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] description = "A streaming multipart parser for Python" readme = "README.rst" license = "Apache-2.0" -requires-python = ">=3.7" +requires-python = ">=3.8" authors = [{ name = "Andrew Dunham", email = "andrew@du.nham.ca" }] classifiers = [ 'Development Status :: 5 - Production/Stable', @@ -18,7 +18,6 @@ classifiers = [ 'Operating System :: OS Independent', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', diff --git a/tox.ini b/tox.ini index 8dd469d..abf6e29 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,py310,py311,py312 +envlist = py38,py39,py310,py311,py312 [testenv] deps= From 7f9980626b309218ff400c66d84648439da66cd8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 10 Feb 2024 04:54:53 -0700 Subject: [PATCH 24/94] Bump the python-packages group with 20 updates (#92) Bumps the python-packages group with 20 updates: | Package | From | To | | --- | --- | --- | | [atomicwrites](https://github.com/untitaker/python-atomicwrites) | `1.2.1` | `1.4.1` | | [attrs](https://github.com/sponsors/hynek) | `19.2.0` | `23.2.0` | | [coverage](https://github.com/nedbat/coveragepy) | `3.6` | `7.4.1` | | [more-itertools](https://github.com/more-itertools/more-itertools) | `4.3.0` | `10.2.0` | | [pbr](https://docs.openstack.org/pbr/latest/) | `4.3.0` | `6.0.0` | | [pluggy](https://github.com/pytest-dev/pluggy) | `1.0.0` | `1.4.0` | | [py](https://github.com/pytest-dev/py) | `1.10.0` | `1.11.0` | | [pytest](https://github.com/pytest-dev/pytest) | `6.2.6` | `8.0.0` | | [pytest-cov](https://github.com/pytest-dev/pytest-cov) | `1.6` | `4.1.0` | | [pyyaml](https://github.com/yaml/pyyaml) | `5.1` | `6.0.1` | | [invoke](https://github.com/pyinvoke/invoke) | `0.2.0` | `2.2.0` | | [pytest-timeout](https://github.com/pytest-dev/pytest-timeout) | `0.3` | `2.2.0` | | [pygments](https://github.com/pygments/pygments) | `2.15.0` | `2.17.2` | | [sphinx](https://github.com/sphinx-doc/sphinx) | `1.2b1` | `7.2.6` | | [cov-core](https://github.com/schlamar/cov-core) | `1.7` | `1.15.0` | | [distribute](http://packages.python.org/distribute) | `0.6.34` | `0.7.3` | | [docutils](https://docutils.sourceforge.io/) | `0.10` | `0.20.1` | | [sphinx-bootstrap-theme](https://github.com/ryan-roemer/sphinx-bootstrap-theme) | `0.2.0` | `0.8.1` | | [tox](https://github.com/tox-dev/tox) | `1.4.3` | `4.12.1` | | [virtualenv](https://github.com/pypa/virtualenv) | `1.9.1` | `20.25.0` | Updates `atomicwrites` from 1.2.1 to 1.4.1 - [Commits](https://github.com/untitaker/python-atomicwrites/compare/1.2.1...1.4.1) Updates `attrs` from 19.2.0 to 23.2.0 - [Commits](https://github.com/sponsors/hynek/commits) Updates `coverage` from 3.6 to 7.4.1 - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/coverage-3.6...7.4.1) Updates `more-itertools` from 4.3.0 to 10.2.0 - [Release notes](https://github.com/more-itertools/more-itertools/releases) - [Commits](https://github.com/more-itertools/more-itertools/compare/4.3.0...v10.2.0) Updates `pbr` from 4.3.0 to 6.0.0 Updates `pluggy` from 1.0.0 to 1.4.0 - [Changelog](https://github.com/pytest-dev/pluggy/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pluggy/compare/1.0.0...1.4.0) Updates `py` from 1.10.0 to 1.11.0 - [Changelog](https://github.com/pytest-dev/py/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/py/compare/1.10.0...1.11.0) Updates `pytest` from 6.2.6 to 8.0.0 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/commits/8.0.0) Updates `pytest-cov` from 1.6 to 4.1.0 - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v1.6...v4.1.0) Updates `pyyaml` from 5.1 to 6.0.1 - [Changelog](https://github.com/yaml/pyyaml/blob/main/CHANGES) - [Commits](https://github.com/yaml/pyyaml/compare/5.1...6.0.1) Updates `invoke` from 0.2.0 to 2.2.0 - [Commits](https://github.com/pyinvoke/invoke/compare/0.2.0...2.2.0) Updates `pytest-timeout` from 0.3 to 2.2.0 - [Commits](https://github.com/pytest-dev/pytest-timeout/compare/0.3...2.2.0) Updates `pygments` from 2.15.0 to 2.17.2 - [Release notes](https://github.com/pygments/pygments/releases) - [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES) - [Commits](https://github.com/pygments/pygments/compare/2.15.0...2.17.2) Updates `sphinx` from 1.2b1 to 7.2.6 - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst) - [Commits](https://github.com/sphinx-doc/sphinx/commits/v7.2.6) Updates `cov-core` from 1.7 to 1.15.0 - [Commits](https://github.com/schlamar/cov-core/compare/v1.7...v1.15.0) Updates `distribute` from 0.6.34 to 0.7.3 Updates `docutils` from 0.10 to 0.20.1 Updates `sphinx-bootstrap-theme` from 0.2.0 to 0.8.1 - [Changelog](https://github.com/ryan-roemer/sphinx-bootstrap-theme/blob/master/HISTORY.rst) - [Commits](https://github.com/ryan-roemer/sphinx-bootstrap-theme/compare/v0.2.0...v0.8.1) Updates `tox` from 1.4.3 to 4.12.1 - [Release notes](https://github.com/tox-dev/tox/releases) - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/tox/compare/1.4.3...4.12.1) Updates `virtualenv` from 1.9.1 to 20.25.0 - [Release notes](https://github.com/pypa/virtualenv/releases) - [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst) - [Commits](https://github.com/pypa/virtualenv/compare/1.9.1...20.25.0) --- updated-dependencies: - dependency-name: atomicwrites dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: attrs dependency-type: direct:production update-type: version-update:semver-major dependency-group: python-packages - dependency-name: coverage dependency-type: direct:production update-type: version-update:semver-major dependency-group: python-packages - dependency-name: more-itertools dependency-type: direct:production update-type: version-update:semver-major dependency-group: python-packages - dependency-name: pbr dependency-type: direct:production update-type: version-update:semver-major dependency-group: python-packages - dependency-name: pluggy dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: py dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-major dependency-group: python-packages - dependency-name: pytest-cov dependency-type: direct:production update-type: version-update:semver-major dependency-group: python-packages - dependency-name: pyyaml dependency-type: direct:production update-type: version-update:semver-major dependency-group: python-packages - dependency-name: invoke dependency-type: direct:production update-type: version-update:semver-major dependency-group: python-packages - dependency-name: pytest-timeout dependency-type: direct:production update-type: version-update:semver-major dependency-group: python-packages - dependency-name: pygments dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: sphinx dependency-type: direct:production dependency-group: python-packages - dependency-name: cov-core dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: distribute dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: docutils dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: sphinx-bootstrap-theme dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: tox dependency-type: direct:production update-type: version-update:semver-major dependency-group: python-packages - dependency-name: virtualenv dependency-type: direct:production update-type: version-update:semver-major dependency-group: python-packages ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs_requirements.txt | 30 +++++++++++++++--------------- pyproject.toml | 20 ++++++++++---------- requirements.txt | 16 ++++++++-------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/docs_requirements.txt b/docs_requirements.txt index fccc00d..8c3eda5 100644 --- a/docs_requirements.txt +++ b/docs_requirements.txt @@ -1,19 +1,19 @@ Jinja2==3.1.3 -PyYAML==5.4 -Pygments==2.15.0 -Sphinx==1.2b1 -cov-core==1.7 -coverage==3.6 -distribute==0.6.34 -docutils==0.10 -invoke==0.2.0 +PyYAML==6.0.1 +Pygments==2.17.2 +Sphinx==7.2.6 +cov-core==1.15.0 +coverage==7.4.1 +distribute==0.7.3 +docutils==0.20.1 +invoke==2.2.0 pexpect-u==2.5.1 -py==1.10.0 -pytest==6.2.6 +py==1.11.0 +pytest==8.0.0 pytest-capturelog==0.7 -pytest-cov==1.6 -pytest-timeout==0.3 -sphinx-bootstrap-theme==0.2.0 -tox==1.4.3 -virtualenv==1.9.1 +pytest-cov==4.1.0 +pytest-timeout==2.2.0 +sphinx-bootstrap-theme==0.8.1 +tox==4.12.1 +virtualenv==20.25.0 wsgiref==0.1.2 diff --git a/pyproject.toml b/pyproject.toml index 62db4ce..085ab6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,18 +29,18 @@ dependencies = [] [project.optional-dependencies] dev = [ - "atomicwrites==1.2.1", - "attrs==19.2.0", - "coverage==6.5.0", - "more-itertools==4.3.0", - "pbr==4.3.0", - "pluggy==1.0.0", + "atomicwrites==1.4.1", + "attrs==23.2.0", + "coverage==7.4.1", + "more-itertools==10.2.0", + "pbr==6.0.0", + "pluggy==1.4.0", "py==1.11.0", - "pytest==7.2.0", - "pytest-cov==4.0.0", - "PyYAML==5.1", + "pytest==8.0.0", + "pytest-cov==4.1.0", + "PyYAML==6.0.1", "invoke==2.2.0", - "pytest-timeout==2.1.0", + "pytest-timeout==2.2.0", "hatch", ] diff --git a/requirements.txt b/requirements.txt index ad6bb55..9622472 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ -atomicwrites==1.2.1 -attrs==19.2.0 -coverage==4.5.1 -more-itertools==4.3.0 -pbr==4.3.0 -pluggy==1.0.0 +atomicwrites==1.4.1 +attrs==23.2.0 +coverage==7.4.1 +more-itertools==10.2.0 +pbr==6.0.0 +pluggy==1.4.0 py==1.11.0 -pytest==7.2.0 -PyYAML==5.4 +pytest==8.0.0 +PyYAML==6.0.1 From 4e24a3e6bf7cd9453289a27cc64a0eb18271f82f Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Feb 2024 13:20:06 +0100 Subject: [PATCH 25/94] Add ruff and `MultipartState(IntEnum)` (#96) * Several improvements * Add ruff to the pipeline * Use single quotes * Use double quotes * nitpicks * Add ruff to dev optional --- .github/workflows/{test.yaml => main.yaml} | 4 + .github/workflows/publish.yaml | 2 - multipart/__init__.py | 15 +- multipart/decoders.py | 28 +- multipart/exceptions.py | 20 +- multipart/multipart.py | 581 +++++++++---------- pyproject.toml | 13 + requirements.txt | 1 + tests/compat.py | 26 +- tests/test_multipart.py | 644 ++++++++++----------- 10 files changed, 637 insertions(+), 697 deletions(-) rename .github/workflows/{test.yaml => main.yaml} (85%) diff --git a/.github/workflows/test.yaml b/.github/workflows/main.yaml similarity index 85% rename from .github/workflows/test.yaml rename to .github/workflows/main.yaml index 7448d91..7b4fd5c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/main.yaml @@ -26,6 +26,10 @@ jobs: run: | python -m pip install --upgrade pip pip install .[dev] + - name: Lint + if: matrix.python-version == '3.8' + run: | + ruff multipart tests - name: Test with pytest run: | inv test diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 221b67f..cc38611 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -18,12 +18,10 @@ permissions: jobs: deploy: - runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python 3.10 uses: actions/setup-python@v5 with: diff --git a/multipart/__init__.py b/multipart/__init__.py index 28c7ad6..3c8a2e8 100644 --- a/multipart/__init__.py +++ b/multipart/__init__.py @@ -4,12 +4,13 @@ __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" __version__ = "0.0.8" +from .multipart import FormParser, MultipartParser, OctetStreamParser, QuerystringParser, create_form_parser, parse_form -from .multipart import ( - FormParser, - MultipartParser, - OctetStreamParser, - QuerystringParser, - create_form_parser, - parse_form, +__all__ = ( + "FormParser", + "MultipartParser", + "OctetStreamParser", + "QuerystringParser", + "create_form_parser", + "parse_form", ) diff --git a/multipart/decoders.py b/multipart/decoders.py index 0d7ab32..417650c 100644 --- a/multipart/decoders.py +++ b/multipart/decoders.py @@ -59,8 +59,7 @@ def write(self, data): try: decoded = base64.b64decode(val) except binascii.Error: - raise DecodeError('There was an error raised while decoding ' - 'base64-encoded data.') + raise DecodeError("There was an error raised while decoding base64-encoded data.") self.underlying.write(decoded) @@ -69,7 +68,7 @@ def write(self, data): if remaining_len > 0: self.cache = data[-remaining_len:] else: - self.cache = b'' + self.cache = b"" # Return the length of the data to indicate no error. return len(data) @@ -78,7 +77,7 @@ def close(self): """Close this decoder. If the underlying object has a `close()` method, this function will call it. """ - if hasattr(self.underlying, 'close'): + if hasattr(self.underlying, "close"): self.underlying.close() def finalize(self): @@ -91,11 +90,11 @@ def finalize(self): call it. """ if len(self.cache) > 0: - raise DecodeError('There are %d bytes remaining in the ' - 'Base64Decoder cache when finalize() is called' - % len(self.cache)) + raise DecodeError( + "There are %d bytes remaining in the Base64Decoder cache when finalize() is called" % len(self.cache) + ) - if hasattr(self.underlying, 'finalize'): + if hasattr(self.underlying, "finalize"): self.underlying.finalize() def __repr__(self): @@ -111,8 +110,9 @@ class QuotedPrintableDecoder: :param underlying: the underlying object to pass writes to """ + def __init__(self, underlying): - self.cache = b'' + self.cache = b"" self.underlying = underlying def write(self, data): @@ -128,11 +128,11 @@ def write(self, data): # If the last 2 characters have an '=' sign in it, then we won't be # able to decode the encoded value and we'll need to save it for the # next decoding step. - if data[-2:].find(b'=') != -1: + if data[-2:].find(b"=") != -1: enc, rest = data[:-2], data[-2:] else: enc = data - rest = b'' + rest = b"" # Encode and write, if we have data. if len(enc) > 0: @@ -146,7 +146,7 @@ def close(self): """Close this decoder. If the underlying object has a `close()` method, this function will call it. """ - if hasattr(self.underlying, 'close'): + if hasattr(self.underlying, "close"): self.underlying.close() def finalize(self): @@ -161,10 +161,10 @@ def finalize(self): # If we have a cache, write and then remove it. if len(self.cache) > 0: self.underlying.write(binascii.a2b_qp(self.cache)) - self.cache = b'' + self.cache = b"" # Finalize our underlying stream. - if hasattr(self.underlying, 'finalize'): + if hasattr(self.underlying, "finalize"): self.underlying.finalize() def __repr__(self): diff --git a/multipart/exceptions.py b/multipart/exceptions.py index 016e7f7..cc3671f 100644 --- a/multipart/exceptions.py +++ b/multipart/exceptions.py @@ -1,6 +1,5 @@ class FormParserError(ValueError): """Base error class for our form parser.""" - pass class ParseError(FormParserError): @@ -17,30 +16,19 @@ class MultipartParseError(ParseError): """This is a specific error that is raised when the MultipartParser detects an error while parsing. """ - pass class QuerystringParseError(ParseError): """This is a specific error that is raised when the QuerystringParser detects an error while parsing. """ - pass class DecodeError(ParseError): """This exception is raised when there is a decoding error - for example with the Base64Decoder or QuotedPrintableDecoder. """ - pass - - -# On Python 3.3, IOError is the same as OSError, so we don't want to inherit -# from both of them. We handle this case below. -if IOError is not OSError: # pragma: no cover - class FileError(FormParserError, IOError, OSError): - """Exception class for problems with the File class.""" - pass -else: # pragma: no cover - class FileError(FormParserError, OSError): - """Exception class for problems with the File class.""" - pass + + +class FileError(FormParserError, OSError): + """Exception class for problems with the File class.""" diff --git a/multipart/multipart.py b/multipart/multipart.py index 73910da..a427f14 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -1,70 +1,82 @@ -from .decoders import * -from .exceptions import * +from __future__ import annotations +import logging import os -import sys import shutil -import logging +import sys import tempfile +from email.message import Message +from enum import IntEnum from io import BytesIO from numbers import Number -from email.message import Message -from typing import Dict, Union, Tuple +from typing import Dict, Tuple, Union + +from .decoders import Base64Decoder, QuotedPrintableDecoder +from .exceptions import FileError, FormParserError, MultipartParseError, QuerystringParseError # Unique missing object. _missing = object() # States for the querystring parser. STATE_BEFORE_FIELD = 0 -STATE_FIELD_NAME = 1 -STATE_FIELD_DATA = 2 - -# States for the multipart parser -STATE_START = 0 -STATE_START_BOUNDARY = 1 -STATE_HEADER_FIELD_START = 2 -STATE_HEADER_FIELD = 3 -STATE_HEADER_VALUE_START = 4 -STATE_HEADER_VALUE = 5 -STATE_HEADER_VALUE_ALMOST_DONE = 6 -STATE_HEADERS_ALMOST_DONE = 7 -STATE_PART_DATA_START = 8 -STATE_PART_DATA = 9 -STATE_PART_DATA_END = 10 -STATE_END = 11 - -STATES = [ - "START", - "START_BOUNDARY", "HEADER_FIELD_START", "HEADER_FIELD", "HEADER_VALUE_START", "HEADER_VALUE", - "HEADER_VALUE_ALMOST_DONE", "HEADRES_ALMOST_DONE", "PART_DATA_START", "PART_DATA", "PART_DATA_END", "END" -] +STATE_FIELD_NAME = 1 +STATE_FIELD_DATA = 2 + + +class MultipartState(IntEnum): + """Multipart parser states. + + These are used to keep track of the state of the parser, and are used to determine + what to do when new data is encountered. + """ + + START = 0 + START_BOUNDARY = 1 + HEADER_FIELD_START = 2 + HEADER_FIELD = 3 + HEADER_VALUE_START = 4 + HEADER_VALUE = 5 + HEADER_VALUE_ALMOST_DONE = 6 + HEADERS_ALMOST_DONE = 7 + PART_DATA_START = 8 + PART_DATA = 9 + PART_DATA_END = 10 + END = 11 # Flags for the multipart parser. -FLAG_PART_BOUNDARY = 1 -FLAG_LAST_BOUNDARY = 2 +FLAG_PART_BOUNDARY = 1 +FLAG_LAST_BOUNDARY = 2 # Get constants. Since iterating over a str on Python 2 gives you a 1-length # string, but iterating over a bytes object on Python 3 gives you an integer, # we need to save these constants. -CR = b'\r'[0] -LF = b'\n'[0] -COLON = b':'[0] -SPACE = b' '[0] -HYPHEN = b'-'[0] -AMPERSAND = b'&'[0] -SEMICOLON = b';'[0] -LOWER_A = b'a'[0] -LOWER_Z = b'z'[0] -NULL = b'\x00'[0] +CR = b"\r"[0] +LF = b"\n"[0] +COLON = b":"[0] +SPACE = b" "[0] +HYPHEN = b"-"[0] +AMPERSAND = b"&"[0] +SEMICOLON = b";"[0] +LOWER_A = b"a"[0] +LOWER_Z = b"z"[0] +NULL = b"\x00"[0] + # Lower-casing a character is different, because of the difference between # str on Py2, and bytes on Py3. Same with getting the ordinal value of a byte, # and joining a list of bytes together. # These functions abstract that. -lower_char = lambda c: c | 0x20 -ord_char = lambda c: c -join_bytes = lambda b: bytes(list(b)) +def lower_char(c): + return c | 0x20 + + +def ord_char(c): + return c + + +def join_bytes(b): + return bytes(list(b)) def parse_options_header(value: Union[str, bytes]) -> Tuple[bytes, Dict[bytes, bytes]]: @@ -75,27 +87,27 @@ def parse_options_header(value: Union[str, bytes]) -> Tuple[bytes, Dict[bytes, b # Uses email.message.Message to parse the header as described in PEP 594. # Ref: https://peps.python.org/pep-0594/#cgi if not value: - return (b'', {}) + return (b"", {}) # If we are passed bytes, we assume that it conforms to WSGI, encoding in latin-1. if isinstance(value, bytes): # pragma: no cover - value = value.decode('latin-1') + value = value.decode("latin-1") # For types - assert isinstance(value, str), 'Value should be a string by now' + assert isinstance(value, str), "Value should be a string by now" # If we have no options, return the string as-is. if ";" not in value: - return (value.lower().strip().encode('latin-1'), {}) + return (value.lower().strip().encode("latin-1"), {}) # Split at the first semicolon, to get our value and then options. # ctype, rest = value.split(b';', 1) message = Message() - message['content-type'] = value + message["content-type"] = value params = message.get_params() # If there were no parameters, this would have already returned above - assert params, 'At least the content type value should be present' - ctype = params.pop(0)[0].encode('latin-1') + assert params, "At least the content type value should be present" + ctype = params.pop(0)[0].encode("latin-1") options = {} for param in params: key, value = param @@ -106,10 +118,10 @@ def parse_options_header(value: Union[str, bytes]) -> Tuple[bytes, Dict[bytes, b value = value[-1] # If the value is a filename, we need to fix a bug on IE6 that sends # the full file path instead of the filename. - if key == 'filename': - if value[1:3] == ':\\' or value[:2] == '\\\\': - value = value.split('\\')[-1] - options[key.encode('latin-1')] = value.encode('latin-1') + if key == "filename": + if value[1:3] == ":\\" or value[:2] == "\\\\": + value = value.split("\\")[-1] + options[key.encode("latin-1")] = value.encode("latin-1") return ctype, options @@ -128,6 +140,7 @@ class Field: :param name: the name of the form field """ + def __init__(self, name): self._name = name self._value = [] @@ -172,22 +185,19 @@ def on_data(self, data): return len(data) def on_end(self): - """This method is called whenever the Field is finalized. - """ + """This method is called whenever the Field is finalized.""" if self._cache is _missing: - self._cache = b''.join(self._value) + self._cache = b"".join(self._value) def finalize(self): - """Finalize the form field. - """ + """Finalize the form field.""" self.on_end() def close(self): - """Close the Field object. This will free any underlying cache. - """ + """Close the Field object. This will free any underlying cache.""" # Free our value array. if self._cache is _missing: - self._cache = b''.join(self._value) + self._cache = b"".join(self._value) del self._value @@ -209,16 +219,13 @@ def field_name(self): def value(self): """This property returns the value of the form field.""" if self._cache is _missing: - self._cache = b''.join(self._value) + self._cache = b"".join(self._value) return self._cache def __eq__(self, other): if isinstance(other, Field): - return ( - self.field_name == other.field_name and - self.value == other.value - ) + return self.field_name == other.field_name and self.value == other.value else: return NotImplemented @@ -230,11 +237,7 @@ def __repr__(self): else: v = repr(self.value) - return "{}(field_name={!r}, value={})".format( - self.__class__.__name__, - self.field_name, - v - ) + return "{}(field_name={!r}, value={})".format(self.__class__.__name__, self.field_name, v) class File: @@ -296,6 +299,7 @@ class File: :param config: The configuration for this File. See above for valid configuration keys and their corresponding values. """ + def __init__(self, file_name, field_name=None, config={}): # Save configuration, set other variables default. self.logger = logging.getLogger(__name__) @@ -327,8 +331,7 @@ def field_name(self): @property def file_name(self): - """The file name given in the upload request. - """ + """The file name given in the upload request.""" return self._file_name @property @@ -369,9 +372,7 @@ def flush_to_disk(self): warning will be logged to this module's logger. """ if not self._in_memory: - self.logger.warning( - "Trying to flush to disk when we're not in memory" - ) + self.logger.warning("Trying to flush to disk when we're not in memory") return # Go back to the start of our file. @@ -397,14 +398,13 @@ def flush_to_disk(self): old_fileobj.close() def _get_disk_file(self): - """This function is responsible for getting a file object on-disk for us. - """ + """This function is responsible for getting a file object on-disk for us.""" self.logger.info("Opening a file on disk") - file_dir = self._config.get('UPLOAD_DIR') - keep_filename = self._config.get('UPLOAD_KEEP_FILENAME', False) - keep_extensions = self._config.get('UPLOAD_KEEP_EXTENSIONS', False) - delete_tmp = self._config.get('UPLOAD_DELETE_TMP', True) + file_dir = self._config.get("UPLOAD_DIR") + keep_filename = self._config.get("UPLOAD_KEEP_FILENAME", False) + keep_extensions = self._config.get("UPLOAD_KEEP_EXTENSIONS", False) + delete_tmp = self._config.get("UPLOAD_DELETE_TMP", True) # If we have a directory and are to keep the filename... if file_dir is not None and keep_filename: @@ -419,8 +419,8 @@ def _get_disk_file(self): path = os.path.join(file_dir, fname) try: self.logger.info("Opening file: %r", path) - tmp_file = open(path, 'w+b') - except OSError as e: + tmp_file = open(path, "w+b") + except OSError: tmp_file = None self.logger.exception("Error opening temporary file") @@ -435,18 +435,17 @@ def _get_disk_file(self): if isinstance(ext, bytes): ext = ext.decode(sys.getfilesystemencoding()) - options['suffix'] = ext + options["suffix"] = ext if file_dir is not None: d = file_dir if isinstance(d, bytes): d = d.decode(sys.getfilesystemencoding()) - options['dir'] = d - options['delete'] = delete_tmp + options["dir"] = d + options["delete"] = delete_tmp # Create a temporary (named) file with the appropriate settings. - self.logger.info("Creating a temporary file with options: %r", - options) + self.logger.info("Creating a temporary file with options: %r", options) try: tmp_file = tempfile.NamedTemporaryFile(**options) except OSError: @@ -483,18 +482,18 @@ def on_data(self, data): # If the bytes written isn't the same as the length, just return. if bwritten != len(data): - self.logger.warning("bwritten != len(data) (%d != %d)", bwritten, - len(data)) + self.logger.warning("bwritten != len(data) (%d != %d)", bwritten, len(data)) return bwritten # Keep track of how many bytes we've written. self._bytes_written += bwritten # If we're in-memory and are over our limit, we create a file. - if (self._in_memory and - self._config.get('MAX_MEMORY_FILE_SIZE') is not None and - (self._bytes_written > - self._config.get('MAX_MEMORY_FILE_SIZE'))): + if ( + self._in_memory + and self._config.get("MAX_MEMORY_FILE_SIZE") is not None + and (self._bytes_written > self._config.get("MAX_MEMORY_FILE_SIZE")) + ): self.logger.info("Flushing to disk") self.flush_to_disk() @@ -502,8 +501,7 @@ def on_data(self, data): return bwritten def on_end(self): - """This method is called whenever the Field is finalized. - """ + """This method is called whenever the Field is finalized.""" # Flush the underlying file object self._fileobj.flush() @@ -521,11 +519,7 @@ def close(self): self._fileobj.close() def __repr__(self): - return "{}(file_name={!r}, field_name={!r})".format( - self.__class__.__name__, - self.file_name, - self.field_name - ) + return "{}(file_name={!r}, field_name={!r})".format(self.__class__.__name__, self.file_name, self.field_name) class BaseParser: @@ -548,6 +542,7 @@ class BaseParser: The callback is not passed a copy of the data, since copying severely hurts performance. """ + def __init__(self): self.logger = logging.getLogger(__name__) @@ -593,15 +588,15 @@ def set_callback(self, name, new_func): exist). """ if new_func is None: - self.callbacks.pop('on_' + name, None) + self.callbacks.pop("on_" + name, None) else: - self.callbacks['on_' + name] = new_func + self.callbacks["on_" + name] = new_func def close(self): - pass # pragma: no cover + pass # pragma: no cover def finalize(self): - pass # pragma: no cover + pass # pragma: no cover def __repr__(self): return "%s()" % self.__class__.__name__ @@ -634,14 +629,14 @@ class OctetStreamParser(BaseParser): :param max_size: The maximum size of body to parse. Defaults to infinity - i.e. unbounded. """ - def __init__(self, callbacks={}, max_size=float('inf')): + + def __init__(self, callbacks={}, max_size=float("inf")): super().__init__() self.callbacks = callbacks self._started = False if not isinstance(max_size, Number) or max_size < 1: - raise ValueError("max_size must be a positive number, not %r" % - max_size) + raise ValueError("max_size must be a positive number, not %r" % max_size) self.max_size = max_size self._current_size = 0 @@ -652,7 +647,7 @@ def write(self, data): :param data: a bytestring """ if not self._started: - self.callback('start') + self.callback("start") self._started = True # Truncate data length. @@ -660,22 +655,25 @@ def write(self, data): if (self._current_size + data_len) > self.max_size: # We truncate the length of data that we are to process. new_size = int(self.max_size - self._current_size) - self.logger.warning("Current size is %d (max %d), so truncating " - "data length from %d to %d", - self._current_size, self.max_size, data_len, - new_size) + self.logger.warning( + "Current size is %d (max %d), so truncating data length from %d to %d", + self._current_size, + self.max_size, + data_len, + new_size, + ) data_len = new_size # Increment size, then callback, in case there's an exception. self._current_size += data_len - self.callback('data', data, 0, data_len) + self.callback("data", data, 0, data_len) return data_len def finalize(self): """Finalize this parser, which signals to that we are finished parsing, and sends the on_end callback. """ - self.callback('end') + self.callback("end") def __repr__(self): return "%s()" % self.__class__.__name__ @@ -726,8 +724,8 @@ class QuerystringParser(BaseParser): :param max_size: The maximum size of body to parse. Defaults to infinity - i.e. unbounded. """ - def __init__(self, callbacks={}, strict_parsing=False, - max_size=float('inf')): + + def __init__(self, callbacks={}, strict_parsing=False, max_size=float("inf")): super().__init__() self.state = STATE_BEFORE_FIELD self._found_sep = False @@ -736,8 +734,7 @@ def __init__(self, callbacks={}, strict_parsing=False, # Max-size stuff if not isinstance(max_size, Number) or max_size < 1: - raise ValueError("max_size must be a positive number, not %r" % - max_size) + raise ValueError("max_size must be a positive number, not %r" % max_size) self.max_size = max_size self._current_size = 0 @@ -759,10 +756,13 @@ def write(self, data): if (self._current_size + data_len) > self.max_size: # We truncate the length of data that we are to process. new_size = int(self.max_size - self._current_size) - self.logger.warning("Current size is %d (max %d), so truncating " - "data length from %d to %d", - self._current_size, self.max_size, data_len, - new_size) + self.logger.warning( + "Current size is %d (max %d), so truncating data length from %d to %d", + self._current_size, + self.max_size, + data_len, + new_size, + ) data_len = new_size l = 0 @@ -794,15 +794,11 @@ def _internal_write(self, data, length): if found_sep: # If we're parsing strictly, we disallow blank chunks. if strict_parsing: - e = QuerystringParseError( - "Skipping duplicate ampersand/semicolon at " - "%d" % i - ) + e = QuerystringParseError("Skipping duplicate ampersand/semicolon at %d" % i) e.offset = i raise e else: - self.logger.debug("Skipping duplicate ampersand/" - "semicolon at %d", i) + self.logger.debug("Skipping duplicate ampersand/semicolon at %d", i) else: # This case is when we're skipping the (first) # separator between fields, so we just set our flag @@ -812,7 +808,7 @@ def _internal_write(self, data, length): # Emit a field-start event, and go to that state. Also, # reset the "found_sep" flag, for the next time we get to # this state. - self.callback('field_start') + self.callback("field_start") i -= 1 state = STATE_FIELD_NAME found_sep = False @@ -820,21 +816,21 @@ def _internal_write(self, data, length): elif state == STATE_FIELD_NAME: # Try and find a separator - we ensure that, if we do, we only # look for the equal sign before it. - sep_pos = data.find(b'&', i) + sep_pos = data.find(b"&", i) if sep_pos == -1: - sep_pos = data.find(b';', i) + sep_pos = data.find(b";", i) # See if we can find an equals sign in the remaining data. If # so, we can immediately emit the field name and jump to the # data state. if sep_pos != -1: - equals_pos = data.find(b'=', i, sep_pos) + equals_pos = data.find(b"=", i, sep_pos) else: - equals_pos = data.find(b'=', i) + equals_pos = data.find(b"=", i) if equals_pos != -1: # Emit this name. - self.callback('field_name', data, i, equals_pos) + self.callback("field_name", data, i, equals_pos) # Jump i to this position. Note that it will then have 1 # added to it below, which means the next iteration of this @@ -849,47 +845,46 @@ def _internal_write(self, data, length): # end - there's no data callback at all (not even with # a blank value). if sep_pos != -1: - self.callback('field_name', data, i, sep_pos) - self.callback('field_end') + self.callback("field_name", data, i, sep_pos) + self.callback("field_end") i = sep_pos - 1 state = STATE_BEFORE_FIELD else: # Otherwise, no separator in this block, so the # rest of this chunk must be a name. - self.callback('field_name', data, i, length) + self.callback("field_name", data, i, length) i = length else: # We're parsing strictly. If we find a separator, # this is an error - we require an equals sign. if sep_pos != -1: - e = QuerystringParseError( + e = QuerystringParseError( "When strict_parsing is True, we require an " "equals sign in all field chunks. Did not " - "find one in the chunk that starts at %d" % - (i,) + "find one in the chunk that starts at %d" % (i,) ) e.offset = i raise e # No separator in the rest of this chunk, so it's just # a field name. - self.callback('field_name', data, i, length) + self.callback("field_name", data, i, length) i = length elif state == STATE_FIELD_DATA: # Try finding either an ampersand or a semicolon after this # position. - sep_pos = data.find(b'&', i) + sep_pos = data.find(b"&", i) if sep_pos == -1: - sep_pos = data.find(b';', i) + sep_pos = data.find(b";", i) # If we found it, callback this bit as data and then go back # to expecting to find a field. if sep_pos != -1: - self.callback('field_data', data, i, sep_pos) - self.callback('field_end') + self.callback("field_data", data, i, sep_pos) + self.callback("field_end") # Note that we go to the separator, which brings us to the # "before field" state. This allows us to properly emit @@ -900,10 +895,10 @@ def _internal_write(self, data, length): # Otherwise, emit the rest as data and finish. else: - self.callback('field_data', data, i, length) + self.callback("field_data", data, i, length) i = length - else: # pragma: no cover (error case) + else: # pragma: no cover (error case) msg = "Reached an unknown state %d at %d" % (state, i) self.logger.warning(msg) e = QuerystringParseError(msg) @@ -923,13 +918,12 @@ def finalize(self): """ # If we're currently in the middle of a field, we finish it. if self.state == STATE_FIELD_DATA: - self.callback('field_end') - self.callback('end') + self.callback("field_end") + self.callback("end") def __repr__(self): return "{}(strict_parsing={!r}, max_size={!r})".format( - self.__class__.__name__, - self.strict_parsing, self.max_size + self.__class__.__name__, self.strict_parsing, self.max_size ) @@ -988,17 +982,16 @@ class MultipartParser(BaseParser): i.e. unbounded. """ - def __init__(self, boundary, callbacks={}, max_size=float('inf')): + def __init__(self, boundary, callbacks={}, max_size=float("inf")): # Initialize parser state. super().__init__() - self.state = STATE_START + self.state = MultipartState.START self.index = self.flags = 0 self.callbacks = callbacks if not isinstance(max_size, Number) or max_size < 1: - raise ValueError("max_size must be a positive number, not %r" % - max_size) + raise ValueError("max_size must be a positive number, not %r" % max_size) self.max_size = max_size self._current_size = 0 @@ -1015,9 +1008,9 @@ def __init__(self, boundary, callbacks={}, max_size=float('inf')): # self.skip = tuple(skip) # Save our boundary. - if isinstance(boundary, str): # pragma: no cover - boundary = boundary.encode('latin-1') - self.boundary = b'\r\n--' + boundary + if isinstance(boundary, str): # pragma: no cover + boundary = boundary.encode("latin-1") + self.boundary = b"\r\n--" + boundary # Get a set of characters that belong to our boundary. self.boundary_chars = frozenset(self.boundary) @@ -1043,10 +1036,13 @@ def write(self, data): if (self._current_size + data_len) > self.max_size: # We truncate the length of data that we are to process. new_size = int(self.max_size - self._current_size) - self.logger.warning("Current size is %d (max %d), so truncating " - "data length from %d to %d", - self._current_size, self.max_size, data_len, - new_size) + self.logger.warning( + "Current size is %d (max %d), so truncating data length from %d to %d", + self._current_size, + self.max_size, + data_len, + new_size, + ) data_len = new_size l = 0 @@ -1104,7 +1100,7 @@ def data_callback(name, remaining=False): while i < length: c = data[i] - if state == STATE_START: + if state == MultipartState.START: # Skip leading newlines if c == CR or c == LF: i += 1 @@ -1116,10 +1112,10 @@ def data_callback(name, remaining=False): # Move to the next state, but decrement i so that we re-process # this character. - state = STATE_START_BOUNDARY + state = MultipartState.START_BOUNDARY i -= 1 - elif state == STATE_START_BOUNDARY: + elif state == MultipartState.START_BOUNDARY: # Check to ensure that the last 2 characters in our boundary # are CRLF. if index == len(boundary) - 2: @@ -1145,16 +1141,15 @@ def data_callback(name, remaining=False): index = 0 # Callback for the start of a part. - self.callback('part_begin') + self.callback("part_begin") # Move to the next character and state. - state = STATE_HEADER_FIELD_START + state = MultipartState.HEADER_FIELD_START else: # Check to ensure our boundary matches if c != boundary[index + 2]: - msg = "Did not find boundary character %r at index " \ - "%d" % (c, index + 2) + msg = "Did not find boundary character %r at index " "%d" % (c, index + 2) self.logger.warning(msg) e = MultipartParseError(msg) e.offset = i @@ -1163,25 +1158,25 @@ def data_callback(name, remaining=False): # Increment index into boundary and continue. index += 1 - elif state == STATE_HEADER_FIELD_START: + elif state == MultipartState.HEADER_FIELD_START: # Mark the start of a header field here, reset the index, and # continue parsing our header field. index = 0 # Set a mark of our header field. - set_mark('header_field') + set_mark("header_field") # Move to parsing header fields. - state = STATE_HEADER_FIELD + state = MultipartState.HEADER_FIELD i -= 1 - elif state == STATE_HEADER_FIELD: + elif state == MultipartState.HEADER_FIELD: # If we've reached a CR at the beginning of a header, it means # that we've reached the second of 2 newlines, and so there are # no more headers to parse. if c == CR: - delete_mark('header_field') - state = STATE_HEADERS_ALMOST_DONE + delete_mark("header_field") + state = MultipartState.HEADERS_ALMOST_DONE i += 1 continue @@ -1203,49 +1198,47 @@ def data_callback(name, remaining=False): raise e # Call our callback with the header field. - data_callback('header_field') + data_callback("header_field") # Move to parsing the header value. - state = STATE_HEADER_VALUE_START + state = MultipartState.HEADER_VALUE_START else: # Lower-case this character, and ensure that it is in fact # a valid letter. If not, it's an error. cl = lower_char(c) if cl < LOWER_A or cl > LOWER_Z: - msg = "Found non-alphanumeric character %r in " \ - "header at %d" % (c, i) + msg = "Found non-alphanumeric character %r in " "header at %d" % (c, i) self.logger.warning(msg) e = MultipartParseError(msg) e.offset = i raise e - elif state == STATE_HEADER_VALUE_START: + elif state == MultipartState.HEADER_VALUE_START: # Skip leading spaces. if c == SPACE: i += 1 continue # Mark the start of the header value. - set_mark('header_value') + set_mark("header_value") # Move to the header-value state, reprocessing this character. - state = STATE_HEADER_VALUE + state = MultipartState.HEADER_VALUE i -= 1 - elif state == STATE_HEADER_VALUE: + elif state == MultipartState.HEADER_VALUE: # If we've got a CR, we're nearly done our headers. Otherwise, # we do nothing and just move past this character. if c == CR: - data_callback('header_value') - self.callback('header_end') - state = STATE_HEADER_VALUE_ALMOST_DONE + data_callback("header_value") + self.callback("header_end") + state = MultipartState.HEADER_VALUE_ALMOST_DONE - elif state == STATE_HEADER_VALUE_ALMOST_DONE: + elif state == MultipartState.HEADER_VALUE_ALMOST_DONE: # The last character should be a LF. If not, it's an error. if c != LF: - msg = "Did not find LF character at end of header " \ - "(found %r)" % (c,) + msg = "Did not find LF character at end of header " "(found %r)" % (c,) self.logger.warning(msg) e = MultipartParseError(msg) e.offset = i @@ -1254,9 +1247,9 @@ def data_callback(name, remaining=False): # Move back to the start of another header. Note that if that # state detects ANOTHER newline, it'll trigger the end of our # headers. - state = STATE_HEADER_FIELD_START + state = MultipartState.HEADER_FIELD_START - elif state == STATE_HEADERS_ALMOST_DONE: + elif state == MultipartState.HEADERS_ALMOST_DONE: # We're almost done our headers. This is reached when we parse # a CR at the beginning of a header, so our next character # should be a LF, or it's an error. @@ -1267,18 +1260,18 @@ def data_callback(name, remaining=False): e.offset = i raise e - self.callback('headers_finished') - state = STATE_PART_DATA_START + self.callback("headers_finished") + state = MultipartState.PART_DATA_START - elif state == STATE_PART_DATA_START: + elif state == MultipartState.PART_DATA_START: # Mark the start of our part data. - set_mark('part_data') + set_mark("part_data") # Start processing part data, including this character. - state = STATE_PART_DATA + state = MultipartState.PART_DATA i -= 1 - elif state == STATE_PART_DATA: + elif state == MultipartState.PART_DATA: # We're processing our part data right now. During this, we # need to efficiently search for our boundary, since any data # on any number of lines can be a part of the current data. @@ -1320,7 +1313,7 @@ def data_callback(name, remaining=False): # If we found a match for our boundary, we send the # existing data. if index == 0: - data_callback('part_data') + data_callback("part_data") # The current character matches, so continue! index += 1 @@ -1356,23 +1349,23 @@ def data_callback(name, remaining=False): # We need a LF character next. if c == LF: # Unset the part boundary flag. - flags &= (~FLAG_PART_BOUNDARY) + flags &= ~FLAG_PART_BOUNDARY # Callback indicating that we've reached the end of # a part, and are starting a new one. - self.callback('part_end') - self.callback('part_begin') + self.callback("part_end") + self.callback("part_begin") # Move to parsing new headers. index = 0 - state = STATE_HEADER_FIELD_START + state = MultipartState.HEADER_FIELD_START i += 1 continue # We didn't find an LF character, so no match. Reset # our index and clear our flag. index = 0 - flags &= (~FLAG_PART_BOUNDARY) + flags &= ~FLAG_PART_BOUNDARY # Otherwise, if we're at the last boundary (i.e. we've # seen a hyphen already)... @@ -1381,9 +1374,9 @@ def data_callback(name, remaining=False): if c == HYPHEN: # Callback to end the current part, and then the # message. - self.callback('part_end') - self.callback('end') - state = STATE_END + self.callback("part_end") + self.callback("end") + state = MultipartState.END else: # No match, so reset index. index = 0 @@ -1400,24 +1393,24 @@ def data_callback(name, remaining=False): elif prev_index > 0: # Callback to write the saved data. lb_data = join_bytes(self.lookbehind) - self.callback('part_data', lb_data, 0, prev_index) + self.callback("part_data", lb_data, 0, prev_index) # Overwrite our previous index. prev_index = 0 # Re-set our mark for part data. - set_mark('part_data') + set_mark("part_data") # Re-consider the current character, since this could be # the start of the boundary itself. i -= 1 - elif state == STATE_END: + elif state == MultipartState.END: # Do nothing and just consume a byte in the end state. if c not in (CR, LF): self.logger.warning("Consuming a byte '0x%x' in the end state", c) - else: # pragma: no cover (error case) + else: # pragma: no cover (error case) # We got into a strange state somehow! Just stop processing. msg = "Reached an unknown state %d at %d" % (state, i) self.logger.warning(msg) @@ -1436,9 +1429,9 @@ def data_callback(name, remaining=False): # that we haven't yet reached the end of this 'thing'. So, by setting # the mark to 0, we cause any data callbacks that take place in future # calls to this function to start from the beginning of that buffer. - data_callback('header_field', True) - data_callback('header_value', True) - data_callback('part_data', True) + data_callback("header_field", True) + data_callback("header_value", True) + data_callback("part_data", True) # Save values to locals. self.state = state @@ -1456,7 +1449,7 @@ def finalize(self): are in the final state of the parser (i.e. the end of the multipart message is well-formed), and, if not, throw an error. """ - # TODO: verify that we're in the state STATE_END, otherwise throw an + # TODO: verify that we're in the state MultipartState.END, otherwise throw an # error or otherwise state that we're not finished parsing. pass @@ -1516,23 +1509,31 @@ class FormParser: default values. """ + #: This is the default configuration for our form parser. #: Note: all file sizes should be in bytes. DEFAULT_CONFIG = { - 'MAX_BODY_SIZE': float('inf'), - 'MAX_MEMORY_FILE_SIZE': 1 * 1024 * 1024, - 'UPLOAD_DIR': None, - 'UPLOAD_KEEP_FILENAME': False, - 'UPLOAD_KEEP_EXTENSIONS': False, - + "MAX_BODY_SIZE": float("inf"), + "MAX_MEMORY_FILE_SIZE": 1 * 1024 * 1024, + "UPLOAD_DIR": None, + "UPLOAD_KEEP_FILENAME": False, + "UPLOAD_KEEP_EXTENSIONS": False, # Error on invalid Content-Transfer-Encoding? - 'UPLOAD_ERROR_ON_BAD_CTE': False, + "UPLOAD_ERROR_ON_BAD_CTE": False, } - def __init__(self, content_type, on_field, on_file, on_end=None, - boundary=None, file_name=None, FileClass=File, - FieldClass=Field, config={}): - + def __init__( + self, + content_type, + on_field, + on_file, + on_end=None, + boundary=None, + file_name=None, + FileClass=File, + FieldClass=Field, + config={}, + ): self.logger = logging.getLogger(__name__) # Save variables. @@ -1555,7 +1556,7 @@ def __init__(self, content_type, on_field, on_file, on_end=None, self.config.update(config) # Depending on the Content-Type, we instantiate the correct parser. - if content_type == 'application/octet-stream': + if content_type == "application/octet-stream": # Work around the lack of 'nonlocal' in Py2 class vars: f = None @@ -1577,19 +1578,12 @@ def on_end(): if self.on_end is not None: self.on_end() - callbacks = { - 'on_start': on_start, - 'on_data': on_data, - 'on_end': on_end, - } + callbacks = {"on_start": on_start, "on_data": on_data, "on_end": on_end} # Instantiate an octet-stream parser - parser = OctetStreamParser(callbacks, - max_size=self.config['MAX_BODY_SIZE']) - - elif (content_type == 'application/x-www-form-urlencoded' or - content_type == 'application/x-url-encoded'): + parser = OctetStreamParser(callbacks, max_size=self.config["MAX_BODY_SIZE"]) + elif content_type == "application/x-www-form-urlencoded" or content_type == "application/x-url-encoded": name_buffer = [] class vars: @@ -1603,7 +1597,7 @@ def on_field_name(data, start, end): def on_field_data(data, start, end): if vars.f is None: - vars.f = FieldClass(b''.join(name_buffer)) + vars.f = FieldClass(b"".join(name_buffer)) del name_buffer[:] vars.f.write(data[start:end]) @@ -1612,7 +1606,7 @@ def on_field_end(): if vars.f is None: # If we get here, it's because there was no field data. # We create a field, set it to None, and then continue. - vars.f = FieldClass(b''.join(name_buffer)) + vars.f = FieldClass(b"".join(name_buffer)) del name_buffer[:] vars.f.set_none() @@ -1626,20 +1620,17 @@ def on_end(): # Setup callbacks. callbacks = { - 'on_field_start': on_field_start, - 'on_field_name': on_field_name, - 'on_field_data': on_field_data, - 'on_field_end': on_field_end, - 'on_end': on_end, + "on_field_start": on_field_start, + "on_field_name": on_field_name, + "on_field_data": on_field_data, + "on_field_end": on_field_end, + "on_end": on_end, } # Instantiate parser. - parser = QuerystringParser( - callbacks=callbacks, - max_size=self.config['MAX_BODY_SIZE'] - ) + parser = QuerystringParser(callbacks=callbacks, max_size=self.config["MAX_BODY_SIZE"]) - elif content_type == 'multipart/form-data': + elif content_type == "multipart/form-data": if boundary is None: self.logger.error("No boundary given") raise FormParserError("No boundary given") @@ -1676,7 +1667,7 @@ def on_header_value(data, start, end): header_value.append(data[start:end]) def on_header_end(): - headers[b''.join(header_name)] = b''.join(header_value) + headers[b"".join(header_name)] = b"".join(header_value) del header_name[:] del header_value[:] @@ -1686,12 +1677,12 @@ def on_headers_finished(): # Parse the content-disposition header. # TODO: handle mixed case - content_disp = headers.get(b'Content-Disposition') + content_disp = headers.get(b"Content-Disposition") disp, options = parse_options_header(content_disp) # Get the field and filename. - field_name = options.get(b'name') - file_name = options.get(b'filename') + field_name = options.get(b"name") + file_name = options.get(b"filename") # TODO: check for errors # Create the proper class. @@ -1704,29 +1695,21 @@ def on_headers_finished(): # Parse the given Content-Transfer-Encoding to determine what # we need to do with the incoming data. # TODO: check that we properly handle 8bit / 7bit encoding. - transfer_encoding = headers.get(b'Content-Transfer-Encoding', - b'7bit') + transfer_encoding = headers.get(b"Content-Transfer-Encoding", b"7bit") - if (transfer_encoding == b'binary' or - transfer_encoding == b'8bit' or - transfer_encoding == b'7bit'): + if transfer_encoding == b"binary" or transfer_encoding == b"8bit" or transfer_encoding == b"7bit": vars.writer = vars.f - elif transfer_encoding == b'base64': + elif transfer_encoding == b"base64": vars.writer = Base64Decoder(vars.f) - elif transfer_encoding == b'quoted-printable': + elif transfer_encoding == b"quoted-printable": vars.writer = QuotedPrintableDecoder(vars.f) else: - self.logger.warning("Unknown Content-Transfer-Encoding: " - "%r", transfer_encoding) - if self.config['UPLOAD_ERROR_ON_BAD_CTE']: - raise FormParserError( - 'Unknown Content-Transfer-Encoding "{}"'.format( - transfer_encoding - ) - ) + self.logger.warning("Unknown Content-Transfer-Encoding: %r", transfer_encoding) + if self.config["UPLOAD_ERROR_ON_BAD_CTE"]: + raise FormParserError('Unknown Content-Transfer-Encoding "{}"'.format(transfer_encoding)) else: # If we aren't erroring, then we just treat this as an # unencoded Content-Transfer-Encoding. @@ -1739,25 +1722,22 @@ def on_end(): # These are our callbacks for the parser. callbacks = { - 'on_part_begin': on_part_begin, - 'on_part_data': on_part_data, - 'on_part_end': on_part_end, - 'on_header_field': on_header_field, - 'on_header_value': on_header_value, - 'on_header_end': on_header_end, - 'on_headers_finished': on_headers_finished, - 'on_end': on_end, + "on_part_begin": on_part_begin, + "on_part_data": on_part_data, + "on_part_end": on_part_end, + "on_header_field": on_header_field, + "on_header_value": on_header_value, + "on_header_end": on_header_end, + "on_headers_finished": on_headers_finished, + "on_end": on_end, } # Instantiate a multipart parser. - parser = MultipartParser(boundary, callbacks, - max_size=self.config['MAX_BODY_SIZE']) + parser = MultipartParser(boundary, callbacks, max_size=self.config["MAX_BODY_SIZE"]) else: self.logger.warning("Unknown Content-Type: %r", content_type) - raise FormParserError("Unknown Content-Type: {}".format( - content_type - )) + raise FormParserError("Unknown Content-Type: {}".format(content_type)) self.parser = parser @@ -1773,24 +1753,19 @@ def write(self, data): def finalize(self): """Finalize the parser.""" - if self.parser is not None and hasattr(self.parser, 'finalize'): + if self.parser is not None and hasattr(self.parser, "finalize"): self.parser.finalize() def close(self): """Close the parser.""" - if self.parser is not None and hasattr(self.parser, 'close'): + if self.parser is not None and hasattr(self.parser, "close"): self.parser.close() def __repr__(self): - return "{}(content_type={!r}, parser={!r})".format( - self.__class__.__name__, - self.content_type, - self.parser, - ) + return "{}(content_type={!r}, parser={!r})".format(self.__class__.__name__, self.content_type, self.parser) -def create_form_parser(headers, on_field, on_file, trust_x_headers=False, - config={}): +def create_form_parser(headers, on_field, on_file, trust_x_headers=False, config={}): """This function is a helper function to aid in creating a FormParser instances. Given a dictionary-like headers object, it will determine the correct information needed, instantiate a FormParser with the @@ -1810,7 +1785,7 @@ def create_form_parser(headers, on_field, on_file, trust_x_headers=False, :param config: Configuration variables to pass to the FormParser. """ - content_type = headers.get('Content-Type') + content_type = headers.get("Content-Type") if content_type is None: logging.getLogger(__name__).warning("No Content-Type header given") raise ValueError("No Content-Type header given!") @@ -1818,28 +1793,22 @@ def create_form_parser(headers, on_field, on_file, trust_x_headers=False, # Boundaries are optional (the FormParser will raise if one is needed # but not given). content_type, params = parse_options_header(content_type) - boundary = params.get(b'boundary') + boundary = params.get(b"boundary") # We need content_type to be a string, not a bytes object. - content_type = content_type.decode('latin-1') + content_type = content_type.decode("latin-1") # File names are optional. - file_name = headers.get('X-File-Name') + file_name = headers.get("X-File-Name") # Instantiate a form parser. - form_parser = FormParser(content_type, - on_field, - on_file, - boundary=boundary, - file_name=file_name, - config=config) + form_parser = FormParser(content_type, on_field, on_file, boundary=boundary, file_name=file_name, config=config) # Return our parser. return form_parser -def parse_form(headers, input_stream, on_field, on_file, chunk_size=1048576, - **kwargs): +def parse_form(headers, input_stream, on_field, on_file, chunk_size=1048576, **kwargs): """This function is useful if you just want to parse a request body, without too much work. Pass it a dictionary-like object of the request's headers, and a file-like object for the input stream, along with two @@ -1864,11 +1833,11 @@ def parse_form(headers, input_stream, on_field, on_file, chunk_size=1048576, # Read chunks of 100KiB and write to the parser, but never read more than # the given Content-Length, if any. - content_length = headers.get('Content-Length') + content_length = headers.get("Content-Length") if content_length is not None: content_length = int(content_length) else: - content_length = float('inf') + content_length = float("inf") bytes_read = 0 while True: diff --git a/pyproject.toml b/pyproject.toml index 085ab6a..5833d83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dev = [ "PyYAML==6.0.1", "invoke==2.2.0", "pytest-timeout==2.2.0", + "ruff==0.2.1", "hatch", ] @@ -58,3 +59,15 @@ packages = ["multipart"] [tool.hatch.build.targets.sdist] include = ["/multipart", "/tests"] + +[tool.ruff] +line-length = 120 +select = ["E", "F", "I", "FA"] +ignore = ["B904", "B028", "F841", "E741"] + +[tool.ruff.format] +skip-magic-trailing-comma = true + +[tool.ruff.lint.isort] +combine-as-imports = true +split-on-trailing-comma = false diff --git a/requirements.txt b/requirements.txt index 9622472..23baf78 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ pluggy==1.4.0 py==1.11.0 pytest==8.0.0 PyYAML==6.0.1 +ruff==0.2.1 diff --git a/tests/compat.py b/tests/compat.py index 897188d..8b0ccae 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -1,8 +1,8 @@ +import functools import os import re import sys import types -import functools def ensure_in_path(path): @@ -10,7 +10,7 @@ def ensure_in_path(path): Ensure that a given path is in the sys.path array """ if not os.path.isdir(path): - raise RuntimeError('Tried to add nonexisting path') + raise RuntimeError("Tried to add nonexisting path") def _samefile(x, y): try: @@ -44,7 +44,9 @@ def _samefile(x, y): xfail = pytest.mark.xfail else: - slow_test = lambda x: x + + def slow_test(x): + return x def xfail(*args, **kwargs): if len(args) > 0 and isinstance(args[0], types.FunctionType): @@ -64,8 +66,8 @@ def parametrize(field_names, field_values): # Create a decorator that saves this list of field names and values on the # function for later parametrizing. def decorator(func): - func.__dict__['param_names'] = field_names - func.__dict__['param_values'] = field_values + func.__dict__["param_names"] = field_names + func.__dict__["param_values"] = field_values return func return decorator @@ -73,7 +75,7 @@ def decorator(func): # This is a metaclass that actually performs the parametrization. class ParametrizingMetaclass(type): - IDENTIFIER_RE = re.compile('[^A-Za-z0-9]') + IDENTIFIER_RE = re.compile("[^A-Za-z0-9]") def __new__(klass, name, bases, attrs): new_attrs = attrs.copy() @@ -82,8 +84,8 @@ def __new__(klass, name, bases, attrs): if not isinstance(attr, types.FunctionType): continue - param_names = attr.__dict__.pop('param_names', None) - param_values = attr.__dict__.pop('param_values', None) + param_names = attr.__dict__.pop("param_names", None) + param_values = attr.__dict__.pop("param_values", None) if param_names is None or param_values is None: continue @@ -92,9 +94,7 @@ def __new__(klass, name, bases, attrs): assert len(param_names) == len(values) # Get a repr of the values, and fix it to be a valid identifier - human = '_'.join( - [klass.IDENTIFIER_RE.sub('', repr(x)) for x in values] - ) + human = "_".join([klass.IDENTIFIER_RE.sub("", repr(x)) for x in values]) # Create a new name. # new_name = attr.__name__ + "_%d" % i @@ -128,6 +128,4 @@ def new_func(self): # This is a class decorator that actually applies the above metaclass. def parametrize_class(klass): - return ParametrizingMetaclass(klass.__name__, - klass.__bases__, - klass.__dict__) + return ParametrizingMetaclass(klass.__name__, klass.__bases__, klass.__dict__) diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 5cfacf4..b9cba86 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -1,19 +1,30 @@ import os -import sys -import yaml import random +import sys import tempfile import unittest -from .compat import ( - parametrize, - parametrize_class, - slow_test, -) from io import BytesIO from unittest.mock import Mock -from multipart.multipart import * +import yaml + +from multipart.decoders import Base64Decoder, QuotedPrintableDecoder +from multipart.exceptions import DecodeError, FileError, FormParserError, MultipartParseError +from multipart.multipart import ( + BaseParser, + Field, + File, + FormParser, + MultipartParser, + OctetStreamParser, + QuerystringParseError, + QuerystringParser, + create_form_parser, + parse_form, + parse_options_header, +) +from .compat import parametrize, parametrize_class, slow_test # Get the current directory for our later test cases. curr_dir = os.path.abspath(os.path.dirname(__file__)) @@ -28,53 +39,53 @@ def force_bytes(val): class TestField(unittest.TestCase): def setUp(self): - self.f = Field('foo') + self.f = Field("foo") def test_name(self): - self.assertEqual(self.f.field_name, 'foo') + self.assertEqual(self.f.field_name, "foo") def test_data(self): - self.f.write(b'test123') - self.assertEqual(self.f.value, b'test123') + self.f.write(b"test123") + self.assertEqual(self.f.value, b"test123") def test_cache_expiration(self): - self.f.write(b'test') - self.assertEqual(self.f.value, b'test') - self.f.write(b'123') - self.assertEqual(self.f.value, b'test123') + self.f.write(b"test") + self.assertEqual(self.f.value, b"test") + self.f.write(b"123") + self.assertEqual(self.f.value, b"test123") def test_finalize(self): - self.f.write(b'test123') + self.f.write(b"test123") self.f.finalize() - self.assertEqual(self.f.value, b'test123') + self.assertEqual(self.f.value, b"test123") def test_close(self): - self.f.write(b'test123') + self.f.write(b"test123") self.f.close() - self.assertEqual(self.f.value, b'test123') + self.assertEqual(self.f.value, b"test123") def test_from_value(self): - f = Field.from_value(b'name', b'value') - self.assertEqual(f.field_name, b'name') - self.assertEqual(f.value, b'value') + f = Field.from_value(b"name", b"value") + self.assertEqual(f.field_name, b"name") + self.assertEqual(f.value, b"value") - f2 = Field.from_value(b'name', None) + f2 = Field.from_value(b"name", None) self.assertEqual(f2.value, None) def test_equality(self): - f1 = Field.from_value(b'name', b'value') - f2 = Field.from_value(b'name', b'value') + f1 = Field.from_value(b"name", b"value") + f2 = Field.from_value(b"name", b"value") self.assertEqual(f1, f2) def test_equality_with_other(self): - f = Field.from_value(b'foo', b'bar') - self.assertFalse(f == b'foo') - self.assertFalse(b'foo' == f) + f = Field.from_value(b"foo", b"bar") + self.assertFalse(f == b"foo") + self.assertFalse(b"foo" == f) def test_set_none(self): - f = Field(b'foo') - self.assertEqual(f.value, b'') + f = Field(b"foo") + self.assertEqual(f.value, b"") f.set_none() self.assertEqual(f.value, None) @@ -84,7 +95,7 @@ class TestFile(unittest.TestCase): def setUp(self): self.c = {} self.d = force_bytes(tempfile.mkdtemp()) - self.f = File(b'foo.txt', config=self.c) + self.f = File(b"foo.txt", config=self.c) def assert_data(self, data): f = self.f.file_object @@ -98,26 +109,26 @@ def assert_exists(self): self.assertTrue(os.path.exists(full_path)) def test_simple(self): - self.f.write(b'foobar') - self.assert_data(b'foobar') + self.f.write(b"foobar") + self.assert_data(b"foobar") def test_invalid_write(self): m = Mock() m.write.return_value = 5 self.f._fileobj = m - v = self.f.write(b'foobar') + v = self.f.write(b"foobar") self.assertEqual(v, 5) def test_file_fallback(self): - self.c['MAX_MEMORY_FILE_SIZE'] = 1 + self.c["MAX_MEMORY_FILE_SIZE"] = 1 - self.f.write(b'1') + self.f.write(b"1") self.assertTrue(self.f.in_memory) - self.assert_data(b'1') + self.assert_data(b"1") - self.f.write(b'123') + self.f.write(b"123") self.assertFalse(self.f.in_memory) - self.assert_data(b'123') + self.assert_data(b"123") # Test flushing too. old_obj = self.f.file_object @@ -126,23 +137,23 @@ def test_file_fallback(self): self.assertIs(self.f.file_object, old_obj) def test_file_fallback_with_data(self): - self.c['MAX_MEMORY_FILE_SIZE'] = 10 + self.c["MAX_MEMORY_FILE_SIZE"] = 10 - self.f.write(b'1' * 10) + self.f.write(b"1" * 10) self.assertTrue(self.f.in_memory) - self.f.write(b'2' * 10) + self.f.write(b"2" * 10) self.assertFalse(self.f.in_memory) - self.assert_data(b'11111111112222222222') + self.assert_data(b"11111111112222222222") def test_file_name(self): # Write to this dir. - self.c['UPLOAD_DIR'] = self.d - self.c['MAX_MEMORY_FILE_SIZE'] = 10 + self.c["UPLOAD_DIR"] = self.d + self.c["MAX_MEMORY_FILE_SIZE"] = 10 # Write. - self.f.write(b'12345678901') + self.f.write(b"12345678901") self.assertFalse(self.f.in_memory) # Assert that the file exists @@ -151,135 +162,124 @@ def test_file_name(self): def test_file_full_name(self): # Write to this dir. - self.c['UPLOAD_DIR'] = self.d - self.c['UPLOAD_KEEP_FILENAME'] = True - self.c['MAX_MEMORY_FILE_SIZE'] = 10 + self.c["UPLOAD_DIR"] = self.d + self.c["UPLOAD_KEEP_FILENAME"] = True + self.c["MAX_MEMORY_FILE_SIZE"] = 10 # Write. - self.f.write(b'12345678901') + self.f.write(b"12345678901") self.assertFalse(self.f.in_memory) # Assert that the file exists - self.assertEqual(self.f.actual_file_name, b'foo') + self.assertEqual(self.f.actual_file_name, b"foo") self.assert_exists() def test_file_full_name_with_ext(self): - self.c['UPLOAD_DIR'] = self.d - self.c['UPLOAD_KEEP_FILENAME'] = True - self.c['UPLOAD_KEEP_EXTENSIONS'] = True - self.c['MAX_MEMORY_FILE_SIZE'] = 10 + self.c["UPLOAD_DIR"] = self.d + self.c["UPLOAD_KEEP_FILENAME"] = True + self.c["UPLOAD_KEEP_EXTENSIONS"] = True + self.c["MAX_MEMORY_FILE_SIZE"] = 10 # Write. - self.f.write(b'12345678901') + self.f.write(b"12345678901") self.assertFalse(self.f.in_memory) # Assert that the file exists - self.assertEqual(self.f.actual_file_name, b'foo.txt') - self.assert_exists() - - def test_file_full_name_with_ext(self): - self.c['UPLOAD_DIR'] = self.d - self.c['UPLOAD_KEEP_FILENAME'] = True - self.c['UPLOAD_KEEP_EXTENSIONS'] = True - self.c['MAX_MEMORY_FILE_SIZE'] = 10 - - # Write. - self.f.write(b'12345678901') - self.assertFalse(self.f.in_memory) - - # Assert that the file exists - self.assertEqual(self.f.actual_file_name, b'foo.txt') + self.assertEqual(self.f.actual_file_name, b"foo.txt") self.assert_exists() def test_no_dir_with_extension(self): - self.c['UPLOAD_KEEP_EXTENSIONS'] = True - self.c['MAX_MEMORY_FILE_SIZE'] = 10 + self.c["UPLOAD_KEEP_EXTENSIONS"] = True + self.c["MAX_MEMORY_FILE_SIZE"] = 10 # Write. - self.f.write(b'12345678901') + self.f.write(b"12345678901") self.assertFalse(self.f.in_memory) # Assert that the file exists ext = os.path.splitext(self.f.actual_file_name)[1] - self.assertEqual(ext, b'.txt') + self.assertEqual(ext, b".txt") self.assert_exists() def test_invalid_dir_with_name(self): # Write to this dir. - self.c['UPLOAD_DIR'] = force_bytes(os.path.join('/', 'tmp', 'notexisting')) - self.c['UPLOAD_KEEP_FILENAME'] = True - self.c['MAX_MEMORY_FILE_SIZE'] = 5 + self.c["UPLOAD_DIR"] = force_bytes(os.path.join("/", "tmp", "notexisting")) + self.c["UPLOAD_KEEP_FILENAME"] = True + self.c["MAX_MEMORY_FILE_SIZE"] = 5 # Write. with self.assertRaises(FileError): - self.f.write(b'1234567890') + self.f.write(b"1234567890") def test_invalid_dir_no_name(self): # Write to this dir. - self.c['UPLOAD_DIR'] = force_bytes(os.path.join('/', 'tmp', 'notexisting')) - self.c['UPLOAD_KEEP_FILENAME'] = False - self.c['MAX_MEMORY_FILE_SIZE'] = 5 + self.c["UPLOAD_DIR"] = force_bytes(os.path.join("/", "tmp", "notexisting")) + self.c["UPLOAD_KEEP_FILENAME"] = False + self.c["MAX_MEMORY_FILE_SIZE"] = 5 # Write. with self.assertRaises(FileError): - self.f.write(b'1234567890') + self.f.write(b"1234567890") # TODO: test uploading two files with the same name. class TestParseOptionsHeader(unittest.TestCase): def test_simple(self): - t, p = parse_options_header('application/json') - self.assertEqual(t, b'application/json') + t, p = parse_options_header("application/json") + self.assertEqual(t, b"application/json") self.assertEqual(p, {}) def test_blank(self): - t, p = parse_options_header('') - self.assertEqual(t, b'') + t, p = parse_options_header("") + self.assertEqual(t, b"") self.assertEqual(p, {}) def test_single_param(self): - t, p = parse_options_header('application/json;par=val') - self.assertEqual(t, b'application/json') - self.assertEqual(p, {b'par': b'val'}) + t, p = parse_options_header("application/json;par=val") + self.assertEqual(t, b"application/json") + self.assertEqual(p, {b"par": b"val"}) def test_single_param_with_spaces(self): - t, p = parse_options_header(b'application/json; par=val') - self.assertEqual(t, b'application/json') - self.assertEqual(p, {b'par': b'val'}) + t, p = parse_options_header(b"application/json; par=val") + self.assertEqual(t, b"application/json") + self.assertEqual(p, {b"par": b"val"}) def test_multiple_params(self): - t, p = parse_options_header(b'application/json;par=val;asdf=foo') - self.assertEqual(t, b'application/json') - self.assertEqual(p, {b'par': b'val', b'asdf': b'foo'}) + t, p = parse_options_header(b"application/json;par=val;asdf=foo") + self.assertEqual(t, b"application/json") + self.assertEqual(p, {b"par": b"val", b"asdf": b"foo"}) def test_quoted_param(self): t, p = parse_options_header(b'application/json;param="quoted"') - self.assertEqual(t, b'application/json') - self.assertEqual(p, {b'param': b'quoted'}) + self.assertEqual(t, b"application/json") + self.assertEqual(p, {b"param": b"quoted"}) def test_quoted_param_with_semicolon(self): t, p = parse_options_header(b'application/json;param="quoted;with;semicolons"') - self.assertEqual(p[b'param'], b'quoted;with;semicolons') + self.assertEqual(p[b"param"], b"quoted;with;semicolons") def test_quoted_param_with_escapes(self): t, p = parse_options_header(b'application/json;param="This \\" is \\" a \\" quote"') - self.assertEqual(p[b'param'], b'This " is " a " quote') + self.assertEqual(p[b"param"], b'This " is " a " quote') def test_handles_ie6_bug(self): t, p = parse_options_header(b'text/plain; filename="C:\\this\\is\\a\\path\\file.txt"') - self.assertEqual(p[b'filename'], b'file.txt') - + self.assertEqual(p[b"filename"], b"file.txt") + def test_redos_attack_header(self): - t, p = parse_options_header(b'application/x-www-form-urlencoded; !="\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\') + t, p = parse_options_header( + b'application/x-www-form-urlencoded; !="' + b"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\" + ) # If vulnerable, this test wouldn't finish, the line above would hang - self.assertIn(b'"\\', p[b'!']) + self.assertIn(b'"\\', p[b"!"]) def test_handles_rfc_2231(self): - t, p = parse_options_header(b'text/plain; param*=us-ascii\'en-us\'encoded%20message') + t, p = parse_options_header(b"text/plain; param*=us-ascii'en-us'encoded%20message") - self.assertEqual(p[b'param'], b'encoded message') + self.assertEqual(p[b"param"], b"encoded message") class TestBaseParser(unittest.TestCase): @@ -290,25 +290,26 @@ def setUp(self): def test_callbacks(self): # The stupid list-ness is to get around lack of nonlocal on py2 l = [0] + def on_foo(): l[0] += 1 - self.b.set_callback('foo', on_foo) - self.b.callback('foo') + self.b.set_callback("foo", on_foo) + self.b.callback("foo") self.assertEqual(l[0], 1) - self.b.set_callback('foo', None) - self.b.callback('foo') + self.b.set_callback("foo", None) + self.b.callback("foo") self.assertEqual(l[0], 1) class TestQuerystringParser(unittest.TestCase): def assert_fields(self, *args, **kwargs): - if kwargs.pop('finalize', True): + if kwargs.pop("finalize", True): self.p.finalize() self.assertEqual(self.f, list(args)) - if kwargs.get('reset', True): + if kwargs.get("reset", True): self.f = [] def setUp(self): @@ -327,103 +328,80 @@ def on_field_data(data, start, end): data_buffer.append(data[start:end]) def on_field_end(): - self.f.append(( - b''.join(name_buffer), - b''.join(data_buffer) - )) + self.f.append((b"".join(name_buffer), b"".join(data_buffer))) del name_buffer[:] del data_buffer[:] - callbacks = { - 'on_field_name': on_field_name, - 'on_field_data': on_field_data, - 'on_field_end': on_field_end - } + callbacks = {"on_field_name": on_field_name, "on_field_data": on_field_data, "on_field_end": on_field_end} self.p = QuerystringParser(callbacks) def test_simple_querystring(self): - self.p.write(b'foo=bar') + self.p.write(b"foo=bar") - self.assert_fields((b'foo', b'bar')) + self.assert_fields((b"foo", b"bar")) def test_querystring_blank_beginning(self): - self.p.write(b'&foo=bar') + self.p.write(b"&foo=bar") - self.assert_fields((b'foo', b'bar')) + self.assert_fields((b"foo", b"bar")) def test_querystring_blank_end(self): - self.p.write(b'foo=bar&') + self.p.write(b"foo=bar&") - self.assert_fields((b'foo', b'bar')) + self.assert_fields((b"foo", b"bar")) def test_multiple_querystring(self): - self.p.write(b'foo=bar&asdf=baz') + self.p.write(b"foo=bar&asdf=baz") - self.assert_fields( - (b'foo', b'bar'), - (b'asdf', b'baz') - ) + self.assert_fields((b"foo", b"bar"), (b"asdf", b"baz")) def test_streaming_simple(self): - self.p.write(b'foo=bar&') - self.assert_fields( - (b'foo', b'bar'), - finalize=False - ) + self.p.write(b"foo=bar&") + self.assert_fields((b"foo", b"bar"), finalize=False) - self.p.write(b'asdf=baz') - self.assert_fields( - (b'asdf', b'baz') - ) + self.p.write(b"asdf=baz") + self.assert_fields((b"asdf", b"baz")) def test_streaming_break(self): - self.p.write(b'foo=one') + self.p.write(b"foo=one") self.assert_fields(finalize=False) - self.p.write(b'two') + self.p.write(b"two") self.assert_fields(finalize=False) - self.p.write(b'three') + self.p.write(b"three") self.assert_fields(finalize=False) - self.p.write(b'&asd') - self.assert_fields( - (b'foo', b'onetwothree'), - finalize=False - ) + self.p.write(b"&asd") + self.assert_fields((b"foo", b"onetwothree"), finalize=False) - self.p.write(b'f=baz') - self.assert_fields( - (b'asdf', b'baz') - ) + self.p.write(b"f=baz") + self.assert_fields((b"asdf", b"baz")) def test_semicolon_separator(self): - self.p.write(b'foo=bar;asdf=baz') + self.p.write(b"foo=bar;asdf=baz") - self.assert_fields( - (b'foo', b'bar'), - (b'asdf', b'baz') - ) + self.assert_fields((b"foo", b"bar"), (b"asdf", b"baz")) def test_too_large_field(self): self.p.max_size = 15 # Note: len = 8 self.p.write(b"foo=bar&") - self.assert_fields((b'foo', b'bar'), finalize=False) + self.assert_fields((b"foo", b"bar"), finalize=False) # Note: len = 8, only 7 bytes processed - self.p.write(b'a=123456') - self.assert_fields((b'a', b'12345')) + self.p.write(b"a=123456") + self.assert_fields((b"a", b"12345")) def test_invalid_max_size(self): with self.assertRaises(ValueError): p = QuerystringParser(max_size=-100) def test_strict_parsing_pass(self): - data = b'foo=bar&another=asdf' + data = b"foo=bar&another=asdf" for first, last in split_all(data): self.reset() self.p.strict_parsing = True @@ -432,10 +410,10 @@ def test_strict_parsing_pass(self): self.p.write(first) self.p.write(last) - self.assert_fields((b'foo', b'bar'), (b'another', b'asdf')) + self.assert_fields((b"foo", b"bar"), (b"another", b"asdf")) def test_strict_parsing_fail_double_sep(self): - data = b'foo=bar&&another=asdf' + data = b"foo=bar&&another=asdf" for first, last in split_all(data): self.reset() self.p.strict_parsing = True @@ -452,7 +430,7 @@ def test_strict_parsing_fail_double_sep(self): self.assertEqual(cm.exception.offset, 8 - cnt) def test_double_sep(self): - data = b'foo=bar&&another=asdf' + data = b"foo=bar&&another=asdf" for first, last in split_all(data): print(f" {first!r} / {last!r} ") self.reset() @@ -461,23 +439,19 @@ def test_double_sep(self): cnt += self.p.write(first) cnt += self.p.write(last) - self.assert_fields((b'foo', b'bar'), (b'another', b'asdf')) + self.assert_fields((b"foo", b"bar"), (b"another", b"asdf")) def test_strict_parsing_fail_no_value(self): self.p.strict_parsing = True with self.assertRaises(QuerystringParseError) as cm: - self.p.write(b'foo=bar&blank&another=asdf') + self.p.write(b"foo=bar&blank&another=asdf") if cm is not None: self.assertEqual(cm.exception.offset, 8) def test_success_no_value(self): - self.p.write(b'foo=bar&blank&another=asdf') - self.assert_fields( - (b'foo', b'bar'), - (b'blank', b''), - (b'another', b'asdf') - ) + self.p.write(b"foo=bar&blank&another=asdf") + self.assert_fields((b"foo", b"bar"), (b"blank", b""), (b"another", b"asdf")) def test_repr(self): # Issue #29; verify we don't assert on repr() @@ -499,16 +473,12 @@ def on_data(data, start, end): def on_end(): self.finished += 1 - callbacks = { - 'on_start': on_start, - 'on_data': on_data, - 'on_end': on_end - } + callbacks = {"on_start": on_start, "on_data": on_data, "on_end": on_end} self.p = OctetStreamParser(callbacks) def assert_data(self, data, finalize=True): - self.assertEqual(b''.join(self.d), data) + self.assertEqual(b"".join(self.d), data) self.d = [] def assert_started(self, val=True): @@ -528,9 +498,9 @@ def test_simple(self): self.assert_started(False) # Write something, it should then be started + have data - self.p.write(b'foobar') + self.p.write(b"foobar") self.assert_started() - self.assert_data(b'foobar') + self.assert_data(b"foobar") # Finalize, and check self.assert_finished(False) @@ -538,26 +508,26 @@ def test_simple(self): self.assert_finished() def test_multiple_chunks(self): - self.p.write(b'foo') - self.p.write(b'bar') - self.p.write(b'baz') + self.p.write(b"foo") + self.p.write(b"bar") + self.p.write(b"baz") self.p.finalize() - self.assert_data(b'foobarbaz') + self.assert_data(b"foobarbaz") self.assert_finished() def test_max_size(self): self.p.max_size = 5 - self.p.write(b'0123456789') + self.p.write(b"0123456789") self.p.finalize() - self.assert_data(b'01234') + self.assert_data(b"01234") self.assert_finished() def test_invalid_max_size(self): with self.assertRaises(ValueError): - q = OctetStreamParser(max_size='foo') + q = OctetStreamParser(max_size="foo") class TestBase64Decoder(unittest.TestCase): @@ -576,37 +546,37 @@ def assert_data(self, data, finalize=True): self.f.truncate() def test_simple(self): - self.d.write(b'Zm9vYmFy') - self.assert_data(b'foobar') + self.d.write(b"Zm9vYmFy") + self.assert_data(b"foobar") def test_bad(self): with self.assertRaises(DecodeError): - self.d.write(b'Zm9v!mFy') + self.d.write(b"Zm9v!mFy") def test_split_properly(self): - self.d.write(b'Zm9v') - self.d.write(b'YmFy') - self.assert_data(b'foobar') + self.d.write(b"Zm9v") + self.d.write(b"YmFy") + self.assert_data(b"foobar") def test_bad_split(self): - buff = b'Zm9v' + buff = b"Zm9v" for i in range(1, 4): first, second = buff[:i], buff[i:] self.setUp() self.d.write(first) self.d.write(second) - self.assert_data(b'foo') + self.assert_data(b"foo") def test_long_bad_split(self): - buff = b'Zm9vYmFy' + buff = b"Zm9vYmFy" for i in range(5, 8): first, second = buff[:i], buff[i:] self.setUp() self.d.write(first) self.d.write(second) - self.assert_data(b'foobar') + self.assert_data(b"foobar") def test_close_and_finalize(self): parser = Mock() @@ -619,7 +589,7 @@ def test_close_and_finalize(self): parser.close.assert_called_once_with() def test_bad_length(self): - self.d.write(b'Zm9vYmF') # missing ending 'y' + self.d.write(b"Zm9vYmF") # missing ending 'y' with self.assertRaises(DecodeError): self.d.finalize() @@ -640,35 +610,35 @@ def assert_data(self, data, finalize=True): self.f.truncate() def test_simple(self): - self.d.write(b'foobar') - self.assert_data(b'foobar') + self.d.write(b"foobar") + self.assert_data(b"foobar") def test_with_escape(self): - self.d.write(b'foo=3Dbar') - self.assert_data(b'foo=bar') + self.d.write(b"foo=3Dbar") + self.assert_data(b"foo=bar") def test_with_newline_escape(self): - self.d.write(b'foo=\r\nbar') - self.assert_data(b'foobar') + self.d.write(b"foo=\r\nbar") + self.assert_data(b"foobar") def test_with_only_newline_escape(self): - self.d.write(b'foo=\nbar') - self.assert_data(b'foobar') + self.d.write(b"foo=\nbar") + self.assert_data(b"foobar") def test_with_split_escape(self): - self.d.write(b'foo=3') - self.d.write(b'Dbar') - self.assert_data(b'foo=bar') + self.d.write(b"foo=3") + self.d.write(b"Dbar") + self.assert_data(b"foo=bar") def test_with_split_newline_escape_1(self): - self.d.write(b'foo=\r') - self.d.write(b'\nbar') - self.assert_data(b'foobar') + self.d.write(b"foo=\r") + self.d.write(b"\nbar") + self.assert_data(b"foobar") def test_with_split_newline_escape_2(self): - self.d.write(b'foo=') - self.d.write(b'\r\nbar') - self.assert_data(b'foobar') + self.d.write(b"foo=") + self.d.write(b"\r\nbar") + self.assert_data(b"foobar") def test_close_and_finalize(self): parser = Mock() @@ -684,23 +654,23 @@ def test_not_aligned(self): """ https://github.com/andrew-d/python-multipart/issues/6 """ - self.d.write(b'=3AX') - self.assert_data(b':X') + self.d.write(b"=3AX") + self.assert_data(b":X") # Additional offset tests - self.d.write(b'=3') - self.d.write(b'AX') - self.assert_data(b':X') + self.d.write(b"=3") + self.d.write(b"AX") + self.assert_data(b":X") - self.d.write(b'q=3AX') - self.assert_data(b'q:X') + self.d.write(b"q=3AX") + self.assert_data(b"q:X") # Load our list of HTTP test cases. -http_tests_dir = os.path.join(curr_dir, 'test_data', 'http') +http_tests_dir = os.path.join(curr_dir, "test_data", "http") # Read in all test cases and load them. -NON_PARAMETRIZED_TESTS = {'single_field_blocks'} +NON_PARAMETRIZED_TESTS = {"single_field_blocks"} http_tests = [] for f in os.listdir(http_tests_dir): # Only load the HTTP test cases. @@ -708,22 +678,18 @@ def test_not_aligned(self): if fname in NON_PARAMETRIZED_TESTS: continue - if ext == '.http': + if ext == ".http": # Get the YAML file and load it too. - yaml_file = os.path.join(http_tests_dir, fname + '.yaml') + yaml_file = os.path.join(http_tests_dir, fname + ".yaml") # Load both. - with open(os.path.join(http_tests_dir, f), 'rb') as f: + with open(os.path.join(http_tests_dir, f), "rb") as f: test_data = f.read() - with open(yaml_file, 'rb') as f: + with open(yaml_file, "rb") as f: yaml_data = yaml.safe_load(f) - http_tests.append({ - 'name': fname, - 'test': test_data, - 'result': yaml_data - }) + http_tests.append({"name": fname, "test": test_data, "result": yaml_data}) def split_all(val): @@ -754,8 +720,7 @@ def on_end(): self.ended = True # Get a form-parser instance. - self.f = FormParser('multipart/form-data', on_field, on_file, on_end, - boundary=boundary, config=config) + self.f = FormParser("multipart/form-data", on_field, on_file, on_end, boundary=boundary, config=config) def assert_file_data(self, f, data): o = f.file_object @@ -800,18 +765,18 @@ def assert_field(self, name, value): # Remove it for future iterations. self.fields.remove(found) - @parametrize('param', http_tests) + @parametrize("param", http_tests) def test_http(self, param): # Firstly, create our parser with the given boundary. - boundary = param['result']['boundary'] + boundary = param["result"]["boundary"] if isinstance(boundary, str): - boundary = boundary.encode('latin-1') + boundary = boundary.encode("latin-1") self.make(boundary) # Now, we feed the parser with data. exc = None try: - processed = self.f.write(param['test']) + processed = self.f.write(param["test"]) self.f.finalize() except MultipartParseError as e: processed = 0 @@ -823,29 +788,25 @@ def test_http(self, param): # print(repr(self.files)) # Do we expect an error? - if 'error' in param['result']['expected']: + if "error" in param["result"]["expected"]: self.assertIsNotNone(exc) - self.assertEqual(param['result']['expected']['error'], exc.offset) + self.assertEqual(param["result"]["expected"]["error"], exc.offset) return # No error! - self.assertEqual(processed, len(param['test'])) + self.assertEqual(processed, len(param["test"])) # Assert that the parser gave us the appropriate fields/files. - for e in param['result']['expected']: + for e in param["result"]["expected"]: # Get our type and name. - type = e['type'] - name = e['name'].encode('latin-1') + type = e["type"] + name = e["name"].encode("latin-1") - if type == 'field': - self.assert_field(name, e['data']) + if type == "field": + self.assert_field(name, e["data"]) - elif type == 'file': - self.assert_file( - name, - e['file_name'].encode('latin-1'), - e['data'] - ) + elif type == "file": + self.assert_file(name, e["file_name"].encode("latin-1"), e["data"]) else: assert False @@ -856,14 +817,14 @@ def test_random_splitting(self): through every possible split. """ # Load test data. - test_file = 'single_field_single_file.http' - with open(os.path.join(http_tests_dir, test_file), 'rb') as f: + test_file = "single_field_single_file.http" + with open(os.path.join(http_tests_dir, test_file), "rb") as f: test_data = f.read() # We split the file through all cases. for first, last in split_all(test_data): # Create form parser. - self.make('boundary') + self.make("boundary") # Feed with data in 2 chunks. i = 0 @@ -875,27 +836,27 @@ def test_random_splitting(self): self.assertEqual(i, len(test_data)) # Assert that our file and field are here. - self.assert_field(b'field', b'test1') - self.assert_file(b'file', b'file.txt', b'test2') + self.assert_field(b"field", b"test1") + self.assert_file(b"file", b"file.txt", b"test2") def test_feed_single_bytes(self): """ This test parses a simple multipart body 1 byte at a time. """ # Load test data. - test_file = 'single_field_single_file.http' - with open(os.path.join(http_tests_dir, test_file), 'rb') as f: + test_file = "single_field_single_file.http" + with open(os.path.join(http_tests_dir, test_file), "rb") as f: test_data = f.read() # Create form parser. - self.make('boundary') + self.make("boundary") # Write all bytes. # NOTE: Can't simply do `for b in test_data`, since that gives # an integer when iterating over a bytes object on Python 3. i = 0 for x in range(len(test_data)): - b = test_data[x:x + 1] + b = test_data[x : x + 1] i += self.f.write(b) self.f.finalize() @@ -904,24 +865,23 @@ def test_feed_single_bytes(self): self.assertEqual(i, len(test_data)) # Assert that our file and field are here. - self.assert_field(b'field', b'test1') - self.assert_file(b'file', b'file.txt', b'test2') + self.assert_field(b"field", b"test1") + self.assert_file(b"file", b"file.txt", b"test2") def test_feed_blocks(self): """ This test parses a simple multipart body 1 byte at a time. """ # Load test data. - test_file = 'single_field_blocks.http' - with open(os.path.join(http_tests_dir, test_file), 'rb') as f: + test_file = "single_field_blocks.http" + with open(os.path.join(http_tests_dir, test_file), "rb") as f: test_data = f.read() for c in range(1, len(test_data) + 1): # Skip first `d` bytes - not interesting for d in range(c): - # Create form parser. - self.make('boundary') + self.make("boundary") # Skip i = 0 self.f.write(test_data[:d]) @@ -930,7 +890,7 @@ def test_feed_blocks(self): # Write a chunk to achieve condition # `i == data_length - 1` # in boundary search loop (multipatr.py:1302) - b = test_data[x:x + c] + b = test_data[x : x + c] i += self.f.write(b) self.f.finalize() @@ -939,8 +899,7 @@ def test_feed_blocks(self): self.assertEqual(i, len(test_data)) # Assert that our field is here. - self.assert_field(b'field', - b'0123456789ABCDEFGHIJ0123456789ABCDEFGHIJ') + self.assert_field(b"field", b"0123456789ABCDEFGHIJ0123456789ABCDEFGHIJ") @slow_test def test_request_body_fuzz(self): @@ -953,8 +912,8 @@ def test_request_body_fuzz(self): - Randomly swapping two bytes """ # Load test data. - test_file = 'single_field_single_file.http' - with open(os.path.join(http_tests_dir, test_file), 'rb') as f: + test_file = "single_field_single_file.http" + with open(os.path.join(http_tests_dir, test_file), "rb") as f: test_data = f.read() iterations = 1000 @@ -995,7 +954,7 @@ def test_request_body_fuzz(self): print(" " + msg) # Create form parser. - self.make('boundary') + self.make("boundary") # Feed with data, and ignore form parser exceptions. i = 0 @@ -1033,7 +992,7 @@ def test_request_body_fuzz_random_data(self): print(" Testing with %d random bytes..." % (data_size,)) # Create form parser. - self.make('boundary') + self.make("boundary") # Feed with data, and ignore form parser exceptions. i = 0 @@ -1054,40 +1013,44 @@ def test_request_body_fuzz_random_data(self): print("Exceptions: %d" % (exceptions,)) def test_bad_start_boundary(self): - self.make('boundary') - data = b'--boundary\rfoobar' + self.make("boundary") + data = b"--boundary\rfoobar" with self.assertRaises(MultipartParseError): self.f.write(data) - self.make('boundary') - data = b'--boundaryfoobar' + self.make("boundary") + data = b"--boundaryfoobar" with self.assertRaises(MultipartParseError): i = self.f.write(data) def test_octet_stream(self): files = [] + def on_file(f): files.append(f) + on_field = Mock() on_end = Mock() - f = FormParser('application/octet-stream', on_field, on_file, on_end=on_end, file_name=b'foo.txt') + f = FormParser("application/octet-stream", on_field, on_file, on_end=on_end, file_name=b"foo.txt") self.assertTrue(isinstance(f.parser, OctetStreamParser)) - f.write(b'test') - f.write(b'1234') + f.write(b"test") + f.write(b"1234") f.finalize() # Assert that we only received a single file, with the right data, and that we're done. self.assertFalse(on_field.called) self.assertEqual(len(files), 1) - self.assert_file_data(files[0], b'test1234') + self.assert_file_data(files[0], b"test1234") self.assertTrue(on_end.called) def test_querystring(self): fields = [] + def on_field(f): fields.append(f) + on_file = Mock() on_end = Mock() @@ -1098,8 +1061,8 @@ def simple_test(f): on_end.reset_mock() # Write test data. - f.write(b'foo=bar') - f.write(b'&test=asdf') + f.write(b"foo=bar") + f.write(b"&test=asdf") f.finalize() # Assert we only received 2 fields... @@ -1107,26 +1070,26 @@ def simple_test(f): self.assertEqual(len(fields), 2) # ...assert that we have the correct data... - self.assertEqual(fields[0].field_name, b'foo') - self.assertEqual(fields[0].value, b'bar') + self.assertEqual(fields[0].field_name, b"foo") + self.assertEqual(fields[0].value, b"bar") - self.assertEqual(fields[1].field_name, b'test') - self.assertEqual(fields[1].value, b'asdf') + self.assertEqual(fields[1].field_name, b"test") + self.assertEqual(fields[1].value, b"asdf") # ... and assert that we've finished. self.assertTrue(on_end.called) - f = FormParser('application/x-www-form-urlencoded', on_field, on_file, on_end=on_end) + f = FormParser("application/x-www-form-urlencoded", on_field, on_file, on_end=on_end) self.assertTrue(isinstance(f.parser, QuerystringParser)) simple_test(f) - f = FormParser('application/x-url-encoded', on_field, on_file, on_end=on_end) + f = FormParser("application/x-url-encoded", on_field, on_file, on_end=on_end) self.assertTrue(isinstance(f.parser, QuerystringParser)) simple_test(f) def test_close_methods(self): parser = Mock() - f = FormParser('application/x-url-encoded', None, None) + f = FormParser("application/x-url-encoded", None, None) f.parser = parser f.finalize() @@ -1138,69 +1101,76 @@ def test_close_methods(self): def test_bad_content_type(self): # We should raise a ValueError for a bad Content-Type with self.assertRaises(ValueError): - f = FormParser('application/bad', None, None) + f = FormParser("application/bad", None, None) def test_no_boundary_given(self): # We should raise a FormParserError when parsing a multipart message # without a boundary. with self.assertRaises(FormParserError): - f = FormParser('multipart/form-data', None, None) + f = FormParser("multipart/form-data", None, None) def test_bad_content_transfer_encoding(self): - data = b'----boundary\r\nContent-Disposition: form-data; name="file"; filename="test.txt"\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: badstuff\r\n\r\nTest\r\n----boundary--\r\n' + data = ( + b'----boundary\r\nContent-Disposition: form-data; name="file"; filename="test.txt"\r\n' + b"Content-Type: text/plain\r\n" + b"Content-Transfer-Encoding: badstuff\r\n\r\n" + b"Test\r\n----boundary--\r\n" + ) files = [] + def on_file(f): files.append(f) + on_field = Mock() on_end = Mock() # Test with erroring. - config = {'UPLOAD_ERROR_ON_BAD_CTE': True} - f = FormParser('multipart/form-data', on_field, on_file, - on_end=on_end, boundary='--boundary', config=config) + config = {"UPLOAD_ERROR_ON_BAD_CTE": True} + f = FormParser("multipart/form-data", on_field, on_file, on_end=on_end, boundary="--boundary", config=config) with self.assertRaises(FormParserError): f.write(data) f.finalize() # Test without erroring. - config = {'UPLOAD_ERROR_ON_BAD_CTE': False} - f = FormParser('multipart/form-data', on_field, on_file, - on_end=on_end, boundary='--boundary', config=config) + config = {"UPLOAD_ERROR_ON_BAD_CTE": False} + f = FormParser("multipart/form-data", on_field, on_file, on_end=on_end, boundary="--boundary", config=config) f.write(data) f.finalize() - self.assert_file_data(files[0], b'Test') + self.assert_file_data(files[0], b"Test") def test_handles_None_fields(self): fields = [] + def on_field(f): fields.append(f) + on_file = Mock() on_end = Mock() - f = FormParser('application/x-www-form-urlencoded', on_field, on_file, on_end=on_end) - f.write(b'foo=bar&another&baz=asdf') + f = FormParser("application/x-www-form-urlencoded", on_field, on_file, on_end=on_end) + f.write(b"foo=bar&another&baz=asdf") f.finalize() - self.assertEqual(fields[0].field_name, b'foo') - self.assertEqual(fields[0].value, b'bar') + self.assertEqual(fields[0].field_name, b"foo") + self.assertEqual(fields[0].value, b"bar") - self.assertEqual(fields[1].field_name, b'another') + self.assertEqual(fields[1].field_name, b"another") self.assertEqual(fields[1].value, None) - self.assertEqual(fields[2].field_name, b'baz') - self.assertEqual(fields[2].value, b'asdf') + self.assertEqual(fields[2].field_name, b"baz") + self.assertEqual(fields[2].value, b"asdf") def test_max_size_multipart(self): # Load test data. - test_file = 'single_field_single_file.http' - with open(os.path.join(http_tests_dir, test_file), 'rb') as f: + test_file = "single_field_single_file.http" + with open(os.path.join(http_tests_dir, test_file), "rb") as f: test_data = f.read() # Create form parser. - self.make('boundary') + self.make("boundary") # Set the maximum length that we can process to be halfway through the # given data. @@ -1214,14 +1184,14 @@ def test_max_size_multipart(self): def test_max_size_form_parser(self): # Load test data. - test_file = 'single_field_single_file.http' - with open(os.path.join(http_tests_dir, test_file), 'rb') as f: + test_file = "single_field_single_file.http" + with open(os.path.join(http_tests_dir, test_file), "rb") as f: test_data = f.read() # Create form parser setting the maximum length that we can process to # be halfway through the given data. size = len(test_data) / 2 - self.make('boundary', config={'MAX_BODY_SIZE': size}) + self.make("boundary", config={"MAX_BODY_SIZE": size}) i = self.f.write(test_data) self.f.finalize() @@ -1231,29 +1201,35 @@ def test_max_size_form_parser(self): def test_octet_stream_max_size(self): files = [] + def on_file(f): files.append(f) + on_field = Mock() on_end = Mock() - f = FormParser('application/octet-stream', on_field, on_file, - on_end=on_end, file_name=b'foo.txt', - config={'MAX_BODY_SIZE': 10}) + f = FormParser( + "application/octet-stream", + on_field, + on_file, + on_end=on_end, + file_name=b"foo.txt", + config={"MAX_BODY_SIZE": 10}, + ) - f.write(b'0123456789012345689') + f.write(b"0123456789012345689") f.finalize() - self.assert_file_data(files[0], b'0123456789') + self.assert_file_data(files[0], b"0123456789") def test_invalid_max_size_multipart(self): with self.assertRaises(ValueError): - q = MultipartParser(b'bound', max_size='foo') + q = MultipartParser(b"bound", max_size="foo") class TestHelperFunctions(unittest.TestCase): def test_create_form_parser(self): - r = create_form_parser({'Content-Type': 'application/octet-stream'}, - None, None) + r = create_form_parser({"Content-Type": "application/octet-stream"}, None, None) self.assertTrue(isinstance(r, FormParser)) def test_create_form_parser_error(self): @@ -1265,13 +1241,7 @@ def test_parse_form(self): on_field = Mock() on_file = Mock() - parse_form( - {'Content-Type': 'application/octet-stream', - }, - BytesIO(b'123456789012345'), - on_field, - on_file - ) + parse_form({"Content-Type": "application/octet-stream"}, BytesIO(b"123456789012345"), on_field, on_file) assert on_file.call_count == 1 @@ -1281,23 +1251,21 @@ def test_parse_form(self): def test_parse_form_content_length(self): files = [] + def on_file(file): files.append(file) parse_form( - {'Content-Type': 'application/octet-stream', - 'Content-Length': '10' - }, - BytesIO(b'123456789012345'), + {"Content-Type": "application/octet-stream", "Content-Length": "10"}, + BytesIO(b"123456789012345"), None, - on_file + on_file, ) self.assertEqual(len(files), 1) self.assertEqual(files[0].size, 10) - def suite(): suite = unittest.TestSuite() suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestFile)) From 9c936b739faa23af4133f4697f363d2f64f01d2a Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Feb 2024 13:26:13 +0100 Subject: [PATCH 26/94] Add `QuerystringState` (#97) --- .gitignore | 2 +- multipart/multipart.py | 35 +++++++++++++++++++++-------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index cfa9998..546cf0a 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,7 @@ lib64 pip-log.txt # Unit test / coverage reports -.coverage +.coverage.* .tox nosetests.xml diff --git a/multipart/multipart.py b/multipart/multipart.py index a427f14..651bfc1 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -17,10 +17,17 @@ # Unique missing object. _missing = object() -# States for the querystring parser. -STATE_BEFORE_FIELD = 0 -STATE_FIELD_NAME = 1 -STATE_FIELD_DATA = 2 + +class QuerystringState(IntEnum): + """Querystring parser states. + + These are used to keep track of the state of the parser, and are used to determine + what to do when new data is encountered. + """ + + BEFORE_FIELD = 0 + FIELD_NAME = 1 + FIELD_DATA = 2 class MultipartState(IntEnum): @@ -727,7 +734,7 @@ class QuerystringParser(BaseParser): def __init__(self, callbacks={}, strict_parsing=False, max_size=float("inf")): super().__init__() - self.state = STATE_BEFORE_FIELD + self.state = QuerystringState.BEFORE_FIELD self._found_sep = False self.callbacks = callbacks @@ -783,7 +790,7 @@ def _internal_write(self, data, length): ch = data[i] # Depending on our state... - if state == STATE_BEFORE_FIELD: + if state == QuerystringState.BEFORE_FIELD: # If the 'found_sep' flag is set, we've already encountered # and skipped a single separator. If so, we check our strict # parsing flag and decide what to do. Otherwise, we haven't @@ -810,10 +817,10 @@ def _internal_write(self, data, length): # this state. self.callback("field_start") i -= 1 - state = STATE_FIELD_NAME + state = QuerystringState.FIELD_NAME found_sep = False - elif state == STATE_FIELD_NAME: + elif state == QuerystringState.FIELD_NAME: # Try and find a separator - we ensure that, if we do, we only # look for the equal sign before it. sep_pos = data.find(b"&", i) @@ -836,11 +843,11 @@ def _internal_write(self, data, length): # added to it below, which means the next iteration of this # loop will inspect the character after the equals sign. i = equals_pos - state = STATE_FIELD_DATA + state = QuerystringState.FIELD_DATA else: # No equals sign found. if not strict_parsing: - # See also comments in the STATE_FIELD_DATA case below. + # See also comments in the QuerystringState.FIELD_DATA case below. # If we found the separator, we emit the name and just # end - there's no data callback at all (not even with # a blank value). @@ -849,7 +856,7 @@ def _internal_write(self, data, length): self.callback("field_end") i = sep_pos - 1 - state = STATE_BEFORE_FIELD + state = QuerystringState.BEFORE_FIELD else: # Otherwise, no separator in this block, so the # rest of this chunk must be a name. @@ -873,7 +880,7 @@ def _internal_write(self, data, length): self.callback("field_name", data, i, length) i = length - elif state == STATE_FIELD_DATA: + elif state == QuerystringState.FIELD_DATA: # Try finding either an ampersand or a semicolon after this # position. sep_pos = data.find(b"&", i) @@ -891,7 +898,7 @@ def _internal_write(self, data, length): # "field_start" events only when we actually have data for # a field of some sort. i = sep_pos - 1 - state = STATE_BEFORE_FIELD + state = QuerystringState.BEFORE_FIELD # Otherwise, emit the rest as data and finish. else: @@ -917,7 +924,7 @@ def finalize(self): then the on_end callback. """ # If we're currently in the middle of a field, we finish it. - if self.state == STATE_FIELD_DATA: + if self.state == QuerystringState.FIELD_DATA: self.callback("field_end") self.callback("end") From 8fdf6f897937f09afa3bcc4c5d4916f17928310a Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Feb 2024 13:49:11 +0100 Subject: [PATCH 27/94] Add `TypedDict` callbacks (#98) --- .gitignore | 2 +- multipart/multipart.py | 75 ++++++++++++++++++++++++++++------------- tests/test_multipart.py | 16 ++++----- 3 files changed, 60 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index 546cf0a..f7f7b71 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,7 @@ lib64 pip-log.txt # Unit test / coverage reports -.coverage.* +.coverage* .tox nosetests.xml diff --git a/multipart/multipart.py b/multipart/multipart.py index 651bfc1..ac2648e 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -9,11 +9,38 @@ from enum import IntEnum from io import BytesIO from numbers import Number -from typing import Dict, Tuple, Union +from typing import TYPE_CHECKING from .decoders import Base64Decoder, QuotedPrintableDecoder from .exceptions import FileError, FormParserError, MultipartParseError, QuerystringParseError +if TYPE_CHECKING: # pragma: no cover + from typing import Callable, TypedDict + + class QuerystringCallbacks(TypedDict, total=False): + on_field_start: Callable[[], None] + on_field_name: Callable[[bytes, int, int], None] + on_field_data: Callable[[bytes, int, int], None] + on_field_end: Callable[[], None] + on_end: Callable[[], None] + + class OctetStreamCallbacks(TypedDict, total=False): + on_start: Callable[[], None] + on_data: Callable[[bytes, int, int], None] + on_end: Callable[[], None] + + class MultipartCallbacks(TypedDict, total=False): + on_part_begin: Callable[[], None] + on_part_data: Callable[[bytes, int, int], None] + on_part_end: Callable[[], None] + on_headers_begin: Callable[[], None] + on_header_field: Callable[[bytes, int, int], None] + on_header_value: Callable[[bytes, int, int], None] + on_header_end: Callable[[], None] + on_headers_finished: Callable[[], None] + on_end: Callable[[], None] + + # Unique missing object. _missing = object() @@ -86,7 +113,7 @@ def join_bytes(b): return bytes(list(b)) -def parse_options_header(value: Union[str, bytes]) -> Tuple[bytes, Dict[bytes, bytes]]: +def parse_options_header(value: str | bytes) -> tuple[bytes, dict[bytes, bytes]]: """ Parses a Content-Type header into a value in the following format: (content_type, {parameters}) @@ -148,15 +175,15 @@ class Field: :param name: the name of the form field """ - def __init__(self, name): + def __init__(self, name: str): self._name = name - self._value = [] + self._value: list[bytes] = [] # We cache the joined version of _value for speed. self._cache = _missing @classmethod - def from_value(klass, name, value): + def from_value(cls, name: str, value: bytes | None) -> Field: """Create an instance of a :class:`Field`, and set the corresponding value - either None or an actual value. This method will also finalize the Field itself. @@ -166,7 +193,7 @@ def from_value(klass, name, value): None """ - f = klass(name) + f = cls(name) if value is None: f.set_none() else: @@ -174,14 +201,14 @@ def from_value(klass, name, value): f.finalize() return f - def write(self, data): + def write(self, data: bytes) -> int: """Write some data into the form field. :param data: a bytestring """ return self.on_data(data) - def on_data(self, data): + def on_data(self, data: bytes) -> int: """This method is a callback that will be called whenever data is written to the Field. @@ -191,16 +218,16 @@ def on_data(self, data): self._cache = _missing return len(data) - def on_end(self): + def on_end(self) -> None: """This method is called whenever the Field is finalized.""" if self._cache is _missing: self._cache = b"".join(self._value) - def finalize(self): + def finalize(self) -> None: """Finalize the form field.""" self.on_end() - def close(self): + def close(self) -> None: """Close the Field object. This will free any underlying cache.""" # Free our value array. if self._cache is _missing: @@ -208,7 +235,7 @@ def close(self): del self._value - def set_none(self): + def set_none(self) -> None: """Some fields in a querystring can possibly have a value of None - for example, the string "foo&bar=&baz=asdf" will have a field with the name "foo" and value None, one with name "bar" and value "", and one @@ -218,7 +245,7 @@ def set_none(self): self._cache = None @property - def field_name(self): + def field_name(self) -> str: """This property returns the name of the field.""" return self._name @@ -230,13 +257,13 @@ def value(self): return self._cache - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if isinstance(other, Field): return self.field_name == other.field_name and self.value == other.value else: return NotImplemented - def __repr__(self): + def __repr__(self) -> str: if len(self.value) > 97: # We get the repr, and then insert three dots before the final # quote. @@ -553,7 +580,7 @@ class BaseParser: def __init__(self): self.logger = logging.getLogger(__name__) - def callback(self, name, data=None, start=None, end=None): + def callback(self, name: str, data=None, start=None, end=None): """This function calls a provided callback with some data. If the callback is not set, will do nothing. @@ -584,7 +611,7 @@ def callback(self, name, data=None, start=None, end=None): self.logger.debug("Calling %s with no data", name) func() - def set_callback(self, name, new_func): + def set_callback(self, name: str, new_func): """Update the function for a callback. Removes from the callbacks dict if new_func is None. @@ -637,7 +664,7 @@ class OctetStreamParser(BaseParser): i.e. unbounded. """ - def __init__(self, callbacks={}, max_size=float("inf")): + def __init__(self, callbacks: OctetStreamCallbacks = {}, max_size=float("inf")): super().__init__() self.callbacks = callbacks self._started = False @@ -647,7 +674,7 @@ def __init__(self, callbacks={}, max_size=float("inf")): self.max_size = max_size self._current_size = 0 - def write(self, data): + def write(self, data: bytes): """Write some data to the parser, which will perform size verification, and then pass the data to the underlying callback. @@ -732,7 +759,9 @@ class QuerystringParser(BaseParser): i.e. unbounded. """ - def __init__(self, callbacks={}, strict_parsing=False, max_size=float("inf")): + state: QuerystringState + + def __init__(self, callbacks: QuerystringCallbacks = {}, strict_parsing=False, max_size=float("inf")): super().__init__() self.state = QuerystringState.BEFORE_FIELD self._found_sep = False @@ -748,7 +777,7 @@ def __init__(self, callbacks={}, strict_parsing=False, max_size=float("inf")): # Should parsing be strict? self.strict_parsing = strict_parsing - def write(self, data): + def write(self, data: bytes): """Write some data to the parser, which will perform size verification, parse into either a field name or value, and then pass the corresponding data to the underlying callback. If an error is @@ -780,7 +809,7 @@ def write(self, data): return l - def _internal_write(self, data, length): + def _internal_write(self, data: bytes, length: int): state = self.state strict_parsing = self.strict_parsing found_sep = self._found_sep @@ -989,7 +1018,7 @@ class MultipartParser(BaseParser): i.e. unbounded. """ - def __init__(self, boundary, callbacks={}, max_size=float("inf")): + def __init__(self, boundary, callbacks: MultipartCallbacks = {}, max_size=float("inf")): # Initialize parser state. super().__init__() self.state = MultipartState.START diff --git a/tests/test_multipart.py b/tests/test_multipart.py index b9cba86..16db5b3 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -333,9 +333,9 @@ def on_field_end(): del name_buffer[:] del data_buffer[:] - callbacks = {"on_field_name": on_field_name, "on_field_data": on_field_data, "on_field_end": on_field_end} - - self.p = QuerystringParser(callbacks) + self.p = QuerystringParser( + callbacks={"on_field_name": on_field_name, "on_field_data": on_field_data, "on_field_end": on_field_end} + ) def test_simple_querystring(self): self.p.write(b"foo=bar") @@ -464,18 +464,16 @@ def setUp(self): self.started = 0 self.finished = 0 - def on_start(): + def on_start() -> None: self.started += 1 - def on_data(data, start, end): + def on_data(data: bytes, start: int, end: int) -> None: self.d.append(data[start:end]) - def on_end(): + def on_end() -> None: self.finished += 1 - callbacks = {"on_start": on_start, "on_data": on_data, "on_end": on_end} - - self.p = OctetStreamParser(callbacks) + self.p = OctetStreamParser(callbacks={"on_start": on_start, "on_data": on_data, "on_end": on_end}) def assert_data(self, data, finalize=True): self.assertEqual(b"".join(self.d), data) From c26e6d1f6ddfd02bf0e0ad5e440a7e831825e97b Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Feb 2024 14:25:24 +0100 Subject: [PATCH 28/94] Add config `TypedDict`s (#99) --- multipart/multipart.py | 159 +++++++++++++++++++++++------------------ 1 file changed, 88 insertions(+), 71 deletions(-) diff --git a/multipart/multipart.py b/multipart/multipart.py index ac2648e..221bb71 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -40,6 +40,21 @@ class MultipartCallbacks(TypedDict, total=False): on_headers_finished: Callable[[], None] on_end: Callable[[], None] + class FormParserConfig(TypedDict, total=False): + UPLOAD_DIR: str | None + UPLOAD_KEEP_FILENAME: bool + UPLOAD_KEEP_EXTENSIONS: bool + UPLOAD_ERROR_ON_BAD_CTE: bool + MAX_MEMORY_FILE_SIZE: int + MAX_BODY_SIZE: float + + class FileConfig(TypedDict, total=False): + UPLOAD_DIR: str | None + UPLOAD_DELETE_TMP: bool + UPLOAD_KEEP_FILENAME: bool + UPLOAD_KEEP_EXTENSIONS: bool + MAX_MEMORY_FILE_SIZE: int + # Unique missing object. _missing = object() @@ -334,7 +349,7 @@ class File: configuration keys and their corresponding values. """ - def __init__(self, file_name, field_name=None, config={}): + def __init__(self, file_name: bytes | None, field_name: bytes | None = None, config: FileConfig = {}): # Save configuration, set other variables default. self.logger = logging.getLogger(__name__) self._config = config @@ -357,14 +372,14 @@ def __init__(self, file_name, field_name=None, config={}): self._ext = ext @property - def field_name(self): + def field_name(self) -> bytes | None: """The form field associated with this file. May be None if there isn't one, for example when we have an application/octet-stream upload. """ return self._field_name @property - def file_name(self): + def file_name(self) -> bytes | None: """The file name given in the upload request.""" return self._file_name @@ -391,13 +406,13 @@ def size(self): return self._bytes_written @property - def in_memory(self): + def in_memory(self) -> bool: """A boolean representing whether or not this file object is currently stored in-memory or on-disk. """ return self._in_memory - def flush_to_disk(self): + def flush_to_disk(self) -> None: """If the file is already on-disk, do nothing. Otherwise, copy from the in-memory buffer to a disk file, and then reassign our internal file object to this new disk file. @@ -495,14 +510,14 @@ def _get_disk_file(self): self._actual_file_name = fname return tmp_file - def write(self, data): + def write(self, data: bytes): """Write some data to the File. :param data: a bytestring """ return self.on_data(data) - def on_data(self, data): + def on_data(self, data: bytes): """This method is a callback that will be called whenever data is written to the File. @@ -534,25 +549,25 @@ def on_data(self, data): # Return the number of bytes written. return bwritten - def on_end(self): + def on_end(self) -> None: """This method is called whenever the Field is finalized.""" # Flush the underlying file object self._fileobj.flush() - def finalize(self): + def finalize(self) -> None: """Finalize the form file. This will not close the underlying file, but simply signal that we are finished writing to the File. """ self.on_end() - def close(self): + def close(self) -> None: """Close the File object. This will actually close the underlying file object (whether it's a :class:`io.BytesIO` or an actual file object). """ self._fileobj.close() - def __repr__(self): + def __repr__(self) -> str: return "{}(file_name={!r}, field_name={!r})".format(self.__class__.__name__, self.file_name, self.field_name) @@ -703,13 +718,13 @@ def write(self, data: bytes): self.callback("data", data, 0, data_len) return data_len - def finalize(self): + def finalize(self) -> None: """Finalize this parser, which signals to that we are finished parsing, and sends the on_end callback. """ self.callback("end") - def __repr__(self): + def __repr__(self) -> str: return "%s()" % self.__class__.__name__ @@ -761,7 +776,7 @@ class QuerystringParser(BaseParser): state: QuerystringState - def __init__(self, callbacks: QuerystringCallbacks = {}, strict_parsing=False, max_size=float("inf")): + def __init__(self, callbacks: QuerystringCallbacks = {}, strict_parsing: bool = False, max_size=float("inf")): super().__init__() self.state = QuerystringState.BEFORE_FIELD self._found_sep = False @@ -777,7 +792,7 @@ def __init__(self, callbacks: QuerystringCallbacks = {}, strict_parsing=False, m # Should parsing be strict? self.strict_parsing = strict_parsing - def write(self, data: bytes): + def write(self, data: bytes) -> int: """Write some data to the parser, which will perform size verification, parse into either a field name or value, and then pass the corresponding data to the underlying callback. If an error is @@ -809,7 +824,7 @@ def write(self, data: bytes): return l - def _internal_write(self, data: bytes, length: int): + def _internal_write(self, data: bytes, length: int) -> int: state = self.state strict_parsing = self.strict_parsing found_sep = self._found_sep @@ -947,7 +962,7 @@ def _internal_write(self, data: bytes, length: int): self._found_sep = found_sep return len(data) - def finalize(self): + def finalize(self) -> None: """Finalize this parser, which signals to that we are finished parsing, if we're still in the middle of a field, an on_field_end callback, and then the on_end callback. @@ -957,7 +972,7 @@ def finalize(self): self.callback("field_end") self.callback("end") - def __repr__(self): + def __repr__(self) -> str: return "{}(strict_parsing={!r}, max_size={!r})".format( self.__class__.__name__, self.strict_parsing, self.max_size ) @@ -1018,7 +1033,7 @@ class MultipartParser(BaseParser): i.e. unbounded. """ - def __init__(self, boundary, callbacks: MultipartCallbacks = {}, max_size=float("inf")): + def __init__(self, boundary: bytes | str, callbacks: MultipartCallbacks = {}, max_size=float("inf")): # Initialize parser state. super().__init__() self.state = MultipartState.START @@ -1057,7 +1072,7 @@ def __init__(self, boundary, callbacks: MultipartCallbacks = {}, max_size=float( # '--\r\n' is 8 bytes. self.lookbehind = [NULL for x in range(len(boundary) + 8)] - def write(self, data): + def write(self, data: bytes) -> int: """Write some data to the parser, which will perform size verification, and then parse the data into the appropriate location (e.g. header, data, etc.), and pass this on to the underlying callback. If an error @@ -1089,7 +1104,7 @@ def write(self, data): return l - def _internal_write(self, data, length): + def _internal_write(self, data: bytes, length: int) -> int: # Get values from locals. boundary = self.boundary @@ -1478,7 +1493,7 @@ def data_callback(name, remaining=False): # all of it. return length - def finalize(self): + def finalize(self) -> None: """Finalize this parser, which signals to that we are finished parsing. Note: It does not currently, but in the future, it will verify that we @@ -1548,7 +1563,7 @@ class FormParser: #: This is the default configuration for our form parser. #: Note: all file sizes should be in bytes. - DEFAULT_CONFIG = { + DEFAULT_CONFIG: FormParserConfig = { "MAX_BODY_SIZE": float("inf"), "MAX_MEMORY_FILE_SIZE": 1 * 1024 * 1024, "UPLOAD_DIR": None, @@ -1568,7 +1583,7 @@ def __init__( file_name=None, FileClass=File, FieldClass=Field, - config={}, + config: FormParserConfig = {}, ): self.logger = logging.getLogger(__name__) @@ -1597,13 +1612,13 @@ def __init__( class vars: f = None - def on_start(): + def on_start() -> None: vars.f = FileClass(file_name, None, config=self.config) - def on_data(data, start, end): + def on_data(data: bytes, start: int, end: int) -> None: vars.f.write(data[start:end]) - def on_end(): + def on_end() -> None: # Finalize the file itself. vars.f.finalize() @@ -1614,30 +1629,31 @@ def on_end(): if self.on_end is not None: self.on_end() - callbacks = {"on_start": on_start, "on_data": on_data, "on_end": on_end} - # Instantiate an octet-stream parser - parser = OctetStreamParser(callbacks, max_size=self.config["MAX_BODY_SIZE"]) + parser = OctetStreamParser( + callbacks={"on_start": on_start, "on_data": on_data, "on_end": on_end}, + max_size=self.config["MAX_BODY_SIZE"], + ) elif content_type == "application/x-www-form-urlencoded" or content_type == "application/x-url-encoded": - name_buffer = [] + name_buffer: list[bytes] = [] class vars: f = None - def on_field_start(): + def on_field_start() -> None: pass - def on_field_name(data, start, end): + def on_field_name(data: bytes, start: int, end: int) -> None: name_buffer.append(data[start:end]) - def on_field_data(data, start, end): + def on_field_data(data: bytes, start: int, end: int) -> None: if vars.f is None: vars.f = FieldClass(b"".join(name_buffer)) del name_buffer[:] vars.f.write(data[start:end]) - def on_field_end(): + def on_field_end() -> None: # Finalize and call callback. if vars.f is None: # If we get here, it's because there was no field data. @@ -1650,29 +1666,29 @@ def on_field_end(): on_field(vars.f) vars.f = None - def on_end(): + def on_end() -> None: if self.on_end is not None: self.on_end() - # Setup callbacks. - callbacks = { - "on_field_start": on_field_start, - "on_field_name": on_field_name, - "on_field_data": on_field_data, - "on_field_end": on_field_end, - "on_end": on_end, - } - # Instantiate parser. - parser = QuerystringParser(callbacks=callbacks, max_size=self.config["MAX_BODY_SIZE"]) + parser = QuerystringParser( + callbacks={ + "on_field_start": on_field_start, + "on_field_name": on_field_name, + "on_field_data": on_field_data, + "on_field_end": on_field_end, + "on_end": on_end, + }, + max_size=self.config["MAX_BODY_SIZE"], + ) elif content_type == "multipart/form-data": if boundary is None: self.logger.error("No boundary given") raise FormParserError("No boundary given") - header_name = [] - header_value = [] + header_name: list[bytes] = [] + header_value: list[bytes] = [] headers = {} # No 'nonlocal' on Python 2 :-( @@ -1684,22 +1700,22 @@ class vars: def on_part_begin(): pass - def on_part_data(data, start, end): + def on_part_data(data: bytes, start: int, end: int): bytes_processed = vars.writer.write(data[start:end]) # TODO: check for error here. return bytes_processed - def on_part_end(): + def on_part_end() -> None: vars.f.finalize() if vars.is_file: on_file(vars.f) else: on_field(vars.f) - def on_header_field(data, start, end): + def on_header_field(data: bytes, start: int, end: int): header_name.append(data[start:end]) - def on_header_value(data, start, end): + def on_header_value(data: bytes, start: int, end: int): header_value.append(data[start:end]) def on_header_end(): @@ -1707,7 +1723,7 @@ def on_header_end(): del header_name[:] del header_value[:] - def on_headers_finished(): + def on_headers_finished() -> None: # Reset the 'is file' flag. vars.is_file = False @@ -1751,25 +1767,26 @@ def on_headers_finished(): # unencoded Content-Transfer-Encoding. vars.writer = vars.f - def on_end(): + def on_end() -> None: vars.writer.finalize() if self.on_end is not None: self.on_end() - # These are our callbacks for the parser. - callbacks = { - "on_part_begin": on_part_begin, - "on_part_data": on_part_data, - "on_part_end": on_part_end, - "on_header_field": on_header_field, - "on_header_value": on_header_value, - "on_header_end": on_header_end, - "on_headers_finished": on_headers_finished, - "on_end": on_end, - } - # Instantiate a multipart parser. - parser = MultipartParser(boundary, callbacks, max_size=self.config["MAX_BODY_SIZE"]) + parser = MultipartParser( + boundary, + callbacks={ + "on_part_begin": on_part_begin, + "on_part_data": on_part_data, + "on_part_end": on_part_end, + "on_header_field": on_header_field, + "on_header_value": on_header_value, + "on_header_end": on_header_end, + "on_headers_finished": on_headers_finished, + "on_end": on_end, + }, + max_size=self.config["MAX_BODY_SIZE"], + ) else: self.logger.warning("Unknown Content-Type: %r", content_type) @@ -1777,7 +1794,7 @@ def on_end(): self.parser = parser - def write(self, data): + def write(self, data: bytes): """Write some data. The parser will forward this to the appropriate underlying parser. @@ -1787,17 +1804,17 @@ def write(self, data): # TODO: check the parser's return value for errors? return self.parser.write(data) - def finalize(self): + def finalize(self) -> None: """Finalize the parser.""" if self.parser is not None and hasattr(self.parser, "finalize"): self.parser.finalize() - def close(self): + def close(self) -> None: """Close the parser.""" if self.parser is not None and hasattr(self.parser, "close"): self.parser.close() - def __repr__(self): + def __repr__(self) -> str: return "{}(content_type={!r}, parser={!r})".format(self.__class__.__name__, self.content_type, self.parser) From 3035c45b87a4a1bcb857e17f0ecbc4696ea75e47 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Feb 2024 14:30:24 +0100 Subject: [PATCH 29/94] Version 0.0.9 (#100) --- CHANGELOG.md | 9 +++++++++ multipart/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d6f080..23c0fff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.0.9 (2024-02-10) + +* Add support for Python 3.12 [#85](https://github.com/Kludex/python-multipart/pull/85). +* Drop support for Python 3.7 [#95](https://github.com/Kludex/python-multipart/pull/95). +* Add `MultipartState(IntEnum)` [#96](https://github.com/Kludex/python-multipart/pull/96). +* Add `QuerystringState` [#97](https://github.com/Kludex/python-multipart/pull/97). +* Add `TypedDict` callbacks [#98](https://github.com/Kludex/python-multipart/pull/98). +* Add config `TypedDict`s [#99](https://github.com/Kludex/python-multipart/pull/99). + ## 0.0.8 (2024-02-09) * Check if Message.get_params return 3-tuple instead of str on parse_options_header [#79](https://github.com/Kludex/python-multipart/pull/79). diff --git a/multipart/__init__.py b/multipart/__init__.py index 3c8a2e8..dc13f13 100644 --- a/multipart/__init__.py +++ b/multipart/__init__.py @@ -2,7 +2,7 @@ __author__ = "Andrew Dunham" __license__ = "Apache" __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" -__version__ = "0.0.8" +__version__ = "0.0.9" from .multipart import FormParser, MultipartParser, OctetStreamParser, QuerystringParser, create_form_parser, parse_form From 6181731b0d82875d5d1d207068d661e71b1974b9 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Feb 2024 15:35:12 +0100 Subject: [PATCH 30/94] Update links to Kludex (#102) --- .github/workflows/main.yaml | 2 +- README.md | 23 +++++++++++++++++++++++ README.rst | 28 ---------------------------- pyproject.toml | 13 ++++++++----- 4 files changed, 32 insertions(+), 34 deletions(-) create mode 100644 README.md delete mode 100644 README.rst diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 7b4fd5c..072ae70 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,4 +1,4 @@ -name: Python package +name: CI on: push: diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a6f970 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Python-Multipart + +[![Build Status](https://github.com/Kludex/python-multipart/workflows/CI/badge.svg)](https://github.com/Kludex/python-multipart/actions) +[![Package version](https://badge.fury.io/py/python-multipart.svg)](https://pypi.python.org/pypi/python-multipart) +[![Supported Python Version](https://img.shields.io/pypi/pyversions/python-multipart.svg?color=%2334D058)](https://pypi.org/project/python-multipart) + +--- + +`python-multipart` is an Apache2 licensed streaming multipart parser for Python. +Test coverage is currently 100%. + +## Why? + +Because streaming uploads are awesome for large files. + +## How to Test + +If you want to test: + +```bash +$ pip install '.[dev]' +$ inv test +``` diff --git a/README.rst b/README.rst deleted file mode 100644 index 7bdc864..0000000 --- a/README.rst +++ /dev/null @@ -1,28 +0,0 @@ -================== - Python-Multipart -================== - -.. image:: https://github.com/andrew-d/python-multipart/actions/workflows/test.yaml/badge.svg - :target: https://github.com/andrew-d/python-multipart/actions - - -python-multipart is an Apache2 licensed streaming multipart parser for Python. -Test coverage is currently 100%. -Documentation is available `here`_. - -.. _here: https://andrew-d.github.io/python-multipart/ - -Why? ----- - -Because streaming uploads are awesome for large files. - -How to Test ------------ - -If you want to test: - -.. code-block:: bash - - $ pip install '.[dev]' - $ inv test diff --git a/pyproject.toml b/pyproject.toml index 5833d83..ed22c97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,10 +6,13 @@ build-backend = "hatchling.build" name = "python-multipart" dynamic = ["version"] description = "A streaming multipart parser for Python" -readme = "README.rst" +readme = "README.md" license = "Apache-2.0" requires-python = ">=3.8" -authors = [{ name = "Andrew Dunham", email = "andrew@du.nham.ca" }] +authors = [ + { name = "Andrew Dunham", email = "andrew@du.nham.ca" }, + { name = "Marcelo Trylesinski", email = "marcelotryle@gmail.com" }, +] classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', @@ -46,10 +49,10 @@ dev = [ ] [project.urls] -Homepage = "https://github.com/andrew-d/python-multipart" +Homepage = "https://github.com/Kludex/python-multipart" Documentation = "https://andrew-d.github.io/python-multipart/" -Changelog = "https://github.com/andrew-d/python-multipart/blob/master/CHANGELOG.md" -Source = "https://github.com/andrew-d/python-multipart" +Changelog = "https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md" +Source = "https://github.com/Kludex/python-multipart" [tool.hatch.version] path = "multipart/__init__.py" From 54c4d18a255c4844e2224a15ead8ce3ca135c399 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Feb 2024 15:54:19 +0100 Subject: [PATCH 31/94] Support `on_header_begin` (#103) --- multipart/multipart.py | 23 ++++++++++++++++------- tests/test_multipart.py | 31 ++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/multipart/multipart.py b/multipart/multipart.py index 221bb71..abe4dff 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -9,7 +9,7 @@ from enum import IntEnum from io import BytesIO from numbers import Number -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from .decoders import Base64Decoder, QuotedPrintableDecoder from .exceptions import FileError, FormParserError, MultipartParseError, QuerystringParseError @@ -33,7 +33,7 @@ class MultipartCallbacks(TypedDict, total=False): on_part_begin: Callable[[], None] on_part_data: Callable[[bytes, int, int], None] on_part_end: Callable[[], None] - on_headers_begin: Callable[[], None] + on_header_begin: Callable[[], None] on_header_field: Callable[[bytes, int, int], None] on_header_value: Callable[[bytes, int, int], None] on_header_end: Callable[[], None] @@ -592,10 +592,12 @@ class BaseParser: performance. """ + callbacks: dict[str, Callable[..., Any]] + def __init__(self): self.logger = logging.getLogger(__name__) - def callback(self, name: str, data=None, start=None, end=None): + def callback(self, name: str, data: bytes | None = None, start: int | None = None, end: int | None = None): """This function calls a provided callback with some data. If the callback is not set, will do nothing. @@ -1047,7 +1049,7 @@ def __init__(self, boundary: bytes | str, callbacks: MultipartCallbacks = {}, ma self._current_size = 0 # Setup marks. These are used to track the state of data received. - self.marks = {} + self.marks: dict[str, int] = {} # TODO: Actually use this rather than the dumb version we currently use # # Precompute the skip table for the Boyer-Moore-Horspool algorithm. @@ -1118,11 +1120,11 @@ def _internal_write(self, data: bytes, length: int) -> int: i = 0 # Set a mark. - def set_mark(name): + def set_mark(name: str): self.marks[name] = i # Remove a mark. - def delete_mark(name, reset=False): + def delete_mark(name: str, reset: bool = False): self.marks.pop(name, None) # Helper function that makes calling a callback with data easier. The @@ -1130,7 +1132,7 @@ def delete_mark(name, reset=False): # end of the buffer, and reset the mark, instead of deleting it. This # is used at the end of the function to call our callbacks with any # remaining data in this chunk. - def data_callback(name, remaining=False): + def data_callback(name: str, remaining: bool = False): marked_index = self.marks.get(name) if marked_index is None: return @@ -1217,6 +1219,13 @@ def data_callback(name, remaining=False): # Set a mark of our header field. set_mark("header_field") + # Notify that we're starting a header if the next character is + # not a CR; a CR at the beginning of the header will cause us + # to stop parsing headers in the MultipartState.HEADER_FIELD state, + # below. + if c != CR: + self.callback("header_begin") + # Move to parsing header fields. state = MultipartState.HEADER_FIELD i -= 1 diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 16db5b3..9dc10bb 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -1222,7 +1222,36 @@ def on_file(f): def test_invalid_max_size_multipart(self): with self.assertRaises(ValueError): - q = MultipartParser(b"bound", max_size="foo") + MultipartParser(b"bound", max_size="foo") + + def test_header_begin_callback(self): + """ + This test verifies we call the `on_header_begin` callback. + See GitHub issue #23 + """ + # Load test data. + test_file = "single_field_single_file.http" + with open(os.path.join(http_tests_dir, test_file), "rb") as f: + test_data = f.read() + + calls = 0 + + def on_header_begin() -> None: + nonlocal calls + calls += 1 + + parser = MultipartParser("boundary", callbacks={"on_header_begin": on_header_begin}, max_size=1000) + + # Create multipart parser and feed it + i = parser.write(test_data) + parser.finalize() + + # Assert we processed everything. + self.assertEqual(i, len(test_data)) + + # Assert that we called our 'header_begin' callback three times; once + # for each header in the multipart message. + self.assertEqual(calls, 3) class TestHelperFunctions(unittest.TestCase): From 59543a4988d91359210ab120027b66b522826760 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Feb 2024 16:40:27 +0100 Subject: [PATCH 32/94] Improve type hints on `FormParser` (#104) --- multipart/multipart.py | 140 ++++++++++++++++++++++++----------------- 1 file changed, 84 insertions(+), 56 deletions(-) diff --git a/multipart/multipart.py b/multipart/multipart.py index abe4dff..3adf9ac 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -15,7 +15,7 @@ from .exceptions import FileError, FormParserError, MultipartParseError, QuerystringParseError if TYPE_CHECKING: # pragma: no cover - from typing import Callable, TypedDict + from typing import Callable, Protocol, TypedDict class QuerystringCallbacks(TypedDict, total=False): on_field_start: Callable[[], None] @@ -55,6 +55,30 @@ class FileConfig(TypedDict, total=False): UPLOAD_KEEP_EXTENSIONS: bool MAX_MEMORY_FILE_SIZE: int + class _FormProtocol(Protocol): + def write(self, data: bytes) -> int: + ... + + def finalize(self) -> None: + ... + + def close(self) -> None: + ... + + class FieldProtocol(_FormProtocol, Protocol): + def __init__(self, name: bytes) -> None: + ... + + def set_none(self) -> None: + ... + + class FileProtocol(_FormProtocol, Protocol): + def __init__(self, file_name: bytes | None, field_name: bytes | None, config: FileConfig) -> None: + ... + + OnFieldCallback = Callable[[FieldProtocol], None] + OnFileCallback = Callable[[FieldProtocol], None] + # Unique missing object. _missing = object() @@ -190,7 +214,7 @@ class Field: :param name: the name of the form field """ - def __init__(self, name: str): + def __init__(self, name: bytes): self._name = name self._value: list[bytes] = [] @@ -198,7 +222,7 @@ def __init__(self, name: str): self._cache = _missing @classmethod - def from_value(cls, name: str, value: bytes | None) -> Field: + def from_value(cls, name: bytes, value: bytes | None) -> Field: """Create an instance of a :class:`Field`, and set the corresponding value - either None or an actual value. This method will also finalize the Field itself. @@ -260,7 +284,7 @@ def set_none(self) -> None: self._cache = None @property - def field_name(self) -> str: + def field_name(self) -> bytes: """This property returns the name of the field.""" return self._name @@ -1562,6 +1586,7 @@ class FormParser: field_instance.write(data) field_instance.finalize() field_instance.close() + field_instance.set_none() :param config: Configuration to use for this FormParser. The default values are taken from the DEFAULT_CONFIG value, and then @@ -1584,14 +1609,14 @@ class FormParser: def __init__( self, - content_type, - on_field, - on_file, - on_end=None, - boundary=None, - file_name=None, - FileClass=File, - FieldClass=Field, + content_type: str, + on_field: OnFieldCallback, + on_file: OnFileCallback, + on_end: Callable[[], None] | None = None, + boundary: bytes | str | None = None, + file_name: bytes | None = None, + FileClass: type[FileProtocol] = File, + FieldClass: type[FieldProtocol] = Field, config: FormParserConfig = {}, ): self.logger = logging.getLogger(__name__) @@ -1617,22 +1642,22 @@ def __init__( # Depending on the Content-Type, we instantiate the correct parser. if content_type == "application/octet-stream": - # Work around the lack of 'nonlocal' in Py2 - class vars: - f = None + f: FileProtocol | None = None def on_start() -> None: - vars.f = FileClass(file_name, None, config=self.config) + nonlocal f + f = FileClass(file_name, None, config=self.config) def on_data(data: bytes, start: int, end: int) -> None: - vars.f.write(data[start:end]) + nonlocal f + f.write(data[start:end]) - def on_end() -> None: + def _on_end() -> None: # Finalize the file itself. - vars.f.finalize() + f.finalize() # Call our callback. - on_file(vars.f) + on_file(f) # Call the on-end callback. if self.on_end is not None: @@ -1640,15 +1665,14 @@ def on_end() -> None: # Instantiate an octet-stream parser parser = OctetStreamParser( - callbacks={"on_start": on_start, "on_data": on_data, "on_end": on_end}, + callbacks={"on_start": on_start, "on_data": on_data, "on_end": _on_end}, max_size=self.config["MAX_BODY_SIZE"], ) elif content_type == "application/x-www-form-urlencoded" or content_type == "application/x-url-encoded": name_buffer: list[bytes] = [] - class vars: - f = None + f: FieldProtocol | None = None def on_field_start() -> None: pass @@ -1657,25 +1681,27 @@ def on_field_name(data: bytes, start: int, end: int) -> None: name_buffer.append(data[start:end]) def on_field_data(data: bytes, start: int, end: int) -> None: - if vars.f is None: - vars.f = FieldClass(b"".join(name_buffer)) + nonlocal f + if f is None: + f = FieldClass(b"".join(name_buffer)) del name_buffer[:] - vars.f.write(data[start:end]) + f.write(data[start:end]) def on_field_end() -> None: + nonlocal f # Finalize and call callback. - if vars.f is None: + if f is None: # If we get here, it's because there was no field data. # We create a field, set it to None, and then continue. - vars.f = FieldClass(b"".join(name_buffer)) + f = FieldClass(b"".join(name_buffer)) del name_buffer[:] - vars.f.set_none() + f.set_none() - vars.f.finalize() - on_field(vars.f) - vars.f = None + f.finalize() + on_field(f) + f = None - def on_end() -> None: + def _on_end() -> None: if self.on_end is not None: self.on_end() @@ -1686,7 +1712,7 @@ def on_end() -> None: "on_field_name": on_field_name, "on_field_data": on_field_data, "on_field_end": on_field_end, - "on_end": on_end, + "on_end": _on_end, }, max_size=self.config["MAX_BODY_SIZE"], ) @@ -1700,26 +1726,26 @@ def on_end() -> None: header_value: list[bytes] = [] headers = {} - # No 'nonlocal' on Python 2 :-( - class vars: - f = None - writer = None - is_file = False + f: FileProtocol | FieldProtocol | None = None + writer = None + is_file = False def on_part_begin(): pass - def on_part_data(data: bytes, start: int, end: int): - bytes_processed = vars.writer.write(data[start:end]) + def on_part_data(data: bytes, start: int, end: int) -> None: + nonlocal writer + bytes_processed = writer.write(data[start:end]) # TODO: check for error here. return bytes_processed def on_part_end() -> None: - vars.f.finalize() - if vars.is_file: - on_file(vars.f) + nonlocal f, is_file + f.finalize() + if is_file: + on_file(f) else: - on_field(vars.f) + on_field(f) def on_header_field(data: bytes, start: int, end: int): header_name.append(data[start:end]) @@ -1733,8 +1759,9 @@ def on_header_end(): del header_value[:] def on_headers_finished() -> None: + nonlocal is_file, f, writer # Reset the 'is file' flag. - vars.is_file = False + is_file = False # Parse the content-disposition header. # TODO: handle mixed case @@ -1748,10 +1775,10 @@ def on_headers_finished() -> None: # Create the proper class. if file_name is None: - vars.f = FieldClass(field_name) + f = FieldClass(field_name) else: - vars.f = FileClass(file_name, field_name, config=self.config) - vars.is_file = True + f = FileClass(file_name, field_name, config=self.config) + is_file = True # Parse the given Content-Transfer-Encoding to determine what # we need to do with the incoming data. @@ -1759,13 +1786,13 @@ def on_headers_finished() -> None: transfer_encoding = headers.get(b"Content-Transfer-Encoding", b"7bit") if transfer_encoding == b"binary" or transfer_encoding == b"8bit" or transfer_encoding == b"7bit": - vars.writer = vars.f + writer = f elif transfer_encoding == b"base64": - vars.writer = Base64Decoder(vars.f) + writer = Base64Decoder(f) elif transfer_encoding == b"quoted-printable": - vars.writer = QuotedPrintableDecoder(vars.f) + writer = QuotedPrintableDecoder(f) else: self.logger.warning("Unknown Content-Transfer-Encoding: %r", transfer_encoding) @@ -1774,10 +1801,11 @@ def on_headers_finished() -> None: else: # If we aren't erroring, then we just treat this as an # unencoded Content-Transfer-Encoding. - vars.writer = vars.f + writer = f - def on_end() -> None: - vars.writer.finalize() + def _on_end() -> None: + nonlocal writer + writer.finalize() if self.on_end is not None: self.on_end() @@ -1792,7 +1820,7 @@ def on_end() -> None: "on_header_value": on_header_value, "on_header_end": on_header_end, "on_headers_finished": on_headers_finished, - "on_end": on_end, + "on_end": _on_end, }, max_size=self.config["MAX_BODY_SIZE"], ) From 8505ad4beb81452256683ae3b3b138fe0a938f82 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Feb 2024 18:21:16 +0100 Subject: [PATCH 33/94] Fix simple example from the documentation (#105) --- docs/source/quickstart.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 8439954..b68b06f 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -21,10 +21,10 @@ in a simple WSGI application:: # The following two callbacks just append the name to the return value. def on_field(field): - ret.append("Parsed field named: %s" % (field.field_name,)) + ret.append(b"Parsed field named: %s" % (field.field_name,)) def on_file(file): - ret.append("Parsed file named: %s" % (file.field_name,)) + ret.append(b"Parsed file named: %s" % (file.field_name,)) # Create headers object. We need to convert from WSGI to the actual # name of the header, since this library does not assume that you are @@ -40,10 +40,12 @@ in a simple WSGI application:: # Return something. start_response('200 OK', [('Content-type', 'text/plain')]) - ret.append('\n') + ret.append(b'\n') return ret - + from wsgiref.simple_server import make_server + from wsgiref.validate import validator + httpd = make_server('', 8123, simple_app) print("Serving on port 8123...") httpd.serve_forever() From c69d603fc4001a6707d492ca7a67ee5fea50c2ec Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Feb 2024 18:31:33 +0100 Subject: [PATCH 34/94] Fix `OnFileCallback` type (#106) --- multipart/multipart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multipart/multipart.py b/multipart/multipart.py index 3adf9ac..21d9ac1 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -77,7 +77,7 @@ def __init__(self, file_name: bytes | None, field_name: bytes | None, config: Fi ... OnFieldCallback = Callable[[FieldProtocol], None] - OnFileCallback = Callable[[FieldProtocol], None] + OnFileCallback = Callable[[FileProtocol], None] # Unique missing object. From 147479613d0ae6f883b7f16d897fa7d96b85102b Mon Sep 17 00:00:00 2001 From: Onuralp SEZER Date: Sun, 11 Feb 2024 23:22:50 +0300 Subject: [PATCH 35/94] Use MkDocs (#108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 🧹 python.gitignore added Signed-off-by: Onuralp SEZER * docs: 🧹 removal of old documents,packages,requirements Signed-off-by: Onuralp SEZER * docs: ✨ mkdocs-material packages added as doc-dep Signed-off-by: Onuralp SEZER * docs: ✨ initial mkdocs pages and documentation added, old docs removed Signed-off-by: Onuralp SEZER * docs: ✨ set sphinx doc style as default Signed-off-by: Onuralp SEZER * docs: ✨ decoders,exceptions,helper-functions,main-class,parsers,support-classes added Signed-off-by: Onuralp SEZER * fix: 🐞 package typo fix Signed-off-by: Onuralp SEZER * docs: ✨ home page, quickstart sections added Signed-off-by: Onuralp SEZER * ci: 👷 docs workflow added Signed-off-by: Onuralp SEZER * Apply my opinion --------- Signed-off-by: Onuralp SEZER Co-authored-by: Marcelo Trylesinski --- .github/workflows/{main.yaml => main.yml} | 0 .../workflows/{publish.yaml => publish.yml} | 6 +- .gitignore | 185 +++++++- .gitmodules | 4 - docs/Makefile | 177 -------- docs/api.md | 1 + docs/build/.gitignore | 1 - docs/build/html | 1 - docs/changelog.md | 1 + docs/index.md | 178 ++++++++ docs/make.bat | 242 ----------- docs/source/_themes/.gitignore | 3 - docs/source/_themes/LICENSE | 37 -- docs/source/_themes/README | 31 -- docs/source/_themes/flask/layout.html | 25 -- docs/source/_themes/flask/relations.html | 19 - docs/source/_themes/flask/static/flasky.css_t | 395 ------------------ .../_themes/flask/static/small_flask.css | 70 ---- docs/source/_themes/flask/theme.conf | 9 - docs/source/_themes/flask_small/layout.html | 22 - .../_themes/flask_small/static/flasky.css_t | 287 ------------- docs/source/_themes/flask_small/theme.conf | 10 - docs/source/_themes/flask_theme_support.py | 86 ---- docs/source/api.rst | 74 ---- docs/source/conf.py | 256 ------------ docs/source/contents.rst.inc | 20 - docs/source/index.rst | 10 - docs/source/quickstart.rst | 195 --------- docs_requirements.txt | 19 - mkdocs.yml | 84 ++++ pyproject.toml | 7 + 31 files changed, 436 insertions(+), 2019 deletions(-) rename .github/workflows/{main.yaml => main.yml} (100%) rename .github/workflows/{publish.yaml => publish.yml} (91%) delete mode 100644 .gitmodules delete mode 100644 docs/Makefile create mode 100644 docs/api.md delete mode 100644 docs/build/.gitignore delete mode 160000 docs/build/html create mode 100644 docs/changelog.md create mode 100644 docs/index.md delete mode 100644 docs/make.bat delete mode 100644 docs/source/_themes/.gitignore delete mode 100644 docs/source/_themes/LICENSE delete mode 100644 docs/source/_themes/README delete mode 100644 docs/source/_themes/flask/layout.html delete mode 100644 docs/source/_themes/flask/relations.html delete mode 100644 docs/source/_themes/flask/static/flasky.css_t delete mode 100644 docs/source/_themes/flask/static/small_flask.css delete mode 100644 docs/source/_themes/flask/theme.conf delete mode 100644 docs/source/_themes/flask_small/layout.html delete mode 100644 docs/source/_themes/flask_small/static/flasky.css_t delete mode 100644 docs/source/_themes/flask_small/theme.conf delete mode 100644 docs/source/_themes/flask_theme_support.py delete mode 100644 docs/source/api.rst delete mode 100644 docs/source/conf.py delete mode 100644 docs/source/contents.rst.inc delete mode 100644 docs/source/index.rst delete mode 100644 docs/source/quickstart.rst delete mode 100644 docs_requirements.txt create mode 100644 mkdocs.yml diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yml similarity index 100% rename from .github/workflows/main.yaml rename to .github/workflows/main.yml diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yml similarity index 91% rename from .github/workflows/publish.yaml rename to .github/workflows/publish.yml index cc38611..9a96079 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yml @@ -26,17 +26,17 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.10" - - name: Install dependencies run: | python -m pip install --upgrade pip pip install build - + pip install -e '.[docs]' - name: Build package run: python -m build - - name: Publish package uses: pypa/gh-action-pypi-publish@v1.8.11 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} + - name: Publish docs + run: mkdocs gh-deploy --force diff --git a/.gitignore b/.gitignore index f7f7b71..f52a6b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,6 @@ # Project-specific ignores. -venv/ tmp/ -# Parts below taken from GitHub's gitignores repository: https://github.com/github/gitignore -*.py[cod] - -# C extensions -*.so - -# Packages -*.egg -*.egg-info -MANIFEST -dist -/build -eggs -parts -bin -var -sdist -develop-eggs -.installed.cfg -lib -lib64 - # Installer logs pip-log.txt @@ -59,3 +36,165 @@ $RECYCLE.BIN/ # OS X ignores .DS_Store + + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 3b9cefa..0000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "docs/build/html"] - path = docs/build/html - url = https://github.com/andrew-d/python-multipart.git - branch = gh-pages diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index c045a56..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,177 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-multipart.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-multipart.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/python-multipart" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-multipart" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..5e833b6 --- /dev/null +++ b/docs/api.md @@ -0,0 +1 @@ +:::multipart diff --git a/docs/build/.gitignore b/docs/build/.gitignore deleted file mode 100644 index fad74e7..0000000 --- a/docs/build/.gitignore +++ /dev/null @@ -1 +0,0 @@ -doctrees diff --git a/docs/build/html b/docs/build/html deleted file mode 160000 index 79f2967..0000000 --- a/docs/build/html +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 79f29674b090ff0af0b62ec63c24477f2b66ca1d diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..786b75d --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1 @@ +--8<-- "CHANGELOG.md" diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..0640374 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,178 @@ +# Python-Multipart + +Python-Multipart is a streaming multipart parser for Python. + +## Quickstart + +### Simple Example + +The following example shows a quick example of parsing an incoming request body in a simple WSGI application: + +```python +import multipart + +def simple_app(environ, start_response): + ret = [] + + # The following two callbacks just append the name to the return value. + def on_field(field): + ret.append(b"Parsed field named: %s" % (field.field_name,)) + + def on_file(file): + ret.append(b"Parsed file named: %s" % (file.field_name,)) + + # Create headers object. We need to convert from WSGI to the actual + # name of the header, since this library does not assume that you are + # using WSGI. + headers = {'Content-Type': environ['CONTENT_TYPE']} + if 'HTTP_X_FILE_NAME' in environ: + headers['X-File-Name'] = environ['HTTP_X_FILE_NAME'] + if 'CONTENT_LENGTH' in environ: + headers['Content-Length'] = environ['CONTENT_LENGTH'] + + # Parse the form. + multipart.parse_form(headers, environ['wsgi.input'], on_field, on_file) + + # Return something. + start_response('200 OK', [('Content-type', 'text/plain')]) + ret.append(b'\n') + return ret + +from wsgiref.simple_server import make_server +from wsgiref.validate import validator + +httpd = make_server('', 8123, simple_app) +print("Serving on port 8123...") +httpd.serve_forever() +``` + +If you test this with curl, you can see that the parser works: + +```console +$ curl -ik -F "foo=bar" http://localhost:8123/ +HTTP/1.0 200 OK +Date: Sun, 07 Apr 2013 01:40:52 GMT +Server: WSGIServer/0.1 Python/2.7.3 +Content-type: text/plain + +Parsed field named: foo +``` + +For a more in-depth example showing how the various parts fit together, check out the next section. + +### In-Depth Example + +In this section, we’ll build an application that computes the SHA-256 hash of all uploaded files in a streaming manner. + +To start, we need a simple WSGI application. We could do this with a framework like Flask, Django, or Tornado, but for now let’s stick to plain WSGI: + +```python +import multipart + +def simple_app(environ, start_response): + start_response('200 OK', [('Content-type', 'text/plain')]) + return ['Hashes:\n'] + +from wsgiref.simple_server import make_server +httpd = make_server('', 8123, simple_app) +print("Serving on port 8123...") +httpd.serve_forever() +``` + +You can run this and check with curl that it works properly: + +```console +$ curl -ik http://localhost:8123/ +HTTP/1.0 200 OK +Date: Sun, 07 Apr 2013 01:49:03 GMT +Server: WSGIServer/0.1 Python/2.7.3 +Content-type: text/plain +Content-Length: 8 + +Hashes: +``` + +Good! It works. Now, let’s add some of the code that we need. What we need to do, essentially, is set up the appropriate parser and callbacks so that we can access each portion of the request as it arrives, without needing to store any parts in memory. + +We can start off by checking if we need to create the parser at all - if the Content-Type isn’t multipart/form-data, then we’re not going to do anything. + +The final code should look like this: + +```python +import hashlib +import multipart +from multipart.multipart import parse_options_header + +def simple_app(environ, start_response): + ret = [] + + # Python 2 doesn't have the "nonlocal" keyword from Python 3, so we get + # around it by setting attributes on a dummy object. + class g(object): + hash = None + + # This is called when a new part arrives. We create a new hash object + # in this callback. + def on_part_begin(): + g.hash = hashlib.sha256() + + # We got some data! Update our hash. + def on_part_data(data, start, end): + g.hash.update(data[start:end]) + + # Our current part is done, so we can finish the hash. + def on_part_end(): + ret.append("Part hash: %s" % (g.hash.hexdigest(),)) + + # Parse the Content-Type header to get the multipart boundary. + content_type, params = parse_options_header(environ['CONTENT_TYPE']) + boundary = params.get(b'boundary') + + # Callbacks dictionary. + callbacks = { + 'on_part_begin': on_part_begin, + 'on_part_data': on_part_data, + 'on_part_end': on_part_end, + } + + # Create the parser. + parser = multipart.MultipartParser(boundary, callbacks) + + # The input stream is from the WSGI environ. + inp = environ['wsgi.input'] + + # Feed the parser with data from the request. + size = int(environ['CONTENT_LENGTH']) + while size > 0: + to_read = min(size, 1024 * 1024) + data = inp.read(to_read) + parser.write(data) + + size -= len(data) + if len(data) != to_read: + break + + start_response('200 OK', [('Content-type', 'text/plain')]) + return ret + +from wsgiref.simple_server import make_server +httpd = make_server('', 8123, simple_app) +print("Serving on port 8123...") +httpd.serve_forever() +``` + +And you can see that this works: + +```console +$ echo "Foo bar" > /tmp/test.txt +$ shasum -a 256 /tmp/test.txt +0b64696c0f7ddb9e3435341720988d5455b3b0f0724688f98ec8e6019af3d931 /tmp/test.txt +$ curl -ik -F file=@/tmp/test.txt http://localhost:8123/ +HTTP/1.0 200 OK +Date: Sun, 07 Apr 2013 02:09:10 GMT +Server: WSGIServer/0.1 Python/2.7.3 +Content-type: text/plain + +Hashes: +Part hash: 0b64696c0f7ddb9e3435341720988d5455b3b0f0724688f98ec8e6019af3d931 +``` diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 10fdc25..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,242 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source -set I18NSPHINXOPTS=%SPHINXOPTS% source -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\python-multipart.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\python-multipart.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end diff --git a/docs/source/_themes/.gitignore b/docs/source/_themes/.gitignore deleted file mode 100644 index 66b6e4c..0000000 --- a/docs/source/_themes/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.pyc -*.pyo -.DS_Store diff --git a/docs/source/_themes/LICENSE b/docs/source/_themes/LICENSE deleted file mode 100644 index 8daab7e..0000000 --- a/docs/source/_themes/LICENSE +++ /dev/null @@ -1,37 +0,0 @@ -Copyright (c) 2010 by Armin Ronacher. - -Some rights reserved. - -Redistribution and use in source and binary forms of the theme, with or -without modification, are permitted provided that the following conditions -are met: - -* Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - -* The names of the contributors may not be used to endorse or - promote products derived from this software without specific - prior written permission. - -We kindly ask you to only use these themes in an unmodified manner just -for Flask and Flask-related products, not for unrelated projects. If you -like the visual style and want to use it for your own projects, please -consider making some larger changes to the themes (such as changing -font faces, sizes, colors or margins). - -THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/source/_themes/README b/docs/source/_themes/README deleted file mode 100644 index b3292bd..0000000 --- a/docs/source/_themes/README +++ /dev/null @@ -1,31 +0,0 @@ -Flask Sphinx Styles -=================== - -This repository contains sphinx styles for Flask and Flask related -projects. To use this style in your Sphinx documentation, follow -this guide: - -1. put this folder as _themes into your docs folder. Alternatively - you can also use git submodules to check out the contents there. -2. add this to your conf.py: - - sys.path.append(os.path.abspath('_themes')) - html_theme_path = ['_themes'] - html_theme = 'flask' - -The following themes exist: - -- 'flask' - the standard flask documentation theme for large - projects -- 'flask_small' - small one-page theme. Intended to be used by - very small addon libraries for flask. - -The following options exist for the flask_small theme: - - [options] - index_logo = '' filename of a picture in _static - to be used as replacement for the - h1 in the index.rst file. - index_logo_height = 120px height of the index logo - github_fork = '' repository name on github for the - "fork me" badge diff --git a/docs/source/_themes/flask/layout.html b/docs/source/_themes/flask/layout.html deleted file mode 100644 index 6a80763..0000000 --- a/docs/source/_themes/flask/layout.html +++ /dev/null @@ -1,25 +0,0 @@ -{%- extends "basic/layout.html" %} -{%- block extrahead %} - {{ super() }} - {% if theme_touch_icon %} - - {% endif %} - -{% endblock %} -{%- block relbar2 %}{% endblock %} -{% block header %} - {{ super() }} - {% if pagename == 'index' %} -
- {% endif %} -{% endblock %} -{%- block footer %} - - {% if pagename == 'index' %} -
- {% endif %} -{%- endblock %} diff --git a/docs/source/_themes/flask/relations.html b/docs/source/_themes/flask/relations.html deleted file mode 100644 index 3bbcde8..0000000 --- a/docs/source/_themes/flask/relations.html +++ /dev/null @@ -1,19 +0,0 @@ -

Related Topics

- diff --git a/docs/source/_themes/flask/static/flasky.css_t b/docs/source/_themes/flask/static/flasky.css_t deleted file mode 100644 index b5ca39b..0000000 --- a/docs/source/_themes/flask/static/flasky.css_t +++ /dev/null @@ -1,395 +0,0 @@ -/* - * flasky.css_t - * ~~~~~~~~~~~~ - * - * :copyright: Copyright 2010 by Armin Ronacher. - * :license: Flask Design License, see LICENSE for details. - */ - -{% set page_width = '940px' %} -{% set sidebar_width = '220px' %} - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: 'Georgia', serif; - font-size: 17px; - background-color: white; - color: #000; - margin: 0; - padding: 0; -} - -div.document { - width: {{ page_width }}; - margin: 30px auto 0 auto; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 {{ sidebar_width }}; -} - -div.sphinxsidebar { - width: {{ sidebar_width }}; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 0 30px; -} - -img.floatingflask { - padding: 0 0 10px 10px; - float: right; -} - -div.footer { - width: {{ page_width }}; - margin: 20px auto 30px auto; - font-size: 14px; - color: #888; - text-align: right; -} - -div.footer a { - color: #888; -} - -div.related { - display: none; -} - -div.sphinxsidebar a { - color: #444; - text-decoration: none; - border-bottom: 1px dotted #999; -} - -div.sphinxsidebar a:hover { - border-bottom: 1px solid #999; -} - -div.sphinxsidebar { - font-size: 14px; - line-height: 1.5; -} - -div.sphinxsidebarwrapper { - padding: 18px 10px; -} - -div.sphinxsidebarwrapper p.logo { - padding: 0 0 20px 0; - margin: 0; - text-align: center; -} - -div.sphinxsidebar h3, -div.sphinxsidebar h4 { - font-family: 'Garamond', 'Georgia', serif; - color: #444; - font-size: 24px; - font-weight: normal; - margin: 0 0 5px 0; - padding: 0; -} - -div.sphinxsidebar h4 { - font-size: 20px; -} - -div.sphinxsidebar h3 a { - color: #444; -} - -div.sphinxsidebar p.logo a, -div.sphinxsidebar h3 a, -div.sphinxsidebar p.logo a:hover, -div.sphinxsidebar h3 a:hover { - border: none; -} - -div.sphinxsidebar p { - color: #555; - margin: 10px 0; -} - -div.sphinxsidebar ul { - margin: 10px 0; - padding: 0; - color: #000; -} - -div.sphinxsidebar input { - border: 1px solid #ccc; - font-family: 'Georgia', serif; - font-size: 1em; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #004B6B; - text-decoration: underline; -} - -a:hover { - color: #6D4100; - text-decoration: underline; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - margin: 30px 0px 10px 0px; - padding: 0; -} - -{% if theme_index_logo %} -div.indexwrapper h1 { - text-indent: -999999px; - background: url({{ theme_index_logo }}) no-repeat center center; - height: {{ theme_index_logo_height }}; -} -{% endif %} - -div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } -div.body h2 { font-size: 180%; } -div.body h3 { font-size: 150%; } -div.body h4 { font-size: 130%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: #ddd; - padding: 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - color: #444; - background: #eaeaea; -} - -div.body p, div.body dd, div.body li { - line-height: 1.4em; -} - -div.admonition { - background: #fafafa; - margin: 20px -30px; - padding: 10px 30px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; -} - -div.admonition tt.xref, div.admonition a tt { - border-bottom: 1px solid #fafafa; -} - -dd div.admonition { - margin-left: -60px; - padding-left: 60px; -} - -div.admonition p.admonition-title { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - font-size: 24px; - margin: 0 0 10px 0; - padding: 0; - line-height: 1; -} - -div.admonition p.last { - margin-bottom: 0; -} - -div.highlight { - background-color: white; -} - -dt:target, .highlight { - background: #FAF3E8; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre, tt { - font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; - font-size: 0.9em; -} - -img.screenshot { -} - -tt.descname, tt.descclassname { - font-size: 0.95em; -} - -tt.descname { - padding-right: 0.08em; -} - -img.screenshot { - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils { - border: 1px solid #888; - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils td, table.docutils th { - border: 1px solid #888; - padding: 0.25em 0.7em; -} - -table.field-list, table.footnote { - border: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} - -table.footnote { - margin: 15px 0; - width: 100%; - border: 1px solid #eee; - background: #fdfdfd; - font-size: 0.9em; -} - -table.footnote + table.footnote { - margin-top: -15px; - border-top: none; -} - -table.field-list th { - padding: 0 0.8em 0 0; -} - -table.field-list td { - padding: 0; -} - -table.footnote td.label { - width: 0px; - padding: 0.3em 0 0.3em 0.5em; -} - -table.footnote td { - padding: 0.3em 0.5em; -} - -dl { - margin: 0; - padding: 0; -} - -dl dd { - margin-left: 30px; -} - -blockquote { - margin: 0 0 0 30px; - padding: 0; -} - -ul, ol { - margin: 10px 0 10px 30px; - padding: 0; -} - -pre { - background: #eee; - padding: 7px 30px; - margin: 15px -30px; - line-height: 1.3em; -} - -dl pre, blockquote pre, li pre { - margin-left: -60px; - padding-left: 60px; -} - -dl dl pre { - margin-left: -90px; - padding-left: 90px; -} - -tt { - background-color: #ecf0f3; - color: #222; - /* padding: 1px 2px; */ -} - -tt.xref, a tt { - background-color: #FBFBFB; - border-bottom: 1px solid white; -} - -a.reference { - text-decoration: none; - border-bottom: 1px dotted #004B6B; -} - -a.reference:hover { - border-bottom: 1px solid #6D4100; -} - -a.footnote-reference { - text-decoration: none; - font-size: 0.7em; - vertical-align: top; - border-bottom: 1px dotted #004B6B; -} - -a.footnote-reference:hover { - border-bottom: 1px solid #6D4100; -} - -a:hover tt { - background: #EEE; -} diff --git a/docs/source/_themes/flask/static/small_flask.css b/docs/source/_themes/flask/static/small_flask.css deleted file mode 100644 index 1c6df30..0000000 --- a/docs/source/_themes/flask/static/small_flask.css +++ /dev/null @@ -1,70 +0,0 @@ -/* - * small_flask.css_t - * ~~~~~~~~~~~~~~~~~ - * - * :copyright: Copyright 2010 by Armin Ronacher. - * :license: Flask Design License, see LICENSE for details. - */ - -body { - margin: 0; - padding: 20px 30px; -} - -div.documentwrapper { - float: none; - background: white; -} - -div.sphinxsidebar { - display: block; - float: none; - width: 102.5%; - margin: 50px -30px -20px -30px; - padding: 10px 20px; - background: #333; - color: white; -} - -div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, -div.sphinxsidebar h3 a { - color: white; -} - -div.sphinxsidebar a { - color: #aaa; -} - -div.sphinxsidebar p.logo { - display: none; -} - -div.document { - width: 100%; - margin: 0; -} - -div.related { - display: block; - margin: 0; - padding: 10px 0 20px 0; -} - -div.related ul, -div.related ul li { - margin: 0; - padding: 0; -} - -div.footer { - display: none; -} - -div.bodywrapper { - margin: 0; -} - -div.body { - min-height: 0; - padding: 0; -} diff --git a/docs/source/_themes/flask/theme.conf b/docs/source/_themes/flask/theme.conf deleted file mode 100644 index 18c720f..0000000 --- a/docs/source/_themes/flask/theme.conf +++ /dev/null @@ -1,9 +0,0 @@ -[theme] -inherit = basic -stylesheet = flasky.css -pygments_style = flask_theme_support.FlaskyStyle - -[options] -index_logo = '' -index_logo_height = 120px -touch_icon = diff --git a/docs/source/_themes/flask_small/layout.html b/docs/source/_themes/flask_small/layout.html deleted file mode 100644 index 83e5213..0000000 --- a/docs/source/_themes/flask_small/layout.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "basic/layout.html" %} -{% block header %} - {{ super() }} - {% if pagename == 'index' %} -
- {% endif %} -{% endblock %} -{% block footer %} - {% if pagename == 'index' %} -
- {% endif %} -{% endblock %} -{# do not display relbars #} -{% block relbar1 %}{% endblock %} -{% block relbar2 %} - {% if theme_github_fork %} - Fork me on GitHub - {% endif %} -{% endblock %} -{% block sidebar1 %}{% endblock %} -{% block sidebar2 %}{% endblock %} diff --git a/docs/source/_themes/flask_small/static/flasky.css_t b/docs/source/_themes/flask_small/static/flasky.css_t deleted file mode 100644 index fe2141c..0000000 --- a/docs/source/_themes/flask_small/static/flasky.css_t +++ /dev/null @@ -1,287 +0,0 @@ -/* - * flasky.css_t - * ~~~~~~~~~~~~ - * - * Sphinx stylesheet -- flasky theme based on nature theme. - * - * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: 'Georgia', serif; - font-size: 17px; - color: #000; - background: white; - margin: 0; - padding: 0; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 40px auto 0 auto; - width: 700px; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 30px 30px; -} - -img.floatingflask { - padding: 0 0 10px 10px; - float: right; -} - -div.footer { - text-align: right; - color: #888; - padding: 10px; - font-size: 14px; - width: 650px; - margin: 0 auto 40px auto; -} - -div.footer a { - color: #888; - text-decoration: underline; -} - -div.related { - line-height: 32px; - color: #888; -} - -div.related ul { - padding: 0 0 0 10px; -} - -div.related a { - color: #444; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #004B6B; - text-decoration: underline; -} - -a:hover { - color: #6D4100; - text-decoration: underline; -} - -div.body { - padding-bottom: 40px; /* saved for footer */ -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - margin: 30px 0px 10px 0px; - padding: 0; -} - -{% if theme_index_logo %} -div.indexwrapper h1 { - text-indent: -999999px; - background: url({{ theme_index_logo }}) no-repeat center center; - height: {{ theme_index_logo_height }}; -} -{% endif %} - -div.body h2 { font-size: 180%; } -div.body h3 { font-size: 150%; } -div.body h4 { font-size: 130%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: white; - padding: 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - color: #444; - background: #eaeaea; -} - -div.body p, div.body dd, div.body li { - line-height: 1.4em; -} - -div.admonition { - background: #fafafa; - margin: 20px -30px; - padding: 10px 30px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; -} - -div.admonition p.admonition-title { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - font-size: 24px; - margin: 0 0 10px 0; - padding: 0; - line-height: 1; -} - -div.admonition p.last { - margin-bottom: 0; -} - -div.highlight{ - background-color: white; -} - -dt:target, .highlight { - background: #FAF3E8; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -div.warning { - background-color: #ffe4e4; - border: 1px solid #f66; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre, tt { - font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; - font-size: 0.85em; -} - -img.screenshot { -} - -tt.descname, tt.descclassname { - font-size: 0.95em; -} - -tt.descname { - padding-right: 0.08em; -} - -img.screenshot { - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils { - border: 1px solid #888; - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils td, table.docutils th { - border: 1px solid #888; - padding: 0.25em 0.7em; -} - -table.field-list, table.footnote { - border: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} - -table.footnote { - margin: 15px 0; - width: 100%; - border: 1px solid #eee; -} - -table.field-list th { - padding: 0 0.8em 0 0; -} - -table.field-list td { - padding: 0; -} - -table.footnote td { - padding: 0.5em; -} - -dl { - margin: 0; - padding: 0; -} - -dl dd { - margin-left: 30px; -} - -pre { - padding: 0; - margin: 15px -30px; - padding: 8px; - line-height: 1.3em; - padding: 7px 30px; - background: #eee; - border-radius: 2px; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; -} - -dl pre { - margin-left: -60px; - padding-left: 60px; -} - -tt { - background-color: #ecf0f3; - color: #222; - /* padding: 1px 2px; */ -} - -tt.xref, a tt { - background-color: #FBFBFB; -} - -a:hover tt { - background: #EEE; -} diff --git a/docs/source/_themes/flask_small/theme.conf b/docs/source/_themes/flask_small/theme.conf deleted file mode 100644 index 542b462..0000000 --- a/docs/source/_themes/flask_small/theme.conf +++ /dev/null @@ -1,10 +0,0 @@ -[theme] -inherit = basic -stylesheet = flasky.css -nosidebar = true -pygments_style = flask_theme_support.FlaskyStyle - -[options] -index_logo = '' -index_logo_height = 120px -github_fork = '' diff --git a/docs/source/_themes/flask_theme_support.py b/docs/source/_themes/flask_theme_support.py deleted file mode 100644 index 33f4744..0000000 --- a/docs/source/_themes/flask_theme_support.py +++ /dev/null @@ -1,86 +0,0 @@ -# flasky extensions. flasky pygments style based on tango style -from pygments.style import Style -from pygments.token import Keyword, Name, Comment, String, Error, \ - Number, Operator, Generic, Whitespace, Punctuation, Other, Literal - - -class FlaskyStyle(Style): - background_color = "#f8f8f8" - default_style = "" - - styles = { - # No corresponding class for the following: - #Text: "", # class: '' - Whitespace: "underline #f8f8f8", # class: 'w' - Error: "#a40000 border:#ef2929", # class: 'err' - Other: "#000000", # class 'x' - - Comment: "italic #8f5902", # class: 'c' - Comment.Preproc: "noitalic", # class: 'cp' - - Keyword: "bold #004461", # class: 'k' - Keyword.Constant: "bold #004461", # class: 'kc' - Keyword.Declaration: "bold #004461", # class: 'kd' - Keyword.Namespace: "bold #004461", # class: 'kn' - Keyword.Pseudo: "bold #004461", # class: 'kp' - Keyword.Reserved: "bold #004461", # class: 'kr' - Keyword.Type: "bold #004461", # class: 'kt' - - Operator: "#582800", # class: 'o' - Operator.Word: "bold #004461", # class: 'ow' - like keywords - - Punctuation: "bold #000000", # class: 'p' - - # because special names such as Name.Class, Name.Function, etc. - # are not recognized as such later in the parsing, we choose them - # to look the same as ordinary variables. - Name: "#000000", # class: 'n' - Name.Attribute: "#c4a000", # class: 'na' - to be revised - Name.Builtin: "#004461", # class: 'nb' - Name.Builtin.Pseudo: "#3465a4", # class: 'bp' - Name.Class: "#000000", # class: 'nc' - to be revised - Name.Constant: "#000000", # class: 'no' - to be revised - Name.Decorator: "#888", # class: 'nd' - to be revised - Name.Entity: "#ce5c00", # class: 'ni' - Name.Exception: "bold #cc0000", # class: 'ne' - Name.Function: "#000000", # class: 'nf' - Name.Property: "#000000", # class: 'py' - Name.Label: "#f57900", # class: 'nl' - Name.Namespace: "#000000", # class: 'nn' - to be revised - Name.Other: "#000000", # class: 'nx' - Name.Tag: "bold #004461", # class: 'nt' - like a keyword - Name.Variable: "#000000", # class: 'nv' - to be revised - Name.Variable.Class: "#000000", # class: 'vc' - to be revised - Name.Variable.Global: "#000000", # class: 'vg' - to be revised - Name.Variable.Instance: "#000000", # class: 'vi' - to be revised - - Number: "#990000", # class: 'm' - - Literal: "#000000", # class: 'l' - Literal.Date: "#000000", # class: 'ld' - - String: "#4e9a06", # class: 's' - String.Backtick: "#4e9a06", # class: 'sb' - String.Char: "#4e9a06", # class: 'sc' - String.Doc: "italic #8f5902", # class: 'sd' - like a comment - String.Double: "#4e9a06", # class: 's2' - String.Escape: "#4e9a06", # class: 'se' - String.Heredoc: "#4e9a06", # class: 'sh' - String.Interpol: "#4e9a06", # class: 'si' - String.Other: "#4e9a06", # class: 'sx' - String.Regex: "#4e9a06", # class: 'sr' - String.Single: "#4e9a06", # class: 's1' - String.Symbol: "#4e9a06", # class: 'ss' - - Generic: "#000000", # class: 'g' - Generic.Deleted: "#a40000", # class: 'gd' - Generic.Emph: "italic #000000", # class: 'ge' - Generic.Error: "#ef2929", # class: 'gr' - Generic.Heading: "bold #000080", # class: 'gh' - Generic.Inserted: "#00A000", # class: 'gi' - Generic.Output: "#888", # class: 'go' - Generic.Prompt: "#745334", # class: 'gp' - Generic.Strong: "bold #000000", # class: 'gs' - Generic.Subheading: "bold #800080", # class: 'gu' - Generic.Traceback: "bold #a40000", # class: 'gt' - } diff --git a/docs/source/api.rst b/docs/source/api.rst deleted file mode 100644 index 2b4e5f6..0000000 --- a/docs/source/api.rst +++ /dev/null @@ -1,74 +0,0 @@ -.. _api: - -API -=== - -.. module:: multipart - -This section of the documentation covers all of the public interfaces of -python-multipart. - - -Helper Functions ----------------- - -.. currentmodule:: multipart.multipart - -.. autofunction:: parse_form - -.. autofunction:: create_form_parser - - -Main Class ----------- - -.. currentmodule:: multipart.multipart - -.. autoclass:: FormParser - :members: - - -Parsers -------- - -.. currentmodule:: multipart.multipart - -.. autoclass:: BaseParser - :members: - -.. autoclass:: OctetStreamParser - :members: - -.. autoclass:: QuerystringParser - :members: - -.. autoclass:: MultipartParser - :members: - - -Support Classes ---------------- - -.. currentmodule:: multipart.multipart - -.. autoclass:: Field - :members: - -.. autoclass:: File - :members: - - -Decoders --------- - -.. automodule:: multipart.decoders - :members: - - -Exceptions ----------- - -The following are all custom exceptions that python-multipart will raise, for various cases. Each method that will raise an exception will document it in this documentation. - -.. automodule:: multipart.exceptions - :members: diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index d13072e..0000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,256 +0,0 @@ -# python-multipart documentation build configuration file, created by -# sphinx-quickstart on Fri Apr 5 20:24:27 2013. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys, os - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath(os.path.join('..', '..'))) -import multipart -from multipart import __version__ - -# -- General configuration ----------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# 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.intersphinx', 'sphinx.ext.ifconfig'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'python-multipart' -copyright = '2013, Andrew Dunham' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -from multipart import __version__ -# The short X.Y version. -version = __version__ -# The full version, including alpha/beta/rc tags. -release = version - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -sys.path.append(os.path.abspath('_themes')) - -html_theme = 'flask' - -# 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 -# documentation. -html_theme_options = { -} - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['_themes'] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# 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'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -html_show_sourcelink = False - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -html_show_sphinx = False - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'python-multipartdoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'python-multipart.tex', 'python-multipart Documentation', - 'Andrew Dunham', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'python-multipart', 'python-multipart Documentation', - ['Andrew Dunham'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------------ - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'python-multipart', 'python-multipart Documentation', - 'Andrew Dunham', 'python-multipart', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/docs/source/contents.rst.inc b/docs/source/contents.rst.inc deleted file mode 100644 index fa92a94..0000000 --- a/docs/source/contents.rst.inc +++ /dev/null @@ -1,20 +0,0 @@ -User's Guide ------------- - -.. toctree:: - :maxdepth: 2 - - quickstart - - -API Reference -------------- - -If you are looking for information on a specific function, class or -method, this part of the documentation is for you. - -.. toctree:: - :maxdepth: 2 - - api - diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index edf594c..0000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -:orphan: - -Python-Multipart -================ - -.. module:: multipart - -Python-Multipart is a streaming multipart parser for Python. - -.. include:: contents.rst.inc diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst deleted file mode 100644 index b68b06f..0000000 --- a/docs/source/quickstart.rst +++ /dev/null @@ -1,195 +0,0 @@ -.. _quickstart: - -Quickstart -========== - -.. currentmodule:: multipart - -Python-Multipart foo bar baz - - -Simple Example --------------- - -The following example shows a quick example of parsing an incoming request body -in a simple WSGI application:: - - import multipart - - def simple_app(environ, start_response): - ret = [] - - # The following two callbacks just append the name to the return value. - def on_field(field): - ret.append(b"Parsed field named: %s" % (field.field_name,)) - - def on_file(file): - ret.append(b"Parsed file named: %s" % (file.field_name,)) - - # Create headers object. We need to convert from WSGI to the actual - # name of the header, since this library does not assume that you are - # using WSGI. - headers = {'Content-Type': environ['CONTENT_TYPE']} - if 'HTTP_X_FILE_NAME' in environ: - headers['X-File-Name'] = environ['HTTP_X_FILE_NAME'] - if 'CONTENT_LENGTH' in environ: - headers['Content-Length'] = environ['CONTENT_LENGTH'] - - # Parse the form. - multipart.parse_form(headers, environ['wsgi.input'], on_field, on_file) - - # Return something. - start_response('200 OK', [('Content-type', 'text/plain')]) - ret.append(b'\n') - return ret - - from wsgiref.simple_server import make_server - from wsgiref.validate import validator - - httpd = make_server('', 8123, simple_app) - print("Serving on port 8123...") - httpd.serve_forever() - -If you test this with curl, you can see that the parser works: - -.. code-block:: bash - - $ curl -ik -F "foo=bar" http://localhost:8123/ - HTTP/1.0 200 OK - Date: Sun, 07 Apr 2013 01:40:52 GMT - Server: WSGIServer/0.1 Python/2.7.3 - Content-type: text/plain - - Parsed field named: foo - -For a more in-depth example showing how the various parts fit together, check -out the next section. - - -In-Depth Example ----------------- - -In this section, we'll build an application that computes the SHA-256 hash of -all uploaded files in a streaming manner. - -To start, we need a simple WSGI application. We could do this with a framework -like Flask, Django, or Tornado, but for now let's stick to plain WSGI: - -.. code-block:: python - - import multipart - - def simple_app(environ, start_response): - start_response('200 OK', [('Content-type', 'text/plain')]) - return ['Hashes:\n'] - - from wsgiref.simple_server import make_server - httpd = make_server('', 8123, simple_app) - print("Serving on port 8123...") - httpd.serve_forever() - -You can run this and check with curl that it works properly: - -.. code-block:: bash - - $ curl -ik http://localhost:8123/ - HTTP/1.0 200 OK - Date: Sun, 07 Apr 2013 01:49:03 GMT - Server: WSGIServer/0.1 Python/2.7.3 - Content-type: text/plain - Content-Length: 8 - - Hashes: - -Good! It works. Now, let's add some of the code that we need. What we need -to do, essentially, is set up the appropriate parser and callbacks so that we -can access each portion of the request as it arrives, without needing to store -any parts in memory. - -We can start off by checking if we need to create the parser at all - if the -Content-Type isn't `multipart/form-data`, then we're not going to do anything. - - - - -The final code should look like this: - -.. code-block:: python - - import hashlib - import multipart - from multipart.multipart import parse_options_header - - def simple_app(environ, start_response): - ret = [] - - # Python 2 doesn't have the "nonlocal" keyword from Python 3, so we get - # around it by setting attributes on a dummy object. - class g(object): - hash = None - - # This is called when a new part arrives. We create a new hash object - # in this callback. - def on_part_begin(): - g.hash = hashlib.sha256() - - # We got some data! Update our hash. - def on_part_data(data, start, end): - g.hash.update(data[start:end]) - - # Our current part is done, so we can finish the hash. - def on_part_end(): - ret.append("Part hash: %s" % (g.hash.hexdigest(),)) - - # Parse the Content-Type header to get the multipart boundary. - content_type, params = parse_options_header(environ['CONTENT_TYPE']) - boundary = params.get(b'boundary') - - # Callbacks dictionary. - callbacks = { - 'on_part_begin': on_part_begin, - 'on_part_data': on_part_data, - 'on_part_end': on_part_end, - } - - # Create the parser. - parser = multipart.MultipartParser(boundary, callbacks) - - # The input stream is from the WSGI environ. - inp = environ['wsgi.input'] - - # Feed the parser with data from the request. - size = int(environ['CONTENT_LENGTH']) - while size > 0: - to_read = min(size, 1024 * 1024) - data = inp.read(to_read) - parser.write(data) - - size -= len(data) - if len(data) != to_read: - break - - start_response('200 OK', [('Content-type', 'text/plain')]) - return ret - - from wsgiref.simple_server import make_server - httpd = make_server('', 8123, simple_app) - print("Serving on port 8123...") - httpd.serve_forever() - - -And you can see that this works: - -.. code-block:: bash - - $ echo "Foo bar" > /tmp/test.txt - $ shasum -a 256 /tmp/test.txt - 0b64696c0f7ddb9e3435341720988d5455b3b0f0724688f98ec8e6019af3d931 /tmp/test.txt - $ curl -ik -F file=@/tmp/test.txt http://localhost:8123/ - HTTP/1.0 200 OK - Date: Sun, 07 Apr 2013 02:09:10 GMT - Server: WSGIServer/0.1 Python/2.7.3 - Content-type: text/plain - - Hashes: - Part hash: 0b64696c0f7ddb9e3435341720988d5455b3b0f0724688f98ec8e6019af3d931 diff --git a/docs_requirements.txt b/docs_requirements.txt deleted file mode 100644 index 8c3eda5..0000000 --- a/docs_requirements.txt +++ /dev/null @@ -1,19 +0,0 @@ -Jinja2==3.1.3 -PyYAML==6.0.1 -Pygments==2.17.2 -Sphinx==7.2.6 -cov-core==1.15.0 -coverage==7.4.1 -distribute==0.7.3 -docutils==0.20.1 -invoke==2.2.0 -pexpect-u==2.5.1 -py==1.11.0 -pytest==8.0.0 -pytest-capturelog==0.7 -pytest-cov==4.1.0 -pytest-timeout==2.2.0 -sphinx-bootstrap-theme==0.8.1 -tox==4.12.1 -virtualenv==20.25.0 -wsgiref==0.1.2 diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..e4b624d --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,84 @@ +site_name: Python Multipart +site_url: https://kludex.github.io/python-multipart/ +site_author: Marcelo Trylesinski,Andrew Dunham +site_description: A streaming multipart parser for Python. +repo_name: kludex/python-multipart +repo_url: https://github.com//kludex/python-multipart +edit_uri: https://github.com/kludex/python-multipart/tree/master/docs + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/kludex/python-multipart + - icon: fontawesome/brands/python + link: https://pypi.org/project/python-multipart + +theme: + name: "material" + features: + - navigation.tracking + - content.code.copy + - content.tooltips + - content.code.annotate + - navigation.sections + + palette: + # Palette for light mode + - scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to light mode + font: + text: Roboto + code: Roboto Mono + features: + - content.code.copy + - content.code.annotate + +nav: + - Introduction: index.md + - API Reference: api.md + - Changelog: changelog.md + +plugins: + - search + - mkdocstrings: + default_handler: python + handlers: + python: + options: + parameter_headings: true + paths: [python-multipart] + load_external_modules: true + allow_inspection: true + show_bases: true + group_by_category: true + docstring_style: sphinx + show_symbol_type_heading: true + show_symbol_type_toc: true + show_category_heading: true + domains: [std, py] + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences + - attr_list + - md_in_html + - pymdownx.tabbed: + alternate_style: true + - toc: + permalink: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.snippets: + check_paths: true + - pymdownx.highlight: + anchor_linenums: true diff --git a/pyproject.toml b/pyproject.toml index ed22c97..13f9c28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,13 @@ dev = [ "ruff==0.2.1", "hatch", ] +docs = [ + "mkdocs==1.5.3", + "mkdocs-material==9.5.9", + "mkdocstrings==0.24.0", + "mkdocstrings-python==1.8.0", + "mkdocs-autorefs==0.5.0", +] [project.urls] Homepage = "https://github.com/Kludex/python-multipart" From 2baf8b1e55f80ace461c3937f345d5f3a6cd23a4 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 11 Feb 2024 21:55:03 +0100 Subject: [PATCH 36/94] Improve type hints (#110) --- multipart/decoders.py | 2 +- multipart/multipart.py | 49 +++++++++++++++++++++++++++-------------- tests/test_multipart.py | 30 +++++++++++++------------ 3 files changed, 49 insertions(+), 32 deletions(-) diff --git a/multipart/decoders.py b/multipart/decoders.py index 417650c..e401fa0 100644 --- a/multipart/decoders.py +++ b/multipart/decoders.py @@ -73,7 +73,7 @@ def write(self, data): # Return the length of the data to indicate no error. return len(data) - def close(self): + def close(self) -> None: """Close this decoder. If the underlying object has a `close()` method, this function will call it. """ diff --git a/multipart/multipart.py b/multipart/multipart.py index 21d9ac1..0c7c447 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -1,5 +1,6 @@ from __future__ import annotations +import io import logging import os import shutil @@ -534,14 +535,14 @@ def _get_disk_file(self): self._actual_file_name = fname return tmp_file - def write(self, data: bytes): + def write(self, data: bytes) -> int: """Write some data to the File. :param data: a bytestring """ return self.on_data(data) - def on_data(self, data: bytes): + def on_data(self, data: bytes) -> int: """This method is a callback that will be called whenever data is written to the File. @@ -652,7 +653,7 @@ def callback(self, name: str, data: bytes | None = None, start: int | None = Non self.logger.debug("Calling %s with no data", name) func() - def set_callback(self, name: str, new_func): + def set_callback(self, name: str, new_func: Callable[..., Any] | None) -> None: """Update the function for a callback. Removes from the callbacks dict if new_func is None. @@ -1096,7 +1097,7 @@ def __init__(self, boundary: bytes | str, callbacks: MultipartCallbacks = {}, ma # Note: the +8 is since we can have, at maximum, "\r\n--" + boundary + # "--\r\n" at the final boundary, and the length of '\r\n--' and # '--\r\n' is 8 bytes. - self.lookbehind = [NULL for x in range(len(boundary) + 8)] + self.lookbehind = [NULL for _ in range(len(boundary) + 8)] def write(self, data: bytes) -> int: """Write some data to the parser, which will perform size verification, @@ -1642,22 +1643,23 @@ def __init__( # Depending on the Content-Type, we instantiate the correct parser. if content_type == "application/octet-stream": - f: FileProtocol | None = None + file: FileProtocol = None # type: ignore def on_start() -> None: - nonlocal f - f = FileClass(file_name, None, config=self.config) + nonlocal file + file = FileClass(file_name, None, config=self.config) def on_data(data: bytes, start: int, end: int) -> None: - nonlocal f - f.write(data[start:end]) + nonlocal file + file.write(data[start:end]) def _on_end() -> None: + nonlocal file # Finalize the file itself. - f.finalize() + file.finalize() # Call our callback. - on_file(f) + on_file(file) # Call the on-end callback. if self.on_end is not None: @@ -1672,7 +1674,7 @@ def _on_end() -> None: elif content_type == "application/x-www-form-urlencoded" or content_type == "application/x-url-encoded": name_buffer: list[bytes] = [] - f: FieldProtocol | None = None + f: FieldProtocol = None # type: ignore def on_field_start() -> None: pass @@ -1747,13 +1749,13 @@ def on_part_end() -> None: else: on_field(f) - def on_header_field(data: bytes, start: int, end: int): + def on_header_field(data: bytes, start: int, end: int) -> None: header_name.append(data[start:end]) - def on_header_value(data: bytes, start: int, end: int): + def on_header_value(data: bytes, start: int, end: int) -> None: header_value.append(data[start:end]) - def on_header_end(): + def on_header_end() -> None: headers[b"".join(header_name)] = b"".join(header_value) del header_name[:] del header_value[:] @@ -1855,7 +1857,13 @@ def __repr__(self) -> str: return "{}(content_type={!r}, parser={!r})".format(self.__class__.__name__, self.content_type, self.parser) -def create_form_parser(headers, on_field, on_file, trust_x_headers=False, config={}): +def create_form_parser( + headers: dict[str, bytes], + on_field: OnFieldCallback, + on_file: OnFileCallback, + trust_x_headers: bool = False, + config={}, +): """This function is a helper function to aid in creating a FormParser instances. Given a dictionary-like headers object, it will determine the correct information needed, instantiate a FormParser with the @@ -1898,7 +1906,14 @@ def create_form_parser(headers, on_field, on_file, trust_x_headers=False, config return form_parser -def parse_form(headers, input_stream, on_field, on_file, chunk_size=1048576, **kwargs): +def parse_form( + headers: dict[str, bytes], + input_stream: io.FileIO, + on_field: OnFieldCallback, + on_file: OnFileCallback, + chunk_size: int = 1048576, + **kwargs, +): """This function is useful if you just want to parse a request body, without too much work. Pass it a dictionary-like object of the request's headers, and a file-like object for the input stream, along with two diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 9dc10bb..79968e0 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import random import sys @@ -288,19 +290,19 @@ def setUp(self): self.b.callbacks = {} def test_callbacks(self): - # The stupid list-ness is to get around lack of nonlocal on py2 - l = [0] + called = 0 def on_foo(): - l[0] += 1 + nonlocal called + called += 1 self.b.set_callback("foo", on_foo) self.b.callback("foo") - self.assertEqual(l[0], 1) + self.assertEqual(called, 1) self.b.set_callback("foo", None) self.b.callback("foo") - self.assertEqual(l[0], 1) + self.assertEqual(called, 1) class TestQuerystringParser(unittest.TestCase): @@ -316,15 +318,15 @@ def setUp(self): self.reset() def reset(self): - self.f = [] + self.f: list[tuple[bytes, bytes]] = [] - name_buffer = [] - data_buffer = [] + name_buffer: list[bytes] = [] + data_buffer: list[bytes] = [] - def on_field_name(data, start, end): + def on_field_name(data: bytes, start: int, end: int) -> None: name_buffer.append(data[start:end]) - def on_field_data(data, start, end): + def on_field_data(data: bytes, start: int, end: int) -> None: data_buffer.append(data[start:end]) def on_field_end(): @@ -705,13 +707,13 @@ def split_all(val): class TestFormParser(unittest.TestCase): def make(self, boundary, config={}): self.ended = False - self.files = [] - self.fields = [] + self.files: list[File] = [] + self.fields: list[Field] = [] - def on_field(f): + def on_field(f: Field) -> None: self.fields.append(f) - def on_file(f): + def on_file(f: File) -> None: self.files.append(f) def on_end(): From f4479c6b3ae841c6f48931dd406f18d6f35f64c1 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 12 Feb 2024 23:43:17 +0100 Subject: [PATCH 37/94] Improve type hints on `File` (#111) --- multipart/decoders.py | 19 ++++++----- multipart/multipart.py | 74 ++++++++++++++++++----------------------- tests/test_multipart.py | 6 +++- 3 files changed, 48 insertions(+), 51 deletions(-) diff --git a/multipart/decoders.py b/multipart/decoders.py index e401fa0..218abe4 100644 --- a/multipart/decoders.py +++ b/multipart/decoders.py @@ -1,5 +1,6 @@ import base64 import binascii +from io import BufferedWriter from .exceptions import DecodeError @@ -33,11 +34,11 @@ class Base64Decoder: :param underlying: the underlying object to pass writes to """ - def __init__(self, underlying): + def __init__(self, underlying: BufferedWriter): self.cache = bytearray() self.underlying = underlying - def write(self, data): + def write(self, data: bytes) -> int: """Takes any input data provided, decodes it as base64, and passes it on to the underlying object. If the data provided is invalid base64 data, then this method will raise @@ -80,7 +81,7 @@ def close(self) -> None: if hasattr(self.underlying, "close"): self.underlying.close() - def finalize(self): + def finalize(self) -> None: """Finalize this object. This should be called when no more data should be written to the stream. This function can raise a :class:`multipart.exceptions.DecodeError` if there is some remaining @@ -97,7 +98,7 @@ def finalize(self): if hasattr(self.underlying, "finalize"): self.underlying.finalize() - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}(underlying={self.underlying!r})" @@ -111,11 +112,11 @@ class QuotedPrintableDecoder: :param underlying: the underlying object to pass writes to """ - def __init__(self, underlying): + def __init__(self, underlying: BufferedWriter) -> None: self.cache = b"" self.underlying = underlying - def write(self, data): + def write(self, data: bytes) -> int: """Takes any input data provided, decodes it as quoted-printable, and passes it on to the underlying object. @@ -142,14 +143,14 @@ def write(self, data): self.cache = rest return len(data) - def close(self): + def close(self) -> None: """Close this decoder. If the underlying object has a `close()` method, this function will call it. """ if hasattr(self.underlying, "close"): self.underlying.close() - def finalize(self): + def finalize(self) -> None: """Finalize this object. This should be called when no more data should be written to the stream. This function will not raise any exceptions, but it may write more data to the underlying object if @@ -167,5 +168,5 @@ def finalize(self): if hasattr(self.underlying, "finalize"): self.underlying.finalize() - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}(underlying={self.underlying!r})" diff --git a/multipart/multipart.py b/multipart/multipart.py index 0c7c447..9f61eb8 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -41,7 +41,7 @@ class MultipartCallbacks(TypedDict, total=False): on_headers_finished: Callable[[], None] on_end: Callable[[], None] - class FormParserConfig(TypedDict, total=False): + class FormParserConfig(TypedDict): UPLOAD_DIR: str | None UPLOAD_KEEP_FILENAME: bool UPLOAD_KEEP_EXTENSIONS: bool @@ -50,7 +50,7 @@ class FormParserConfig(TypedDict, total=False): MAX_BODY_SIZE: float class FileConfig(TypedDict, total=False): - UPLOAD_DIR: str | None + UPLOAD_DIR: str | bytes | None UPLOAD_DELETE_TMP: bool UPLOAD_KEEP_FILENAME: bool UPLOAD_KEEP_EXTENSIONS: bool @@ -374,7 +374,7 @@ class File: configuration keys and their corresponding values. """ - def __init__(self, file_name: bytes | None, field_name: bytes | None = None, config: FileConfig = {}): + def __init__(self, file_name: bytes | None, field_name: bytes | None = None, config: FileConfig = {}) -> None: # Save configuration, set other variables default. self.logger = logging.getLogger(__name__) self._config = config @@ -471,7 +471,7 @@ def flush_to_disk(self) -> None: # Close the old file object. old_fileobj.close() - def _get_disk_file(self): + def _get_disk_file(self) -> io.BufferedRandom | tempfile._TemporaryFileWrapper[bytes]: # type: ignore[reportPrivateUsage] """This function is responsible for getting a file object on-disk for us.""" self.logger.info("Opening a file on disk") @@ -486,9 +486,7 @@ def _get_disk_file(self): # Build our filename. # TODO: what happens if we don't have a filename? - fname = self._file_base - if keep_extensions: - fname = fname + self._ext + fname = self._file_base + self._ext if keep_extensions else self._file_base path = os.path.join(file_dir, fname) try: @@ -503,25 +501,21 @@ def _get_disk_file(self): # Build options array. # Note that on Python 3, tempfile doesn't support byte names. We # encode our paths using the default filesystem encoding. - options = {} - if keep_extensions: - ext = self._ext - if isinstance(ext, bytes): - ext = ext.decode(sys.getfilesystemencoding()) - - options["suffix"] = ext - if file_dir is not None: - d = file_dir - if isinstance(d, bytes): - d = d.decode(sys.getfilesystemencoding()) + suffix = self._ext.decode(sys.getfilesystemencoding()) if keep_extensions else None - options["dir"] = d - options["delete"] = delete_tmp + if file_dir is None: + dir = None + elif isinstance(file_dir, bytes): + dir = file_dir.decode(sys.getfilesystemencoding()) + else: + dir = file_dir # Create a temporary (named) file with the appropriate settings. - self.logger.info("Creating a temporary file with options: %r", options) + self.logger.info( + "Creating a temporary file with options: %r", {"suffix": suffix, "delete": delete_tmp, "dir": dir} + ) try: - tmp_file = tempfile.NamedTemporaryFile(**options) + tmp_file = tempfile.NamedTemporaryFile(suffix=suffix, delete=delete_tmp, dir=dir) except OSError: self.logger.exception("Error creating named temporary file") raise FileError("Error creating named temporary file") @@ -563,11 +557,8 @@ def on_data(self, data: bytes) -> int: self._bytes_written += bwritten # If we're in-memory and are over our limit, we create a file. - if ( - self._in_memory - and self._config.get("MAX_MEMORY_FILE_SIZE") is not None - and (self._bytes_written > self._config.get("MAX_MEMORY_FILE_SIZE")) - ): + max_memory_file_size = self._config.get("MAX_MEMORY_FILE_SIZE") + if self._in_memory and max_memory_file_size is not None and (self._bytes_written > max_memory_file_size): self.logger.info("Flushing to disk") self.flush_to_disk() @@ -617,9 +608,7 @@ class BaseParser: performance. """ - callbacks: dict[str, Callable[..., Any]] - - def __init__(self): + def __init__(self) -> None: self.logger = logging.getLogger(__name__) def callback(self, name: str, data: bytes | None = None, start: int | None = None, end: int | None = None): @@ -706,7 +695,7 @@ class OctetStreamParser(BaseParser): i.e. unbounded. """ - def __init__(self, callbacks: OctetStreamCallbacks = {}, max_size=float("inf")): + def __init__(self, callbacks: OctetStreamCallbacks = {}, max_size: float = float("inf")): super().__init__() self.callbacks = callbacks self._started = False @@ -716,7 +705,7 @@ def __init__(self, callbacks: OctetStreamCallbacks = {}, max_size=float("inf")): self.max_size = max_size self._current_size = 0 - def write(self, data: bytes): + def write(self, data: bytes) -> int: """Write some data to the parser, which will perform size verification, and then pass the data to the underlying callback. @@ -803,7 +792,9 @@ class QuerystringParser(BaseParser): state: QuerystringState - def __init__(self, callbacks: QuerystringCallbacks = {}, strict_parsing: bool = False, max_size=float("inf")): + def __init__( + self, callbacks: QuerystringCallbacks = {}, strict_parsing: bool = False, max_size: float = float("inf") + ) -> None: super().__init__() self.state = QuerystringState.BEFORE_FIELD self._found_sep = False @@ -1060,7 +1051,9 @@ class MultipartParser(BaseParser): i.e. unbounded. """ - def __init__(self, boundary: bytes | str, callbacks: MultipartCallbacks = {}, max_size=float("inf")): + def __init__( + self, boundary: bytes | str, callbacks: MultipartCallbacks = {}, max_size: float = float("inf") + ) -> None: # Initialize parser state. super().__init__() self.state = MultipartState.START @@ -1618,8 +1611,8 @@ def __init__( file_name: bytes | None = None, FileClass: type[FileProtocol] = File, FieldClass: type[FieldProtocol] = Field, - config: FormParserConfig = {}, - ): + config: dict[Any, Any] = {}, + ) -> None: self.logger = logging.getLogger(__name__) # Save variables. @@ -1787,7 +1780,7 @@ def on_headers_finished() -> None: # TODO: check that we properly handle 8bit / 7bit encoding. transfer_encoding = headers.get(b"Content-Transfer-Encoding", b"7bit") - if transfer_encoding == b"binary" or transfer_encoding == b"8bit" or transfer_encoding == b"7bit": + if transfer_encoding in (b"binary", b"8bit", b"7bit"): writer = f elif transfer_encoding == b"base64": @@ -1862,8 +1855,8 @@ def create_form_parser( on_field: OnFieldCallback, on_file: OnFileCallback, trust_x_headers: bool = False, - config={}, -): + config: dict[Any, Any] = {}, +) -> FormParser: """This function is a helper function to aid in creating a FormParser instances. Given a dictionary-like headers object, it will determine the correct information needed, instantiate a FormParser with the @@ -1912,8 +1905,7 @@ def parse_form( on_field: OnFieldCallback, on_file: OnFileCallback, chunk_size: int = 1048576, - **kwargs, -): +) -> None: """This function is useful if you just want to parse a request body, without too much work. Pass it a dictionary-like object of the request's headers, and a file-like object for the input stream, along with two diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 79968e0..93fd38d 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -6,6 +6,7 @@ import tempfile import unittest from io import BytesIO +from typing import TYPE_CHECKING from unittest.mock import Mock import yaml @@ -28,6 +29,9 @@ from .compat import parametrize, parametrize_class, slow_test +if TYPE_CHECKING: + from multipart.multipart import FileConfig + # Get the current directory for our later test cases. curr_dir = os.path.abspath(os.path.dirname(__file__)) @@ -95,7 +99,7 @@ def test_set_none(self): class TestFile(unittest.TestCase): def setUp(self): - self.c = {} + self.c: FileConfig = {} self.d = force_bytes(tempfile.mkdtemp()) self.f = File(b"foo.txt", config=self.c) From 5c7dce879dbc3a41136d0f40fbd4ebfd9e78b6fb Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 12 Feb 2024 23:52:48 +0100 Subject: [PATCH 38/94] Add type hint to helper functions (#112) --- multipart/multipart.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/multipart/multipart.py b/multipart/multipart.py index 9f61eb8..ea8ccca 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -141,15 +141,15 @@ class MultipartState(IntEnum): # str on Py2, and bytes on Py3. Same with getting the ordinal value of a byte, # and joining a list of bytes together. # These functions abstract that. -def lower_char(c): +def lower_char(c: int) -> int: return c | 0x20 -def ord_char(c): +def ord_char(c: int) -> int: return c -def join_bytes(b): +def join_bytes(b: bytes) -> bytes: return bytes(list(b)) @@ -290,7 +290,7 @@ def field_name(self) -> bytes: return self._name @property - def value(self): + def value(self) -> bytes | None: """This property returns the value of the form field.""" if self._cache is _missing: self._cache = b"".join(self._value) @@ -424,7 +424,7 @@ def file_object(self): return self._fileobj @property - def size(self): + def size(self) -> int: """The total size of this file, counted as the number of bytes that currently have been written to the file. """ @@ -1142,7 +1142,7 @@ def set_mark(name: str): self.marks[name] = i # Remove a mark. - def delete_mark(name: str, reset: bool = False): + def delete_mark(name: str, reset: bool = False) -> None: self.marks.pop(name, None) # Helper function that makes calling a callback with data easier. The @@ -1150,7 +1150,7 @@ def delete_mark(name: str, reset: bool = False): # end of the buffer, and reset the mark, instead of deleting it. This # is used at the end of the function to call our callbacks with any # remaining data in this chunk. - def data_callback(name: str, remaining: bool = False): + def data_callback(name: str, remaining: bool = False) -> None: marked_index = self.marks.get(name) if marked_index is None: return @@ -1531,7 +1531,7 @@ def finalize(self) -> None: # error or otherwise state that we're not finished parsing. pass - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}(boundary={self.boundary!r})" From 1888ecce894cba44fb7f3090213d18f7b848df8c Mon Sep 17 00:00:00 2001 From: Onuralp SEZER Date: Tue, 13 Feb 2024 22:32:17 +0300 Subject: [PATCH 39/94] feat(ruff): lint config updated to new usage (#113) --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 13f9c28..595f97f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,8 @@ include = ["/multipart", "/tests"] [tool.ruff] line-length = 120 + +[tool.ruff.lint] select = ["E", "F", "I", "FA"] ignore = ["B904", "B028", "F841", "E741"] From a437ebcc7f7477cece6850466318b6968891633b Mon Sep 17 00:00:00 2001 From: eltbus <33374178+eltbus@users.noreply.github.com> Date: Sat, 17 Feb 2024 10:56:48 +0100 Subject: [PATCH 40/94] Minor fix for `Field.__repr__` (#114) --- multipart/multipart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multipart/multipart.py b/multipart/multipart.py index ea8ccca..170151f 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -304,7 +304,7 @@ def __eq__(self, other: object) -> bool: return NotImplemented def __repr__(self) -> str: - if len(self.value) > 97: + if self.value is not None and len(self.value) > 97: # We get the repr, and then insert three dots before the final # quote. v = repr(self.value[:97])[:-1] + "...'" From c9895359ae91d4b488172c449eadde99528b46a1 Mon Sep 17 00:00:00 2001 From: Janus Date: Mon, 19 Feb 2024 21:55:40 +0100 Subject: [PATCH 41/94] Change documentation link to new location (#119) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 595f97f..6a424bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ docs = [ [project.urls] Homepage = "https://github.com/Kludex/python-multipart" -Documentation = "https://andrew-d.github.io/python-multipart/" +Documentation = "https://kludex.github.io/python-multipart/" Changelog = "https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md" Source = "https://github.com/Kludex/python-multipart" From a0cdfd10366c2298b85242ceda9d91d88e3b5e4c Mon Sep 17 00:00:00 2001 From: Janus Date: Tue, 20 Feb 2024 15:02:08 +0100 Subject: [PATCH 42/94] Remove `slow_test` pytest marker (#120) * explicit register slow_test pytest mark to get rid of pytest warning * import pytest instead of pytest.mark * removed slow_test marker * removed unused pytest import --- tasks.py | 4 ---- tests/compat.py | 23 ----------------------- tests/test_multipart.py | 4 +--- 3 files changed, 1 insertion(+), 30 deletions(-) diff --git a/tasks.py b/tasks.py index f2cec11..dba9e2a 100644 --- a/tasks.py +++ b/tasks.py @@ -23,10 +23,6 @@ def test(ctx, all=False): '--timeout=30' # Each test should timeout after 30 sec ] - # Default to not running the slow tests. - if not all: - test_cmd.append('-m "not slow_test"') - # Test in this directory test_cmd.append("tests") diff --git a/tests/compat.py b/tests/compat.py index 8b0ccae..845a926 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -32,29 +32,6 @@ def _samefile(x, y): sys.path.insert(0, path) -# Check if pytest is imported. If so, we use it to create marking decorators. -# If not, we just create a function that does nothing. -try: - import pytest -except ImportError: - pytest = None - -if pytest is not None: - slow_test = pytest.mark.slow_test - xfail = pytest.mark.xfail - -else: - - def slow_test(x): - return x - - def xfail(*args, **kwargs): - if len(args) > 0 and isinstance(args[0], types.FunctionType): - return args[0] - - return lambda x: x - - # We don't use the pytest parametrizing function, since it seems to break # with unittest.TestCase subclasses. def parametrize(field_names, field_values): diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 93fd38d..3a814fb 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -27,7 +27,7 @@ parse_options_header, ) -from .compat import parametrize, parametrize_class, slow_test +from .compat import parametrize, parametrize_class if TYPE_CHECKING: from multipart.multipart import FileConfig @@ -905,7 +905,6 @@ def test_feed_blocks(self): # Assert that our field is here. self.assert_field(b"field", b"0123456789ABCDEFGHIJ0123456789ABCDEFGHIJ") - @slow_test def test_request_body_fuzz(self): """ This test randomly fuzzes the request body to ensure that no strange @@ -978,7 +977,6 @@ def test_request_body_fuzz(self): print("Failures: %d" % (failures,)) print("Exceptions: %d" % (exceptions,)) - @slow_test def test_request_body_fuzz_random_data(self): """ This test will fuzz the multipart parser with some number of iterations From ed02d22333fa8e65ad9afe15f1d8f61113fd7fa4 Mon Sep 17 00:00:00 2001 From: yecril23pl <151100823+yecril23pl@users.noreply.github.com> Date: Tue, 20 Feb 2024 22:49:25 +0100 Subject: [PATCH 43/94] Minor spelling in README.md (#121) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7a6f970..b5d4ae1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ --- -`python-multipart` is an Apache2 licensed streaming multipart parser for Python. +`python-multipart` is an Apache2-licensed streaming multipart parser for Python. Test coverage is currently 100%. ## Why? From cf25a5470d26baceebb20623ca8ff59f4e00289f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 07:32:26 +0100 Subject: [PATCH 44/94] Bump the python-packages group with 6 updates (#130) --- pyproject.toml | 12 ++++++------ requirements.txt | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6a424bb..682013a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,25 +34,25 @@ dependencies = [] dev = [ "atomicwrites==1.4.1", "attrs==23.2.0", - "coverage==7.4.1", + "coverage==7.4.3", "more-itertools==10.2.0", "pbr==6.0.0", "pluggy==1.4.0", "py==1.11.0", - "pytest==8.0.0", + "pytest==8.0.2", "pytest-cov==4.1.0", "PyYAML==6.0.1", "invoke==2.2.0", "pytest-timeout==2.2.0", - "ruff==0.2.1", + "ruff==0.3.0", "hatch", ] docs = [ "mkdocs==1.5.3", - "mkdocs-material==9.5.9", - "mkdocstrings==0.24.0", + "mkdocs-material==9.5.12", + "mkdocstrings==0.24.1", "mkdocstrings-python==1.8.0", - "mkdocs-autorefs==0.5.0", + "mkdocs-autorefs==1.0.1", ] [project.urls] diff --git a/requirements.txt b/requirements.txt index 23baf78..a70070b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ atomicwrites==1.4.1 attrs==23.2.0 -coverage==7.4.1 +coverage==7.4.3 more-itertools==10.2.0 pbr==6.0.0 pluggy==1.4.0 py==1.11.0 -pytest==8.0.0 +pytest==8.0.2 PyYAML==6.0.1 -ruff==0.2.1 +ruff==0.3.0 From 7b31383bc1d1fb5848bfe73e160dfbe9320dc98b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 07:32:46 +0100 Subject: [PATCH 45/94] Bump the github-actions group with 1 update (#129) --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9a96079..c521727 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,7 +34,7 @@ jobs: - name: Build package run: python -m build - name: Publish package - uses: pypa/gh-action-pypi-publish@v1.8.11 + uses: pypa/gh-action-pypi-publish@v1.8.12 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} From 3b15e3889a9e9ff7877a665c39414244b72f3ee1 Mon Sep 17 00:00:00 2001 From: Onuralp SEZER Date: Fri, 8 Mar 2024 20:42:52 +0300 Subject: [PATCH 46/94] Convert sphinx tables to markdown tables on the docstrings (#109) Signed-off-by: Onuralp SEZER --- multipart/multipart.py | 134 +++++++++-------------------------------- 1 file changed, 28 insertions(+), 106 deletions(-) diff --git a/multipart/multipart.py b/multipart/multipart.py index 170151f..49aeb23 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -322,47 +322,13 @@ class File: There are some options that can be passed to the File to change behavior of the class. Valid options are as follows: - .. list-table:: - :widths: 15 5 5 30 - :header-rows: 1 - - * - Name - - Type - - Default - - Description - * - UPLOAD_DIR - - `str` - - None - - The directory to store uploaded files in. If this is None, a - temporary file will be created in the system's standard location. - * - UPLOAD_DELETE_TMP - - `bool` - - True - - Delete automatically created TMP file - * - UPLOAD_KEEP_FILENAME - - `bool` - - False - - Whether or not to keep the filename of the uploaded file. If True, - then the filename will be converted to a safe representation (e.g. - by removing any invalid path segments), and then saved with the - same name). Otherwise, a temporary name will be used. - * - UPLOAD_KEEP_EXTENSIONS - - `bool` - - False - - Whether or not to keep the uploaded file's extension. If False, the - file will be saved with the default temporary extension (usually - ".tmp"). Otherwise, the file's extension will be maintained. Note - that this will properly combine with the UPLOAD_KEEP_FILENAME - setting. - * - MAX_MEMORY_FILE_SIZE - - `int` - - 1 MiB - - The maximum number of bytes of a File to keep in memory. By - default, the contents of a File are kept into memory until a certain - limit is reached, after which the contents of the File are written - to a temporary file. This behavior can be disabled by setting this - value to an appropriately large value (or, for example, infinity, - such as `float('inf')`. + | Name | Type | Default | Description | + |-----------------------|-------|---------|-------------| + | UPLOAD_DIR | `str` | None | The directory to store uploaded files in. If this is None, a temporary file will be created in the system's standard location. | + | UPLOAD_DELETE_TMP | `bool`| True | Delete automatically created TMP file | + | UPLOAD_KEEP_FILENAME | `bool`| False | Whether or not to keep the filename of the uploaded file. If True, then the filename will be converted to a safe representation (e.g. by removing any invalid path segments), and then saved with the same name). Otherwise, a temporary name will be used. | + | UPLOAD_KEEP_EXTENSIONS| `bool`| False | Whether or not to keep the uploaded file's extension. If False, the file will be saved with the default temporary extension (usually ".tmp"). Otherwise, the file's extension will be maintained. Note that this will properly combine with the UPLOAD_KEEP_FILENAME setting. | + | MAX_MEMORY_FILE_SIZE | `int` | 1 MiB | The maximum number of bytes of a File to keep in memory. By default, the contents of a File are kept into memory until a certain limit is reached, after which the contents of the File are written to a temporary file. This behavior can be disabled by setting this value to an appropriately large value (or, for example, infinity, such as `float('inf')`. | :param file_name: The name of the file that this :class:`File` represents @@ -372,7 +338,7 @@ class File: :param config: The configuration for this File. See above for valid configuration keys and their corresponding values. - """ + """ # noqa: E501 def __init__(self, file_name: bytes | None, field_name: bytes | None = None, config: FileConfig = {}) -> None: # Save configuration, set other variables default. @@ -748,28 +714,13 @@ class QuerystringParser(BaseParser): """This is a streaming querystring parser. It will consume data, and call the callbacks given when it has data. - .. list-table:: - :widths: 15 10 30 - :header-rows: 1 - - * - Callback Name - - Parameters - - Description - * - on_field_start - - None - - Called when a new field is encountered. - * - on_field_name - - data, start, end - - Called when a portion of a field's name is encountered. - * - on_field_data - - data, start, end - - Called when a portion of a field's data is encountered. - * - on_field_end - - None - - Called when the end of a field is encountered. - * - on_end - - None - - Called when the parser is finished parsing all data. + | Callback Name | Parameters | Description | + |----------------|-----------------|-----------------------------------------------------| + | on_field_start | None | Called when a new field is encountered. | + | on_field_name | data, start, end| Called when a portion of a field's name is encountered. | + | on_field_data | data, start, end| Called when a portion of a field's data is encountered. | + | on_field_end | None | Called when the end of a field is encountered. | + | on_end | None | Called when the parser is finished parsing all data.| :param callbacks: A dictionary of callbacks. See the documentation for :class:`BaseParser`. @@ -788,7 +739,7 @@ class QuerystringParser(BaseParser): :param max_size: The maximum size of body to parse. Defaults to infinity - i.e. unbounded. - """ + """ # noqa: E501 state: QuerystringState @@ -999,46 +950,17 @@ def __repr__(self) -> str: class MultipartParser(BaseParser): """This class is a streaming multipart/form-data parser. - .. list-table:: - :widths: 15 10 30 - :header-rows: 1 - - * - Callback Name - - Parameters - - Description - * - on_part_begin - - None - - Called when a new part of the multipart message is encountered. - * - on_part_data - - data, start, end - - Called when a portion of a part's data is encountered. - * - on_part_end - - None - - Called when the end of a part is reached. - * - on_header_begin - - None - - Called when we've found a new header in a part of a multipart - message - * - on_header_field - - data, start, end - - Called each time an additional portion of a header is read (i.e. the - part of the header that is before the colon; the "Foo" in - "Foo: Bar"). - * - on_header_value - - data, start, end - - Called when we get data for a header. - * - on_header_end - - None - - Called when the current header is finished - i.e. we've reached the - newline at the end of the header. - * - on_headers_finished - - None - - Called when all headers are finished, and before the part data - starts. - * - on_end - - None - - Called when the parser is finished parsing all data. - + | Callback Name | Parameters | Description | + |--------------------|-----------------|-------------| + | on_part_begin | None | Called when a new part of the multipart message is encountered. | + | on_part_data | data, start, end| Called when a portion of a part's data is encountered. | + | on_part_end | None | Called when the end of a part is reached. | + | on_header_begin | None | Called when we've found a new header in a part of a multipart message | + | on_header_field | data, start, end| Called each time an additional portion of a header is read (i.e. the part of the header that is before the colon; the "Foo" in "Foo: Bar"). | + | on_header_value | data, start, end| Called when we get data for a header. | + | on_header_end | None | Called when the current header is finished - i.e. we've reached the newline at the end of the header. | + | on_headers_finished| None | Called when all headers are finished, and before the part data starts. | + | on_end | None | Called when the parser is finished parsing all data. | :param boundary: The multipart boundary. This is required, and must match what is given in the HTTP request - usually in the @@ -1049,7 +971,7 @@ class MultipartParser(BaseParser): :param max_size: The maximum size of body to parse. Defaults to infinity - i.e. unbounded. - """ + """ # noqa: E501 def __init__( self, boundary: bytes | str, callbacks: MultipartCallbacks = {}, max_size: float = float("inf") From 21ae150173f0c84533745cde131beafff521a2d3 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 9 Mar 2024 15:16:23 +0000 Subject: [PATCH 47/94] Turn docstrings to Google style (#131) --- multipart/multipart.py | 277 ++++++++++++++++++----------------------- 1 file changed, 121 insertions(+), 156 deletions(-) diff --git a/multipart/multipart.py b/multipart/multipart.py index 49aeb23..9c4bafd 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -154,10 +154,7 @@ def join_bytes(b: bytes) -> bytes: def parse_options_header(value: str | bytes) -> tuple[bytes, dict[bytes, bytes]]: - """ - Parses a Content-Type header into a value in the following format: - (content_type, {parameters}) - """ + """Parses a Content-Type header into a value in the following format: (content_type, {parameters}).""" # Uses email.message.Message to parse the header as described in PEP 594. # Ref: https://peps.python.org/pep-0594/#cgi if not value: @@ -182,7 +179,7 @@ def parse_options_header(value: str | bytes) -> tuple[bytes, dict[bytes, bytes]] # If there were no parameters, this would have already returned above assert params, "At least the content type value should be present" ctype = params.pop(0)[0].encode("latin-1") - options = {} + options: dict[bytes, bytes] = {} for param in params: key, value = param # If the value returned from get_params() is a 3-tuple, the last @@ -212,10 +209,11 @@ class Field: will be called when data is written to the Field, and when the Field is finalized, respectively. - :param name: the name of the form field + Args: + name: The name of the form field. """ - def __init__(self, name: bytes): + def __init__(self, name: bytes) -> None: self._name = name self._value: list[bytes] = [] @@ -228,9 +226,12 @@ def from_value(cls, name: bytes, value: bytes | None) -> Field: value - either None or an actual value. This method will also finalize the Field itself. - :param name: the name of the form field - :param value: the value of the form field - either a bytestring or - None + Args: + name: the name of the form field. + value: the value of the form field - either a bytestring or None. + + Returns: + A new instance of a [`Field`][multipart.Field]. """ f = cls(name) @@ -244,7 +245,11 @@ def from_value(cls, name: bytes, value: bytes | None) -> Field: def write(self, data: bytes) -> int: """Write some data into the form field. - :param data: a bytestring + Args: + data: The data to write to the field. + + Returns: + The number of bytes written. """ return self.on_data(data) @@ -252,7 +257,11 @@ def on_data(self, data: bytes) -> int: """This method is a callback that will be called whenever data is written to the Field. - :param data: a bytestring + Args: + data: The data to write to the field. + + Returns: + The number of bytes written. """ self._value.append(data) self._cache = _missing @@ -330,15 +339,12 @@ class File: | UPLOAD_KEEP_EXTENSIONS| `bool`| False | Whether or not to keep the uploaded file's extension. If False, the file will be saved with the default temporary extension (usually ".tmp"). Otherwise, the file's extension will be maintained. Note that this will properly combine with the UPLOAD_KEEP_FILENAME setting. | | MAX_MEMORY_FILE_SIZE | `int` | 1 MiB | The maximum number of bytes of a File to keep in memory. By default, the contents of a File are kept into memory until a certain limit is reached, after which the contents of the File are written to a temporary file. This behavior can be disabled by setting this value to an appropriately large value (or, for example, infinity, such as `float('inf')`. | - :param file_name: The name of the file that this :class:`File` represents - - :param field_name: The field name that uploaded this file. Note that this - can be None, if, for example, the file was uploaded - with Content-Type application/octet-stream - - :param config: The configuration for this File. See above for valid - configuration keys and their corresponding values. - """ # noqa: E501 + Args: + file_name: The name of the file that this [`File`][multipart.File] represents. + field_name: The name of the form field that this file was uploaded with. This can be None, if, for example, + the file was uploaded with Content-Type application/octet-stream. + config: The configuration for this File. See above for valid configuration keys and their corresponding values. + """ # noqa: E501 def __init__(self, file_name: bytes | None, field_name: bytes | None = None, config: FileConfig = {}) -> None: # Save configuration, set other variables default. @@ -506,7 +512,11 @@ def on_data(self, data: bytes) -> int: """This method is a callback that will be called whenever data is written to the File. - :param data: a bytestring + Args: + data: The data to write to the file. + + Returns: + The number of bytes written. """ pos = self._fileobj.tell() bwritten = self._fileobj.write(data) @@ -581,15 +591,12 @@ def callback(self, name: str, data: bytes | None = None, start: int | None = Non """This function calls a provided callback with some data. If the callback is not set, will do nothing. - :param name: The name of the callback to call (as a string). - - :param data: Data to pass to the callback. If None, then it is - assumed that the callback is a notification callback, - and no parameters are given. - - :param end: An integer that is passed to the data callback. - - :param start: An integer that is passed to the data callback. + Args: + name: The name of the callback to call (as a string). + data: Data to pass to the callback. If None, then it is assumed that the callback is a notification + callback, and no parameters are given. + end: An integer that is passed to the data callback. + start: An integer that is passed to the data callback. """ name = "on_" + name func = self.callbacks.get(name) @@ -637,28 +644,15 @@ class OctetStreamParser(BaseParser): """This parser parses an octet-stream request body and calls callbacks when incoming data is received. Callbacks are as follows: - .. list-table:: - :widths: 15 10 30 - :header-rows: 1 - - * - Callback Name - - Parameters - - Description - * - on_start - - None - - Called when the first data is parsed. - * - on_data - - data, start, end - - Called for each data chunk that is parsed. - * - on_end - - None - - Called when the parser is finished parsing all data. - - :param callbacks: A dictionary of callbacks. See the documentation for - :class:`BaseParser`. - - :param max_size: The maximum size of body to parse. Defaults to infinity - - i.e. unbounded. + | Callback Name | Parameters | Description | + |----------------|-----------------|-----------------------------------------------------| + | on_start | None | Called when the first data is parsed. | + | on_data | data, start, end| Called for each data chunk that is parsed. | + | on_end | None | Called when the parser is finished parsing all data.| + + Args: + callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][multipart.BaseParser]. + max_size: The maximum size of body to parse. Defaults to infinity - i.e. unbounded. """ def __init__(self, callbacks: OctetStreamCallbacks = {}, max_size: float = float("inf")): @@ -675,7 +669,11 @@ def write(self, data: bytes) -> int: """Write some data to the parser, which will perform size verification, and then pass the data to the underlying callback. - :param data: a bytestring + Args: + data: The data to write to the parser. + + Returns: + The number of bytes written. """ if not self._started: self.callback("start") @@ -722,24 +720,15 @@ class QuerystringParser(BaseParser): | on_field_end | None | Called when the end of a field is encountered. | | on_end | None | Called when the parser is finished parsing all data.| - :param callbacks: A dictionary of callbacks. See the documentation for - :class:`BaseParser`. - - :param strict_parsing: Whether or not to parse the body strictly. Defaults - to False. If this is set to True, then the behavior - of the parser changes as the following: if a field - has a value with an equal sign (e.g. "foo=bar", or - "foo="), it is always included. If a field has no - equals sign (e.g. "...&name&..."), it will be - treated as an error if 'strict_parsing' is True, - otherwise included. If an error is encountered, - then a - :class:`multipart.exceptions.QuerystringParseError` - will be raised. - - :param max_size: The maximum size of body to parse. Defaults to infinity - - i.e. unbounded. - """ # noqa: E501 + Args: + callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][multipart.BaseParser]. + strict_parsing: Whether or not to parse the body strictly. Defaults to False. If this is set to True, then the + behavior of the parser changes as the following: if a field has a value with an equal sign + (e.g. "foo=bar", or "foo="), it is always included. If a field has no equals sign (e.g. "...&name&..."), + it will be treated as an error if 'strict_parsing' is True, otherwise included. If an error is encountered, + then a [`QuerystringParseError`][multipart.exceptions.QuerystringParseError] will be raised. + max_size: The maximum size of body to parse. Defaults to infinity - i.e. unbounded. + """ # noqa: E501 state: QuerystringState @@ -769,7 +758,11 @@ def write(self, data: bytes) -> int: "offset" attribute of the raised exception will be set to the offset in the input data chunk (NOT the overall stream) that caused the error. - :param data: a bytestring + Args: + data: The data to write to the parser. + + Returns: + The number of bytes written. """ # Handle sizing. data_len = len(data) @@ -962,16 +955,11 @@ class MultipartParser(BaseParser): | on_headers_finished| None | Called when all headers are finished, and before the part data starts. | | on_end | None | Called when the parser is finished parsing all data. | - :param boundary: The multipart boundary. This is required, and must match - what is given in the HTTP request - usually in the - Content-Type header. - - :param callbacks: A dictionary of callbacks. See the documentation for - :class:`BaseParser`. - - :param max_size: The maximum size of body to parse. Defaults to infinity - - i.e. unbounded. - """ # noqa: E501 + Args: + boundary: The multipart boundary. This is required, and must match what is given in the HTTP request - usually in the Content-Type header. + callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][multipart.BaseParser]. + max_size: The maximum size of body to parse. Defaults to infinity - i.e. unbounded. + """ # noqa: E501 def __init__( self, boundary: bytes | str, callbacks: MultipartCallbacks = {}, max_size: float = float("inf") @@ -1022,7 +1010,11 @@ def write(self, data: bytes) -> int: attribute on the raised exception will be set to the offset of the byte in the input chunk that caused the error. - :param data: a bytestring + Args: + data: The data to write to the parser. + + Returns: + The number of bytes written. """ # Handle sizing. data_len = len(data) @@ -1464,51 +1456,31 @@ class FormParser: is parsed, and call the two given callbacks with each field and file as they become available. - :param content_type: The Content-Type of the incoming request. This is - used to select the appropriate parser. - - :param on_field: The callback to call when a field has been parsed and is - ready for usage. See above for parameters. - - :param on_file: The callback to call when a file has been parsed and is - ready for usage. See above for parameters. - - :param on_end: An optional callback to call when all fields and files in a - request has been parsed. Can be None. - - :param boundary: If the request is a multipart/form-data request, this - should be the boundary of the request, as given in the - Content-Type header, as a bytestring. - - :param file_name: If the request is of type application/octet-stream, then - the body of the request will not contain any information - about the uploaded file. In such cases, you can provide - the file name of the uploaded file manually. - - :param FileClass: The class to use for uploaded files. Defaults to - :class:`File`, but you can provide your own class if you - wish to customize behaviour. The class will be - instantiated as FileClass(file_name, field_name), and it - must provide the following functions:: - file_instance.write(data) - file_instance.finalize() - file_instance.close() - - :param FieldClass: The class to use for uploaded fields. Defaults to - :class:`Field`, but you can provide your own class if - you wish to customize behaviour. The class will be - instantiated as FieldClass(field_name), and it must - provide the following functions:: - field_instance.write(data) - field_instance.finalize() - field_instance.close() - field_instance.set_none() - - :param config: Configuration to use for this FormParser. The default - values are taken from the DEFAULT_CONFIG value, and then - any keys present in this dictionary will overwrite the - default values. - + Args: + content_type: The Content-Type of the incoming request. This is used to select the appropriate parser. + on_field: The callback to call when a field has been parsed and is ready for usage. See above for parameters. + on_file: The callback to call when a file has been parsed and is ready for usage. See above for parameters. + on_end: An optional callback to call when all fields and files in a request has been parsed. Can be None. + boundary: If the request is a multipart/form-data request, this should be the boundary of the request, as given + in the Content-Type header, as a bytestring. + file_name: If the request is of type application/octet-stream, then the body of the request will not contain any + information about the uploaded file. In such cases, you can provide the file name of the uploaded file + manually. + FileClass: The class to use for uploaded files. Defaults to :class:`File`, but you can provide your own class + if you wish to customize behaviour. The class will be instantiated as FileClass(file_name, field_name), and + it must provide the following functions:: + - file_instance.write(data) + - file_instance.finalize() + - file_instance.close() + FieldClass: The class to use for uploaded fields. Defaults to :class:`Field`, but you can provide your own + class if you wish to customize behaviour. The class will be instantiated as FieldClass(field_name), and it + must provide the following functions:: + - field_instance.write(data) + - field_instance.finalize() + - field_instance.close() + - field_instance.set_none() + config: Configuration to use for this FormParser. The default values are taken from the DEFAULT_CONFIG value, + and then any keys present in this dictionary will overwrite the default values. """ #: This is the default configuration for our form parser. @@ -1748,11 +1720,15 @@ def _on_end() -> None: self.parser = parser - def write(self, data: bytes): + def write(self, data: bytes) -> int: """Write some data. The parser will forward this to the appropriate underlying parser. - :param data: a bytestring + Args: + data: The data to write. + + Returns: + The number of bytes processed. """ self.bytes_received += len(data) # TODO: check the parser's return value for errors? @@ -1785,18 +1761,13 @@ def create_form_parser( appropriate values and given callbacks, and then return the corresponding parser. - :param headers: A dictionary-like object of HTTP headers. The only - required header is Content-Type. - - :param on_field: Callback to call with each parsed field. - - :param on_file: Callback to call with each parsed file. - - :param trust_x_headers: Whether or not to trust information received from - certain X-Headers - for example, the file name from - X-File-Name. - - :param config: Configuration variables to pass to the FormParser. + Args: + headers: A dictionary-like object of HTTP headers. The only required header is Content-Type. + on_field: Callback to call with each parsed field. + on_file: Callback to call with each parsed file. + trust_x_headers: Whether or not to trust information received from certain X-Headers - for example, the file + name from X-File-Name. + config: Configuration variables to pass to the FormParser. """ content_type = headers.get("Content-Type") if content_type is None: @@ -1833,20 +1804,14 @@ def parse_form( headers, and a file-like object for the input stream, along with two callbacks that will get called whenever a field or file is parsed. - :param headers: A dictionary-like object of HTTP headers. The only - required header is Content-Type. - - :param input_stream: A file-like object that represents the request body. - The read() method must return bytestrings. - - :param on_field: Callback to call with each parsed field. - - :param on_file: Callback to call with each parsed file. - - :param chunk_size: The maximum size to read from the input stream and write - to the parser at one time. Defaults to 1 MiB. + Args: + headers: A dictionary-like object of HTTP headers. The only required header is Content-Type. + input_stream: A file-like object that represents the request body. The read() method must return bytestrings. + on_field: Callback to call with each parsed field. + on_file: Callback to call with each parsed file. + chunk_size: The maximum size to read from the input stream and write to the parser at one time. + Defaults to 1 MiB. """ - # Create our form parser. parser = create_form_parser(headers, on_field, on_file) From 810ac1aebc7555b387e24ae959da1f14c4e87ab2 Mon Sep 17 00:00:00 2001 From: yecril23pl <151100823+yecril23pl@users.noreply.github.com> Date: Sat, 9 Mar 2024 16:18:25 +0100 Subject: [PATCH 48/94] Add hyperlink to documentation (#126) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b5d4ae1..f794630 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Python-Multipart +# [Python-Multipart](https://kludex.github.io/python-multipart/) [![Build Status](https://github.com/Kludex/python-multipart/workflows/CI/badge.svg)](https://github.com/Kludex/python-multipart/actions) [![Package version](https://badge.fury.io/py/python-multipart.svg)](https://pypi.python.org/pypi/python-multipart) From 29829f7285079914ec4e989571923899273c936d Mon Sep 17 00:00:00 2001 From: manunio Date: Fri, 15 Mar 2024 17:30:43 +0530 Subject: [PATCH 49/94] fuzz: initial integration (#117) * fuzz: initial integration * ci: skip atheris for 3.12 --- fuzz/README.md | 37 +++++++++++++ fuzz/corpus/fuzz_decoders/fuzz_decoders | 0 fuzz/corpus/fuzz_form/fuzz_form | 0 .../fuzz_options_header/fuzz_options_header | 0 fuzz/fuzz_decoders.py | 40 ++++++++++++++ fuzz/fuzz_form.py | 53 +++++++++++++++++++ fuzz/fuzz_options_header.py | 26 +++++++++ fuzz/helpers.py | 9 ++++ pyproject.toml | 1 + requirements.txt | 1 + 10 files changed, 167 insertions(+) create mode 100644 fuzz/README.md create mode 100644 fuzz/corpus/fuzz_decoders/fuzz_decoders create mode 100644 fuzz/corpus/fuzz_form/fuzz_form create mode 100644 fuzz/corpus/fuzz_options_header/fuzz_options_header create mode 100644 fuzz/fuzz_decoders.py create mode 100644 fuzz/fuzz_form.py create mode 100644 fuzz/fuzz_options_header.py create mode 100644 fuzz/helpers.py diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 0000000..09c08fe --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,37 @@ +# Fuzz Testing + +Fuzz testing is: + +> An automated software testing technique that involves providing invalid, unexpected, or random data as inputs to a program. + +We use coverage guided fuzz testing to automatically discover bugs in python-multipart. + +This `fuzz/` directory contains the configuration and the fuzz tests for python-multipart. +To generate and run fuzz tests, we use the [Atheris](https://github.com/google/atheris) library. + +## Running a fuzzer + +This directory contains fuzzers like for example `fuzz_form.py`. You can run it with: + +Run fuzz target: +```sh +$ python fuzz/fuzz_form.py +``` + +You should see output that looks something like this: + +``` +#2 INITED cov: 32 ft: 32 corp: 1/1b exec/s: 0 rss: 49Mb +#3 NEW cov: 33 ft: 33 corp: 2/2b lim: 4 exec/s: 0 rss: 49Mb L: 1/1 MS: 1 ChangeByte- +#4 NEW cov: 97 ft: 97 corp: 3/4b lim: 4 exec/s: 0 rss: 49Mb L: 2/2 MS: 1 InsertByte- +#11 NEW cov: 116 ft: 119 corp: 4/5b lim: 4 exec/s: 0 rss: 49Mb L: 1/2 MS: 2 ChangeBinInt-EraseBytes- +#30 NEW cov: 131 ft: 134 corp: 5/8b lim: 4 exec/s: 0 rss: 49Mb L: 3/3 MS: 4 ChangeByte-ChangeBit-InsertByte-CopyPart- +#31 NEW cov: 135 ft: 138 corp: 6/11b lim: 4 exec/s: 0 rss: 49Mb L: 3/3 MS: 1 CrossOver- +#39 NEW cov: 135 ft: 142 corp: 7/15b lim: 4 exec/s: 0 rss: 49Mb L: 4/4 MS: 3 ChangeBit-CrossOver-CopyPart- +``` + +It will continue to generate random inputs forever, until it finds a +bug or is terminated. The testcases for bugs it finds can be seen in +the form of `crash-*` or `timeout-*` at the place from where command is run. +You can rerun the fuzzer on a single input by passing it on the +command line `python fuzz/fuzz_form.py /path/to/testcase`. diff --git a/fuzz/corpus/fuzz_decoders/fuzz_decoders b/fuzz/corpus/fuzz_decoders/fuzz_decoders new file mode 100644 index 0000000..e69de29 diff --git a/fuzz/corpus/fuzz_form/fuzz_form b/fuzz/corpus/fuzz_form/fuzz_form new file mode 100644 index 0000000..e69de29 diff --git a/fuzz/corpus/fuzz_options_header/fuzz_options_header b/fuzz/corpus/fuzz_options_header/fuzz_options_header new file mode 100644 index 0000000..e69de29 diff --git a/fuzz/fuzz_decoders.py b/fuzz/fuzz_decoders.py new file mode 100644 index 0000000..1c4425e --- /dev/null +++ b/fuzz/fuzz_decoders.py @@ -0,0 +1,40 @@ +import io +import sys + +import atheris +from helpers import EnhancedDataProvider + +with atheris.instrument_imports(): + from multipart.decoders import Base64Decoder, DecodeError, QuotedPrintableDecoder + + +def fuzz_base64_decoder(fdp: EnhancedDataProvider) -> None: + decoder = Base64Decoder(io.BytesIO()) + decoder.write(fdp.ConsumeRandomBytes()) + decoder.finalize() + + +def fuzz_quoted_decoder(fdp: EnhancedDataProvider) -> None: + decoder = QuotedPrintableDecoder(io.BytesIO()) + decoder.write(fdp.ConsumeRandomBytes()) + decoder.finalize() + + +def TestOneInput(data: bytes) -> None: + fdp = EnhancedDataProvider(data) + targets = [fuzz_base64_decoder, fuzz_quoted_decoder] + target = fdp.PickValueInList(targets) + + try: + target(fdp) + except DecodeError: + return + + +def main(): + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz() + + +if __name__ == "__main__": + main() diff --git a/fuzz/fuzz_form.py b/fuzz/fuzz_form.py new file mode 100644 index 0000000..0a7646a --- /dev/null +++ b/fuzz/fuzz_form.py @@ -0,0 +1,53 @@ +import io +import sys +from unittest.mock import Mock + +import atheris +from helpers import EnhancedDataProvider + +with atheris.instrument_imports(): + from multipart.exceptions import FormParserError + from multipart.multipart import parse_form + +on_field = Mock() +on_file = Mock() + + +def parse_octet_stream(fdp: EnhancedDataProvider) -> None: + header = {"Content-Type": "application/octet-stream"} + parse_form(header, io.BytesIO(fdp.ConsumeRandomBytes()), on_field, on_file) + + +def parse_url_encoded(fdp: EnhancedDataProvider) -> None: + header = {"Content-Type": "application/x-url-encoded"} + parse_form(header, io.BytesIO(fdp.ConsumeRandomBytes()), on_field, on_file) + + +def parse_form_urlencoded(fdp: EnhancedDataProvider) -> None: + header = {"Content-Type": "application/x-www-form-urlencoded"} + parse_form(header, io.BytesIO(fdp.ConsumeRandomBytes()), on_field, on_file) + + +def parse_multipart_form_data(fdp: EnhancedDataProvider) -> None: + header = {"Content-Type": "multipart/form-data; boundary=--boundary"} + parse_form(header, io.BytesIO(fdp.ConsumeRandomBytes()), on_field, on_file) + + +def TestOneInput(data: bytes) -> None: + fdp = EnhancedDataProvider(data) + targets = [parse_octet_stream, parse_url_encoded, parse_form_urlencoded, parse_multipart_form_data] + target = fdp.PickValueInList(targets) + + try: + target(fdp) + except FormParserError: + return + + +def main(): + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz() + + +if __name__ == "__main__": + main() diff --git a/fuzz/fuzz_options_header.py b/fuzz/fuzz_options_header.py new file mode 100644 index 0000000..dd1cb44 --- /dev/null +++ b/fuzz/fuzz_options_header.py @@ -0,0 +1,26 @@ +import sys + +import atheris +from helpers import EnhancedDataProvider + +with atheris.instrument_imports(): + from multipart.multipart import parse_options_header + + +def TestOneInput(data: bytes) -> None: + fdp = EnhancedDataProvider(data) + try: + parse_options_header(fdp.ConsumeRandomBytes()) + except AssertionError: + return + except TypeError: + return + + +def main(): + atheris.Setup(sys.argv, TestOneInput) + atheris.Fuzz() + + +if __name__ == "__main__": + main() diff --git a/fuzz/helpers.py b/fuzz/helpers.py new file mode 100644 index 0000000..7fcd45c --- /dev/null +++ b/fuzz/helpers.py @@ -0,0 +1,9 @@ +import atheris + + +class EnhancedDataProvider(atheris.FuzzedDataProvider): + def ConsumeRandomBytes(self) -> bytes: + return self.ConsumeBytes(self.ConsumeIntInRange(0, self.remaining_bytes())) + + def ConsumeRandomString(self) -> str: + return self.ConsumeUnicodeNoSurrogates(self.ConsumeIntInRange(0, self.remaining_bytes())) diff --git a/pyproject.toml b/pyproject.toml index 682013a..e6d1406 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ dev = [ "pytest-timeout==2.2.0", "ruff==0.3.0", "hatch", + "atheris==2.3.0; python_version != '3.12'", ] docs = [ "mkdocs==1.5.3", diff --git a/requirements.txt b/requirements.txt index a70070b..2eca21d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ py==1.11.0 pytest==8.0.2 PyYAML==6.0.1 ruff==0.3.0 +atheris==2.3.0 From b88150beb92ac687d81781f97ebfca3cd07f6b58 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Tue, 19 Mar 2024 17:53:16 +0100 Subject: [PATCH 50/94] Use `pyproject.toml` for coverage settings (#132) --- .coveragerc | 34 ---------------------------------- pyproject.toml | 18 ++++++++++++++++++ tox.ini | 2 +- 3 files changed, 19 insertions(+), 35 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 5724e30..0000000 --- a/.coveragerc +++ /dev/null @@ -1,34 +0,0 @@ -# This file contains settings for coverage.py - -[run] -# Change to True some day? -branch = False -# branch = True -omit = - tests/* - -[report] -# Regexes for lines to exclude from consideration -exclude_lines = - # Re-enable the standard pragma - pragma: no cover - - # Ignore defensive assertions - raise NotImplementedError - - # Ignore debugging representations. - def __str__ - def __repr__ - - # Exclude code that can't be executed. - if 0: - if False: - if __name__ == .__main__.: - - # Exclude stuff for debugging - if self\.config\['DEBUG'\]: - if self\.debug: - - # Ignore import exceptions - except ImportError: - diff --git a/pyproject.toml b/pyproject.toml index e6d1406..2c1897f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,3 +84,21 @@ skip-magic-trailing-comma = true [tool.ruff.lint.isort] combine-as-imports = true split-on-trailing-comma = false + +[tool.coverage.run] +branch = false +omit = ["tests/*"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "raise NotImplementedError", + "def __str__", + "def __repr__", + "if 0:", + "if False:", + "if __name__ == .__main__.:", + "if self\\.config\\['DEBUG'\\]:", + "if self\\.debug:", + "except ImportError:", +] diff --git a/tox.ini b/tox.ini index abf6e29..65a9be0 100644 --- a/tox.ini +++ b/tox.ini @@ -8,4 +8,4 @@ deps= pytest-timeout PyYAML commands= - pytest --cov-report term-missing --cov-config .coveragerc --cov multipart --timeout=30 tests + pytest --cov-report term-missing --cov-config pyproject.toml --cov multipart --timeout=30 tests From eb7b1fc3921dbd75fe4435d389e925cbc1458647 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 00:52:36 -0600 Subject: [PATCH 51/94] Bump the github-actions group with 1 update (#139) Bumps the github-actions group with 1 update: [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish). Updates `pypa/gh-action-pypi-publish` from 1.8.12 to 1.8.14 - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.12...v1.8.14) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c521727..1502451 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,7 +34,7 @@ jobs: - name: Build package run: python -m build - name: Publish package - uses: pypa/gh-action-pypi-publish@v1.8.12 + uses: pypa/gh-action-pypi-publish@v1.8.14 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} From b5a5c19902f5ac3c6e69fe665fb2951419735308 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 00:53:07 -0600 Subject: [PATCH 52/94] Bump the python-packages group with 7 updates (#138) Bumps the python-packages group with 7 updates: | Package | From | To | | --- | --- | --- | | [coverage](https://github.com/nedbat/coveragepy) | `7.4.3` | `7.4.4` | | [pytest](https://github.com/pytest-dev/pytest) | `8.0.2` | `8.1.1` | | [pytest-cov](https://github.com/pytest-dev/pytest-cov) | `4.1.0` | `5.0.0` | | [pytest-timeout](https://github.com/pytest-dev/pytest-timeout) | `2.2.0` | `2.3.1` | | [ruff](https://github.com/astral-sh/ruff) | `0.3.0` | `0.3.4` | | [mkdocs-material](https://github.com/squidfunk/mkdocs-material) | `9.5.12` | `9.5.16` | | [mkdocstrings-python](https://github.com/mkdocstrings/python) | `1.8.0` | `1.9.0` | Updates `coverage` from 7.4.3 to 7.4.4 - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/7.4.3...7.4.4) Updates `pytest` from 8.0.2 to 8.1.1 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.0.2...8.1.1) Updates `pytest-cov` from 4.1.0 to 5.0.0 - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.1.0...v5.0.0) Updates `pytest-timeout` from 2.2.0 to 2.3.1 - [Commits](https://github.com/pytest-dev/pytest-timeout/compare/2.2.0...2.3.1) Updates `ruff` from 0.3.0 to 0.3.4 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.3.0...v0.3.4) Updates `mkdocs-material` from 9.5.12 to 9.5.16 - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.12...9.5.16) Updates `mkdocstrings-python` from 1.8.0 to 1.9.0 - [Release notes](https://github.com/mkdocstrings/python/releases) - [Changelog](https://github.com/mkdocstrings/python/blob/main/CHANGELOG.md) - [Commits](https://github.com/mkdocstrings/python/compare/1.8.0...1.9.0) --- updated-dependencies: - dependency-name: coverage dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-packages - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: pytest-cov dependency-type: direct:production update-type: version-update:semver-major dependency-group: python-packages - dependency-name: pytest-timeout dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-packages - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-packages - dependency-name: mkdocstrings-python dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 14 +++++++------- requirements.txt | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2c1897f..8b26206 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,25 +34,25 @@ dependencies = [] dev = [ "atomicwrites==1.4.1", "attrs==23.2.0", - "coverage==7.4.3", + "coverage==7.4.4", "more-itertools==10.2.0", "pbr==6.0.0", "pluggy==1.4.0", "py==1.11.0", - "pytest==8.0.2", - "pytest-cov==4.1.0", + "pytest==8.1.1", + "pytest-cov==5.0.0", "PyYAML==6.0.1", "invoke==2.2.0", - "pytest-timeout==2.2.0", - "ruff==0.3.0", + "pytest-timeout==2.3.1", + "ruff==0.3.4", "hatch", "atheris==2.3.0; python_version != '3.12'", ] docs = [ "mkdocs==1.5.3", - "mkdocs-material==9.5.12", + "mkdocs-material==9.5.16", "mkdocstrings==0.24.1", - "mkdocstrings-python==1.8.0", + "mkdocstrings-python==1.9.0", "mkdocs-autorefs==1.0.1", ] diff --git a/requirements.txt b/requirements.txt index 2eca21d..e768612 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ atomicwrites==1.4.1 attrs==23.2.0 -coverage==7.4.3 +coverage==7.4.4 more-itertools==10.2.0 pbr==6.0.0 pluggy==1.4.0 py==1.11.0 -pytest==8.0.2 +pytest==8.1.1 PyYAML==6.0.1 -ruff==0.3.0 +ruff==0.3.4 atheris==2.3.0 From 3a722ed61ab8c3e094bc8d9e7e74133623060ca5 Mon Sep 17 00:00:00 2001 From: John Stark Date: Thu, 18 Apr 2024 10:00:00 +0100 Subject: [PATCH 53/94] Fix use of chunk_size parameter (#136) --- multipart/multipart.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/multipart/multipart.py b/multipart/multipart.py index 9c4bafd..9a4d9cd 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -1815,7 +1815,7 @@ def parse_form( # Create our form parser. parser = create_form_parser(headers, on_field, on_file) - # Read chunks of 100KiB and write to the parser, but never read more than + # Read chunks of 1MiB and write to the parser, but never read more than # the given Content-Length, if any. content_length = headers.get("Content-Length") if content_length is not None: @@ -1826,7 +1826,7 @@ def parse_form( while True: # Read only up to the Content-Length given. - max_readable = min(content_length - bytes_read, 1048576) + max_readable = min(content_length - bytes_read, chunk_size) buff = input_stream.read(max_readable) # Write to the parser and update our length. From 3ea51c714ea8ddb66acd766964414fe50ef0eecf Mon Sep 17 00:00:00 2001 From: John Stark Date: Thu, 18 Apr 2024 19:53:22 +0100 Subject: [PATCH 54/94] Allow digits and valid token chars in headers (#134) --- multipart/multipart.py | 37 ++++++++------------ tests/test_data/http/bad_header_char.http | 2 +- tests/test_data/http/header_with_number.http | 11 ++++++ tests/test_data/http/header_with_number.yaml | 7 ++++ 4 files changed, 34 insertions(+), 23 deletions(-) create mode 100644 tests/test_data/http/header_with_number.http create mode 100644 tests/test_data/http/header_with_number.yaml diff --git a/multipart/multipart.py b/multipart/multipart.py index 9a4d9cd..0505c1b 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -136,13 +136,14 @@ class MultipartState(IntEnum): LOWER_Z = b"z"[0] NULL = b"\x00"[0] - -# Lower-casing a character is different, because of the difference between -# str on Py2, and bytes on Py3. Same with getting the ordinal value of a byte, -# and joining a list of bytes together. -# These functions abstract that. -def lower_char(c: int) -> int: - return c | 0x20 +# Mask for ASCII characters that can be http tokens. +# Per RFC7230 - 3.2.6, this is all alpha-numeric characters +# and these: !#$%&'*+-.^_`|~ +TOKEN_CHARS_SET = frozenset( + b"ABCDEFGHIJKLMNOPQRSTUVWXYZ" + b"abcdefghijklmnopqrstuvwxyz" + b"0123456789" + b"!#$%&'*+-.^_`|~") def ord_char(c: int) -> int: @@ -1175,12 +1176,8 @@ def data_callback(name: str, remaining: bool = False) -> None: # Increment our index in the header. index += 1 - # Do nothing if we encounter a hyphen. - if c == HYPHEN: - pass - # If we've reached a colon, we're done with this header. - elif c == COLON: + if c == COLON: # A 0-length header is an error. if index == 1: msg = "Found 0-length header at %d" % (i,) @@ -1195,16 +1192,12 @@ def data_callback(name: str, remaining: bool = False) -> None: # Move to parsing the header value. state = MultipartState.HEADER_VALUE_START - else: - # Lower-case this character, and ensure that it is in fact - # a valid letter. If not, it's an error. - cl = lower_char(c) - if cl < LOWER_A or cl > LOWER_Z: - msg = "Found non-alphanumeric character %r in " "header at %d" % (c, i) - self.logger.warning(msg) - e = MultipartParseError(msg) - e.offset = i - raise e + elif c not in TOKEN_CHARS_SET: + msg = "Found invalid character %r in header at %d" % (c, i) + self.logger.warning(msg) + e = MultipartParseError(msg) + e.offset = i + raise e elif state == MultipartState.HEADER_VALUE_START: # Skip leading spaces. diff --git a/tests/test_data/http/bad_header_char.http b/tests/test_data/http/bad_header_char.http index c0a3b21..1d9fd4d 100644 --- a/tests/test_data/http/bad_header_char.http +++ b/tests/test_data/http/bad_header_char.http @@ -1,5 +1,5 @@ ------WebKitFormBoundaryTkr3kCBQlBe1nrhc -Content-999position: form-data; name="field" +Content-<< Date: Thu, 18 Apr 2024 19:55:54 +0100 Subject: [PATCH 55/94] Fix headers being carried between parts. fixes #63 (#135) The contents of the headers dict wasn't cleared when a new part started, If the second part didn't overwrite the header value, it would appear in the wrong part. --- multipart/multipart.py | 4 +++- .../http/mixed_plain_and_base64_encoding.http | 23 +++++++++++++++++++ .../http/mixed_plain_and_base64_encoding.yaml | 18 +++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/test_data/http/mixed_plain_and_base64_encoding.http create mode 100644 tests/test_data/http/mixed_plain_and_base64_encoding.yaml diff --git a/multipart/multipart.py b/multipart/multipart.py index 0505c1b..1ad5011 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -1613,7 +1613,9 @@ def _on_end() -> None: is_file = False def on_part_begin(): - pass + # Reset headers in case this isn't the first part. + nonlocal headers + headers = {} def on_part_data(data: bytes, start: int, end: int) -> None: nonlocal writer diff --git a/tests/test_data/http/mixed_plain_and_base64_encoding.http b/tests/test_data/http/mixed_plain_and_base64_encoding.http new file mode 100644 index 0000000..9de1a3b --- /dev/null +++ b/tests/test_data/http/mixed_plain_and_base64_encoding.http @@ -0,0 +1,23 @@ +----boundary +Content-Type: text/plain; charset="UTF-8" +Content-Disposition: form-data; name=field1 +Content-Transfer-Encoding: base64 + +VGVzdCAxMjM= +----boundary +Content-Type: text/plain; charset="UTF-8" +Content-Disposition: form-data; name=field2 + +This is a test. +----boundary +Content-Type: text/plain; charset="UTF-8" +Content-Disposition: form-data; name=field3 +Content-Transfer-Encoding: base64 + +VGVzdCBzdHJpbmcuCg== +----boundary +Content-Type: text/plain; charset="UTF-8" +Content-Disposition: form-data; name=field4 + +This is also a test. +----boundary-- \ No newline at end of file diff --git a/tests/test_data/http/mixed_plain_and_base64_encoding.yaml b/tests/test_data/http/mixed_plain_and_base64_encoding.yaml new file mode 100644 index 0000000..31bfe77 --- /dev/null +++ b/tests/test_data/http/mixed_plain_and_base64_encoding.yaml @@ -0,0 +1,18 @@ +boundary: --boundary +expected: + - name: field1 + type: field + data: !!binary | + VGVzdCAxMjM= + - name: field2 + type: field + data: !!binary | + VGhpcyBpcyBhIHRlc3Qu + - name: field3 + type: field + data: !!binary | + VGVzdCBzdHJpbmcuCg== + - name: field4 + type: field + data: !!binary | + VGhpcyBpcyBhbHNvIGEgdGVzdC4= From c664cef3bb67a5c0cae0194c68afa11d5be1ffbd Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 21 Sep 2024 09:17:44 +0200 Subject: [PATCH 56/94] Use uv (#153) * Use uv * Name CI instead of Test * Use master instead of main * Remove 100% coverage condition --- .github/workflows/main.yml | 46 +- .github/workflows/publish.yml | 4 + README.md | 1 - multipart/multipart.py | 22 +- pyproject.toml | 13 +- requirements.txt | 11 - scripts/lint | 6 + scripts/test | 6 + tasks.py | 84 --- tox.ini | 11 - uv.lock | 1005 +++++++++++++++++++++++++++++++++ 11 files changed, 1065 insertions(+), 144 deletions(-) delete mode 100644 requirements.txt create mode 100755 scripts/lint create mode 100755 scripts/test delete mode 100644 tasks.py delete mode 100644 tox.ini create mode 100644 uv.lock diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 072ae70..c7b7129 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,30 +6,40 @@ on: pull_request: branches: ["master"] - jobs: - build: - + test: runs-on: ubuntu-latest strategy: - fail-fast: false matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] - steps: - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + + - name: Install uv + uses: astral-sh/setup-uv@v2 with: - python-version: ${{ matrix.python-version }} + version: "0.4.12" + enable-cache: true + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install .[dev] - - name: Lint - if: matrix.python-version == '3.8' - run: | - ruff multipart tests - - name: Test with pytest - run: | - inv test + run: uv sync --python ${{ matrix.python-version }} --frozen + + - name: Run tests + run: scripts/test + + - name: Run linters + run: scripts/lint + + # https://github.com/marketplace/actions/alls-green#why used for branch protection checks + check: + if: always() + needs: [test] + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1502451..82b3ef5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,17 +22,21 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Set up Python 3.10 uses: actions/setup-python@v5 with: python-version: "3.10" + - name: Install dependencies run: | python -m pip install --upgrade pip pip install build pip install -e '.[docs]' + - name: Build package run: python -m build + - name: Publish package uses: pypa/gh-action-pypi-publish@v1.8.14 with: diff --git a/README.md b/README.md index f794630..5a2aba8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # [Python-Multipart](https://kludex.github.io/python-multipart/) -[![Build Status](https://github.com/Kludex/python-multipart/workflows/CI/badge.svg)](https://github.com/Kludex/python-multipart/actions) [![Package version](https://badge.fury.io/py/python-multipart.svg)](https://pypi.python.org/pypi/python-multipart) [![Supported Python Version](https://img.shields.io/pypi/pyversions/python-multipart.svg?color=%2334D058)](https://pypi.org/project/python-multipart) diff --git a/multipart/multipart.py b/multipart/multipart.py index 1ad5011..0d0ee8c 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -57,25 +57,19 @@ class FileConfig(TypedDict, total=False): MAX_MEMORY_FILE_SIZE: int class _FormProtocol(Protocol): - def write(self, data: bytes) -> int: - ... + def write(self, data: bytes) -> int: ... - def finalize(self) -> None: - ... + def finalize(self) -> None: ... - def close(self) -> None: - ... + def close(self) -> None: ... class FieldProtocol(_FormProtocol, Protocol): - def __init__(self, name: bytes) -> None: - ... + def __init__(self, name: bytes) -> None: ... - def set_none(self) -> None: - ... + def set_none(self) -> None: ... class FileProtocol(_FormProtocol, Protocol): - def __init__(self, file_name: bytes | None, field_name: bytes | None, config: FileConfig) -> None: - ... + def __init__(self, file_name: bytes | None, field_name: bytes | None, config: FileConfig) -> None: ... OnFieldCallback = Callable[[FieldProtocol], None] OnFileCallback = Callable[[FileProtocol], None] @@ -136,14 +130,16 @@ class MultipartState(IntEnum): LOWER_Z = b"z"[0] NULL = b"\x00"[0] +# fmt: off # Mask for ASCII characters that can be http tokens. -# Per RFC7230 - 3.2.6, this is all alpha-numeric characters +# Per RFC7230 - 3.2.6, this is all alpha-numeric characters # and these: !#$%&'*+-.^_`|~ TOKEN_CHARS_SET = frozenset( b"ABCDEFGHIJKLMNOPQRSTUVWXYZ" b"abcdefghijklmnopqrstuvwxyz" b"0123456789" b"!#$%&'*+-.^_`|~") +# fmt: on def ord_char(c: int) -> int: diff --git a/pyproject.toml b/pyproject.toml index 8b26206..ba2811b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,8 @@ classifiers = [ ] dependencies = [] -[project.optional-dependencies] -dev = [ +[tool.uv] +dev-dependencies = [ "atomicwrites==1.4.1", "attrs==23.2.0", "coverage==7.4.4", @@ -45,10 +45,8 @@ dev = [ "invoke==2.2.0", "pytest-timeout==2.3.1", "ruff==0.3.4", - "hatch", "atheris==2.3.0; python_version != '3.12'", -] -docs = [ + # Documentation "mkdocs==1.5.3", "mkdocs-material==9.5.16", "mkdocstrings==0.24.1", @@ -69,7 +67,7 @@ path = "multipart/__init__.py" packages = ["multipart"] [tool.hatch.build.targets.sdist] -include = ["/multipart", "/tests"] +include = ["/multipart", "/tests", "CHANGELOG.md", "LICENSE.txt"] [tool.ruff] line-length = 120 @@ -90,6 +88,9 @@ branch = false omit = ["tests/*"] [tool.coverage.report] +# fail_under = 100 +skip_covered = true +show_missing = true exclude_lines = [ "pragma: no cover", "raise NotImplementedError", diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e768612..0000000 --- a/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -atomicwrites==1.4.1 -attrs==23.2.0 -coverage==7.4.4 -more-itertools==10.2.0 -pbr==6.0.0 -pluggy==1.4.0 -py==1.11.0 -pytest==8.1.1 -PyYAML==6.0.1 -ruff==0.3.4 -atheris==2.3.0 diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 0000000..3685246 --- /dev/null +++ b/scripts/lint @@ -0,0 +1,6 @@ +#!/bin/sh -e + +set -x + +uvx ruff check --fix +uvx ruff format diff --git a/scripts/test b/scripts/test new file mode 100755 index 0000000..141731f --- /dev/null +++ b/scripts/test @@ -0,0 +1,6 @@ +#!/bin/sh -e + +set -x # print executed commands to the terminal + +uv run coverage run -m pytest "${@}" +uv run coverage report diff --git a/tasks.py b/tasks.py deleted file mode 100644 index dba9e2a..0000000 --- a/tasks.py +++ /dev/null @@ -1,84 +0,0 @@ -import os -import re -import sys - -from invoke import task, run - - -version_file = os.path.join('multipart', '__init__.py') -version_regex = re.compile(r'((?:\d+)\.(?:\d+)\.(?:\d+))') - -# Get around Python 2.X's lack of 'nonlocal' keyword -class g: - test_success = False - - -@task -def test(ctx, all=False): - test_cmd = [ - 'pytest', # Test command - '--cov-report term-missing', # Print only uncovered lines to stdout - '--cov-config .coveragerc', # Use this file for configuration - '--cov multipart', # Test only this module - '--timeout=30' # Each test should timeout after 30 sec - ] - - # Test in this directory - test_cmd.append("tests") - - # Run the command. - # TODO: why does this fail with pty=True? - res = run(' '.join(test_cmd), pty=False) - g.test_success = res.ok - - -@task -def bump(ctx, type): - # Read and parse version. - with open(version_file, 'r') as f: - file_data = f.read().replace('\r\n', '\n') - - m = version_regex.search(file_data) - if m is None: - print(f"Could not find version in '{version_file}'!", file=sys.stderr) - return - - version = m.group(0) - before = file_data[0:m.start(0)] - after = file_data[m.end(0):] - - # Bump properly. - ver_nums = [int(x) for x in version.split('.')] - - if type == 'patch': - ver_nums[2] += 1 - elif type == 'minor': - ver_nums[1] += 1 - elif type == 'major': - ver_nums[0] += 1 - else: - print(f"Invalid version type: '{type}'", file=sys.stderr) - return - - # Construct new data and write to file. - new_ver = ".".join(str(x) for x in ver_nums) - new_data = before + new_ver + after - - with open(version_file, 'w') as f: - f.write(new_data) - - # Print information. - print(f"Bumped version from: {version} --> {new_ver}") - - -@task(pre=[test]) -def deploy(ctx): - if not g.test_success: - print("Tests must pass before deploying!", file=sys.stderr) - return - - # # Build source distribution and wheel - run('hatch build') - # - # # Upload distributions from last step to pypi - run('hatch publish') diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 65a9be0..0000000 --- a/tox.ini +++ /dev/null @@ -1,11 +0,0 @@ -[tox] -envlist = py38,py39,py310,py311,py312 - -[testenv] -deps= - pytest - pytest-cov - pytest-timeout - PyYAML -commands= - pytest --cov-report term-missing --cov-config pyproject.toml --cov multipart --timeout=30 tests diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..6fd204d --- /dev/null +++ b/uv.lock @@ -0,0 +1,1005 @@ +version = 1 +requires-python = ">=3.8" + +[[package]] +name = "astunparse" +version = "1.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732 }, +] + +[[package]] +name = "atheris" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/80/65938e910e1b61ecf2a66339f2e1860b84d1d0f0e604a0b08910d00707a5/atheris-2.3.0.tar.gz", hash = "sha256:cf1fdf5fa220a41a2f262b32363fc566549502b2cb0addf4e1baad5531c0e825", size = 304139 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/db/d141a3ac3974d267946b49abaeedea999725eca44a34534b8c5450356e8f/atheris-2.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91cb296d60915c3efa4f6db48f09c4678b574cddb7ca98035f1cb9d9fb96f64", size = 30877874 }, + { url = "https://files.pythonhosted.org/packages/00/01/3a2a31c0016233599b12e8fa6c956ec6c1df78630d8cec9cfa78904d0013/atheris-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4e43d1ee4760916a84ff73c9c6cf9ac6eee80fc030479bbed43fe0b8e994981", size = 31182450 }, + { url = "https://files.pythonhosted.org/packages/38/35/00c7d7da1ee1789d0fbed6e4109c3b7d08eac1ac85c7ad73c9920d99a00a/atheris-2.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9877b6c2bd5386f9fbbb2989a939c717383d9639f5476411c06fe50fe2fe09a6", size = 30875022 }, + { url = "https://files.pythonhosted.org/packages/00/1e/0529d9dff0b3c8a69276df192cfb24cc35e5ef3304e9301ed5cf148682c6/atheris-2.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:005bde4b5a70e998b7fa097e9aa195972dcc2e04092156a0149cff7aa0de970e", size = 30877792 }, +] + +[[package]] +name = "atomicwrites" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/c6/53da25344e3e3a9c01095a89f16dbcda021c609ddb42dd6d7c0528236fb2/atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11", size = 14227 } + +[[package]] +name = "attrs" +version = "23.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/fc/f800d51204003fa8ae392c4e8278f256206e7a919b708eef054f5f4b650d/attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", size = 780820 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/44/827b2a91a5816512fcaf3cc4ebc465ccd5d598c45cefa6703fcf4a79018f/attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1", size = 60752 }, +] + +[[package]] +name = "babel" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/61/095a0aa1a84d1481998b534177c8566fdc50bb1233ea9a0478cd3cc075bd/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", size = 194219 }, + { url = "https://files.pythonhosted.org/packages/cc/94/f7cf5e5134175de79ad2059edf2adce18e0685ebdb9227ff0139975d0e93/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", size = 122521 }, + { url = "https://files.pythonhosted.org/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", size = 120383 }, + { url = "https://files.pythonhosted.org/packages/b8/60/e2f67915a51be59d4539ed189eb0a2b0d292bf79270410746becb32bc2c3/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", size = 138223 }, + { url = "https://files.pythonhosted.org/packages/05/8c/eb854996d5fef5e4f33ad56927ad053d04dc820e4a3d39023f35cad72617/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", size = 148101 }, + { url = "https://files.pythonhosted.org/packages/f6/93/bb6cbeec3bf9da9b2eba458c15966658d1daa8b982c642f81c93ad9b40e1/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", size = 140699 }, + { url = "https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", size = 142065 }, + { url = "https://files.pythonhosted.org/packages/3f/ba/3f5e7be00b215fa10e13d64b1f6237eb6ebea66676a41b2bcdd09fe74323/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", size = 144505 }, + { url = "https://files.pythonhosted.org/packages/33/c3/3b96a435c5109dd5b6adc8a59ba1d678b302a97938f032e3770cc84cd354/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", size = 139425 }, + { url = "https://files.pythonhosted.org/packages/43/05/3bf613e719efe68fb3a77f9c536a389f35b95d75424b96b426a47a45ef1d/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", size = 145287 }, + { url = "https://files.pythonhosted.org/packages/58/78/a0bc646900994df12e07b4ae5c713f2b3e5998f58b9d3720cce2aa45652f/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", size = 149929 }, + { url = "https://files.pythonhosted.org/packages/eb/5c/97d97248af4920bc68687d9c3b3c0f47c910e21a8ff80af4565a576bd2f0/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", size = 141605 }, + { url = "https://files.pythonhosted.org/packages/a8/31/47d018ef89f95b8aded95c589a77c072c55e94b50a41aa99c0a2008a45a4/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", size = 142646 }, + { url = "https://files.pythonhosted.org/packages/ae/d5/4fecf1d58bedb1340a50f165ba1c7ddc0400252d6832ff619c4568b36cc0/charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", size = 92846 }, + { url = "https://files.pythonhosted.org/packages/a2/a0/4af29e22cb5942488cf45630cbdd7cefd908768e69bdd90280842e4e8529/charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", size = 100343 }, + { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647 }, + { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434 }, + { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979 }, + { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582 }, + { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645 }, + { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398 }, + { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273 }, + { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577 }, + { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747 }, + { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375 }, + { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232 }, + { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859 }, + { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509 }, + { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870 }, + { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892 }, + { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213 }, + { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404 }, + { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275 }, + { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518 }, + { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182 }, + { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869 }, + { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042 }, + { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275 }, + { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819 }, + { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415 }, + { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212 }, + { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167 }, + { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041 }, + { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397 }, + { url = "https://files.pythonhosted.org/packages/ef/d4/a1d72a8f6aa754fdebe91b848912025d30ab7dced61e9ed8aabbf791ed65/charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", size = 191415 }, + { url = "https://files.pythonhosted.org/packages/13/82/83c188028b6f38d39538442dd127dc794c602ae6d45d66c469f4063a4c30/charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", size = 121051 }, + { url = "https://files.pythonhosted.org/packages/16/ea/a9e284aa38cccea06b7056d4cbc7adf37670b1f8a668a312864abf1ff7c6/charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", size = 119143 }, + { url = "https://files.pythonhosted.org/packages/34/2a/f392457d45e24a0c9bfc012887ed4f3c54bf5d4d05a5deb970ffec4b7fc0/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", size = 137506 }, + { url = "https://files.pythonhosted.org/packages/be/4d/9e370f8281cec2fcc9452c4d1ac513324c32957c5f70c73dd2fa8442a21a/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", size = 147272 }, + { url = "https://files.pythonhosted.org/packages/33/95/ef68482e4a6adf781fae8d183fb48d6f2be8facb414f49c90ba6a5149cd1/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", size = 139734 }, + { url = "https://files.pythonhosted.org/packages/3d/09/d82fe4a34c5f0585f9ea1df090e2a71eb9bb1e469723053e1ee9f57c16f3/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", size = 141094 }, + { url = "https://files.pythonhosted.org/packages/81/b2/160893421adfa3c45554fb418e321ed342bb10c0a4549e855b2b2a3699cb/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", size = 144113 }, + { url = "https://files.pythonhosted.org/packages/9e/ef/cd47a63d3200b232792e361cd67530173a09eb011813478b1c0fb8aa7226/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", size = 138555 }, + { url = "https://files.pythonhosted.org/packages/a8/6f/4ff299b97da2ed6358154b6eb3a2db67da2ae204e53d205aacb18a7e4f34/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", size = 144944 }, + { url = "https://files.pythonhosted.org/packages/d1/2f/0d1efd07c74c52b6886c32a3b906fb8afd2fecf448650e73ecb90a5a27f1/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", size = 148925 }, + { url = "https://files.pythonhosted.org/packages/bd/28/7ea29e73eea52c7e15b4b9108d0743fc9e4cc2cdb00d275af1df3d46d360/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", size = 140732 }, + { url = "https://files.pythonhosted.org/packages/b3/c1/ebca8e87c714a6a561cfee063f0655f742e54b8ae6e78151f60ba8708b3a/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", size = 141288 }, + { url = "https://files.pythonhosted.org/packages/74/20/8923a06f15eb3d7f6a306729360bd58f9ead1dc39bc7ea8831f4b407e4ae/charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", size = 92373 }, + { url = "https://files.pythonhosted.org/packages/db/fb/d29e343e7c57bbf1231275939f6e75eb740cd47a9d7cb2c52ffeb62ef869/charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", size = 99577 }, + { url = "https://files.pythonhosted.org/packages/f7/9d/bcf4a449a438ed6f19790eee543a86a740c77508fbc5ddab210ab3ba3a9a/charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", size = 194198 }, + { url = "https://files.pythonhosted.org/packages/66/fe/c7d3da40a66a6bf2920cce0f436fa1f62ee28aaf92f412f0bf3b84c8ad6c/charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", size = 122494 }, + { url = "https://files.pythonhosted.org/packages/2a/9d/a6d15bd1e3e2914af5955c8eb15f4071997e7078419328fee93dfd497eb7/charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", size = 120393 }, + { url = "https://files.pythonhosted.org/packages/3d/85/5b7416b349609d20611a64718bed383b9251b5a601044550f0c8983b8900/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", size = 138331 }, + { url = "https://files.pythonhosted.org/packages/79/66/8946baa705c588521afe10b2d7967300e49380ded089a62d38537264aece/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", size = 148097 }, + { url = "https://files.pythonhosted.org/packages/44/80/b339237b4ce635b4af1c73742459eee5f97201bd92b2371c53e11958392e/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", size = 140711 }, + { url = "https://files.pythonhosted.org/packages/98/69/5d8751b4b670d623aa7a47bef061d69c279e9f922f6705147983aa76c3ce/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", size = 142251 }, + { url = "https://files.pythonhosted.org/packages/1f/8d/33c860a7032da5b93382cbe2873261f81467e7b37f4ed91e25fed62fd49b/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", size = 144636 }, + { url = "https://files.pythonhosted.org/packages/c2/65/52aaf47b3dd616c11a19b1052ce7fa6321250a7a0b975f48d8c366733b9f/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", size = 139514 }, + { url = "https://files.pythonhosted.org/packages/51/fd/0ee5b1c2860bb3c60236d05b6e4ac240cf702b67471138571dad91bcfed8/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", size = 145528 }, + { url = "https://files.pythonhosted.org/packages/e1/9c/60729bf15dc82e3aaf5f71e81686e42e50715a1399770bcde1a9e43d09db/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", size = 149804 }, + { url = "https://files.pythonhosted.org/packages/53/cd/aa4b8a4d82eeceb872f83237b2d27e43e637cac9ffaef19a1321c3bafb67/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", size = 141708 }, + { url = "https://files.pythonhosted.org/packages/54/7f/cad0b328759630814fcf9d804bfabaf47776816ad4ef2e9938b7e1123d04/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561", size = 142708 }, + { url = "https://files.pythonhosted.org/packages/c1/9d/254a2f1bcb0ce9acad838e94ed05ba71a7cb1e27affaa4d9e1ca3958cdb6/charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", size = 92830 }, + { url = "https://files.pythonhosted.org/packages/2f/0e/d7303ccae9735ff8ff01e36705ad6233ad2002962e8668a970fc000c5e1b/charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", size = 100376 }, + { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/d5/f809d8b630cf4c11fe490e20037a343d12a74ec2783c6cdb5aee725e7137/coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49", size = 783727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/f4/10bf725621aeec5cc2fa1bc73021f5ba1ac01bcbf2c7278d8d34e1df6457/coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2", size = 206123 }, + { url = "https://files.pythonhosted.org/packages/10/1e/f676e1655d10bf59a6cb8de0601b7ea3c252c764782a3c2263f6d6bbcf28/coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf", size = 206473 }, + { url = "https://files.pythonhosted.org/packages/07/58/0e076ea3a59dbfb3e981577c4e5572b432345cedd921e83006a0215b9afe/coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8", size = 234388 }, + { url = "https://files.pythonhosted.org/packages/d3/6d/72b9f5035c50a14bc5c5fda0c28ac16c426e957a7a3debe02906b614fc4f/coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562", size = 232667 }, + { url = "https://files.pythonhosted.org/packages/93/41/e6e9dbb322f3c93aba7bc519b9c62846d923d7b57398bdd7eda3f0acdd11/coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2", size = 233463 }, + { url = "https://files.pythonhosted.org/packages/91/4e/feff6d115dcc239e5850570ca2ea27a243c8a69596e7f1dabe54a6102d89/coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7", size = 239529 }, + { url = "https://files.pythonhosted.org/packages/50/32/829d0e709fa699dc4e498fa77a561d25fc57954ba32466279952b98f0836/coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87", size = 237720 }, + { url = "https://files.pythonhosted.org/packages/7e/60/62a8c190d20bf605c89a000fd6d41e3563b5792e7275b12eeefe6803b473/coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c", size = 238910 }, + { url = "https://files.pythonhosted.org/packages/5a/52/3641a452e1afa686094910287a8d8a472aafa09f833b2716c2c11c5cabb4/coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d", size = 208336 }, + { url = "https://files.pythonhosted.org/packages/0e/de/7ff914b162e60d66a809632d037f32af74b0df41dc9f0532b1cd7db360bb/coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f", size = 209180 }, + { url = "https://files.pythonhosted.org/packages/c4/26/e9bd37635e0e0343f41394e715725982de8811a1229ace1b3e94c9e47b86/coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf", size = 206305 }, + { url = "https://files.pythonhosted.org/packages/ec/1b/0c493f14813e9518ae71b8bd3061af63a332b41e6fee983996a7b90deb07/coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083", size = 206574 }, + { url = "https://files.pythonhosted.org/packages/64/9b/d0a8c02209f17549ce2283829b7be2b4eaef8bc7c7e0d8016774e73d54c0/coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63", size = 238036 }, + { url = "https://files.pythonhosted.org/packages/0f/86/d5d971283ef625391595d79321d3f9bef09dcaa0537db665fb0d4f445c7d/coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f", size = 235610 }, + { url = "https://files.pythonhosted.org/packages/ab/1c/f8fefae78482f1998f7a9d68419b22089b5ce69a7e0fa0035827d2ce2206/coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227", size = 237314 }, + { url = "https://files.pythonhosted.org/packages/5e/7c/d700521aafd6a23a61b5eb60db2f42a2306e494b3097030fcf400ce768a3/coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd", size = 246411 }, + { url = "https://files.pythonhosted.org/packages/95/44/c3f2e14450239fcdaff38e66a165f4aa8ac3a0753d1db33321c692558a15/coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384", size = 244786 }, + { url = "https://files.pythonhosted.org/packages/f4/ce/98e90709f9879d5834d04b49b86736118a78d848a9162333aa659c6442a7/coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b", size = 245869 }, + { url = "https://files.pythonhosted.org/packages/a8/79/9dceb3847177d3bed1df3dd25a7672cc634369bc3cb6d2eed57ed6366a86/coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286", size = 208337 }, + { url = "https://files.pythonhosted.org/packages/d0/b2/994e08535fcc094df65c00440d71a05133cc8dc0c371eecf84bbb58154f0/coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec", size = 209273 }, + { url = "https://files.pythonhosted.org/packages/a0/de/a54b245e781bfd6f3fd7ce5566a695686b5c25ee7c743f514e7634428972/coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76", size = 206409 }, + { url = "https://files.pythonhosted.org/packages/88/92/07f9c593cd27e3c595b8cb83b95adad8c9ba3d611debceed097a5fd6be4b/coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818", size = 206568 }, + { url = "https://files.pythonhosted.org/packages/41/6d/e142c823e5d4b24481f990da4cf9d2d577a6f4e1fb6faf39d9a4e42b1d43/coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978", size = 238920 }, + { url = "https://files.pythonhosted.org/packages/30/1a/105f0139df6a2adbcaa0c110711a46dbd9f59e93a09ca15a97d59c2564f2/coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70", size = 236288 }, + { url = "https://files.pythonhosted.org/packages/98/79/185cb42910b6a2b2851980407c8445ac0da0750dff65e420e86f973c8396/coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51", size = 238223 }, + { url = "https://files.pythonhosted.org/packages/92/12/2303d1c543a11ea060dbc7144ed3174fc09107b5dd333649415c95ede58b/coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c", size = 245161 }, + { url = "https://files.pythonhosted.org/packages/96/5a/7d0e945c4759fe9d19aad1679dd3096aeb4cb9fcf0062fe24554dc4787b8/coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48", size = 243066 }, + { url = "https://files.pythonhosted.org/packages/f4/1b/79cdb7b11bbbd6540a536ac79412904b5c1f8903d5c1330084212afa8ceb/coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9", size = 244805 }, + { url = "https://files.pythonhosted.org/packages/af/7f/54dc676e7e63549838a3a7b95a8e11df80441bf7d64c6ce8f1cdbc0d1ff0/coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0", size = 208590 }, + { url = "https://files.pythonhosted.org/packages/46/c4/1dfe76d96034a347d717a2392b004d42d45934cb94efa362ad41ca871f6e/coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e", size = 209415 }, + { url = "https://files.pythonhosted.org/packages/6f/ab/95a048c3acda69c9e4a40b3ae57f06c45b30c5d9401e6dc7246e9de83306/coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384", size = 206089 }, + { url = "https://files.pythonhosted.org/packages/23/7c/9863790fb889101c35018ecb9e241cb4f900a77ef100491bb043bfa5976c/coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1", size = 206440 }, + { url = "https://files.pythonhosted.org/packages/32/d4/60b1071c35bd3828590483ae0f8531f07b77d737e2c81dc51887c03bf890/coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a", size = 235515 }, + { url = "https://files.pythonhosted.org/packages/0a/4f/0e04c34df68716b90bedf8b791c684d6a54cab92fbc9ca2c236a8ca268e6/coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409", size = 233354 }, + { url = "https://files.pythonhosted.org/packages/ad/6a/7eebb71ebdf5e56b6da69e5ca8f05b743e054ce9d4dfd440dbcb3f9be0f0/coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e", size = 234600 }, + { url = "https://files.pythonhosted.org/packages/dc/8e/6df9cfab2eb2c5d8e634a18ade3451b587fd75a434366982bdcbefc125e6/coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd", size = 240471 }, + { url = "https://files.pythonhosted.org/packages/af/9c/bd573c65cf554b9979241c575916897e27107a70205b2fbe71218eaa24c4/coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7", size = 238927 }, + { url = "https://files.pythonhosted.org/packages/60/6b/7ac6da198b2c22fc6ba53e479cc800ec230bc7a40c14ed62358d7f1c809f/coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c", size = 239814 }, + { url = "https://files.pythonhosted.org/packages/f9/43/9d3ed7750d2f8a9f7d0d4682fe87ed07080c44f56a8a16a5d4d87c81c278/coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e", size = 208314 }, + { url = "https://files.pythonhosted.org/packages/7d/8f/2665744d223dcea532b1cf3a9edd236632f54fd3925b9b464f1d03b4421e/coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8", size = 209170 }, + { url = "https://files.pythonhosted.org/packages/1a/15/ae47f23bfd557364e731ad2ed182331ba72e8c063b806ba317cd327e73cc/coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d", size = 206118 }, + { url = "https://files.pythonhosted.org/packages/64/09/91be1d04914deea7dd0e2f3e94d925c23e9b81ce23b0da014f1ff07dd772/coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357", size = 206473 }, + { url = "https://files.pythonhosted.org/packages/8b/c7/54cde44ebed02848db20d67388d0f82db1b65eca09d48181df71fbd81cf5/coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e", size = 233988 }, + { url = "https://files.pythonhosted.org/packages/78/ab/39feda43fbd0ca46f695b36bfe1f6836efce9657e81889bb0dcc55fb1745/coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e", size = 232280 }, + { url = "https://files.pythonhosted.org/packages/5b/ec/9bd500128995e9eec2ab50361ce8b853bab2b4839316ddcfd6a34f5bbfed/coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4", size = 233062 }, + { url = "https://files.pythonhosted.org/packages/ad/c6/385cf65448b5739881ba630d144e9c38464737ce68ae4fe4d6a2c7bb3809/coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec", size = 239107 }, + { url = "https://files.pythonhosted.org/packages/7c/a2/9302717d181eeaac738941b2a58e6bd776ef665db24f41f82e32cc8fe814/coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd", size = 237306 }, + { url = "https://files.pythonhosted.org/packages/4d/39/0cfdb5a4bde5843eead02c0f8bc43f8ab3129408cbec53f9ad4f11fc27cf/coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade", size = 238461 }, + { url = "https://files.pythonhosted.org/packages/25/41/5af6b1c2ce964d60d68e80de24c1e5615a4b845958c5f361371b730288f3/coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57", size = 208352 }, + { url = "https://files.pythonhosted.org/packages/29/bc/65b8b11611b1e3cc83fb78c6757a7b2abf638ae449085406017adc4a6c74/coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c", size = 209203 }, + { url = "https://files.pythonhosted.org/packages/99/15/dbcb5d0a22bf5357cf456dfd16f9ceb89c54544d6201d53bc77c75077a8e/coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677", size = 198370 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, +] + +[[package]] +name = "griffe" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astunparse", marker = "python_full_version < '3.9'" }, + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/d1/dcd486d6d577cb12490c78cfa88679fb9b481b227807f14632ba9bd82245/griffe-1.3.1.tar.gz", hash = "sha256:3f86a716b631a4c0f96a43cb75d05d3c85975003c20540426c0eba3b0581c56a", size = 382412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/19/63971981a20aecfa7cbd07c5cac6914cf1180b3dd8db5fe8ab2ea410315f/griffe-1.3.1-py3-none-any.whl", hash = "sha256:940aeb630bc3054b4369567f150b6365be6f11eef46b0ed8623aea96e6d17b19", size = 126902 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "invoke" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/42/127e6d792884ab860defc3f4d80a8f9812e48ace584ffc5a346de58cdc6c/invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5", size = 299835 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/66/7f8c48009c72d73bc6bbe6eb87ac838d6a526146f7dab14af671121eb379/invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820", size = 160274 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "markdown" +version = "3.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/28/c5441a6642681d92de56063fa7984df56f783d3f1eba518dc3e7a253b606/Markdown-3.5.2.tar.gz", hash = "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8", size = 349398 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/f4/f0031854de10a0bc7821ef9fca0b92ca0d7aa6fbfbf504c5473ba825e49c/Markdown-3.5.2-py3-none-any.whl", hash = "sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd", size = 103870 }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206 }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079 }, + { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620 }, + { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818 }, + { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493 }, + { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630 }, + { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745 }, + { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021 }, + { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659 }, + { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213 }, + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219 }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098 }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014 }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220 }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756 }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988 }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718 }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317 }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670 }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224 }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, + { url = "https://files.pythonhosted.org/packages/f8/ff/2c942a82c35a49df5de3a630ce0a8456ac2969691b230e530ac12314364c/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", size = 18192 }, + { url = "https://files.pythonhosted.org/packages/4f/14/6f294b9c4f969d0c801a4615e221c1e084722ea6114ab2114189c5b8cbe0/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", size = 14072 }, + { url = "https://files.pythonhosted.org/packages/81/d4/fd74714ed30a1dedd0b82427c02fa4deec64f173831ec716da11c51a50aa/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", size = 26928 }, + { url = "https://files.pythonhosted.org/packages/c7/bd/50319665ce81bb10e90d1cf76f9e1aa269ea6f7fa30ab4521f14d122a3df/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", size = 26106 }, + { url = "https://files.pythonhosted.org/packages/4c/6f/f2b0f675635b05f6afd5ea03c094557bdb8622fa8e673387444fe8d8e787/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68", size = 25781 }, + { url = "https://files.pythonhosted.org/packages/51/e0/393467cf899b34a9d3678e78961c2c8cdf49fb902a959ba54ece01273fb1/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", size = 30518 }, + { url = "https://files.pythonhosted.org/packages/f6/02/5437e2ad33047290dafced9df741d9efc3e716b75583bbd73a9984f1b6f7/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", size = 29669 }, + { url = "https://files.pythonhosted.org/packages/0e/7d/968284145ffd9d726183ed6237c77938c021abacde4e073020f920e060b2/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", size = 29933 }, + { url = "https://files.pythonhosted.org/packages/bf/f3/ecb00fc8ab02b7beae8699f34db9357ae49d9f21d4d3de6f305f34fa949e/MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", size = 16656 }, + { url = "https://files.pythonhosted.org/packages/92/21/357205f03514a49b293e214ac39de01fadd0970a6e05e4bf1ddd0ffd0881/MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", size = 17206 }, + { url = "https://files.pythonhosted.org/packages/0f/31/780bb297db036ba7b7bbede5e1d7f1e14d704ad4beb3ce53fb495d22bc62/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", size = 18193 }, + { url = "https://files.pythonhosted.org/packages/6c/77/d77701bbef72892affe060cdacb7a2ed7fd68dae3b477a8642f15ad3b132/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", size = 14073 }, + { url = "https://files.pythonhosted.org/packages/d9/a7/1e558b4f78454c8a3a0199292d96159eb4d091f983bc35ef258314fe7269/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", size = 26486 }, + { url = "https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", size = 25685 }, + { url = "https://files.pythonhosted.org/packages/6a/18/ae5a258e3401f9b8312f92b028c54d7026a97ec3ab20bfaddbdfa7d8cce8/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", size = 25338 }, + { url = "https://files.pythonhosted.org/packages/0b/cc/48206bd61c5b9d0129f4d75243b156929b04c94c09041321456fd06a876d/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", size = 30439 }, + { url = "https://files.pythonhosted.org/packages/d1/06/a41c112ab9ffdeeb5f77bc3e331fdadf97fa65e52e44ba31880f4e7f983c/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", size = 29531 }, + { url = "https://files.pythonhosted.org/packages/02/8c/ab9a463301a50dab04d5472e998acbd4080597abc048166ded5c7aa768c8/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", size = 29823 }, + { url = "https://files.pythonhosted.org/packages/bc/29/9bc18da763496b055d8e98ce476c8e718dcfd78157e17f555ce6dd7d0895/MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", size = 16658 }, + { url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211 }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, +] + +[[package]] +name = "mkdocs" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "ghp-import" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/bb/24a22f8154cf79b07b45da070633613837d6e59c7d870076f693b7b1c556/mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2", size = 3654364 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/58/aa3301b23966a71d7f8e55233f467b3cec94a651434e9cd9053811342539/mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1", size = 3694750 }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/75/0ced93354fd9df40531c548f07d6462912eea9519e8cd78a8e6b42d73c4a/mkdocs_autorefs-1.0.1.tar.gz", hash = "sha256:f684edf847eced40b570b57846b15f0bf57fb93ac2c510450775dcf16accb971", size = 17743 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/01/d413c98335ed75d8c211afb91320811366d55fb0ef7f4b01b9ab19630eac/mkdocs_autorefs-1.0.1-py3-none-any.whl", hash = "sha256:aacdfae1ab197780fb7a2dac92ad8a3d8f7ca8049a9cbe56a4218cd52e8da570", size = 13444 }, +] + +[[package]] +name = "mkdocs-material" +version = "9.5.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/a5/56d359426ede2694f8f8eb3257b72ab0ea3d17a1a48e99e6003cb5de8fc2/mkdocs_material-9.5.16.tar.gz", hash = "sha256:8b89b639592660f24657bb058de4aff0060cd0383148f8f51711201730f17503", size = 4061982 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/51/598e319d8b98e64d5353e9e64e25ad5a1604d2ad5a4de3867d97205dba19/mkdocs_material-9.5.16-py3-none-any.whl", hash = "sha256:32fce3cd8ecbd5dca6e5887cc0cf5bc78707a36f7d0f6f1bbbe9edaf428b8055", size = 8741526 }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, +] + +[[package]] +name = "mkdocstrings" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "platformdirs" }, + { name = "pymdown-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/b1/ce782ff49d0fcf2ef65e9a77aa651f115a8c5e48aab6a72f2001bfcc404b/mkdocstrings-0.24.1.tar.gz", hash = "sha256:cc83f9a1c8724fc1be3c2fa071dd73d91ce902ef6a79710249ec8d0ee1064401", size = 31954 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/53/941fc52aa984f6f03b4f7473d7ec787b22076794eda40701a705cab1ab01/mkdocstrings-0.24.1-py3-none-any.whl", hash = "sha256:b4206f9a2ca8a648e222d5a0ca1d36ba7dee53c88732818de183b536f9042b5d", size = 28259 }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, + { name = "markdown" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/ed/d65dbd6e594c9de25a94f1cb78daf6a4a82bee186945f26814e9d7c5be6b/mkdocstrings_python-1.9.0.tar.gz", hash = "sha256:6e1a442367cf75d30cf69774cbb1ad02aebec58bfff26087439df4955efecfde", size = 33732 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/69/07eaed30332b0bf8299d6dd172375846eebbf86608d6e1b0039777648b2f/mkdocstrings_python-1.9.0-py3-none-any.whl", hash = "sha256:fad27d7314b4ec9c0359a187b477fb94c65ef561fdae941dca1b717c59aae96f", size = 58266 }, +] + +[[package]] +name = "more-itertools" +version = "10.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/7905a7fd46ffb61d976133a4f47799388209e73cbc8c1253593335da88b4/more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1", size = 114449 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/e2/8e10e465ee3987bb7c9ab69efb91d867d93959095f4807db102d07995d94/more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684", size = 57015 }, +] + +[[package]] +name = "packaging" +version = "24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "pbr" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/c2/ee43b3b11bf2b40e56536183fc9f22afbb04e882720332b6276ee2454c24/pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9", size = 123150 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/dd/171c9fb653591cf265bcc89c436eec75c9bde3dec921cc236fa71e5698df/pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda", size = 107506 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/c6/43f9d44d92aed815e781ca25ba8c174257e27253a94630d21be8725a2b59/pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be", size = 65812 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/5b/0cc789b59e8cc1bf288b38111d002d8c5917123194d45b29dcdac64723cc/pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", size = 20120 }, +] + +[[package]] +name = "py" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/ec/e3d966cfb286d5a48e7c43a559a297b857ab935209ee9072e5a5492706c9/pymdown_extensions-10.7.1.tar.gz", hash = "sha256:c70e146bdd83c744ffc766b4671999796aba18842b268510a329f7f64700d584", size = 811769 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/4b/4ebb08f36e83e91b31117fe8f67844bac7f5c7d11ed151ae5db807be363f/pymdown_extensions-10.7.1-py3-none-any.whl", hash = "sha256:f5cc7000d7ff0d1ce9395d216017fa4df3dde800afb1fb72d1c7d3fd35e710f4", size = 250794 }, +] + +[[package]] +name = "pytest" +version = "8.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/b7/7d44bbc04c531dcc753056920e0988032e5871ac674b5a84cb979de6e7af/pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044", size = 1409703 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/7e/c79cecfdb6aa85c6c2e3cf63afc56d0f165f24f5c66c03c695c4d9b84756/pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7", size = 337359 }, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, +] + +[[package]] +name = "pytest-timeout" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/0d/04719abc7a4bdb3a7a1f968f24b0f5253d698c9cc94975330e9d3145befb/pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9", size = 17697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-multipart" +version = "0.0.9" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "atheris", marker = "python_full_version != '3.12.*'" }, + { name = "atomicwrites" }, + { name = "attrs" }, + { name = "coverage" }, + { name = "invoke" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings" }, + { name = "mkdocstrings-python" }, + { name = "more-itertools" }, + { name = "pbr" }, + { name = "pluggy" }, + { name = "py" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-timeout" }, + { name = "pyyaml" }, + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "atheris", marker = "python_full_version != '3.12.*'", specifier = "==2.3.0" }, + { name = "atomicwrites", specifier = "==1.4.1" }, + { name = "attrs", specifier = "==23.2.0" }, + { name = "coverage", specifier = "==7.4.4" }, + { name = "invoke", specifier = "==2.2.0" }, + { name = "mkdocs", specifier = "==1.5.3" }, + { name = "mkdocs-autorefs", specifier = "==1.0.1" }, + { name = "mkdocs-material", specifier = "==9.5.16" }, + { name = "mkdocstrings", specifier = "==0.24.1" }, + { name = "mkdocstrings-python", specifier = "==1.9.0" }, + { name = "more-itertools", specifier = "==10.2.0" }, + { name = "pbr", specifier = "==6.0.0" }, + { name = "pluggy", specifier = "==1.4.0" }, + { name = "py", specifier = "==1.11.0" }, + { name = "pytest", specifier = "==8.1.1" }, + { name = "pytest-cov", specifier = "==5.0.0" }, + { name = "pytest-timeout", specifier = "==2.3.1" }, + { name = "pyyaml", specifier = "==6.0.1" }, + { name = "ruff", specifier = "==0.3.4" }, +] + +[[package]] +name = "pytz" +version = "2024.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/af35f7ea75cf72f2cd079c95ee16797de7cd71f29ea7c68ae5ce7be1eda0/PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", size = 125201 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/06/4beb652c0fe16834032e54f0956443d4cc797fe645527acee59e7deaa0a2/PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", size = 189447 }, + { url = "https://files.pythonhosted.org/packages/5b/07/10033a403b23405a8fc48975444463d3d10a5c2736b7eb2550b07b367429/PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f", size = 169264 }, + { url = "https://files.pythonhosted.org/packages/f1/26/55e4f21db1f72eaef092015d9017c11510e7e6301c62a6cfee91295d13c6/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", size = 677003 }, + { url = "https://files.pythonhosted.org/packages/ba/91/090818dfa62e85181f3ae23dd1e8b7ea7f09684864a900cab72d29c57346/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", size = 699070 }, + { url = "https://files.pythonhosted.org/packages/29/61/bf33c6c85c55bc45a29eee3195848ff2d518d84735eb0e2d8cb42e0d285e/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", size = 705525 }, + { url = "https://files.pythonhosted.org/packages/07/91/45dfd0ef821a7f41d9d0136ea3608bb5b1653e42fd56a7970532cb5c003f/PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", size = 707514 }, + { url = "https://files.pythonhosted.org/packages/b6/a0/b6700da5d49e9fed49dc3243d3771b598dad07abb37cc32e524607f96adc/PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", size = 130488 }, + { url = "https://files.pythonhosted.org/packages/24/97/9b59b43431f98d01806b288532da38099cc6f2fea0f3d712e21e269c0279/PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", size = 145338 }, + { url = "https://files.pythonhosted.org/packages/ec/0d/26fb23e8863e0aeaac0c64e03fd27367ad2ae3f3cccf3798ee98ce160368/PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", size = 187867 }, + { url = "https://files.pythonhosted.org/packages/28/09/55f715ddbf95a054b764b547f617e22f1d5e45d83905660e9a088078fe67/PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", size = 167530 }, + { url = "https://files.pythonhosted.org/packages/5e/94/7d5ee059dfb92ca9e62f4057dcdec9ac08a9e42679644854dc01177f8145/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", size = 732244 }, + { url = "https://files.pythonhosted.org/packages/06/92/e0224aa6ebf9dc54a06a4609da37da40bb08d126f5535d81bff6b417b2ae/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", size = 752871 }, + { url = "https://files.pythonhosted.org/packages/7b/5e/efd033ab7199a0b2044dab3b9f7a4f6670e6a52c089de572e928d2873b06/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", size = 757729 }, + { url = "https://files.pythonhosted.org/packages/03/5c/c4671451b2f1d76ebe352c0945d4cd13500adb5d05f5a51ee296d80152f7/PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", size = 748528 }, + { url = "https://files.pythonhosted.org/packages/73/9c/766e78d1efc0d1fca637a6b62cea1b4510a7fb93617eb805223294fef681/PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", size = 130286 }, + { url = "https://files.pythonhosted.org/packages/b3/34/65bb4b2d7908044963ebf614fe0fdb080773fc7030d7e39c8d3eddcd4257/PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", size = 144699 }, + { url = "https://files.pythonhosted.org/packages/bc/06/1b305bf6aa704343be85444c9d011f626c763abb40c0edc1cad13bfd7f86/PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", size = 178692 }, + { url = "https://files.pythonhosted.org/packages/84/02/404de95ced348b73dd84f70e15a41843d817ff8c1744516bf78358f2ffd2/PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", size = 165622 }, + { url = "https://files.pythonhosted.org/packages/c7/4c/4a2908632fc980da6d918b9de9c1d9d7d7e70b2672b1ad5166ed27841ef7/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", size = 696937 }, + { url = "https://files.pythonhosted.org/packages/b4/33/720548182ffa8344418126017aa1d4ab4aeec9a2275f04ce3f3573d8ace8/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", size = 724969 }, + { url = "https://files.pythonhosted.org/packages/4f/78/77b40157b6cb5f2d3d31a3d9b2efd1ba3505371f76730d267e8b32cf4b7f/PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", size = 712604 }, + { url = "https://files.pythonhosted.org/packages/2e/97/3e0e089ee85e840f4b15bfa00e4e63d84a3691ababbfea92d6f820ea6f21/PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", size = 126098 }, + { url = "https://files.pythonhosted.org/packages/2b/9f/fbade56564ad486809c27b322d0f7e6a89c01f6b4fe208402e90d4443a99/PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", size = 138675 }, + { url = "https://files.pythonhosted.org/packages/7f/5d/2779ea035ba1e533c32ed4a249b4e0448f583ba10830b21a3cddafe11a4e/PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", size = 191734 }, + { url = "https://files.pythonhosted.org/packages/e1/a1/27bfac14b90adaaccf8c8289f441e9f76d94795ec1e7a8f134d9f2cb3d0b/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", size = 723767 }, + { url = "https://files.pythonhosted.org/packages/c1/39/47ed4d65beec9ce07267b014be85ed9c204fa373515355d3efa62d19d892/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", size = 749067 }, + { url = "https://files.pythonhosted.org/packages/c8/6b/6600ac24725c7388255b2f5add93f91e58a5d7efaf4af244fdbcc11a541b/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", size = 736569 }, + { url = "https://files.pythonhosted.org/packages/0d/46/62ae77677e532c0af6c81ddd6f3dbc16bdcc1208b077457354442d220bfb/PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", size = 787738 }, + { url = "https://files.pythonhosted.org/packages/d6/6a/439d1a6f834b9a9db16332ce16c4a96dd0e3970b65fe08cbecd1711eeb77/PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", size = 139797 }, + { url = "https://files.pythonhosted.org/packages/29/0f/9782fa5b10152abf033aec56a601177ead85ee03b57781f2d9fced09eefc/PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", size = 157350 }, + { url = "https://files.pythonhosted.org/packages/57/c5/5d09b66b41d549914802f482a2118d925d876dc2a35b2d127694c1345c34/PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", size = 197846 }, + { url = "https://files.pythonhosted.org/packages/0e/88/21b2f16cb2123c1e9375f2c93486e35fdc86e63f02e274f0e99c589ef153/PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", size = 174396 }, + { url = "https://files.pythonhosted.org/packages/ac/6c/967d91a8edf98d2b2b01d149bd9e51b8f9fb527c98d80ebb60c6b21d60c4/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", size = 731824 }, + { url = "https://files.pythonhosted.org/packages/4a/4b/c71ef18ef83c82f99e6da8332910692af78ea32bd1d1d76c9787dfa36aea/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", size = 754777 }, + { url = "https://files.pythonhosted.org/packages/7d/39/472f2554a0f1e825bd7c5afc11c817cd7a2f3657460f7159f691fbb37c51/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", size = 738883 }, + { url = "https://files.pythonhosted.org/packages/40/da/a175a35cf5583580e90ac3e2a3dbca90e43011593ae62ce63f79d7b28d92/PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", size = 750294 }, + { url = "https://files.pythonhosted.org/packages/24/62/7fcc372442ec8ea331da18c24b13710e010c5073ab851ef36bf9dacb283f/PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", size = 136936 }, + { url = "https://files.pythonhosted.org/packages/84/4d/82704d1ab9290b03da94e6425f5e87396b999fd7eb8e08f3a92c158402bf/PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", size = 152751 }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, +] + +[[package]] +name = "regex" +version = "2024.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/38/148df33b4dbca3bd069b963acab5e0fa1a9dbd6820f8c322d0dd6faeff96/regex-2024.9.11.tar.gz", hash = "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd", size = 399403 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/12/497bd6599ce8a239ade68678132296aec5ee25ebea45fc8ba91aa60fceec/regex-2024.9.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408", size = 482488 }, + { url = "https://files.pythonhosted.org/packages/c1/24/595ddb9bec2a9b151cdaf9565b0c9f3da9f0cb1dca6c158bc5175332ddf8/regex-2024.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d", size = 287443 }, + { url = "https://files.pythonhosted.org/packages/69/a8/b2fb45d9715b1469383a0da7968f8cacc2f83e9fbbcd6b8713752dd980a6/regex-2024.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5", size = 284561 }, + { url = "https://files.pythonhosted.org/packages/88/87/1ce4a5357216b19b7055e7d3b0efc75a6e426133bf1e7d094321df514257/regex-2024.9.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c", size = 783177 }, + { url = "https://files.pythonhosted.org/packages/3c/65/b9f002ab32f7b68e7d1dcabb67926f3f47325b8dbc22cc50b6a043e1d07c/regex-2024.9.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8", size = 823193 }, + { url = "https://files.pythonhosted.org/packages/22/91/8339dd3abce101204d246e31bc26cdd7ec07c9f91598472459a3a902aa41/regex-2024.9.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35", size = 809950 }, + { url = "https://files.pythonhosted.org/packages/cb/19/556638aa11c2ec9968a1da998f07f27ec0abb9bf3c647d7c7985ca0b8eea/regex-2024.9.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71", size = 782661 }, + { url = "https://files.pythonhosted.org/packages/d1/e9/7a5bc4c6ef8d9cd2bdd83a667888fc35320da96a4cc4da5fa084330f53db/regex-2024.9.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8", size = 772348 }, + { url = "https://files.pythonhosted.org/packages/f1/0b/29f2105bfac3ed08e704914c38e93b07c784a6655f8a015297ee7173e95b/regex-2024.9.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a", size = 697460 }, + { url = "https://files.pythonhosted.org/packages/71/3a/52ff61054d15a4722605f5872ad03962b319a04c1ebaebe570b8b9b7dde1/regex-2024.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d", size = 769151 }, + { url = "https://files.pythonhosted.org/packages/97/07/37e460ab5ca84be8e1e197c3b526c5c86993dcc9e13cbc805c35fc2463c1/regex-2024.9.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137", size = 777478 }, + { url = "https://files.pythonhosted.org/packages/65/7b/953075723dd5ab00780043ac2f9de667306ff9e2a85332975e9f19279174/regex-2024.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6", size = 845373 }, + { url = "https://files.pythonhosted.org/packages/40/b8/3e9484c6230b8b6e8f816ab7c9a080e631124991a4ae2c27a81631777db0/regex-2024.9.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca", size = 845369 }, + { url = "https://files.pythonhosted.org/packages/b7/99/38434984d912edbd2e1969d116257e869578f67461bd7462b894c45ed874/regex-2024.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a", size = 773935 }, + { url = "https://files.pythonhosted.org/packages/ab/67/43174d2b46fa947b7b9dfe56b6c8a8a76d44223f35b1d64645a732fd1d6f/regex-2024.9.11-cp310-cp310-win32.whl", hash = "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0", size = 261624 }, + { url = "https://files.pythonhosted.org/packages/c4/2a/4f9c47d9395b6aff24874c761d8d620c0232f97c43ef3cf668c8b355e7a7/regex-2024.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623", size = 274020 }, + { url = "https://files.pythonhosted.org/packages/86/a1/d526b7b6095a0019aa360948c143aacfeb029919c898701ce7763bbe4c15/regex-2024.9.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df", size = 482483 }, + { url = "https://files.pythonhosted.org/packages/32/d9/bfdd153179867c275719e381e1e8e84a97bd186740456a0dcb3e7125c205/regex-2024.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268", size = 287442 }, + { url = "https://files.pythonhosted.org/packages/33/c4/60f3370735135e3a8d673ddcdb2507a8560d0e759e1398d366e43d000253/regex-2024.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad", size = 284561 }, + { url = "https://files.pythonhosted.org/packages/b1/51/91a5ebdff17f9ec4973cb0aa9d37635efec1c6868654bbc25d1543aca4ec/regex-2024.9.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679", size = 791779 }, + { url = "https://files.pythonhosted.org/packages/07/4a/022c5e6f0891a90cd7eb3d664d6c58ce2aba48bff107b00013f3d6167069/regex-2024.9.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4", size = 832605 }, + { url = "https://files.pythonhosted.org/packages/ac/1c/3793990c8c83ca04e018151ddda83b83ecc41d89964f0f17749f027fc44d/regex-2024.9.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664", size = 818556 }, + { url = "https://files.pythonhosted.org/packages/e9/5c/8b385afbfacb853730682c57be56225f9fe275c5bf02ac1fc88edbff316d/regex-2024.9.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50", size = 792808 }, + { url = "https://files.pythonhosted.org/packages/9b/8b/a4723a838b53c771e9240951adde6af58c829fb6a6a28f554e8131f53839/regex-2024.9.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199", size = 781115 }, + { url = "https://files.pythonhosted.org/packages/83/5f/031a04b6017033d65b261259c09043c06f4ef2d4eac841d0649d76d69541/regex-2024.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4", size = 778155 }, + { url = "https://files.pythonhosted.org/packages/fd/cd/4660756070b03ce4a66663a43f6c6e7ebc2266cc6b4c586c167917185eb4/regex-2024.9.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd", size = 784614 }, + { url = "https://files.pythonhosted.org/packages/93/8d/65b9bea7df120a7be8337c415b6d256ba786cbc9107cebba3bf8ff09da99/regex-2024.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f", size = 853744 }, + { url = "https://files.pythonhosted.org/packages/96/a7/fba1eae75eb53a704475baf11bd44b3e6ccb95b316955027eb7748f24ef8/regex-2024.9.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96", size = 855890 }, + { url = "https://files.pythonhosted.org/packages/45/14/d864b2db80a1a3358534392373e8a281d95b28c29c87d8548aed58813910/regex-2024.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1", size = 781887 }, + { url = "https://files.pythonhosted.org/packages/4d/a9/bfb29b3de3eb11dc9b412603437023b8e6c02fb4e11311863d9bf62c403a/regex-2024.9.11-cp311-cp311-win32.whl", hash = "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9", size = 261644 }, + { url = "https://files.pythonhosted.org/packages/c7/ab/1ad2511cf6a208fde57fafe49829cab8ca018128ab0d0b48973d8218634a/regex-2024.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf", size = 274033 }, + { url = "https://files.pythonhosted.org/packages/6e/92/407531450762bed778eedbde04407f68cbd75d13cee96c6f8d6903d9c6c1/regex-2024.9.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7", size = 483590 }, + { url = "https://files.pythonhosted.org/packages/8e/a2/048acbc5ae1f615adc6cba36cc45734e679b5f1e4e58c3c77f0ed611d4e2/regex-2024.9.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231", size = 288175 }, + { url = "https://files.pythonhosted.org/packages/8a/ea/909d8620329ab710dfaf7b4adee41242ab7c9b95ea8d838e9bfe76244259/regex-2024.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d", size = 284749 }, + { url = "https://files.pythonhosted.org/packages/ca/fa/521eb683b916389b4975337873e66954e0f6d8f91bd5774164a57b503185/regex-2024.9.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64", size = 795181 }, + { url = "https://files.pythonhosted.org/packages/28/db/63047feddc3280cc242f9c74f7aeddc6ee662b1835f00046f57d5630c827/regex-2024.9.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42", size = 835842 }, + { url = "https://files.pythonhosted.org/packages/e3/94/86adc259ff8ec26edf35fcca7e334566c1805c7493b192cb09679f9c3dee/regex-2024.9.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766", size = 823533 }, + { url = "https://files.pythonhosted.org/packages/29/52/84662b6636061277cb857f658518aa7db6672bc6d1a3f503ccd5aefc581e/regex-2024.9.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a", size = 797037 }, + { url = "https://files.pythonhosted.org/packages/c3/2a/cd4675dd987e4a7505f0364a958bc41f3b84942de9efaad0ef9a2646681c/regex-2024.9.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9", size = 784106 }, + { url = "https://files.pythonhosted.org/packages/6f/75/3ea7ec29de0bbf42f21f812f48781d41e627d57a634f3f23947c9a46e303/regex-2024.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d", size = 782468 }, + { url = "https://files.pythonhosted.org/packages/d3/67/15519d69b52c252b270e679cb578e22e0c02b8dd4e361f2b04efcc7f2335/regex-2024.9.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822", size = 790324 }, + { url = "https://files.pythonhosted.org/packages/9c/71/eff77d3fe7ba08ab0672920059ec30d63fa7e41aa0fb61c562726e9bd721/regex-2024.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0", size = 860214 }, + { url = "https://files.pythonhosted.org/packages/81/11/e1bdf84a72372e56f1ea4b833dd583b822a23138a616ace7ab57a0e11556/regex-2024.9.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a", size = 859420 }, + { url = "https://files.pythonhosted.org/packages/ea/75/9753e9dcebfa7c3645563ef5c8a58f3a47e799c872165f37c55737dadd3e/regex-2024.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a", size = 787333 }, + { url = "https://files.pythonhosted.org/packages/bc/4e/ba1cbca93141f7416624b3ae63573e785d4bc1834c8be44a8f0747919eca/regex-2024.9.11-cp312-cp312-win32.whl", hash = "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776", size = 262058 }, + { url = "https://files.pythonhosted.org/packages/6e/16/efc5f194778bf43e5888209e5cec4b258005d37c613b67ae137df3b89c53/regex-2024.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009", size = 273526 }, + { url = "https://files.pythonhosted.org/packages/93/0a/d1c6b9af1ff1e36832fe38d74d5c5bab913f2bdcbbd6bc0e7f3ce8b2f577/regex-2024.9.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784", size = 483376 }, + { url = "https://files.pythonhosted.org/packages/a4/42/5910a050c105d7f750a72dcb49c30220c3ae4e2654e54aaaa0e9bc0584cb/regex-2024.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36", size = 288112 }, + { url = "https://files.pythonhosted.org/packages/8d/56/0c262aff0e9224fa7ffce47b5458d373f4d3e3ff84e99b5ff0cb15e0b5b2/regex-2024.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92", size = 284608 }, + { url = "https://files.pythonhosted.org/packages/b9/54/9fe8f9aec5007bbbbce28ba3d2e3eaca425f95387b7d1e84f0d137d25237/regex-2024.9.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86", size = 795337 }, + { url = "https://files.pythonhosted.org/packages/b2/e7/6b2f642c3cded271c4f16cc4daa7231be544d30fe2b168e0223724b49a61/regex-2024.9.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85", size = 835848 }, + { url = "https://files.pythonhosted.org/packages/cd/9e/187363bdf5d8c0e4662117b92aa32bf52f8f09620ae93abc7537d96d3311/regex-2024.9.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963", size = 823503 }, + { url = "https://files.pythonhosted.org/packages/f8/10/601303b8ee93589f879664b0cfd3127949ff32b17f9b6c490fb201106c4d/regex-2024.9.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6", size = 797049 }, + { url = "https://files.pythonhosted.org/packages/ef/1c/ea200f61ce9f341763f2717ab4daebe4422d83e9fd4ac5e33435fd3a148d/regex-2024.9.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802", size = 784144 }, + { url = "https://files.pythonhosted.org/packages/d8/5c/d2429be49ef3292def7688401d3deb11702c13dcaecdc71d2b407421275b/regex-2024.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29", size = 782483 }, + { url = "https://files.pythonhosted.org/packages/12/d9/cbc30f2ff7164f3b26a7760f87c54bf8b2faed286f60efd80350a51c5b99/regex-2024.9.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8", size = 790320 }, + { url = "https://files.pythonhosted.org/packages/19/1d/43ed03a236313639da5a45e61bc553c8d41e925bcf29b0f8ecff0c2c3f25/regex-2024.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84", size = 860435 }, + { url = "https://files.pythonhosted.org/packages/34/4f/5d04da61c7c56e785058a46349f7285ae3ebc0726c6ea7c5c70600a52233/regex-2024.9.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554", size = 859571 }, + { url = "https://files.pythonhosted.org/packages/12/7f/8398c8155a3c70703a8e91c29532558186558e1aea44144b382faa2a6f7a/regex-2024.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8", size = 787398 }, + { url = "https://files.pythonhosted.org/packages/58/3a/f5903977647a9a7e46d5535e9e96c194304aeeca7501240509bde2f9e17f/regex-2024.9.11-cp313-cp313-win32.whl", hash = "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8", size = 262035 }, + { url = "https://files.pythonhosted.org/packages/ff/80/51ba3a4b7482f6011095b3a036e07374f64de180b7d870b704ed22509002/regex-2024.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f", size = 273510 }, + { url = "https://files.pythonhosted.org/packages/58/03/ac6839452b59793683c33a3eb782671863800f4d514aec81f38098d2846f/regex-2024.9.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4", size = 482618 }, + { url = "https://files.pythonhosted.org/packages/c2/8f/acb2dbdcb0ec4ce99a06544868c4a3463faad344f89437712419ccbd70a4/regex-2024.9.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e", size = 287536 }, + { url = "https://files.pythonhosted.org/packages/28/47/d267e0c8f327d717f565cdba76d354993a350a0f6aba8efa650c0fe93d79/regex-2024.9.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60", size = 284558 }, + { url = "https://files.pythonhosted.org/packages/7f/0d/7ec6c7c306cea8fcf7413565d0f778ba056bf2b2fa97508e0506521987e7/regex-2024.9.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b", size = 784441 }, + { url = "https://files.pythonhosted.org/packages/ab/c3/b1db10548c31491fe8c8e904e032f1b9af1fd6d193bab32324caed4caf7a/regex-2024.9.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366", size = 824268 }, + { url = "https://files.pythonhosted.org/packages/5e/82/90127f8e15384c1edee89cc9b937c453e7e2419e635dc160ac2bd7f8239e/regex-2024.9.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8", size = 810498 }, + { url = "https://files.pythonhosted.org/packages/75/d1/ea4e9b22e2b19463d0def76418e21316b9a8acc88ce6b764353834015ee0/regex-2024.9.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb", size = 785037 }, + { url = "https://files.pythonhosted.org/packages/9c/92/4360fab411bad3dc9862742407d9c1790858e160e1e732ad2491747c4053/regex-2024.9.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4", size = 774303 }, + { url = "https://files.pythonhosted.org/packages/89/7b/4c3b129108fbcab118f93da68d7ac84dff831d71fafd8bcda711f572ac0d/regex-2024.9.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca", size = 702542 }, + { url = "https://files.pythonhosted.org/packages/3f/9f/d1834185895df468860597f5934272539f21bc8112013f2e892fd6dd588e/regex-2024.9.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb", size = 771060 }, + { url = "https://files.pythonhosted.org/packages/4d/87/95091e0f6fc69d1235e61ddabc5dc64f00c6ee6288872564233f9a41bf27/regex-2024.9.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168", size = 777218 }, + { url = "https://files.pythonhosted.org/packages/d0/d4/d5d5c2f757b25a5556033571421bfa469c9b0dfc5e59efeaf0eb88ecfa39/regex-2024.9.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e", size = 846146 }, + { url = "https://files.pythonhosted.org/packages/41/ba/4ce9ef3e3fe1645a55b1546353768a11ac8ffc8e7a9b0a445affcd3aabe2/regex-2024.9.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c", size = 846445 }, + { url = "https://files.pythonhosted.org/packages/5f/13/387d6c7d39c55dbfb06552b7ace7a2ddc05493403618a2f55da71f495832/regex-2024.9.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd", size = 775188 }, + { url = "https://files.pythonhosted.org/packages/a4/f3/262c44485a858e496efea890f141621a05354753fa59ac4f2a41e9bf12a4/regex-2024.9.11-cp38-cp38-win32.whl", hash = "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771", size = 261652 }, + { url = "https://files.pythonhosted.org/packages/33/fc/07e14d7727a9f5773abb728f18f3497ea917c76f16155a68594460d86b8b/regex-2024.9.11-cp38-cp38-win_amd64.whl", hash = "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508", size = 274070 }, + { url = "https://files.pythonhosted.org/packages/a1/aa/e31baf8482ad690ccb3cdf20d1963a01e98d137e4d9ee493dbb0fa8ba2c6/regex-2024.9.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066", size = 482489 }, + { url = "https://files.pythonhosted.org/packages/a1/b5/449c2f14fc20dc42ef9729469fcff42809393470f021ed6c6fcf5f3d3297/regex-2024.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62", size = 287440 }, + { url = "https://files.pythonhosted.org/packages/3f/36/4b60a0c2e4cc6ecb2651be828117a31f42fae55a51a484a8071729df56a6/regex-2024.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16", size = 284566 }, + { url = "https://files.pythonhosted.org/packages/b4/21/feaa5b0d3e5e3bad659cd7d640e6b76cc0719504dbd9bc8f67cfa21bde82/regex-2024.9.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3", size = 782747 }, + { url = "https://files.pythonhosted.org/packages/bb/89/93516f0aa3e8a9366df2cf79bb0290abdc7dbe5dd27373d9bea0978b7ba6/regex-2024.9.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199", size = 822700 }, + { url = "https://files.pythonhosted.org/packages/d5/e7/79c04ccb81cee2831d9d4499274919b9153c1741ce8b3421d69cb0032f1b/regex-2024.9.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8", size = 809327 }, + { url = "https://files.pythonhosted.org/packages/01/e6/a7256c99c312b68f01cfd4f8eae6e770906fffb3832ecb66f35ca5b86b96/regex-2024.9.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca", size = 781970 }, + { url = "https://files.pythonhosted.org/packages/18/c4/29e8b6ff2208775858b5d4a2caa6428d40b5fade95aee426de7e42ffff39/regex-2024.9.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9", size = 771885 }, + { url = "https://files.pythonhosted.org/packages/95/78/7acd8882ac335f1f5ae1756417739fda3053e0bcacea8716ae4a04e74553/regex-2024.9.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a", size = 696978 }, + { url = "https://files.pythonhosted.org/packages/cb/d2/1d44f9b4a3d33ff5773fd79bea53e992d00f81e0af6f1f4e2efac1e4d897/regex-2024.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39", size = 768655 }, + { url = "https://files.pythonhosted.org/packages/79/ba/92ef9d3b8f59cb3df9febef07098dfb4a43c3bdcf35b1084c2009b0a93bf/regex-2024.9.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba", size = 776922 }, + { url = "https://files.pythonhosted.org/packages/16/71/d964c0c9d447f04bbe6ab5eafd220208e7d52b9608e452e6fcad553b38e0/regex-2024.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664", size = 845014 }, + { url = "https://files.pythonhosted.org/packages/83/cb/a378cdc2468782eefefa50183bbeabc3357fb588d4109d845f0a56e68713/regex-2024.9.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89", size = 844916 }, + { url = "https://files.pythonhosted.org/packages/b9/f0/82ea1565a6639270cfe96263002b3d91084a1db5048d9b6084f83bd5972d/regex-2024.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35", size = 773409 }, + { url = "https://files.pythonhosted.org/packages/97/9e/0400d742b9647b4940609a96d550de89e4e89c85f6a370796dab25b5979c/regex-2024.9.11-cp39-cp39-win32.whl", hash = "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142", size = 261680 }, + { url = "https://files.pythonhosted.org/packages/b6/f1/aef1112652ac7b3922d2c129f8325a4fd286b66691127dd99f380f8ede19/regex-2024.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919", size = 274066 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "ruff" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/98/91e1ad8a6777c300b15cad46a1b507375010f8a53cfeaa17f0385bde1103/ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1", size = 2129882 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/61/797dce050c288fc8325e6b723baa1dd6aff4851ee1b769350b54fd3e0fe5/ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4", size = 16472324 }, + { url = "https://files.pythonhosted.org/packages/b9/3c/5025d7eee9dd76abb489c1a98c05797e1889329abf8b8b4efcd7095e74f5/ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91", size = 8447934 }, + { url = "https://files.pythonhosted.org/packages/5e/c3/2e6aca190ac828dc94bf86384e89513a4a987816c6ddd6a1db4fca0fdd17/ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854", size = 8106257 }, + { url = "https://files.pythonhosted.org/packages/03/92/57b9193e5600445a20d331c9a23dc6c17d27fc50642315bde6fbdaa83499/ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7", size = 7470593 }, + { url = "https://files.pythonhosted.org/packages/93/80/26e4cc40921d759bbdf49b898861aeaf7e1bed80001fc26073a97aac613f/ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365", size = 8635128 }, + { url = "https://files.pythonhosted.org/packages/b5/42/b90b05d167c056aeb71b954cb61fad97a61aaea2a4d5e4e6cba4570c8221/ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369", size = 9389231 }, + { url = "https://files.pythonhosted.org/packages/8e/d7/cd9e7e8d8ca4034577fd28e9ff11551df8d2df9e77a16eecee12121d0f7d/ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c", size = 9094312 }, + { url = "https://files.pythonhosted.org/packages/f6/bb/c583d2a0c8e91ee84a13c31b714070a89863348bbecd2e31ca6ed9b18924/ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9", size = 9909854 }, + { url = "https://files.pythonhosted.org/packages/2e/95/ec159b3cae9960811fe573586ca905578ff78d33f025ae054d30ef6c2b73/ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50", size = 8658269 }, + { url = "https://files.pythonhosted.org/packages/0e/27/13e2cf723209f8e8169de81d4be5b985ff46549b452d112d3e36899ec2ef/ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed", size = 8008722 }, + { url = "https://files.pythonhosted.org/packages/c6/04/036aa4328dfcb50009e80baac7bc78b8532ea9e8c0b6a1d4b75a684301a5/ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488", size = 7463983 }, + { url = "https://files.pythonhosted.org/packages/32/cc/728245664c1fe2adbe90af1044ff2f548527ed12fc607bae74043387990f/ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e", size = 8232832 }, + { url = "https://files.pythonhosted.org/packages/a1/ff/88a45e7b8b87c7a8dac38786ebb800325f9523a9af89f21382104874d9d9/ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378", size = 8705875 }, + { url = "https://files.pythonhosted.org/packages/b9/4b/290e829a7c33fa996f0a598f2cdc954b4820262bb027e0a2edd888c3600d/ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102", size = 7645340 }, + { url = "https://files.pythonhosted.org/packages/09/a1/ecbd844e714a4bed4b9072f5a73bbdc2a3a6e6ee9d9c5b3962be83d5bac8/ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6", size = 8436394 }, + { url = "https://files.pythonhosted.org/packages/f3/c4/afb3bb366074fa98faeb6389618bf10b3eb00bd1eb48d980c205da9b2022/ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232", size = 7991316 }, +] + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, +] + +[[package]] +name = "tomli" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] + +[[package]] +name = "watchdog" +version = "4.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/38/764baaa25eb5e35c9a043d4c4588f9836edfe52a708950f4b6d5f714fd42/watchdog-4.0.2.tar.gz", hash = "sha256:b4dfbb6c49221be4535623ea4474a4d6ee0a9cef4a80b20c28db4d858b64e270", size = 126587 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/b0/219893d41c16d74d0793363bf86df07d50357b81f64bba4cb94fe76e7af4/watchdog-4.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ede7f010f2239b97cc79e6cb3c249e72962404ae3865860855d5cbe708b0fd22", size = 100257 }, + { url = "https://files.pythonhosted.org/packages/6d/c6/8e90c65693e87d98310b2e1e5fd7e313266990853b489e85ce8396cc26e3/watchdog-4.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2cffa171445b0efa0726c561eca9a27d00a1f2b83846dbd5a4f639c4f8ca8e1", size = 92249 }, + { url = "https://files.pythonhosted.org/packages/6f/cd/2e306756364a934532ff8388d90eb2dc8bb21fe575cd2b33d791ce05a02f/watchdog-4.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c50f148b31b03fbadd6d0b5980e38b558046b127dc483e5e4505fcef250f9503", size = 92888 }, + { url = "https://files.pythonhosted.org/packages/de/78/027ad372d62f97642349a16015394a7680530460b1c70c368c506cb60c09/watchdog-4.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c7d4bf585ad501c5f6c980e7be9c4f15604c7cc150e942d82083b31a7548930", size = 100256 }, + { url = "https://files.pythonhosted.org/packages/59/a9/412b808568c1814d693b4ff1cec0055dc791780b9dc947807978fab86bc1/watchdog-4.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:914285126ad0b6eb2258bbbcb7b288d9dfd655ae88fa28945be05a7b475a800b", size = 92252 }, + { url = "https://files.pythonhosted.org/packages/04/57/179d76076cff264982bc335dd4c7da6d636bd3e9860bbc896a665c3447b6/watchdog-4.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:984306dc4720da5498b16fc037b36ac443816125a3705dfde4fd90652d8028ef", size = 92888 }, + { url = "https://files.pythonhosted.org/packages/92/f5/ea22b095340545faea37ad9a42353b265ca751f543da3fb43f5d00cdcd21/watchdog-4.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1cdcfd8142f604630deef34722d695fb455d04ab7cfe9963055df1fc69e6727a", size = 100342 }, + { url = "https://files.pythonhosted.org/packages/cb/d2/8ce97dff5e465db1222951434e3115189ae54a9863aef99c6987890cc9ef/watchdog-4.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7ab624ff2f663f98cd03c8b7eedc09375a911794dfea6bf2a359fcc266bff29", size = 92306 }, + { url = "https://files.pythonhosted.org/packages/49/c4/1aeba2c31b25f79b03b15918155bc8c0b08101054fc727900f1a577d0d54/watchdog-4.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:132937547a716027bd5714383dfc40dc66c26769f1ce8a72a859d6a48f371f3a", size = 92915 }, + { url = "https://files.pythonhosted.org/packages/79/63/eb8994a182672c042d85a33507475c50c2ee930577524dd97aea05251527/watchdog-4.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd67c7df93eb58f360c43802acc945fa8da70c675b6fa37a241e17ca698ca49b", size = 100343 }, + { url = "https://files.pythonhosted.org/packages/ce/82/027c0c65c2245769580605bcd20a1dc7dfd6c6683c8c4e2ef43920e38d27/watchdog-4.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcfd02377be80ef3b6bc4ce481ef3959640458d6feaae0bd43dd90a43da90a7d", size = 92313 }, + { url = "https://files.pythonhosted.org/packages/2a/89/ad4715cbbd3440cb0d336b78970aba243a33a24b1a79d66f8d16b4590d6a/watchdog-4.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:980b71510f59c884d684b3663d46e7a14b457c9611c481e5cef08f4dd022eed7", size = 92919 }, + { url = "https://files.pythonhosted.org/packages/55/08/1a9086a3380e8828f65b0c835b86baf29ebb85e5e94a2811a2eb4f889cfd/watchdog-4.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:aa160781cafff2719b663c8a506156e9289d111d80f3387cf3af49cedee1f040", size = 100255 }, + { url = "https://files.pythonhosted.org/packages/6c/3e/064974628cf305831f3f78264800bd03b3358ec181e3e9380a36ff156b93/watchdog-4.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f6ee8dedd255087bc7fe82adf046f0b75479b989185fb0bdf9a98b612170eac7", size = 92257 }, + { url = "https://files.pythonhosted.org/packages/23/69/1d2ad9c12d93bc1e445baa40db46bc74757f3ffc3a3be592ba8dbc51b6e5/watchdog-4.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0b4359067d30d5b864e09c8597b112fe0a0a59321a0f331498b013fb097406b4", size = 92886 }, + { url = "https://files.pythonhosted.org/packages/68/eb/34d3173eceab490d4d1815ba9a821e10abe1da7a7264a224e30689b1450c/watchdog-4.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:770eef5372f146997638d737c9a3c597a3b41037cfbc5c41538fc27c09c3a3f9", size = 100254 }, + { url = "https://files.pythonhosted.org/packages/18/a1/4bbafe7ace414904c2cc9bd93e472133e8ec11eab0b4625017f0e34caad8/watchdog-4.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eeea812f38536a0aa859972d50c76e37f4456474b02bd93674d1947cf1e39578", size = 92249 }, + { url = "https://files.pythonhosted.org/packages/f3/11/ec5684e0ca692950826af0de862e5db167523c30c9cbf9b3f4ce7ec9cc05/watchdog-4.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b2c45f6e1e57ebb4687690c05bc3a2c1fb6ab260550c4290b8abb1335e0fd08b", size = 92891 }, + { url = "https://files.pythonhosted.org/packages/3b/9a/6f30f023324de7bad8a3eb02b0afb06bd0726003a3550e9964321315df5a/watchdog-4.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10b6683df70d340ac3279eff0b2766813f00f35a1d37515d2c99959ada8f05fa", size = 91775 }, + { url = "https://files.pythonhosted.org/packages/87/62/8be55e605d378a154037b9ba484e00a5478e627b69c53d0f63e3ef413ba6/watchdog-4.0.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f7c739888c20f99824f7aa9d31ac8a97353e22d0c0e54703a547a218f6637eb3", size = 92255 }, + { url = "https://files.pythonhosted.org/packages/6b/59/12e03e675d28f450bade6da6bc79ad6616080b317c472b9ae688d2495a03/watchdog-4.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c100d09ac72a8a08ddbf0629ddfa0b8ee41740f9051429baa8e31bb903ad7508", size = 91682 }, + { url = "https://files.pythonhosted.org/packages/ef/69/241998de9b8e024f5c2fbdf4324ea628b4231925305011ca8b7e1c3329f6/watchdog-4.0.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f5315a8c8dd6dd9425b974515081fc0aadca1d1d61e078d2246509fd756141ee", size = 92249 }, + { url = "https://files.pythonhosted.org/packages/70/3f/2173b4d9581bc9b5df4d7f2041b6c58b5e5448407856f68d4be9981000d0/watchdog-4.0.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2d468028a77b42cc685ed694a7a550a8d1771bb05193ba7b24006b8241a571a1", size = 91773 }, + { url = "https://files.pythonhosted.org/packages/f0/de/6fff29161d5789048f06ef24d94d3ddcc25795f347202b7ea503c3356acb/watchdog-4.0.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f15edcae3830ff20e55d1f4e743e92970c847bcddc8b7509bcd172aa04de506e", size = 92250 }, + { url = "https://files.pythonhosted.org/packages/8a/b1/25acf6767af6f7e44e0086309825bd8c098e301eed5868dc5350642124b9/watchdog-4.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:936acba76d636f70db8f3c66e76aa6cb5136a936fc2a5088b9ce1c7a3508fc83", size = 82947 }, + { url = "https://files.pythonhosted.org/packages/e8/90/aebac95d6f954bd4901f5d46dcd83d68e682bfd21798fd125a95ae1c9dbf/watchdog-4.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:e252f8ca942a870f38cf785aef420285431311652d871409a64e2a0a52a2174c", size = 82942 }, + { url = "https://files.pythonhosted.org/packages/15/3a/a4bd8f3b9381824995787488b9282aff1ed4667e1110f31a87b871ea851c/watchdog-4.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:0e83619a2d5d436a7e58a1aea957a3c1ccbf9782c43c0b4fed80580e5e4acd1a", size = 82947 }, + { url = "https://files.pythonhosted.org/packages/09/cc/238998fc08e292a4a18a852ed8274159019ee7a66be14441325bcd811dfd/watchdog-4.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:88456d65f207b39f1981bf772e473799fcdc10801062c36fd5ad9f9d1d463a73", size = 82946 }, + { url = "https://files.pythonhosted.org/packages/80/f1/d4b915160c9d677174aa5fae4537ae1f5acb23b3745ab0873071ef671f0a/watchdog-4.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:32be97f3b75693a93c683787a87a0dc8db98bb84701539954eef991fb35f5fbc", size = 82947 }, + { url = "https://files.pythonhosted.org/packages/db/02/56ebe2cf33b352fe3309588eb03f020d4d1c061563d9858a9216ba004259/watchdog-4.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:c82253cfc9be68e3e49282831afad2c1f6593af80c0daf1287f6a92657986757", size = 82944 }, + { url = "https://files.pythonhosted.org/packages/01/d2/c8931ff840a7e5bd5dcb93f2bb2a1fd18faf8312e9f7f53ff1cf76ecc8ed/watchdog-4.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c0b14488bd336c5b1845cee83d3e631a1f8b4e9c5091ec539406e4a324f882d8", size = 82947 }, + { url = "https://files.pythonhosted.org/packages/d0/d8/cdb0c21a4a988669d7c210c75c6a2c9a0e16a3b08d9f7e633df0d9a16ad8/watchdog-4.0.2-py3-none-win32.whl", hash = "sha256:0d8a7e523ef03757a5aa29f591437d64d0d894635f8a50f370fe37f913ce4e19", size = 82935 }, + { url = "https://files.pythonhosted.org/packages/99/2e/b69dfaae7a83ea64ce36538cc103a3065e12c447963797793d5c0a1d5130/watchdog-4.0.2-py3-none-win_amd64.whl", hash = "sha256:c344453ef3bf875a535b0488e3ad28e341adbd5a9ffb0f7d62cefacc8824ef2b", size = 82934 }, + { url = "https://files.pythonhosted.org/packages/b0/0b/43b96a9ecdd65ff5545b1b13b687ca486da5c6249475b1a45f24d63a1858/watchdog-4.0.2-py3-none-win_ia64.whl", hash = "sha256:baececaa8edff42cd16558a639a9b0ddf425f93d892e8392a56bf904f5eff22c", size = 82933 }, +] + +[[package]] +name = "wheel" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/a0/95e9e962c5fd9da11c1e28aa4c0d8210ab277b1ada951d2aee336b505813/wheel-0.44.0.tar.gz", hash = "sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49", size = 100733 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d1/9babe2ccaecff775992753d8686970b1e2755d21c8a63be73aba7a4e7d77/wheel-0.44.0-py3-none-any.whl", hash = "sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f", size = 67059 }, +] + +[[package]] +name = "zipp" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200 }, +] From 0defda6213edfe4e1f56d893bd80872df96432ca Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 21 Sep 2024 09:23:08 +0200 Subject: [PATCH 57/94] Update pipelines (#154) --- .github/workflows/docs.yml | 34 +++++++++++++++ .github/workflows/publish.yml | 79 ++++++++++++++++++++--------------- docs/CNAME | 1 + 3 files changed, 81 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/CNAME diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..c600001 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,34 @@ +name: Deploy Documentation + +on: + push: + tags: + - "**" + +permissions: + contents: write + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Configure Git Credentials + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + + - name: Install uv + uses: astral-sh/setup-uv@v2 + with: + version: "0.4.12" + enable-cache: true + + - name: Set up Python + run: uv python install 3.12 + + - name: Install dependencies + run: uv sync --frozen + + - run: uv run mkdocs gh-deploy --force diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 82b3ef5..2d12eb3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,46 +1,59 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Upload Python Package +name: Publish Python Package on: push: tags: - - '*' - -permissions: - contents: read + - "**" jobs: - deploy: + build: runs-on: ubuntu-latest - + outputs: + version: ${{ steps.inspect_package.outputs.version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Set up Python 3.10 - uses: actions/setup-python@v5 - with: - python-version: "3.10" + - name: Install uv + uses: astral-sh/setup-uv@v2 + with: + version: "0.4.12" + enable-cache: true + + - name: Set up Python + run: uv python install 3.12 + + - name: Build package + run: uv build - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - pip install -e '.[docs]' + - name: Inspect package version + id: inspect_package + run: | + version=$(uvx hatchling version) + echo "version=$version" >> "$GITHUB_OUTPUT" - - name: Build package - run: python -m build + - name: Upload package + uses: actions/upload-artifact@v4 + with: + name: package-distributions + path: dist/ - - name: Publish package - uses: pypa/gh-action-pypi-publish@v1.8.14 + pypi-publish: + runs-on: ubuntu-latest + needs: build + + permissions: + id-token: write + + environment: + name: pypi + url: https://pypi.org/project/python-multipart/${{ needs.build.outputs.version }} + + steps: + - name: Download package + uses: actions/download-artifact@v4 with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} - - name: Publish docs - run: mkdocs gh-deploy --force + name: package-distributions + path: dist/ + + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 0000000..c1cab6e --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +multipart.fastapiexpert.com From 21825fced43cd7ef043a2c4d0e142309891482f9 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 21 Sep 2024 09:26:27 +0200 Subject: [PATCH 58/94] Version 0.0.10 (#155) --- multipart/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multipart/__init__.py b/multipart/__init__.py index dc13f13..867707f 100644 --- a/multipart/__init__.py +++ b/multipart/__init__.py @@ -2,7 +2,7 @@ __author__ = "Andrew Dunham" __license__ = "Apache" __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" -__version__ = "0.0.9" +__version__ = "0.0.10" from .multipart import FormParser, MultipartParser, OctetStreamParser, QuerystringParser, create_form_parser, parse_form From 265d6a4d1cd22aec1627f89b3633d2228f7fe55f Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 21 Sep 2024 09:39:47 +0200 Subject: [PATCH 59/94] Upgrade documentation packages (#156) --- docs/api.md | 4 ++- multipart/__init__.py | 11 ++++++- pyproject.toml | 9 +++--- uv.lock | 73 +++++++++++++++++++++++++------------------ 4 files changed, 60 insertions(+), 37 deletions(-) diff --git a/docs/api.md b/docs/api.md index 5e833b6..cc102fd 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1 +1,3 @@ -:::multipart +::: multipart + +::: multipart.exceptions diff --git a/multipart/__init__.py b/multipart/__init__.py index 867707f..a3c7229 100644 --- a/multipart/__init__.py +++ b/multipart/__init__.py @@ -4,9 +4,18 @@ __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" __version__ = "0.0.10" -from .multipart import FormParser, MultipartParser, OctetStreamParser, QuerystringParser, create_form_parser, parse_form +from .multipart import ( + BaseParser, + FormParser, + MultipartParser, + OctetStreamParser, + QuerystringParser, + create_form_parser, + parse_form, +) __all__ = ( + "BaseParser", "FormParser", "MultipartParser", "OctetStreamParser", diff --git a/pyproject.toml b/pyproject.toml index ba2811b..bc29d3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,11 +47,10 @@ dev-dependencies = [ "ruff==0.3.4", "atheris==2.3.0; python_version != '3.12'", # Documentation - "mkdocs==1.5.3", - "mkdocs-material==9.5.16", - "mkdocstrings==0.24.1", - "mkdocstrings-python==1.9.0", - "mkdocs-autorefs==1.0.1", + "mkdocs", + "mkdocs-material", + "mkdocstrings-python", + "mkdocs-autorefs", ] [project.urls] diff --git a/uv.lock b/uv.lock index 6fd204d..69f3835 100644 --- a/uv.lock +++ b/uv.lock @@ -318,14 +318,14 @@ wheels = [ [[package]] name = "markdown" -version = "3.5.2" +version = "3.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/28/c5441a6642681d92de56063fa7984df56f783d3f1eba518dc3e7a253b606/Markdown-3.5.2.tar.gz", hash = "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8", size = 349398 } +sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/f4/f0031854de10a0bc7821ef9fca0b92ca0d7aa6fbfbf504c5473ba825e49c/Markdown-3.5.2-py3-none-any.whl", hash = "sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd", size = 103870 }, + { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 }, ] [[package]] @@ -397,7 +397,7 @@ wheels = [ [[package]] name = "mkdocs" -version = "1.5.3" +version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -408,35 +408,50 @@ dependencies = [ { name = "markdown" }, { name = "markupsafe" }, { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, { name = "packaging" }, { name = "pathspec" }, - { name = "platformdirs" }, { name = "pyyaml" }, { name = "pyyaml-env-tag" }, { name = "watchdog" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/bb/24a22f8154cf79b07b45da070633613837d6e59c7d870076f693b7b1c556/mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2", size = 3654364 } +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/58/aa3301b23966a71d7f8e55233f467b3cec94a651434e9cd9053811342539/mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1", size = 3694750 }, + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 }, ] [[package]] name = "mkdocs-autorefs" -version = "1.0.1" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/75/0ced93354fd9df40531c548f07d6462912eea9519e8cd78a8e6b42d73c4a/mkdocs_autorefs-1.0.1.tar.gz", hash = "sha256:f684edf847eced40b570b57846b15f0bf57fb93ac2c510450775dcf16accb971", size = 17743 } +sdist = { url = "https://files.pythonhosted.org/packages/fb/ae/0f1154c614d6a8b8a36fff084e5b82af3a15f7d2060cf0dcdb1c53297a71/mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f", size = 40262 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/01/d413c98335ed75d8c211afb91320811366d55fb0ef7f4b01b9ab19630eac/mkdocs_autorefs-1.0.1-py3-none-any.whl", hash = "sha256:aacdfae1ab197780fb7a2dac92ad8a3d8f7ca8049a9cbe56a4218cd52e8da570", size = 13444 }, + { url = "https://files.pythonhosted.org/packages/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f", size = 16522 }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, ] [[package]] name = "mkdocs-material" -version = "9.5.16" +version = "9.5.35" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, @@ -451,9 +466,9 @@ dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/a5/56d359426ede2694f8f8eb3257b72ab0ea3d17a1a48e99e6003cb5de8fc2/mkdocs_material-9.5.16.tar.gz", hash = "sha256:8b89b639592660f24657bb058de4aff0060cd0383148f8f51711201730f17503", size = 4061982 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/69/e19bc4de98bce00345ecf9d5d6a19178dd121c0d06a121b374ffd27fcac7/mkdocs_material-9.5.35.tar.gz", hash = "sha256:0d233d7db067ac896bf22ee7950eebf2b1eaf26c155bb27382bf4174021cc117", size = 3994310 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/51/598e319d8b98e64d5353e9e64e25ad5a1604d2ad5a4de3867d97205dba19/mkdocs_material-9.5.16-py3-none-any.whl", hash = "sha256:32fce3cd8ecbd5dca6e5887cc0cf5bc78707a36f7d0f6f1bbbe9edaf428b8055", size = 8741526 }, + { url = "https://files.pythonhosted.org/packages/59/6b/84c7cde89fd957461fdbbf6ca02611a4a043d8afbd0bf990aa6a2d0b52c6/mkdocs_material-9.5.35-py3-none-any.whl", hash = "sha256:44e069d87732d29f4a2533ae0748fa0e67e270043270c71f04d0fba11a357b24", size = 8698196 }, ] [[package]] @@ -467,7 +482,7 @@ wheels = [ [[package]] name = "mkdocstrings" -version = "0.24.1" +version = "0.26.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -481,23 +496,23 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/b1/ce782ff49d0fcf2ef65e9a77aa651f115a8c5e48aab6a72f2001bfcc404b/mkdocstrings-0.24.1.tar.gz", hash = "sha256:cc83f9a1c8724fc1be3c2fa071dd73d91ce902ef6a79710249ec8d0ee1064401", size = 31954 } +sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/170ff04de72227f715d67da32950c7b8434449f3805b2ec3dd1085db4d7c/mkdocstrings-0.26.1.tar.gz", hash = "sha256:bb8b8854d6713d5348ad05b069a09f3b79edbc6a0f33a34c6821141adb03fe33", size = 92677 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/53/941fc52aa984f6f03b4f7473d7ec787b22076794eda40701a705cab1ab01/mkdocstrings-0.24.1-py3-none-any.whl", hash = "sha256:b4206f9a2ca8a648e222d5a0ca1d36ba7dee53c88732818de183b536f9042b5d", size = 28259 }, + { url = "https://files.pythonhosted.org/packages/23/cc/8ba127aaee5d1e9046b0d33fa5b3d17da95a9d705d44902792e0569257fd/mkdocstrings-0.26.1-py3-none-any.whl", hash = "sha256:29738bfb72b4608e8e55cc50fb8a54f325dc7ebd2014e4e3881a49892d5983cf", size = 29643 }, ] [[package]] name = "mkdocstrings-python" -version = "1.9.0" +version = "1.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffe" }, - { name = "markdown" }, + { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/ed/d65dbd6e594c9de25a94f1cb78daf6a4a82bee186945f26814e9d7c5be6b/mkdocstrings_python-1.9.0.tar.gz", hash = "sha256:6e1a442367cf75d30cf69774cbb1ad02aebec58bfff26087439df4955efecfde", size = 33732 } +sdist = { url = "https://files.pythonhosted.org/packages/fc/ba/534c934cd0a809f51c91332d6ed278782ee4126b8ba8db02c2003f162b47/mkdocstrings_python-1.11.1.tar.gz", hash = "sha256:8824b115c5359304ab0b5378a91f6202324a849e1da907a3485b59208b797322", size = 166890 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/69/07eaed30332b0bf8299d6dd172375846eebbf86608d6e1b0039777648b2f/mkdocstrings_python-1.9.0-py3-none-any.whl", hash = "sha256:fad27d7314b4ec9c0359a187b477fb94c65ef561fdae941dca1b717c59aae96f", size = 58266 }, + { url = "https://files.pythonhosted.org/packages/2f/f2/2a2c48fda645ac6bbe73bcc974587a579092b6868e6ff8bc6d177f4db38a/mkdocstrings_python-1.11.1-py3-none-any.whl", hash = "sha256:a21a1c05acef129a618517bb5aae3e33114f569b11588b1e7af3e9d4061a71af", size = 109297 }, ] [[package]] @@ -583,15 +598,15 @@ wheels = [ [[package]] name = "pymdown-extensions" -version = "10.7.1" +version = "10.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/ec/e3d966cfb286d5a48e7c43a559a297b857ab935209ee9072e5a5492706c9/pymdown_extensions-10.7.1.tar.gz", hash = "sha256:c70e146bdd83c744ffc766b4671999796aba18842b268510a329f7f64700d584", size = 811769 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/d3/fb86beeaa4416f73a28a5e8d440976b7cada2b2d7b5e715b2bd849d4de32/pymdown_extensions-10.9.tar.gz", hash = "sha256:6ff740bcd99ec4172a938970d42b96128bdc9d4b9bcad72494f29921dc69b753", size = 812128 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/4b/4ebb08f36e83e91b31117fe8f67844bac7f5c7d11ed151ae5db807be363f/pymdown_extensions-10.7.1-py3-none-any.whl", hash = "sha256:f5cc7000d7ff0d1ce9395d216017fa4df3dde800afb1fb72d1c7d3fd35e710f4", size = 250794 }, + { url = "https://files.pythonhosted.org/packages/7b/41/18b5dc5e97ec3ff1c2f51d372e570a9fbe231f1124dcc36dbc6b47f93058/pymdown_extensions-10.9-py3-none-any.whl", hash = "sha256:d323f7e90d83c86113ee78f3fe62fc9dee5f56b54d912660703ea1816fed5626", size = 250954 }, ] [[package]] @@ -650,7 +665,7 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.9" +version = "0.0.10" source = { editable = "." } [package.dev-dependencies] @@ -663,7 +678,6 @@ dev = [ { name = "mkdocs" }, { name = "mkdocs-autorefs" }, { name = "mkdocs-material" }, - { name = "mkdocstrings" }, { name = "mkdocstrings-python" }, { name = "more-itertools" }, { name = "pbr" }, @@ -685,11 +699,10 @@ dev = [ { name = "attrs", specifier = "==23.2.0" }, { name = "coverage", specifier = "==7.4.4" }, { name = "invoke", specifier = "==2.2.0" }, - { name = "mkdocs", specifier = "==1.5.3" }, - { name = "mkdocs-autorefs", specifier = "==1.0.1" }, - { name = "mkdocs-material", specifier = "==9.5.16" }, - { name = "mkdocstrings", specifier = "==0.24.1" }, - { name = "mkdocstrings-python", specifier = "==1.9.0" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings-python" }, { name = "more-itertools", specifier = "==10.2.0" }, { name = "pbr", specifier = "==6.0.0" }, { name = "pluggy", specifier = "==1.4.0" }, From 851a0263fc0052eeecdbee34331bcde2c2967e75 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 21 Sep 2024 16:13:01 +0200 Subject: [PATCH 60/94] Add entry to changelog (#157) --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23c0fff..2be283b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.0.10 (2024-09-21) + +* Support `on_header_begin` [#103](https://github.com/Kludex/python-multipart/pull/103). +* Improve type hints on `FormParser` [#104](https://github.com/Kludex/python-multipart/pull/104). +* Fix `OnFileCallback` type [#106](https://github.com/Kludex/python-multipart/pull/106). +* Improve type hints [#110](https://github.com/Kludex/python-multipart/pull/110). +* Improve type hints on `File` [#111](https://github.com/Kludex/python-multipart/pull/111). +* Add type hint to helper functions [#112](https://github.com/Kludex/python-multipart/pull/112). +* Minor fix for Field.__repr__ [#114](https://github.com/Kludex/python-multipart/pull/114). +* Fix use of chunk_size parameter [#136](https://github.com/Kludex/python-multipart/pull/136). +* Allow digits and valid token chars in headers [#134](https://github.com/Kludex/python-multipart/pull/134). +* Fix headers being carried between parts [#135](https://github.com/Kludex/python-multipart/pull/135). + ## 0.0.9 (2024-02-10) * Add support for Python 3.12 [#85](https://github.com/Kludex/python-multipart/pull/85). From dcf0ba14a5dbf23d61a9116b1ec8f60952cf485b Mon Sep 17 00:00:00 2001 From: John Stark Date: Sat, 28 Sep 2024 10:33:28 +0100 Subject: [PATCH 61/94] Handle invalid CRLF in header name. fixes #122 (#141) --- multipart/multipart.py | 2 +- tests/test_data/http/CRLF_in_header.http | 6 ++++++ tests/test_data/http/CRLF_in_header.yaml | 3 +++ tests/test_data/http/CR_in_header.yaml | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 tests/test_data/http/CRLF_in_header.http create mode 100644 tests/test_data/http/CRLF_in_header.yaml diff --git a/multipart/multipart.py b/multipart/multipart.py index 0d0ee8c..3275075 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -1163,7 +1163,7 @@ def data_callback(name: str, remaining: bool = False) -> None: # If we've reached a CR at the beginning of a header, it means # that we've reached the second of 2 newlines, and so there are # no more headers to parse. - if c == CR: + if c == CR and index == 0: delete_mark("header_field") state = MultipartState.HEADERS_ALMOST_DONE i += 1 diff --git a/tests/test_data/http/CRLF_in_header.http b/tests/test_data/http/CRLF_in_header.http new file mode 100644 index 0000000..41e9e0b --- /dev/null +++ b/tests/test_data/http/CRLF_in_header.http @@ -0,0 +1,6 @@ +------WebKitFormBoundaryTkr3kCBQlBe1nrhc +Content- +isposition: form-data; name="field" + +This is a test. +------WebKitFormBoundaryTkr3kCBQlBe1nrhc-- \ No newline at end of file diff --git a/tests/test_data/http/CRLF_in_header.yaml b/tests/test_data/http/CRLF_in_header.yaml new file mode 100644 index 0000000..9d5f62a --- /dev/null +++ b/tests/test_data/http/CRLF_in_header.yaml @@ -0,0 +1,3 @@ +boundary: ----WebKitFormBoundaryTkr3kCBQlBe1nrhc +expected: + error: 50 diff --git a/tests/test_data/http/CR_in_header.yaml b/tests/test_data/http/CR_in_header.yaml index c9b55f2..9d5f62a 100644 --- a/tests/test_data/http/CR_in_header.yaml +++ b/tests/test_data/http/CR_in_header.yaml @@ -1,3 +1,3 @@ boundary: ----WebKitFormBoundaryTkr3kCBQlBe1nrhc expected: - error: 51 + error: 50 From a790e40477bf7e4be04dd709272660a0ef62984c Mon Sep 17 00:00:00 2001 From: John Stark Date: Sat, 28 Sep 2024 11:19:11 +0100 Subject: [PATCH 62/94] Improve performance, especially in data with many CR-LF (#137) * Improve parsing content with many cr-lf Drops the look-behind buffer since the content is always the boundary. * Improve performance by using built-in bytes.find. The Boyer-Moore-Horspool algorithm was removed and replaced with Python's built-in `find` method. This appears to be faster, sometimes by an order of magnitude. * Delete unused join_bytes --------- Co-authored-by: Marcelo Trylesinski --- multipart/multipart.py | 130 +++++++++++++++++++--------------------- tests/test_multipart.py | 35 ++++++++--- 2 files changed, 89 insertions(+), 76 deletions(-) diff --git a/multipart/multipart.py b/multipart/multipart.py index 3275075..eac3ff8 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -146,10 +146,6 @@ def ord_char(c: int) -> int: return c -def join_bytes(b: bytes) -> bytes: - return bytes(list(b)) - - def parse_options_header(value: str | bytes) -> tuple[bytes, dict[bytes, bytes]]: """Parses a Content-Type header into a value in the following format: (content_type, {parameters}).""" # Uses email.message.Message to parse the header as described in PEP 594. @@ -976,29 +972,11 @@ def __init__( # Setup marks. These are used to track the state of data received. self.marks: dict[str, int] = {} - # TODO: Actually use this rather than the dumb version we currently use - # # Precompute the skip table for the Boyer-Moore-Horspool algorithm. - # skip = [len(boundary) for x in range(256)] - # for i in range(len(boundary) - 1): - # skip[ord_char(boundary[i])] = len(boundary) - i - 1 - # - # # We use a tuple since it's a constant, and marginally faster. - # self.skip = tuple(skip) - # Save our boundary. if isinstance(boundary, str): # pragma: no cover boundary = boundary.encode("latin-1") self.boundary = b"\r\n--" + boundary - # Get a set of characters that belong to our boundary. - self.boundary_chars = frozenset(self.boundary) - - # We also create a lookbehind list. - # Note: the +8 is since we can have, at maximum, "\r\n--" + boundary + - # "--\r\n" at the final boundary, and the length of '\r\n--' and - # '--\r\n' is 8 bytes. - self.lookbehind = [NULL for _ in range(len(boundary) + 8)] - def write(self, data: bytes) -> int: """Write some data to the parser, which will perform size verification, and then parse the data into the appropriate location (e.g. header, @@ -1061,21 +1039,43 @@ def delete_mark(name: str, reset: bool = False) -> None: # end of the buffer, and reset the mark, instead of deleting it. This # is used at the end of the function to call our callbacks with any # remaining data in this chunk. - def data_callback(name: str, remaining: bool = False) -> None: + def data_callback(name: str, end_i: int, remaining: bool = False) -> None: marked_index = self.marks.get(name) if marked_index is None: return - # If we're getting remaining data, we ignore the current i value - # and just call with the remaining data. - if remaining: - self.callback(name, data, marked_index, length) - self.marks[name] = 0 - # Otherwise, we call it from the mark to the current byte we're # processing. + if end_i <= marked_index: + # There is no additional data to send. + pass + elif marked_index >= 0: + # We are emitting data from the local buffer. + self.callback(name, data, marked_index, end_i) + else: + # Some of the data comes from a partial boundary match. + # and requires look-behind. + # We need to use self.flags (and not flags) because we care about + # the state when we entered the loop. + lookbehind_len = -marked_index + if lookbehind_len <= len(boundary): + self.callback(name, boundary, 0, lookbehind_len) + elif self.flags & FLAG_PART_BOUNDARY: + lookback = boundary + b"\r\n" + self.callback(name, lookback, 0, lookbehind_len) + elif self.flags & FLAG_LAST_BOUNDARY: + lookback = boundary + b"--\r\n" + self.callback(name, lookback, 0, lookbehind_len) + else: # pragma: no cover (error case) + self.logger.warning("Look-back buffer error") + + if end_i > 0: + self.callback(name, data, 0, end_i) + # If we're getting remaining data, we have got all the data we + # can be certain is not a boundary, leaving only a partial boundary match. + if remaining: + self.marks[name] = end_i - length else: - self.callback(name, data, marked_index, i) self.marks.pop(name, None) # For each byte... @@ -1183,7 +1183,7 @@ def data_callback(name: str, remaining: bool = False) -> None: raise e # Call our callback with the header field. - data_callback("header_field") + data_callback("header_field", i) # Move to parsing the header value. state = MultipartState.HEADER_VALUE_START @@ -1212,7 +1212,7 @@ def data_callback(name: str, remaining: bool = False) -> None: # If we've got a CR, we're nearly done our headers. Otherwise, # we do nothing and just move past this character. if c == CR: - data_callback("header_value") + data_callback("header_value", i) self.callback("header_end") state = MultipartState.HEADER_VALUE_ALMOST_DONE @@ -1256,9 +1256,6 @@ def data_callback(name: str, remaining: bool = False) -> None: # We're processing our part data right now. During this, we # need to efficiently search for our boundary, since any data # on any number of lines can be a part of the current data. - # We use the Boyer-Moore-Horspool algorithm to efficiently - # search through the remainder of the buffer looking for our - # boundary. # Save the current value of our index. We use this in case we # find part of a boundary, but it doesn't match fully. @@ -1266,24 +1263,32 @@ def data_callback(name: str, remaining: bool = False) -> None: # Set up variables. boundary_length = len(boundary) - boundary_end = boundary_length - 1 data_length = length - boundary_chars = self.boundary_chars # If our index is 0, we're starting a new part, so start our # search. if index == 0: - # Search forward until we either hit the end of our buffer, - # or reach a character that's in our boundary. - i += boundary_end - while i < data_length - 1 and data[i] not in boundary_chars: - i += boundary_length - - # Reset i back the length of our boundary, which is the - # earliest possible location that could be our match (i.e. - # if we've just broken out of our loop since we saw the - # last character in our boundary) - i -= boundary_end + # The most common case is likely to be that the whole + # boundary is present in the buffer. + # Calling `find` is much faster than iterating here. + i0 = data.find(boundary, i, data_length) + if i0 >= 0: + # We matched the whole boundary string. + index = boundary_length - 1 + i = i0 + boundary_length - 1 + else: + # No match found for whole string. + # There may be a partial boundary at the end of the + # data, which the find will not match. + # Since the length should to be searched is limited to + # the boundary length, just perform a naive search. + i = max(i, data_length - boundary_length) + + # Search forward until we either hit the end of our buffer, + # or reach a potential start of the boundary. + while i < data_length - 1 and data[i] != boundary[0]: + i += 1 + c = data[i] # Now, we have a couple of cases here. If our index is before @@ -1291,11 +1296,6 @@ def data_callback(name: str, remaining: bool = False) -> None: if index < boundary_length: # If the character matches... if boundary[index] == c: - # If we found a match for our boundary, we send the - # existing data. - if index == 0: - data_callback("part_data") - # The current character matches, so continue! index += 1 else: @@ -1332,6 +1332,8 @@ def data_callback(name: str, remaining: bool = False) -> None: # Unset the part boundary flag. flags &= ~FLAG_PART_BOUNDARY + # We have identified a boundary, callback for any data before it. + data_callback("part_data", i - index) # Callback indicating that we've reached the end of # a part, and are starting a new one. self.callback("part_end") @@ -1353,6 +1355,8 @@ def data_callback(name: str, remaining: bool = False) -> None: elif flags & FLAG_LAST_BOUNDARY: # We need a second hyphen here. if c == HYPHEN: + # We have identified a boundary, callback for any data before it. + data_callback("part_data", i - index) # Callback to end the current part, and then the # message. self.callback("part_end") @@ -1362,26 +1366,14 @@ def data_callback(name: str, remaining: bool = False) -> None: # No match, so reset index. index = 0 - # If we have an index, we need to keep this byte for later, in - # case we can't match the full boundary. - if index > 0: - self.lookbehind[index - 1] = c - # Otherwise, our index is 0. If the previous index is not, it # means we reset something, and we need to take the data we # thought was part of our boundary and send it along as actual # data. - elif prev_index > 0: - # Callback to write the saved data. - lb_data = join_bytes(self.lookbehind) - self.callback("part_data", lb_data, 0, prev_index) - + if index == 0 and prev_index > 0: # Overwrite our previous index. prev_index = 0 - # Re-set our mark for part data. - set_mark("part_data") - # Re-consider the current character, since this could be # the start of the boundary itself. i -= 1 @@ -1410,9 +1402,9 @@ def data_callback(name: str, remaining: bool = False) -> None: # that we haven't yet reached the end of this 'thing'. So, by setting # the mark to 0, we cause any data callbacks that take place in future # calls to this function to start from the beginning of that buffer. - data_callback("header_field", True) - data_callback("header_value", True) - data_callback("part_data", True) + data_callback("header_field", length, True) + data_callback("header_value", length, True) + data_callback("part_data", length - index, True) # Save values to locals. self.state = state diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 3a814fb..2e22812 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -695,6 +695,14 @@ def test_not_aligned(self): http_tests.append({"name": fname, "test": test_data, "result": yaml_data}) +# Datasets used for single-byte writing test. +single_byte_tests = [ + "almost_match_boundary", + "almost_match_boundary_without_CR", + "almost_match_boundary_without_LF", + "almost_match_boundary_without_final_hyphen", + "single_field_single_file", +] def split_all(val): """ @@ -843,17 +851,19 @@ def test_random_splitting(self): self.assert_field(b"field", b"test1") self.assert_file(b"file", b"file.txt", b"test2") - def test_feed_single_bytes(self): + @parametrize("param", [ t for t in http_tests if t["name"] in single_byte_tests]) + def test_feed_single_bytes(self, param): """ - This test parses a simple multipart body 1 byte at a time. + This test parses multipart bodies 1 byte at a time. """ # Load test data. - test_file = "single_field_single_file.http" + test_file = param["name"] + ".http" + boundary = param["result"]["boundary"] with open(os.path.join(http_tests_dir, test_file), "rb") as f: test_data = f.read() # Create form parser. - self.make("boundary") + self.make(boundary) # Write all bytes. # NOTE: Can't simply do `for b in test_data`, since that gives @@ -868,9 +878,20 @@ def test_feed_single_bytes(self): # Assert we processed everything. self.assertEqual(i, len(test_data)) - # Assert that our file and field are here. - self.assert_field(b"field", b"test1") - self.assert_file(b"file", b"file.txt", b"test2") + # Assert that the parser gave us the appropriate fields/files. + for e in param["result"]["expected"]: + # Get our type and name. + type = e["type"] + name = e["name"].encode("latin-1") + + if type == "field": + self.assert_field(name, e["data"]) + + elif type == "file": + self.assert_file(name, e["file_name"].encode("latin-1"), e["data"]) + + else: + assert False def test_feed_blocks(self): """ From 293ea342c64328819862259877c0d3b7af4f3734 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 28 Sep 2024 12:24:15 +0200 Subject: [PATCH 63/94] Version 0.0.11 (#158) --- CHANGELOG.md | 5 +++++ multipart/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2be283b..da7ae82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.0.11 (2024-09-28) + +* Improve performance, especially in data with many CR-LF [#137](https://github.com/Kludex/python-multipart/pull/137). +* Handle invalid CRLF in header name [#141](https://github.com/Kludex/python-multipart/pull/141). + ## 0.0.10 (2024-09-21) * Support `on_header_begin` [#103](https://github.com/Kludex/python-multipart/pull/103). diff --git a/multipart/__init__.py b/multipart/__init__.py index a3c7229..a813076 100644 --- a/multipart/__init__.py +++ b/multipart/__init__.py @@ -2,7 +2,7 @@ __author__ = "Andrew Dunham" __license__ = "Apache" __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" -__version__ = "0.0.10" +__version__ = "0.0.11" from .multipart import ( BaseParser, From 24d5f5749766f0bfec1a097336fed78a4cddf3fa Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 29 Sep 2024 09:16:14 +0200 Subject: [PATCH 64/94] Enforce 100% coverage (#159) --- .github/workflows/main.yml | 6 +++--- multipart/decoders.py | 2 +- multipart/multipart.py | 12 ++---------- pyproject.toml | 2 +- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c7b7129..9b5ed27 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,12 +27,12 @@ jobs: - name: Install dependencies run: uv sync --python ${{ matrix.python-version }} --frozen - - name: Run tests - run: scripts/test - - name: Run linters run: scripts/lint + - name: Run tests + run: scripts/test + # https://github.com/marketplace/actions/alls-green#why used for branch protection checks check: if: always() diff --git a/multipart/decoders.py b/multipart/decoders.py index 218abe4..135c56c 100644 --- a/multipart/decoders.py +++ b/multipart/decoders.py @@ -160,7 +160,7 @@ def finalize(self) -> None: call it. """ # If we have a cache, write and then remove it. - if len(self.cache) > 0: + if len(self.cache) > 0: # pragma: no cover self.underlying.write(binascii.a2b_qp(self.cache)) self.cache = b"" diff --git a/multipart/multipart.py b/multipart/multipart.py index eac3ff8..18d0f1d 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -142,10 +142,6 @@ class MultipartState(IntEnum): # fmt: on -def ord_char(c: int) -> int: - return c - - def parse_options_header(value: str | bytes) -> tuple[bytes, dict[bytes, bytes]]: """Parses a Content-Type header into a value in the following format: (content_type, {parameters}).""" # Uses email.message.Message to parse the header as described in PEP 594. @@ -473,7 +469,7 @@ def _get_disk_file(self) -> io.BufferedRandom | tempfile._TemporaryFileWrapper[b elif isinstance(file_dir, bytes): dir = file_dir.decode(sys.getfilesystemencoding()) else: - dir = file_dir + dir = file_dir # pragma: no cover # Create a temporary (named) file with the appropriate settings. self.logger.info( @@ -511,11 +507,7 @@ def on_data(self, data: bytes) -> int: Returns: The number of bytes written. """ - pos = self._fileobj.tell() bwritten = self._fileobj.write(data) - # true file objects write returns None - if bwritten is None: - bwritten = self._fileobj.tell() - pos # If the bytes written isn't the same as the length, just return. if bwritten != len(data): @@ -1381,7 +1373,7 @@ def data_callback(name: str, end_i: int, remaining: bool = False) -> None: elif state == MultipartState.END: # Do nothing and just consume a byte in the end state. if c not in (CR, LF): - self.logger.warning("Consuming a byte '0x%x' in the end state", c) + self.logger.warning("Consuming a byte '0x%x' in the end state", c) # pragma: no cover else: # pragma: no cover (error case) # We got into a strange state somehow! Just stop processing. diff --git a/pyproject.toml b/pyproject.toml index bc29d3e..f672c70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ branch = false omit = ["tests/*"] [tool.coverage.report] -# fail_under = 100 +fail_under = 100 skip_covered = true show_missing = true exclude_lines = [ From a169d93db50853105cf912653de91f5d4a790db2 Mon Sep 17 00:00:00 2001 From: John Stark Date: Sun, 29 Sep 2024 08:45:57 +0100 Subject: [PATCH 65/94] Add mypy strict typing (#140) * No errors with mypy --strict * Apply ruff formatting * Add py.typed file * Make it more modern * Add strict mode to mypy * Use --with instead of --from --------- Co-authored-by: Marcelo Trylesinski --- .github/workflows/main.yml | 2 +- .gitignore | 1 + multipart/decoders.py | 23 ++- multipart/multipart.py | 172 +++++++++++------- multipart/py.typed | 0 pyproject.toml | 5 + scripts/README.md | 8 + scripts/check | 9 + scripts/setup | 3 + tests/compat.py | 26 ++- tests/test_multipart.py | 351 +++++++++++++++++++------------------ uv.lock | 63 ++++++- 12 files changed, 412 insertions(+), 251 deletions(-) create mode 100644 multipart/py.typed create mode 100644 scripts/README.md create mode 100755 scripts/check create mode 100755 scripts/setup diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9b5ed27..1881a56 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,7 @@ jobs: run: uv sync --python ${{ matrix.python-version }} --frozen - name: Run linters - run: scripts/lint + run: scripts/check - name: Run tests run: scripts/test diff --git a/.gitignore b/.gitignore index f52a6b1..8c8a694 100644 --- a/.gitignore +++ b/.gitignore @@ -89,6 +89,7 @@ coverage.xml *.py,cover .hypothesis/ .pytest_cache/ +.ruff_cache/ cover/ # Translations diff --git a/multipart/decoders.py b/multipart/decoders.py index 135c56c..07bf742 100644 --- a/multipart/decoders.py +++ b/multipart/decoders.py @@ -1,9 +1,22 @@ import base64 import binascii -from io import BufferedWriter +from typing import TYPE_CHECKING from .exceptions import DecodeError +if TYPE_CHECKING: # pragma: no cover + from typing import Protocol, TypeVar + + _T_contra = TypeVar("_T_contra", contravariant=True) + + class SupportsWrite(Protocol[_T_contra]): + def write(self, __b: _T_contra) -> object: ... + + # No way to specify optional methods. See + # https://github.com/python/typing/issues/601 + # close() [Optional] + # finalize() [Optional] + class Base64Decoder: """This object provides an interface to decode a stream of Base64 data. It @@ -34,7 +47,7 @@ class Base64Decoder: :param underlying: the underlying object to pass writes to """ - def __init__(self, underlying: BufferedWriter): + def __init__(self, underlying: "SupportsWrite[bytes]") -> None: self.cache = bytearray() self.underlying = underlying @@ -67,9 +80,9 @@ def write(self, data: bytes) -> int: # Get the remaining bytes and save in our cache. remaining_len = len(data) % 4 if remaining_len > 0: - self.cache = data[-remaining_len:] + self.cache[:] = data[-remaining_len:] else: - self.cache = b"" + self.cache[:] = b"" # Return the length of the data to indicate no error. return len(data) @@ -112,7 +125,7 @@ class QuotedPrintableDecoder: :param underlying: the underlying object to pass writes to """ - def __init__(self, underlying: BufferedWriter) -> None: + def __init__(self, underlying: "SupportsWrite[bytes]") -> None: self.cache = b"" self.underlying = underlying diff --git a/multipart/multipart.py b/multipart/multipart.py index 18d0f1d..137d6e7 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -1,6 +1,5 @@ from __future__ import annotations -import io import logging import os import shutil @@ -8,15 +7,20 @@ import tempfile from email.message import Message from enum import IntEnum -from io import BytesIO +from io import BufferedRandom, BytesIO from numbers import Number -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, cast from .decoders import Base64Decoder, QuotedPrintableDecoder from .exceptions import FileError, FormParserError, MultipartParseError, QuerystringParseError if TYPE_CHECKING: # pragma: no cover - from typing import Callable, Protocol, TypedDict + from typing import Any, Callable, Literal, Protocol, TypedDict + + from typing_extensions import TypeAlias + + class SupportsRead(Protocol): + def read(self, __n: int) -> bytes: ... class QuerystringCallbacks(TypedDict, total=False): on_field_start: Callable[[], None] @@ -64,7 +68,7 @@ def finalize(self) -> None: ... def close(self) -> None: ... class FieldProtocol(_FormProtocol, Protocol): - def __init__(self, name: bytes) -> None: ... + def __init__(self, name: bytes | None) -> None: ... def set_none(self) -> None: ... @@ -74,6 +78,23 @@ def __init__(self, file_name: bytes | None, field_name: bytes | None, config: Fi OnFieldCallback = Callable[[FieldProtocol], None] OnFileCallback = Callable[[FileProtocol], None] + CallbackName: TypeAlias = Literal[ + "start", + "data", + "end", + "field_start", + "field_name", + "field_data", + "field_end", + "part_begin", + "part_data", + "part_end", + "header_begin", + "header_field", + "header_value", + "header_end", + "headers_finished", + ] # Unique missing object. _missing = object() @@ -142,7 +163,7 @@ class MultipartState(IntEnum): # fmt: on -def parse_options_header(value: str | bytes) -> tuple[bytes, dict[bytes, bytes]]: +def parse_options_header(value: str | bytes | None) -> tuple[bytes, dict[bytes, bytes]]: """Parses a Content-Type header into a value in the following format: (content_type, {parameters}).""" # Uses email.message.Message to parse the header as described in PEP 594. # Ref: https://peps.python.org/pep-0594/#cgi @@ -202,7 +223,7 @@ class Field: name: The name of the form field. """ - def __init__(self, name: bytes) -> None: + def __init__(self, name: bytes | None) -> None: self._name = name self._value: list[bytes] = [] @@ -283,7 +304,7 @@ def set_none(self) -> None: self._cache = None @property - def field_name(self) -> bytes: + def field_name(self) -> bytes | None: """This property returns the name of the field.""" return self._name @@ -293,6 +314,7 @@ def value(self) -> bytes | None: if self._cache is _missing: self._cache = b"".join(self._value) + assert isinstance(self._cache, bytes) or self._cache is None return self._cache def __eq__(self, other: object) -> bool: @@ -341,7 +363,7 @@ def __init__(self, file_name: bytes | None, field_name: bytes | None = None, con self._config = config self._in_memory = True self._bytes_written = 0 - self._fileobj = BytesIO() + self._fileobj: BytesIO | BufferedRandom = BytesIO() # Save the provided field/file name. self._field_name = field_name @@ -349,7 +371,7 @@ def __init__(self, file_name: bytes | None, field_name: bytes | None = None, con # Our actual file name is None by default, since, depending on our # config, we may not actually use the provided name. - self._actual_file_name = None + self._actual_file_name: bytes | None = None # Split the extension from the filename. if file_name is not None: @@ -370,14 +392,14 @@ def file_name(self) -> bytes | None: return self._file_name @property - def actual_file_name(self): + def actual_file_name(self) -> bytes | None: """The file name that this file is saved as. Will be None if it's not currently saved on disk. """ return self._actual_file_name @property - def file_object(self): + def file_object(self) -> BytesIO | BufferedRandom: """The file object that we're currently writing to. Note that this will either be an instance of a :class:`io.BytesIO`, or a regular file object. @@ -432,7 +454,7 @@ def flush_to_disk(self) -> None: # Close the old file object. old_fileobj.close() - def _get_disk_file(self) -> io.BufferedRandom | tempfile._TemporaryFileWrapper[bytes]: # type: ignore[reportPrivateUsage] + def _get_disk_file(self) -> BufferedRandom: """This function is responsible for getting a file object on-disk for us.""" self.logger.info("Opening a file on disk") @@ -440,6 +462,7 @@ def _get_disk_file(self) -> io.BufferedRandom | tempfile._TemporaryFileWrapper[b keep_filename = self._config.get("UPLOAD_KEEP_FILENAME", False) keep_extensions = self._config.get("UPLOAD_KEEP_EXTENSIONS", False) delete_tmp = self._config.get("UPLOAD_DELETE_TMP", True) + tmp_file: None | BufferedRandom = None # If we have a directory and are to keep the filename... if file_dir is not None and keep_filename: @@ -449,7 +472,7 @@ def _get_disk_file(self) -> io.BufferedRandom | tempfile._TemporaryFileWrapper[b # TODO: what happens if we don't have a filename? fname = self._file_base + self._ext if keep_extensions else self._file_base - path = os.path.join(file_dir, fname) + path = os.path.join(file_dir, fname) # type: ignore[arg-type] try: self.logger.info("Opening file: %r", path) tmp_file = open(path, "w+b") @@ -476,16 +499,17 @@ def _get_disk_file(self) -> io.BufferedRandom | tempfile._TemporaryFileWrapper[b "Creating a temporary file with options: %r", {"suffix": suffix, "delete": delete_tmp, "dir": dir} ) try: - tmp_file = tempfile.NamedTemporaryFile(suffix=suffix, delete=delete_tmp, dir=dir) + tmp_file = cast(BufferedRandom, tempfile.NamedTemporaryFile(suffix=suffix, delete=delete_tmp, dir=dir)) except OSError: self.logger.exception("Error creating named temporary file") raise FileError("Error creating named temporary file") - fname = tmp_file.name - + assert tmp_file is not None # Encode filename as bytes. - if isinstance(fname, str): - fname = fname.encode(sys.getfilesystemencoding()) + if isinstance(tmp_file.name, str): + fname = tmp_file.name.encode(sys.getfilesystemencoding()) + else: + fname = cast(bytes, tmp_file.name) # pragma: no cover self._actual_file_name = fname return tmp_file @@ -571,8 +595,11 @@ class BaseParser: def __init__(self) -> None: self.logger = logging.getLogger(__name__) + self.callbacks: QuerystringCallbacks | OctetStreamCallbacks | MultipartCallbacks = {} - def callback(self, name: str, data: bytes | None = None, start: int | None = None, end: int | None = None): + def callback( + self, name: CallbackName, data: bytes | None = None, start: int | None = None, end: int | None = None + ) -> None: """This function calls a provided callback with some data. If the callback is not set, will do nothing. @@ -583,24 +610,24 @@ def callback(self, name: str, data: bytes | None = None, start: int | None = Non end: An integer that is passed to the data callback. start: An integer that is passed to the data callback. """ - name = "on_" + name - func = self.callbacks.get(name) + on_name = "on_" + name + func = self.callbacks.get(on_name) if func is None: return - + func = cast("Callable[..., Any]", func) # Depending on whether we're given a buffer... if data is not None: # Don't do anything if we have start == end. if start is not None and start == end: return - self.logger.debug("Calling %s with data[%d:%d]", name, start, end) + self.logger.debug("Calling %s with data[%d:%d]", on_name, start, end) func(data, start, end) else: - self.logger.debug("Calling %s with no data", name) + self.logger.debug("Calling %s with no data", on_name) func() - def set_callback(self, name: str, new_func: Callable[..., Any] | None) -> None: + def set_callback(self, name: CallbackName, new_func: Callable[..., Any] | None) -> None: """Update the function for a callback. Removes from the callbacks dict if new_func is None. @@ -611,17 +638,17 @@ def set_callback(self, name: str, new_func: Callable[..., Any] | None) -> None: exist). """ if new_func is None: - self.callbacks.pop("on_" + name, None) + self.callbacks.pop("on_" + name, None) # type: ignore[misc] else: - self.callbacks["on_" + name] = new_func + self.callbacks["on_" + name] = new_func # type: ignore[literal-required] - def close(self): + def close(self) -> None: pass # pragma: no cover - def finalize(self): + def finalize(self) -> None: pass # pragma: no cover - def __repr__(self): + def __repr__(self) -> str: return "%s()" % self.__class__.__name__ @@ -647,7 +674,7 @@ def __init__(self, callbacks: OctetStreamCallbacks = {}, max_size: float = float if not isinstance(max_size, Number) or max_size < 1: raise ValueError("max_size must be a positive number, not %r" % max_size) - self.max_size = max_size + self.max_size: int | float = max_size self._current_size = 0 def write(self, data: bytes) -> int: @@ -729,7 +756,7 @@ def __init__( # Max-size stuff if not isinstance(max_size, Number) or max_size < 1: raise ValueError("max_size must be a positive number, not %r" % max_size) - self.max_size = max_size + self.max_size: int | float = max_size self._current_size = 0 # Should parsing be strict? @@ -1019,7 +1046,7 @@ def _internal_write(self, data: bytes, length: int) -> int: i = 0 # Set a mark. - def set_mark(name: str): + def set_mark(name: str) -> None: self.marks[name] = i # Remove a mark. @@ -1031,7 +1058,7 @@ def delete_mark(name: str, reset: bool = False) -> None: # end of the buffer, and reset the mark, instead of deleting it. This # is used at the end of the function to call our callbacks with any # remaining data in this chunk. - def data_callback(name: str, end_i: int, remaining: bool = False) -> None: + def data_callback(name: CallbackName, end_i: int, remaining: bool = False) -> None: marked_index = self.marks.get(name) if marked_index is None: return @@ -1471,8 +1498,8 @@ class if you wish to customize behaviour. The class will be instantiated as Fie def __init__( self, content_type: str, - on_field: OnFieldCallback, - on_file: OnFileCallback, + on_field: OnFieldCallback | None, + on_file: OnFileCallback | None, on_end: Callable[[], None] | None = None, boundary: bytes | str | None = None, file_name: bytes | None = None, @@ -1498,8 +1525,10 @@ def __init__( self.FieldClass = Field # Set configuration options. - self.config = self.DEFAULT_CONFIG.copy() - self.config.update(config) + self.config: FormParserConfig = self.DEFAULT_CONFIG.copy() + self.config.update(config) # type: ignore[typeddict-item] + + parser: OctetStreamParser | MultipartParser | QuerystringParser | None = None # Depending on the Content-Type, we instantiate the correct parser. if content_type == "application/octet-stream": @@ -1507,7 +1536,7 @@ def __init__( def on_start() -> None: nonlocal file - file = FileClass(file_name, None, config=self.config) + file = FileClass(file_name, None, config=cast("FileConfig", self.config)) def on_data(data: bytes, start: int, end: int) -> None: nonlocal file @@ -1519,7 +1548,8 @@ def _on_end() -> None: file.finalize() # Call our callback. - on_file(file) + if on_file: + on_file(file) # Call the on-end callback. if self.on_end is not None: @@ -1534,7 +1564,7 @@ def _on_end() -> None: elif content_type == "application/x-www-form-urlencoded" or content_type == "application/x-url-encoded": name_buffer: list[bytes] = [] - f: FieldProtocol = None # type: ignore + f: FieldProtocol | None = None def on_field_start() -> None: pass @@ -1560,7 +1590,8 @@ def on_field_end() -> None: f.set_none() f.finalize() - on_field(f) + if on_field: + on_field(f) f = None def _on_end() -> None: @@ -1586,30 +1617,33 @@ def _on_end() -> None: header_name: list[bytes] = [] header_value: list[bytes] = [] - headers = {} + headers: dict[bytes, bytes] = {} - f: FileProtocol | FieldProtocol | None = None + f_multi: FileProtocol | FieldProtocol | None = None writer = None is_file = False - def on_part_begin(): + def on_part_begin() -> None: # Reset headers in case this isn't the first part. nonlocal headers headers = {} def on_part_data(data: bytes, start: int, end: int) -> None: nonlocal writer - bytes_processed = writer.write(data[start:end]) + assert writer is not None + writer.write(data[start:end]) # TODO: check for error here. - return bytes_processed def on_part_end() -> None: - nonlocal f, is_file - f.finalize() + nonlocal f_multi, is_file + assert f_multi is not None + f_multi.finalize() if is_file: - on_file(f) + if on_file: + on_file(f_multi) else: - on_field(f) + if on_field: + on_field(cast("FieldProtocol", f_multi)) def on_header_field(data: bytes, start: int, end: int) -> None: header_name.append(data[start:end]) @@ -1623,7 +1657,7 @@ def on_header_end() -> None: del header_value[:] def on_headers_finished() -> None: - nonlocal is_file, f, writer + nonlocal is_file, f_multi, writer # Reset the 'is file' flag. is_file = False @@ -1639,9 +1673,9 @@ def on_headers_finished() -> None: # Create the proper class. if file_name is None: - f = FieldClass(field_name) + f_multi = FieldClass(field_name) else: - f = FileClass(file_name, field_name, config=self.config) + f_multi = FileClass(file_name, field_name, config=cast("FileConfig", self.config)) is_file = True # Parse the given Content-Transfer-Encoding to determine what @@ -1650,25 +1684,26 @@ def on_headers_finished() -> None: transfer_encoding = headers.get(b"Content-Transfer-Encoding", b"7bit") if transfer_encoding in (b"binary", b"8bit", b"7bit"): - writer = f + writer = f_multi elif transfer_encoding == b"base64": - writer = Base64Decoder(f) + writer = Base64Decoder(f_multi) elif transfer_encoding == b"quoted-printable": - writer = QuotedPrintableDecoder(f) + writer = QuotedPrintableDecoder(f_multi) else: self.logger.warning("Unknown Content-Transfer-Encoding: %r", transfer_encoding) if self.config["UPLOAD_ERROR_ON_BAD_CTE"]: - raise FormParserError('Unknown Content-Transfer-Encoding "{}"'.format(transfer_encoding)) + raise FormParserError('Unknown Content-Transfer-Encoding "{!r}"'.format(transfer_encoding)) else: # If we aren't erroring, then we just treat this as an # unencoded Content-Transfer-Encoding. - writer = f + writer = f_multi def _on_end() -> None: nonlocal writer + assert writer is not None writer.finalize() if self.on_end is not None: self.on_end() @@ -1707,6 +1742,7 @@ def write(self, data: bytes) -> int: """ self.bytes_received += len(data) # TODO: check the parser's return value for errors? + assert self.parser is not None return self.parser.write(data) def finalize(self) -> None: @@ -1725,8 +1761,8 @@ def __repr__(self) -> str: def create_form_parser( headers: dict[str, bytes], - on_field: OnFieldCallback, - on_file: OnFileCallback, + on_field: OnFieldCallback | None, + on_file: OnFileCallback | None, trust_x_headers: bool = False, config: dict[Any, Any] = {}, ) -> FormParser: @@ -1744,7 +1780,7 @@ def create_form_parser( name from X-File-Name. config: Configuration variables to pass to the FormParser. """ - content_type = headers.get("Content-Type") + content_type: str | bytes | None = headers.get("Content-Type") if content_type is None: logging.getLogger(__name__).warning("No Content-Type header given") raise ValueError("No Content-Type header given!") @@ -1769,9 +1805,9 @@ def create_form_parser( def parse_form( headers: dict[str, bytes], - input_stream: io.FileIO, - on_field: OnFieldCallback, - on_file: OnFileCallback, + input_stream: SupportsRead, + on_field: OnFieldCallback | None, + on_file: OnFileCallback | None, chunk_size: int = 1048576, ) -> None: """This function is useful if you just want to parse a request body, @@ -1792,7 +1828,7 @@ def parse_form( # Read chunks of 1MiB and write to the parser, but never read more than # the given Content-Length, if any. - content_length = headers.get("Content-Length") + content_length: int | float | bytes | None = headers.get("Content-Length") if content_length is not None: content_length = int(content_length) else: @@ -1801,7 +1837,7 @@ def parse_form( while True: # Read only up to the Content-Length given. - max_readable = min(content_length - bytes_read, chunk_size) + max_readable = int(min(content_length - bytes_read, chunk_size)) buff = input_stream.read(max_readable) # Write to the parser and update our length. diff --git a/multipart/py.typed b/multipart/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml index f672c70..fb03f83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,8 @@ dev-dependencies = [ "invoke==2.2.0", "pytest-timeout==2.3.1", "ruff==0.3.4", + "mypy", + "types-PyYAML", "atheris==2.3.0; python_version != '3.12'", # Documentation "mkdocs", @@ -68,6 +70,9 @@ packages = ["multipart"] [tool.hatch.build.targets.sdist] include = ["/multipart", "/tests", "CHANGELOG.md", "LICENSE.txt"] +[tool.mypy] +strict = true + [tool.ruff] line-length = 120 diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..1742ebd --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,8 @@ +# Development Scripts + +* `scripts/setup` - Install dependencies. +* `scripts/test` - Run the test suite. +* `scripts/lint` - Run the code format. +* `scripts/check` - Run the lint in check mode, and the type checker. + +Styled after GitHub's ["Scripts to Rule Them All"](https://github.com/github/scripts-to-rule-them-all). diff --git a/scripts/check b/scripts/check new file mode 100755 index 0000000..0b6a294 --- /dev/null +++ b/scripts/check @@ -0,0 +1,9 @@ +#!/bin/sh -e + +set -x + +SOURCE_FILES="multipart tests" + +uvx ruff format --check --diff $SOURCE_FILES +uvx ruff check $SOURCE_FILES +uvx --with types-PyYAML mypy $SOURCE_FILES diff --git a/scripts/setup b/scripts/setup new file mode 100755 index 0000000..33797fc --- /dev/null +++ b/scripts/setup @@ -0,0 +1,3 @@ +#!/bin/sh -ex + +uv sync --frozen diff --git a/tests/compat.py b/tests/compat.py index 845a926..2253107 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -1,18 +1,24 @@ +from __future__ import annotations + import functools import os import re import sys import types +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable -def ensure_in_path(path): +def ensure_in_path(path: str) -> None: """ Ensure that a given path is in the sys.path array """ if not os.path.isdir(path): raise RuntimeError("Tried to add nonexisting path") - def _samefile(x, y): + def _samefile(x: str, y: str) -> bool: try: return os.path.samefile(x, y) except OSError: @@ -34,7 +40,7 @@ def _samefile(x, y): # We don't use the pytest parametrizing function, since it seems to break # with unittest.TestCase subclasses. -def parametrize(field_names, field_values): +def parametrize(field_names: tuple[str] | list[str] | str, field_values: list[Any] | Any) -> Callable[..., Any]: # If we're not given a list of field names, we make it. if not isinstance(field_names, (tuple, list)): field_names = (field_names,) @@ -42,7 +48,7 @@ def parametrize(field_names, field_values): # Create a decorator that saves this list of field names and values on the # function for later parametrizing. - def decorator(func): + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: func.__dict__["param_names"] = field_names func.__dict__["param_values"] = field_values return func @@ -54,7 +60,7 @@ def decorator(func): class ParametrizingMetaclass(type): IDENTIFIER_RE = re.compile("[^A-Za-z0-9]") - def __new__(klass, name, bases, attrs): + def __new__(klass, name: str, bases: tuple[type, ...], attrs: types.MappingProxyType[str, Any]) -> type: new_attrs = attrs.copy() for attr_name, attr in attrs.items(): # We only care about functions @@ -67,7 +73,7 @@ def __new__(klass, name, bases, attrs): continue # Create multiple copies of the function. - for i, values in enumerate(param_values): + for _, values in enumerate(param_values): assert len(param_names) == len(values) # Get a repr of the values, and fix it to be a valid identifier @@ -78,12 +84,14 @@ def __new__(klass, name, bases, attrs): new_name = attr.__name__ + "__" + human # Create a replacement function. - def create_new_func(func, names, values): + def create_new_func( + func: types.FunctionType, names: list[str], values: list[Any] + ) -> Callable[..., Any]: # Create a kwargs dictionary. kwargs = dict(zip(names, values)) @functools.wraps(func) - def new_func(self): + def new_func(self: types.FunctionType) -> Any: return func(self, **kwargs) # Manually set the name and return the new function. @@ -104,5 +112,5 @@ def new_func(self): # This is a class decorator that actually applies the above metaclass. -def parametrize_class(klass): +def parametrize_class(klass: type) -> ParametrizingMetaclass: return ParametrizingMetaclass(klass.__name__, klass.__bases__, klass.__dict__) diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 2e22812..f55e228 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -6,13 +6,13 @@ import tempfile import unittest from io import BytesIO -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from unittest.mock import Mock import yaml from multipart.decoders import Base64Decoder, QuotedPrintableDecoder -from multipart.exceptions import DecodeError, FileError, FormParserError, MultipartParseError +from multipart.exceptions import DecodeError, FileError, FormParserError, MultipartParseError, QuerystringParseError from multipart.multipart import ( BaseParser, Field, @@ -20,7 +20,6 @@ FormParser, MultipartParser, OctetStreamParser, - QuerystringParseError, QuerystringParser, create_form_parser, parse_form, @@ -30,13 +29,21 @@ from .compat import parametrize, parametrize_class if TYPE_CHECKING: - from multipart.multipart import FileConfig + from typing import Any, Iterator, TypedDict + + from multipart.multipart import FieldProtocol, FileConfig, FileProtocol + + class TestParams(TypedDict): + name: str + test: bytes + result: Any + # Get the current directory for our later test cases. curr_dir = os.path.abspath(os.path.dirname(__file__)) -def force_bytes(val): +def force_bytes(val: str | bytes) -> bytes: if isinstance(val, str): val = val.encode(sys.getfilesystemencoding()) @@ -44,33 +51,33 @@ def force_bytes(val): class TestField(unittest.TestCase): - def setUp(self): - self.f = Field("foo") + def setUp(self) -> None: + self.f = Field(b"foo") - def test_name(self): - self.assertEqual(self.f.field_name, "foo") + def test_name(self) -> None: + self.assertEqual(self.f.field_name, b"foo") - def test_data(self): + def test_data(self) -> None: self.f.write(b"test123") self.assertEqual(self.f.value, b"test123") - def test_cache_expiration(self): + def test_cache_expiration(self) -> None: self.f.write(b"test") self.assertEqual(self.f.value, b"test") self.f.write(b"123") self.assertEqual(self.f.value, b"test123") - def test_finalize(self): + def test_finalize(self) -> None: self.f.write(b"test123") self.f.finalize() self.assertEqual(self.f.value, b"test123") - def test_close(self): + def test_close(self) -> None: self.f.write(b"test123") self.f.close() self.assertEqual(self.f.value, b"test123") - def test_from_value(self): + def test_from_value(self) -> None: f = Field.from_value(b"name", b"value") self.assertEqual(f.field_name, b"name") self.assertEqual(f.value, b"value") @@ -78,18 +85,18 @@ def test_from_value(self): f2 = Field.from_value(b"name", None) self.assertEqual(f2.value, None) - def test_equality(self): + def test_equality(self) -> None: f1 = Field.from_value(b"name", b"value") f2 = Field.from_value(b"name", b"value") self.assertEqual(f1, f2) - def test_equality_with_other(self): + def test_equality_with_other(self) -> None: f = Field.from_value(b"foo", b"bar") self.assertFalse(f == b"foo") self.assertFalse(b"foo" == f) - def test_set_none(self): + def test_set_none(self) -> None: f = Field(b"foo") self.assertEqual(f.value, b"") @@ -98,34 +105,35 @@ def test_set_none(self): class TestFile(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: self.c: FileConfig = {} self.d = force_bytes(tempfile.mkdtemp()) self.f = File(b"foo.txt", config=self.c) - def assert_data(self, data): + def assert_data(self, data: bytes) -> None: f = self.f.file_object f.seek(0) self.assertEqual(f.read(), data) f.seek(0) f.truncate() - def assert_exists(self): + def assert_exists(self) -> None: + assert self.f.actual_file_name is not None full_path = os.path.join(self.d, self.f.actual_file_name) self.assertTrue(os.path.exists(full_path)) - def test_simple(self): + def test_simple(self) -> None: self.f.write(b"foobar") self.assert_data(b"foobar") - def test_invalid_write(self): + def test_invalid_write(self) -> None: m = Mock() m.write.return_value = 5 self.f._fileobj = m v = self.f.write(b"foobar") self.assertEqual(v, 5) - def test_file_fallback(self): + def test_file_fallback(self) -> None: self.c["MAX_MEMORY_FILE_SIZE"] = 1 self.f.write(b"1") @@ -142,7 +150,7 @@ def test_file_fallback(self): self.assertFalse(self.f.in_memory) self.assertIs(self.f.file_object, old_obj) - def test_file_fallback_with_data(self): + def test_file_fallback_with_data(self) -> None: self.c["MAX_MEMORY_FILE_SIZE"] = 10 self.f.write(b"1" * 10) @@ -153,7 +161,7 @@ def test_file_fallback_with_data(self): self.assert_data(b"11111111112222222222") - def test_file_name(self): + def test_file_name(self) -> None: # Write to this dir. self.c["UPLOAD_DIR"] = self.d self.c["MAX_MEMORY_FILE_SIZE"] = 10 @@ -166,7 +174,7 @@ def test_file_name(self): self.assertIsNotNone(self.f.actual_file_name) self.assert_exists() - def test_file_full_name(self): + def test_file_full_name(self) -> None: # Write to this dir. self.c["UPLOAD_DIR"] = self.d self.c["UPLOAD_KEEP_FILENAME"] = True @@ -180,7 +188,7 @@ def test_file_full_name(self): self.assertEqual(self.f.actual_file_name, b"foo") self.assert_exists() - def test_file_full_name_with_ext(self): + def test_file_full_name_with_ext(self) -> None: self.c["UPLOAD_DIR"] = self.d self.c["UPLOAD_KEEP_FILENAME"] = True self.c["UPLOAD_KEEP_EXTENSIONS"] = True @@ -194,7 +202,7 @@ def test_file_full_name_with_ext(self): self.assertEqual(self.f.actual_file_name, b"foo.txt") self.assert_exists() - def test_no_dir_with_extension(self): + def test_no_dir_with_extension(self) -> None: self.c["UPLOAD_KEEP_EXTENSIONS"] = True self.c["MAX_MEMORY_FILE_SIZE"] = 10 @@ -203,11 +211,12 @@ def test_no_dir_with_extension(self): self.assertFalse(self.f.in_memory) # Assert that the file exists + assert self.f.actual_file_name is not None ext = os.path.splitext(self.f.actual_file_name)[1] self.assertEqual(ext, b".txt") self.assert_exists() - def test_invalid_dir_with_name(self): + def test_invalid_dir_with_name(self) -> None: # Write to this dir. self.c["UPLOAD_DIR"] = force_bytes(os.path.join("/", "tmp", "notexisting")) self.c["UPLOAD_KEEP_FILENAME"] = True @@ -217,7 +226,7 @@ def test_invalid_dir_with_name(self): with self.assertRaises(FileError): self.f.write(b"1234567890") - def test_invalid_dir_no_name(self): + def test_invalid_dir_no_name(self) -> None: # Write to this dir. self.c["UPLOAD_DIR"] = force_bytes(os.path.join("/", "tmp", "notexisting")) self.c["UPLOAD_KEEP_FILENAME"] = False @@ -231,50 +240,50 @@ def test_invalid_dir_no_name(self): class TestParseOptionsHeader(unittest.TestCase): - def test_simple(self): + def test_simple(self) -> None: t, p = parse_options_header("application/json") self.assertEqual(t, b"application/json") self.assertEqual(p, {}) - def test_blank(self): + def test_blank(self) -> None: t, p = parse_options_header("") self.assertEqual(t, b"") self.assertEqual(p, {}) - def test_single_param(self): + def test_single_param(self) -> None: t, p = parse_options_header("application/json;par=val") self.assertEqual(t, b"application/json") self.assertEqual(p, {b"par": b"val"}) - def test_single_param_with_spaces(self): + def test_single_param_with_spaces(self) -> None: t, p = parse_options_header(b"application/json; par=val") self.assertEqual(t, b"application/json") self.assertEqual(p, {b"par": b"val"}) - def test_multiple_params(self): + def test_multiple_params(self) -> None: t, p = parse_options_header(b"application/json;par=val;asdf=foo") self.assertEqual(t, b"application/json") self.assertEqual(p, {b"par": b"val", b"asdf": b"foo"}) - def test_quoted_param(self): + def test_quoted_param(self) -> None: t, p = parse_options_header(b'application/json;param="quoted"') self.assertEqual(t, b"application/json") self.assertEqual(p, {b"param": b"quoted"}) - def test_quoted_param_with_semicolon(self): + def test_quoted_param_with_semicolon(self) -> None: t, p = parse_options_header(b'application/json;param="quoted;with;semicolons"') self.assertEqual(p[b"param"], b"quoted;with;semicolons") - def test_quoted_param_with_escapes(self): + def test_quoted_param_with_escapes(self) -> None: t, p = parse_options_header(b'application/json;param="This \\" is \\" a \\" quote"') self.assertEqual(p[b"param"], b'This " is " a " quote') - def test_handles_ie6_bug(self): + def test_handles_ie6_bug(self) -> None: t, p = parse_options_header(b'text/plain; filename="C:\\this\\is\\a\\path\\file.txt"') self.assertEqual(p[b"filename"], b"file.txt") - def test_redos_attack_header(self): + def test_redos_attack_header(self) -> None: t, p = parse_options_header( b'application/x-www-form-urlencoded; !="' b"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\" @@ -282,47 +291,47 @@ def test_redos_attack_header(self): # If vulnerable, this test wouldn't finish, the line above would hang self.assertIn(b'"\\', p[b"!"]) - def test_handles_rfc_2231(self): + def test_handles_rfc_2231(self) -> None: t, p = parse_options_header(b"text/plain; param*=us-ascii'en-us'encoded%20message") self.assertEqual(p[b"param"], b"encoded message") class TestBaseParser(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: self.b = BaseParser() self.b.callbacks = {} - def test_callbacks(self): + def test_callbacks(self) -> None: called = 0 - def on_foo(): + def on_foo() -> None: nonlocal called called += 1 - self.b.set_callback("foo", on_foo) - self.b.callback("foo") + self.b.set_callback("foo", on_foo) # type: ignore[arg-type] + self.b.callback("foo") # type: ignore[arg-type] self.assertEqual(called, 1) - self.b.set_callback("foo", None) - self.b.callback("foo") + self.b.set_callback("foo", None) # type: ignore[arg-type] + self.b.callback("foo") # type: ignore[arg-type] self.assertEqual(called, 1) class TestQuerystringParser(unittest.TestCase): - def assert_fields(self, *args, **kwargs): + def assert_fields(self, *args: tuple[bytes, bytes], **kwargs: Any) -> None: if kwargs.pop("finalize", True): self.p.finalize() self.assertEqual(self.f, list(args)) if kwargs.get("reset", True): - self.f = [] + self.f: list[tuple[bytes, bytes]] = [] - def setUp(self): + def setUp(self) -> None: self.reset() - def reset(self): - self.f: list[tuple[bytes, bytes]] = [] + def reset(self) -> None: + self.f = [] name_buffer: list[bytes] = [] data_buffer: list[bytes] = [] @@ -333,7 +342,7 @@ def on_field_name(data: bytes, start: int, end: int) -> None: def on_field_data(data: bytes, start: int, end: int) -> None: data_buffer.append(data[start:end]) - def on_field_end(): + def on_field_end() -> None: self.f.append((b"".join(name_buffer), b"".join(data_buffer))) del name_buffer[:] @@ -343,34 +352,34 @@ def on_field_end(): callbacks={"on_field_name": on_field_name, "on_field_data": on_field_data, "on_field_end": on_field_end} ) - def test_simple_querystring(self): + def test_simple_querystring(self) -> None: self.p.write(b"foo=bar") self.assert_fields((b"foo", b"bar")) - def test_querystring_blank_beginning(self): + def test_querystring_blank_beginning(self) -> None: self.p.write(b"&foo=bar") self.assert_fields((b"foo", b"bar")) - def test_querystring_blank_end(self): + def test_querystring_blank_end(self) -> None: self.p.write(b"foo=bar&") self.assert_fields((b"foo", b"bar")) - def test_multiple_querystring(self): + def test_multiple_querystring(self) -> None: self.p.write(b"foo=bar&asdf=baz") self.assert_fields((b"foo", b"bar"), (b"asdf", b"baz")) - def test_streaming_simple(self): + def test_streaming_simple(self) -> None: self.p.write(b"foo=bar&") self.assert_fields((b"foo", b"bar"), finalize=False) self.p.write(b"asdf=baz") self.assert_fields((b"asdf", b"baz")) - def test_streaming_break(self): + def test_streaming_break(self) -> None: self.p.write(b"foo=one") self.assert_fields(finalize=False) @@ -386,12 +395,12 @@ def test_streaming_break(self): self.p.write(b"f=baz") self.assert_fields((b"asdf", b"baz")) - def test_semicolon_separator(self): + def test_semicolon_separator(self) -> None: self.p.write(b"foo=bar;asdf=baz") self.assert_fields((b"foo", b"bar"), (b"asdf", b"baz")) - def test_too_large_field(self): + def test_too_large_field(self) -> None: self.p.max_size = 15 # Note: len = 8 @@ -402,11 +411,11 @@ def test_too_large_field(self): self.p.write(b"a=123456") self.assert_fields((b"a", b"12345")) - def test_invalid_max_size(self): + def test_invalid_max_size(self) -> None: with self.assertRaises(ValueError): p = QuerystringParser(max_size=-100) - def test_strict_parsing_pass(self): + def test_strict_parsing_pass(self) -> None: data = b"foo=bar&another=asdf" for first, last in split_all(data): self.reset() @@ -418,7 +427,7 @@ def test_strict_parsing_pass(self): self.p.write(last) self.assert_fields((b"foo", b"bar"), (b"another", b"asdf")) - def test_strict_parsing_fail_double_sep(self): + def test_strict_parsing_fail_double_sep(self) -> None: data = b"foo=bar&&another=asdf" for first, last in split_all(data): self.reset() @@ -435,7 +444,7 @@ def test_strict_parsing_fail_double_sep(self): if cm is not None: self.assertEqual(cm.exception.offset, 8 - cnt) - def test_double_sep(self): + def test_double_sep(self) -> None: data = b"foo=bar&&another=asdf" for first, last in split_all(data): print(f" {first!r} / {last!r} ") @@ -447,7 +456,7 @@ def test_double_sep(self): self.assert_fields((b"foo", b"bar"), (b"another", b"asdf")) - def test_strict_parsing_fail_no_value(self): + def test_strict_parsing_fail_no_value(self) -> None: self.p.strict_parsing = True with self.assertRaises(QuerystringParseError) as cm: self.p.write(b"foo=bar&blank&another=asdf") @@ -455,18 +464,18 @@ def test_strict_parsing_fail_no_value(self): if cm is not None: self.assertEqual(cm.exception.offset, 8) - def test_success_no_value(self): + def test_success_no_value(self) -> None: self.p.write(b"foo=bar&blank&another=asdf") self.assert_fields((b"foo", b"bar"), (b"blank", b""), (b"another", b"asdf")) - def test_repr(self): + def test_repr(self) -> None: # Issue #29; verify we don't assert on repr() _ignored = repr(self.p) class TestOctetStreamParser(unittest.TestCase): - def setUp(self): - self.d = [] + def setUp(self) -> None: + self.d: list[bytes] = [] self.started = 0 self.finished = 0 @@ -481,23 +490,23 @@ def on_end() -> None: self.p = OctetStreamParser(callbacks={"on_start": on_start, "on_data": on_data, "on_end": on_end}) - def assert_data(self, data, finalize=True): + def assert_data(self, data: bytes, finalize: bool = True) -> None: self.assertEqual(b"".join(self.d), data) self.d = [] - def assert_started(self, val=True): + def assert_started(self, val: bool = True) -> None: if val: self.assertEqual(self.started, 1) else: self.assertEqual(self.started, 0) - def assert_finished(self, val=True): + def assert_finished(self, val: bool = True) -> None: if val: self.assertEqual(self.finished, 1) else: self.assertEqual(self.finished, 0) - def test_simple(self): + def test_simple(self) -> None: # Assert is not started self.assert_started(False) @@ -511,7 +520,7 @@ def test_simple(self): self.p.finalize() self.assert_finished() - def test_multiple_chunks(self): + def test_multiple_chunks(self) -> None: self.p.write(b"foo") self.p.write(b"bar") self.p.write(b"baz") @@ -520,7 +529,7 @@ def test_multiple_chunks(self): self.assert_data(b"foobarbaz") self.assert_finished() - def test_max_size(self): + def test_max_size(self) -> None: self.p.max_size = 5 self.p.write(b"0123456789") @@ -529,18 +538,18 @@ def test_max_size(self): self.assert_data(b"01234") self.assert_finished() - def test_invalid_max_size(self): + def test_invalid_max_size(self) -> None: with self.assertRaises(ValueError): - q = OctetStreamParser(max_size="foo") + q = OctetStreamParser(max_size="foo") # type: ignore[arg-type] class TestBase64Decoder(unittest.TestCase): # Note: base64('foobar') == 'Zm9vYmFy' - def setUp(self): + def setUp(self) -> None: self.f = BytesIO() self.d = Base64Decoder(self.f) - def assert_data(self, data, finalize=True): + def assert_data(self, data: bytes, finalize: bool = True) -> None: if finalize: self.d.finalize() @@ -549,20 +558,20 @@ def assert_data(self, data, finalize=True): self.f.seek(0) self.f.truncate() - def test_simple(self): + def test_simple(self) -> None: self.d.write(b"Zm9vYmFy") self.assert_data(b"foobar") - def test_bad(self): + def test_bad(self) -> None: with self.assertRaises(DecodeError): self.d.write(b"Zm9v!mFy") - def test_split_properly(self): + def test_split_properly(self) -> None: self.d.write(b"Zm9v") self.d.write(b"YmFy") self.assert_data(b"foobar") - def test_bad_split(self): + def test_bad_split(self) -> None: buff = b"Zm9v" for i in range(1, 4): first, second = buff[:i], buff[i:] @@ -572,7 +581,7 @@ def test_bad_split(self): self.d.write(second) self.assert_data(b"foo") - def test_long_bad_split(self): + def test_long_bad_split(self) -> None: buff = b"Zm9vYmFy" for i in range(5, 8): first, second = buff[:i], buff[i:] @@ -582,7 +591,7 @@ def test_long_bad_split(self): self.d.write(second) self.assert_data(b"foobar") - def test_close_and_finalize(self): + def test_close_and_finalize(self) -> None: parser = Mock() f = Base64Decoder(parser) @@ -592,7 +601,7 @@ def test_close_and_finalize(self): f.close() parser.close.assert_called_once_with() - def test_bad_length(self): + def test_bad_length(self) -> None: self.d.write(b"Zm9vYmF") # missing ending 'y' with self.assertRaises(DecodeError): @@ -600,11 +609,11 @@ def test_bad_length(self): class TestQuotedPrintableDecoder(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: self.f = BytesIO() self.d = QuotedPrintableDecoder(self.f) - def assert_data(self, data, finalize=True): + def assert_data(self, data: bytes, finalize: bool = True) -> None: if finalize: self.d.finalize() @@ -613,38 +622,38 @@ def assert_data(self, data, finalize=True): self.f.seek(0) self.f.truncate() - def test_simple(self): + def test_simple(self) -> None: self.d.write(b"foobar") self.assert_data(b"foobar") - def test_with_escape(self): + def test_with_escape(self) -> None: self.d.write(b"foo=3Dbar") self.assert_data(b"foo=bar") - def test_with_newline_escape(self): + def test_with_newline_escape(self) -> None: self.d.write(b"foo=\r\nbar") self.assert_data(b"foobar") - def test_with_only_newline_escape(self): + def test_with_only_newline_escape(self) -> None: self.d.write(b"foo=\nbar") self.assert_data(b"foobar") - def test_with_split_escape(self): + def test_with_split_escape(self) -> None: self.d.write(b"foo=3") self.d.write(b"Dbar") self.assert_data(b"foo=bar") - def test_with_split_newline_escape_1(self): + def test_with_split_newline_escape_1(self) -> None: self.d.write(b"foo=\r") self.d.write(b"\nbar") self.assert_data(b"foobar") - def test_with_split_newline_escape_2(self): + def test_with_split_newline_escape_2(self) -> None: self.d.write(b"foo=") self.d.write(b"\r\nbar") self.assert_data(b"foobar") - def test_close_and_finalize(self): + def test_close_and_finalize(self) -> None: parser = Mock() f = QuotedPrintableDecoder(parser) @@ -654,7 +663,7 @@ def test_close_and_finalize(self): f.close() parser.close.assert_called_once_with() - def test_not_aligned(self): + def test_not_aligned(self) -> None: """ https://github.com/andrew-d/python-multipart/issues/6 """ @@ -675,7 +684,7 @@ def test_not_aligned(self): # Read in all test cases and load them. NON_PARAMETRIZED_TESTS = {"single_field_blocks"} -http_tests = [] +http_tests: list[TestParams] = [] for f in os.listdir(http_tests_dir): # Only load the HTTP test cases. fname, ext = os.path.splitext(f) @@ -687,11 +696,11 @@ def test_not_aligned(self): yaml_file = os.path.join(http_tests_dir, fname + ".yaml") # Load both. - with open(os.path.join(http_tests_dir, f), "rb") as f: - test_data = f.read() + with open(os.path.join(http_tests_dir, f), "rb") as fh: + test_data = fh.read() - with open(yaml_file, "rb") as f: - yaml_data = yaml.safe_load(f) + with open(yaml_file, "rb") as fy: + yaml_data = yaml.safe_load(fy) http_tests.append({"name": fname, "test": test_data, "result": yaml_data}) @@ -704,7 +713,8 @@ def test_not_aligned(self): "single_field_single_file", ] -def split_all(val): + +def split_all(val: bytes) -> Iterator[tuple[bytes, bytes]]: """ This function will split an array all possible ways. For example: split_all([1,2,3,4]) @@ -717,30 +727,30 @@ def split_all(val): @parametrize_class class TestFormParser(unittest.TestCase): - def make(self, boundary, config={}): + def make(self, boundary: str | bytes, config: dict[str, Any] = {}) -> None: self.ended = False self.files: list[File] = [] self.fields: list[Field] = [] - def on_field(f: Field) -> None: - self.fields.append(f) + def on_field(f: FieldProtocol) -> None: + self.fields.append(cast(Field, f)) - def on_file(f: File) -> None: - self.files.append(f) + def on_file(f: FileProtocol) -> None: + self.files.append(cast(File, f)) - def on_end(): + def on_end() -> None: self.ended = True # Get a form-parser instance. self.f = FormParser("multipart/form-data", on_field, on_file, on_end, boundary=boundary, config=config) - def assert_file_data(self, f, data): + def assert_file_data(self, f: File, data: bytes) -> None: o = f.file_object o.seek(0) file_data = o.read() self.assertEqual(file_data, data) - def assert_file(self, field_name, file_name, data): + def assert_file(self, field_name: bytes, file_name: bytes, data: bytes) -> None: # Find this file. found = None for f in self.files: @@ -750,6 +760,7 @@ def assert_file(self, field_name, file_name, data): # Assert that we found it. self.assertIsNotNone(found) + assert found is not None try: # Assert about this file. @@ -762,7 +773,7 @@ def assert_file(self, field_name, file_name, data): # Close our file found.close() - def assert_field(self, name, value): + def assert_field(self, name: bytes, value: bytes) -> None: # Find this field in our fields list. found = None for f in self.fields: @@ -772,13 +783,14 @@ def assert_field(self, name, value): # Assert that it exists and matches. self.assertIsNotNone(found) + assert found is not None # typing self.assertEqual(value, found.value) # Remove it for future iterations. self.fields.remove(found) @parametrize("param", http_tests) - def test_http(self, param): + def test_http(self, param: TestParams) -> None: # Firstly, create our parser with the given boundary. boundary = param["result"]["boundary"] if isinstance(boundary, str): @@ -790,9 +802,9 @@ def test_http(self, param): try: processed = self.f.write(param["test"]) self.f.finalize() - except MultipartParseError as e: + except MultipartParseError as err: processed = 0 - exc = e + exc = err # print(repr(param)) # print("") @@ -802,6 +814,7 @@ def test_http(self, param): # Do we expect an error? if "error" in param["result"]["expected"]: self.assertIsNotNone(exc) + assert exc is not None self.assertEqual(param["result"]["expected"]["error"], exc.offset) return @@ -823,7 +836,7 @@ def test_http(self, param): else: assert False - def test_random_splitting(self): + def test_random_splitting(self) -> None: """ This test runs a simple multipart body with one field and one file through every possible split. @@ -851,8 +864,8 @@ def test_random_splitting(self): self.assert_field(b"field", b"test1") self.assert_file(b"file", b"file.txt", b"test2") - @parametrize("param", [ t for t in http_tests if t["name"] in single_byte_tests]) - def test_feed_single_bytes(self, param): + @parametrize("param", [t for t in http_tests if t["name"] in single_byte_tests]) + def test_feed_single_bytes(self, param: TestParams) -> None: """ This test parses multipart bodies 1 byte at a time. """ @@ -893,7 +906,7 @@ def test_feed_single_bytes(self, param): else: assert False - def test_feed_blocks(self): + def test_feed_blocks(self) -> None: """ This test parses a simple multipart body 1 byte at a time. """ @@ -926,7 +939,7 @@ def test_feed_blocks(self): # Assert that our field is here. self.assert_field(b"field", b"0123456789ABCDEFGHIJ0123456789ABCDEFGHIJ") - def test_request_body_fuzz(self): + def test_request_body_fuzz(self) -> None: """ This test randomly fuzzes the request body to ensure that no strange exceptions are raised and we don't end up in a strange state. The @@ -998,7 +1011,7 @@ def test_request_body_fuzz(self): print("Failures: %d" % (failures,)) print("Exceptions: %d" % (exceptions,)) - def test_request_body_fuzz_random_data(self): + def test_request_body_fuzz_random_data(self) -> None: """ This test will fuzz the multipart parser with some number of iterations of randomly-generated data. @@ -1035,7 +1048,7 @@ def test_request_body_fuzz_random_data(self): print("Failures: %d" % (failures,)) print("Exceptions: %d" % (exceptions,)) - def test_bad_start_boundary(self): + def test_bad_start_boundary(self) -> None: self.make("boundary") data = b"--boundary\rfoobar" with self.assertRaises(MultipartParseError): @@ -1046,11 +1059,11 @@ def test_bad_start_boundary(self): with self.assertRaises(MultipartParseError): i = self.f.write(data) - def test_octet_stream(self): - files = [] + def test_octet_stream(self) -> None: + files: list[File] = [] - def on_file(f): - files.append(f) + def on_file(f: FileProtocol) -> None: + files.append(cast(File, f)) on_field = Mock() on_end = Mock() @@ -1068,16 +1081,16 @@ def on_file(f): self.assert_file_data(files[0], b"test1234") self.assertTrue(on_end.called) - def test_querystring(self): - fields = [] + def test_querystring(self) -> None: + fields: list[Field] = [] - def on_field(f): - fields.append(f) + def on_field(f: FieldProtocol) -> None: + fields.append(cast(Field, f)) on_file = Mock() on_end = Mock() - def simple_test(f): + def simple_test(f: FormParser) -> None: # Reset tracking. del fields[:] on_file.reset_mock() @@ -1110,7 +1123,7 @@ def simple_test(f): self.assertTrue(isinstance(f.parser, QuerystringParser)) simple_test(f) - def test_close_methods(self): + def test_close_methods(self) -> None: parser = Mock() f = FormParser("application/x-url-encoded", None, None) f.parser = parser @@ -1121,18 +1134,18 @@ def test_close_methods(self): f.close() parser.close.assert_called_once_with() - def test_bad_content_type(self): + def test_bad_content_type(self) -> None: # We should raise a ValueError for a bad Content-Type with self.assertRaises(ValueError): f = FormParser("application/bad", None, None) - def test_no_boundary_given(self): + def test_no_boundary_given(self) -> None: # We should raise a FormParserError when parsing a multipart message # without a boundary. with self.assertRaises(FormParserError): f = FormParser("multipart/form-data", None, None) - def test_bad_content_transfer_encoding(self): + def test_bad_content_transfer_encoding(self) -> None: data = ( b'----boundary\r\nContent-Disposition: form-data; name="file"; filename="test.txt"\r\n' b"Content-Type: text/plain\r\n" @@ -1140,10 +1153,10 @@ def test_bad_content_transfer_encoding(self): b"Test\r\n----boundary--\r\n" ) - files = [] + files: list[File] = [] - def on_file(f): - files.append(f) + def on_file(f: FileProtocol) -> None: + files.append(cast(File, f)) on_field = Mock() on_end = Mock() @@ -1164,11 +1177,11 @@ def on_file(f): f.finalize() self.assert_file_data(files[0], b"Test") - def test_handles_None_fields(self): - fields = [] + def test_handles_None_fields(self) -> None: + fields: list[Field] = [] - def on_field(f): - fields.append(f) + def on_field(f: FieldProtocol) -> None: + fields.append(cast(Field, f)) on_file = Mock() on_end = Mock() @@ -1186,7 +1199,7 @@ def on_field(f): self.assertEqual(fields[2].field_name, b"baz") self.assertEqual(fields[2].value, b"asdf") - def test_max_size_multipart(self): + def test_max_size_multipart(self) -> None: # Load test data. test_file = "single_field_single_file.http" with open(os.path.join(http_tests_dir, test_file), "rb") as f: @@ -1197,7 +1210,8 @@ def test_max_size_multipart(self): # Set the maximum length that we can process to be halfway through the # given data. - self.f.parser.max_size = len(test_data) / 2 + assert self.f.parser is not None + self.f.parser.max_size = float(len(test_data)) / 2 i = self.f.write(test_data) self.f.finalize() @@ -1205,7 +1219,7 @@ def test_max_size_multipart(self): # Assert we processed the correct amount. self.assertEqual(i, len(test_data) / 2) - def test_max_size_form_parser(self): + def test_max_size_form_parser(self) -> None: # Load test data. test_file = "single_field_single_file.http" with open(os.path.join(http_tests_dir, test_file), "rb") as f: @@ -1222,11 +1236,11 @@ def test_max_size_form_parser(self): # Assert we processed the correct amount. self.assertEqual(i, len(test_data) / 2) - def test_octet_stream_max_size(self): - files = [] + def test_octet_stream_max_size(self) -> None: + files: list[File] = [] - def on_file(f): - files.append(f) + def on_file(f: FileProtocol) -> None: + files.append(cast(File, f)) on_field = Mock() on_end = Mock() @@ -1245,11 +1259,11 @@ def on_file(f): self.assert_file_data(files[0], b"0123456789") - def test_invalid_max_size_multipart(self): + def test_invalid_max_size_multipart(self) -> None: with self.assertRaises(ValueError): - MultipartParser(b"bound", max_size="foo") + MultipartParser(b"bound", max_size="foo") # type: ignore[arg-type] - def test_header_begin_callback(self): + def test_header_begin_callback(self) -> None: """ This test verifies we call the `on_header_begin` callback. See GitHub issue #23 @@ -1280,20 +1294,20 @@ def on_header_begin() -> None: class TestHelperFunctions(unittest.TestCase): - def test_create_form_parser(self): - r = create_form_parser({"Content-Type": "application/octet-stream"}, None, None) + def test_create_form_parser(self) -> None: + r = create_form_parser({"Content-Type": b"application/octet-stream"}, None, None) self.assertTrue(isinstance(r, FormParser)) - def test_create_form_parser_error(self): - headers = {} + def test_create_form_parser_error(self) -> None: + headers: dict[str, bytes] = {} with self.assertRaises(ValueError): create_form_parser(headers, None, None) - def test_parse_form(self): + def test_parse_form(self) -> None: on_field = Mock() on_file = Mock() - parse_form({"Content-Type": "application/octet-stream"}, BytesIO(b"123456789012345"), on_field, on_file) + parse_form({"Content-Type": b"application/octet-stream"}, BytesIO(b"123456789012345"), on_field, on_file) assert on_file.call_count == 1 @@ -1301,24 +1315,27 @@ def test_parse_form(self): # 15 - i.e. all data is written. self.assertEqual(on_file.call_args[0][0].size, 15) - def test_parse_form_content_length(self): - files = [] + def test_parse_form_content_length(self) -> None: + files: list[FileProtocol] = [] - def on_file(file): + def on_field(field: FieldProtocol) -> None: + pass + + def on_file(file: FileProtocol) -> None: files.append(file) parse_form( - {"Content-Type": "application/octet-stream", "Content-Length": "10"}, + {"Content-Type": b"application/octet-stream", "Content-Length": b"10"}, BytesIO(b"123456789012345"), - None, + on_field, on_file, ) self.assertEqual(len(files), 1) - self.assertEqual(files[0].size, 10) + self.assertEqual(files[0].size, 10) # type: ignore[attr-defined] -def suite(): +def suite() -> unittest.TestSuite: suite = unittest.TestSuite() suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestFile)) suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestParseOptionsHeader)) diff --git a/uv.lock b/uv.lock index 69f3835..2ae1c4e 100644 --- a/uv.lock +++ b/uv.lock @@ -524,6 +524,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/e2/8e10e465ee3987bb7c9ab69efb91d867d93959095f4807db102d07995d94/more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684", size = 57015 }, ] +[[package]] +name = "mypy" +version = "1.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/86/5d7cbc4974fd564550b80fbb8103c05501ea11aa7835edf3351d90095896/mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", size = 3078806 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/cd/815368cd83c3a31873e5e55b317551500b12f2d1d7549720632f32630333/mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a", size = 10939401 }, + { url = "https://files.pythonhosted.org/packages/f1/27/e18c93a195d2fad75eb96e1f1cbc431842c332e8eba2e2b77eaf7313c6b7/mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef", size = 10111697 }, + { url = "https://files.pythonhosted.org/packages/dc/08/cdc1fc6d0d5a67d354741344cc4aa7d53f7128902ebcbe699ddd4f15a61c/mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383", size = 12500508 }, + { url = "https://files.pythonhosted.org/packages/64/12/aad3af008c92c2d5d0720ea3b6674ba94a98cdb86888d389acdb5f218c30/mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8", size = 13020712 }, + { url = "https://files.pythonhosted.org/packages/03/e6/a7d97cc124a565be5e9b7d5c2a6ebf082379ffba99646e4863ed5bbcb3c3/mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7", size = 9567319 }, + { url = "https://files.pythonhosted.org/packages/e2/aa/cc56fb53ebe14c64f1fe91d32d838d6f4db948b9494e200d2f61b820b85d/mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385", size = 10859630 }, + { url = "https://files.pythonhosted.org/packages/04/c8/b19a760fab491c22c51975cf74e3d253b8c8ce2be7afaa2490fbf95a8c59/mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca", size = 10037973 }, + { url = "https://files.pythonhosted.org/packages/88/57/7e7e39f2619c8f74a22efb9a4c4eff32b09d3798335625a124436d121d89/mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104", size = 12416659 }, + { url = "https://files.pythonhosted.org/packages/fc/a6/37f7544666b63a27e46c48f49caeee388bf3ce95f9c570eb5cfba5234405/mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4", size = 12897010 }, + { url = "https://files.pythonhosted.org/packages/84/8b/459a513badc4d34acb31c736a0101c22d2bd0697b969796ad93294165cfb/mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6", size = 9562873 }, + { url = "https://files.pythonhosted.org/packages/35/3a/ed7b12ecc3f6db2f664ccf85cb2e004d3e90bec928e9d7be6aa2f16b7cdf/mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318", size = 10990335 }, + { url = "https://files.pythonhosted.org/packages/04/e4/1a9051e2ef10296d206519f1df13d2cc896aea39e8683302f89bf5792a59/mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", size = 10007119 }, + { url = "https://files.pythonhosted.org/packages/f3/3c/350a9da895f8a7e87ade0028b962be0252d152e0c2fbaafa6f0658b4d0d4/mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987", size = 12506856 }, + { url = "https://files.pythonhosted.org/packages/b6/49/ee5adf6a49ff13f4202d949544d3d08abb0ea1f3e7f2a6d5b4c10ba0360a/mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca", size = 12952066 }, + { url = "https://files.pythonhosted.org/packages/27/c0/b19d709a42b24004d720db37446a42abadf844d5c46a2c442e2a074d70d9/mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70", size = 9664000 }, + { url = "https://files.pythonhosted.org/packages/42/ad/5a8567700410f8aa7c755b0ebd4cacff22468cbc5517588773d65075c0cb/mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b", size = 10876550 }, + { url = "https://files.pythonhosted.org/packages/1b/bc/9fc16ea7a27ceb93e123d300f1cfe27a6dd1eac9a8beea4f4d401e737e9d/mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86", size = 10068086 }, + { url = "https://files.pythonhosted.org/packages/cd/8f/a1e460f1288405a13352dad16b24aba6dce4f850fc76510c540faa96eda3/mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce", size = 12459214 }, + { url = "https://files.pythonhosted.org/packages/c7/74/746b31aef7cc7512dab8bdc2311ef88d63fadc1c453a09c8cab7e57e59bf/mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1", size = 12962942 }, + { url = "https://files.pythonhosted.org/packages/28/a4/7fae712240b640d75bb859294ad4776b9960b3216ccb7fa747f578e6c632/mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b", size = 9545616 }, + { url = "https://files.pythonhosted.org/packages/16/64/bb5ed751487e2bea0dfaa6f640a7e3bb88083648f522e766d5ef4a76f578/mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6", size = 10937294 }, + { url = "https://files.pythonhosted.org/packages/a9/a3/67a0069abed93c3bf3b0bebb8857e2979a02828a4a3fd82f107f8f1143e8/mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70", size = 10107707 }, + { url = "https://files.pythonhosted.org/packages/2f/4d/0379daf4258b454b1f9ed589a9dabd072c17f97496daea7b72fdacf7c248/mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d", size = 12498367 }, + { url = "https://files.pythonhosted.org/packages/3b/dc/3976a988c280b3571b8eb6928882dc4b723a403b21735a6d8ae6ed20e82b/mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d", size = 13018014 }, + { url = "https://files.pythonhosted.org/packages/83/84/adffc7138fb970e7e2a167bd20b33bb78958370179853a4ebe9008139342/mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24", size = 9568056 }, + { url = "https://files.pythonhosted.org/packages/42/3a/bdf730640ac523229dd6578e8a581795720a9321399de494374afc437ec5/mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", size = 2619625 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + [[package]] name = "packaging" version = "24.1" @@ -665,7 +713,7 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.10" +version = "0.0.11" source = { editable = "." } [package.dev-dependencies] @@ -680,6 +728,7 @@ dev = [ { name = "mkdocs-material" }, { name = "mkdocstrings-python" }, { name = "more-itertools" }, + { name = "mypy" }, { name = "pbr" }, { name = "pluggy" }, { name = "py" }, @@ -688,6 +737,7 @@ dev = [ { name = "pytest-timeout" }, { name = "pyyaml" }, { name = "ruff" }, + { name = "types-pyyaml" }, ] [package.metadata] @@ -704,6 +754,7 @@ dev = [ { name = "mkdocs-material" }, { name = "mkdocstrings-python" }, { name = "more-itertools", specifier = "==10.2.0" }, + { name = "mypy" }, { name = "pbr", specifier = "==6.0.0" }, { name = "pluggy", specifier = "==1.4.0" }, { name = "py", specifier = "==1.11.0" }, @@ -712,6 +763,7 @@ dev = [ { name = "pytest-timeout", specifier = "==2.3.1" }, { name = "pyyaml", specifier = "==6.0.1" }, { name = "ruff", specifier = "==0.3.4" }, + { name = "types-pyyaml" }, ] [[package]] @@ -939,6 +991,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, ] +[[package]] +name = "types-pyyaml" +version = "6.0.12.20240917" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/7d/a95df0a11f95c8f48d7683f03e4aed1a2c0fc73e9de15cca4d38034bea1a/types-PyYAML-6.0.12.20240917.tar.gz", hash = "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587", size = 12381 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/2c/c1d81d680997d24b0542aa336f0a65bd7835e5224b7670f33a7d617da379/types_PyYAML-6.0.12.20240917-py3-none-any.whl", hash = "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570", size = 15264 }, +] + [[package]] name = "typing-extensions" version = "4.12.2" From 83191cb3ee0fd69aef474914e001b985e043c46a Mon Sep 17 00:00:00 2001 From: yecril23pl <151100823+yecril23pl@users.noreply.github.com> Date: Sun, 29 Sep 2024 10:05:42 +0200 Subject: [PATCH 66/94] ensure our boundary matches: improve the message (#124) * ensure our boundary matches: improve the message The error message should report expected actual mismatch. * improve syntax * use walrus operator * Oops, walrus not supported * reverse random paste * Test multi parser error boundary mismatch error message * Fix expected error message * rebase --------- Co-authored-by: Marcelo Trylesinski --- multipart/multipart.py | 2 +- tests/test_multipart.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/multipart/multipart.py b/multipart/multipart.py index 137d6e7..158b7e6 100644 --- a/multipart/multipart.py +++ b/multipart/multipart.py @@ -1150,7 +1150,7 @@ def data_callback(name: CallbackName, end_i: int, remaining: bool = False) -> No else: # Check to ensure our boundary matches if c != boundary[index + 2]: - msg = "Did not find boundary character %r at index " "%d" % (c, index + 2) + msg = "Expected boundary character %r, got %r at index %d" % (boundary[index + 2], c, index + 2) self.logger.warning(msg) e = MultipartParseError(msg) e.offset = i diff --git a/tests/test_multipart.py b/tests/test_multipart.py index f55e228..b824f19 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -1057,7 +1057,12 @@ def test_bad_start_boundary(self) -> None: self.make("boundary") data = b"--boundaryfoobar" with self.assertRaises(MultipartParseError): - i = self.f.write(data) + self.f.write(data) + + self.make("boundary") + data = b"--Boundary\r\nfoobar" + with self.assertRaisesRegex(MultipartParseError, "Expected boundary character %r, got %r" % (b"b"[0], b"B"[0])): + self.f.write(data) def test_octet_stream(self) -> None: files: list[File] = [] From 3f7233d02196d3b66cb07be294b17f2425241959 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 29 Sep 2024 10:10:50 +0200 Subject: [PATCH 67/94] Version 0.0.12 (#160) --- CHANGELOG.md | 6 ++++++ multipart/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da7ae82..abf5bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.0.12 (2024-09-29) + +* Improve error message when boundary character does not match [#124](https://github.com/Kludex/python-multipart/pull/124). +* Add mypy strict typing [#140](https://github.com/Kludex/python-multipart/pull/140). +* Enforce 100% coverage [#159](https://github.com/Kludex/python-multipart/pull/159). + ## 0.0.11 (2024-09-28) * Improve performance, especially in data with many CR-LF [#137](https://github.com/Kludex/python-multipart/pull/137). diff --git a/multipart/__init__.py b/multipart/__init__.py index a813076..5fd2f41 100644 --- a/multipart/__init__.py +++ b/multipart/__init__.py @@ -2,7 +2,7 @@ __author__ = "Andrew Dunham" __license__ = "Apache" __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" -__version__ = "0.0.11" +__version__ = "0.0.12" from .multipart import ( BaseParser, From f92851f1fd4f32350063bc69f8b21d9da681da6e Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 29 Sep 2024 10:12:33 +0200 Subject: [PATCH 68/94] Remove old instructions to run the test suite (#161) --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index 5a2aba8..29dd249 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,3 @@ Test coverage is currently 100%. ## Why? Because streaming uploads are awesome for large files. - -## How to Test - -If you want to test: - -```bash -$ pip install '.[dev]' -$ inv test -``` From 5303590e767c8f6125729912923cddedb1cc0946 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 07:45:53 +0200 Subject: [PATCH 69/94] Bump astral-sh/setup-uv from 2 to 3 in the github-actions group (#163) --- .github/workflows/docs.yml | 2 +- .github/workflows/main.yml | 2 +- .github/workflows/publish.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c600001..b970488 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,7 +20,7 @@ jobs: git config user.email 41898282+github-actions[bot]@users.noreply.github.com - name: Install uv - uses: astral-sh/setup-uv@v2 + uses: astral-sh/setup-uv@v3 with: version: "0.4.12" enable-cache: true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1881a56..2fbd796 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v2 + uses: astral-sh/setup-uv@v3 with: version: "0.4.12" enable-cache: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2d12eb3..66915ad 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v2 + uses: astral-sh/setup-uv@v3 with: version: "0.4.12" enable-cache: true From a629ae0224e6b617ad8511189b010ee24f6a7c5f Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sun, 20 Oct 2024 08:06:15 -0400 Subject: [PATCH 70/94] refactor: rename import to python_multipart (#166) --- .github/workflows/main.yml | 3 ++ _python_multipart.pth | 1 + _python_multipart_loader.py | 37 +++++++++++++++++++ docs/api.md | 4 +- docs/index.md | 20 +++++++--- fuzz/fuzz_decoders.py | 2 +- fuzz/fuzz_form.py | 4 +- fuzz/fuzz_options_header.py | 2 +- noxfile.py | 29 +++++++++++++++ pyproject.toml | 14 ++++--- {multipart => python_multipart}/__init__.py | 0 {multipart => python_multipart}/decoders.py | 8 ++-- {multipart => python_multipart}/exceptions.py | 0 {multipart => python_multipart}/multipart.py | 12 +++--- {multipart => python_multipart}/py.typed | 0 scripts/check | 2 +- tests/test_multipart.py | 14 +++++-- 17 files changed, 120 insertions(+), 32 deletions(-) create mode 100644 _python_multipart.pth create mode 100644 _python_multipart_loader.py create mode 100644 noxfile.py rename {multipart => python_multipart}/__init__.py (100%) rename {multipart => python_multipart}/decoders.py (95%) rename {multipart => python_multipart}/exceptions.py (100%) rename {multipart => python_multipart}/multipart.py (99%) rename {multipart => python_multipart}/py.typed (100%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2fbd796..fdeed33 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,6 +33,9 @@ jobs: - name: Run tests run: scripts/test + - name: Run rename test + run: uvx nox -s rename -P ${{ matrix.python-version }} + # https://github.com/marketplace/actions/alls-green#why used for branch protection checks check: if: always() diff --git a/_python_multipart.pth b/_python_multipart.pth new file mode 100644 index 0000000..e681c13 --- /dev/null +++ b/_python_multipart.pth @@ -0,0 +1 @@ +import _python_multipart_loader diff --git a/_python_multipart_loader.py b/_python_multipart_loader.py new file mode 100644 index 0000000..7d34377 --- /dev/null +++ b/_python_multipart_loader.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +# The purpose of this file is to allow `import multipart` to continue to work +# unless `multipart` (the PyPI package) is also installed, in which case +# a collision is avoided, and `import multipart` is no longer injected. +import importlib +import importlib.abc +import importlib.machinery +import importlib.util +import sys +import warnings + + +class PythonMultipartCompatFinder(importlib.abc.MetaPathFinder): + def find_spec( + self, fullname: str, path: object = None, target: object = None + ) -> importlib.machinery.ModuleSpec | None: + if fullname != "multipart": + return None + old_sys_meta_path = sys.meta_path + try: + sys.meta_path = [p for p in sys.meta_path if not isinstance(p, type(self))] + if multipart := importlib.util.find_spec("multipart"): + return multipart + + warnings.warn("Please use `import python_multipart` instead.", FutureWarning, stacklevel=2) + sys.modules["multipart"] = importlib.import_module("python_multipart") + return importlib.util.find_spec("python_multipart") + finally: + sys.meta_path = old_sys_meta_path + + +def install() -> None: + sys.meta_path.insert(0, PythonMultipartCompatFinder()) + + +install() diff --git a/docs/api.md b/docs/api.md index cc102fd..aab37c0 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,3 +1,3 @@ -::: multipart +::: python_multipart -::: multipart.exceptions +::: python_multipart.exceptions diff --git a/docs/index.md b/docs/index.md index 0640374..7802011 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,7 +9,7 @@ Python-Multipart is a streaming multipart parser for Python. The following example shows a quick example of parsing an incoming request body in a simple WSGI application: ```python -import multipart +import python_multipart def simple_app(environ, start_response): ret = [] @@ -31,7 +31,7 @@ def simple_app(environ, start_response): headers['Content-Length'] = environ['CONTENT_LENGTH'] # Parse the form. - multipart.parse_form(headers, environ['wsgi.input'], on_field, on_file) + python_multipart.parse_form(headers, environ['wsgi.input'], on_field, on_file) # Return something. start_response('200 OK', [('Content-type', 'text/plain')]) @@ -67,7 +67,7 @@ In this section, we’ll build an application that computes the SHA-256 hash of To start, we need a simple WSGI application. We could do this with a framework like Flask, Django, or Tornado, but for now let’s stick to plain WSGI: ```python -import multipart +import python_multipart def simple_app(environ, start_response): start_response('200 OK', [('Content-type', 'text/plain')]) @@ -100,8 +100,8 @@ The final code should look like this: ```python import hashlib -import multipart -from multipart.multipart import parse_options_header +import python_multipart +from python_multipart.multipart import parse_options_header def simple_app(environ, start_response): ret = [] @@ -136,7 +136,7 @@ def simple_app(environ, start_response): } # Create the parser. - parser = multipart.MultipartParser(boundary, callbacks) + parser = python_multipart.MultipartParser(boundary, callbacks) # The input stream is from the WSGI environ. inp = environ['wsgi.input'] @@ -176,3 +176,11 @@ Content-type: text/plain Hashes: Part hash: 0b64696c0f7ddb9e3435341720988d5455b3b0f0724688f98ec8e6019af3d931 ``` + + +## Historical note + +This package used to be accessed via `import multipart`. This still works for +now (with a warning) as long as the Python package `multipart` is not also +installed. If both are installed, you need to use the full PyPI name +`python_multipart` for this package. diff --git a/fuzz/fuzz_decoders.py b/fuzz/fuzz_decoders.py index 1c4425e..543c299 100644 --- a/fuzz/fuzz_decoders.py +++ b/fuzz/fuzz_decoders.py @@ -5,7 +5,7 @@ from helpers import EnhancedDataProvider with atheris.instrument_imports(): - from multipart.decoders import Base64Decoder, DecodeError, QuotedPrintableDecoder + from python_multipart.decoders import Base64Decoder, DecodeError, QuotedPrintableDecoder def fuzz_base64_decoder(fdp: EnhancedDataProvider) -> None: diff --git a/fuzz/fuzz_form.py b/fuzz/fuzz_form.py index 0a7646a..c990639 100644 --- a/fuzz/fuzz_form.py +++ b/fuzz/fuzz_form.py @@ -6,8 +6,8 @@ from helpers import EnhancedDataProvider with atheris.instrument_imports(): - from multipart.exceptions import FormParserError - from multipart.multipart import parse_form + from python_multipart.exceptions import FormParserError + from python_multipart.multipart import parse_form on_field = Mock() on_file = Mock() diff --git a/fuzz/fuzz_options_header.py b/fuzz/fuzz_options_header.py index dd1cb44..2546eaf 100644 --- a/fuzz/fuzz_options_header.py +++ b/fuzz/fuzz_options_header.py @@ -4,7 +4,7 @@ from helpers import EnhancedDataProvider with atheris.instrument_imports(): - from multipart.multipart import parse_options_header + from python_multipart.multipart import parse_options_header def TestOneInput(data: bytes) -> None: diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..fda8050 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,29 @@ +import nox + +nox.needs_version = ">=2024.4.15" +nox.options.default_venv_backend = "uv|virtualenv" + +ALL_PYTHONS = [ + c.split()[-1] + for c in nox.project.load_toml("pyproject.toml")["project"]["classifiers"] + if c.startswith("Programming Language :: Python :: 3.") +] + + +@nox.session(python=ALL_PYTHONS) +def rename(session: nox.Session) -> None: + session.install(".") + assert "import python_multipart" in session.run("python", "-c", "import multipart", silent=True) + assert "import python_multipart" in session.run("python", "-c", "import multipart.exceptions", silent=True) + assert "import python_multipart" in session.run("python", "-c", "from multipart import exceptions", silent=True) + assert "import python_multipart" in session.run( + "python", "-c", "from multipart.exceptions import FormParserError", silent=True + ) + + session.install("multipart") + assert "import python_multipart" not in session.run( + "python", "-c", "import multipart; multipart.parse_form_data", silent=True + ) + assert "import python_multipart" not in session.run( + "python", "-c", "import python_multipart; python_multipart.parse_form", silent=True + ) diff --git a/pyproject.toml b/pyproject.toml index fb03f83..1a81077 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,9 @@ dev-dependencies = [ "mkdocs-autorefs", ] +[tool.uv.pip] +reinstall-package = ["python-multipart"] + [project.urls] Homepage = "https://github.com/Kludex/python-multipart" Documentation = "https://kludex.github.io/python-multipart/" @@ -62,13 +65,14 @@ Changelog = "https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md Source = "https://github.com/Kludex/python-multipart" [tool.hatch.version] -path = "multipart/__init__.py" - -[tool.hatch.build.targets.wheel] -packages = ["multipart"] +path = "python_multipart/__init__.py" [tool.hatch.build.targets.sdist] -include = ["/multipart", "/tests", "CHANGELOG.md", "LICENSE.txt"] +include = ["/python_multipart", "/tests", "CHANGELOG.md", "LICENSE.txt", "_python_multipart.pth", "_python_multipart_loader.py"] + +[tool.hatch.build.targets.wheel.force-include] +"_python_multipart.pth" = "_python_multipart.pth" +"_python_multipart_loader.py" = "_python_multipart_loader.py" [tool.mypy] strict = true diff --git a/multipart/__init__.py b/python_multipart/__init__.py similarity index 100% rename from multipart/__init__.py rename to python_multipart/__init__.py diff --git a/multipart/decoders.py b/python_multipart/decoders.py similarity index 95% rename from multipart/decoders.py rename to python_multipart/decoders.py index 07bf742..82b56a1 100644 --- a/multipart/decoders.py +++ b/python_multipart/decoders.py @@ -25,7 +25,7 @@ class Base64Decoder: call write() on the underlying object. This is primarily used for decoding form data encoded as Base64, but can be used for other purposes:: - from multipart.decoders import Base64Decoder + from python_multipart.decoders import Base64Decoder fd = open("notb64.txt", "wb") decoder = Base64Decoder(fd) try: @@ -55,7 +55,7 @@ def write(self, data: bytes) -> int: """Takes any input data provided, decodes it as base64, and passes it on to the underlying object. If the data provided is invalid base64 data, then this method will raise - a :class:`multipart.exceptions.DecodeError` + a :class:`python_multipart.exceptions.DecodeError` :param data: base64 data to decode """ @@ -97,7 +97,7 @@ def close(self) -> None: def finalize(self) -> None: """Finalize this object. This should be called when no more data should be written to the stream. This function can raise a - :class:`multipart.exceptions.DecodeError` if there is some remaining + :class:`python_multipart.exceptions.DecodeError` if there is some remaining data in the cache. If the underlying object has a `finalize()` method, this function will @@ -118,7 +118,7 @@ def __repr__(self) -> str: class QuotedPrintableDecoder: """This object provides an interface to decode a stream of quoted-printable data. It is instantiated with an "underlying object", in the same manner - as the :class:`multipart.decoders.Base64Decoder` class. This class behaves + as the :class:`python_multipart.decoders.Base64Decoder` class. This class behaves in exactly the same way, including maintaining a cache of quoted-printable chunks. diff --git a/multipart/exceptions.py b/python_multipart/exceptions.py similarity index 100% rename from multipart/exceptions.py rename to python_multipart/exceptions.py diff --git a/multipart/multipart.py b/python_multipart/multipart.py similarity index 99% rename from multipart/multipart.py rename to python_multipart/multipart.py index 158b7e6..ace4a8f 100644 --- a/multipart/multipart.py +++ b/python_multipart/multipart.py @@ -241,7 +241,7 @@ def from_value(cls, name: bytes, value: bytes | None) -> Field: value: the value of the form field - either a bytestring or None. Returns: - A new instance of a [`Field`][multipart.Field]. + A new instance of a [`Field`][python_multipart.Field]. """ f = cls(name) @@ -351,7 +351,7 @@ class File: | MAX_MEMORY_FILE_SIZE | `int` | 1 MiB | The maximum number of bytes of a File to keep in memory. By default, the contents of a File are kept into memory until a certain limit is reached, after which the contents of the File are written to a temporary file. This behavior can be disabled by setting this value to an appropriately large value (or, for example, infinity, such as `float('inf')`. | Args: - file_name: The name of the file that this [`File`][multipart.File] represents. + file_name: The name of the file that this [`File`][python_multipart.File] represents. field_name: The name of the form field that this file was uploaded with. This can be None, if, for example, the file was uploaded with Content-Type application/octet-stream. config: The configuration for this File. See above for valid configuration keys and their corresponding values. @@ -663,7 +663,7 @@ class OctetStreamParser(BaseParser): | on_end | None | Called when the parser is finished parsing all data.| Args: - callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][multipart.BaseParser]. + callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][python_multipart.BaseParser]. max_size: The maximum size of body to parse. Defaults to infinity - i.e. unbounded. """ @@ -733,12 +733,12 @@ class QuerystringParser(BaseParser): | on_end | None | Called when the parser is finished parsing all data.| Args: - callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][multipart.BaseParser]. + callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][python_multipart.BaseParser]. strict_parsing: Whether or not to parse the body strictly. Defaults to False. If this is set to True, then the behavior of the parser changes as the following: if a field has a value with an equal sign (e.g. "foo=bar", or "foo="), it is always included. If a field has no equals sign (e.g. "...&name&..."), it will be treated as an error if 'strict_parsing' is True, otherwise included. If an error is encountered, - then a [`QuerystringParseError`][multipart.exceptions.QuerystringParseError] will be raised. + then a [`QuerystringParseError`][python_multipart.exceptions.QuerystringParseError] will be raised. max_size: The maximum size of body to parse. Defaults to infinity - i.e. unbounded. """ # noqa: E501 @@ -969,7 +969,7 @@ class MultipartParser(BaseParser): Args: boundary: The multipart boundary. This is required, and must match what is given in the HTTP request - usually in the Content-Type header. - callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][multipart.BaseParser]. + callbacks: A dictionary of callbacks. See the documentation for [`BaseParser`][python_multipart.BaseParser]. max_size: The maximum size of body to parse. Defaults to infinity - i.e. unbounded. """ # noqa: E501 diff --git a/multipart/py.typed b/python_multipart/py.typed similarity index 100% rename from multipart/py.typed rename to python_multipart/py.typed diff --git a/scripts/check b/scripts/check index 0b6a294..f38e9c0 100755 --- a/scripts/check +++ b/scripts/check @@ -2,7 +2,7 @@ set -x -SOURCE_FILES="multipart tests" +SOURCE_FILES="python_multipart tests" uvx ruff format --check --diff $SOURCE_FILES uvx ruff check $SOURCE_FILES diff --git a/tests/test_multipart.py b/tests/test_multipart.py index b824f19..be01fbf 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -11,9 +11,15 @@ import yaml -from multipart.decoders import Base64Decoder, QuotedPrintableDecoder -from multipart.exceptions import DecodeError, FileError, FormParserError, MultipartParseError, QuerystringParseError -from multipart.multipart import ( +from python_multipart.decoders import Base64Decoder, QuotedPrintableDecoder +from python_multipart.exceptions import ( + DecodeError, + FileError, + FormParserError, + MultipartParseError, + QuerystringParseError, +) +from python_multipart.multipart import ( BaseParser, Field, File, @@ -31,7 +37,7 @@ if TYPE_CHECKING: from typing import Any, Iterator, TypedDict - from multipart.multipart import FieldProtocol, FileConfig, FileProtocol + from python_multipart.multipart import FieldProtocol, FileConfig, FileProtocol class TestParams(TypedDict): name: str From 72e30eabb9ec4440c9b420700bee799953810150 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 20 Oct 2024 14:10:10 +0200 Subject: [PATCH 71/94] Version 0.0.13 (#167) --- CHANGELOG.md | 4 ++++ python_multipart/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abf5bda..c177948 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.0.13 (2024-10-20) + +* Rename import to `python_multipart` [#166](https://github.com/Kludex/python-multipart/pull/166). + ## 0.0.12 (2024-09-29) * Improve error message when boundary character does not match [#124](https://github.com/Kludex/python-multipart/pull/124). diff --git a/python_multipart/__init__.py b/python_multipart/__init__.py index 5fd2f41..312194a 100644 --- a/python_multipart/__init__.py +++ b/python_multipart/__init__.py @@ -2,7 +2,7 @@ __author__ = "Andrew Dunham" __license__ = "Apache" __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" -__version__ = "0.0.12" +__version__ = "0.0.13" from .multipart import ( BaseParser, From 0c04f4ecdc9209da7b054b9a22c4c89316494391 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 24 Oct 2024 10:28:38 -0400 Subject: [PATCH 72/94] fix: use alternate scheme for importing multipart (#168) Co-authored-by: Marcelo Trylesinski --- .github/workflows/main.yml | 3 +-- _python_multipart.pth | 1 - _python_multipart_loader.py | 37 ------------------------------------- multipart/__init__.py | 20 ++++++++++++++++++++ multipart/decoders.py | 1 + multipart/exceptions.py | 1 + multipart/multipart.py | 1 + noxfile.py | 28 +++++++++++++++++++++------- pyproject.toml | 8 +++++--- scripts/README.md | 1 + scripts/check | 2 +- scripts/rename | 7 +++++++ 12 files changed, 59 insertions(+), 51 deletions(-) delete mode 100644 _python_multipart.pth delete mode 100644 _python_multipart_loader.py create mode 100644 multipart/__init__.py create mode 100644 multipart/decoders.py create mode 100644 multipart/exceptions.py create mode 100644 multipart/multipart.py create mode 100755 scripts/rename diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fdeed33..4ceafb7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,8 +34,7 @@ jobs: run: scripts/test - name: Run rename test - run: uvx nox -s rename -P ${{ matrix.python-version }} - + run: scripts/rename # https://github.com/marketplace/actions/alls-green#why used for branch protection checks check: if: always() diff --git a/_python_multipart.pth b/_python_multipart.pth deleted file mode 100644 index e681c13..0000000 --- a/_python_multipart.pth +++ /dev/null @@ -1 +0,0 @@ -import _python_multipart_loader diff --git a/_python_multipart_loader.py b/_python_multipart_loader.py deleted file mode 100644 index 7d34377..0000000 --- a/_python_multipart_loader.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations - -# The purpose of this file is to allow `import multipart` to continue to work -# unless `multipart` (the PyPI package) is also installed, in which case -# a collision is avoided, and `import multipart` is no longer injected. -import importlib -import importlib.abc -import importlib.machinery -import importlib.util -import sys -import warnings - - -class PythonMultipartCompatFinder(importlib.abc.MetaPathFinder): - def find_spec( - self, fullname: str, path: object = None, target: object = None - ) -> importlib.machinery.ModuleSpec | None: - if fullname != "multipart": - return None - old_sys_meta_path = sys.meta_path - try: - sys.meta_path = [p for p in sys.meta_path if not isinstance(p, type(self))] - if multipart := importlib.util.find_spec("multipart"): - return multipart - - warnings.warn("Please use `import python_multipart` instead.", FutureWarning, stacklevel=2) - sys.modules["multipart"] = importlib.import_module("python_multipart") - return importlib.util.find_spec("python_multipart") - finally: - sys.meta_path = old_sys_meta_path - - -def install() -> None: - sys.meta_path.insert(0, PythonMultipartCompatFinder()) - - -install() diff --git a/multipart/__init__.py b/multipart/__init__.py new file mode 100644 index 0000000..212af4e --- /dev/null +++ b/multipart/__init__.py @@ -0,0 +1,20 @@ +# This only works if using a file system, other loaders not implemented. + +import importlib.util +import sys +import warnings +from pathlib import Path + +for p in sys.path: + file_path = Path(p, "multipart.py") + if file_path.is_file(): + spec = importlib.util.spec_from_file_location("multipart", file_path) + assert spec is not None, f"{file_path} found but not loadable!" + module = importlib.util.module_from_spec(spec) + sys.modules["multipart"] = module + assert spec.loader is not None, f"{file_path} must be loadable!" + spec.loader.exec_module(module) + break +else: + warnings.warn("Please use `import python_multipart` instead.", FutureWarning, stacklevel=2) + from python_multipart import * diff --git a/multipart/decoders.py b/multipart/decoders.py new file mode 100644 index 0000000..31acdfb --- /dev/null +++ b/multipart/decoders.py @@ -0,0 +1 @@ +from python_multipart.decoders import * diff --git a/multipart/exceptions.py b/multipart/exceptions.py new file mode 100644 index 0000000..36815d1 --- /dev/null +++ b/multipart/exceptions.py @@ -0,0 +1 @@ +from python_multipart.exceptions import * diff --git a/multipart/multipart.py b/multipart/multipart.py new file mode 100644 index 0000000..7bf567d --- /dev/null +++ b/multipart/multipart.py @@ -0,0 +1 @@ +from python_multipart.multipart import * diff --git a/noxfile.py b/noxfile.py index fda8050..1df0fd7 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,16 +1,12 @@ +import inspect + import nox nox.needs_version = ">=2024.4.15" nox.options.default_venv_backend = "uv|virtualenv" -ALL_PYTHONS = [ - c.split()[-1] - for c in nox.project.load_toml("pyproject.toml")["project"]["classifiers"] - if c.startswith("Programming Language :: Python :: 3.") -] - -@nox.session(python=ALL_PYTHONS) +@nox.session def rename(session: nox.Session) -> None: session.install(".") assert "import python_multipart" in session.run("python", "-c", "import multipart", silent=True) @@ -27,3 +23,21 @@ def rename(session: nox.Session) -> None: assert "import python_multipart" not in session.run( "python", "-c", "import python_multipart; python_multipart.parse_form", silent=True ) + + +@nox.session +def rename_inline(session: nox.Session) -> None: + session.install("pip") + res = session.run( + "python", + "-c", + inspect.cleandoc(""" + import subprocess + + subprocess.run(["pip", "install", "."]) + + import multipart + """), + silent=True, + ) + assert "FutureWarning: Please use `import python_multipart` instead." in res diff --git a/pyproject.toml b/pyproject.toml index 1a81077..29206de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,9 +70,8 @@ path = "python_multipart/__init__.py" [tool.hatch.build.targets.sdist] include = ["/python_multipart", "/tests", "CHANGELOG.md", "LICENSE.txt", "_python_multipart.pth", "_python_multipart_loader.py"] -[tool.hatch.build.targets.wheel.force-include] -"_python_multipart.pth" = "_python_multipart.pth" -"_python_multipart_loader.py" = "_python_multipart_loader.py" +[tool.hatch.build.targets.wheel] +packages = ["python_multipart", "multipart"] [tool.mypy] strict = true @@ -91,6 +90,9 @@ skip-magic-trailing-comma = true combine-as-imports = true split-on-trailing-comma = false +[tool.ruff.lint.per-file-ignores] +"multipart/*.py" = ["F403"] + [tool.coverage.run] branch = false omit = ["tests/*"] diff --git a/scripts/README.md b/scripts/README.md index 1742ebd..dce491e 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -4,5 +4,6 @@ * `scripts/test` - Run the test suite. * `scripts/lint` - Run the code format. * `scripts/check` - Run the lint in check mode, and the type checker. +* `scripts/rename` - Check that the backward-compat `multipart` name works as expected. Styled after GitHub's ["Scripts to Rule Them All"](https://github.com/github/scripts-to-rule-them-all). diff --git a/scripts/check b/scripts/check index f38e9c0..141f2a9 100755 --- a/scripts/check +++ b/scripts/check @@ -2,7 +2,7 @@ set -x -SOURCE_FILES="python_multipart tests" +SOURCE_FILES="python_multipart multipart tests" uvx ruff format --check --diff $SOURCE_FILES uvx ruff check $SOURCE_FILES diff --git a/scripts/rename b/scripts/rename new file mode 100755 index 0000000..251cc1d --- /dev/null +++ b/scripts/rename @@ -0,0 +1,7 @@ +#!/bin/sh -e + +set -x + +uvx nox -s rename + +uvx nox -s rename_inline From 6e0a3d89ab78e64356ce7b1eaac4a8993cab39e4 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 24 Oct 2024 16:32:06 +0200 Subject: [PATCH 73/94] Version 0.0.14 (#169) --- CHANGELOG.md | 4 ++++ python_multipart/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c177948..4e341a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.0.14 (2024-10-24) + +* Fix import scheme for `multipart` module ([#168](https://github.com/user/repo/issues/168)). + ## 0.0.13 (2024-10-20) * Rename import to `python_multipart` [#166](https://github.com/Kludex/python-multipart/pull/166). diff --git a/python_multipart/__init__.py b/python_multipart/__init__.py index 312194a..80d5cc0 100644 --- a/python_multipart/__init__.py +++ b/python_multipart/__init__.py @@ -2,7 +2,7 @@ __author__ = "Andrew Dunham" __license__ = "Apache" __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" -__version__ = "0.0.13" +__version__ = "0.0.14" from .multipart import ( BaseParser, From 5578583c8c9b0c58ed60fc82a5d0d861103b4c2b Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sun, 27 Oct 2024 03:46:05 -0400 Subject: [PATCH 74/94] fix: reduce visibility of warning for next release (#174) --- multipart/__init__.py | 2 +- noxfile.py | 23 ++++++++++++++--------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/multipart/__init__.py b/multipart/__init__.py index 212af4e..f28c0c3 100644 --- a/multipart/__init__.py +++ b/multipart/__init__.py @@ -16,5 +16,5 @@ spec.loader.exec_module(module) break else: - warnings.warn("Please use `import python_multipart` instead.", FutureWarning, stacklevel=2) + warnings.warn("Please use `import python_multipart` instead.", PendingDeprecationWarning, stacklevel=2) from python_multipart import * diff --git a/noxfile.py b/noxfile.py index 1df0fd7..20de33e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -7,21 +7,25 @@ @nox.session -def rename(session: nox.Session) -> None: - session.install(".") - assert "import python_multipart" in session.run("python", "-c", "import multipart", silent=True) - assert "import python_multipart" in session.run("python", "-c", "import multipart.exceptions", silent=True) - assert "import python_multipart" in session.run("python", "-c", "from multipart import exceptions", silent=True) +@nox.parametrize("editable", [True, False]) +def rename(session: nox.Session, editable: bool) -> None: + session.install("-e." if editable else ".") + # Ensure warning is not visible by default + assert "import python_multipart" not in session.run("python", "-c", "import multipart", silent=True) + + assert "import python_multipart" in session.run("python", "-Wdefault", "-c", "import multipart", silent=True) + assert "import python_multipart" in session.run("python", "-Wdefault", "-c", "import multipart.exceptions", silent=True) + assert "import python_multipart" in session.run("python", "-Wdefault", "-c", "from multipart import exceptions", silent=True) assert "import python_multipart" in session.run( - "python", "-c", "from multipart.exceptions import FormParserError", silent=True + "python", "-Wdefault", "-c", "from multipart.exceptions import FormParserError", silent=True ) session.install("multipart") assert "import python_multipart" not in session.run( - "python", "-c", "import multipart; multipart.parse_form_data", silent=True + "python", "-Wdefault", "-c", "import multipart; multipart.parse_form_data", silent=True ) assert "import python_multipart" not in session.run( - "python", "-c", "import python_multipart; python_multipart.parse_form", silent=True + "python", "-Wdefault", "-c", "import python_multipart; python_multipart.parse_form", silent=True ) @@ -30,6 +34,7 @@ def rename_inline(session: nox.Session) -> None: session.install("pip") res = session.run( "python", + "-Wdefault", "-c", inspect.cleandoc(""" import subprocess @@ -40,4 +45,4 @@ def rename_inline(session: nox.Session) -> None: """), silent=True, ) - assert "FutureWarning: Please use `import python_multipart` instead." in res + assert "Please use `import python_multipart` instead." in res From c06830d2b7d0dc4060f1cac6e53f49cc887b6e71 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sun, 27 Oct 2024 03:47:51 -0400 Subject: [PATCH 75/94] fix: add missing files to SDist (#171) Co-authored-by: Marcelo Trylesinski --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 29206de..ea348fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ Source = "https://github.com/Kludex/python-multipart" path = "python_multipart/__init__.py" [tool.hatch.build.targets.sdist] -include = ["/python_multipart", "/tests", "CHANGELOG.md", "LICENSE.txt", "_python_multipart.pth", "_python_multipart_loader.py"] +include = ["/python_multipart", "/multipart", "/tests", "CHANGELOG.md", "LICENSE.txt"] [tool.hatch.build.targets.wheel] packages = ["python_multipart", "multipart"] From 73fb55d1f8fec576759fcc3c11cc0807d246af00 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sun, 27 Oct 2024 03:50:04 -0400 Subject: [PATCH 76/94] ci: check-sdist (#172) --- .github/workflows/main.yml | 1 + pyproject.toml | 9 +++++++++ scripts/check | 1 + 3 files changed, 11 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4ceafb7..c3d9d99 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,6 +35,7 @@ jobs: - name: Run rename test run: scripts/rename + # https://github.com/marketplace/actions/alls-green#why used for branch protection checks check: if: always() diff --git a/pyproject.toml b/pyproject.toml index ea348fc..12e4922 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,3 +113,12 @@ exclude_lines = [ "if self\\.debug:", "except ImportError:", ] + +[tool.check-sdist] +git-only = [ + "docs", + "fuzz", + "scripts", + "mkdocs.yml", + "uv.lock" +] diff --git a/scripts/check b/scripts/check index 141f2a9..13ce9ed 100755 --- a/scripts/check +++ b/scripts/check @@ -7,3 +7,4 @@ SOURCE_FILES="python_multipart multipart tests" uvx ruff format --check --diff $SOURCE_FILES uvx ruff check $SOURCE_FILES uvx --with types-PyYAML mypy $SOURCE_FILES +uvx check-sdist From ce85154ff138227654e19d5a47eea6b316bba427 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 27 Oct 2024 08:54:43 +0100 Subject: [PATCH 77/94] Version 0.0.15 (#175) --- CHANGELOG.md | 7 ++++++- python_multipart/__init__.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e341a2..a08dc65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ # Changelog +## 0.0.15 (2024-10-27) + +* Replace `FutureWarning` to `PendingDeprecationWarning` [#174](https://github.com/Kludex/python-multipart/pull/174). +* Add missing files to SDist [#171](https://github.com/Kludex/python-multipart/pull/171). + ## 0.0.14 (2024-10-24) -* Fix import scheme for `multipart` module ([#168](https://github.com/user/repo/issues/168)). +* Fix import scheme for `multipart` module ([#168](https://github.com/Kludex/python-multipart/pull/168)). ## 0.0.13 (2024-10-20) diff --git a/python_multipart/__init__.py b/python_multipart/__init__.py index 80d5cc0..88df444 100644 --- a/python_multipart/__init__.py +++ b/python_multipart/__init__.py @@ -2,7 +2,7 @@ __author__ = "Andrew Dunham" __license__ = "Apache" __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" -__version__ = "0.0.14" +__version__ = "0.0.15" from .multipart import ( BaseParser, From 876406774d9b98c7b3afa24c3a0c901215f87029 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 27 Oct 2024 12:52:23 +0100 Subject: [PATCH 78/94] Version 0.0.16 (#177) --- CHANGELOG.md | 4 ++++ multipart/__init__.py | 1 + pyproject.toml | 17 +++++++++-------- python_multipart/__init__.py | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a08dc65..56dbb3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.0.16 (2024-10-27) + +* Add dunder attributes to `multipart` package [#177](https://github.com/Kludex/python-multipart/pull/177). + ## 0.0.15 (2024-10-27) * Replace `FutureWarning` to `PendingDeprecationWarning` [#174](https://github.com/Kludex/python-multipart/pull/174). diff --git a/multipart/__init__.py b/multipart/__init__.py index f28c0c3..cdc0154 100644 --- a/multipart/__init__.py +++ b/multipart/__init__.py @@ -18,3 +18,4 @@ else: warnings.warn("Please use `import python_multipart` instead.", PendingDeprecationWarning, stacklevel=2) from python_multipart import * + from python_multipart import __all__, __author__, __copyright__, __license__, __version__ diff --git a/pyproject.toml b/pyproject.toml index 12e4922..e22ea1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,13 @@ Source = "https://github.com/Kludex/python-multipart" path = "python_multipart/__init__.py" [tool.hatch.build.targets.sdist] -include = ["/python_multipart", "/multipart", "/tests", "CHANGELOG.md", "LICENSE.txt"] +include = [ + "/python_multipart", + "/multipart", + "/tests", + "CHANGELOG.md", + "LICENSE.txt", +] [tool.hatch.build.targets.wheel] packages = ["python_multipart", "multipart"] @@ -92,6 +98,7 @@ split-on-trailing-comma = false [tool.ruff.lint.per-file-ignores] "multipart/*.py" = ["F403"] +"__init__.py" = ["F401"] [tool.coverage.run] branch = false @@ -115,10 +122,4 @@ exclude_lines = [ ] [tool.check-sdist] -git-only = [ - "docs", - "fuzz", - "scripts", - "mkdocs.yml", - "uv.lock" -] +git-only = ["docs", "fuzz", "scripts", "mkdocs.yml", "uv.lock"] diff --git a/python_multipart/__init__.py b/python_multipart/__init__.py index 88df444..a2d50d2 100644 --- a/python_multipart/__init__.py +++ b/python_multipart/__init__.py @@ -2,7 +2,7 @@ __author__ = "Andrew Dunham" __license__ = "Apache" __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" -__version__ = "0.0.15" +__version__ = "0.0.16" from .multipart import ( BaseParser, From ca52662eda368bd61fbb9508bfaffb0fc4af6028 Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Thu, 31 Oct 2024 08:04:56 +0100 Subject: [PATCH 79/94] Handle PermissionError in fallback code for old import name (#182) --- multipart/__init__.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/multipart/__init__.py b/multipart/__init__.py index cdc0154..67f0e5b 100644 --- a/multipart/__init__.py +++ b/multipart/__init__.py @@ -7,14 +7,17 @@ for p in sys.path: file_path = Path(p, "multipart.py") - if file_path.is_file(): - spec = importlib.util.spec_from_file_location("multipart", file_path) - assert spec is not None, f"{file_path} found but not loadable!" - module = importlib.util.module_from_spec(spec) - sys.modules["multipart"] = module - assert spec.loader is not None, f"{file_path} must be loadable!" - spec.loader.exec_module(module) - break + try: + if file_path.is_file(): + spec = importlib.util.spec_from_file_location("multipart", file_path) + assert spec is not None, f"{file_path} found but not loadable!" + module = importlib.util.module_from_spec(spec) + sys.modules["multipart"] = module + assert spec.loader is not None, f"{file_path} must be loadable!" + spec.loader.exec_module(module) + break + except PermissionError: + pass else: warnings.warn("Please use `import python_multipart` instead.", PendingDeprecationWarning, stacklevel=2) from python_multipart import * From 616b81e72fe67ce67e332c446513ef89b9d816dc Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 31 Oct 2024 08:07:53 +0100 Subject: [PATCH 80/94] Version 0.0.17 (#183) --- CHANGELOG.md | 4 ++++ python_multipart/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56dbb3f..9a6fbba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.0.17 (2024-10-31) + +* Handle PermissionError in fallback code for old import name [#182](https://github.com/Kludex/python-multipart/pull/182). + ## 0.0.16 (2024-10-27) * Add dunder attributes to `multipart` package [#177](https://github.com/Kludex/python-multipart/pull/177). diff --git a/python_multipart/__init__.py b/python_multipart/__init__.py index a2d50d2..be8327f 100644 --- a/python_multipart/__init__.py +++ b/python_multipart/__init__.py @@ -2,7 +2,7 @@ __author__ = "Andrew Dunham" __license__ = "Apache" __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" -__version__ = "0.0.16" +__version__ = "0.0.17" from .multipart import ( BaseParser, From 02d1ec148b40470b8bbb25dd833c1e1aace51a8b Mon Sep 17 00:00:00 2001 From: manunio Date: Thu, 31 Oct 2024 12:40:56 +0530 Subject: [PATCH 81/94] fuzz: fix boundary error (#179) Co-authored-by: Marcelo Trylesinski --- fuzz/fuzz_form.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/fuzz/fuzz_form.py b/fuzz/fuzz_form.py index c990639..9a3d854 100644 --- a/fuzz/fuzz_form.py +++ b/fuzz/fuzz_form.py @@ -29,8 +29,15 @@ def parse_form_urlencoded(fdp: EnhancedDataProvider) -> None: def parse_multipart_form_data(fdp: EnhancedDataProvider) -> None: - header = {"Content-Type": "multipart/form-data; boundary=--boundary"} - parse_form(header, io.BytesIO(fdp.ConsumeRandomBytes()), on_field, on_file) + boundary = "boundary" + header = {"Content-Type": f"multipart/form-data; boundary={boundary}"} + body = ( + f"--{boundary}\r\n" + f"Content-Type: multipart/form-data; boundary={boundary}\r\n\r\n" + f"{fdp.ConsumeRandomString()}\r\n" + f"--{boundary}--\r\n" + ) + parse_form(header, io.BytesIO(body.encode("latin1", errors="ignore")), on_field, on_file) def TestOneInput(data: bytes) -> None: From e53b541356981b2353914ef5dbf6a1b0605f31c5 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 23 Nov 2024 14:03:21 +0100 Subject: [PATCH 82/94] Create SECURITY.md (#187) --- SECURITY.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..759bd10 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +If you think you have identified a security issue with `python-multipart`, **do not open a public issue**. + +To responsibly report a security issue, please navigate to the Security tab for the repo and click "Report a vulnerability." + +![Screenshot of repo security tab showing "Report a vulnerability" button](https://github.com/encode/.github/raw/master/img/github-demos-private-vulnerability-reporting.png) + +Be sure to include as much detail as necessary in your report. As with reporting normal issues, a minimal reproducible example will help the maintainers address the issue faster. + +Thank you. From 170e6043ffeb8f9fb6ad622729f3eda3f45b98cb Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 24 Nov 2024 13:45:21 +0100 Subject: [PATCH 83/94] Update ruff & mypy (#188) --- noxfile.py | 8 +++- pyproject.toml | 4 +- uv.lock | 102 ++++++++++++++++++++++++++----------------------- 3 files changed, 62 insertions(+), 52 deletions(-) diff --git a/noxfile.py b/noxfile.py index 20de33e..8c12ee4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -14,8 +14,12 @@ def rename(session: nox.Session, editable: bool) -> None: assert "import python_multipart" not in session.run("python", "-c", "import multipart", silent=True) assert "import python_multipart" in session.run("python", "-Wdefault", "-c", "import multipart", silent=True) - assert "import python_multipart" in session.run("python", "-Wdefault", "-c", "import multipart.exceptions", silent=True) - assert "import python_multipart" in session.run("python", "-Wdefault", "-c", "from multipart import exceptions", silent=True) + assert "import python_multipart" in session.run( + "python", "-Wdefault", "-c", "import multipart.exceptions", silent=True + ) + assert "import python_multipart" in session.run( + "python", "-Wdefault", "-c", "from multipart import exceptions", silent=True + ) assert "import python_multipart" in session.run( "python", "-Wdefault", "-c", "from multipart.exceptions import FormParserError", silent=True ) diff --git a/pyproject.toml b/pyproject.toml index e22ea1d..01a907d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dev-dependencies = [ "PyYAML==6.0.1", "invoke==2.2.0", "pytest-timeout==2.3.1", - "ruff==0.3.4", + "ruff==0.8.0", "mypy", "types-PyYAML", "atheris==2.3.0; python_version != '3.12'", @@ -122,4 +122,4 @@ exclude_lines = [ ] [tool.check-sdist] -git-only = ["docs", "fuzz", "scripts", "mkdocs.yml", "uv.lock"] +git-only = ["docs", "fuzz", "scripts", "mkdocs.yml", "uv.lock", "SECURITY.md"] diff --git a/uv.lock b/uv.lock index 2ae1c4e..dbdacc9 100644 --- a/uv.lock +++ b/uv.lock @@ -526,41 +526,46 @@ wheels = [ [[package]] name = "mypy" -version = "1.11.2" +version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/86/5d7cbc4974fd564550b80fbb8103c05501ea11aa7835edf3351d90095896/mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", size = 3078806 } +sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/cd/815368cd83c3a31873e5e55b317551500b12f2d1d7549720632f32630333/mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a", size = 10939401 }, - { url = "https://files.pythonhosted.org/packages/f1/27/e18c93a195d2fad75eb96e1f1cbc431842c332e8eba2e2b77eaf7313c6b7/mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef", size = 10111697 }, - { url = "https://files.pythonhosted.org/packages/dc/08/cdc1fc6d0d5a67d354741344cc4aa7d53f7128902ebcbe699ddd4f15a61c/mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383", size = 12500508 }, - { url = "https://files.pythonhosted.org/packages/64/12/aad3af008c92c2d5d0720ea3b6674ba94a98cdb86888d389acdb5f218c30/mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8", size = 13020712 }, - { url = "https://files.pythonhosted.org/packages/03/e6/a7d97cc124a565be5e9b7d5c2a6ebf082379ffba99646e4863ed5bbcb3c3/mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7", size = 9567319 }, - { url = "https://files.pythonhosted.org/packages/e2/aa/cc56fb53ebe14c64f1fe91d32d838d6f4db948b9494e200d2f61b820b85d/mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385", size = 10859630 }, - { url = "https://files.pythonhosted.org/packages/04/c8/b19a760fab491c22c51975cf74e3d253b8c8ce2be7afaa2490fbf95a8c59/mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca", size = 10037973 }, - { url = "https://files.pythonhosted.org/packages/88/57/7e7e39f2619c8f74a22efb9a4c4eff32b09d3798335625a124436d121d89/mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104", size = 12416659 }, - { url = "https://files.pythonhosted.org/packages/fc/a6/37f7544666b63a27e46c48f49caeee388bf3ce95f9c570eb5cfba5234405/mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4", size = 12897010 }, - { url = "https://files.pythonhosted.org/packages/84/8b/459a513badc4d34acb31c736a0101c22d2bd0697b969796ad93294165cfb/mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6", size = 9562873 }, - { url = "https://files.pythonhosted.org/packages/35/3a/ed7b12ecc3f6db2f664ccf85cb2e004d3e90bec928e9d7be6aa2f16b7cdf/mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318", size = 10990335 }, - { url = "https://files.pythonhosted.org/packages/04/e4/1a9051e2ef10296d206519f1df13d2cc896aea39e8683302f89bf5792a59/mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", size = 10007119 }, - { url = "https://files.pythonhosted.org/packages/f3/3c/350a9da895f8a7e87ade0028b962be0252d152e0c2fbaafa6f0658b4d0d4/mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987", size = 12506856 }, - { url = "https://files.pythonhosted.org/packages/b6/49/ee5adf6a49ff13f4202d949544d3d08abb0ea1f3e7f2a6d5b4c10ba0360a/mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca", size = 12952066 }, - { url = "https://files.pythonhosted.org/packages/27/c0/b19d709a42b24004d720db37446a42abadf844d5c46a2c442e2a074d70d9/mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70", size = 9664000 }, - { url = "https://files.pythonhosted.org/packages/42/ad/5a8567700410f8aa7c755b0ebd4cacff22468cbc5517588773d65075c0cb/mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b", size = 10876550 }, - { url = "https://files.pythonhosted.org/packages/1b/bc/9fc16ea7a27ceb93e123d300f1cfe27a6dd1eac9a8beea4f4d401e737e9d/mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86", size = 10068086 }, - { url = "https://files.pythonhosted.org/packages/cd/8f/a1e460f1288405a13352dad16b24aba6dce4f850fc76510c540faa96eda3/mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce", size = 12459214 }, - { url = "https://files.pythonhosted.org/packages/c7/74/746b31aef7cc7512dab8bdc2311ef88d63fadc1c453a09c8cab7e57e59bf/mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1", size = 12962942 }, - { url = "https://files.pythonhosted.org/packages/28/a4/7fae712240b640d75bb859294ad4776b9960b3216ccb7fa747f578e6c632/mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b", size = 9545616 }, - { url = "https://files.pythonhosted.org/packages/16/64/bb5ed751487e2bea0dfaa6f640a7e3bb88083648f522e766d5ef4a76f578/mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6", size = 10937294 }, - { url = "https://files.pythonhosted.org/packages/a9/a3/67a0069abed93c3bf3b0bebb8857e2979a02828a4a3fd82f107f8f1143e8/mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70", size = 10107707 }, - { url = "https://files.pythonhosted.org/packages/2f/4d/0379daf4258b454b1f9ed589a9dabd072c17f97496daea7b72fdacf7c248/mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d", size = 12498367 }, - { url = "https://files.pythonhosted.org/packages/3b/dc/3976a988c280b3571b8eb6928882dc4b723a403b21735a6d8ae6ed20e82b/mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d", size = 13018014 }, - { url = "https://files.pythonhosted.org/packages/83/84/adffc7138fb970e7e2a167bd20b33bb78958370179853a4ebe9008139342/mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24", size = 9568056 }, - { url = "https://files.pythonhosted.org/packages/42/3a/bdf730640ac523229dd6578e8a581795720a9321399de494374afc437ec5/mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", size = 2619625 }, + { url = "https://files.pythonhosted.org/packages/5e/8c/206de95a27722b5b5a8c85ba3100467bd86299d92a4f71c6b9aa448bfa2f/mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a", size = 11020731 }, + { url = "https://files.pythonhosted.org/packages/ab/bb/b31695a29eea76b1569fd28b4ab141a1adc9842edde080d1e8e1776862c7/mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80", size = 10184276 }, + { url = "https://files.pythonhosted.org/packages/a5/2d/4a23849729bb27934a0e079c9c1aad912167d875c7b070382a408d459651/mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7", size = 12587706 }, + { url = "https://files.pythonhosted.org/packages/5c/c3/d318e38ada50255e22e23353a469c791379825240e71b0ad03e76ca07ae6/mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f", size = 13105586 }, + { url = "https://files.pythonhosted.org/packages/4a/25/3918bc64952370c3dbdbd8c82c363804678127815febd2925b7273d9482c/mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372", size = 9632318 }, + { url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027 }, + { url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699 }, + { url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263 }, + { url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688 }, + { url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 }, + { url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900 }, + { url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818 }, + { url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 }, + { url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 }, + { url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 }, + { url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 }, + { url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 }, + { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 }, + { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 }, + { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 }, + { url = "https://files.pythonhosted.org/packages/5e/2a/13e9ad339131c0fba5c70584f639005a47088f5eed77081a3d00479df0ca/mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a", size = 10955147 }, + { url = "https://files.pythonhosted.org/packages/94/39/02929067dc16b72d78109195cfed349ac4ec85f3d52517ac62b9a5263685/mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb", size = 10138373 }, + { url = "https://files.pythonhosted.org/packages/4a/cc/066709bb01734e3dbbd1375749f8789bf9693f8b842344fc0cf52109694f/mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b", size = 12543621 }, + { url = "https://files.pythonhosted.org/packages/f5/a2/124df839025348c7b9877d0ce134832a9249968e3ab36bb826bab0e9a1cf/mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74", size = 13050348 }, + { url = "https://files.pythonhosted.org/packages/45/86/cc94b1e7f7e756a63043cf425c24fb7470013ee1c032180282db75b1b335/mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6", size = 9615311 }, + { url = "https://files.pythonhosted.org/packages/5f/d4/b33ddd40dad230efb317898a2d1c267c04edba73bc5086bf77edeb410fb2/mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", size = 11013906 }, + { url = "https://files.pythonhosted.org/packages/f4/e6/f414bca465b44d01cd5f4a82761e15044bedd1bf8025c5af3cc64518fac5/mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732", size = 10180657 }, + { url = "https://files.pythonhosted.org/packages/38/e9/fc3865e417722f98d58409770be01afb961e2c1f99930659ff4ae7ca8b7e/mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc", size = 12586394 }, + { url = "https://files.pythonhosted.org/packages/2e/35/f4d8b6d2cb0b3dad63e96caf159419dda023f45a358c6c9ac582ccaee354/mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d", size = 13103591 }, + { url = "https://files.pythonhosted.org/packages/22/1d/80594aef135f921dd52e142fa0acd19df197690bd0cde42cea7b88cf5aa2/mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24", size = 9634690 }, + { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 }, ] [[package]] @@ -713,7 +718,7 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.11" +version = "0.0.17" source = { editable = "." } [package.dev-dependencies] @@ -762,7 +767,7 @@ dev = [ { name = "pytest-cov", specifier = "==5.0.0" }, { name = "pytest-timeout", specifier = "==2.3.1" }, { name = "pyyaml", specifier = "==6.0.1" }, - { name = "ruff", specifier = "==0.3.4" }, + { name = "ruff", specifier = "==0.8.0" }, { name = "types-pyyaml" }, ] @@ -951,26 +956,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.3.4" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a0/98/91e1ad8a6777c300b15cad46a1b507375010f8a53cfeaa17f0385bde1103/ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1", size = 2129882 } +sdist = { url = "https://files.pythonhosted.org/packages/b2/d6/a2373f3ba7180ddb44420d2a9d1f1510e1a4d162b3d27282bedcb09c8da9/ruff-0.8.0.tar.gz", hash = "sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44", size = 3276537 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/61/797dce050c288fc8325e6b723baa1dd6aff4851ee1b769350b54fd3e0fe5/ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4", size = 16472324 }, - { url = "https://files.pythonhosted.org/packages/b9/3c/5025d7eee9dd76abb489c1a98c05797e1889329abf8b8b4efcd7095e74f5/ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91", size = 8447934 }, - { url = "https://files.pythonhosted.org/packages/5e/c3/2e6aca190ac828dc94bf86384e89513a4a987816c6ddd6a1db4fca0fdd17/ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854", size = 8106257 }, - { url = "https://files.pythonhosted.org/packages/03/92/57b9193e5600445a20d331c9a23dc6c17d27fc50642315bde6fbdaa83499/ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7", size = 7470593 }, - { url = "https://files.pythonhosted.org/packages/93/80/26e4cc40921d759bbdf49b898861aeaf7e1bed80001fc26073a97aac613f/ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365", size = 8635128 }, - { url = "https://files.pythonhosted.org/packages/b5/42/b90b05d167c056aeb71b954cb61fad97a61aaea2a4d5e4e6cba4570c8221/ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369", size = 9389231 }, - { url = "https://files.pythonhosted.org/packages/8e/d7/cd9e7e8d8ca4034577fd28e9ff11551df8d2df9e77a16eecee12121d0f7d/ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c", size = 9094312 }, - { url = "https://files.pythonhosted.org/packages/f6/bb/c583d2a0c8e91ee84a13c31b714070a89863348bbecd2e31ca6ed9b18924/ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9", size = 9909854 }, - { url = "https://files.pythonhosted.org/packages/2e/95/ec159b3cae9960811fe573586ca905578ff78d33f025ae054d30ef6c2b73/ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50", size = 8658269 }, - { url = "https://files.pythonhosted.org/packages/0e/27/13e2cf723209f8e8169de81d4be5b985ff46549b452d112d3e36899ec2ef/ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed", size = 8008722 }, - { url = "https://files.pythonhosted.org/packages/c6/04/036aa4328dfcb50009e80baac7bc78b8532ea9e8c0b6a1d4b75a684301a5/ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488", size = 7463983 }, - { url = "https://files.pythonhosted.org/packages/32/cc/728245664c1fe2adbe90af1044ff2f548527ed12fc607bae74043387990f/ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e", size = 8232832 }, - { url = "https://files.pythonhosted.org/packages/a1/ff/88a45e7b8b87c7a8dac38786ebb800325f9523a9af89f21382104874d9d9/ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378", size = 8705875 }, - { url = "https://files.pythonhosted.org/packages/b9/4b/290e829a7c33fa996f0a598f2cdc954b4820262bb027e0a2edd888c3600d/ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102", size = 7645340 }, - { url = "https://files.pythonhosted.org/packages/09/a1/ecbd844e714a4bed4b9072f5a73bbdc2a3a6e6ee9d9c5b3962be83d5bac8/ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6", size = 8436394 }, - { url = "https://files.pythonhosted.org/packages/f3/c4/afb3bb366074fa98faeb6389618bf10b3eb00bd1eb48d980c205da9b2022/ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232", size = 7991316 }, + { url = "https://files.pythonhosted.org/packages/ec/77/e889ee3ce7fd8baa3ed1b77a03b9fb8ec1be68be1418261522fd6a5405e0/ruff-0.8.0-py3-none-linux_armv6l.whl", hash = "sha256:fcb1bf2cc6706adae9d79c8d86478677e3bbd4ced796ccad106fd4776d395fea", size = 10518283 }, + { url = "https://files.pythonhosted.org/packages/da/c8/0a47de01edf19fb22f5f9b7964f46a68d0bdff20144d134556ffd1ba9154/ruff-0.8.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:295bb4c02d58ff2ef4378a1870c20af30723013f441c9d1637a008baaf928c8b", size = 10317691 }, + { url = "https://files.pythonhosted.org/packages/41/17/9885e4a0eeae07abd2a4ebabc3246f556719f24efa477ba2739146c4635a/ruff-0.8.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b1f1c76b47c18fa92ee78b60d2d20d7e866c55ee603e7d19c1e991fad933a9a", size = 9940999 }, + { url = "https://files.pythonhosted.org/packages/3e/cd/46b6f7043597eb318b5f5482c8ae8f5491cccce771e85f59d23106f2d179/ruff-0.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb0d4f250a7711b67ad513fde67e8870109e5ce590a801c3722580fe98c33a99", size = 10772437 }, + { url = "https://files.pythonhosted.org/packages/5d/87/afc95aeb8bc78b1d8a3461717a4419c05aa8aa943d4c9cbd441630f85584/ruff-0.8.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e55cce9aa93c5d0d4e3937e47b169035c7e91c8655b0974e61bb79cf398d49c", size = 10299156 }, + { url = "https://files.pythonhosted.org/packages/65/fa/04c647bb809c4d65e8eae1ed1c654d9481b21dd942e743cd33511687b9f9/ruff-0.8.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f4cd64916d8e732ce6b87f3f5296a8942d285bbbc161acee7fe561134af64f9", size = 11325819 }, + { url = "https://files.pythonhosted.org/packages/90/26/7dad6e7d833d391a8a1afe4ee70ca6f36c4a297d3cca83ef10e83e9aacf3/ruff-0.8.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c5c1466be2a2ebdf7c5450dd5d980cc87c8ba6976fb82582fea18823da6fa362", size = 12023927 }, + { url = "https://files.pythonhosted.org/packages/24/a0/be5296dda6428ba8a13bda8d09fbc0e14c810b485478733886e61597ae2b/ruff-0.8.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2dabfd05b96b7b8f2da00d53c514eea842bff83e41e1cceb08ae1966254a51df", size = 11589702 }, + { url = "https://files.pythonhosted.org/packages/26/3f/7602eb11d2886db545834182a9dbe500b8211fcbc9b4064bf9d358bbbbb4/ruff-0.8.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:facebdfe5a5af6b1588a1d26d170635ead6892d0e314477e80256ef4a8470cf3", size = 12782936 }, + { url = "https://files.pythonhosted.org/packages/4c/5d/083181bdec4ec92a431c1291d3fff65eef3ded630a4b55eb735000ef5f3b/ruff-0.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a8e86bae0dbd749c815211ca11e3a7bd559b9710746c559ed63106d382bd9c", size = 11138488 }, + { url = "https://files.pythonhosted.org/packages/b7/23/c12cdef58413cee2436d6a177aa06f7a366ebbca916cf10820706f632459/ruff-0.8.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85e654f0ded7befe2d61eeaf3d3b1e4ef3894469cd664ffa85006c7720f1e4a2", size = 10744474 }, + { url = "https://files.pythonhosted.org/packages/29/61/a12f3b81520083cd7c5caa24ba61bb99fd1060256482eff0ef04cc5ccd1b/ruff-0.8.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:83a55679c4cb449fa527b8497cadf54f076603cc36779b2170b24f704171ce70", size = 10369029 }, + { url = "https://files.pythonhosted.org/packages/08/2a/c013f4f3e4a54596c369cee74c24870ed1d534f31a35504908b1fc97017a/ruff-0.8.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:812e2052121634cf13cd6fddf0c1871d0ead1aad40a1a258753c04c18bb71bbd", size = 10867481 }, + { url = "https://files.pythonhosted.org/packages/d5/f7/685b1e1d42a3e94ceb25eab23c70bdd8c0ab66a43121ef83fe6db5a58756/ruff-0.8.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:780d5d8523c04202184405e60c98d7595bdb498c3c6abba3b6d4cdf2ca2af426", size = 11237117 }, + { url = "https://files.pythonhosted.org/packages/03/20/401132c0908e8837625e3b7e32df9962e7cd681a4df1e16a10e2a5b4ecda/ruff-0.8.0-py3-none-win32.whl", hash = "sha256:5fdb6efecc3eb60bba5819679466471fd7d13c53487df7248d6e27146e985468", size = 8783511 }, + { url = "https://files.pythonhosted.org/packages/1d/5c/4d800fca7854f62ad77f2c0d99b4b585f03e2d87a6ec1ecea85543a14a3c/ruff-0.8.0-py3-none-win_amd64.whl", hash = "sha256:582891c57b96228d146725975fbb942e1f30a0c4ba19722e692ca3eb25cc9b4f", size = 9559876 }, + { url = "https://files.pythonhosted.org/packages/5b/bc/cc8a6a5ca4960b226dc15dd8fb511dd11f2014ff89d325c0b9b9faa9871f/ruff-0.8.0-py3-none-win_arm64.whl", hash = "sha256:ba93e6294e9a737cd726b74b09a6972e36bb511f9a102f1d9a7e1ce94dd206a6", size = 8939733 }, ] [[package]] From 9205a0ec8c646b9f705430a6bfb52bd957b76c19 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 28 Nov 2024 20:10:45 +0100 Subject: [PATCH 84/94] Hard break if found data after last boundary on `MultipartParser` (#189) --- python_multipart/multipart.py | 8 +++---- tests/test_multipart.py | 40 ++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/python_multipart/multipart.py b/python_multipart/multipart.py index ace4a8f..be76d24 100644 --- a/python_multipart/multipart.py +++ b/python_multipart/multipart.py @@ -1105,7 +1105,6 @@ def data_callback(name: CallbackName, end_i: int, remaining: bool = False) -> No # Skip leading newlines if c == CR or c == LF: i += 1 - self.logger.debug("Skipping leading CR/LF at %d", i) continue # index is used as in index into our boundary. Set to 0. @@ -1398,9 +1397,10 @@ def data_callback(name: CallbackName, end_i: int, remaining: bool = False) -> No i -= 1 elif state == MultipartState.END: - # Do nothing and just consume a byte in the end state. - if c not in (CR, LF): - self.logger.warning("Consuming a byte '0x%x' in the end state", c) # pragma: no cover + # Skip data after the last boundary. + self.logger.warning("Skipping data after last boundary") + i = length + break else: # pragma: no cover (error case) # We got into a strange state somehow! Just stop processing. diff --git a/tests/test_multipart.py b/tests/test_multipart.py index be01fbf..7fbeff7 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -825,7 +825,7 @@ def test_http(self, param: TestParams) -> None: return # No error! - self.assertEqual(processed, len(param["test"])) + self.assertEqual(processed, len(param["test"]), param["name"]) # Assert that the parser gave us the appropriate fields/files. for e in param["result"]["expected"]: @@ -1210,6 +1210,44 @@ def on_field(f: FieldProtocol) -> None: self.assertEqual(fields[2].field_name, b"baz") self.assertEqual(fields[2].value, b"asdf") + def test_multipart_parser_newlines_before_first_boundary(self) -> None: + """This test makes sure that the parser does not handle when there is junk data after the last boundary.""" + num = 5_000_000 + data = ( + "\r\n" * num + "--boundary\r\n" + 'Content-Disposition: form-data; name="file"; filename="filename.txt"\r\n' + "Content-Type: text/plain\r\n\r\n" + "hello\r\n" + "--boundary--" + ) + + files: list[File] = [] + + def on_file(f: FileProtocol) -> None: + files.append(cast(File, f)) + + f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary") + f.write(data.encode("latin-1")) + + def test_multipart_parser_data_after_last_boundary(self) -> None: + """This test makes sure that the parser does not handle when there is junk data after the last boundary.""" + num = 50_000_000 + data = ( + "--boundary\r\n" + 'Content-Disposition: form-data; name="file"; filename="filename.txt"\r\n' + "Content-Type: text/plain\r\n\r\n" + "hello\r\n" + "--boundary--" + "-" * num + "\r\n" + ) + + files: list[File] = [] + + def on_file(f: FileProtocol) -> None: + files.append(cast(File, f)) + + f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary") + f.write(data.encode("latin-1")) + def test_max_size_multipart(self) -> None: # Load test data. test_file = "single_field_single_file.http" From 5b1aed83adadbff1677779cd0df53723cd80a0d6 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 28 Nov 2024 20:14:32 +0100 Subject: [PATCH 85/94] Version 0.0.18 (#191) --- CHANGELOG.md | 4 ++++ python_multipart/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a6fbba..2c7faf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.0.18 (2024-11-28) + +* Hard break if found data after last boundary on `MultipartParser` [#189](https://github.com/Kludex/python-multipart/pull/189). + ## 0.0.17 (2024-10-31) * Handle PermissionError in fallback code for old import name [#182](https://github.com/Kludex/python-multipart/pull/182). diff --git a/python_multipart/__init__.py b/python_multipart/__init__.py index be8327f..69a3ed4 100644 --- a/python_multipart/__init__.py +++ b/python_multipart/__init__.py @@ -2,7 +2,7 @@ __author__ = "Andrew Dunham" __license__ = "Apache" __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" -__version__ = "0.0.17" +__version__ = "0.0.18" from .multipart import ( BaseParser, From c4fe4d3cebc08c660e57dd709af1ffa7059b3177 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 1 Dec 2024 07:59:34 +0100 Subject: [PATCH 86/94] Don't warn when CRLF is found after last boundary (#193) --- CHANGELOG.md | 4 ++++ python_multipart/__init__.py | 2 +- python_multipart/multipart.py | 4 ++++ scripts/check | 2 +- tests/test_multipart.py | 26 ++++++++++++++++++++++++++ 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c7faf0..50074c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.0.19 (2024-11-30) + +* Don't warn when CRLF is found after last boundary on `MultipartParser` [#193](https://github.com/Kludex/python-multipart/pull/193). + ## 0.0.18 (2024-11-28) * Hard break if found data after last boundary on `MultipartParser` [#189](https://github.com/Kludex/python-multipart/pull/189). diff --git a/python_multipart/__init__.py b/python_multipart/__init__.py index 69a3ed4..d555f80 100644 --- a/python_multipart/__init__.py +++ b/python_multipart/__init__.py @@ -2,7 +2,7 @@ __author__ = "Andrew Dunham" __license__ = "Apache" __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" -__version__ = "0.0.18" +__version__ = "0.0.19" from .multipart import ( BaseParser, diff --git a/python_multipart/multipart.py b/python_multipart/multipart.py index be76d24..a996379 100644 --- a/python_multipart/multipart.py +++ b/python_multipart/multipart.py @@ -1397,6 +1397,10 @@ def data_callback(name: CallbackName, end_i: int, remaining: bool = False) -> No i -= 1 elif state == MultipartState.END: + # Don't do anything if chunk ends with CRLF. + if c == CR and i + 1 < length and data[i + 1] == LF: + i += 2 + continue # Skip data after the last boundary. self.logger.warning("Skipping data after last boundary") i = length diff --git a/scripts/check b/scripts/check index 13ce9ed..bc37333 100755 --- a/scripts/check +++ b/scripts/check @@ -6,5 +6,5 @@ SOURCE_FILES="python_multipart multipart tests" uvx ruff format --check --diff $SOURCE_FILES uvx ruff check $SOURCE_FILES -uvx --with types-PyYAML mypy $SOURCE_FILES +uv run mypy $SOURCE_FILES uvx check-sdist diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 7fbeff7..ce92ff4 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import os import random import sys @@ -9,6 +10,7 @@ from typing import TYPE_CHECKING, cast from unittest.mock import Mock +import pytest import yaml from python_multipart.decoders import Base64Decoder, QuotedPrintableDecoder @@ -1248,6 +1250,30 @@ def on_file(f: FileProtocol) -> None: f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary") f.write(data.encode("latin-1")) + @pytest.fixture(autouse=True) + def inject_fixtures(self, caplog: pytest.LogCaptureFixture) -> None: + self._caplog = caplog + + def test_multipart_parser_data_end_with_crlf_without_warnings(self) -> None: + """This test makes sure that the parser does not handle when the data ends with a CRLF.""" + data = ( + "--boundary\r\n" + 'Content-Disposition: form-data; name="file"; filename="filename.txt"\r\n' + "Content-Type: text/plain\r\n\r\n" + "hello\r\n" + "--boundary--\r\n" + ) + + files: list[File] = [] + + def on_file(f: FileProtocol) -> None: + files.append(cast(File, f)) + + f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary") + with self._caplog.at_level(logging.WARNING): + f.write(data.encode("latin-1")) + assert len(self._caplog.records) == 0 + def test_max_size_multipart(self) -> None: # Load test data. test_file = "single_field_single_file.http" From 6f3295bc79a1f8decdb23ce1720a6428908d8e33 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 07:09:56 +0000 Subject: [PATCH 87/94] Bump astral-sh/setup-uv from 3 to 4 in the github-actions group (#194) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Marcelo Trylesinski --- .github/workflows/docs.yml | 2 +- .github/workflows/main.yml | 2 +- .github/workflows/publish.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b970488..08a70f6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,7 +20,7 @@ jobs: git config user.email 41898282+github-actions[bot]@users.noreply.github.com - name: Install uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v4 with: version: "0.4.12" enable-cache: true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c3d9d99..05a5597 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v4 with: version: "0.4.12" enable-cache: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 66915ad..4208117 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v4 with: version: "0.4.12" enable-cache: true From 4bffa0c7c6c836ace85486b95c1e144e340059d8 Mon Sep 17 00:00:00 2001 From: yecril23pl <151100823+yecril23pl@users.noreply.github.com> Date: Fri, 6 Dec 2024 08:15:24 +0100 Subject: [PATCH 88/94] doc: A file parameter is not a field (#127) Co-authored-by: Marcelo Trylesinski --- docs/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index 7802011..c84bb99 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,10 +16,10 @@ def simple_app(environ, start_response): # The following two callbacks just append the name to the return value. def on_field(field): - ret.append(b"Parsed field named: %s" % (field.field_name,)) + ret.append(b"Parsed value parameter named: %s" % (field.field_name,)) def on_file(file): - ret.append(b"Parsed file named: %s" % (file.field_name,)) + ret.append(b"Parsed file parameter named: %s" % (file.field_name,)) # Create headers object. We need to convert from WSGI to the actual # name of the header, since this library does not assume that you are @@ -55,7 +55,7 @@ Date: Sun, 07 Apr 2013 01:40:52 GMT Server: WSGIServer/0.1 Python/2.7.3 Content-type: text/plain -Parsed field named: foo +Parsed value parameter named: foo ``` For a more in-depth example showing how the various parts fit together, check out the next section. From f1c5a2821b24786f418ae535aa2fbb5ae4c60d6c Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Fri, 6 Dec 2024 13:05:43 +0530 Subject: [PATCH 89/94] feat: Add python 3.13 in CI matrix. (#185) Co-authored-by: Marcelo Trylesinski --- .github/workflows/main.yml | 9 +++------ pyproject.toml | 2 +- scripts/check | 1 + uv.lock | 6 +++--- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 05a5597..13495cf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,21 +11,18 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v4 with: - version: "0.4.12" + python-version: ${{ matrix.python-version }} enable-cache: true - - name: Set up Python ${{ matrix.python-version }} - run: uv python install ${{ matrix.python-version }} - - name: Install dependencies - run: uv sync --python ${{ matrix.python-version }} --frozen + run: uv sync --frozen - name: Run linters run: scripts/check diff --git a/pyproject.toml b/pyproject.toml index 01a907d..48d12a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dev-dependencies = [ "ruff==0.8.0", "mypy", "types-PyYAML", - "atheris==2.3.0; python_version != '3.12'", + "atheris==2.3.0; python_version <= '3.11'", # Documentation "mkdocs", "mkdocs-material", diff --git a/scripts/check b/scripts/check index bc37333..294cb9f 100755 --- a/scripts/check +++ b/scripts/check @@ -8,3 +8,4 @@ uvx ruff format --check --diff $SOURCE_FILES uvx ruff check $SOURCE_FILES uv run mypy $SOURCE_FILES uvx check-sdist +uv lock diff --git a/uv.lock b/uv.lock index dbdacc9..21940b3 100644 --- a/uv.lock +++ b/uv.lock @@ -718,12 +718,12 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.17" +version = "0.0.19" source = { editable = "." } [package.dev-dependencies] dev = [ - { name = "atheris", marker = "python_full_version != '3.12.*'" }, + { name = "atheris", marker = "python_full_version < '3.12'" }, { name = "atomicwrites" }, { name = "attrs" }, { name = "coverage" }, @@ -749,7 +749,7 @@ dev = [ [package.metadata.requires-dev] dev = [ - { name = "atheris", marker = "python_full_version != '3.12.*'", specifier = "==2.3.0" }, + { name = "atheris", marker = "python_full_version < '3.12'", specifier = "==2.3.0" }, { name = "atomicwrites", specifier = "==1.4.1" }, { name = "attrs", specifier = "==23.2.0" }, { name = "coverage", specifier = "==7.4.4" }, From 04d3cf5ef58c8ac8d28d36ea410fba131f5eff3f Mon Sep 17 00:00:00 2001 From: John Stark Date: Wed, 11 Dec 2024 16:42:43 +0000 Subject: [PATCH 90/94] Handle messages containing only end boundary, fixes #38 (#142) --- python_multipart/multipart.py | 24 +++++++++++++++---- tests/test_data/http/empty_message.http | 1 + tests/test_data/http/empty_message.yaml | 2 ++ .../http/empty_message_with_bad_end.http | 1 + .../http/empty_message_with_bad_end.yaml | 3 +++ 5 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 tests/test_data/http/empty_message.http create mode 100644 tests/test_data/http/empty_message.yaml create mode 100644 tests/test_data/http/empty_message_with_bad_end.http create mode 100644 tests/test_data/http/empty_message_with_bad_end.yaml diff --git a/python_multipart/multipart.py b/python_multipart/multipart.py index a996379..f26a815 100644 --- a/python_multipart/multipart.py +++ b/python_multipart/multipart.py @@ -130,7 +130,8 @@ class MultipartState(IntEnum): PART_DATA_START = 8 PART_DATA = 9 PART_DATA_END = 10 - END = 11 + END_BOUNDARY = 11 + END = 12 # Flags for the multipart parser. @@ -1119,7 +1120,10 @@ def data_callback(name: CallbackName, end_i: int, remaining: bool = False) -> No # Check to ensure that the last 2 characters in our boundary # are CRLF. if index == len(boundary) - 2: - if c != CR: + if c == HYPHEN: + # Potential empty message. + state = MultipartState.END_BOUNDARY + elif c != CR: # Error! msg = "Did not find CR at end of boundary (%d)" % (i,) self.logger.warning(msg) @@ -1396,6 +1400,18 @@ def data_callback(name: CallbackName, end_i: int, remaining: bool = False) -> No # the start of the boundary itself. i -= 1 + elif state == MultipartState.END_BOUNDARY: + if index == len(boundary) - 2 + 1: + if c != HYPHEN: + msg = "Did not find - at end of boundary (%d)" % (i,) + self.logger.warning(msg) + e = MultipartParseError(msg) + e.offset = i + raise e + index += 1 + self.callback("end") + state = MultipartState.END + elif state == MultipartState.END: # Don't do anything if chunk ends with CRLF. if c == CR and i + 1 < length and data[i + 1] == LF: @@ -1707,8 +1723,8 @@ def on_headers_finished() -> None: def _on_end() -> None: nonlocal writer - assert writer is not None - writer.finalize() + if writer is not None: + writer.finalize() if self.on_end is not None: self.on_end() diff --git a/tests/test_data/http/empty_message.http b/tests/test_data/http/empty_message.http new file mode 100644 index 0000000..baff7d5 --- /dev/null +++ b/tests/test_data/http/empty_message.http @@ -0,0 +1 @@ +----boundary-- diff --git a/tests/test_data/http/empty_message.yaml b/tests/test_data/http/empty_message.yaml new file mode 100644 index 0000000..ab33940 --- /dev/null +++ b/tests/test_data/http/empty_message.yaml @@ -0,0 +1,2 @@ +boundary: --boundary +expected: [] diff --git a/tests/test_data/http/empty_message_with_bad_end.http b/tests/test_data/http/empty_message_with_bad_end.http new file mode 100644 index 0000000..a085714 --- /dev/null +++ b/tests/test_data/http/empty_message_with_bad_end.http @@ -0,0 +1 @@ +----boundary-X diff --git a/tests/test_data/http/empty_message_with_bad_end.yaml b/tests/test_data/http/empty_message_with_bad_end.yaml new file mode 100644 index 0000000..ae920bf --- /dev/null +++ b/tests/test_data/http/empty_message_with_bad_end.yaml @@ -0,0 +1,3 @@ +boundary: --boundary +expected: + error: 13 From b083cef4d6c68cf036bae1d9c68a986c6e1e3cc4 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 16 Dec 2024 20:44:25 +0100 Subject: [PATCH 91/94] Version 0.0.20 (#197) --- CHANGELOG.md | 4 ++++ python_multipart/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50074c8..f0d80aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.0.20 (2024-12-16) + +* Handle messages containing only end boundary [#142](https://github.com/Kludex/python-multipart/pull/142). + ## 0.0.19 (2024-11-30) * Don't warn when CRLF is found after last boundary on `MultipartParser` [#193](https://github.com/Kludex/python-multipart/pull/193). diff --git a/python_multipart/__init__.py b/python_multipart/__init__.py index d555f80..e426526 100644 --- a/python_multipart/__init__.py +++ b/python_multipart/__init__.py @@ -2,7 +2,7 @@ __author__ = "Andrew Dunham" __license__ = "Apache" __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham" -__version__ = "0.0.19" +__version__ = "0.0.20" from .multipart import ( BaseParser, From 3e909f52a774365b8817a52d4312db4ba8061d07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jan 2025 09:47:37 +0100 Subject: [PATCH 92/94] Bump astral-sh/setup-uv from 4 to 5 in the github-actions group (#198) --- .github/workflows/docs.yml | 2 +- .github/workflows/main.yml | 2 +- .github/workflows/publish.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 08a70f6..d53c7ca 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,7 +20,7 @@ jobs: git config user.email 41898282+github-actions[bot]@users.noreply.github.com - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v5 with: version: "0.4.12" enable-cache: true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 13495cf..14f485e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v5 with: python-version: ${{ matrix.python-version }} enable-cache: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4208117..310b67b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v5 with: version: "0.4.12" enable-cache: true From 7aa8d9932b6b20c0a1969e672433044e27a3fbc1 Mon Sep 17 00:00:00 2001 From: Waket Zheng Date: Tue, 29 Apr 2025 14:21:40 +0800 Subject: [PATCH 93/94] Bump ruff from 0.8.0 to 0.11.7 (#203) --- pyproject.toml | 3 +- python_multipart/multipart.py | 2 +- uv.lock | 1497 ++++++++++++++++++++------------- 3 files changed, 919 insertions(+), 583 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 48d12a2..a005b49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ classifiers = [ 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Topic :: Software Development :: Libraries :: Python Modules', ] dependencies = [] @@ -44,7 +45,7 @@ dev-dependencies = [ "PyYAML==6.0.1", "invoke==2.2.0", "pytest-timeout==2.3.1", - "ruff==0.8.0", + "ruff==0.11.7", "mypy", "types-PyYAML", "atheris==2.3.0; python_version <= '3.11'", diff --git a/python_multipart/multipart.py b/python_multipart/multipart.py index f26a815..6c84829 100644 --- a/python_multipart/multipart.py +++ b/python_multipart/multipart.py @@ -1241,7 +1241,7 @@ def data_callback(name: CallbackName, end_i: int, remaining: bool = False) -> No elif state == MultipartState.HEADER_VALUE_ALMOST_DONE: # The last character should be a LF. If not, it's an error. if c != LF: - msg = "Did not find LF character at end of header " "(found %r)" % (c,) + msg = "Did not find LF character at end of header (found %r)" % (c,) self.logger.warning(msg) e = MultipartParseError(msg) e.offset = i diff --git a/uv.lock b/uv.lock index 21940b3..decebaa 100644 --- a/uv.lock +++ b/uv.lock @@ -1,229 +1,269 @@ version = 1 +revision = 2 requires-python = ">=3.8" +resolution-markers = [ + "python_full_version >= '3.9'", + "python_full_version < '3.9'", +] [[package]] name = "astunparse" version = "1.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six" }, - { name = "wheel" }, + { name = "six", marker = "python_full_version < '3.9'" }, + { name = "wheel", marker = "python_full_version < '3.9'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290 } +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290, upload_time = "2019-12-22T18:12:13.129Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732 }, + { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732, upload_time = "2019-12-22T18:12:11.297Z" }, ] [[package]] name = "atheris" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/80/65938e910e1b61ecf2a66339f2e1860b84d1d0f0e604a0b08910d00707a5/atheris-2.3.0.tar.gz", hash = "sha256:cf1fdf5fa220a41a2f262b32363fc566549502b2cb0addf4e1baad5531c0e825", size = 304139 } +sdist = { url = "https://files.pythonhosted.org/packages/3f/80/65938e910e1b61ecf2a66339f2e1860b84d1d0f0e604a0b08910d00707a5/atheris-2.3.0.tar.gz", hash = "sha256:cf1fdf5fa220a41a2f262b32363fc566549502b2cb0addf4e1baad5531c0e825", size = 304139, upload_time = "2023-08-29T20:53:14.063Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/db/d141a3ac3974d267946b49abaeedea999725eca44a34534b8c5450356e8f/atheris-2.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91cb296d60915c3efa4f6db48f09c4678b574cddb7ca98035f1cb9d9fb96f64", size = 30877874 }, - { url = "https://files.pythonhosted.org/packages/00/01/3a2a31c0016233599b12e8fa6c956ec6c1df78630d8cec9cfa78904d0013/atheris-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4e43d1ee4760916a84ff73c9c6cf9ac6eee80fc030479bbed43fe0b8e994981", size = 31182450 }, - { url = "https://files.pythonhosted.org/packages/38/35/00c7d7da1ee1789d0fbed6e4109c3b7d08eac1ac85c7ad73c9920d99a00a/atheris-2.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9877b6c2bd5386f9fbbb2989a939c717383d9639f5476411c06fe50fe2fe09a6", size = 30875022 }, - { url = "https://files.pythonhosted.org/packages/00/1e/0529d9dff0b3c8a69276df192cfb24cc35e5ef3304e9301ed5cf148682c6/atheris-2.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:005bde4b5a70e998b7fa097e9aa195972dcc2e04092156a0149cff7aa0de970e", size = 30877792 }, + { url = "https://files.pythonhosted.org/packages/50/db/d141a3ac3974d267946b49abaeedea999725eca44a34534b8c5450356e8f/atheris-2.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91cb296d60915c3efa4f6db48f09c4678b574cddb7ca98035f1cb9d9fb96f64", size = 30877874, upload_time = "2023-08-29T20:52:55.649Z" }, + { url = "https://files.pythonhosted.org/packages/00/01/3a2a31c0016233599b12e8fa6c956ec6c1df78630d8cec9cfa78904d0013/atheris-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4e43d1ee4760916a84ff73c9c6cf9ac6eee80fc030479bbed43fe0b8e994981", size = 31182450, upload_time = "2023-08-29T20:52:58.899Z" }, + { url = "https://files.pythonhosted.org/packages/38/35/00c7d7da1ee1789d0fbed6e4109c3b7d08eac1ac85c7ad73c9920d99a00a/atheris-2.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9877b6c2bd5386f9fbbb2989a939c717383d9639f5476411c06fe50fe2fe09a6", size = 30875022, upload_time = "2023-08-29T20:53:08.449Z" }, + { url = "https://files.pythonhosted.org/packages/00/1e/0529d9dff0b3c8a69276df192cfb24cc35e5ef3304e9301ed5cf148682c6/atheris-2.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:005bde4b5a70e998b7fa097e9aa195972dcc2e04092156a0149cff7aa0de970e", size = 30877792, upload_time = "2023-08-29T20:53:11.264Z" }, ] [[package]] name = "atomicwrites" version = "1.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/87/c6/53da25344e3e3a9c01095a89f16dbcda021c609ddb42dd6d7c0528236fb2/atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11", size = 14227 } +sdist = { url = "https://files.pythonhosted.org/packages/87/c6/53da25344e3e3a9c01095a89f16dbcda021c609ddb42dd6d7c0528236fb2/atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11", size = 14227, upload_time = "2022-07-08T18:31:40.459Z" } [[package]] name = "attrs" version = "23.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/fc/f800d51204003fa8ae392c4e8278f256206e7a919b708eef054f5f4b650d/attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", size = 780820 } +sdist = { url = "https://files.pythonhosted.org/packages/e3/fc/f800d51204003fa8ae392c4e8278f256206e7a919b708eef054f5f4b650d/attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", size = 780820, upload_time = "2023-12-31T06:30:32.926Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/44/827b2a91a5816512fcaf3cc4ebc465ccd5d598c45cefa6703fcf4a79018f/attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1", size = 60752 }, + { url = "https://files.pythonhosted.org/packages/e0/44/827b2a91a5816512fcaf3cc4ebc465ccd5d598c45cefa6703fcf4a79018f/attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1", size = 60752, upload_time = "2023-12-31T06:30:30.772Z" }, ] [[package]] name = "babel" -version = "2.16.0" +version = "2.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytz", marker = "python_full_version < '3.9'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload_time = "2025-02-01T15:17:41.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload_time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "backrefs" +version = "5.7.post1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/df/30/903f35159c87ff1d92aa3fcf8cb52de97632a21e0ae43ed940f5d033e01a/backrefs-5.7.post1.tar.gz", hash = "sha256:8b0f83b770332ee2f1c8244f4e03c77d127a0fa529328e6a0e77fa25bee99678", size = 6582270, upload_time = "2024-06-16T18:38:20.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/bb/47fc255d1060dcfd55b460236380edd8ebfc5b2a42a0799ca90c9fc983e3/backrefs-5.7.post1-py310-none-any.whl", hash = "sha256:c5e3fd8fd185607a7cb1fefe878cfb09c34c0be3c18328f12c574245f1c0287e", size = 380429, upload_time = "2024-06-16T18:38:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/39ef491caef3abae945f5a5fd72830d3b596bfac0630508629283585e213/backrefs-5.7.post1-py311-none-any.whl", hash = "sha256:712ea7e494c5bf3291156e28954dd96d04dc44681d0e5c030adf2623d5606d51", size = 392234, upload_time = "2024-06-16T18:38:12.283Z" }, + { url = "https://files.pythonhosted.org/packages/6a/00/33403f581b732ca70fdebab558e8bbb426a29c34e0c3ed674a479b74beea/backrefs-5.7.post1-py312-none-any.whl", hash = "sha256:a6142201c8293e75bce7577ac29e1a9438c12e730d73a59efdd1b75528d1a6c5", size = 398110, upload_time = "2024-06-16T18:38:14.257Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ea/df0ac74a26838f6588aa012d5d801831448b87d0a7d0aefbbfabbe894870/backrefs-5.7.post1-py38-none-any.whl", hash = "sha256:ec61b1ee0a4bfa24267f6b67d0f8c5ffdc8e0d7dc2f18a2685fd1d8d9187054a", size = 369477, upload_time = "2024-06-16T18:38:16.196Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e8/e43f535c0a17a695e5768670fc855a0e5d52dc0d4135b3915bfa355f65ac/backrefs-5.7.post1-py39-none-any.whl", hash = "sha256:05c04af2bf752bb9a6c9dcebb2aff2fab372d3d9d311f2a138540e307756bd3a", size = 380429, upload_time = "2024-06-16T18:38:18.079Z" }, +] + +[[package]] +name = "backrefs" +version = "5.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/46/caba1eb32fa5784428ab401a5487f73db4104590ecd939ed9daaf18b47e0/backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd", size = 6773994, upload_time = "2025-02-25T18:15:32.003Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/cb/d019ab87fe70e0fe3946196d50d6a4428623dc0c38a6669c8cae0320fbf3/backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d", size = 380337, upload_time = "2025-02-25T16:53:14.607Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/abd17f50ee21b2248075cb6924c6e7f9d23b4925ca64ec660e869c2633f1/backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b", size = 392142, upload_time = "2025-02-25T16:53:17.266Z" }, + { url = "https://files.pythonhosted.org/packages/b3/04/7b415bd75c8ab3268cc138c76fa648c19495fcc7d155508a0e62f3f82308/backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486", size = 398021, upload_time = "2025-02-25T16:53:26.378Z" }, + { url = "https://files.pythonhosted.org/packages/04/b8/60dcfb90eb03a06e883a92abbc2ab95c71f0d8c9dd0af76ab1d5ce0b1402/backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585", size = 399915, upload_time = "2025-02-25T16:53:28.167Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336, upload_time = "2025-02-25T16:53:29.858Z" }, ] [[package]] name = "certifi" -version = "2024.8.30" +version = "2025.4.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload_time = "2025-04-26T02:12:29.51Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload_time = "2025-04-26T02:12:27.662Z" }, ] [[package]] name = "charset-normalizer" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/61/095a0aa1a84d1481998b534177c8566fdc50bb1233ea9a0478cd3cc075bd/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", size = 194219 }, - { url = "https://files.pythonhosted.org/packages/cc/94/f7cf5e5134175de79ad2059edf2adce18e0685ebdb9227ff0139975d0e93/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", size = 122521 }, - { url = "https://files.pythonhosted.org/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", size = 120383 }, - { url = "https://files.pythonhosted.org/packages/b8/60/e2f67915a51be59d4539ed189eb0a2b0d292bf79270410746becb32bc2c3/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", size = 138223 }, - { url = "https://files.pythonhosted.org/packages/05/8c/eb854996d5fef5e4f33ad56927ad053d04dc820e4a3d39023f35cad72617/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", size = 148101 }, - { url = "https://files.pythonhosted.org/packages/f6/93/bb6cbeec3bf9da9b2eba458c15966658d1daa8b982c642f81c93ad9b40e1/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", size = 140699 }, - { url = "https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", size = 142065 }, - { url = "https://files.pythonhosted.org/packages/3f/ba/3f5e7be00b215fa10e13d64b1f6237eb6ebea66676a41b2bcdd09fe74323/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", size = 144505 }, - { url = "https://files.pythonhosted.org/packages/33/c3/3b96a435c5109dd5b6adc8a59ba1d678b302a97938f032e3770cc84cd354/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", size = 139425 }, - { url = "https://files.pythonhosted.org/packages/43/05/3bf613e719efe68fb3a77f9c536a389f35b95d75424b96b426a47a45ef1d/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", size = 145287 }, - { url = "https://files.pythonhosted.org/packages/58/78/a0bc646900994df12e07b4ae5c713f2b3e5998f58b9d3720cce2aa45652f/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", size = 149929 }, - { url = "https://files.pythonhosted.org/packages/eb/5c/97d97248af4920bc68687d9c3b3c0f47c910e21a8ff80af4565a576bd2f0/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", size = 141605 }, - { url = "https://files.pythonhosted.org/packages/a8/31/47d018ef89f95b8aded95c589a77c072c55e94b50a41aa99c0a2008a45a4/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", size = 142646 }, - { url = "https://files.pythonhosted.org/packages/ae/d5/4fecf1d58bedb1340a50f165ba1c7ddc0400252d6832ff619c4568b36cc0/charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", size = 92846 }, - { url = "https://files.pythonhosted.org/packages/a2/a0/4af29e22cb5942488cf45630cbdd7cefd908768e69bdd90280842e4e8529/charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", size = 100343 }, - { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647 }, - { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434 }, - { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979 }, - { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582 }, - { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645 }, - { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398 }, - { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273 }, - { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577 }, - { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747 }, - { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375 }, - { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474 }, - { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232 }, - { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859 }, - { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509 }, - { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870 }, - { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892 }, - { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213 }, - { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404 }, - { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275 }, - { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518 }, - { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182 }, - { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869 }, - { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042 }, - { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275 }, - { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819 }, - { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415 }, - { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212 }, - { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167 }, - { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041 }, - { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397 }, - { url = "https://files.pythonhosted.org/packages/ef/d4/a1d72a8f6aa754fdebe91b848912025d30ab7dced61e9ed8aabbf791ed65/charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", size = 191415 }, - { url = "https://files.pythonhosted.org/packages/13/82/83c188028b6f38d39538442dd127dc794c602ae6d45d66c469f4063a4c30/charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", size = 121051 }, - { url = "https://files.pythonhosted.org/packages/16/ea/a9e284aa38cccea06b7056d4cbc7adf37670b1f8a668a312864abf1ff7c6/charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", size = 119143 }, - { url = "https://files.pythonhosted.org/packages/34/2a/f392457d45e24a0c9bfc012887ed4f3c54bf5d4d05a5deb970ffec4b7fc0/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", size = 137506 }, - { url = "https://files.pythonhosted.org/packages/be/4d/9e370f8281cec2fcc9452c4d1ac513324c32957c5f70c73dd2fa8442a21a/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", size = 147272 }, - { url = "https://files.pythonhosted.org/packages/33/95/ef68482e4a6adf781fae8d183fb48d6f2be8facb414f49c90ba6a5149cd1/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", size = 139734 }, - { url = "https://files.pythonhosted.org/packages/3d/09/d82fe4a34c5f0585f9ea1df090e2a71eb9bb1e469723053e1ee9f57c16f3/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", size = 141094 }, - { url = "https://files.pythonhosted.org/packages/81/b2/160893421adfa3c45554fb418e321ed342bb10c0a4549e855b2b2a3699cb/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", size = 144113 }, - { url = "https://files.pythonhosted.org/packages/9e/ef/cd47a63d3200b232792e361cd67530173a09eb011813478b1c0fb8aa7226/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", size = 138555 }, - { url = "https://files.pythonhosted.org/packages/a8/6f/4ff299b97da2ed6358154b6eb3a2db67da2ae204e53d205aacb18a7e4f34/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", size = 144944 }, - { url = "https://files.pythonhosted.org/packages/d1/2f/0d1efd07c74c52b6886c32a3b906fb8afd2fecf448650e73ecb90a5a27f1/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", size = 148925 }, - { url = "https://files.pythonhosted.org/packages/bd/28/7ea29e73eea52c7e15b4b9108d0743fc9e4cc2cdb00d275af1df3d46d360/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", size = 140732 }, - { url = "https://files.pythonhosted.org/packages/b3/c1/ebca8e87c714a6a561cfee063f0655f742e54b8ae6e78151f60ba8708b3a/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", size = 141288 }, - { url = "https://files.pythonhosted.org/packages/74/20/8923a06f15eb3d7f6a306729360bd58f9ead1dc39bc7ea8831f4b407e4ae/charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", size = 92373 }, - { url = "https://files.pythonhosted.org/packages/db/fb/d29e343e7c57bbf1231275939f6e75eb740cd47a9d7cb2c52ffeb62ef869/charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", size = 99577 }, - { url = "https://files.pythonhosted.org/packages/f7/9d/bcf4a449a438ed6f19790eee543a86a740c77508fbc5ddab210ab3ba3a9a/charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", size = 194198 }, - { url = "https://files.pythonhosted.org/packages/66/fe/c7d3da40a66a6bf2920cce0f436fa1f62ee28aaf92f412f0bf3b84c8ad6c/charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", size = 122494 }, - { url = "https://files.pythonhosted.org/packages/2a/9d/a6d15bd1e3e2914af5955c8eb15f4071997e7078419328fee93dfd497eb7/charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", size = 120393 }, - { url = "https://files.pythonhosted.org/packages/3d/85/5b7416b349609d20611a64718bed383b9251b5a601044550f0c8983b8900/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", size = 138331 }, - { url = "https://files.pythonhosted.org/packages/79/66/8946baa705c588521afe10b2d7967300e49380ded089a62d38537264aece/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", size = 148097 }, - { url = "https://files.pythonhosted.org/packages/44/80/b339237b4ce635b4af1c73742459eee5f97201bd92b2371c53e11958392e/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", size = 140711 }, - { url = "https://files.pythonhosted.org/packages/98/69/5d8751b4b670d623aa7a47bef061d69c279e9f922f6705147983aa76c3ce/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", size = 142251 }, - { url = "https://files.pythonhosted.org/packages/1f/8d/33c860a7032da5b93382cbe2873261f81467e7b37f4ed91e25fed62fd49b/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", size = 144636 }, - { url = "https://files.pythonhosted.org/packages/c2/65/52aaf47b3dd616c11a19b1052ce7fa6321250a7a0b975f48d8c366733b9f/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", size = 139514 }, - { url = "https://files.pythonhosted.org/packages/51/fd/0ee5b1c2860bb3c60236d05b6e4ac240cf702b67471138571dad91bcfed8/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", size = 145528 }, - { url = "https://files.pythonhosted.org/packages/e1/9c/60729bf15dc82e3aaf5f71e81686e42e50715a1399770bcde1a9e43d09db/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", size = 149804 }, - { url = "https://files.pythonhosted.org/packages/53/cd/aa4b8a4d82eeceb872f83237b2d27e43e637cac9ffaef19a1321c3bafb67/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", size = 141708 }, - { url = "https://files.pythonhosted.org/packages/54/7f/cad0b328759630814fcf9d804bfabaf47776816ad4ef2e9938b7e1123d04/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561", size = 142708 }, - { url = "https://files.pythonhosted.org/packages/c1/9d/254a2f1bcb0ce9acad838e94ed05ba71a7cb1e27affaa4d9e1ca3958cdb6/charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", size = 92830 }, - { url = "https://files.pythonhosted.org/packages/2f/0e/d7303ccae9735ff8ff01e36705ad6233ad2002962e8668a970fc000c5e1b/charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", size = 100376 }, - { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 }, +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188, upload_time = "2024-12-24T18:12:35.43Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013, upload_time = "2024-12-24T18:09:43.671Z" }, + { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285, upload_time = "2024-12-24T18:09:48.113Z" }, + { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449, upload_time = "2024-12-24T18:09:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892, upload_time = "2024-12-24T18:09:52.078Z" }, + { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123, upload_time = "2024-12-24T18:09:54.575Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943, upload_time = "2024-12-24T18:09:57.324Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063, upload_time = "2024-12-24T18:09:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578, upload_time = "2024-12-24T18:10:02.357Z" }, + { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629, upload_time = "2024-12-24T18:10:03.678Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778, upload_time = "2024-12-24T18:10:06.197Z" }, + { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453, upload_time = "2024-12-24T18:10:08.848Z" }, + { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479, upload_time = "2024-12-24T18:10:10.044Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790, upload_time = "2024-12-24T18:10:11.323Z" }, + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995, upload_time = "2024-12-24T18:10:12.838Z" }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471, upload_time = "2024-12-24T18:10:14.101Z" }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831, upload_time = "2024-12-24T18:10:15.512Z" }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335, upload_time = "2024-12-24T18:10:18.369Z" }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862, upload_time = "2024-12-24T18:10:19.743Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673, upload_time = "2024-12-24T18:10:21.139Z" }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211, upload_time = "2024-12-24T18:10:22.382Z" }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039, upload_time = "2024-12-24T18:10:24.802Z" }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939, upload_time = "2024-12-24T18:10:26.124Z" }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075, upload_time = "2024-12-24T18:10:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340, upload_time = "2024-12-24T18:10:32.679Z" }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205, upload_time = "2024-12-24T18:10:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441, upload_time = "2024-12-24T18:10:37.574Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105, upload_time = "2024-12-24T18:10:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404, upload_time = "2024-12-24T18:10:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423, upload_time = "2024-12-24T18:10:45.492Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184, upload_time = "2024-12-24T18:10:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268, upload_time = "2024-12-24T18:10:50.589Z" }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601, upload_time = "2024-12-24T18:10:52.541Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098, upload_time = "2024-12-24T18:10:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520, upload_time = "2024-12-24T18:10:55.048Z" }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852, upload_time = "2024-12-24T18:10:57.647Z" }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488, upload_time = "2024-12-24T18:10:59.43Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192, upload_time = "2024-12-24T18:11:00.676Z" }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550, upload_time = "2024-12-24T18:11:01.952Z" }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785, upload_time = "2024-12-24T18:11:03.142Z" }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698, upload_time = "2024-12-24T18:11:05.834Z" }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162, upload_time = "2024-12-24T18:11:07.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263, upload_time = "2024-12-24T18:11:08.374Z" }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966, upload_time = "2024-12-24T18:11:09.831Z" }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992, upload_time = "2024-12-24T18:11:12.03Z" }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162, upload_time = "2024-12-24T18:11:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972, upload_time = "2024-12-24T18:11:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095, upload_time = "2024-12-24T18:11:17.672Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668, upload_time = "2024-12-24T18:11:18.989Z" }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073, upload_time = "2024-12-24T18:11:21.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732, upload_time = "2024-12-24T18:11:22.774Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391, upload_time = "2024-12-24T18:11:24.139Z" }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702, upload_time = "2024-12-24T18:11:26.535Z" }, + { url = "https://files.pythonhosted.org/packages/10/bd/6517ea94f2672e801011d50b5d06be2a0deaf566aea27bcdcd47e5195357/charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", size = 195653, upload_time = "2024-12-24T18:11:45.568Z" }, + { url = "https://files.pythonhosted.org/packages/e5/0d/815a2ba3f283b4eeaa5ece57acade365c5b4135f65a807a083c818716582/charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", size = 140701, upload_time = "2024-12-24T18:11:46.968Z" }, + { url = "https://files.pythonhosted.org/packages/aa/17/c94be7ee0d142687e047fe1de72060f6d6837f40eedc26e87e6e124a3fc6/charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", size = 150495, upload_time = "2024-12-24T18:11:48.375Z" }, + { url = "https://files.pythonhosted.org/packages/f7/33/557ac796c47165fc141e4fb71d7b0310f67e05cb420756f3a82e0a0068e0/charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", size = 142946, upload_time = "2024-12-24T18:11:53.619Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/38ef4ae41e9248d63fc4998d933cae22473b1b2ac4122cf908d0f5eb32aa/charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", size = 144737, upload_time = "2024-12-24T18:11:54.993Z" }, + { url = "https://files.pythonhosted.org/packages/43/01/754cdb29dd0560f58290aaaa284d43eea343ad0512e6ad3b8b5c11f08592/charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", size = 147471, upload_time = "2024-12-24T18:11:58.169Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cd/861883ba5160c7a9bd242c30b2c71074cda2aefcc0addc91118e0d4e0765/charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", size = 140801, upload_time = "2024-12-24T18:12:01.02Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7f/0c0dad447819e90b93f8ed238cc8f11b91353c23c19e70fa80483a155bed/charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", size = 149312, upload_time = "2024-12-24T18:12:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/8e/09/9f8abcc6fff60fb727268b63c376c8c79cc37b833c2dfe1f535dfb59523b/charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", size = 152347, upload_time = "2024-12-24T18:12:04.145Z" }, + { url = "https://files.pythonhosted.org/packages/be/e5/3f363dad2e24378f88ccf63ecc39e817c29f32e308ef21a7a6d9c1201165/charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", size = 149888, upload_time = "2024-12-24T18:12:05.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/10/a78c0e91f487b4ad0ef7480ac765e15b774f83de2597f1b6ef0eaf7a2f99/charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", size = 145169, upload_time = "2024-12-24T18:12:06.846Z" }, + { url = "https://files.pythonhosted.org/packages/d3/81/396e7d7f5d7420da8273c91175d2e9a3f569288e3611d521685e4b9ac9cc/charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", size = 95094, upload_time = "2024-12-24T18:12:08.048Z" }, + { url = "https://files.pythonhosted.org/packages/40/bb/20affbbd9ea29c71ea123769dc568a6d42052ff5089c5fe23e21e21084a6/charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", size = 102139, upload_time = "2024-12-24T18:12:09.161Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867, upload_time = "2024-12-24T18:12:10.438Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385, upload_time = "2024-12-24T18:12:11.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367, upload_time = "2024-12-24T18:12:13.177Z" }, + { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928, upload_time = "2024-12-24T18:12:14.497Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203, upload_time = "2024-12-24T18:12:15.731Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082, upload_time = "2024-12-24T18:12:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053, upload_time = "2024-12-24T18:12:20.036Z" }, + { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625, upload_time = "2024-12-24T18:12:22.804Z" }, + { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549, upload_time = "2024-12-24T18:12:24.163Z" }, + { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945, upload_time = "2024-12-24T18:12:25.415Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595, upload_time = "2024-12-24T18:12:28.03Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453, upload_time = "2024-12-24T18:12:29.569Z" }, + { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811, upload_time = "2024-12-24T18:12:30.83Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload_time = "2024-12-24T18:12:32.852Z" }, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload_time = "2024-12-21T18:38:44.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload_time = "2024-12-21T18:38:41.666Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload_time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload_time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "coverage" version = "7.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bf/d5/f809d8b630cf4c11fe490e20037a343d12a74ec2783c6cdb5aee725e7137/coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49", size = 783727 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/45/f4/10bf725621aeec5cc2fa1bc73021f5ba1ac01bcbf2c7278d8d34e1df6457/coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2", size = 206123 }, - { url = "https://files.pythonhosted.org/packages/10/1e/f676e1655d10bf59a6cb8de0601b7ea3c252c764782a3c2263f6d6bbcf28/coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf", size = 206473 }, - { url = "https://files.pythonhosted.org/packages/07/58/0e076ea3a59dbfb3e981577c4e5572b432345cedd921e83006a0215b9afe/coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8", size = 234388 }, - { url = "https://files.pythonhosted.org/packages/d3/6d/72b9f5035c50a14bc5c5fda0c28ac16c426e957a7a3debe02906b614fc4f/coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562", size = 232667 }, - { url = "https://files.pythonhosted.org/packages/93/41/e6e9dbb322f3c93aba7bc519b9c62846d923d7b57398bdd7eda3f0acdd11/coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2", size = 233463 }, - { url = "https://files.pythonhosted.org/packages/91/4e/feff6d115dcc239e5850570ca2ea27a243c8a69596e7f1dabe54a6102d89/coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7", size = 239529 }, - { url = "https://files.pythonhosted.org/packages/50/32/829d0e709fa699dc4e498fa77a561d25fc57954ba32466279952b98f0836/coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87", size = 237720 }, - { url = "https://files.pythonhosted.org/packages/7e/60/62a8c190d20bf605c89a000fd6d41e3563b5792e7275b12eeefe6803b473/coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c", size = 238910 }, - { url = "https://files.pythonhosted.org/packages/5a/52/3641a452e1afa686094910287a8d8a472aafa09f833b2716c2c11c5cabb4/coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d", size = 208336 }, - { url = "https://files.pythonhosted.org/packages/0e/de/7ff914b162e60d66a809632d037f32af74b0df41dc9f0532b1cd7db360bb/coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f", size = 209180 }, - { url = "https://files.pythonhosted.org/packages/c4/26/e9bd37635e0e0343f41394e715725982de8811a1229ace1b3e94c9e47b86/coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf", size = 206305 }, - { url = "https://files.pythonhosted.org/packages/ec/1b/0c493f14813e9518ae71b8bd3061af63a332b41e6fee983996a7b90deb07/coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083", size = 206574 }, - { url = "https://files.pythonhosted.org/packages/64/9b/d0a8c02209f17549ce2283829b7be2b4eaef8bc7c7e0d8016774e73d54c0/coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63", size = 238036 }, - { url = "https://files.pythonhosted.org/packages/0f/86/d5d971283ef625391595d79321d3f9bef09dcaa0537db665fb0d4f445c7d/coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f", size = 235610 }, - { url = "https://files.pythonhosted.org/packages/ab/1c/f8fefae78482f1998f7a9d68419b22089b5ce69a7e0fa0035827d2ce2206/coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227", size = 237314 }, - { url = "https://files.pythonhosted.org/packages/5e/7c/d700521aafd6a23a61b5eb60db2f42a2306e494b3097030fcf400ce768a3/coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd", size = 246411 }, - { url = "https://files.pythonhosted.org/packages/95/44/c3f2e14450239fcdaff38e66a165f4aa8ac3a0753d1db33321c692558a15/coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384", size = 244786 }, - { url = "https://files.pythonhosted.org/packages/f4/ce/98e90709f9879d5834d04b49b86736118a78d848a9162333aa659c6442a7/coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b", size = 245869 }, - { url = "https://files.pythonhosted.org/packages/a8/79/9dceb3847177d3bed1df3dd25a7672cc634369bc3cb6d2eed57ed6366a86/coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286", size = 208337 }, - { url = "https://files.pythonhosted.org/packages/d0/b2/994e08535fcc094df65c00440d71a05133cc8dc0c371eecf84bbb58154f0/coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec", size = 209273 }, - { url = "https://files.pythonhosted.org/packages/a0/de/a54b245e781bfd6f3fd7ce5566a695686b5c25ee7c743f514e7634428972/coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76", size = 206409 }, - { url = "https://files.pythonhosted.org/packages/88/92/07f9c593cd27e3c595b8cb83b95adad8c9ba3d611debceed097a5fd6be4b/coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818", size = 206568 }, - { url = "https://files.pythonhosted.org/packages/41/6d/e142c823e5d4b24481f990da4cf9d2d577a6f4e1fb6faf39d9a4e42b1d43/coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978", size = 238920 }, - { url = "https://files.pythonhosted.org/packages/30/1a/105f0139df6a2adbcaa0c110711a46dbd9f59e93a09ca15a97d59c2564f2/coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70", size = 236288 }, - { url = "https://files.pythonhosted.org/packages/98/79/185cb42910b6a2b2851980407c8445ac0da0750dff65e420e86f973c8396/coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51", size = 238223 }, - { url = "https://files.pythonhosted.org/packages/92/12/2303d1c543a11ea060dbc7144ed3174fc09107b5dd333649415c95ede58b/coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c", size = 245161 }, - { url = "https://files.pythonhosted.org/packages/96/5a/7d0e945c4759fe9d19aad1679dd3096aeb4cb9fcf0062fe24554dc4787b8/coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48", size = 243066 }, - { url = "https://files.pythonhosted.org/packages/f4/1b/79cdb7b11bbbd6540a536ac79412904b5c1f8903d5c1330084212afa8ceb/coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9", size = 244805 }, - { url = "https://files.pythonhosted.org/packages/af/7f/54dc676e7e63549838a3a7b95a8e11df80441bf7d64c6ce8f1cdbc0d1ff0/coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0", size = 208590 }, - { url = "https://files.pythonhosted.org/packages/46/c4/1dfe76d96034a347d717a2392b004d42d45934cb94efa362ad41ca871f6e/coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e", size = 209415 }, - { url = "https://files.pythonhosted.org/packages/6f/ab/95a048c3acda69c9e4a40b3ae57f06c45b30c5d9401e6dc7246e9de83306/coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384", size = 206089 }, - { url = "https://files.pythonhosted.org/packages/23/7c/9863790fb889101c35018ecb9e241cb4f900a77ef100491bb043bfa5976c/coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1", size = 206440 }, - { url = "https://files.pythonhosted.org/packages/32/d4/60b1071c35bd3828590483ae0f8531f07b77d737e2c81dc51887c03bf890/coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a", size = 235515 }, - { url = "https://files.pythonhosted.org/packages/0a/4f/0e04c34df68716b90bedf8b791c684d6a54cab92fbc9ca2c236a8ca268e6/coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409", size = 233354 }, - { url = "https://files.pythonhosted.org/packages/ad/6a/7eebb71ebdf5e56b6da69e5ca8f05b743e054ce9d4dfd440dbcb3f9be0f0/coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e", size = 234600 }, - { url = "https://files.pythonhosted.org/packages/dc/8e/6df9cfab2eb2c5d8e634a18ade3451b587fd75a434366982bdcbefc125e6/coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd", size = 240471 }, - { url = "https://files.pythonhosted.org/packages/af/9c/bd573c65cf554b9979241c575916897e27107a70205b2fbe71218eaa24c4/coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7", size = 238927 }, - { url = "https://files.pythonhosted.org/packages/60/6b/7ac6da198b2c22fc6ba53e479cc800ec230bc7a40c14ed62358d7f1c809f/coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c", size = 239814 }, - { url = "https://files.pythonhosted.org/packages/f9/43/9d3ed7750d2f8a9f7d0d4682fe87ed07080c44f56a8a16a5d4d87c81c278/coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e", size = 208314 }, - { url = "https://files.pythonhosted.org/packages/7d/8f/2665744d223dcea532b1cf3a9edd236632f54fd3925b9b464f1d03b4421e/coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8", size = 209170 }, - { url = "https://files.pythonhosted.org/packages/1a/15/ae47f23bfd557364e731ad2ed182331ba72e8c063b806ba317cd327e73cc/coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d", size = 206118 }, - { url = "https://files.pythonhosted.org/packages/64/09/91be1d04914deea7dd0e2f3e94d925c23e9b81ce23b0da014f1ff07dd772/coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357", size = 206473 }, - { url = "https://files.pythonhosted.org/packages/8b/c7/54cde44ebed02848db20d67388d0f82db1b65eca09d48181df71fbd81cf5/coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e", size = 233988 }, - { url = "https://files.pythonhosted.org/packages/78/ab/39feda43fbd0ca46f695b36bfe1f6836efce9657e81889bb0dcc55fb1745/coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e", size = 232280 }, - { url = "https://files.pythonhosted.org/packages/5b/ec/9bd500128995e9eec2ab50361ce8b853bab2b4839316ddcfd6a34f5bbfed/coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4", size = 233062 }, - { url = "https://files.pythonhosted.org/packages/ad/c6/385cf65448b5739881ba630d144e9c38464737ce68ae4fe4d6a2c7bb3809/coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec", size = 239107 }, - { url = "https://files.pythonhosted.org/packages/7c/a2/9302717d181eeaac738941b2a58e6bd776ef665db24f41f82e32cc8fe814/coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd", size = 237306 }, - { url = "https://files.pythonhosted.org/packages/4d/39/0cfdb5a4bde5843eead02c0f8bc43f8ab3129408cbec53f9ad4f11fc27cf/coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade", size = 238461 }, - { url = "https://files.pythonhosted.org/packages/25/41/5af6b1c2ce964d60d68e80de24c1e5615a4b845958c5f361371b730288f3/coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57", size = 208352 }, - { url = "https://files.pythonhosted.org/packages/29/bc/65b8b11611b1e3cc83fb78c6757a7b2abf638ae449085406017adc4a6c74/coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c", size = 209203 }, - { url = "https://files.pythonhosted.org/packages/99/15/dbcb5d0a22bf5357cf456dfd16f9ceb89c54544d6201d53bc77c75077a8e/coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677", size = 198370 }, +sdist = { url = "https://files.pythonhosted.org/packages/bf/d5/f809d8b630cf4c11fe490e20037a343d12a74ec2783c6cdb5aee725e7137/coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49", size = 783727, upload_time = "2024-03-14T19:11:17.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/f4/10bf725621aeec5cc2fa1bc73021f5ba1ac01bcbf2c7278d8d34e1df6457/coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2", size = 206123, upload_time = "2024-03-14T19:09:10.035Z" }, + { url = "https://files.pythonhosted.org/packages/10/1e/f676e1655d10bf59a6cb8de0601b7ea3c252c764782a3c2263f6d6bbcf28/coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf", size = 206473, upload_time = "2024-03-14T19:09:13.944Z" }, + { url = "https://files.pythonhosted.org/packages/07/58/0e076ea3a59dbfb3e981577c4e5572b432345cedd921e83006a0215b9afe/coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8", size = 234388, upload_time = "2024-03-14T19:09:16.512Z" }, + { url = "https://files.pythonhosted.org/packages/d3/6d/72b9f5035c50a14bc5c5fda0c28ac16c426e957a7a3debe02906b614fc4f/coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562", size = 232667, upload_time = "2024-03-14T19:09:19.124Z" }, + { url = "https://files.pythonhosted.org/packages/93/41/e6e9dbb322f3c93aba7bc519b9c62846d923d7b57398bdd7eda3f0acdd11/coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2", size = 233463, upload_time = "2024-03-14T19:09:21.82Z" }, + { url = "https://files.pythonhosted.org/packages/91/4e/feff6d115dcc239e5850570ca2ea27a243c8a69596e7f1dabe54a6102d89/coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7", size = 239529, upload_time = "2024-03-14T19:09:24.256Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/829d0e709fa699dc4e498fa77a561d25fc57954ba32466279952b98f0836/coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87", size = 237720, upload_time = "2024-03-14T19:09:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/7e/60/62a8c190d20bf605c89a000fd6d41e3563b5792e7275b12eeefe6803b473/coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c", size = 238910, upload_time = "2024-03-14T19:09:29.673Z" }, + { url = "https://files.pythonhosted.org/packages/5a/52/3641a452e1afa686094910287a8d8a472aafa09f833b2716c2c11c5cabb4/coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d", size = 208336, upload_time = "2024-03-14T19:09:31.611Z" }, + { url = "https://files.pythonhosted.org/packages/0e/de/7ff914b162e60d66a809632d037f32af74b0df41dc9f0532b1cd7db360bb/coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f", size = 209180, upload_time = "2024-03-14T19:09:33.814Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/e9bd37635e0e0343f41394e715725982de8811a1229ace1b3e94c9e47b86/coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf", size = 206305, upload_time = "2024-03-14T19:09:35.936Z" }, + { url = "https://files.pythonhosted.org/packages/ec/1b/0c493f14813e9518ae71b8bd3061af63a332b41e6fee983996a7b90deb07/coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083", size = 206574, upload_time = "2024-03-14T19:09:39.003Z" }, + { url = "https://files.pythonhosted.org/packages/64/9b/d0a8c02209f17549ce2283829b7be2b4eaef8bc7c7e0d8016774e73d54c0/coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63", size = 238036, upload_time = "2024-03-14T19:09:41.129Z" }, + { url = "https://files.pythonhosted.org/packages/0f/86/d5d971283ef625391595d79321d3f9bef09dcaa0537db665fb0d4f445c7d/coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f", size = 235610, upload_time = "2024-03-14T19:09:43.097Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1c/f8fefae78482f1998f7a9d68419b22089b5ce69a7e0fa0035827d2ce2206/coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227", size = 237314, upload_time = "2024-03-14T19:09:45.809Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7c/d700521aafd6a23a61b5eb60db2f42a2306e494b3097030fcf400ce768a3/coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd", size = 246411, upload_time = "2024-03-14T19:09:47.992Z" }, + { url = "https://files.pythonhosted.org/packages/95/44/c3f2e14450239fcdaff38e66a165f4aa8ac3a0753d1db33321c692558a15/coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384", size = 244786, upload_time = "2024-03-14T19:09:50.223Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ce/98e90709f9879d5834d04b49b86736118a78d848a9162333aa659c6442a7/coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b", size = 245869, upload_time = "2024-03-14T19:09:52.3Z" }, + { url = "https://files.pythonhosted.org/packages/a8/79/9dceb3847177d3bed1df3dd25a7672cc634369bc3cb6d2eed57ed6366a86/coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286", size = 208337, upload_time = "2024-03-14T19:09:54.419Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b2/994e08535fcc094df65c00440d71a05133cc8dc0c371eecf84bbb58154f0/coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec", size = 209273, upload_time = "2024-03-14T19:09:56.78Z" }, + { url = "https://files.pythonhosted.org/packages/a0/de/a54b245e781bfd6f3fd7ce5566a695686b5c25ee7c743f514e7634428972/coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76", size = 206409, upload_time = "2024-03-14T19:09:59.428Z" }, + { url = "https://files.pythonhosted.org/packages/88/92/07f9c593cd27e3c595b8cb83b95adad8c9ba3d611debceed097a5fd6be4b/coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818", size = 206568, upload_time = "2024-03-14T19:10:01.887Z" }, + { url = "https://files.pythonhosted.org/packages/41/6d/e142c823e5d4b24481f990da4cf9d2d577a6f4e1fb6faf39d9a4e42b1d43/coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978", size = 238920, upload_time = "2024-03-14T19:10:04.56Z" }, + { url = "https://files.pythonhosted.org/packages/30/1a/105f0139df6a2adbcaa0c110711a46dbd9f59e93a09ca15a97d59c2564f2/coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70", size = 236288, upload_time = "2024-03-14T19:10:07.448Z" }, + { url = "https://files.pythonhosted.org/packages/98/79/185cb42910b6a2b2851980407c8445ac0da0750dff65e420e86f973c8396/coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51", size = 238223, upload_time = "2024-03-14T19:10:10.055Z" }, + { url = "https://files.pythonhosted.org/packages/92/12/2303d1c543a11ea060dbc7144ed3174fc09107b5dd333649415c95ede58b/coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c", size = 245161, upload_time = "2024-03-14T19:10:12.27Z" }, + { url = "https://files.pythonhosted.org/packages/96/5a/7d0e945c4759fe9d19aad1679dd3096aeb4cb9fcf0062fe24554dc4787b8/coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48", size = 243066, upload_time = "2024-03-14T19:10:14.212Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1b/79cdb7b11bbbd6540a536ac79412904b5c1f8903d5c1330084212afa8ceb/coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9", size = 244805, upload_time = "2024-03-14T19:10:16.343Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/54dc676e7e63549838a3a7b95a8e11df80441bf7d64c6ce8f1cdbc0d1ff0/coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0", size = 208590, upload_time = "2024-03-14T19:10:18.615Z" }, + { url = "https://files.pythonhosted.org/packages/46/c4/1dfe76d96034a347d717a2392b004d42d45934cb94efa362ad41ca871f6e/coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e", size = 209415, upload_time = "2024-03-14T19:10:20.722Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/95a048c3acda69c9e4a40b3ae57f06c45b30c5d9401e6dc7246e9de83306/coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384", size = 206089, upload_time = "2024-03-14T19:10:22.916Z" }, + { url = "https://files.pythonhosted.org/packages/23/7c/9863790fb889101c35018ecb9e241cb4f900a77ef100491bb043bfa5976c/coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1", size = 206440, upload_time = "2024-03-14T19:10:25.935Z" }, + { url = "https://files.pythonhosted.org/packages/32/d4/60b1071c35bd3828590483ae0f8531f07b77d737e2c81dc51887c03bf890/coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a", size = 235515, upload_time = "2024-03-14T19:10:28.968Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4f/0e04c34df68716b90bedf8b791c684d6a54cab92fbc9ca2c236a8ca268e6/coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409", size = 233354, upload_time = "2024-03-14T19:10:31.704Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6a/7eebb71ebdf5e56b6da69e5ca8f05b743e054ce9d4dfd440dbcb3f9be0f0/coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e", size = 234600, upload_time = "2024-03-14T19:10:33.99Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8e/6df9cfab2eb2c5d8e634a18ade3451b587fd75a434366982bdcbefc125e6/coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd", size = 240471, upload_time = "2024-03-14T19:10:36.038Z" }, + { url = "https://files.pythonhosted.org/packages/af/9c/bd573c65cf554b9979241c575916897e27107a70205b2fbe71218eaa24c4/coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7", size = 238927, upload_time = "2024-03-14T19:10:38.231Z" }, + { url = "https://files.pythonhosted.org/packages/60/6b/7ac6da198b2c22fc6ba53e479cc800ec230bc7a40c14ed62358d7f1c809f/coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c", size = 239814, upload_time = "2024-03-14T19:10:41.129Z" }, + { url = "https://files.pythonhosted.org/packages/f9/43/9d3ed7750d2f8a9f7d0d4682fe87ed07080c44f56a8a16a5d4d87c81c278/coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e", size = 208314, upload_time = "2024-03-14T19:10:44.033Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8f/2665744d223dcea532b1cf3a9edd236632f54fd3925b9b464f1d03b4421e/coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8", size = 209170, upload_time = "2024-03-14T19:10:46.415Z" }, + { url = "https://files.pythonhosted.org/packages/1a/15/ae47f23bfd557364e731ad2ed182331ba72e8c063b806ba317cd327e73cc/coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d", size = 206118, upload_time = "2024-03-14T19:10:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/64/09/91be1d04914deea7dd0e2f3e94d925c23e9b81ce23b0da014f1ff07dd772/coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357", size = 206473, upload_time = "2024-03-14T19:10:50.719Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c7/54cde44ebed02848db20d67388d0f82db1b65eca09d48181df71fbd81cf5/coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e", size = 233988, upload_time = "2024-03-14T19:10:53.024Z" }, + { url = "https://files.pythonhosted.org/packages/78/ab/39feda43fbd0ca46f695b36bfe1f6836efce9657e81889bb0dcc55fb1745/coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e", size = 232280, upload_time = "2024-03-14T19:10:55.369Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ec/9bd500128995e9eec2ab50361ce8b853bab2b4839316ddcfd6a34f5bbfed/coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4", size = 233062, upload_time = "2024-03-14T19:10:58.184Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c6/385cf65448b5739881ba630d144e9c38464737ce68ae4fe4d6a2c7bb3809/coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec", size = 239107, upload_time = "2024-03-14T19:11:00.907Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/9302717d181eeaac738941b2a58e6bd776ef665db24f41f82e32cc8fe814/coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd", size = 237306, upload_time = "2024-03-14T19:11:03.347Z" }, + { url = "https://files.pythonhosted.org/packages/4d/39/0cfdb5a4bde5843eead02c0f8bc43f8ab3129408cbec53f9ad4f11fc27cf/coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade", size = 238461, upload_time = "2024-03-14T19:11:06.063Z" }, + { url = "https://files.pythonhosted.org/packages/25/41/5af6b1c2ce964d60d68e80de24c1e5615a4b845958c5f361371b730288f3/coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57", size = 208352, upload_time = "2024-03-14T19:11:09.395Z" }, + { url = "https://files.pythonhosted.org/packages/29/bc/65b8b11611b1e3cc83fb78c6757a7b2abf638ae449085406017adc4a6c74/coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c", size = 209203, upload_time = "2024-03-14T19:11:11.886Z" }, + { url = "https://files.pythonhosted.org/packages/99/15/dbcb5d0a22bf5357cf456dfd16f9ceb89c54544d6201d53bc77c75077a8e/coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677", size = 198370, upload_time = "2024-03-14T19:11:14.459Z" }, ] [package.optional-dependencies] @@ -235,9 +275,9 @@ toml = [ name = "exceptiongroup" version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload_time = "2024-07-12T22:26:00.161Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload_time = "2024-07-12T22:25:58.476Z" }, ] [[package]] @@ -247,152 +287,281 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload_time = "2022-05-02T15:47:16.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload_time = "2022-05-02T15:47:14.552Z" }, ] [[package]] name = "griffe" -version = "1.3.1" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] dependencies = [ { name = "astunparse", marker = "python_full_version < '3.9'" }, - { name = "colorama" }, + { name = "colorama", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e9/b2c86ad9d69053e497a24ceb25d661094fb321ab4ed39a8b71793dcbae82/griffe-1.4.0.tar.gz", hash = "sha256:8fccc585896d13f1221035d32c50dec65830c87d23f9adb9b1e6f3d63574f7f5", size = 381028, upload_time = "2024-10-11T12:53:54.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/7c/e9e66869c2e4c9b378474e49c993128ec0131ef4721038b6d06e50538caf/griffe-1.4.0-py3-none-any.whl", hash = "sha256:e589de8b8c137e99a46ec45f9598fc0ac5b6868ce824b24db09c02d117b89bc5", size = 127015, upload_time = "2024-10-11T12:53:52.383Z" }, +] + +[[package]] +name = "griffe" +version = "1.7.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.9'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/d1/dcd486d6d577cb12490c78cfa88679fb9b481b227807f14632ba9bd82245/griffe-1.3.1.tar.gz", hash = "sha256:3f86a716b631a4c0f96a43cb75d05d3c85975003c20540426c0eba3b0581c56a", size = 382412 } +sdist = { url = "https://files.pythonhosted.org/packages/a9/3e/5aa9a61f7c3c47b0b52a1d930302992229d191bf4bc76447b324b731510a/griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b", size = 395137, upload_time = "2025-04-23T11:29:09.147Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/19/63971981a20aecfa7cbd07c5cac6914cf1180b3dd8db5fe8ab2ea410315f/griffe-1.3.1-py3-none-any.whl", hash = "sha256:940aeb630bc3054b4369567f150b6365be6f11eef46b0ed8623aea96e6d17b19", size = 126902 }, + { url = "https://files.pythonhosted.org/packages/58/c6/5c20af38c2a57c15d87f7f38bee77d63c1d2a3689f74fefaf35915dd12b2/griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75", size = 129303, upload_time = "2025-04-23T11:29:07.145Z" }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload_time = "2024-09-15T18:07:39.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload_time = "2024-09-15T18:07:37.964Z" }, ] [[package]] name = "importlib-metadata" version = "8.5.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "zipp", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload_time = "2024-09-11T14:56:08.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload_time = "2024-09-11T14:56:07.019Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] dependencies = [ - { name = "zipp" }, + { name = "zipp", version = "3.21.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } +sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767, upload_time = "2025-01-20T22:21:30.429Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, + { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971, upload_time = "2025-01-20T22:21:29.177Z" }, ] [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload_time = "2025-03-19T20:09:59.721Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload_time = "2025-03-19T20:10:01.071Z" }, ] [[package]] name = "invoke" version = "2.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/42/127e6d792884ab860defc3f4d80a8f9812e48ace584ffc5a346de58cdc6c/invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5", size = 299835 } +sdist = { url = "https://files.pythonhosted.org/packages/f9/42/127e6d792884ab860defc3f4d80a8f9812e48ace584ffc5a346de58cdc6c/invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5", size = 299835, upload_time = "2023-07-12T18:05:17.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/66/7f8c48009c72d73bc6bbe6eb87ac838d6a526146f7dab14af671121eb379/invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820", size = 160274 }, + { url = "https://files.pythonhosted.org/packages/0a/66/7f8c48009c72d73bc6bbe6eb87ac838d6a526146f7dab14af671121eb379/invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820", size = 160274, upload_time = "2023-07-12T18:05:16.294Z" }, ] [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markupsafe" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload_time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload_time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "markdown" version = "3.7" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086, upload_time = "2024-08-16T15:55:17.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349, upload_time = "2024-08-16T15:55:16.176Z" }, +] + +[[package]] +name = "markdown" +version = "3.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", version = "8.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/15/222b423b0b88689c266d9eac4e61396fe2cc53464459d6a37618ac863b24/markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f", size = 360906, upload_time = "2025-04-11T14:42:50.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 }, + { url = "https://files.pythonhosted.org/packages/51/3f/afe76f8e2246ffbc867440cbcf90525264df0e658f8a5ca1f872b3f6192a/markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc", size = 106210, upload_time = "2025-04-11T14:42:49.178Z" }, ] [[package]] name = "markupsafe" version = "2.1.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206 }, - { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079 }, - { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620 }, - { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818 }, - { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493 }, - { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630 }, - { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745 }, - { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021 }, - { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659 }, - { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213 }, - { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219 }, - { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098 }, - { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014 }, - { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220 }, - { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756 }, - { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988 }, - { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718 }, - { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317 }, - { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670 }, - { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224 }, - { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, - { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, - { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, - { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, - { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, - { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, - { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, - { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, - { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, - { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, - { url = "https://files.pythonhosted.org/packages/f8/ff/2c942a82c35a49df5de3a630ce0a8456ac2969691b230e530ac12314364c/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", size = 18192 }, - { url = "https://files.pythonhosted.org/packages/4f/14/6f294b9c4f969d0c801a4615e221c1e084722ea6114ab2114189c5b8cbe0/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", size = 14072 }, - { url = "https://files.pythonhosted.org/packages/81/d4/fd74714ed30a1dedd0b82427c02fa4deec64f173831ec716da11c51a50aa/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", size = 26928 }, - { url = "https://files.pythonhosted.org/packages/c7/bd/50319665ce81bb10e90d1cf76f9e1aa269ea6f7fa30ab4521f14d122a3df/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", size = 26106 }, - { url = "https://files.pythonhosted.org/packages/4c/6f/f2b0f675635b05f6afd5ea03c094557bdb8622fa8e673387444fe8d8e787/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68", size = 25781 }, - { url = "https://files.pythonhosted.org/packages/51/e0/393467cf899b34a9d3678e78961c2c8cdf49fb902a959ba54ece01273fb1/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", size = 30518 }, - { url = "https://files.pythonhosted.org/packages/f6/02/5437e2ad33047290dafced9df741d9efc3e716b75583bbd73a9984f1b6f7/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", size = 29669 }, - { url = "https://files.pythonhosted.org/packages/0e/7d/968284145ffd9d726183ed6237c77938c021abacde4e073020f920e060b2/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", size = 29933 }, - { url = "https://files.pythonhosted.org/packages/bf/f3/ecb00fc8ab02b7beae8699f34db9357ae49d9f21d4d3de6f305f34fa949e/MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", size = 16656 }, - { url = "https://files.pythonhosted.org/packages/92/21/357205f03514a49b293e214ac39de01fadd0970a6e05e4bf1ddd0ffd0881/MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", size = 17206 }, - { url = "https://files.pythonhosted.org/packages/0f/31/780bb297db036ba7b7bbede5e1d7f1e14d704ad4beb3ce53fb495d22bc62/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", size = 18193 }, - { url = "https://files.pythonhosted.org/packages/6c/77/d77701bbef72892affe060cdacb7a2ed7fd68dae3b477a8642f15ad3b132/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", size = 14073 }, - { url = "https://files.pythonhosted.org/packages/d9/a7/1e558b4f78454c8a3a0199292d96159eb4d091f983bc35ef258314fe7269/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", size = 26486 }, - { url = "https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", size = 25685 }, - { url = "https://files.pythonhosted.org/packages/6a/18/ae5a258e3401f9b8312f92b028c54d7026a97ec3ab20bfaddbdfa7d8cce8/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", size = 25338 }, - { url = "https://files.pythonhosted.org/packages/0b/cc/48206bd61c5b9d0129f4d75243b156929b04c94c09041321456fd06a876d/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", size = 30439 }, - { url = "https://files.pythonhosted.org/packages/d1/06/a41c112ab9ffdeeb5f77bc3e331fdadf97fa65e52e44ba31880f4e7f983c/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", size = 29531 }, - { url = "https://files.pythonhosted.org/packages/02/8c/ab9a463301a50dab04d5472e998acbd4080597abc048166ded5c7aa768c8/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", size = 29823 }, - { url = "https://files.pythonhosted.org/packages/bc/29/9bc18da763496b055d8e98ce476c8e718dcfd78157e17f555ce6dd7d0895/MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", size = 16658 }, - { url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211 }, +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload_time = "2024-02-02T16:31:22.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206, upload_time = "2024-02-02T16:30:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079, upload_time = "2024-02-02T16:30:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620, upload_time = "2024-02-02T16:30:08.31Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818, upload_time = "2024-02-02T16:30:09.577Z" }, + { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493, upload_time = "2024-02-02T16:30:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630, upload_time = "2024-02-02T16:30:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745, upload_time = "2024-02-02T16:30:14.222Z" }, + { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021, upload_time = "2024-02-02T16:30:16.032Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659, upload_time = "2024-02-02T16:30:17.079Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213, upload_time = "2024-02-02T16:30:18.251Z" }, + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219, upload_time = "2024-02-02T16:30:19.988Z" }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098, upload_time = "2024-02-02T16:30:21.063Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014, upload_time = "2024-02-02T16:30:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220, upload_time = "2024-02-02T16:30:24.76Z" }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756, upload_time = "2024-02-02T16:30:25.877Z" }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988, upload_time = "2024-02-02T16:30:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718, upload_time = "2024-02-02T16:30:28.111Z" }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317, upload_time = "2024-02-02T16:30:29.214Z" }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670, upload_time = "2024-02-02T16:30:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224, upload_time = "2024-02-02T16:30:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215, upload_time = "2024-02-02T16:30:33.081Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069, upload_time = "2024-02-02T16:30:34.148Z" }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452, upload_time = "2024-02-02T16:30:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462, upload_time = "2024-02-02T16:30:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869, upload_time = "2024-02-02T16:30:37.834Z" }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906, upload_time = "2024-02-02T16:30:39.366Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296, upload_time = "2024-02-02T16:30:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038, upload_time = "2024-02-02T16:30:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572, upload_time = "2024-02-02T16:30:43.326Z" }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127, upload_time = "2024-02-02T16:30:44.418Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ff/2c942a82c35a49df5de3a630ce0a8456ac2969691b230e530ac12314364c/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", size = 18192, upload_time = "2024-02-02T16:30:57.715Z" }, + { url = "https://files.pythonhosted.org/packages/4f/14/6f294b9c4f969d0c801a4615e221c1e084722ea6114ab2114189c5b8cbe0/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", size = 14072, upload_time = "2024-02-02T16:30:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/81/d4/fd74714ed30a1dedd0b82427c02fa4deec64f173831ec716da11c51a50aa/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", size = 26928, upload_time = "2024-02-02T16:30:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/50319665ce81bb10e90d1cf76f9e1aa269ea6f7fa30ab4521f14d122a3df/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", size = 26106, upload_time = "2024-02-02T16:31:01.582Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6f/f2b0f675635b05f6afd5ea03c094557bdb8622fa8e673387444fe8d8e787/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68", size = 25781, upload_time = "2024-02-02T16:31:02.71Z" }, + { url = "https://files.pythonhosted.org/packages/51/e0/393467cf899b34a9d3678e78961c2c8cdf49fb902a959ba54ece01273fb1/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", size = 30518, upload_time = "2024-02-02T16:31:04.392Z" }, + { url = "https://files.pythonhosted.org/packages/f6/02/5437e2ad33047290dafced9df741d9efc3e716b75583bbd73a9984f1b6f7/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", size = 29669, upload_time = "2024-02-02T16:31:05.53Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7d/968284145ffd9d726183ed6237c77938c021abacde4e073020f920e060b2/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", size = 29933, upload_time = "2024-02-02T16:31:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f3/ecb00fc8ab02b7beae8699f34db9357ae49d9f21d4d3de6f305f34fa949e/MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", size = 16656, upload_time = "2024-02-02T16:31:07.767Z" }, + { url = "https://files.pythonhosted.org/packages/92/21/357205f03514a49b293e214ac39de01fadd0970a6e05e4bf1ddd0ffd0881/MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", size = 17206, upload_time = "2024-02-02T16:31:08.843Z" }, + { url = "https://files.pythonhosted.org/packages/0f/31/780bb297db036ba7b7bbede5e1d7f1e14d704ad4beb3ce53fb495d22bc62/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", size = 18193, upload_time = "2024-02-02T16:31:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d77701bbef72892affe060cdacb7a2ed7fd68dae3b477a8642f15ad3b132/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", size = 14073, upload_time = "2024-02-02T16:31:11.442Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a7/1e558b4f78454c8a3a0199292d96159eb4d091f983bc35ef258314fe7269/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", size = 26486, upload_time = "2024-02-02T16:31:12.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", size = 25685, upload_time = "2024-02-02T16:31:13.726Z" }, + { url = "https://files.pythonhosted.org/packages/6a/18/ae5a258e3401f9b8312f92b028c54d7026a97ec3ab20bfaddbdfa7d8cce8/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", size = 25338, upload_time = "2024-02-02T16:31:14.812Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cc/48206bd61c5b9d0129f4d75243b156929b04c94c09041321456fd06a876d/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", size = 30439, upload_time = "2024-02-02T16:31:15.946Z" }, + { url = "https://files.pythonhosted.org/packages/d1/06/a41c112ab9ffdeeb5f77bc3e331fdadf97fa65e52e44ba31880f4e7f983c/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", size = 29531, upload_time = "2024-02-02T16:31:17.13Z" }, + { url = "https://files.pythonhosted.org/packages/02/8c/ab9a463301a50dab04d5472e998acbd4080597abc048166ded5c7aa768c8/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", size = 29823, upload_time = "2024-02-02T16:31:18.247Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/9bc18da763496b055d8e98ce476c8e718dcfd78157e17f555ce6dd7d0895/MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", size = 16658, upload_time = "2024-02-02T16:31:19.583Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211, upload_time = "2024-02-02T16:31:20.96Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload_time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload_time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload_time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload_time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload_time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload_time = "2024-10-18T15:20:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload_time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload_time = "2024-10-18T15:20:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload_time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload_time = "2024-10-18T15:21:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload_time = "2024-10-18T15:21:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload_time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload_time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload_time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload_time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload_time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload_time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload_time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload_time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload_time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload_time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload_time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload_time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload_time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload_time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload_time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload_time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload_time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload_time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload_time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload_time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload_time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload_time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload_time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload_time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload_time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload_time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload_time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload_time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload_time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload_time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload_time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload_time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload_time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload_time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload_time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload_time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload_time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload_time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload_time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload_time = "2024-10-18T15:21:42.784Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload_time = "2024-10-18T15:21:43.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload_time = "2024-10-18T15:21:44.666Z" }, + { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload_time = "2024-10-18T15:21:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload_time = "2024-10-18T15:21:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload_time = "2024-10-18T15:21:47.134Z" }, + { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload_time = "2024-10-18T15:21:48.334Z" }, + { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload_time = "2024-10-18T15:21:49.587Z" }, + { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload_time = "2024-10-18T15:21:50.441Z" }, + { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload_time = "2024-10-18T15:21:51.385Z" }, + { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload_time = "2024-10-18T15:21:52.974Z" }, ] [[package]] name = "mergedeep" version = "1.3.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload_time = "2021-02-05T18:55:30.623Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload_time = "2021-02-05T18:55:29.583Z" }, ] [[package]] @@ -401,37 +570,61 @@ version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markdown", version = "3.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "mergedeep" }, { name = "mkdocs-get-deps" }, { name = "packaging" }, { name = "pathspec" }, { name = "pyyaml" }, { name = "pyyaml-env-tag" }, - { name = "watchdog" }, + { name = "watchdog", version = "4.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "watchdog", version = "6.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 } +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload_time = "2024-08-30T12:24:06.899Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 }, + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload_time = "2024-08-30T12:24:05.054Z" }, ] [[package]] name = "mkdocs-autorefs" version = "1.2.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] dependencies = [ - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mkdocs" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocs", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/ae/0f1154c614d6a8b8a36fff084e5b82af3a15f7d2060cf0dcdb1c53297a71/mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f", size = 40262, upload_time = "2024-09-01T18:29:18.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f", size = 16522, upload_time = "2024-09-01T18:29:16.605Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +dependencies = [ + { name = "markdown", version = "3.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "markupsafe", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocs", marker = "python_full_version >= '3.9'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/ae/0f1154c614d6a8b8a36fff084e5b82af3a15f7d2060cf0dcdb1c53297a71/mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f", size = 40262 } +sdist = { url = "https://files.pythonhosted.org/packages/c2/44/140469d87379c02f1e1870315f3143718036a983dd0416650827b8883192/mkdocs_autorefs-1.4.1.tar.gz", hash = "sha256:4b5b6235a4becb2b10425c2fa191737e415b37aa3418919db33e5d774c9db079", size = 4131355, upload_time = "2025-03-08T13:35:21.232Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f", size = 16522 }, + { url = "https://files.pythonhosted.org/packages/f8/29/1125f7b11db63e8e32bcfa0752a4eea30abff3ebd0796f808e14571ddaa2/mkdocs_autorefs-1.4.1-py3-none-any.whl", hash = "sha256:9793c5ac06a6ebbe52ec0f8439256e66187badf4b5334b5fde0b128ec134df4f", size = 5782047, upload_time = "2025-03-08T13:35:18.889Z" }, ] [[package]] @@ -439,227 +632,348 @@ name = "mkdocs-get-deps" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "mergedeep" }, - { name = "platformdirs" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "platformdirs", version = "4.3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload_time = "2023-11-20T17:51:09.981Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload_time = "2023-11-20T17:51:08.587Z" }, ] [[package]] name = "mkdocs-material" -version = "9.5.35" +version = "9.6.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, + { name = "backrefs", version = "5.7.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "backrefs", version = "5.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "colorama" }, { name = "jinja2" }, - { name = "markdown" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markdown", version = "3.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "mkdocs" }, { name = "mkdocs-material-extensions" }, { name = "paginate" }, { name = "pygments" }, { name = "pymdown-extensions" }, - { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/69/e19bc4de98bce00345ecf9d5d6a19178dd121c0d06a121b374ffd27fcac7/mkdocs_material-9.5.35.tar.gz", hash = "sha256:0d233d7db067ac896bf22ee7950eebf2b1eaf26c155bb27382bf4174021cc117", size = 3994310 } +sdist = { url = "https://files.pythonhosted.org/packages/2d/ef/25fc10dbbb8faeeeb10ed7734d84a347cd2ec5d7200733f11c5553c02608/mkdocs_material-9.6.12.tar.gz", hash = "sha256:add6a6337b29f9ea7912cb1efc661de2c369060b040eb5119855d794ea85b473", size = 3951532, upload_time = "2025-04-17T10:40:41.48Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/6b/84c7cde89fd957461fdbbf6ca02611a4a043d8afbd0bf990aa6a2d0b52c6/mkdocs_material-9.5.35-py3-none-any.whl", hash = "sha256:44e069d87732d29f4a2533ae0748fa0e67e270043270c71f04d0fba11a357b24", size = 8698196 }, + { url = "https://files.pythonhosted.org/packages/09/00/592940f4d150327a4f455171b2c9d4c3be7779a88e18b0a086183fcd8f06/mkdocs_material-9.6.12-py3-none-any.whl", hash = "sha256:92b4fbdc329e4febc267ca6e2c51e8501fa97b2225c5f4deb4d4e43550f8e61e", size = 8703654, upload_time = "2025-04-17T10:40:38.304Z" }, ] [[package]] name = "mkdocs-material-extensions" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload_time = "2023-11-22T19:09:45.208Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload_time = "2023-11-22T19:09:43.465Z" }, ] [[package]] name = "mkdocstrings" version = "0.26.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] dependencies = [ - { name = "click" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mkdocs" }, - { name = "mkdocs-autorefs" }, - { name = "platformdirs" }, - { name = "pymdown-extensions" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "click", marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "jinja2", marker = "python_full_version < '3.9'" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocs", marker = "python_full_version < '3.9'" }, + { name = "mkdocs-autorefs", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pymdown-extensions", marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/170ff04de72227f715d67da32950c7b8434449f3805b2ec3dd1085db4d7c/mkdocstrings-0.26.1.tar.gz", hash = "sha256:bb8b8854d6713d5348ad05b069a09f3b79edbc6a0f33a34c6821141adb03fe33", size = 92677, upload_time = "2024-09-06T10:26:06.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/cc/8ba127aaee5d1e9046b0d33fa5b3d17da95a9d705d44902792e0569257fd/mkdocstrings-0.26.1-py3-none-any.whl", hash = "sha256:29738bfb72b4608e8e55cc50fb8a54f325dc7ebd2014e4e3881a49892d5983cf", size = 29643, upload_time = "2024-09-06T10:26:04.498Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/170ff04de72227f715d67da32950c7b8434449f3805b2ec3dd1085db4d7c/mkdocstrings-0.26.1.tar.gz", hash = "sha256:bb8b8854d6713d5348ad05b069a09f3b79edbc6a0f33a34c6821141adb03fe33", size = 92677 } + +[[package]] +name = "mkdocstrings" +version = "0.29.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +dependencies = [ + { name = "importlib-metadata", version = "8.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "jinja2", marker = "python_full_version >= '3.9'" }, + { name = "markdown", version = "3.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "markupsafe", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocs", marker = "python_full_version >= '3.9'" }, + { name = "mkdocs-autorefs", version = "1.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pymdown-extensions", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/e8/d22922664a627a0d3d7ff4a6ca95800f5dde54f411982591b4621a76225d/mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42", size = 1212686, upload_time = "2025-03-31T08:33:11.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/cc/8ba127aaee5d1e9046b0d33fa5b3d17da95a9d705d44902792e0569257fd/mkdocstrings-0.26.1-py3-none-any.whl", hash = "sha256:29738bfb72b4608e8e55cc50fb8a54f325dc7ebd2014e4e3881a49892d5983cf", size = 29643 }, + { url = "https://files.pythonhosted.org/packages/98/14/22533a578bf8b187e05d67e2c1721ce10e3f526610eebaf7a149d557ea7a/mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6", size = 1631075, upload_time = "2025-03-31T08:33:09.661Z" }, ] [[package]] name = "mkdocstrings-python" version = "1.11.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] dependencies = [ - { name = "griffe" }, - { name = "mkdocs-autorefs" }, - { name = "mkdocstrings" }, + { name = "griffe", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocs-autorefs", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocstrings", version = "0.26.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/ba/534c934cd0a809f51c91332d6ed278782ee4126b8ba8db02c2003f162b47/mkdocstrings_python-1.11.1.tar.gz", hash = "sha256:8824b115c5359304ab0b5378a91f6202324a849e1da907a3485b59208b797322", size = 166890, upload_time = "2024-09-03T17:20:54.904Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/f2/2a2c48fda645ac6bbe73bcc974587a579092b6868e6ff8bc6d177f4db38a/mkdocstrings_python-1.11.1-py3-none-any.whl", hash = "sha256:a21a1c05acef129a618517bb5aae3e33114f569b11588b1e7af3e9d4061a71af", size = 109297, upload_time = "2024-09-03T17:20:52.621Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/ba/534c934cd0a809f51c91332d6ed278782ee4126b8ba8db02c2003f162b47/mkdocstrings_python-1.11.1.tar.gz", hash = "sha256:8824b115c5359304ab0b5378a91f6202324a849e1da907a3485b59208b797322", size = 166890 } + +[[package]] +name = "mkdocstrings-python" +version = "1.16.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +dependencies = [ + { name = "griffe", version = "1.7.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocs-autorefs", version = "1.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocstrings", version = "0.29.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/c8/600c4201b6b9e72bab16802316d0c90ce04089f8e6bb5e064cd2a5abba7e/mkdocstrings_python-1.16.10.tar.gz", hash = "sha256:f9eedfd98effb612ab4d0ed6dd2b73aff6eba5215e0a65cea6d877717f75502e", size = 205771, upload_time = "2025-04-03T14:24:48.12Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/f2/2a2c48fda645ac6bbe73bcc974587a579092b6868e6ff8bc6d177f4db38a/mkdocstrings_python-1.11.1-py3-none-any.whl", hash = "sha256:a21a1c05acef129a618517bb5aae3e33114f569b11588b1e7af3e9d4061a71af", size = 109297 }, + { url = "https://files.pythonhosted.org/packages/53/37/19549c5e0179785308cc988a68e16aa7550e4e270ec8a9878334e86070c6/mkdocstrings_python-1.16.10-py3-none-any.whl", hash = "sha256:63bb9f01f8848a644bdb6289e86dc38ceddeaa63ecc2e291e3b2ca52702a6643", size = 124112, upload_time = "2025-04-03T14:24:46.561Z" }, ] [[package]] name = "more-itertools" version = "10.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/ad/7905a7fd46ffb61d976133a4f47799388209e73cbc8c1253593335da88b4/more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1", size = 114449 } +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/7905a7fd46ffb61d976133a4f47799388209e73cbc8c1253593335da88b4/more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1", size = 114449, upload_time = "2024-01-08T15:47:22.007Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/e2/8e10e465ee3987bb7c9ab69efb91d867d93959095f4807db102d07995d94/more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684", size = 57015 }, + { url = "https://files.pythonhosted.org/packages/50/e2/8e10e465ee3987bb7c9ab69efb91d867d93959095f4807db102d07995d94/more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684", size = 57015, upload_time = "2024-01-08T15:47:19.877Z" }, ] [[package]] name = "mypy" -version = "1.13.0" +version = "1.14.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] dependencies = [ - { name = "mypy-extensions" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/8c/206de95a27722b5b5a8c85ba3100467bd86299d92a4f71c6b9aa448bfa2f/mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a", size = 11020731 }, - { url = "https://files.pythonhosted.org/packages/ab/bb/b31695a29eea76b1569fd28b4ab141a1adc9842edde080d1e8e1776862c7/mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80", size = 10184276 }, - { url = "https://files.pythonhosted.org/packages/a5/2d/4a23849729bb27934a0e079c9c1aad912167d875c7b070382a408d459651/mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7", size = 12587706 }, - { url = "https://files.pythonhosted.org/packages/5c/c3/d318e38ada50255e22e23353a469c791379825240e71b0ad03e76ca07ae6/mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f", size = 13105586 }, - { url = "https://files.pythonhosted.org/packages/4a/25/3918bc64952370c3dbdbd8c82c363804678127815febd2925b7273d9482c/mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372", size = 9632318 }, - { url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027 }, - { url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699 }, - { url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263 }, - { url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688 }, - { url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 }, - { url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900 }, - { url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818 }, - { url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 }, - { url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 }, - { url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 }, - { url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 }, - { url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 }, - { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 }, - { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 }, - { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 }, - { url = "https://files.pythonhosted.org/packages/5e/2a/13e9ad339131c0fba5c70584f639005a47088f5eed77081a3d00479df0ca/mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a", size = 10955147 }, - { url = "https://files.pythonhosted.org/packages/94/39/02929067dc16b72d78109195cfed349ac4ec85f3d52517ac62b9a5263685/mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb", size = 10138373 }, - { url = "https://files.pythonhosted.org/packages/4a/cc/066709bb01734e3dbbd1375749f8789bf9693f8b842344fc0cf52109694f/mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b", size = 12543621 }, - { url = "https://files.pythonhosted.org/packages/f5/a2/124df839025348c7b9877d0ce134832a9249968e3ab36bb826bab0e9a1cf/mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74", size = 13050348 }, - { url = "https://files.pythonhosted.org/packages/45/86/cc94b1e7f7e756a63043cf425c24fb7470013ee1c032180282db75b1b335/mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6", size = 9615311 }, - { url = "https://files.pythonhosted.org/packages/5f/d4/b33ddd40dad230efb317898a2d1c267c04edba73bc5086bf77edeb410fb2/mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", size = 11013906 }, - { url = "https://files.pythonhosted.org/packages/f4/e6/f414bca465b44d01cd5f4a82761e15044bedd1bf8025c5af3cc64518fac5/mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732", size = 10180657 }, - { url = "https://files.pythonhosted.org/packages/38/e9/fc3865e417722f98d58409770be01afb961e2c1f99930659ff4ae7ca8b7e/mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc", size = 12586394 }, - { url = "https://files.pythonhosted.org/packages/2e/35/f4d8b6d2cb0b3dad63e96caf159419dda023f45a358c6c9ac582ccaee354/mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d", size = 13103591 }, - { url = "https://files.pythonhosted.org/packages/22/1d/80594aef135f921dd52e142fa0acd19df197690bd0cde42cea7b88cf5aa2/mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24", size = 9634690 }, - { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 }, + { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload_time = "2024-12-30T16:39:07.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload_time = "2024-12-30T16:37:22.435Z" }, + { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload_time = "2024-12-30T16:37:53.526Z" }, + { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload_time = "2024-12-30T16:37:50.332Z" }, + { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload_time = "2024-12-30T16:37:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload_time = "2024-12-30T16:38:05.037Z" }, + { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload_time = "2024-12-30T16:37:19.726Z" }, + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload_time = "2024-12-30T16:37:11.533Z" }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload_time = "2024-12-30T16:37:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload_time = "2024-12-30T16:36:58.73Z" }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload_time = "2024-12-30T16:37:03.741Z" }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload_time = "2024-12-30T16:37:57.948Z" }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload_time = "2024-12-30T16:37:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload_time = "2024-12-30T16:38:02.211Z" }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload_time = "2024-12-30T16:37:46.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload_time = "2024-12-30T16:37:43.534Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload_time = "2024-12-30T16:37:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload_time = "2024-12-30T16:37:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload_time = "2024-12-30T16:37:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload_time = "2024-12-30T16:37:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload_time = "2024-12-30T16:38:08.634Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload_time = "2024-12-30T16:38:12.132Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload_time = "2024-12-30T16:38:17.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload_time = "2024-12-30T16:38:32.839Z" }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload_time = "2024-12-30T16:38:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/39/02/1817328c1372be57c16148ce7d2bfcfa4a796bedaed897381b1aad9b267c/mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31", size = 11143050, upload_time = "2024-12-30T16:38:29.743Z" }, + { url = "https://files.pythonhosted.org/packages/b9/07/99db9a95ece5e58eee1dd87ca456a7e7b5ced6798fd78182c59c35a7587b/mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6", size = 10321087, upload_time = "2024-12-30T16:38:14.739Z" }, + { url = "https://files.pythonhosted.org/packages/9a/eb/85ea6086227b84bce79b3baf7f465b4732e0785830726ce4a51528173b71/mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319", size = 12066766, upload_time = "2024-12-30T16:38:47.038Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bb/f01bebf76811475d66359c259eabe40766d2f8ac8b8250d4e224bb6df379/mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac", size = 12787111, upload_time = "2024-12-30T16:39:02.444Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c9/84837ff891edcb6dcc3c27d85ea52aab0c4a34740ff5f0ccc0eb87c56139/mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b", size = 12974331, upload_time = "2024-12-30T16:38:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/84/5f/901e18464e6a13f8949b4909535be3fa7f823291b8ab4e4b36cfe57d6769/mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837", size = 9763210, upload_time = "2024-12-30T16:38:36.299Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload_time = "2024-12-30T16:38:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload_time = "2024-12-30T16:38:50.623Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload_time = "2024-12-30T16:38:53.735Z" }, + { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload_time = "2024-12-30T16:38:56.437Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload_time = "2024-12-30T16:38:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload_time = "2024-12-30T16:39:05.124Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload_time = "2024-12-30T16:38:42.021Z" }, +] + +[[package]] +name = "mypy" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +dependencies = [ + { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717, upload_time = "2025-02-05T03:50:34.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433, upload_time = "2025-02-05T03:49:29.145Z" }, + { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472, upload_time = "2025-02-05T03:49:16.986Z" }, + { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424, upload_time = "2025-02-05T03:49:46.908Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450, upload_time = "2025-02-05T03:50:05.89Z" }, + { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765, upload_time = "2025-02-05T03:49:33.56Z" }, + { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701, upload_time = "2025-02-05T03:49:38.981Z" }, + { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338, upload_time = "2025-02-05T03:50:17.287Z" }, + { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540, upload_time = "2025-02-05T03:49:51.21Z" }, + { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051, upload_time = "2025-02-05T03:50:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751, upload_time = "2025-02-05T03:49:42.408Z" }, + { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783, upload_time = "2025-02-05T03:49:07.707Z" }, + { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618, upload_time = "2025-02-05T03:49:54.581Z" }, + { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981, upload_time = "2025-02-05T03:50:28.25Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175, upload_time = "2025-02-05T03:50:13.411Z" }, + { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675, upload_time = "2025-02-05T03:50:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020, upload_time = "2025-02-05T03:48:48.705Z" }, + { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582, upload_time = "2025-02-05T03:49:03.628Z" }, + { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614, upload_time = "2025-02-05T03:50:00.313Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592, upload_time = "2025-02-05T03:48:55.789Z" }, + { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611, upload_time = "2025-02-05T03:48:44.581Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443, upload_time = "2025-02-05T03:49:25.514Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541, upload_time = "2025-02-05T03:49:57.623Z" }, + { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348, upload_time = "2025-02-05T03:48:52.361Z" }, + { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648, upload_time = "2025-02-05T03:49:11.395Z" }, + { url = "https://files.pythonhosted.org/packages/5a/fa/79cf41a55b682794abe71372151dbbf856e3008f6767057229e6649d294a/mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078", size = 10737129, upload_time = "2025-02-05T03:50:24.509Z" }, + { url = "https://files.pythonhosted.org/packages/d3/33/dd8feb2597d648de29e3da0a8bf4e1afbda472964d2a4a0052203a6f3594/mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba", size = 9856335, upload_time = "2025-02-05T03:49:36.398Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b5/74508959c1b06b96674b364ffeb7ae5802646b32929b7701fc6b18447592/mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5", size = 11611935, upload_time = "2025-02-05T03:49:14.154Z" }, + { url = "https://files.pythonhosted.org/packages/6c/53/da61b9d9973efcd6507183fdad96606996191657fe79701b2c818714d573/mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b", size = 12365827, upload_time = "2025-02-05T03:48:59.458Z" }, + { url = "https://files.pythonhosted.org/packages/c1/72/965bd9ee89540c79a25778cc080c7e6ef40aa1eeac4d52cec7eae6eb5228/mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2", size = 12541924, upload_time = "2025-02-05T03:50:03.12Z" }, + { url = "https://files.pythonhosted.org/packages/46/d0/f41645c2eb263e6c77ada7d76f894c580c9ddb20d77f0c24d34273a4dab2/mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980", size = 9271176, upload_time = "2025-02-05T03:50:10.86Z" }, + { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777, upload_time = "2025-02-05T03:50:08.348Z" }, ] [[package]] name = "mypy-extensions" -version = "1.0.0" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +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/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, + { 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 = "packaging" -version = "24.1" +version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload_time = "2025-04-19T11:48:59.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload_time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "paginate" version = "0.5.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload_time = "2024-08-25T14:17:24.139Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload_time = "2024-08-25T14:17:22.55Z" }, ] [[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 } +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 }, + { 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 = "pbr" version = "6.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8d/c2/ee43b3b11bf2b40e56536183fc9f22afbb04e882720332b6276ee2454c24/pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9", size = 123150 } +sdist = { url = "https://files.pythonhosted.org/packages/8d/c2/ee43b3b11bf2b40e56536183fc9f22afbb04e882720332b6276ee2454c24/pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9", size = 123150, upload_time = "2023-11-07T18:32:23.815Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/dd/171c9fb653591cf265bcc89c436eec75c9bde3dec921cc236fa71e5698df/pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda", size = 107506 }, + { url = "https://files.pythonhosted.org/packages/64/dd/171c9fb653591cf265bcc89c436eec75c9bde3dec921cc236fa71e5698df/pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda", size = 107506, upload_time = "2023-11-07T18:32:21.527Z" }, ] [[package]] name = "platformdirs" version = "4.3.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload_time = "2024-09-17T19:06:50.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload_time = "2024-09-17T19:06:49.212Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload_time = "2025-03-19T20:36:10.989Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, + { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload_time = "2025-03-19T20:36:09.038Z" }, ] [[package]] name = "pluggy" version = "1.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/c6/43f9d44d92aed815e781ca25ba8c174257e27253a94630d21be8725a2b59/pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be", size = 65812 } +sdist = { url = "https://files.pythonhosted.org/packages/54/c6/43f9d44d92aed815e781ca25ba8c174257e27253a94630d21be8725a2b59/pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be", size = 65812, upload_time = "2024-01-24T13:45:15.875Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/5b/0cc789b59e8cc1bf288b38111d002d8c5917123194d45b29dcdac64723cc/pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", size = 20120 }, + { url = "https://files.pythonhosted.org/packages/a5/5b/0cc789b59e8cc1bf288b38111d002d8c5917123194d45b29dcdac64723cc/pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", size = 20120, upload_time = "2024-01-24T13:45:14.227Z" }, ] [[package]] name = "py" version = "1.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796 } +sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796, upload_time = "2021-11-04T17:17:01.377Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708 }, + { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708, upload_time = "2021-11-04T17:17:00.152Z" }, ] [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload_time = "2025-01-06T17:26:30.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload_time = "2025-01-06T17:26:25.553Z" }, ] [[package]] name = "pymdown-extensions" -version = "10.9" +version = "10.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markdown", version = "3.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/d3/fb86beeaa4416f73a28a5e8d440976b7cada2b2d7b5e715b2bd849d4de32/pymdown_extensions-10.9.tar.gz", hash = "sha256:6ff740bcd99ec4172a938970d42b96128bdc9d4b9bcad72494f29921dc69b753", size = 812128 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/44/e6de2fdc880ad0ec7547ca2e087212be815efbc9a425a8d5ba9ede602cbb/pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b", size = 846846, upload_time = "2025-02-01T15:43:15.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/41/18b5dc5e97ec3ff1c2f51d372e570a9fbe231f1124dcc36dbc6b47f93058/pymdown_extensions-10.9-py3-none-any.whl", hash = "sha256:d323f7e90d83c86113ee78f3fe62fc9dee5f56b54d912660703ea1816fed5626", size = 250954 }, + { url = "https://files.pythonhosted.org/packages/eb/f5/b9e2a42aa8f9e34d52d66de87941ecd236570c7ed2e87775ed23bbe4e224/pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9", size = 264467, upload_time = "2025-02-01T15:43:13.995Z" }, ] [[package]] @@ -674,9 +988,9 @@ dependencies = [ { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/b7/7d44bbc04c531dcc753056920e0988032e5871ac674b5a84cb979de6e7af/pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044", size = 1409703 } +sdist = { url = "https://files.pythonhosted.org/packages/30/b7/7d44bbc04c531dcc753056920e0988032e5871ac674b5a84cb979de6e7af/pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044", size = 1409703, upload_time = "2024-03-09T11:51:08.012Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/7e/c79cecfdb6aa85c6c2e3cf63afc56d0f165f24f5c66c03c695c4d9b84756/pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7", size = 337359 }, + { url = "https://files.pythonhosted.org/packages/4d/7e/c79cecfdb6aa85c6c2e3cf63afc56d0f165f24f5c66c03c695c4d9b84756/pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7", size = 337359, upload_time = "2024-03-09T11:51:04.858Z" }, ] [[package]] @@ -687,9 +1001,9 @@ dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 } +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042, upload_time = "2024-03-24T20:16:34.856Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990, upload_time = "2024-03-24T20:16:32.444Z" }, ] [[package]] @@ -699,9 +1013,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/0d/04719abc7a4bdb3a7a1f968f24b0f5253d698c9cc94975330e9d3145befb/pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9", size = 17697 } +sdist = { url = "https://files.pythonhosted.org/packages/93/0d/04719abc7a4bdb3a7a1f968f24b0f5253d698c9cc94975330e9d3145befb/pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9", size = 17697, upload_time = "2024-03-07T21:04:01.069Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, + { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148, upload_time = "2024-03-07T21:03:58.764Z" }, ] [[package]] @@ -711,14 +1025,13 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload_time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload_time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-multipart" -version = "0.0.19" source = { editable = "." } [package.dev-dependencies] @@ -729,11 +1042,14 @@ dev = [ { name = "coverage" }, { name = "invoke" }, { name = "mkdocs" }, - { name = "mkdocs-autorefs" }, + { name = "mkdocs-autorefs", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocs-autorefs", version = "1.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "mkdocs-material" }, - { name = "mkdocstrings-python" }, + { name = "mkdocstrings-python", version = "1.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocstrings-python", version = "1.16.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "more-itertools" }, - { name = "mypy" }, + { name = "mypy", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mypy", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pbr" }, { name = "pluggy" }, { name = "py" }, @@ -742,7 +1058,8 @@ dev = [ { name = "pytest-timeout" }, { name = "pyyaml" }, { name = "ruff" }, - { name = "types-pyyaml" }, + { name = "types-pyyaml", version = "6.0.12.20241230", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "types-pyyaml", version = "6.0.12.20250402", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] [package.metadata] @@ -767,63 +1084,63 @@ dev = [ { name = "pytest-cov", specifier = "==5.0.0" }, { name = "pytest-timeout", specifier = "==2.3.1" }, { name = "pyyaml", specifier = "==6.0.1" }, - { name = "ruff", specifier = "==0.8.0" }, + { name = "ruff", specifier = "==0.11.7" }, { name = "types-pyyaml" }, ] [[package]] name = "pytz" -version = "2024.2" +version = "2025.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload_time = "2025-03-25T02:25:00.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload_time = "2025-03-25T02:24:58.468Z" }, ] [[package]] name = "pyyaml" version = "6.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/af35f7ea75cf72f2cd079c95ee16797de7cd71f29ea7c68ae5ce7be1eda0/PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", size = 125201 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/06/4beb652c0fe16834032e54f0956443d4cc797fe645527acee59e7deaa0a2/PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", size = 189447 }, - { url = "https://files.pythonhosted.org/packages/5b/07/10033a403b23405a8fc48975444463d3d10a5c2736b7eb2550b07b367429/PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f", size = 169264 }, - { url = "https://files.pythonhosted.org/packages/f1/26/55e4f21db1f72eaef092015d9017c11510e7e6301c62a6cfee91295d13c6/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", size = 677003 }, - { url = "https://files.pythonhosted.org/packages/ba/91/090818dfa62e85181f3ae23dd1e8b7ea7f09684864a900cab72d29c57346/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", size = 699070 }, - { url = "https://files.pythonhosted.org/packages/29/61/bf33c6c85c55bc45a29eee3195848ff2d518d84735eb0e2d8cb42e0d285e/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", size = 705525 }, - { url = "https://files.pythonhosted.org/packages/07/91/45dfd0ef821a7f41d9d0136ea3608bb5b1653e42fd56a7970532cb5c003f/PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", size = 707514 }, - { url = "https://files.pythonhosted.org/packages/b6/a0/b6700da5d49e9fed49dc3243d3771b598dad07abb37cc32e524607f96adc/PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", size = 130488 }, - { url = "https://files.pythonhosted.org/packages/24/97/9b59b43431f98d01806b288532da38099cc6f2fea0f3d712e21e269c0279/PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", size = 145338 }, - { url = "https://files.pythonhosted.org/packages/ec/0d/26fb23e8863e0aeaac0c64e03fd27367ad2ae3f3cccf3798ee98ce160368/PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", size = 187867 }, - { url = "https://files.pythonhosted.org/packages/28/09/55f715ddbf95a054b764b547f617e22f1d5e45d83905660e9a088078fe67/PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", size = 167530 }, - { url = "https://files.pythonhosted.org/packages/5e/94/7d5ee059dfb92ca9e62f4057dcdec9ac08a9e42679644854dc01177f8145/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", size = 732244 }, - { url = "https://files.pythonhosted.org/packages/06/92/e0224aa6ebf9dc54a06a4609da37da40bb08d126f5535d81bff6b417b2ae/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", size = 752871 }, - { url = "https://files.pythonhosted.org/packages/7b/5e/efd033ab7199a0b2044dab3b9f7a4f6670e6a52c089de572e928d2873b06/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", size = 757729 }, - { url = "https://files.pythonhosted.org/packages/03/5c/c4671451b2f1d76ebe352c0945d4cd13500adb5d05f5a51ee296d80152f7/PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", size = 748528 }, - { url = "https://files.pythonhosted.org/packages/73/9c/766e78d1efc0d1fca637a6b62cea1b4510a7fb93617eb805223294fef681/PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", size = 130286 }, - { url = "https://files.pythonhosted.org/packages/b3/34/65bb4b2d7908044963ebf614fe0fdb080773fc7030d7e39c8d3eddcd4257/PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", size = 144699 }, - { url = "https://files.pythonhosted.org/packages/bc/06/1b305bf6aa704343be85444c9d011f626c763abb40c0edc1cad13bfd7f86/PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", size = 178692 }, - { url = "https://files.pythonhosted.org/packages/84/02/404de95ced348b73dd84f70e15a41843d817ff8c1744516bf78358f2ffd2/PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", size = 165622 }, - { url = "https://files.pythonhosted.org/packages/c7/4c/4a2908632fc980da6d918b9de9c1d9d7d7e70b2672b1ad5166ed27841ef7/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", size = 696937 }, - { url = "https://files.pythonhosted.org/packages/b4/33/720548182ffa8344418126017aa1d4ab4aeec9a2275f04ce3f3573d8ace8/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", size = 724969 }, - { url = "https://files.pythonhosted.org/packages/4f/78/77b40157b6cb5f2d3d31a3d9b2efd1ba3505371f76730d267e8b32cf4b7f/PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", size = 712604 }, - { url = "https://files.pythonhosted.org/packages/2e/97/3e0e089ee85e840f4b15bfa00e4e63d84a3691ababbfea92d6f820ea6f21/PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", size = 126098 }, - { url = "https://files.pythonhosted.org/packages/2b/9f/fbade56564ad486809c27b322d0f7e6a89c01f6b4fe208402e90d4443a99/PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", size = 138675 }, - { url = "https://files.pythonhosted.org/packages/7f/5d/2779ea035ba1e533c32ed4a249b4e0448f583ba10830b21a3cddafe11a4e/PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", size = 191734 }, - { url = "https://files.pythonhosted.org/packages/e1/a1/27bfac14b90adaaccf8c8289f441e9f76d94795ec1e7a8f134d9f2cb3d0b/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", size = 723767 }, - { url = "https://files.pythonhosted.org/packages/c1/39/47ed4d65beec9ce07267b014be85ed9c204fa373515355d3efa62d19d892/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", size = 749067 }, - { url = "https://files.pythonhosted.org/packages/c8/6b/6600ac24725c7388255b2f5add93f91e58a5d7efaf4af244fdbcc11a541b/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", size = 736569 }, - { url = "https://files.pythonhosted.org/packages/0d/46/62ae77677e532c0af6c81ddd6f3dbc16bdcc1208b077457354442d220bfb/PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", size = 787738 }, - { url = "https://files.pythonhosted.org/packages/d6/6a/439d1a6f834b9a9db16332ce16c4a96dd0e3970b65fe08cbecd1711eeb77/PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", size = 139797 }, - { url = "https://files.pythonhosted.org/packages/29/0f/9782fa5b10152abf033aec56a601177ead85ee03b57781f2d9fced09eefc/PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", size = 157350 }, - { url = "https://files.pythonhosted.org/packages/57/c5/5d09b66b41d549914802f482a2118d925d876dc2a35b2d127694c1345c34/PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", size = 197846 }, - { url = "https://files.pythonhosted.org/packages/0e/88/21b2f16cb2123c1e9375f2c93486e35fdc86e63f02e274f0e99c589ef153/PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", size = 174396 }, - { url = "https://files.pythonhosted.org/packages/ac/6c/967d91a8edf98d2b2b01d149bd9e51b8f9fb527c98d80ebb60c6b21d60c4/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", size = 731824 }, - { url = "https://files.pythonhosted.org/packages/4a/4b/c71ef18ef83c82f99e6da8332910692af78ea32bd1d1d76c9787dfa36aea/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", size = 754777 }, - { url = "https://files.pythonhosted.org/packages/7d/39/472f2554a0f1e825bd7c5afc11c817cd7a2f3657460f7159f691fbb37c51/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", size = 738883 }, - { url = "https://files.pythonhosted.org/packages/40/da/a175a35cf5583580e90ac3e2a3dbca90e43011593ae62ce63f79d7b28d92/PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", size = 750294 }, - { url = "https://files.pythonhosted.org/packages/24/62/7fcc372442ec8ea331da18c24b13710e010c5073ab851ef36bf9dacb283f/PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", size = 136936 }, - { url = "https://files.pythonhosted.org/packages/84/4d/82704d1ab9290b03da94e6425f5e87396b999fd7eb8e08f3a92c158402bf/PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", size = 152751 }, +sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/af35f7ea75cf72f2cd079c95ee16797de7cd71f29ea7c68ae5ce7be1eda0/PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", size = 125201, upload_time = "2023-07-18T00:00:23.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/06/4beb652c0fe16834032e54f0956443d4cc797fe645527acee59e7deaa0a2/PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", size = 189447, upload_time = "2023-07-17T23:57:04.325Z" }, + { url = "https://files.pythonhosted.org/packages/5b/07/10033a403b23405a8fc48975444463d3d10a5c2736b7eb2550b07b367429/PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f", size = 169264, upload_time = "2023-07-17T23:57:07.787Z" }, + { url = "https://files.pythonhosted.org/packages/f1/26/55e4f21db1f72eaef092015d9017c11510e7e6301c62a6cfee91295d13c6/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", size = 677003, upload_time = "2023-07-17T23:57:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/ba/91/090818dfa62e85181f3ae23dd1e8b7ea7f09684864a900cab72d29c57346/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", size = 699070, upload_time = "2023-07-17T23:57:19.402Z" }, + { url = "https://files.pythonhosted.org/packages/29/61/bf33c6c85c55bc45a29eee3195848ff2d518d84735eb0e2d8cb42e0d285e/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", size = 705525, upload_time = "2023-07-17T23:57:25.272Z" }, + { url = "https://files.pythonhosted.org/packages/07/91/45dfd0ef821a7f41d9d0136ea3608bb5b1653e42fd56a7970532cb5c003f/PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", size = 707514, upload_time = "2023-08-28T18:43:20.945Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a0/b6700da5d49e9fed49dc3243d3771b598dad07abb37cc32e524607f96adc/PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", size = 130488, upload_time = "2023-07-17T23:57:28.144Z" }, + { url = "https://files.pythonhosted.org/packages/24/97/9b59b43431f98d01806b288532da38099cc6f2fea0f3d712e21e269c0279/PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", size = 145338, upload_time = "2023-07-17T23:57:31.118Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0d/26fb23e8863e0aeaac0c64e03fd27367ad2ae3f3cccf3798ee98ce160368/PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", size = 187867, upload_time = "2023-07-17T23:57:34.35Z" }, + { url = "https://files.pythonhosted.org/packages/28/09/55f715ddbf95a054b764b547f617e22f1d5e45d83905660e9a088078fe67/PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", size = 167530, upload_time = "2023-07-17T23:57:36.975Z" }, + { url = "https://files.pythonhosted.org/packages/5e/94/7d5ee059dfb92ca9e62f4057dcdec9ac08a9e42679644854dc01177f8145/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", size = 732244, upload_time = "2023-07-17T23:57:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/06/92/e0224aa6ebf9dc54a06a4609da37da40bb08d126f5535d81bff6b417b2ae/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", size = 752871, upload_time = "2023-07-17T23:57:51.921Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5e/efd033ab7199a0b2044dab3b9f7a4f6670e6a52c089de572e928d2873b06/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", size = 757729, upload_time = "2023-07-17T23:57:59.865Z" }, + { url = "https://files.pythonhosted.org/packages/03/5c/c4671451b2f1d76ebe352c0945d4cd13500adb5d05f5a51ee296d80152f7/PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", size = 748528, upload_time = "2023-08-28T18:43:23.207Z" }, + { url = "https://files.pythonhosted.org/packages/73/9c/766e78d1efc0d1fca637a6b62cea1b4510a7fb93617eb805223294fef681/PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", size = 130286, upload_time = "2023-07-17T23:58:02.964Z" }, + { url = "https://files.pythonhosted.org/packages/b3/34/65bb4b2d7908044963ebf614fe0fdb080773fc7030d7e39c8d3eddcd4257/PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", size = 144699, upload_time = "2023-07-17T23:58:05.586Z" }, + { url = "https://files.pythonhosted.org/packages/bc/06/1b305bf6aa704343be85444c9d011f626c763abb40c0edc1cad13bfd7f86/PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", size = 178692, upload_time = "2023-08-28T18:43:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/84/02/404de95ced348b73dd84f70e15a41843d817ff8c1744516bf78358f2ffd2/PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", size = 165622, upload_time = "2023-08-28T18:43:26.54Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4c/4a2908632fc980da6d918b9de9c1d9d7d7e70b2672b1ad5166ed27841ef7/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", size = 696937, upload_time = "2024-01-18T20:40:22.92Z" }, + { url = "https://files.pythonhosted.org/packages/b4/33/720548182ffa8344418126017aa1d4ab4aeec9a2275f04ce3f3573d8ace8/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", size = 724969, upload_time = "2023-08-28T18:43:28.56Z" }, + { url = "https://files.pythonhosted.org/packages/4f/78/77b40157b6cb5f2d3d31a3d9b2efd1ba3505371f76730d267e8b32cf4b7f/PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", size = 712604, upload_time = "2023-08-28T18:43:30.206Z" }, + { url = "https://files.pythonhosted.org/packages/2e/97/3e0e089ee85e840f4b15bfa00e4e63d84a3691ababbfea92d6f820ea6f21/PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", size = 126098, upload_time = "2023-08-28T18:43:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/fbade56564ad486809c27b322d0f7e6a89c01f6b4fe208402e90d4443a99/PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", size = 138675, upload_time = "2023-08-28T18:43:33.613Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5d/2779ea035ba1e533c32ed4a249b4e0448f583ba10830b21a3cddafe11a4e/PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", size = 191734, upload_time = "2023-07-17T23:59:13.869Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a1/27bfac14b90adaaccf8c8289f441e9f76d94795ec1e7a8f134d9f2cb3d0b/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", size = 723767, upload_time = "2023-07-17T23:59:20.686Z" }, + { url = "https://files.pythonhosted.org/packages/c1/39/47ed4d65beec9ce07267b014be85ed9c204fa373515355d3efa62d19d892/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", size = 749067, upload_time = "2023-07-17T23:59:28.747Z" }, + { url = "https://files.pythonhosted.org/packages/c8/6b/6600ac24725c7388255b2f5add93f91e58a5d7efaf4af244fdbcc11a541b/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", size = 736569, upload_time = "2023-07-17T23:59:37.216Z" }, + { url = "https://files.pythonhosted.org/packages/0d/46/62ae77677e532c0af6c81ddd6f3dbc16bdcc1208b077457354442d220bfb/PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", size = 787738, upload_time = "2023-08-28T18:43:35.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6a/439d1a6f834b9a9db16332ce16c4a96dd0e3970b65fe08cbecd1711eeb77/PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", size = 139797, upload_time = "2023-07-17T23:59:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/29/0f/9782fa5b10152abf033aec56a601177ead85ee03b57781f2d9fced09eefc/PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", size = 157350, upload_time = "2023-07-17T23:59:42.94Z" }, + { url = "https://files.pythonhosted.org/packages/57/c5/5d09b66b41d549914802f482a2118d925d876dc2a35b2d127694c1345c34/PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", size = 197846, upload_time = "2023-07-17T23:59:46.424Z" }, + { url = "https://files.pythonhosted.org/packages/0e/88/21b2f16cb2123c1e9375f2c93486e35fdc86e63f02e274f0e99c589ef153/PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", size = 174396, upload_time = "2023-07-17T23:59:49.538Z" }, + { url = "https://files.pythonhosted.org/packages/ac/6c/967d91a8edf98d2b2b01d149bd9e51b8f9fb527c98d80ebb60c6b21d60c4/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", size = 731824, upload_time = "2023-07-17T23:59:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4b/c71ef18ef83c82f99e6da8332910692af78ea32bd1d1d76c9787dfa36aea/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", size = 754777, upload_time = "2023-07-18T00:00:06.716Z" }, + { url = "https://files.pythonhosted.org/packages/7d/39/472f2554a0f1e825bd7c5afc11c817cd7a2f3657460f7159f691fbb37c51/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", size = 738883, upload_time = "2023-07-18T00:00:14.423Z" }, + { url = "https://files.pythonhosted.org/packages/40/da/a175a35cf5583580e90ac3e2a3dbca90e43011593ae62ce63f79d7b28d92/PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", size = 750294, upload_time = "2023-08-28T18:43:37.153Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/7fcc372442ec8ea331da18c24b13710e010c5073ab851ef36bf9dacb283f/PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", size = 136936, upload_time = "2023-07-18T00:00:17.167Z" }, + { url = "https://files.pythonhosted.org/packages/84/4d/82704d1ab9290b03da94e6425f5e87396b999fd7eb8e08f3a92c158402bf/PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", size = 152751, upload_time = "2023-07-18T00:00:19.939Z" }, ] [[package]] @@ -833,110 +1150,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, -] - -[[package]] -name = "regex" -version = "2024.9.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/38/148df33b4dbca3bd069b963acab5e0fa1a9dbd6820f8c322d0dd6faeff96/regex-2024.9.11.tar.gz", hash = "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd", size = 399403 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/63/12/497bd6599ce8a239ade68678132296aec5ee25ebea45fc8ba91aa60fceec/regex-2024.9.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408", size = 482488 }, - { url = "https://files.pythonhosted.org/packages/c1/24/595ddb9bec2a9b151cdaf9565b0c9f3da9f0cb1dca6c158bc5175332ddf8/regex-2024.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d", size = 287443 }, - { url = "https://files.pythonhosted.org/packages/69/a8/b2fb45d9715b1469383a0da7968f8cacc2f83e9fbbcd6b8713752dd980a6/regex-2024.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5", size = 284561 }, - { url = "https://files.pythonhosted.org/packages/88/87/1ce4a5357216b19b7055e7d3b0efc75a6e426133bf1e7d094321df514257/regex-2024.9.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c", size = 783177 }, - { url = "https://files.pythonhosted.org/packages/3c/65/b9f002ab32f7b68e7d1dcabb67926f3f47325b8dbc22cc50b6a043e1d07c/regex-2024.9.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8", size = 823193 }, - { url = "https://files.pythonhosted.org/packages/22/91/8339dd3abce101204d246e31bc26cdd7ec07c9f91598472459a3a902aa41/regex-2024.9.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35", size = 809950 }, - { url = "https://files.pythonhosted.org/packages/cb/19/556638aa11c2ec9968a1da998f07f27ec0abb9bf3c647d7c7985ca0b8eea/regex-2024.9.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71", size = 782661 }, - { url = "https://files.pythonhosted.org/packages/d1/e9/7a5bc4c6ef8d9cd2bdd83a667888fc35320da96a4cc4da5fa084330f53db/regex-2024.9.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8", size = 772348 }, - { url = "https://files.pythonhosted.org/packages/f1/0b/29f2105bfac3ed08e704914c38e93b07c784a6655f8a015297ee7173e95b/regex-2024.9.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a", size = 697460 }, - { url = "https://files.pythonhosted.org/packages/71/3a/52ff61054d15a4722605f5872ad03962b319a04c1ebaebe570b8b9b7dde1/regex-2024.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d", size = 769151 }, - { url = "https://files.pythonhosted.org/packages/97/07/37e460ab5ca84be8e1e197c3b526c5c86993dcc9e13cbc805c35fc2463c1/regex-2024.9.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137", size = 777478 }, - { url = "https://files.pythonhosted.org/packages/65/7b/953075723dd5ab00780043ac2f9de667306ff9e2a85332975e9f19279174/regex-2024.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6", size = 845373 }, - { url = "https://files.pythonhosted.org/packages/40/b8/3e9484c6230b8b6e8f816ab7c9a080e631124991a4ae2c27a81631777db0/regex-2024.9.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca", size = 845369 }, - { url = "https://files.pythonhosted.org/packages/b7/99/38434984d912edbd2e1969d116257e869578f67461bd7462b894c45ed874/regex-2024.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a", size = 773935 }, - { url = "https://files.pythonhosted.org/packages/ab/67/43174d2b46fa947b7b9dfe56b6c8a8a76d44223f35b1d64645a732fd1d6f/regex-2024.9.11-cp310-cp310-win32.whl", hash = "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0", size = 261624 }, - { url = "https://files.pythonhosted.org/packages/c4/2a/4f9c47d9395b6aff24874c761d8d620c0232f97c43ef3cf668c8b355e7a7/regex-2024.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623", size = 274020 }, - { url = "https://files.pythonhosted.org/packages/86/a1/d526b7b6095a0019aa360948c143aacfeb029919c898701ce7763bbe4c15/regex-2024.9.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df", size = 482483 }, - { url = "https://files.pythonhosted.org/packages/32/d9/bfdd153179867c275719e381e1e8e84a97bd186740456a0dcb3e7125c205/regex-2024.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268", size = 287442 }, - { url = "https://files.pythonhosted.org/packages/33/c4/60f3370735135e3a8d673ddcdb2507a8560d0e759e1398d366e43d000253/regex-2024.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad", size = 284561 }, - { url = "https://files.pythonhosted.org/packages/b1/51/91a5ebdff17f9ec4973cb0aa9d37635efec1c6868654bbc25d1543aca4ec/regex-2024.9.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679", size = 791779 }, - { url = "https://files.pythonhosted.org/packages/07/4a/022c5e6f0891a90cd7eb3d664d6c58ce2aba48bff107b00013f3d6167069/regex-2024.9.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4", size = 832605 }, - { url = "https://files.pythonhosted.org/packages/ac/1c/3793990c8c83ca04e018151ddda83b83ecc41d89964f0f17749f027fc44d/regex-2024.9.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664", size = 818556 }, - { url = "https://files.pythonhosted.org/packages/e9/5c/8b385afbfacb853730682c57be56225f9fe275c5bf02ac1fc88edbff316d/regex-2024.9.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50", size = 792808 }, - { url = "https://files.pythonhosted.org/packages/9b/8b/a4723a838b53c771e9240951adde6af58c829fb6a6a28f554e8131f53839/regex-2024.9.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199", size = 781115 }, - { url = "https://files.pythonhosted.org/packages/83/5f/031a04b6017033d65b261259c09043c06f4ef2d4eac841d0649d76d69541/regex-2024.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4", size = 778155 }, - { url = "https://files.pythonhosted.org/packages/fd/cd/4660756070b03ce4a66663a43f6c6e7ebc2266cc6b4c586c167917185eb4/regex-2024.9.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd", size = 784614 }, - { url = "https://files.pythonhosted.org/packages/93/8d/65b9bea7df120a7be8337c415b6d256ba786cbc9107cebba3bf8ff09da99/regex-2024.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f", size = 853744 }, - { url = "https://files.pythonhosted.org/packages/96/a7/fba1eae75eb53a704475baf11bd44b3e6ccb95b316955027eb7748f24ef8/regex-2024.9.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96", size = 855890 }, - { url = "https://files.pythonhosted.org/packages/45/14/d864b2db80a1a3358534392373e8a281d95b28c29c87d8548aed58813910/regex-2024.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1", size = 781887 }, - { url = "https://files.pythonhosted.org/packages/4d/a9/bfb29b3de3eb11dc9b412603437023b8e6c02fb4e11311863d9bf62c403a/regex-2024.9.11-cp311-cp311-win32.whl", hash = "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9", size = 261644 }, - { url = "https://files.pythonhosted.org/packages/c7/ab/1ad2511cf6a208fde57fafe49829cab8ca018128ab0d0b48973d8218634a/regex-2024.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf", size = 274033 }, - { url = "https://files.pythonhosted.org/packages/6e/92/407531450762bed778eedbde04407f68cbd75d13cee96c6f8d6903d9c6c1/regex-2024.9.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7", size = 483590 }, - { url = "https://files.pythonhosted.org/packages/8e/a2/048acbc5ae1f615adc6cba36cc45734e679b5f1e4e58c3c77f0ed611d4e2/regex-2024.9.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231", size = 288175 }, - { url = "https://files.pythonhosted.org/packages/8a/ea/909d8620329ab710dfaf7b4adee41242ab7c9b95ea8d838e9bfe76244259/regex-2024.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d", size = 284749 }, - { url = "https://files.pythonhosted.org/packages/ca/fa/521eb683b916389b4975337873e66954e0f6d8f91bd5774164a57b503185/regex-2024.9.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64", size = 795181 }, - { url = "https://files.pythonhosted.org/packages/28/db/63047feddc3280cc242f9c74f7aeddc6ee662b1835f00046f57d5630c827/regex-2024.9.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42", size = 835842 }, - { url = "https://files.pythonhosted.org/packages/e3/94/86adc259ff8ec26edf35fcca7e334566c1805c7493b192cb09679f9c3dee/regex-2024.9.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766", size = 823533 }, - { url = "https://files.pythonhosted.org/packages/29/52/84662b6636061277cb857f658518aa7db6672bc6d1a3f503ccd5aefc581e/regex-2024.9.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a", size = 797037 }, - { url = "https://files.pythonhosted.org/packages/c3/2a/cd4675dd987e4a7505f0364a958bc41f3b84942de9efaad0ef9a2646681c/regex-2024.9.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9", size = 784106 }, - { url = "https://files.pythonhosted.org/packages/6f/75/3ea7ec29de0bbf42f21f812f48781d41e627d57a634f3f23947c9a46e303/regex-2024.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d", size = 782468 }, - { url = "https://files.pythonhosted.org/packages/d3/67/15519d69b52c252b270e679cb578e22e0c02b8dd4e361f2b04efcc7f2335/regex-2024.9.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822", size = 790324 }, - { url = "https://files.pythonhosted.org/packages/9c/71/eff77d3fe7ba08ab0672920059ec30d63fa7e41aa0fb61c562726e9bd721/regex-2024.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0", size = 860214 }, - { url = "https://files.pythonhosted.org/packages/81/11/e1bdf84a72372e56f1ea4b833dd583b822a23138a616ace7ab57a0e11556/regex-2024.9.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a", size = 859420 }, - { url = "https://files.pythonhosted.org/packages/ea/75/9753e9dcebfa7c3645563ef5c8a58f3a47e799c872165f37c55737dadd3e/regex-2024.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a", size = 787333 }, - { url = "https://files.pythonhosted.org/packages/bc/4e/ba1cbca93141f7416624b3ae63573e785d4bc1834c8be44a8f0747919eca/regex-2024.9.11-cp312-cp312-win32.whl", hash = "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776", size = 262058 }, - { url = "https://files.pythonhosted.org/packages/6e/16/efc5f194778bf43e5888209e5cec4b258005d37c613b67ae137df3b89c53/regex-2024.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009", size = 273526 }, - { url = "https://files.pythonhosted.org/packages/93/0a/d1c6b9af1ff1e36832fe38d74d5c5bab913f2bdcbbd6bc0e7f3ce8b2f577/regex-2024.9.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784", size = 483376 }, - { url = "https://files.pythonhosted.org/packages/a4/42/5910a050c105d7f750a72dcb49c30220c3ae4e2654e54aaaa0e9bc0584cb/regex-2024.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36", size = 288112 }, - { url = "https://files.pythonhosted.org/packages/8d/56/0c262aff0e9224fa7ffce47b5458d373f4d3e3ff84e99b5ff0cb15e0b5b2/regex-2024.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92", size = 284608 }, - { url = "https://files.pythonhosted.org/packages/b9/54/9fe8f9aec5007bbbbce28ba3d2e3eaca425f95387b7d1e84f0d137d25237/regex-2024.9.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86", size = 795337 }, - { url = "https://files.pythonhosted.org/packages/b2/e7/6b2f642c3cded271c4f16cc4daa7231be544d30fe2b168e0223724b49a61/regex-2024.9.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85", size = 835848 }, - { url = "https://files.pythonhosted.org/packages/cd/9e/187363bdf5d8c0e4662117b92aa32bf52f8f09620ae93abc7537d96d3311/regex-2024.9.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963", size = 823503 }, - { url = "https://files.pythonhosted.org/packages/f8/10/601303b8ee93589f879664b0cfd3127949ff32b17f9b6c490fb201106c4d/regex-2024.9.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6", size = 797049 }, - { url = "https://files.pythonhosted.org/packages/ef/1c/ea200f61ce9f341763f2717ab4daebe4422d83e9fd4ac5e33435fd3a148d/regex-2024.9.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802", size = 784144 }, - { url = "https://files.pythonhosted.org/packages/d8/5c/d2429be49ef3292def7688401d3deb11702c13dcaecdc71d2b407421275b/regex-2024.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29", size = 782483 }, - { url = "https://files.pythonhosted.org/packages/12/d9/cbc30f2ff7164f3b26a7760f87c54bf8b2faed286f60efd80350a51c5b99/regex-2024.9.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8", size = 790320 }, - { url = "https://files.pythonhosted.org/packages/19/1d/43ed03a236313639da5a45e61bc553c8d41e925bcf29b0f8ecff0c2c3f25/regex-2024.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84", size = 860435 }, - { url = "https://files.pythonhosted.org/packages/34/4f/5d04da61c7c56e785058a46349f7285ae3ebc0726c6ea7c5c70600a52233/regex-2024.9.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554", size = 859571 }, - { url = "https://files.pythonhosted.org/packages/12/7f/8398c8155a3c70703a8e91c29532558186558e1aea44144b382faa2a6f7a/regex-2024.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8", size = 787398 }, - { url = "https://files.pythonhosted.org/packages/58/3a/f5903977647a9a7e46d5535e9e96c194304aeeca7501240509bde2f9e17f/regex-2024.9.11-cp313-cp313-win32.whl", hash = "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8", size = 262035 }, - { url = "https://files.pythonhosted.org/packages/ff/80/51ba3a4b7482f6011095b3a036e07374f64de180b7d870b704ed22509002/regex-2024.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f", size = 273510 }, - { url = "https://files.pythonhosted.org/packages/58/03/ac6839452b59793683c33a3eb782671863800f4d514aec81f38098d2846f/regex-2024.9.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4", size = 482618 }, - { url = "https://files.pythonhosted.org/packages/c2/8f/acb2dbdcb0ec4ce99a06544868c4a3463faad344f89437712419ccbd70a4/regex-2024.9.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e", size = 287536 }, - { url = "https://files.pythonhosted.org/packages/28/47/d267e0c8f327d717f565cdba76d354993a350a0f6aba8efa650c0fe93d79/regex-2024.9.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60", size = 284558 }, - { url = "https://files.pythonhosted.org/packages/7f/0d/7ec6c7c306cea8fcf7413565d0f778ba056bf2b2fa97508e0506521987e7/regex-2024.9.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b", size = 784441 }, - { url = "https://files.pythonhosted.org/packages/ab/c3/b1db10548c31491fe8c8e904e032f1b9af1fd6d193bab32324caed4caf7a/regex-2024.9.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366", size = 824268 }, - { url = "https://files.pythonhosted.org/packages/5e/82/90127f8e15384c1edee89cc9b937c453e7e2419e635dc160ac2bd7f8239e/regex-2024.9.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8", size = 810498 }, - { url = "https://files.pythonhosted.org/packages/75/d1/ea4e9b22e2b19463d0def76418e21316b9a8acc88ce6b764353834015ee0/regex-2024.9.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb", size = 785037 }, - { url = "https://files.pythonhosted.org/packages/9c/92/4360fab411bad3dc9862742407d9c1790858e160e1e732ad2491747c4053/regex-2024.9.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4", size = 774303 }, - { url = "https://files.pythonhosted.org/packages/89/7b/4c3b129108fbcab118f93da68d7ac84dff831d71fafd8bcda711f572ac0d/regex-2024.9.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca", size = 702542 }, - { url = "https://files.pythonhosted.org/packages/3f/9f/d1834185895df468860597f5934272539f21bc8112013f2e892fd6dd588e/regex-2024.9.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb", size = 771060 }, - { url = "https://files.pythonhosted.org/packages/4d/87/95091e0f6fc69d1235e61ddabc5dc64f00c6ee6288872564233f9a41bf27/regex-2024.9.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168", size = 777218 }, - { url = "https://files.pythonhosted.org/packages/d0/d4/d5d5c2f757b25a5556033571421bfa469c9b0dfc5e59efeaf0eb88ecfa39/regex-2024.9.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e", size = 846146 }, - { url = "https://files.pythonhosted.org/packages/41/ba/4ce9ef3e3fe1645a55b1546353768a11ac8ffc8e7a9b0a445affcd3aabe2/regex-2024.9.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c", size = 846445 }, - { url = "https://files.pythonhosted.org/packages/5f/13/387d6c7d39c55dbfb06552b7ace7a2ddc05493403618a2f55da71f495832/regex-2024.9.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd", size = 775188 }, - { url = "https://files.pythonhosted.org/packages/a4/f3/262c44485a858e496efea890f141621a05354753fa59ac4f2a41e9bf12a4/regex-2024.9.11-cp38-cp38-win32.whl", hash = "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771", size = 261652 }, - { url = "https://files.pythonhosted.org/packages/33/fc/07e14d7727a9f5773abb728f18f3497ea917c76f16155a68594460d86b8b/regex-2024.9.11-cp38-cp38-win_amd64.whl", hash = "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508", size = 274070 }, - { url = "https://files.pythonhosted.org/packages/a1/aa/e31baf8482ad690ccb3cdf20d1963a01e98d137e4d9ee493dbb0fa8ba2c6/regex-2024.9.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066", size = 482489 }, - { url = "https://files.pythonhosted.org/packages/a1/b5/449c2f14fc20dc42ef9729469fcff42809393470f021ed6c6fcf5f3d3297/regex-2024.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62", size = 287440 }, - { url = "https://files.pythonhosted.org/packages/3f/36/4b60a0c2e4cc6ecb2651be828117a31f42fae55a51a484a8071729df56a6/regex-2024.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16", size = 284566 }, - { url = "https://files.pythonhosted.org/packages/b4/21/feaa5b0d3e5e3bad659cd7d640e6b76cc0719504dbd9bc8f67cfa21bde82/regex-2024.9.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3", size = 782747 }, - { url = "https://files.pythonhosted.org/packages/bb/89/93516f0aa3e8a9366df2cf79bb0290abdc7dbe5dd27373d9bea0978b7ba6/regex-2024.9.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199", size = 822700 }, - { url = "https://files.pythonhosted.org/packages/d5/e7/79c04ccb81cee2831d9d4499274919b9153c1741ce8b3421d69cb0032f1b/regex-2024.9.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8", size = 809327 }, - { url = "https://files.pythonhosted.org/packages/01/e6/a7256c99c312b68f01cfd4f8eae6e770906fffb3832ecb66f35ca5b86b96/regex-2024.9.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca", size = 781970 }, - { url = "https://files.pythonhosted.org/packages/18/c4/29e8b6ff2208775858b5d4a2caa6428d40b5fade95aee426de7e42ffff39/regex-2024.9.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9", size = 771885 }, - { url = "https://files.pythonhosted.org/packages/95/78/7acd8882ac335f1f5ae1756417739fda3053e0bcacea8716ae4a04e74553/regex-2024.9.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a", size = 696978 }, - { url = "https://files.pythonhosted.org/packages/cb/d2/1d44f9b4a3d33ff5773fd79bea53e992d00f81e0af6f1f4e2efac1e4d897/regex-2024.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39", size = 768655 }, - { url = "https://files.pythonhosted.org/packages/79/ba/92ef9d3b8f59cb3df9febef07098dfb4a43c3bdcf35b1084c2009b0a93bf/regex-2024.9.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba", size = 776922 }, - { url = "https://files.pythonhosted.org/packages/16/71/d964c0c9d447f04bbe6ab5eafd220208e7d52b9608e452e6fcad553b38e0/regex-2024.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664", size = 845014 }, - { url = "https://files.pythonhosted.org/packages/83/cb/a378cdc2468782eefefa50183bbeabc3357fb588d4109d845f0a56e68713/regex-2024.9.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89", size = 844916 }, - { url = "https://files.pythonhosted.org/packages/b9/f0/82ea1565a6639270cfe96263002b3d91084a1db5048d9b6084f83bd5972d/regex-2024.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35", size = 773409 }, - { url = "https://files.pythonhosted.org/packages/97/9e/0400d742b9647b4940609a96d550de89e4e89c85f6a370796dab25b5979c/regex-2024.9.11-cp39-cp39-win32.whl", hash = "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142", size = 261680 }, - { url = "https://files.pythonhosted.org/packages/b6/f1/aef1112652ac7b3922d2c129f8325a4fd286b66691127dd99f380f8ede19/regex-2024.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919", size = 274066 }, +sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631, upload_time = "2020-11-12T02:38:26.239Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911, upload_time = "2020-11-12T02:38:24.638Z" }, ] [[package]] @@ -947,139 +1163,258 @@ dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, - { name = "urllib3" }, + { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "urllib3", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload_time = "2024-05-29T15:37:49.536Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload_time = "2024-05-29T15:37:47.027Z" }, ] [[package]] name = "ruff" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/d6/a2373f3ba7180ddb44420d2a9d1f1510e1a4d162b3d27282bedcb09c8da9/ruff-0.8.0.tar.gz", hash = "sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44", size = 3276537 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/77/e889ee3ce7fd8baa3ed1b77a03b9fb8ec1be68be1418261522fd6a5405e0/ruff-0.8.0-py3-none-linux_armv6l.whl", hash = "sha256:fcb1bf2cc6706adae9d79c8d86478677e3bbd4ced796ccad106fd4776d395fea", size = 10518283 }, - { url = "https://files.pythonhosted.org/packages/da/c8/0a47de01edf19fb22f5f9b7964f46a68d0bdff20144d134556ffd1ba9154/ruff-0.8.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:295bb4c02d58ff2ef4378a1870c20af30723013f441c9d1637a008baaf928c8b", size = 10317691 }, - { url = "https://files.pythonhosted.org/packages/41/17/9885e4a0eeae07abd2a4ebabc3246f556719f24efa477ba2739146c4635a/ruff-0.8.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b1f1c76b47c18fa92ee78b60d2d20d7e866c55ee603e7d19c1e991fad933a9a", size = 9940999 }, - { url = "https://files.pythonhosted.org/packages/3e/cd/46b6f7043597eb318b5f5482c8ae8f5491cccce771e85f59d23106f2d179/ruff-0.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb0d4f250a7711b67ad513fde67e8870109e5ce590a801c3722580fe98c33a99", size = 10772437 }, - { url = "https://files.pythonhosted.org/packages/5d/87/afc95aeb8bc78b1d8a3461717a4419c05aa8aa943d4c9cbd441630f85584/ruff-0.8.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e55cce9aa93c5d0d4e3937e47b169035c7e91c8655b0974e61bb79cf398d49c", size = 10299156 }, - { url = "https://files.pythonhosted.org/packages/65/fa/04c647bb809c4d65e8eae1ed1c654d9481b21dd942e743cd33511687b9f9/ruff-0.8.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f4cd64916d8e732ce6b87f3f5296a8942d285bbbc161acee7fe561134af64f9", size = 11325819 }, - { url = "https://files.pythonhosted.org/packages/90/26/7dad6e7d833d391a8a1afe4ee70ca6f36c4a297d3cca83ef10e83e9aacf3/ruff-0.8.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c5c1466be2a2ebdf7c5450dd5d980cc87c8ba6976fb82582fea18823da6fa362", size = 12023927 }, - { url = "https://files.pythonhosted.org/packages/24/a0/be5296dda6428ba8a13bda8d09fbc0e14c810b485478733886e61597ae2b/ruff-0.8.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2dabfd05b96b7b8f2da00d53c514eea842bff83e41e1cceb08ae1966254a51df", size = 11589702 }, - { url = "https://files.pythonhosted.org/packages/26/3f/7602eb11d2886db545834182a9dbe500b8211fcbc9b4064bf9d358bbbbb4/ruff-0.8.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:facebdfe5a5af6b1588a1d26d170635ead6892d0e314477e80256ef4a8470cf3", size = 12782936 }, - { url = "https://files.pythonhosted.org/packages/4c/5d/083181bdec4ec92a431c1291d3fff65eef3ded630a4b55eb735000ef5f3b/ruff-0.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a8e86bae0dbd749c815211ca11e3a7bd559b9710746c559ed63106d382bd9c", size = 11138488 }, - { url = "https://files.pythonhosted.org/packages/b7/23/c12cdef58413cee2436d6a177aa06f7a366ebbca916cf10820706f632459/ruff-0.8.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85e654f0ded7befe2d61eeaf3d3b1e4ef3894469cd664ffa85006c7720f1e4a2", size = 10744474 }, - { url = "https://files.pythonhosted.org/packages/29/61/a12f3b81520083cd7c5caa24ba61bb99fd1060256482eff0ef04cc5ccd1b/ruff-0.8.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:83a55679c4cb449fa527b8497cadf54f076603cc36779b2170b24f704171ce70", size = 10369029 }, - { url = "https://files.pythonhosted.org/packages/08/2a/c013f4f3e4a54596c369cee74c24870ed1d534f31a35504908b1fc97017a/ruff-0.8.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:812e2052121634cf13cd6fddf0c1871d0ead1aad40a1a258753c04c18bb71bbd", size = 10867481 }, - { url = "https://files.pythonhosted.org/packages/d5/f7/685b1e1d42a3e94ceb25eab23c70bdd8c0ab66a43121ef83fe6db5a58756/ruff-0.8.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:780d5d8523c04202184405e60c98d7595bdb498c3c6abba3b6d4cdf2ca2af426", size = 11237117 }, - { url = "https://files.pythonhosted.org/packages/03/20/401132c0908e8837625e3b7e32df9962e7cd681a4df1e16a10e2a5b4ecda/ruff-0.8.0-py3-none-win32.whl", hash = "sha256:5fdb6efecc3eb60bba5819679466471fd7d13c53487df7248d6e27146e985468", size = 8783511 }, - { url = "https://files.pythonhosted.org/packages/1d/5c/4d800fca7854f62ad77f2c0d99b4b585f03e2d87a6ec1ecea85543a14a3c/ruff-0.8.0-py3-none-win_amd64.whl", hash = "sha256:582891c57b96228d146725975fbb942e1f30a0c4ba19722e692ca3eb25cc9b4f", size = 9559876 }, - { url = "https://files.pythonhosted.org/packages/5b/bc/cc8a6a5ca4960b226dc15dd8fb511dd11f2014ff89d325c0b9b9faa9871f/ruff-0.8.0-py3-none-win_arm64.whl", hash = "sha256:ba93e6294e9a737cd726b74b09a6972e36bb511f9a102f1d9a7e1ce94dd206a6", size = 8939733 }, +version = "0.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/89/6f9c9674818ac2e9cc2f2b35b704b7768656e6b7c139064fc7ba8fbc99f1/ruff-0.11.7.tar.gz", hash = "sha256:655089ad3224070736dc32844fde783454f8558e71f501cb207485fe4eee23d4", size = 4054861, upload_time = "2025-04-24T18:49:37.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/ec/21927cb906c5614b786d1621dba405e3d44f6e473872e6df5d1a6bca0455/ruff-0.11.7-py3-none-linux_armv6l.whl", hash = "sha256:d29e909d9a8d02f928d72ab7837b5cbc450a5bdf578ab9ebee3263d0a525091c", size = 10245403, upload_time = "2025-04-24T18:48:40.459Z" }, + { url = "https://files.pythonhosted.org/packages/e2/af/fec85b6c2c725bcb062a354dd7cbc1eed53c33ff3aa665165871c9c16ddf/ruff-0.11.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dd1fb86b168ae349fb01dd497d83537b2c5541fe0626e70c786427dd8363aaee", size = 11007166, upload_time = "2025-04-24T18:48:44.742Z" }, + { url = "https://files.pythonhosted.org/packages/31/9a/2d0d260a58e81f388800343a45898fd8df73c608b8261c370058b675319a/ruff-0.11.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d3d7d2e140a6fbbc09033bce65bd7ea29d6a0adeb90b8430262fbacd58c38ada", size = 10378076, upload_time = "2025-04-24T18:48:47.918Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/9b09b45051404d2e7dd6d9dbcbabaa5ab0093f9febcae664876a77b9ad53/ruff-0.11.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4809df77de390a1c2077d9b7945d82f44b95d19ceccf0c287c56e4dc9b91ca64", size = 10557138, upload_time = "2025-04-24T18:48:51.707Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5e/f62a1b6669870a591ed7db771c332fabb30f83c967f376b05e7c91bccd14/ruff-0.11.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3a0c2e169e6b545f8e2dba185eabbd9db4f08880032e75aa0e285a6d3f48201", size = 10095726, upload_time = "2025-04-24T18:48:54.243Z" }, + { url = "https://files.pythonhosted.org/packages/45/59/a7aa8e716f4cbe07c3500a391e58c52caf665bb242bf8be42c62adef649c/ruff-0.11.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49b888200a320dd96a68e86736cf531d6afba03e4f6cf098401406a257fcf3d6", size = 11672265, upload_time = "2025-04-24T18:48:57.639Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e3/101a8b707481f37aca5f0fcc3e42932fa38b51add87bfbd8e41ab14adb24/ruff-0.11.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2b19cdb9cf7dae00d5ee2e7c013540cdc3b31c4f281f1dacb5a799d610e90db4", size = 12331418, upload_time = "2025-04-24T18:49:00.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/71/037f76cbe712f5cbc7b852e4916cd3cf32301a30351818d32ab71580d1c0/ruff-0.11.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64e0ee994c9e326b43539d133a36a455dbaab477bc84fe7bfbd528abe2f05c1e", size = 11794506, upload_time = "2025-04-24T18:49:03.545Z" }, + { url = "https://files.pythonhosted.org/packages/ca/de/e450b6bab1fc60ef263ef8fcda077fb4977601184877dce1c59109356084/ruff-0.11.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bad82052311479a5865f52c76ecee5d468a58ba44fb23ee15079f17dd4c8fd63", size = 13939084, upload_time = "2025-04-24T18:49:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2c/1e364cc92970075d7d04c69c928430b23e43a433f044474f57e425cbed37/ruff-0.11.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7940665e74e7b65d427b82bffc1e46710ec7f30d58b4b2d5016e3f0321436502", size = 11450441, upload_time = "2025-04-24T18:49:11.41Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7d/1b048eb460517ff9accd78bca0fa6ae61df2b276010538e586f834f5e402/ruff-0.11.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:169027e31c52c0e36c44ae9a9c7db35e505fee0b39f8d9fca7274a6305295a92", size = 10441060, upload_time = "2025-04-24T18:49:14.184Z" }, + { url = "https://files.pythonhosted.org/packages/3a/57/8dc6ccfd8380e5ca3d13ff7591e8ba46a3b330323515a4996b991b10bd5d/ruff-0.11.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:305b93f9798aee582e91e34437810439acb28b5fc1fee6b8205c78c806845a94", size = 10058689, upload_time = "2025-04-24T18:49:17.559Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/20487561ed72654147817885559ba2aa705272d8b5dee7654d3ef2dbf912/ruff-0.11.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a681db041ef55550c371f9cd52a3cf17a0da4c75d6bd691092dfc38170ebc4b6", size = 11073703, upload_time = "2025-04-24T18:49:20.247Z" }, + { url = "https://files.pythonhosted.org/packages/9d/27/04f2db95f4ef73dccedd0c21daf9991cc3b7f29901a4362057b132075aa4/ruff-0.11.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:07f1496ad00a4a139f4de220b0c97da6d4c85e0e4aa9b2624167b7d4d44fd6b6", size = 11532822, upload_time = "2025-04-24T18:49:23.765Z" }, + { url = "https://files.pythonhosted.org/packages/e1/72/43b123e4db52144c8add336581de52185097545981ff6e9e58a21861c250/ruff-0.11.7-py3-none-win32.whl", hash = "sha256:f25dfb853ad217e6e5f1924ae8a5b3f6709051a13e9dad18690de6c8ff299e26", size = 10362436, upload_time = "2025-04-24T18:49:27.377Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a0/3e58cd76fdee53d5c8ce7a56d84540833f924ccdf2c7d657cb009e604d82/ruff-0.11.7-py3-none-win_amd64.whl", hash = "sha256:0a931d85959ceb77e92aea4bbedfded0a31534ce191252721128f77e5ae1f98a", size = 11566676, upload_time = "2025-04-24T18:49:30.938Z" }, + { url = "https://files.pythonhosted.org/packages/68/ca/69d7c7752bce162d1516e5592b1cc6b6668e9328c0d270609ddbeeadd7cf/ruff-0.11.7-py3-none-win_arm64.whl", hash = "sha256:778c1e5d6f9e91034142dfd06110534ca13220bfaad5c3735f6cb844654f6177", size = 10677936, upload_time = "2025-04-24T18:49:34.392Z" }, ] [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload_time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload_time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "tomli" -version = "2.0.1" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload_time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload_time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload_time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload_time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload_time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload_time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload_time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload_time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload_time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload_time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload_time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload_time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload_time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload_time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload_time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload_time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload_time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload_time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload_time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload_time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload_time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload_time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload_time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload_time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload_time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload_time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload_time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload_time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload_time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload_time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload_time = "2024-11-27T22:38:34.455Z" }, + { 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 = "types-pyyaml" +version = "6.0.12.20241230" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164 } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/f9/4d566925bcf9396136c0a2e5dc7e230ff08d86fa011a69888dd184469d80/types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c", size = 17078, upload_time = "2024-12-30T02:44:38.168Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, + { url = "https://files.pythonhosted.org/packages/e8/c1/48474fbead512b70ccdb4f81ba5eb4a58f69d100ba19f17c92c0c4f50ae6/types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6", size = 20029, upload_time = "2024-12-30T02:44:36.162Z" }, ] [[package]] name = "types-pyyaml" -version = "6.0.12.20240917" +version = "6.0.12.20250402" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/7d/a95df0a11f95c8f48d7683f03e4aed1a2c0fc73e9de15cca4d38034bea1a/types-PyYAML-6.0.12.20240917.tar.gz", hash = "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587", size = 12381 } +resolution-markers = [ + "python_full_version >= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/68/609eed7402f87c9874af39d35942744e39646d1ea9011765ec87b01b2a3c/types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075", size = 17282, upload_time = "2025-04-02T02:56:00.235Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/2c/c1d81d680997d24b0542aa336f0a65bd7835e5224b7670f33a7d617da379/types_PyYAML-6.0.12.20240917-py3-none-any.whl", hash = "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570", size = 15264 }, + { url = "https://files.pythonhosted.org/packages/ed/56/1fe61db05685fbb512c07ea9323f06ea727125951f1eb4dff110b3311da3/types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681", size = 20329, upload_time = "2025-04-02T02:55:59.382Z" }, ] [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload_time = "2025-04-10T14:19:05.416Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload_time = "2025-04-10T14:19:03.967Z" }, ] [[package]] name = "urllib3" version = "2.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677, upload_time = "2024-09-12T10:52:18.401Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338, upload_time = "2024-09-12T10:52:16.589Z" }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload_time = "2025-04-10T15:23:39.232Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload_time = "2025-04-10T15:23:37.377Z" }, ] [[package]] name = "watchdog" version = "4.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4f/38/764baaa25eb5e35c9a043d4c4588f9836edfe52a708950f4b6d5f714fd42/watchdog-4.0.2.tar.gz", hash = "sha256:b4dfbb6c49221be4535623ea4474a4d6ee0a9cef4a80b20c28db4d858b64e270", size = 126587 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/b0/219893d41c16d74d0793363bf86df07d50357b81f64bba4cb94fe76e7af4/watchdog-4.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ede7f010f2239b97cc79e6cb3c249e72962404ae3865860855d5cbe708b0fd22", size = 100257 }, - { url = "https://files.pythonhosted.org/packages/6d/c6/8e90c65693e87d98310b2e1e5fd7e313266990853b489e85ce8396cc26e3/watchdog-4.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2cffa171445b0efa0726c561eca9a27d00a1f2b83846dbd5a4f639c4f8ca8e1", size = 92249 }, - { url = "https://files.pythonhosted.org/packages/6f/cd/2e306756364a934532ff8388d90eb2dc8bb21fe575cd2b33d791ce05a02f/watchdog-4.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c50f148b31b03fbadd6d0b5980e38b558046b127dc483e5e4505fcef250f9503", size = 92888 }, - { url = "https://files.pythonhosted.org/packages/de/78/027ad372d62f97642349a16015394a7680530460b1c70c368c506cb60c09/watchdog-4.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c7d4bf585ad501c5f6c980e7be9c4f15604c7cc150e942d82083b31a7548930", size = 100256 }, - { url = "https://files.pythonhosted.org/packages/59/a9/412b808568c1814d693b4ff1cec0055dc791780b9dc947807978fab86bc1/watchdog-4.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:914285126ad0b6eb2258bbbcb7b288d9dfd655ae88fa28945be05a7b475a800b", size = 92252 }, - { url = "https://files.pythonhosted.org/packages/04/57/179d76076cff264982bc335dd4c7da6d636bd3e9860bbc896a665c3447b6/watchdog-4.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:984306dc4720da5498b16fc037b36ac443816125a3705dfde4fd90652d8028ef", size = 92888 }, - { url = "https://files.pythonhosted.org/packages/92/f5/ea22b095340545faea37ad9a42353b265ca751f543da3fb43f5d00cdcd21/watchdog-4.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1cdcfd8142f604630deef34722d695fb455d04ab7cfe9963055df1fc69e6727a", size = 100342 }, - { url = "https://files.pythonhosted.org/packages/cb/d2/8ce97dff5e465db1222951434e3115189ae54a9863aef99c6987890cc9ef/watchdog-4.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7ab624ff2f663f98cd03c8b7eedc09375a911794dfea6bf2a359fcc266bff29", size = 92306 }, - { url = "https://files.pythonhosted.org/packages/49/c4/1aeba2c31b25f79b03b15918155bc8c0b08101054fc727900f1a577d0d54/watchdog-4.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:132937547a716027bd5714383dfc40dc66c26769f1ce8a72a859d6a48f371f3a", size = 92915 }, - { url = "https://files.pythonhosted.org/packages/79/63/eb8994a182672c042d85a33507475c50c2ee930577524dd97aea05251527/watchdog-4.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd67c7df93eb58f360c43802acc945fa8da70c675b6fa37a241e17ca698ca49b", size = 100343 }, - { url = "https://files.pythonhosted.org/packages/ce/82/027c0c65c2245769580605bcd20a1dc7dfd6c6683c8c4e2ef43920e38d27/watchdog-4.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcfd02377be80ef3b6bc4ce481ef3959640458d6feaae0bd43dd90a43da90a7d", size = 92313 }, - { url = "https://files.pythonhosted.org/packages/2a/89/ad4715cbbd3440cb0d336b78970aba243a33a24b1a79d66f8d16b4590d6a/watchdog-4.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:980b71510f59c884d684b3663d46e7a14b457c9611c481e5cef08f4dd022eed7", size = 92919 }, - { url = "https://files.pythonhosted.org/packages/55/08/1a9086a3380e8828f65b0c835b86baf29ebb85e5e94a2811a2eb4f889cfd/watchdog-4.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:aa160781cafff2719b663c8a506156e9289d111d80f3387cf3af49cedee1f040", size = 100255 }, - { url = "https://files.pythonhosted.org/packages/6c/3e/064974628cf305831f3f78264800bd03b3358ec181e3e9380a36ff156b93/watchdog-4.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f6ee8dedd255087bc7fe82adf046f0b75479b989185fb0bdf9a98b612170eac7", size = 92257 }, - { url = "https://files.pythonhosted.org/packages/23/69/1d2ad9c12d93bc1e445baa40db46bc74757f3ffc3a3be592ba8dbc51b6e5/watchdog-4.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0b4359067d30d5b864e09c8597b112fe0a0a59321a0f331498b013fb097406b4", size = 92886 }, - { url = "https://files.pythonhosted.org/packages/68/eb/34d3173eceab490d4d1815ba9a821e10abe1da7a7264a224e30689b1450c/watchdog-4.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:770eef5372f146997638d737c9a3c597a3b41037cfbc5c41538fc27c09c3a3f9", size = 100254 }, - { url = "https://files.pythonhosted.org/packages/18/a1/4bbafe7ace414904c2cc9bd93e472133e8ec11eab0b4625017f0e34caad8/watchdog-4.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eeea812f38536a0aa859972d50c76e37f4456474b02bd93674d1947cf1e39578", size = 92249 }, - { url = "https://files.pythonhosted.org/packages/f3/11/ec5684e0ca692950826af0de862e5db167523c30c9cbf9b3f4ce7ec9cc05/watchdog-4.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b2c45f6e1e57ebb4687690c05bc3a2c1fb6ab260550c4290b8abb1335e0fd08b", size = 92891 }, - { url = "https://files.pythonhosted.org/packages/3b/9a/6f30f023324de7bad8a3eb02b0afb06bd0726003a3550e9964321315df5a/watchdog-4.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10b6683df70d340ac3279eff0b2766813f00f35a1d37515d2c99959ada8f05fa", size = 91775 }, - { url = "https://files.pythonhosted.org/packages/87/62/8be55e605d378a154037b9ba484e00a5478e627b69c53d0f63e3ef413ba6/watchdog-4.0.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f7c739888c20f99824f7aa9d31ac8a97353e22d0c0e54703a547a218f6637eb3", size = 92255 }, - { url = "https://files.pythonhosted.org/packages/6b/59/12e03e675d28f450bade6da6bc79ad6616080b317c472b9ae688d2495a03/watchdog-4.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c100d09ac72a8a08ddbf0629ddfa0b8ee41740f9051429baa8e31bb903ad7508", size = 91682 }, - { url = "https://files.pythonhosted.org/packages/ef/69/241998de9b8e024f5c2fbdf4324ea628b4231925305011ca8b7e1c3329f6/watchdog-4.0.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f5315a8c8dd6dd9425b974515081fc0aadca1d1d61e078d2246509fd756141ee", size = 92249 }, - { url = "https://files.pythonhosted.org/packages/70/3f/2173b4d9581bc9b5df4d7f2041b6c58b5e5448407856f68d4be9981000d0/watchdog-4.0.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2d468028a77b42cc685ed694a7a550a8d1771bb05193ba7b24006b8241a571a1", size = 91773 }, - { url = "https://files.pythonhosted.org/packages/f0/de/6fff29161d5789048f06ef24d94d3ddcc25795f347202b7ea503c3356acb/watchdog-4.0.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f15edcae3830ff20e55d1f4e743e92970c847bcddc8b7509bcd172aa04de506e", size = 92250 }, - { url = "https://files.pythonhosted.org/packages/8a/b1/25acf6767af6f7e44e0086309825bd8c098e301eed5868dc5350642124b9/watchdog-4.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:936acba76d636f70db8f3c66e76aa6cb5136a936fc2a5088b9ce1c7a3508fc83", size = 82947 }, - { url = "https://files.pythonhosted.org/packages/e8/90/aebac95d6f954bd4901f5d46dcd83d68e682bfd21798fd125a95ae1c9dbf/watchdog-4.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:e252f8ca942a870f38cf785aef420285431311652d871409a64e2a0a52a2174c", size = 82942 }, - { url = "https://files.pythonhosted.org/packages/15/3a/a4bd8f3b9381824995787488b9282aff1ed4667e1110f31a87b871ea851c/watchdog-4.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:0e83619a2d5d436a7e58a1aea957a3c1ccbf9782c43c0b4fed80580e5e4acd1a", size = 82947 }, - { url = "https://files.pythonhosted.org/packages/09/cc/238998fc08e292a4a18a852ed8274159019ee7a66be14441325bcd811dfd/watchdog-4.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:88456d65f207b39f1981bf772e473799fcdc10801062c36fd5ad9f9d1d463a73", size = 82946 }, - { url = "https://files.pythonhosted.org/packages/80/f1/d4b915160c9d677174aa5fae4537ae1f5acb23b3745ab0873071ef671f0a/watchdog-4.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:32be97f3b75693a93c683787a87a0dc8db98bb84701539954eef991fb35f5fbc", size = 82947 }, - { url = "https://files.pythonhosted.org/packages/db/02/56ebe2cf33b352fe3309588eb03f020d4d1c061563d9858a9216ba004259/watchdog-4.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:c82253cfc9be68e3e49282831afad2c1f6593af80c0daf1287f6a92657986757", size = 82944 }, - { url = "https://files.pythonhosted.org/packages/01/d2/c8931ff840a7e5bd5dcb93f2bb2a1fd18faf8312e9f7f53ff1cf76ecc8ed/watchdog-4.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c0b14488bd336c5b1845cee83d3e631a1f8b4e9c5091ec539406e4a324f882d8", size = 82947 }, - { url = "https://files.pythonhosted.org/packages/d0/d8/cdb0c21a4a988669d7c210c75c6a2c9a0e16a3b08d9f7e633df0d9a16ad8/watchdog-4.0.2-py3-none-win32.whl", hash = "sha256:0d8a7e523ef03757a5aa29f591437d64d0d894635f8a50f370fe37f913ce4e19", size = 82935 }, - { url = "https://files.pythonhosted.org/packages/99/2e/b69dfaae7a83ea64ce36538cc103a3065e12c447963797793d5c0a1d5130/watchdog-4.0.2-py3-none-win_amd64.whl", hash = "sha256:c344453ef3bf875a535b0488e3ad28e341adbd5a9ffb0f7d62cefacc8824ef2b", size = 82934 }, - { url = "https://files.pythonhosted.org/packages/b0/0b/43b96a9ecdd65ff5545b1b13b687ca486da5c6249475b1a45f24d63a1858/watchdog-4.0.2-py3-none-win_ia64.whl", hash = "sha256:baececaa8edff42cd16558a639a9b0ddf425f93d892e8392a56bf904f5eff22c", size = 82933 }, +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/38/764baaa25eb5e35c9a043d4c4588f9836edfe52a708950f4b6d5f714fd42/watchdog-4.0.2.tar.gz", hash = "sha256:b4dfbb6c49221be4535623ea4474a4d6ee0a9cef4a80b20c28db4d858b64e270", size = 126587, upload_time = "2024-08-11T07:38:01.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/b0/219893d41c16d74d0793363bf86df07d50357b81f64bba4cb94fe76e7af4/watchdog-4.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ede7f010f2239b97cc79e6cb3c249e72962404ae3865860855d5cbe708b0fd22", size = 100257, upload_time = "2024-08-11T07:37:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c6/8e90c65693e87d98310b2e1e5fd7e313266990853b489e85ce8396cc26e3/watchdog-4.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2cffa171445b0efa0726c561eca9a27d00a1f2b83846dbd5a4f639c4f8ca8e1", size = 92249, upload_time = "2024-08-11T07:37:06.364Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cd/2e306756364a934532ff8388d90eb2dc8bb21fe575cd2b33d791ce05a02f/watchdog-4.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c50f148b31b03fbadd6d0b5980e38b558046b127dc483e5e4505fcef250f9503", size = 92888, upload_time = "2024-08-11T07:37:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/de/78/027ad372d62f97642349a16015394a7680530460b1c70c368c506cb60c09/watchdog-4.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c7d4bf585ad501c5f6c980e7be9c4f15604c7cc150e942d82083b31a7548930", size = 100256, upload_time = "2024-08-11T07:37:11.017Z" }, + { url = "https://files.pythonhosted.org/packages/59/a9/412b808568c1814d693b4ff1cec0055dc791780b9dc947807978fab86bc1/watchdog-4.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:914285126ad0b6eb2258bbbcb7b288d9dfd655ae88fa28945be05a7b475a800b", size = 92252, upload_time = "2024-08-11T07:37:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/04/57/179d76076cff264982bc335dd4c7da6d636bd3e9860bbc896a665c3447b6/watchdog-4.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:984306dc4720da5498b16fc037b36ac443816125a3705dfde4fd90652d8028ef", size = 92888, upload_time = "2024-08-11T07:37:15.077Z" }, + { url = "https://files.pythonhosted.org/packages/92/f5/ea22b095340545faea37ad9a42353b265ca751f543da3fb43f5d00cdcd21/watchdog-4.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1cdcfd8142f604630deef34722d695fb455d04ab7cfe9963055df1fc69e6727a", size = 100342, upload_time = "2024-08-11T07:37:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d2/8ce97dff5e465db1222951434e3115189ae54a9863aef99c6987890cc9ef/watchdog-4.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7ab624ff2f663f98cd03c8b7eedc09375a911794dfea6bf2a359fcc266bff29", size = 92306, upload_time = "2024-08-11T07:37:17.997Z" }, + { url = "https://files.pythonhosted.org/packages/49/c4/1aeba2c31b25f79b03b15918155bc8c0b08101054fc727900f1a577d0d54/watchdog-4.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:132937547a716027bd5714383dfc40dc66c26769f1ce8a72a859d6a48f371f3a", size = 92915, upload_time = "2024-08-11T07:37:19.967Z" }, + { url = "https://files.pythonhosted.org/packages/79/63/eb8994a182672c042d85a33507475c50c2ee930577524dd97aea05251527/watchdog-4.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd67c7df93eb58f360c43802acc945fa8da70c675b6fa37a241e17ca698ca49b", size = 100343, upload_time = "2024-08-11T07:37:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/ce/82/027c0c65c2245769580605bcd20a1dc7dfd6c6683c8c4e2ef43920e38d27/watchdog-4.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcfd02377be80ef3b6bc4ce481ef3959640458d6feaae0bd43dd90a43da90a7d", size = 92313, upload_time = "2024-08-11T07:37:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/2a/89/ad4715cbbd3440cb0d336b78970aba243a33a24b1a79d66f8d16b4590d6a/watchdog-4.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:980b71510f59c884d684b3663d46e7a14b457c9611c481e5cef08f4dd022eed7", size = 92919, upload_time = "2024-08-11T07:37:24.715Z" }, + { url = "https://files.pythonhosted.org/packages/55/08/1a9086a3380e8828f65b0c835b86baf29ebb85e5e94a2811a2eb4f889cfd/watchdog-4.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:aa160781cafff2719b663c8a506156e9289d111d80f3387cf3af49cedee1f040", size = 100255, upload_time = "2024-08-11T07:37:26.862Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3e/064974628cf305831f3f78264800bd03b3358ec181e3e9380a36ff156b93/watchdog-4.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f6ee8dedd255087bc7fe82adf046f0b75479b989185fb0bdf9a98b612170eac7", size = 92257, upload_time = "2024-08-11T07:37:28.253Z" }, + { url = "https://files.pythonhosted.org/packages/23/69/1d2ad9c12d93bc1e445baa40db46bc74757f3ffc3a3be592ba8dbc51b6e5/watchdog-4.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0b4359067d30d5b864e09c8597b112fe0a0a59321a0f331498b013fb097406b4", size = 92886, upload_time = "2024-08-11T07:37:29.52Z" }, + { url = "https://files.pythonhosted.org/packages/68/eb/34d3173eceab490d4d1815ba9a821e10abe1da7a7264a224e30689b1450c/watchdog-4.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:770eef5372f146997638d737c9a3c597a3b41037cfbc5c41538fc27c09c3a3f9", size = 100254, upload_time = "2024-08-11T07:37:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/18/a1/4bbafe7ace414904c2cc9bd93e472133e8ec11eab0b4625017f0e34caad8/watchdog-4.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eeea812f38536a0aa859972d50c76e37f4456474b02bd93674d1947cf1e39578", size = 92249, upload_time = "2024-08-11T07:37:32.193Z" }, + { url = "https://files.pythonhosted.org/packages/f3/11/ec5684e0ca692950826af0de862e5db167523c30c9cbf9b3f4ce7ec9cc05/watchdog-4.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b2c45f6e1e57ebb4687690c05bc3a2c1fb6ab260550c4290b8abb1335e0fd08b", size = 92891, upload_time = "2024-08-11T07:37:34.212Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9a/6f30f023324de7bad8a3eb02b0afb06bd0726003a3550e9964321315df5a/watchdog-4.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10b6683df70d340ac3279eff0b2766813f00f35a1d37515d2c99959ada8f05fa", size = 91775, upload_time = "2024-08-11T07:37:35.567Z" }, + { url = "https://files.pythonhosted.org/packages/87/62/8be55e605d378a154037b9ba484e00a5478e627b69c53d0f63e3ef413ba6/watchdog-4.0.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f7c739888c20f99824f7aa9d31ac8a97353e22d0c0e54703a547a218f6637eb3", size = 92255, upload_time = "2024-08-11T07:37:37.596Z" }, + { url = "https://files.pythonhosted.org/packages/6b/59/12e03e675d28f450bade6da6bc79ad6616080b317c472b9ae688d2495a03/watchdog-4.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c100d09ac72a8a08ddbf0629ddfa0b8ee41740f9051429baa8e31bb903ad7508", size = 91682, upload_time = "2024-08-11T07:37:38.901Z" }, + { url = "https://files.pythonhosted.org/packages/ef/69/241998de9b8e024f5c2fbdf4324ea628b4231925305011ca8b7e1c3329f6/watchdog-4.0.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f5315a8c8dd6dd9425b974515081fc0aadca1d1d61e078d2246509fd756141ee", size = 92249, upload_time = "2024-08-11T07:37:40.143Z" }, + { url = "https://files.pythonhosted.org/packages/70/3f/2173b4d9581bc9b5df4d7f2041b6c58b5e5448407856f68d4be9981000d0/watchdog-4.0.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2d468028a77b42cc685ed694a7a550a8d1771bb05193ba7b24006b8241a571a1", size = 91773, upload_time = "2024-08-11T07:37:42.095Z" }, + { url = "https://files.pythonhosted.org/packages/f0/de/6fff29161d5789048f06ef24d94d3ddcc25795f347202b7ea503c3356acb/watchdog-4.0.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f15edcae3830ff20e55d1f4e743e92970c847bcddc8b7509bcd172aa04de506e", size = 92250, upload_time = "2024-08-11T07:37:44.052Z" }, + { url = "https://files.pythonhosted.org/packages/8a/b1/25acf6767af6f7e44e0086309825bd8c098e301eed5868dc5350642124b9/watchdog-4.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:936acba76d636f70db8f3c66e76aa6cb5136a936fc2a5088b9ce1c7a3508fc83", size = 82947, upload_time = "2024-08-11T07:37:45.388Z" }, + { url = "https://files.pythonhosted.org/packages/e8/90/aebac95d6f954bd4901f5d46dcd83d68e682bfd21798fd125a95ae1c9dbf/watchdog-4.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:e252f8ca942a870f38cf785aef420285431311652d871409a64e2a0a52a2174c", size = 82942, upload_time = "2024-08-11T07:37:46.722Z" }, + { url = "https://files.pythonhosted.org/packages/15/3a/a4bd8f3b9381824995787488b9282aff1ed4667e1110f31a87b871ea851c/watchdog-4.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:0e83619a2d5d436a7e58a1aea957a3c1ccbf9782c43c0b4fed80580e5e4acd1a", size = 82947, upload_time = "2024-08-11T07:37:48.941Z" }, + { url = "https://files.pythonhosted.org/packages/09/cc/238998fc08e292a4a18a852ed8274159019ee7a66be14441325bcd811dfd/watchdog-4.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:88456d65f207b39f1981bf772e473799fcdc10801062c36fd5ad9f9d1d463a73", size = 82946, upload_time = "2024-08-11T07:37:50.279Z" }, + { url = "https://files.pythonhosted.org/packages/80/f1/d4b915160c9d677174aa5fae4537ae1f5acb23b3745ab0873071ef671f0a/watchdog-4.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:32be97f3b75693a93c683787a87a0dc8db98bb84701539954eef991fb35f5fbc", size = 82947, upload_time = "2024-08-11T07:37:51.55Z" }, + { url = "https://files.pythonhosted.org/packages/db/02/56ebe2cf33b352fe3309588eb03f020d4d1c061563d9858a9216ba004259/watchdog-4.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:c82253cfc9be68e3e49282831afad2c1f6593af80c0daf1287f6a92657986757", size = 82944, upload_time = "2024-08-11T07:37:52.855Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/c8931ff840a7e5bd5dcb93f2bb2a1fd18faf8312e9f7f53ff1cf76ecc8ed/watchdog-4.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c0b14488bd336c5b1845cee83d3e631a1f8b4e9c5091ec539406e4a324f882d8", size = 82947, upload_time = "2024-08-11T07:37:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d8/cdb0c21a4a988669d7c210c75c6a2c9a0e16a3b08d9f7e633df0d9a16ad8/watchdog-4.0.2-py3-none-win32.whl", hash = "sha256:0d8a7e523ef03757a5aa29f591437d64d0d894635f8a50f370fe37f913ce4e19", size = 82935, upload_time = "2024-08-11T07:37:56.668Z" }, + { url = "https://files.pythonhosted.org/packages/99/2e/b69dfaae7a83ea64ce36538cc103a3065e12c447963797793d5c0a1d5130/watchdog-4.0.2-py3-none-win_amd64.whl", hash = "sha256:c344453ef3bf875a535b0488e3ad28e341adbd5a9ffb0f7d62cefacc8824ef2b", size = 82934, upload_time = "2024-08-11T07:37:57.991Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0b/43b96a9ecdd65ff5545b1b13b687ca486da5c6249475b1a45f24d63a1858/watchdog-4.0.2-py3-none-win_ia64.whl", hash = "sha256:baececaa8edff42cd16558a639a9b0ddf425f93d892e8392a56bf904f5eff22c", size = 82933, upload_time = "2024-08-11T07:37:59.573Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload_time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload_time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload_time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload_time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload_time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload_time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload_time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload_time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload_time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload_time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload_time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload_time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload_time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390, upload_time = "2024-11-01T14:06:49.325Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386, upload_time = "2024-11-01T14:06:50.536Z" }, + { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017, upload_time = "2024-11-01T14:06:51.717Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload_time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload_time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903, upload_time = "2024-11-01T14:06:57.052Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381, upload_time = "2024-11-01T14:06:58.193Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload_time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload_time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload_time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload_time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload_time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload_time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload_time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload_time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload_time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload_time = "2024-11-01T14:07:11.845Z" }, ] [[package]] name = "wheel" -version = "0.44.0" +version = "0.45.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/a0/95e9e962c5fd9da11c1e28aa4c0d8210ab277b1ada951d2aee336b505813/wheel-0.44.0.tar.gz", hash = "sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49", size = 100733 } +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload_time = "2024-11-23T00:18:23.513Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d1/9babe2ccaecff775992753d8686970b1e2755d21c8a63be73aba7a4e7d77/wheel-0.44.0-py3-none-any.whl", hash = "sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f", size = 67059 }, + { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload_time = "2024-11-23T00:18:21.207Z" }, ] [[package]] name = "zipp" version = "3.20.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199 } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199, upload_time = "2024-09-13T13:44:16.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200, upload_time = "2024-09-13T13:44:14.38Z" }, +] + +[[package]] +name = "zipp" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545, upload_time = "2024-11-10T15:05:20.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200 }, + { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630, upload_time = "2024-11-10T15:05:19.275Z" }, ] From 6113e750971918a51f79c3bb2585e95ed1c53245 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 17:49:12 +0200 Subject: [PATCH 94/94] Bump the github-actions group across 1 directory with 3 updates (#210) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 4 ++-- .github/workflows/main.yml | 4 ++-- .github/workflows/publish.yml | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d53c7ca..018e84b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -12,7 +12,7 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Configure Git Credentials run: | @@ -20,7 +20,7 @@ jobs: git config user.email 41898282+github-actions[bot]@users.noreply.github.com - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v6 with: version: "0.4.12" enable-cache: true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 14f485e..2b0661c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,10 +13,10 @@ jobs: matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v6 with: python-version: ${{ matrix.python-version }} enable-cache: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 310b67b..31f0431 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,10 +11,10 @@ jobs: outputs: version: ${{ steps.inspect_package.outputs.version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v6 with: version: "0.4.12" enable-cache: true @@ -50,7 +50,7 @@ jobs: steps: - name: Download package - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: package-distributions path: dist/