diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..12761c0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: uv + directory: "/" + schedule: + interval: daily + time: "14:00" + open-pull-requests-limit: 10 + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: daily + time: "14:00" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 69fc41e..197b122 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,31 +2,39 @@ name: "Code scanning - action" on: push: + branches-ignore: + - 'dependabot/**' pull_request: schedule: - cron: '0 11 * * 2' +permissions: {} + jobs: CodeQL-Build: runs-on: ubuntu-latest + permissions: + security-events: write + steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v5 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. fetch-depth: 2 + persist-credentials: false # If this run was triggered by a pull request event, then checkout # the head of the pull request instead of the merge commit. - run: git checkout HEAD^2 if: ${{ github.event_name == 'pull_request' }} - + # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v3 # Override language selection by uncommenting this and choosing your languages # with: # languages: go, javascript, csharp, python, cpp, java @@ -34,7 +42,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -48,4 +56,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..02fd5d2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,50 @@ +name: Build and upload to PyPI + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + release: + types: + - published + +permissions: {} + +jobs: + build: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + submodules: true + persist-credentials: false + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # 6.5.0 + + - name: Build + run: uv build + + - uses: actions/upload-artifact@v4 + with: + path: | + dist/*.tar.gz + dist/*.whl + + upload_pypi: + needs: build + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write + if: github.event_name == 'release' && github.event.action == 'published' + steps: + - uses: actions/download-artifact@v5 + with: + name: artifact + path: dist + + - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # 1.12.4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5392759 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,39 @@ +name: Python tests + +on: + push: + pull_request: + schedule: + - cron: '3 15 * * SUN' + +permissions: {} + +jobs: + test: + name: test with ${{ matrix.env }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + env: [3.9, "3.10", 3.11, 3.12, 3.13] + os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v5 + with: + submodules: true + persist-credentials: false + - name: Install the latest version of uv + uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # 6.5.0 + - name: Install tox + run: uv tool install --python-preference only-managed --python 3.13 tox --with tox-uv --with tox-gh + - name: Install Python + if: matrix.env != '3.13' + run: uv python install --python-preference only-managed ${{ matrix.env }} + - name: Setup test suite + run: tox run -vv --notest --skip-missing-interpreters false + env: + TOX_GH_MAJOR_MINOR: ${{ matrix.env }} + - name: Run test suite + run: tox run --skip-pkg-install + env: + TOX_GH_MAJOR_MINOR: ${{ matrix.env }} diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..c0857e8 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,34 @@ +name: GitHub Actions Security Analysis with zizmor + +on: + push: + branches: ["main"] + pull_request: + branches: ["**"] + +permissions: {} + +jobs: + zizmor: + name: zizmor latest via PyPI + runs-on: ubuntu-latest + permissions: + security-events: write + # required for workflows in private repositories + contents: read + actions: read + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@d9e0f98d3fc6adb07d1e3d37f3043649ddad06a1 # 6.5.0 + with: + enable-cache: false + + - name: Run zizmor + run: uvx zizmor@1.11.0 --format plain . + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 3e8afcd..c1ac9a6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,8 +13,8 @@ geoip2.egg-info/ MANIFEST .mypy_cache/ *.pyc -pylint.txt .pyre .pytype *.swp -violations.pyflakes.txt +.tox +/venv diff --git a/.gitmodules b/.gitmodules index 9cf24ec..e8246ba 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "tests/data"] path = tests/data - url = git://github.com/maxmind/MaxMind-DB.git + url = https://github.com/maxmind/MaxMind-DB diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index c05420c..0000000 --- a/.pylintrc +++ /dev/null @@ -1,2 +0,0 @@ -[MESSAGES CONTROL] -disable=R0205 diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..569cb1f --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,14 @@ +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.13" + +sphinx: + configuration: docs/conf.py + +python: + install: + - method: pip + path: . diff --git a/.travis-black.sh b/.travis-black.sh deleted file mode 100755 index a3d2475..0000000 --- a/.travis-black.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -set -eux - -diff=$(black --check .) - -if [[ $? != 0 ]]; then - echo "black failed to run." - echo "$diff" - exit $? -elif [[ $diff ]]; then - echo "$diff" - exit 1 -else - exit 0 -fi diff --git a/.travis-pylint.sh b/.travis-pylint.sh deleted file mode 100755 index 7c6d8a9..0000000 --- a/.travis-pylint.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -set -eux - -python setup.py install -pylint geoip2 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4cbacfa..0000000 --- a/.travis.yml +++ /dev/null @@ -1,54 +0,0 @@ ---- -language: python -matrix: - include: - - python: 3.6 - - python: 3.7 - - python: 3.8 - env: - - RUN_SNYK=1 - - RUN_LINTER=1 - - python: nightly - allow_failures: - - python: nightly - -before_install: - - git submodule update --init --recursive - - git clone --recursive git://github.com/maxmind/libmaxminddb - - cd libmaxminddb - - ./bootstrap - - ./configure - - make - - sudo make install - - sudo ldconfig - - cd .. - - "if [[ $RUN_SNYK && $SNYK_TOKEN ]]; then sudo apt-get install -y nodejs; npm install -g snyk; fi" -install: - - pip install -r requirements.txt - - pip install mocket coveralls - - | - if [[ $RUN_LINTER ]]; then - pip install --upgrade pylint black mypy - fi - - "if [[ $RUN_SNYK && $SNYK_TOKEN ]]; then snyk test --org=maxmind; fi" -script: - - coverage run --source=geoip2 setup.py test - - "if [[ $RUN_LINTER ]]; then mypy geoip2 tests; fi" - - "if [[ $RUN_LINTER ]]; then ./.travis-pylint.sh; fi" - - "if [[ $RUN_LINTER ]]; then ./.travis-black.sh; fi" -after_success: - - coveralls - - "if [[ $RUN_SNYK && $SNYK_TOKEN && $TRAVIS_BRANCH == 'master' && $TRAVIS_PULL_REQUEST == 'false' ]]; then snyk monitor --org=maxmind --project-name=maxmind/GeoIP2-python; fi" -notifications: - email: - on_failure: always - on_success: change - recipients: - - dev-ci@maxmind.com - slack: - rooms: - secure: "FZLn7HZnM6GOtIU7+W6tY6zd2y0K4C1ZgIOAiGscWvJydn+Xr63GCT21EPPe9QCulQHz88CWALMD7ZrDdsh6nV0gx0IeE+wq4w7NZYjLokvxs+UMYwuwl57MMrqd7e4byZDC4BLvTQTCvpOHOd8dCHwDXmuBcuyE8zB+Wkndn0I=\n" - -env: - global: - secure: "YUqALSGB89cWyKNiBKIZDxdWPl3xjYjkWx2OP1i53W5qcaKgOWco2jv+2iXTPAk3c5x3Py3J7B1SdiNcRWmjfKqrnE47fZYkiP8rk0c/pVMRw0zURpmtRzSReDF0Sh/1ZTODi2rsU52Ei1f++BTEB5m409RMnTEvQtH7XT2sVDk=" diff --git a/HISTORY.rst b/HISTORY.rst index bc2e278..6d3c976 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,8 +1,116 @@ + .. :changelog: History ------- +5.2.0 +++++++++++++++++++ + +* Setuptools has been replaced with the uv build backend for building the + package. + +5.1.0 (2025-05-05) +++++++++++++++++++ + +* Support for the GeoIP Anonymous Plus database has been added. To do a lookup + in this database, use the ``anonymous_plus`` method on ``Reader``. +* Reorganized module documentation to improve language-server support. + +5.0.1 (2025-01-28) +++++++++++++++++++ + +* Allow ``ip_address`` in the ``Traits`` record to be ``None`` again. The + primary use case for this is from the ``minfraud`` package. + +5.0.0 (2025-01-28) +++++++++++++++++++ + +* BREAKING: The ``raw`` attribute on the model classes has been replaced + with a ``to_dict()`` method. This can be used to get a representation of + the object that is suitable for serialization. +* BREAKING: The ``ip_address`` property on the model classes now always returns + a ``ipaddress.IPv4Address`` or ``ipaddress.IPv6Address``. +* BREAKING: The model and record classes now require all arguments other than + ``locales`` and ``ip_address`` to be keyword arguments. +* BREAKING: ``geoip2.mixins`` has been made internal. This normally would not + have been used by external code. +* IMPORTANT: Python 3.9 or greater is required. If you are using an older + version, please use an earlier release. +* ``metro_code`` on ``geoip2.record.Location`` has been deprecated. The + code values are no longer being maintained. +* The type hinting for the optional ``locales`` keyword argument now allows + any sequence of strings rather than only list of strings. + +4.8.1 (2024-11-18) +++++++++++++++++++ + +* ``setuptools`` was incorrectly listed as a runtime dependency. This has + been removed. Pull request by Mathieu Dupuy. GitHub #174. + +4.8.0 (2023-12-05) +++++++++++++++++++ + +* IMPORTANT: Python 3.8 or greater is required. If you are using an older + version, please use an earlier release. +* The ``is_anycast`` attribute was added to ``geoip2.record.Traits``. + This returns ``True`` if the IP address belongs to an + `anycast network `_. + This is available for the GeoIP2 Country, City Plus, and Insights web services + and the GeoIP2 Country, City, and Enterprise databases. + +4.7.0 (2023-05-09) +++++++++++++++++++ + +* IMPORTANT: Python 3.7 or greater is required. If you are using an older + version, please use an earlier release. + +4.6.0 (2022-06-21) +++++++++++++++++++ + +* The ``AddressNotFoundError`` class now has an ``ip_address`` attribute + with the lookup address and ``network`` property for the empty network + in the database containing the IP address. These are only available + when using a database, not the web service. Pull request by illes. + GitHub #130. + +4.5.0 (2021-11-18) +++++++++++++++++++ + +* Support for mobile country code (MCC) and mobile network codes (MNC) was + added for the GeoIP2 ISP and Enterprise databases as well as the GeoIP2 + City and Insights web services. ``mobile_country_code`` and + ``mobile_network_code`` attributes were added to ``geoip2.model.ISP`` + for the GeoIP2 ISP database and ``geoip2.record.Traits`` for the + Enterprise database and the GeoIP2 City and Insights web services. + We expect this data to be available by late January, 2022. + +4.4.0 (2021-09-24) +++++++++++++++++++ + +* The public API on ``geoip2.database`` is now explicitly defined by + setting ``__all__``. +* The return type of the ``metadata()`` method on ``Reader`` is now + ``maxminddb.reader.Metadata`` rather than a union type. + +4.3.0 (2021-09-20) +++++++++++++++++++ + +* Previously, the ``py.typed`` file was not being added to the source + distribution. It is now explicitly specified in the manifest. +* The type hints for the database file in the ``Reader`` constructor have + been expanded to match those specified by ``maxmindb.open_database``. In + particular, ``os.PathLike`` and ``IO`` have been added. +* Corrected the type hint for the ``metadata()`` method on ``Reader``. It + will return a ``maxminddb.extension.Metadata`` if the C extension is being + used. + +4.2.0 (2021-05-12) +++++++++++++++++++ + +* You may now set a proxy to use when making web service requests by passing + the ``proxy`` parameter to the ``AsyncClient`` or ``Client`` constructor. + 4.1.0 (2020-09-25) ++++++++++++++++++ diff --git a/LICENSE b/LICENSE index d645695..62589ed 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -193,7 +193,7 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/MANIFEST.in b/MANIFEST.in index 60f66ef..66870aa 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ -include HISTORY.rst README.rst LICENSE requirements.txt tests/*.py tests/data/test-data/*.mmdb +exclude .* .github/**/* dev-bin/* +include HISTORY.rst README.rst LICENSE geoip2/py.typed tests/*.py tests/data/test-data/*.mmdb graft docs/html diff --git a/README.rst b/README.rst index 80d4dd1..c4f3a2f 100644 --- a/README.rst +++ b/README.rst @@ -5,11 +5,9 @@ MaxMind GeoIP2 Python API Description ----------- -This package provides an API for the GeoIP2 `web services -`_ and `databases -`_. The API also works with -MaxMind's free `GeoLite2 databases -`_. +This package provides an API for the GeoIP2 and GeoLite2 `web services +`_ and `databases +`_. Installation ------------ @@ -20,12 +18,12 @@ To install the ``geoip2`` module, type: $ pip install geoip2 -If you are not able to use pip, you may also use easy_install from the +If you are not able to install from PyPI, you may also use ``pip`` from the source directory: .. code-block:: bash - $ easy_install . + $ python -m pip install . Database Reader Extension ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -33,7 +31,7 @@ Database Reader Extension If you wish to use the C extension for the database reader, you must first install the `libmaxminddb C API `_. Please `see the instructions distributed with it -`_. +`_. IP Geolocation Usage -------------------- @@ -42,17 +40,22 @@ IP geolocation is inherently imprecise. Locations are often near the center of the population. Any location provided by a GeoIP2 database or web service should not be used to identify a particular address or household. -Usage ------ +Web Service Usage +----------------- -To use this API, you first create either a web service object with your -MaxMind ``account_id`` and ``license_key`` or a database reader object with the -path to your database file. After doing this, you may call the method -corresponding to request type (e.g., ``city`` or ``country``), passing it the -IP address you want to look up. +To use this API, you first construct either a ``geoip2.webservice.Client`` or +``geoip2.webservice.AsyncClient``, passing your MaxMind ``account_id`` and +``license_key`` to the constructor. To use the GeoLite2 web service instead of +the GeoIP2 web service, set the optional ``host`` keyword argument to +``geolite.info``. To use the Sandbox GeoIP2 web service instead of the +production GeoIP2 web service, set the optional ``host`` keyword argument to +``sandbox.maxmind.com``. + +After doing this, you may call the method corresponding to request type +(e.g., ``city`` or ``country``), passing it the IP address you want to look up. If the request succeeds, the method call will return a model class for the -end point you called. This model in turn contains multiple record classes, +endpoint you called. This model in turn contains multiple record classes, each of which represents part of the data returned by the web service. If the request fails, the client class throws an exception. @@ -66,12 +69,16 @@ Sync Web Service Example >>> >>> # This creates a Client object that can be reused across requests. >>> # Replace "42" with your account ID and "license_key" with your license - >>> # key. + >>> # key. Set the "host" keyword argument to "geolite.info" to use the + >>> # GeoLite2 web service instead of the GeoIP2 web service. Set the + >>> # "host" keyword argument to "sandbox.maxmind.com" to use the Sandbox + >>> # GeoIP2 web service instead of the production GeoIP2 web service. >>> with geoip2.webservice.Client(42, 'license_key') as client: >>> - >>> # Replace "insights" with the method corresponding to the web service - >>> # that you are using, e.g., "country", "city". - >>> response = client.insights('203.0.113.0') + >>> # Replace "city" with the method corresponding to the web service + >>> # that you are using, i.e., "country", "city", or "insights". Please + >>> # note that Insights is not supported by the GeoLite2 web service. + >>> response = client.city('203.0.113.0') >>> >>> response.country.iso_code 'US' @@ -104,51 +111,60 @@ Async Web Service Example .. code-block:: pycon - >>> import geoip2.webservice - >>> - >>> # This creates an AsyncClient object that can be reused across - >>> # requests on the running event loop. If you are using multiple event - >>> # loops, you must ensure the object is not used on another loop. - >>> # - >>> # Replace "42" with your account ID and "license_key" with your license - >>> # key. - >>> async with geoip2.webservice.AsyncClient(42, 'license_key') as client: + >>> import asyncio >>> - >>> # Replace "insights" with the method corresponding to the web service - >>> # that you are using, e.g., "country", "city". - >>> response = await client.insights('203.0.113.0') + >>> import geoip2.webservice >>> - >>> response.country.iso_code + >>> async def main(): + >>> # This creates an AsyncClient object that can be reused across + >>> # requests on the running event loop. If you are using multiple event + >>> # loops, you must ensure the object is not used on another loop. + >>> # + >>> # Replace "42" with your account ID and "license_key" with your license + >>> # key. Set the "host" keyword argument to "geolite.info" to use the + >>> # GeoLite2 web service instead of the GeoIP2 web service. Set the + >>> # "host" keyword argument to "sandbox.maxmind.com" to use the Sandbox + >>> # GeoIP2 web service instead of the production GeoIP2 web service. + >>> async with geoip2.webservice.AsyncClient(42, 'license_key') as client: + >>> + >>> # Replace "city" with the method corresponding to the web service + >>> # that you are using, i.e., "country", "city", or "insights". Please + >>> # note that Insights is not supported by the GeoLite2 web service. + >>> response = await client.city('203.0.113.0') + >>> + >>> response.country.iso_code 'US' - >>> response.country.name + >>> response.country.name 'United States' - >>> response.country.names['zh-CN'] + >>> response.country.names['zh-CN'] u'美国' >>> - >>> response.subdivisions.most_specific.name + >>> response.subdivisions.most_specific.name 'Minnesota' - >>> response.subdivisions.most_specific.iso_code + >>> response.subdivisions.most_specific.iso_code 'MN' >>> - >>> response.city.name + >>> response.city.name 'Minneapolis' >>> - >>> response.postal.code + >>> response.postal.code '55455' >>> - >>> response.location.latitude + >>> response.location.latitude 44.9733 - >>> response.location.longitude + >>> response.location.longitude -93.2323 >>> - >>> response.traits.network + >>> response.traits.network IPv4Network('203.0.113.0/32') + >>> + >>> asyncio.run(main()) Web Service Client Exceptions ----------------------------- For details on the possible errors returned by the web service itself, see -http://dev.maxmind.com/geoip/geoip2/web-services for the GeoIP2 Precision web +https://dev.maxmind.com/geoip/docs/web-services?lang=en for the GeoIP2 web service docs. If the web service returns an explicit error document, this is thrown as a @@ -164,8 +180,22 @@ returns any status code besides 200, 4xx, or 5xx, this also becomes an Finally, if the web service returns a 200 but the body is invalid, the client throws a ``GeoIP2Error``. +Database Usage +-------------- + +To use the database API, you first construct a ``geoip2.database.Reader`` using +the path to the file as the first argument. After doing this, you may call the +method corresponding to database type (e.g., ``city`` or ``country``), passing it +the IP address you want to look up. + +If the lookup succeeds, the method call will return a model class for the +database method you called. This model in turn contains multiple record classes, +each of which represents part of the data for the record. + +If the request fails, the reader class throws an exception. + Database Example -------------------- +---------------- City Database ^^^^^^^^^^^^^ @@ -238,6 +268,42 @@ Anonymous IP Database >>> response.network IPv4Network('203.0.113.0/24') +Anonymous Plus Database +^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: pycon + + >>> import geoip2.database + >>> + >>> # This creates a Reader object. You should use the same object + >>> # across multiple requests as creation of it is expensive. + >>> with geoip2.database.Reader('/path/to/GeoIP-Anonymous-Plus.mmdb') as reader: + >>> + >>> response = reader.anonymous_plus('203.0.113.0') + >>> + >>> response.anonymizer_confidence + 30 + >>> response.is_anonymous + True + >>> response.is_anonymous_vpn + True + >>> response.is_hosting_provider + False + >>> response.is_public_proxy + False + >>> response.is_residential_proxy + False + >>> response.is_tor_exit_node + False + >>> response.ip_address + '203.0.113.0' + >>> response.network + IPv4Network('203.0.113.0/24') + >>> response.network_last_seen + datetime.date(2025, 4, 18) + >>> response.provider_name + FooBar VPNs + ASN Database ^^^^^^^^^^^^ @@ -370,11 +436,36 @@ Database Reader Exceptions -------------------------- If the database file does not exist or is not readable, the constructor will -raise a ``FileNotFoundError``. If the IP address passed to a method is -invalid, a ``ValueError`` will be raised. If the file is invalid or there is a -bug in the reader, a ``maxminddb.InvalidDatabaseError`` will be raised with a -description of the problem. If an IP address is not in the database, a -``AddressNotFoundError`` will be raised. +raise a ``FileNotFoundError`` or a ``PermissionError``. If the IP address passed +to a method is invalid, a ``ValueError`` will be raised. If the file is invalid +or there is a bug in the reader, a ``maxminddb.InvalidDatabaseError`` will be +raised with a description of the problem. If an IP address is not in the +database, a ``AddressNotFoundError`` will be raised. + +``AddressNotFoundError`` references the largest subnet where no address would be +found. This can be used to efficiently enumerate entire subnets: + +.. code-block:: python + + import geoip2.database + import geoip2.errors + import ipaddress + + # This creates a Reader object. You should use the same object + # across multiple requests as creation of it is expensive. + with geoip2.database.Reader('/path/to/GeoLite2-ASN.mmdb') as reader: + network = ipaddress.ip_network("192.128.0.0/15") + + ip_address = network[0] + while ip_address in network: + try: + response = reader.asn(ip_address) + response_network = response.network + except geoip2.errors.AddressNotFoundError as e: + response = None + response_network = e.network + print(f"{response_network}: {response!r}") + ip_address = response_network[-1] + 1 # move to next subnet Values to use for Database or Dictionary Keys --------------------------------------------- @@ -394,7 +485,7 @@ What data is returned? ---------------------- While many of the models contain the same basic records, the attributes which -can be populated vary between web service end points or databases. In +can be populated vary between web service endpoints or databases. In addition, while a model may offer a particular piece of data, MaxMind does not always have every piece of data for any given IP address. @@ -407,7 +498,7 @@ attribute in the ``geoip2.records.Traits`` record. Integration with GeoNames ------------------------- -`GeoNames `_ offers web services and downloadable +`GeoNames `_ offers web services and downloadable databases with data on geographical features around the world, including populated places. They offer both free and paid premium data. Each feature is uniquely identified by a ``geoname_id``, which is an integer. @@ -424,10 +515,10 @@ Reporting Data Problems ----------------------- If the problem you find is that an IP address is incorrectly mapped, please -`submit your correction to MaxMind `_. +`submit your correction to MaxMind `_. If you find some other sort of mistake, like an incorrect spelling, please -check the `GeoNames site `_ first. Once you've +check the `GeoNames site `_ first. Once you've searched for a place and found it on the GeoNames map view, there are a number of links you can use to correct data ("move", "edit", "alternate names", etc.). Once the correction is part of the GeoNames data set, it @@ -435,20 +526,20 @@ will be automatically incorporated into future MaxMind releases. If you are a paying MaxMind customer and you're not sure where to submit a correction, please `contact MaxMind support -`_ for help. +`_ for help. Requirements ------------ -Python 3.6 or greater is required. Older versions are not supported. +Python 3.9 or greater is required. Older versions are not supported. The Requests HTTP library is also required. See - for details. + for details. Versioning ---------- -The GeoIP2 Python API uses `Semantic Versioning `_. +The GeoIP2 Python API uses `Semantic Versioning `_. Support ------- @@ -458,4 +549,4 @@ Please report all issues with this code using the `GitHub issue tracker If you are having an issue with a MaxMind service that is not specific to the client API, please contact `MaxMind support -`_ for assistance. +`_ for assistance. diff --git a/dev-bin/release.sh b/dev-bin/release.sh index 2e6d770..7e6d190 100755 --- a/dev-bin/release.sh +++ b/dev-bin/release.sh @@ -21,7 +21,7 @@ version="${BASH_REMATCH[1]}" date="${BASH_REMATCH[2]}" notes="$(echo "${BASH_REMATCH[3]}" | sed -n -e '/^[0-9]\+\.[0-9]\+\.[0-9]\+/,$!p')" -if [[ "$date" != "$(date +"%Y-%m-%d")" ]]; then +if [[ "$date" != "$(date +"%Y-%m-%d")" ]]; then echo "$date is not today!" exit 1 fi @@ -33,13 +33,11 @@ if [ -n "$(git status --porcelain)" ]; then exit 1 fi -# Make sure Sphinx and wheel are installed with the current python -pip install sphinx wheel - perl -pi -e "s/(?<=__version__ = \").+?(?=\")/$version/gsm" geoip2/__init__.py +perl -pi -e "s/(?<=^version = \").+?(?=\")/$version/gsm" pyproject.toml echo $"Test results:" -python setup.py test +tox echo $'\nDiff:' git diff @@ -58,14 +56,6 @@ git commit -m "Update for $tag" -a git push -message="$version - -$notes" - -hub release create -m "$message" "$tag" +gh release create --target "$(git branch --show-current)" -t "$version" -n "$notes" "$tag" git push --tags - -rm -fr dist -python setup.py sdist bdist_wheel -twine upload dist/* diff --git a/docs/conf.py b/docs/conf.py index 650b33d..dc9d5ee 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # geoip2 documentation build configuration file, created by # sphinx-quickstart on Tue Apr 9 13:34:57 2013. @@ -12,8 +11,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys import os +import sys # 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 @@ -49,7 +48,7 @@ # General information about the project. project = "geoip2" -copyright = "2013-2020, MaxMind, Inc." +copyright = "2013-2025, MaxMind, Inc." # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -127,7 +126,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] +# html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. @@ -246,4 +245,6 @@ # texinfo_show_urls = 'footnote' # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {"http://docs.python.org/": None} +intersphinx_mapping = { + "python": ("https://python.readthedocs.org/en/latest/", None), +} diff --git a/docs/index.rst b/docs/index.rst index a1e0323..92790d6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,27 +8,55 @@ .. include:: ../README.rst -======= -Modules -======= - .. automodule:: geoip2 :members: + :inherited-members: + :show-inheritance: + +=============== +Database Reader +=============== .. automodule:: geoip2.database :members: + :inherited-members: + :show-inheritance: + +================== +WebServices Client +================== .. automodule:: geoip2.webservice :members: + :inherited-members: + :show-inheritance: + +====== +Models +====== .. automodule:: geoip2.models :members: + :inherited-members: + :show-inheritance: + +======= +Records +======= .. automodule:: geoip2.records :members: + :inherited-members: + :show-inheritance: + +====== +Errors +====== .. automodule:: geoip2.errors :members: + :inherited-members: + :show-inheritance: ================== Indices and tables @@ -38,6 +66,6 @@ Indices and tables * :ref:`modindex` * :ref:`search` -:copyright: (c) 2013-2020 by MaxMind, Inc. +:copyright: (c) 2013-2025 by MaxMind, Inc. :license: Apache License, Version 2.0 diff --git a/examples/benchmark.py b/examples/benchmark.py old mode 100644 new mode 100755 index 4a60afc..e8e478e --- a/examples/benchmark.py +++ b/examples/benchmark.py @@ -1,15 +1,16 @@ #!/usr/bin/python -# -*- coding: utf-8 -*- - -from __future__ import print_function +"""Simple benchmarking script.""" import argparse -import geoip2.database +import contextlib import random import socket import struct import timeit +import geoip2.database +import geoip2.errors + parser = argparse.ArgumentParser(description="Benchmark maxminddb.") parser.add_argument("--count", default=250000, type=int, help="number of lookups") parser.add_argument("--mode", default=0, type=int, help="reader mode to use") @@ -20,12 +21,11 @@ reader = geoip2.database.Reader(args.file, mode=args.mode) -def lookup_ip_address(): +def lookup_ip_address() -> None: + """Look up IP address.""" ip = socket.inet_ntoa(struct.pack("!L", random.getrandbits(32))) - try: - record = reader.city(str(ip)) - except geoip2.errors.AddressNotFoundError: - pass + with contextlib.suppress(geoip2.errors.AddressNotFoundError): + reader.city(str(ip)) elapsed = timeit.timeit( @@ -34,4 +34,4 @@ def lookup_ip_address(): number=args.count, ) -print(args.count / elapsed, "lookups per second") +print(args.count / elapsed, "lookups per second") # noqa: T201 diff --git a/geoip2/__init__.py b/geoip2/__init__.py deleted file mode 100644 index a80b368..0000000 --- a/geoip2/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# pylint:disable=C0111 - -__title__ = "geoip2" -__version__ = "4.1.0" -__author__ = "Gregory Oschwald" -__license__ = "Apache License, Version 2.0" -__copyright__ = "Copyright (c) 2013-2020 Maxmind, Inc." diff --git a/geoip2/errors.py b/geoip2/errors.py deleted file mode 100644 index a1f65f2..0000000 --- a/geoip2/errors.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Errors -====== - -""" - -from typing import Optional - - -class GeoIP2Error(RuntimeError): - """There was a generic error in GeoIP2. - - This class represents a generic error. It extends :py:exc:`RuntimeError` - and does not add any additional attributes. - - """ - - -class AddressNotFoundError(GeoIP2Error): - """The address you were looking up was not found.""" - - -class AuthenticationError(GeoIP2Error): - """There was a problem authenticating the request.""" - - -class HTTPError(GeoIP2Error): - """There was an error when making your HTTP request. - - This class represents an HTTP transport error. It extends - :py:exc:`GeoIP2Error` and adds attributes of its own. - - :ivar http_status: The HTTP status code returned - :ivar uri: The URI queried - :ivar decoded_content: The decoded response content - - """ - - def __init__( - self, - message: str, - http_status: Optional[int] = None, - uri: Optional[str] = None, - decoded_content: Optional[str] = None, - ) -> None: - super().__init__(message) - self.http_status = http_status - self.uri = uri - self.decoded_content = decoded_content - - -class InvalidRequestError(GeoIP2Error): - """The request was invalid.""" - - -class OutOfQueriesError(GeoIP2Error): - """Your account is out of funds for the service queried.""" - - -class PermissionRequiredError(GeoIP2Error): - """Your account does not have permission to access this service.""" diff --git a/geoip2/mixins.py b/geoip2/mixins.py deleted file mode 100644 index 2209b7b..0000000 --- a/geoip2/mixins.py +++ /dev/null @@ -1,14 +0,0 @@ -"""This package contains utility mixins""" -# pylint: disable=too-few-public-methods -from abc import ABCMeta -from typing import Any - - -class SimpleEquality(metaclass=ABCMeta): - """Naive __dict__ equality mixin""" - - def __eq__(self, other: Any) -> bool: - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not self.__eq__(other) diff --git a/geoip2/models.py b/geoip2/models.py deleted file mode 100644 index 0fe071c..0000000 --- a/geoip2/models.py +++ /dev/null @@ -1,609 +0,0 @@ -""" -Models -====== - -These classes provide models for the data returned by the GeoIP2 -web service and databases. - -The only difference between the City and Insights model classes is which -fields in each record may be populated. See -http://dev.maxmind.com/geoip/geoip2/web-services for more details. - -""" -# pylint: disable=too-many-instance-attributes,too-few-public-methods -import ipaddress -from abc import ABCMeta -from typing import Any, cast, Dict, List, Optional, Union - -import geoip2.records -from geoip2.mixins import SimpleEquality - - -class Country(SimpleEquality): - """Model for the GeoIP2 Precision: Country and the GeoIP2 Country database. - - This class provides the following attributes: - - .. attribute:: continent - - Continent object for the requested IP address. - - :type: :py:class:`geoip2.records.Continent` - - .. attribute:: country - - Country object for the requested IP address. This record represents the - country where MaxMind believes the IP is located. - - :type: :py:class:`geoip2.records.Country` - - .. attribute:: maxmind - - Information related to your MaxMind account. - - :type: :py:class:`geoip2.records.MaxMind` - - .. attribute:: registered_country - - The registered country object for the requested IP address. This record - represents the country where the ISP has registered a given IP block in - and may differ from the user's country. - - :type: :py:class:`geoip2.records.Country` - - .. attribute:: represented_country - - Object for the country represented by the users of the IP address - when that country is different than the country in ``country``. For - instance, the country represented by an overseas military base. - - :type: :py:class:`geoip2.records.RepresentedCountry` - - .. attribute:: traits - - Object with the traits of the requested IP address. - - :type: :py:class:`geoip2.records.Traits` - - """ - - continent: geoip2.records.Continent - country: geoip2.records.Country - maxmind: geoip2.records.MaxMind - registered_country: geoip2.records.Country - represented_country: geoip2.records.RepresentedCountry - traits: geoip2.records.Traits - - def __init__( - self, raw_response: Dict[str, Any], locales: Optional[List[str]] = None - ) -> None: - if locales is None: - locales = ["en"] - self._locales = locales - self.continent = geoip2.records.Continent( - locales, **raw_response.get("continent", {}) - ) - self.country = geoip2.records.Country( - locales, **raw_response.get("country", {}) - ) - self.registered_country = geoip2.records.Country( - locales, **raw_response.get("registered_country", {}) - ) - self.represented_country = geoip2.records.RepresentedCountry( - locales, **raw_response.get("represented_country", {}) - ) - - self.maxmind = geoip2.records.MaxMind(**raw_response.get("maxmind", {})) - - self.traits = geoip2.records.Traits(**raw_response.get("traits", {})) - self.raw = raw_response - - def __repr__(self) -> str: - return ( - f"{self.__module__}.{self.__class__.__name__}({self.raw}, {self._locales})" - ) - - -class City(Country): - """Model for the GeoIP2 Precision: City and the GeoIP2 City database. - - .. attribute:: city - - City object for the requested IP address. - - :type: :py:class:`geoip2.records.City` - - .. attribute:: continent - - Continent object for the requested IP address. - - :type: :py:class:`geoip2.records.Continent` - - .. attribute:: country - - Country object for the requested IP address. This record represents the - country where MaxMind believes the IP is located. - - :type: :py:class:`geoip2.records.Country` - - .. attribute:: location - - Location object for the requested IP address. - - :type: :py:class:`geoip2.records.Location` - - .. attribute:: maxmind - - Information related to your MaxMind account. - - :type: :py:class:`geoip2.records.MaxMind` - - .. attribute:: postal - - Postal object for the requested IP address. - - :type: :py:class:`geoip2.records.Postal` - - .. attribute:: registered_country - - The registered country object for the requested IP address. This record - represents the country where the ISP has registered a given IP block in - and may differ from the user's country. - - :type: :py:class:`geoip2.records.Country` - - .. attribute:: represented_country - - Object for the country represented by the users of the IP address - when that country is different than the country in ``country``. For - instance, the country represented by an overseas military base. - - :type: :py:class:`geoip2.records.RepresentedCountry` - - .. attribute:: subdivisions - - Object (tuple) representing the subdivisions of the country to which - the location of the requested IP address belongs. - - :type: :py:class:`geoip2.records.Subdivisions` - - .. attribute:: traits - - Object with the traits of the requested IP address. - - :type: :py:class:`geoip2.records.Traits` - - """ - - city: geoip2.records.City - location: geoip2.records.Location - postal: geoip2.records.Postal - subdivisions: geoip2.records.Subdivisions - - def __init__( - self, raw_response: Dict[str, Any], locales: Optional[List[str]] = None - ) -> None: - super().__init__(raw_response, locales) - self.city = geoip2.records.City(locales, **raw_response.get("city", {})) - self.location = geoip2.records.Location(**raw_response.get("location", {})) - self.postal = geoip2.records.Postal(**raw_response.get("postal", {})) - self.subdivisions = geoip2.records.Subdivisions( - locales, *raw_response.get("subdivisions", []) - ) - - -class Insights(City): - """Model for the GeoIP2 Precision: Insights web service endpoint. - - .. attribute:: city - - City object for the requested IP address. - - :type: :py:class:`geoip2.records.City` - - .. attribute:: continent - - Continent object for the requested IP address. - - :type: :py:class:`geoip2.records.Continent` - - .. attribute:: country - - Country object for the requested IP address. This record represents the - country where MaxMind believes the IP is located. - - :type: :py:class:`geoip2.records.Country` - - .. attribute:: location - - Location object for the requested IP address. - - .. attribute:: maxmind - - Information related to your MaxMind account. - - :type: :py:class:`geoip2.records.MaxMind` - - .. attribute:: registered_country - - The registered country object for the requested IP address. This record - represents the country where the ISP has registered a given IP block in - and may differ from the user's country. - - :type: :py:class:`geoip2.records.Country` - - .. attribute:: represented_country - - Object for the country represented by the users of the IP address - when that country is different than the country in ``country``. For - instance, the country represented by an overseas military base. - - :type: :py:class:`geoip2.records.RepresentedCountry` - - .. attribute:: subdivisions - - Object (tuple) representing the subdivisions of the country to which - the location of the requested IP address belongs. - - :type: :py:class:`geoip2.records.Subdivisions` - - .. attribute:: traits - - Object with the traits of the requested IP address. - - :type: :py:class:`geoip2.records.Traits` - - """ - - -class Enterprise(City): - """Model for the GeoIP2 Enterprise database. - - .. attribute:: city - - City object for the requested IP address. - - :type: :py:class:`geoip2.records.City` - - .. attribute:: continent - - Continent object for the requested IP address. - - :type: :py:class:`geoip2.records.Continent` - - .. attribute:: country - - Country object for the requested IP address. This record represents the - country where MaxMind believes the IP is located. - - :type: :py:class:`geoip2.records.Country` - - .. attribute:: location - - Location object for the requested IP address. - - .. attribute:: maxmind - - Information related to your MaxMind account. - - :type: :py:class:`geoip2.records.MaxMind` - - .. attribute:: registered_country - - The registered country object for the requested IP address. This record - represents the country where the ISP has registered a given IP block in - and may differ from the user's country. - - :type: :py:class:`geoip2.records.Country` - - .. attribute:: represented_country - - Object for the country represented by the users of the IP address - when that country is different than the country in ``country``. For - instance, the country represented by an overseas military base. - - :type: :py:class:`geoip2.records.RepresentedCountry` - - .. attribute:: subdivisions - - Object (tuple) representing the subdivisions of the country to which - the location of the requested IP address belongs. - - :type: :py:class:`geoip2.records.Subdivisions` - - .. attribute:: traits - - Object with the traits of the requested IP address. - - :type: :py:class:`geoip2.records.Traits` - - """ - - -class SimpleModel(SimpleEquality, metaclass=ABCMeta): - """Provides basic methods for non-location models""" - - raw: Dict[str, Union[bool, str, int]] - ip_address: str - - def __init__(self, raw: Dict[str, Union[bool, str, int]]) -> None: - self.raw = raw - self._network = None - self._prefix_len = raw.get("prefix_len") - self.ip_address = cast(str, raw.get("ip_address")) - - def __repr__(self) -> str: - return f"{self.__module__}.{self.__class__.__name__}({self.raw})" - - @property - def network(self) -> Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]]: - """The network for the record""" - # This code is duplicated for performance reasons - # pylint: disable=duplicate-code - network = self._network - if isinstance(network, (ipaddress.IPv4Network, ipaddress.IPv6Network)): - return network - - ip_address = self.ip_address - prefix_len = self._prefix_len - if ip_address is None or prefix_len is None: - return None - network = ipaddress.ip_network(f"{ip_address}/{prefix_len}", False) - self._network = network - return network - - -class AnonymousIP(SimpleModel): - """Model class for the GeoIP2 Anonymous IP. - - This class provides the following attribute: - - .. attribute:: is_anonymous - - This is true if the IP address belongs to any sort of anonymous network. - - :type: bool - - .. attribute:: is_anonymous_vpn - - This is true if the IP address is registered to an anonymous VPN - provider. - - If a VPN provider does not register subnets under names associated with - them, we will likely only flag their IP ranges using the - ``is_hosting_provider`` attribute. - - :type: bool - - .. attribute:: is_hosting_provider - - This is true if the IP address belongs to a hosting or VPN provider - (see description of ``is_anonymous_vpn`` attribute). - - :type: bool - - .. attribute:: is_public_proxy - - This is true if the IP address belongs to a public proxy. - - :type: bool - - .. attribute:: is_residential_proxy - - This is true if the IP address is on a suspected anonymizing network - and belongs to a residential ISP. - - :type: bool - - .. attribute:: is_tor_exit_node - - This is true if the IP address is a Tor exit node. - - :type: bool - - .. attribute:: ip_address - - The IP address used in the lookup. - - :type: unicode - - .. attribute:: network - - The network associated with the record. In particular, this is the - largest network where all of the fields besides ip_address have the same - value. - - :type: ipaddress.IPv4Network or ipaddress.IPv6Network - """ - - is_anonymous: bool - is_anonymous_vpn: bool - is_hosting_provider: bool - is_public_proxy: bool - is_residential_proxy: bool - is_tor_exit_node: bool - - def __init__(self, raw: Dict[str, bool]) -> None: - super().__init__(raw) # type: ignore - self.is_anonymous = raw.get("is_anonymous", False) - self.is_anonymous_vpn = raw.get("is_anonymous_vpn", False) - self.is_hosting_provider = raw.get("is_hosting_provider", False) - self.is_public_proxy = raw.get("is_public_proxy", False) - self.is_residential_proxy = raw.get("is_residential_proxy", False) - self.is_tor_exit_node = raw.get("is_tor_exit_node", False) - - -class ASN(SimpleModel): - """Model class for the GeoLite2 ASN. - - This class provides the following attribute: - - .. attribute:: autonomous_system_number - - The autonomous system number associated with the IP address. - - :type: int - - .. attribute:: autonomous_system_organization - - The organization associated with the registered autonomous system number - for the IP address. - - :type: unicode - - .. attribute:: ip_address - - The IP address used in the lookup. - - :type: unicode - - .. attribute:: network - - The network associated with the record. In particular, this is the - largest network where all of the fields besides ip_address have the same - value. - - :type: ipaddress.IPv4Network or ipaddress.IPv6Network - """ - - autonomous_system_number: Optional[int] - autonomous_system_organization: Optional[str] - - # pylint:disable=too-many-arguments - def __init__(self, raw: Dict[str, Union[str, int]]) -> None: - super().__init__(raw) - self.autonomous_system_number = cast( - Optional[int], raw.get("autonomous_system_number") - ) - self.autonomous_system_organization = cast( - Optional[str], raw.get("autonomous_system_organization") - ) - - -class ConnectionType(SimpleModel): - """Model class for the GeoIP2 Connection-Type. - - This class provides the following attribute: - - .. attribute:: connection_type - - The connection type may take the following values: - - - Dialup - - Cable/DSL - - Corporate - - Cellular - - Additional values may be added in the future. - - :type: unicode - - .. attribute:: ip_address - - The IP address used in the lookup. - - :type: unicode - - .. attribute:: network - - The network associated with the record. In particular, this is the - largest network where all of the fields besides ip_address have the same - value. - - :type: ipaddress.IPv4Network or ipaddress.IPv6Network - """ - - connection_type: Optional[str] - - def __init__(self, raw: Dict[str, Union[str, int]]) -> None: - super().__init__(raw) - self.connection_type = cast(Optional[str], raw.get("connection_type")) - - -class Domain(SimpleModel): - """Model class for the GeoIP2 Domain. - - This class provides the following attribute: - - .. attribute:: domain - - The domain associated with the IP address. - - :type: unicode - - .. attribute:: ip_address - - The IP address used in the lookup. - - :type: unicode - - .. attribute:: network - - The network associated with the record. In particular, this is the - largest network where all of the fields besides ip_address have the same - value. - - :type: ipaddress.IPv4Network or ipaddress.IPv6Network - - """ - - domain: Optional[str] - - def __init__(self, raw: Dict[str, Union[str, int]]) -> None: - super().__init__(raw) - self.domain = cast(Optional[str], raw.get("domain")) - - -class ISP(ASN): - """Model class for the GeoIP2 ISP. - - This class provides the following attribute: - - .. attribute:: autonomous_system_number - - The autonomous system number associated with the IP address. - - :type: int - - .. attribute:: autonomous_system_organization - - The organization associated with the registered autonomous system number - for the IP address. - - :type: unicode - - .. attribute:: isp - - The name of the ISP associated with the IP address. - - :type: unicode - - .. attribute:: organization - - The name of the organization associated with the IP address. - - :type: unicode - - .. attribute:: ip_address - - The IP address used in the lookup. - - :type: unicode - - .. attribute:: network - - The network associated with the record. In particular, this is the - largest network where all of the fields besides ip_address have the same - value. - - :type: ipaddress.IPv4Network or ipaddress.IPv6Network - """ - - isp: Optional[str] - organization: Optional[str] - - # pylint:disable=too-many-arguments - def __init__(self, raw: Dict[str, Union[str, int]]) -> None: - super().__init__(raw) - self.isp = cast(Optional[str], raw.get("isp")) - self.organization = cast(Optional[str], raw.get("organization")) diff --git a/geoip2/records.py b/geoip2/records.py deleted file mode 100644 index d11506b..0000000 --- a/geoip2/records.py +++ /dev/null @@ -1,884 +0,0 @@ -""" - -Records -======= - -""" -# pylint:disable=too-many-arguments,too-many-instance-attributes,too-many-locals - -import ipaddress - -# pylint:disable=R0903 -from abc import ABCMeta -from typing import Dict, List, Optional, Type, Union - -from geoip2.mixins import SimpleEquality - - -class Record(SimpleEquality, metaclass=ABCMeta): - """All records are subclasses of the abstract class ``Record``.""" - - def __repr__(self) -> str: - args = ", ".join("%s=%r" % x for x in self.__dict__.items()) - return f"{self.__module__}.{self.__class__.__name__}({args})" - - -class PlaceRecord(Record, metaclass=ABCMeta): - """All records with :py:attr:`names` subclass :py:class:`PlaceRecord`.""" - - names: Dict[str, str] - _locales: List[str] - - def __init__( - self, - locales: Optional[List[str]] = None, - names: Optional[Dict[str, str]] = None, - ) -> None: - if locales is None: - locales = ["en"] - self._locales = locales - if names is None: - names = {} - self.names = names - - @property - def name(self) -> Optional[str]: - """Dict with locale codes as keys and localized name as value.""" - # pylint:disable=E1101 - return next((self.names.get(x) for x in self._locales if x in self.names), None) - - -class City(PlaceRecord): - """Contains data for the city record associated with an IP address. - - This class contains the city-level data associated with an IP address. - - This record is returned by ``city``, ``enterprise``, and ``insights``. - - Attributes: - - .. attribute:: confidence - - A value from 0-100 indicating MaxMind's - confidence that the city is correct. This attribute is only available - from the Insights end point and the GeoIP2 Enterprise database. - - :type: int - - .. attribute:: geoname_id - - The GeoName ID for the city. - - :type: int - - .. attribute:: name - - The name of the city based on the locales list passed to the - constructor. - - :type: unicode - - .. attribute:: names - - A dictionary where the keys are locale codes - and the values are names. - - :type: dict - - """ - - confidence: Optional[int] - geoname_id: Optional[int] - - def __init__( - self, - locales: Optional[List[str]] = None, - confidence: Optional[int] = None, - geoname_id: Optional[int] = None, - names: Optional[Dict[str, str]] = None, - **_, - ) -> None: - self.confidence = confidence - self.geoname_id = geoname_id - super().__init__(locales, names) - - -class Continent(PlaceRecord): - """Contains data for the continent record associated with an IP address. - - This class contains the continent-level data associated with an IP - address. - - Attributes: - - - .. attribute:: code - - A two character continent code like "NA" (North America) - or "OC" (Oceania). - - :type: unicode - - .. attribute:: geoname_id - - The GeoName ID for the continent. - - :type: int - - .. attribute:: name - - Returns the name of the continent based on the locales list passed to - the constructor. - - :type: unicode - - .. attribute:: names - - A dictionary where the keys are locale codes - and the values are names. - - :type: dict - - """ - - code: Optional[str] - geoname_id: Optional[int] - - def __init__( - self, - locales: Optional[List[str]] = None, - code: Optional[str] = None, - geoname_id: Optional[int] = None, - names: Optional[Dict[str, str]] = None, - **_, - ) -> None: - self.code = code - self.geoname_id = geoname_id - super().__init__(locales, names) - - -class Country(PlaceRecord): - """Contains data for the country record associated with an IP address. - - This class contains the country-level data associated with an IP address. - - Attributes: - - - .. attribute:: confidence - - A value from 0-100 indicating MaxMind's confidence that - the country is correct. This attribute is only available from the - Insights end point and the GeoIP2 Enterprise database. - - :type: int - - .. attribute:: geoname_id - - The GeoName ID for the country. - - :type: int - - .. attribute:: is_in_european_union - - This is true if the country is a member state of the European Union. - - :type: bool - - .. attribute:: iso_code - - The two-character `ISO 3166-1 - `_ alpha code for the - country. - - :type: unicode - - .. attribute:: name - - The name of the country based on the locales list passed to the - constructor. - - :type: unicode - - .. attribute:: names - - A dictionary where the keys are locale codes and the values - are names. - - :type: dict - - """ - - confidence: Optional[int] - geoname_id: Optional[int] - is_in_european_union: bool - iso_code: Optional[str] - - def __init__( - self, - locales: Optional[List[str]] = None, - confidence: Optional[int] = None, - geoname_id: Optional[int] = None, - is_in_european_union: bool = False, - iso_code: Optional[str] = None, - names: Optional[Dict[str, str]] = None, - **_, - ) -> None: - self.confidence = confidence - self.geoname_id = geoname_id - self.is_in_european_union = is_in_european_union - self.iso_code = iso_code - super().__init__(locales, names) - - -class RepresentedCountry(Country): - """Contains data for the represented country associated with an IP address. - - This class contains the country-level data associated with an IP address - for the IP's represented country. The represented country is the country - represented by something like a military base. - - Attributes: - - - .. attribute:: confidence - - A value from 0-100 indicating MaxMind's confidence that - the country is correct. This attribute is only available from the - Insights end point and the GeoIP2 Enterprise database. - - :type: int - - .. attribute:: geoname_id - - The GeoName ID for the country. - - :type: int - - .. attribute:: is_in_european_union - - This is true if the country is a member state of the European Union. - - :type: bool - - .. attribute:: iso_code - - The two-character `ISO 3166-1 - `_ alpha code for the country. - - :type: unicode - - .. attribute:: name - - The name of the country based on the locales list passed to the - constructor. - - :type: unicode - - .. attribute:: names - - A dictionary where the keys are locale codes and the values - are names. - - :type: dict - - - .. attribute:: type - - A string indicating the type of entity that is representing the - country. Currently we only return ``military`` but this could expand to - include other types in the future. - - :type: unicode - - """ - - type: Optional[str] - - def __init__( - self, - locales: Optional[List[str]] = None, - confidence: Optional[int] = None, - geoname_id: Optional[int] = None, - is_in_european_union: bool = False, - iso_code: Optional[str] = None, - names: Optional[Dict[str, str]] = None, - # pylint:disable=redefined-builtin - type: Optional[str] = None, - **_, - ) -> None: - self.type = type - super().__init__( - locales, confidence, geoname_id, is_in_european_union, iso_code, names - ) - - -class Location(Record): - """Contains data for the location record associated with an IP address. - - This class contains the location data associated with an IP address. - - This record is returned by ``city``, ``enterprise``, and ``insights``. - - Attributes: - - .. attribute:: average_income - - The average income in US dollars associated with the requested IP - address. This attribute is only available from the Insights end point. - - :type: int - - .. attribute:: accuracy_radius - - The approximate accuracy radius in kilometers around the latitude and - longitude for the IP address. This is the radius where we have a 67% - confidence that the device using the IP address resides within the - circle centered at the latitude and longitude with the provided radius. - - :type: int - - .. attribute:: latitude - - The approximate latitude of the location associated with the IP - address. This value is not precise and should not be used to identify a - particular address or household. - - :type: float - - .. attribute:: longitude - - The approximate longitude of the location associated with the IP - address. This value is not precise and should not be used to identify a - particular address or household. - - :type: float - - .. attribute:: metro_code - - The metro code of the location if the - location is in the US. MaxMind returns the same metro codes as the - `Google AdWords API - `_. - - :type: int - - .. attribute:: population_density - - The estimated population per square kilometer associated with the IP - address. This attribute is only available from the Insights end point. - - :type: int - - .. attribute:: time_zone - - The time zone associated with location, as specified by the `IANA Time - Zone Database `_, e.g., - "America/New_York". - - :type: unicode - - """ - - average_income: Optional[int] - accuracy_radius: Optional[int] - latitude: Optional[float] - longitude: Optional[float] - metro_code: Optional[int] - population_density: Optional[int] - time_zone: Optional[str] - - def __init__( - self, - average_income: Optional[int] = None, - accuracy_radius: Optional[int] = None, - latitude: Optional[float] = None, - longitude: Optional[float] = None, - metro_code: Optional[int] = None, - population_density: Optional[int] = None, - time_zone: Optional[str] = None, - **_, - ) -> None: - self.average_income = average_income - self.accuracy_radius = accuracy_radius - self.latitude = latitude - self.longitude = longitude - self.metro_code = metro_code - self.population_density = population_density - self.time_zone = time_zone - - -class MaxMind(Record): - """Contains data related to your MaxMind account. - - Attributes: - - .. attribute:: queries_remaining - - The number of remaining queries you have - for the end point you are calling. - - :type: int - - """ - - queries_remaining: Optional[int] - - def __init__(self, queries_remaining: Optional[int] = None, **_) -> None: - self.queries_remaining = queries_remaining - - -class Postal(Record): - """Contains data for the postal record associated with an IP address. - - This class contains the postal data associated with an IP address. - - This attribute is returned by ``city``, ``enterprise``, and ``insights``. - - Attributes: - - .. attribute:: code - - The postal code of the location. Postal - codes are not available for all countries. In some countries, this will - only contain part of the postal code. - - :type: unicode - - .. attribute:: confidence - - A value from 0-100 indicating - MaxMind's confidence that the postal code is correct. This attribute is - only available from the Insights end point and the GeoIP2 Enterprise - database. - - :type: int - - """ - - code: Optional[str] - confidence: Optional[int] - - def __init__( - self, code: Optional[str] = None, confidence: Optional[int] = None, **_ - ) -> None: - self.code = code - self.confidence = confidence - - -class Subdivision(PlaceRecord): - """Contains data for the subdivisions associated with an IP address. - - This class contains the subdivision data associated with an IP address. - - This attribute is returned by ``city``, ``enterprise``, and ``insights``. - - Attributes: - - .. attribute:: confidence - - This is a value from 0-100 indicating MaxMind's - confidence that the subdivision is correct. This attribute is only - available from the Insights end point and the GeoIP2 Enterprise - database. - - :type: int - - .. attribute:: geoname_id - - This is a GeoName ID for the subdivision. - - :type: int - - .. attribute:: iso_code - - This is a string up to three characters long - contain the subdivision portion of the `ISO 3166-2 code - `_. - - :type: unicode - - .. attribute:: name - - The name of the subdivision based on the locales list passed to the - constructor. - - :type: unicode - - .. attribute:: names - - A dictionary where the keys are locale codes and the - values are names - - :type: dict - - """ - - confidence: Optional[int] - geoname_id: Optional[int] - iso_code: Optional[str] - - def __init__( - self, - locales: Optional[List[str]] = None, - confidence: Optional[int] = None, - geoname_id: Optional[int] = None, - iso_code: Optional[str] = None, - names: Optional[Dict[str, str]] = None, - **_, - ) -> None: - self.confidence = confidence - self.geoname_id = geoname_id - self.iso_code = iso_code - super().__init__(locales, names) - - -class Subdivisions(tuple): - """A tuple-like collection of subdivisions associated with an IP address. - - This class contains the subdivisions of the country associated with the - IP address from largest to smallest. - - For instance, the response for Oxford in the United Kingdom would have - England as the first element and Oxfordshire as the second element. - - This attribute is returned by ``city``, ``enterprise``, and ``insights``. - """ - - def __new__( - cls: Type["Subdivisions"], locales: Optional[List[str]], *subdivisions - ) -> "Subdivisions": - subobjs = tuple(Subdivision(locales, **x) for x in subdivisions) - obj = super().__new__(cls, subobjs) # type: ignore - return obj - - def __init__( - self, locales: Optional[List[str]], *subdivisions # pylint:disable=W0613 - ) -> None: - self._locales = locales - super().__init__() - - @property - def most_specific(self) -> Subdivision: - """The most specific (smallest) subdivision available. - - If there are no :py:class:`Subdivision` objects for the response, - this returns an empty :py:class:`Subdivision`. - - :type: :py:class:`Subdivision` - """ - try: - return self[-1] - except IndexError: - return Subdivision(self._locales) - - -class Traits(Record): - """Contains data for the traits record associated with an IP address. - - This class contains the traits data associated with an IP address. - - This class has the following attributes: - - - .. attribute:: autonomous_system_number - - The `autonomous system - number `_ - associated with the IP address. This attribute is only available from - the City and Insights web service end points and the GeoIP2 Enterprise - database. - - :type: int - - .. attribute:: autonomous_system_organization - - The organization associated with the registered `autonomous system - number `_ for - the IP address. This attribute is only available from the City and - Insights web service end points and the GeoIP2 Enterprise database. - - :type: unicode - - .. attribute:: connection_type - - The connection type may take the following values: - - - Dialup - - Cable/DSL - - Corporate - - Cellular - - Additional values may be added in the future. - - This attribute is only available in the GeoIP2 Enterprise database. - - :type: unicode - - .. attribute:: domain - - The second level domain associated with the - IP address. This will be something like "example.com" or - "example.co.uk", not "foo.example.com". This attribute is only available - from the City and Insights web service end points and the GeoIP2 - Enterprise database. - - :type: unicode - - .. attribute:: ip_address - - The IP address that the data in the model - is for. If you performed a "me" lookup against the web service, this - will be the externally routable IP address for the system the code is - running on. If the system is behind a NAT, this may differ from the IP - address locally assigned to it. - - :type: unicode - - .. attribute:: is_anonymous - - This is true if the IP address belongs to any sort of anonymous network. - This attribute is only available from GeoIP2 Precision Insights. - - :type: bool - - .. attribute:: is_anonymous_proxy - - This is true if the IP is an anonymous - proxy. See http://dev.maxmind.com/faq/geoip#anonproxy for further - details. - - :type: bool - - .. deprecated:: 2.2.0 - Use our our `GeoIP2 Anonymous IP database - `_ - instead. - - .. attribute:: is_anonymous_vpn - - This is true if the IP address is registered to an anonymous VPN - provider. - - If a VPN provider does not register subnets under names associated with - them, we will likely only flag their IP ranges using the - ``is_hosting_provider`` attribute. - - This attribute is only available from GeoIP2 Precision Insights. - - :type: bool - - .. attribute:: is_hosting_provider - - This is true if the IP address belongs to a hosting or VPN provider - (see description of ``is_anonymous_vpn`` attribute). - This attribute is only available from GeoIP2 Precision Insights. - - :type: bool - - .. attribute:: is_legitimate_proxy - - This attribute is true if MaxMind believes this IP address to be a - legitimate proxy, such as an internal VPN used by a corporation. This - attribute is only available in the GeoIP2 Enterprise database. - - :type: bool - - .. attribute:: is_public_proxy - - This is true if the IP address belongs to a public proxy. This attribute - is only available from GeoIP2 Precision Insights. - - :type: bool - - .. attribute:: is_residential_proxy - - This is true if the IP address is on a suspected anonymizing network - and belongs to a residential ISP. This attribute is only available from - GeoIP2 Precision Insights. - - :type: bool - - - .. attribute:: is_satellite_provider - - This is true if the IP address is from a satellite provider that - provides service to multiple countries. - - :type: bool - - .. deprecated:: 2.2.0 - Due to the increased coverage by mobile carriers, very few - satellite providers now serve multiple countries. As a result, the - output does not provide sufficiently relevant data for us to maintain - it. - - .. attribute:: is_tor_exit_node - - This is true if the IP address is a Tor exit node. This attribute is - only available from GeoIP2 Precision Insights. - - :type: bool - - .. attribute:: isp - - The name of the ISP associated with the IP address. This attribute is - only available from the City and Insights web service end points and the - GeoIP2 Enterprise database. - - :type: unicode - - .. attribute:: network - - The network associated with the record. In particular, this is the - largest network where all of the fields besides ip_address have the same - value. - - :type: ipaddress.IPv4Network or ipaddress.IPv6Network - - .. attribute:: organization - - The name of the organization associated with the IP address. This - attribute is only available from the City and Insights web service end - points and the GeoIP2 Enterprise database. - - :type: unicode - - .. attribute:: static_ip_score - - An indicator of how static or dynamic an IP address is. The value ranges - from 0 to 99.99 with higher values meaning a greater static association. - For example, many IP addresses with a user_type of cellular have a - lifetime under one. Static Cable/DSL IPs typically have a lifetime above - thirty. - - This indicator can be useful for deciding whether an IP address represents - the same user over time. This attribute is only available from GeoIP2 - Precision Insights. - - :type: float - - .. attribute:: user_count - - The estimated number of users sharing the IP/network during the past 24 - hours. For IPv4, the count is for the individual IP. For IPv6, the count - is for the /64 network. This attribute is only available from GeoIP2 - Precision Insights. - - :type: int - - .. attribute:: user_type - - The user type associated with the IP - address. This can be one of the following values: - - * business - * cafe - * cellular - * college - * content_delivery_network - * dialup - * government - * hosting - * library - * military - * residential - * router - * school - * search_engine_spider - * traveler - - This attribute is only available from the Insights end point and the - GeoIP2 Enterprise database. - - :type: unicode - - """ - - autonomous_system_number: Optional[int] - autonomous_system_organization: Optional[str] - connection_type: Optional[str] - domain: Optional[str] - is_anonymous: bool - is_anonymous_proxy: bool - is_anonymous_vpn: bool - is_hosting_provider: bool - is_legitimate_proxy: bool - is_public_proxy: bool - is_residential_proxy: bool - is_satellite_provider: bool - is_tor_exit_node: bool - isp: Optional[str] - ip_address: Optional[str] - organization: Optional[str] - static_ip_score: Optional[float] - user_count: Optional[int] - user_type: Optional[str] - _network: Optional[str] - _prefix_len: Optional[int] - - def __init__( - self, - autonomous_system_number: Optional[int] = None, - autonomous_system_organization: Optional[str] = None, - connection_type: Optional[str] = None, - domain: Optional[str] = None, - is_anonymous: bool = False, - is_anonymous_proxy: bool = False, - is_anonymous_vpn: bool = False, - is_hosting_provider: bool = False, - is_legitimate_proxy: bool = False, - is_public_proxy: bool = False, - is_residential_proxy: bool = False, - is_satellite_provider: bool = False, - is_tor_exit_node: bool = False, - isp: Optional[str] = None, - ip_address: Optional[str] = None, - network: Optional[str] = None, - organization: Optional[str] = None, - prefix_len: Optional[int] = None, - static_ip_score: Optional[float] = None, - user_count: Optional[int] = None, - user_type: Optional[str] = None, - **_, - ) -> None: - self.autonomous_system_number = autonomous_system_number - self.autonomous_system_organization = autonomous_system_organization - self.connection_type = connection_type - self.domain = domain - self.is_anonymous = is_anonymous - self.is_anonymous_proxy = is_anonymous_proxy - self.is_anonymous_vpn = is_anonymous_vpn - self.is_hosting_provider = is_hosting_provider - self.is_legitimate_proxy = is_legitimate_proxy - self.is_public_proxy = is_public_proxy - self.is_residential_proxy = is_residential_proxy - self.is_satellite_provider = is_satellite_provider - self.is_tor_exit_node = is_tor_exit_node - self.isp = isp - self.organization = organization - self.static_ip_score = static_ip_score - self.user_type = user_type - self.user_count = user_count - self.ip_address = ip_address - self._network = network - self._prefix_len = prefix_len - - # This code is duplicated for performance reasons - # pylint: disable=duplicate-code - @property - def network(self) -> Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]]: - """The network for the record""" - network = self._network - if isinstance(network, (ipaddress.IPv4Network, ipaddress.IPv6Network)): - return network - - if network is None: - ip_address = self.ip_address - prefix_len = self._prefix_len - if ip_address is None or prefix_len is None: - return None - network = f"{ip_address}/{prefix_len}" - network = ipaddress.ip_network(network, False) - self._network = network - return network # type: ignore diff --git a/pylintrc b/pylintrc deleted file mode 100644 index 53a1201..0000000 --- a/pylintrc +++ /dev/null @@ -1,6 +0,0 @@ -[MESSAGES CONTROL] -disable=C0330 - -[BASIC] - -no-docstring-rgx=_.* diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2e73112 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,133 @@ +[project] +name = "geoip2" +version = "5.1.0" +description = "MaxMind GeoIP2 API" +authors = [ + {name = "Gregory Oschwald", email = "goschwald@maxmind.com"}, +] +dependencies = [ + "aiohttp>=3.6.2,<4.0.0", + "maxminddb>=2.7.0,<3.0.0", + "requests>=2.24.0,<3.0.0", +] +requires-python = ">=3.9" +readme = "README.rst" +license = "Apache-2.0" +license-files = ["LICENSE"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Internet", + "Topic :: Internet :: Proxy Servers", +] + +[dependency-groups] +dev = [ + "pytest>=8.3.5", + "pytest-httpserver>=1.0.10", + "types-requests>=2.32.0.20250328", +] +lint = [ + "mypy>=1.15.0", + "ruff>=0.11.6", +] + +[build-system] +requires = ["uv_build>=0.7.19,<0.8.0"] +build-backend = "uv_build" + +[tool.uv.build-backend] +source-include = [ + "HISTORY.rst", + "README.rst", + "LICENSE", + "docs/html", + "examples/*.py", + "tests/*.py", + "tests/data/test-data/*.mmdb" +] + +[project.urls] +Homepage = "https://www.maxmind.com/" +Documentation = "https://geoip2.readthedocs.org/" +"Source Code" = "https://github.com/maxmind/GeoIP2-python" +"Issue Tracker" = "https://github.com/maxmind/GeoIP2-python/issues" + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + # Skip type annotation on **_ + "ANN003", + + # Redundant as the formatter handles missing trailing commas. + "COM812", + + # documenting magic methods + "D105", + + # Conflicts with D211 + "D203", + + # Conflicts with D212 + "D213", + + # Magic numbers for HTTP status codes seem ok most of the time. + "PLR2004", + + # pytest rules + "PT009", + "PT027", +] + +[tool.ruff.lint.per-file-ignores] +"docs/*" = ["ALL"] +"src/geoip2/{models,records}.py" = [ "D107", "PLR0913" ] +"tests/*" = ["ANN201", "D"] + +[tool.tox] +env_list = [ + "3.9", + "3.10", + "3.11", + "3.12", + "3.13", + "lint", +] +skip_missing_interpreters = false + +[tool.tox.env_run_base] +dependency_groups = [ + "dev", +] +commands = [ + ["pytest", "tests"], +] + +[tool.tox.env.lint] +description = "Code linting" +python = "3.13" +dependency_groups = [ + "dev", + "lint", +] +commands = [ + ["mypy", "src", "tests"], + ["ruff", "check"], + ["ruff", "format", "--check", "--diff", "."], +] + +[tool.tox.gh.python] +"3.13" = ["3.13", "lint"] +"3.12" = ["3.12"] +"3.11" = ["3.11"] +"3.10" = ["3.10"] +"3.9" = ["3.9"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index af2c6ff..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -aiohttp>=3.6.2,<4.0.0 -maxminddb>=2.0.0,<3.0.0 -requests>=2.24.0,<3.0.0 -urllib3>=1.25.2,<2.0.0 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index cdfe10f..0000000 --- a/setup.cfg +++ /dev/null @@ -1,10 +0,0 @@ -[aliases] -build_html = build_sphinx -b html --build-dir docs -sdist = build_html sdist - -[flake8] -# black uses 88 : ¯\_(ツ)_/¯ -max-line-length = 88 - -[wheel] -universal = 1 diff --git a/setup.py b/setup.py deleted file mode 100644 index 2194e8f..0000000 --- a/setup.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python - -import codecs -import os -import sys - -import geoip2 - -from setuptools import setup - -packages = ["geoip2"] - -requirements = [i.strip() for i in open("requirements.txt").readlines()] - -setup( - name="geoip2", - version=geoip2.__version__, - description="MaxMind GeoIP2 API", - long_description=codecs.open("README.rst", "r", "utf-8").read(), - author="Gregory Oschwald", - author_email="goschwald@maxmind.com", - url="http://www.maxmind.com/", - packages=["geoip2"], - package_data={"": ["LICENSE"], "geoip2": ["py.typed"]}, - package_dir={"geoip2": "geoip2"}, - include_package_data=True, - python_requires=">=3.6", - install_requires=requirements, - tests_require=["mocket>=3.8.9"], - test_suite="tests", - license=geoip2.__license__, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Web Environment", - "Intended Audience :: Developers", - "Intended Audience :: System Administrators", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python", - "Topic :: Internet :: Proxy Servers", - "Topic :: Internet", - ], -) diff --git a/src/geoip2/__init__.py b/src/geoip2/__init__.py new file mode 100644 index 0000000..8b92c9b --- /dev/null +++ b/src/geoip2/__init__.py @@ -0,0 +1,7 @@ +"""geoip2 client library.""" + +__title__ = "geoip2" +__version__ = "5.1.0" +__author__ = "Gregory Oschwald" +__license__ = "Apache License, Version 2.0" +__copyright__ = "Copyright (c) 2013-2025 MaxMind, Inc." diff --git a/src/geoip2/_internal.py b/src/geoip2/_internal.py new file mode 100644 index 0000000..30d767e --- /dev/null +++ b/src/geoip2/_internal.py @@ -0,0 +1,53 @@ +"""Internal utilities.""" + +import json +from abc import ABCMeta + + +class Model(metaclass=ABCMeta): # noqa: B024 + """Shared methods for MaxMind model classes.""" + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) and self.to_dict() == other.to_dict() + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) + + def __hash__(self) -> int: + # This is not particularly efficient, but I don't expect it to be used much. + return hash(json.dumps(self.to_dict(), sort_keys=True)) + + def to_dict(self) -> dict: # noqa: C901, PLR0912 + """Return a dict of the object suitable for serialization.""" + result = {} + for key, value in self.__dict__.items(): + if key.startswith("_"): + continue + if hasattr(value, "to_dict") and callable(value.to_dict): + if d := value.to_dict(): + result[key] = d + elif isinstance(value, (list, tuple)): + ls = [] + for e in value: + if hasattr(e, "to_dict") and callable(e.to_dict): + if e := e.to_dict(): + ls.append(e) + elif e is not None: + ls.append(e) + if ls: + result[key] = ls + # We only have dicts of strings currently. Do not bother with + # the general case. + elif isinstance(value, dict): + if value: + result[key] = value + elif value is not None and value is not False: + result[key] = value + + # network and ip_address are properties for performance reasons + if hasattr(self, "ip_address") and self.ip_address is not None: + result["ip_address"] = str(self.ip_address) + if hasattr(self, "network") and self.network is not None: + result["network"] = str(self.network) + + return result diff --git a/geoip2/database.py b/src/geoip2/database.py similarity index 66% rename from geoip2/database.py rename to src/geoip2/database.py index 6d934e2..2c10538 100644 --- a/geoip2/database.py +++ b/src/geoip2/database.py @@ -1,38 +1,53 @@ -""" -====================== -GeoIP2 Database Reader -====================== +"""The database reader for MaxMind MMDB files.""" + +from __future__ import annotations -""" import inspect -from typing import Any, cast, List, Optional, Type, Union +from typing import IO, TYPE_CHECKING, AnyStr, cast import maxminddb - -# pylint: disable=unused-import -from maxminddb import ( # type: ignore +from maxminddb import ( MODE_AUTO, - MODE_MMAP, - MODE_MMAP_EXT, + MODE_FD, MODE_FILE, MODE_MEMORY, - MODE_FD, + MODE_MMAP, + MODE_MMAP_EXT, + InvalidDatabaseError, ) import geoip2 -import geoip2.models import geoip2.errors -from geoip2.types import IPAddress -from geoip2.models import ( - ASN, - AnonymousIP, - City, - ConnectionType, - Country, - Domain, - Enterprise, - ISP, -) +import geoip2.models + +if TYPE_CHECKING: + import os + from collections.abc import Sequence + + from typing_extensions import Self + + from geoip2.models import ( + ASN, + ISP, + AnonymousIP, + AnonymousPlus, + City, + ConnectionType, + Country, + Domain, + Enterprise, + ) + from geoip2.types import IPAddress + +__all__ = [ + "MODE_AUTO", + "MODE_FD", + "MODE_FILE", + "MODE_MEMORY", + "MODE_MMAP", + "MODE_MMAP_EXT", + "Reader", +] class Reader: @@ -59,13 +74,16 @@ class Reader: """ def __init__( - self, fileish: str, locales: Optional[List[str]] = None, mode: int = MODE_AUTO + self, + fileish: AnyStr | int | os.PathLike | IO, + locales: Sequence[str] | None = None, + mode: int = MODE_AUTO, ) -> None: """Create GeoIP2 Reader. - :param fileish: The string path to the GeoIP2 database, or an existing - file descriptor pointing to the database. Note that this latter - usage is only valid when mode is MODE_FD. + :param fileish: A path to the GeoIP2 database or an existing file + descriptor pointing to the database. Note that a file descriptor + is only valid when mode is MODE_FD. :param locales: This is list of locale codes. This argument will be passed on to record classes to use when their name properties are called. The default value is ['en']. @@ -107,10 +125,10 @@ def __init__( self._db_type = self._db_reader.metadata().database_type self._locales = locales - def __enter__(self) -> "Reader": + def __enter__(self) -> Self: return self - def __exit__(self, exc_type: None, exc_value: None, traceback: None) -> None: + def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None: self.close() def country(self, ip_address: IPAddress) -> Country: @@ -121,9 +139,9 @@ def country(self, ip_address: IPAddress) -> Country: :returns: :py:class:`geoip2.models.Country` object """ - return cast( - Country, self._model_for(geoip2.models.Country, "Country", ip_address) + "Country", + self._model_for(geoip2.models.Country, "Country", ip_address), ) def city(self, ip_address: IPAddress) -> City: @@ -134,7 +152,7 @@ def city(self, ip_address: IPAddress) -> City: :returns: :py:class:`geoip2.models.City` object """ - return cast(City, self._model_for(geoip2.models.City, "City", ip_address)) + return cast("City", self._model_for(geoip2.models.City, "City", ip_address)) def anonymous_ip(self, ip_address: IPAddress) -> AnonymousIP: """Get the AnonymousIP object for the IP address. @@ -145,9 +163,28 @@ def anonymous_ip(self, ip_address: IPAddress) -> AnonymousIP: """ return cast( - AnonymousIP, + "AnonymousIP", self._flat_model_for( - geoip2.models.AnonymousIP, "GeoIP2-Anonymous-IP", ip_address + geoip2.models.AnonymousIP, + "GeoIP2-Anonymous-IP", + ip_address, + ), + ) + + def anonymous_plus(self, ip_address: IPAddress) -> AnonymousPlus: + """Get the AnonymousPlus object for the IP address. + + :param ip_address: IPv4 or IPv6 address as a string. + + :returns: :py:class:`geoip2.models.AnonymousPlus` object + + """ + return cast( + "AnonymousPlus", + self._flat_model_for( + geoip2.models.AnonymousPlus, + "GeoIP-Anonymous-Plus", + ip_address, ), ) @@ -160,7 +197,8 @@ def asn(self, ip_address: IPAddress) -> ASN: """ return cast( - ASN, self._flat_model_for(geoip2.models.ASN, "GeoLite2-ASN", ip_address) + "ASN", + self._flat_model_for(geoip2.models.ASN, "GeoLite2-ASN", ip_address), ) def connection_type(self, ip_address: IPAddress) -> ConnectionType: @@ -172,9 +210,11 @@ def connection_type(self, ip_address: IPAddress) -> ConnectionType: """ return cast( - ConnectionType, + "ConnectionType", self._flat_model_for( - geoip2.models.ConnectionType, "GeoIP2-Connection-Type", ip_address + geoip2.models.ConnectionType, + "GeoIP2-Connection-Type", + ip_address, ), ) @@ -187,7 +227,7 @@ def domain(self, ip_address: IPAddress) -> Domain: """ return cast( - Domain, + "Domain", self._flat_model_for(geoip2.models.Domain, "GeoIP2-Domain", ip_address), ) @@ -200,7 +240,7 @@ def enterprise(self, ip_address: IPAddress) -> Enterprise: """ return cast( - Enterprise, + "Enterprise", self._model_for(geoip2.models.Enterprise, "Enterprise", ip_address), ) @@ -213,55 +253,64 @@ def isp(self, ip_address: IPAddress) -> ISP: """ return cast( - ISP, self._flat_model_for(geoip2.models.ISP, "GeoIP2-ISP", ip_address) + "ISP", + self._flat_model_for(geoip2.models.ISP, "GeoIP2-ISP", ip_address), ) - def _get(self, database_type: str, ip_address: IPAddress) -> Any: + def _get(self, database_type: str, ip_address: IPAddress) -> tuple[dict, int]: if database_type not in self._db_type: caller = inspect.stack()[2][3] + msg = ( + f"The {caller} method cannot be used with the {self._db_type} database" + ) raise TypeError( - f"The {caller} method cannot be used with the {self._db_type} database", + msg, ) (record, prefix_len) = self._db_reader.get_with_prefix_len(ip_address) if record is None: + msg = f"The address {ip_address} is not in the database." raise geoip2.errors.AddressNotFoundError( - f"The address {ip_address} is not in the database.", + msg, + str(ip_address), + prefix_len, ) + if not isinstance(record, dict): + msg = f"Expected record to be a dict but was f{type(record)}" + raise InvalidDatabaseError(msg) return record, prefix_len def _model_for( self, - model_class: Union[Type[Country], Type[Enterprise], Type[City]], + model_class: type[City | Country | Enterprise], types: str, ip_address: IPAddress, - ) -> Union[Country, Enterprise, City]: + ) -> City | Country | Enterprise: (record, prefix_len) = self._get(types, ip_address) - traits = record.setdefault("traits", {}) - traits["ip_address"] = ip_address - traits["prefix_len"] = prefix_len - return model_class(record, locales=self._locales) + return model_class( + self._locales, + ip_address=ip_address, + prefix_len=prefix_len, + **record, + ) def _flat_model_for( self, - model_class: Union[ - Type[Domain], Type[ISP], Type[ConnectionType], Type[ASN], Type[AnonymousIP] - ], + model_class: type[Domain | ISP | ConnectionType | ASN | AnonymousIP], types: str, ip_address: IPAddress, - ) -> Union[ConnectionType, ISP, AnonymousIP, Domain, ASN]: + ) -> ConnectionType | ISP | AnonymousIP | Domain | ASN: (record, prefix_len) = self._get(types, ip_address) - record["ip_address"] = ip_address - record["prefix_len"] = prefix_len - return model_class(record) + return model_class(ip_address, prefix_len=prefix_len, **record) - def metadata(self) -> maxminddb.reader.Metadata: - """The metadata for the open database. + def metadata( + self, + ) -> maxminddb.reader.Metadata: + """Get the metadata for the open database. :returns: :py:class:`maxminddb.reader.Metadata` object """ return self._db_reader.metadata() def close(self) -> None: - """Closes the GeoIP2 database.""" - + """Close the GeoIP2 database.""" self._db_reader.close() diff --git a/src/geoip2/errors.py b/src/geoip2/errors.py new file mode 100644 index 0000000..893d9e3 --- /dev/null +++ b/src/geoip2/errors.py @@ -0,0 +1,110 @@ +"""Typed errors thrown by this library.""" + +from __future__ import annotations + +import ipaddress + + +class GeoIP2Error(RuntimeError): + """There was a generic error in GeoIP2. + + This class represents a generic error. It extends :py:exc:`RuntimeError` + and does not add any additional attributes. + + """ + + +class AddressNotFoundError(GeoIP2Error): + """The address you were looking up was not found.""" + + ip_address: str | None + """The IP address used in the lookup. This is only available for database + lookups. + """ + _prefix_len: int | None + + def __init__( + self, + message: str, + ip_address: str | None = None, + prefix_len: int | None = None, + ) -> None: + """Initialize self. + + Arguments: + message: A message describing the error. + ip_address: The IP address that was not found. + prefix_len: The prefix length for the network associated with + the IP address. + + """ + super().__init__(message) + self.ip_address = ip_address + self._prefix_len = prefix_len + + @property + def network(self) -> ipaddress.IPv4Network | ipaddress.IPv6Network | None: + """The network associated with the error. + + In particular, this is the largest network where no address would be + found. This is only available for database lookups. + """ + if self.ip_address is None or self._prefix_len is None: + return None + return ipaddress.ip_network( + f"{self.ip_address}/{self._prefix_len}", + strict=False, + ) + + +class AuthenticationError(GeoIP2Error): + """There was a problem authenticating the request.""" + + +class HTTPError(GeoIP2Error): + """There was an error when making your HTTP request. + + This class represents an HTTP transport error. It extends + :py:exc:`GeoIP2Error` and adds attributes of its own. + + """ + + http_status: int | None + """The HTTP status code returned""" + uri: str | None + """The URI queried""" + decoded_content: str | None + """The decoded response content""" + + def __init__( + self, + message: str, + http_status: int | None = None, + uri: str | None = None, + decoded_content: str | None = None, + ) -> None: + """Initialize self. + + Arguments: + message: A descriptive message for the error. + http_status: The HTTP status code associated with the error, if any. + uri: The URI that was being accessed when the error occurred. + decoded_content: The decoded HTTP response body, if available. + + """ + super().__init__(message) + self.http_status = http_status + self.uri = uri + self.decoded_content = decoded_content + + +class InvalidRequestError(GeoIP2Error): + """The request was invalid.""" + + +class OutOfQueriesError(GeoIP2Error): + """Your account is out of funds for the service queried.""" + + +class PermissionRequiredError(GeoIP2Error): + """Your account does not have permission to access this service.""" diff --git a/src/geoip2/models.py b/src/geoip2/models.py new file mode 100644 index 0000000..b41280b --- /dev/null +++ b/src/geoip2/models.py @@ -0,0 +1,444 @@ +"""The models for response from th GeoIP2 web service and databases. + +The only difference between the City and Insights model classes is which +fields in each record may be populated. See +https://dev.maxmind.com/geoip/docs/web-services?lang=en for more details. +""" + +from __future__ import annotations + +import datetime +import ipaddress +from abc import ABCMeta +from ipaddress import IPv4Address, IPv6Address +from typing import TYPE_CHECKING + +import geoip2.records +from geoip2._internal import Model + +if TYPE_CHECKING: + from collections.abc import Sequence + + from geoip2.types import IPAddress + + +class Country(Model): + """Model for the Country web service and Country database.""" + + continent: geoip2.records.Continent + """Continent object for the requested IP address.""" + + country: geoip2.records.Country + """Country object for the requested IP address. This record represents the + country where MaxMind believes the IP is located. + """ + + maxmind: geoip2.records.MaxMind + """Information related to your MaxMind account.""" + + registered_country: geoip2.records.Country + """The registered country object for the requested IP address. This record + represents the country where the ISP has registered a given IP block in + and may differ from the user's country. + """ + + represented_country: geoip2.records.RepresentedCountry + """Object for the country represented by the users of the IP address + when that country is different than the country in ``country``. For + instance, the country represented by an overseas military base. + """ + + traits: geoip2.records.Traits + """Object with the traits of the requested IP address.""" + + def __init__( + self, + locales: Sequence[str] | None, + *, + continent: dict | None = None, + country: dict | None = None, + ip_address: IPAddress | None = None, + maxmind: dict | None = None, + prefix_len: int | None = None, + registered_country: dict | None = None, + represented_country: dict | None = None, + traits: dict | None = None, + **_, + ) -> None: + self._locales = locales + self.continent = geoip2.records.Continent(locales, **(continent or {})) + self.country = geoip2.records.Country(locales, **(country or {})) + self.registered_country = geoip2.records.Country( + locales, + **(registered_country or {}), + ) + self.represented_country = geoip2.records.RepresentedCountry( + locales, + **(represented_country or {}), + ) + + self.maxmind = geoip2.records.MaxMind(**(maxmind or {})) + + traits = traits or {} + if ip_address is not None: + traits["ip_address"] = ip_address + if prefix_len is not None: + traits["prefix_len"] = prefix_len + + self.traits = geoip2.records.Traits(**traits) + + def __repr__(self) -> str: + return ( + f"{self.__module__}.{self.__class__.__name__}({self._locales!r}, " + f"{', '.join(f'{k}={v!r}' for k, v in self.to_dict().items())})" + ) + + +class City(Country): + """Model for the City Plus web service and the City database.""" + + city: geoip2.records.City + """City object for the requested IP address.""" + + location: geoip2.records.Location + """Location object for the requested IP address.""" + + postal: geoip2.records.Postal + """Postal object for the requested IP address.""" + + subdivisions: geoip2.records.Subdivisions + """Object (tuple) representing the subdivisions of the country to which + the location of the requested IP address belongs. + """ + + def __init__( + self, + locales: Sequence[str] | None, + *, + city: dict | None = None, + continent: dict | None = None, + country: dict | None = None, + location: dict | None = None, + ip_address: IPAddress | None = None, + maxmind: dict | None = None, + postal: dict | None = None, + prefix_len: int | None = None, + registered_country: dict | None = None, + represented_country: dict | None = None, + subdivisions: list[dict] | None = None, + traits: dict | None = None, + **_, + ) -> None: + super().__init__( + locales, + continent=continent, + country=country, + ip_address=ip_address, + maxmind=maxmind, + prefix_len=prefix_len, + registered_country=registered_country, + represented_country=represented_country, + traits=traits, + ) + self.city = geoip2.records.City(locales, **(city or {})) + self.location = geoip2.records.Location(**(location or {})) + self.postal = geoip2.records.Postal(**(postal or {})) + self.subdivisions = geoip2.records.Subdivisions(locales, *(subdivisions or [])) + + +class Insights(City): + """Model for the GeoIP2 Insights web service.""" + + +class Enterprise(City): + """Model for the GeoIP2 Enterprise database.""" + + +class SimpleModel(Model, metaclass=ABCMeta): + """Provides basic methods for non-location models.""" + + _ip_address: IPAddress + _network: ipaddress.IPv4Network | ipaddress.IPv6Network | None + _prefix_len: int | None + + def __init__( + self, + ip_address: IPAddress, + network: str | None, + prefix_len: int | None, + ) -> None: + if network: + self._network = ipaddress.ip_network(network, strict=False) + self._prefix_len = self._network.prefixlen + else: + # This case is for MMDB lookups where performance is paramount. + # This is why we don't generate the network unless .network is + # used. + self._network = None + self._prefix_len = prefix_len + self._ip_address = ip_address + + def __repr__(self) -> str: + d = self.to_dict() + d.pop("ip_address", None) + return ( + f"{self.__module__}.{self.__class__.__name__}(" + + repr(str(self._ip_address)) + + ", " + + ", ".join(f"{k}={v!r}" for k, v in d.items()) + + ")" + ) + + @property + def ip_address(self) -> IPv4Address | IPv6Address: + """The IP address for the record.""" + if not isinstance(self._ip_address, (IPv4Address, IPv6Address)): + self._ip_address = ipaddress.ip_address(self._ip_address) + return self._ip_address + + @property + def network(self) -> ipaddress.IPv4Network | ipaddress.IPv6Network | None: + """The network associated with the record. + + In particular, this is the largest network where all of the fields besides + ``ip_address`` have the same value. + """ + # This code is duplicated for performance reasons + network = self._network + if network is not None: + return network + + ip_address = self.ip_address + prefix_len = self._prefix_len + if ip_address is None or prefix_len is None: + return None + network = ipaddress.ip_network(f"{ip_address}/{prefix_len}", strict=False) + self._network = network + return network + + +class AnonymousIP(SimpleModel): + """Model class for the GeoIP2 Anonymous IP.""" + + is_anonymous: bool + """This is true if the IP address belongs to any sort of anonymous network.""" + + is_anonymous_vpn: bool + """This is true if the IP address is registered to an anonymous VPN + provider. + + If a VPN provider does not register subnets under names associated with + them, we will likely only flag their IP ranges using the + ``is_hosting_provider`` attribute. + """ + + is_hosting_provider: bool + """This is true if the IP address belongs to a hosting or VPN provider + (see description of ``is_anonymous_vpn`` attribute). + """ + + is_public_proxy: bool + """This is true if the IP address belongs to a public proxy.""" + + is_residential_proxy: bool + """This is true if the IP address is on a suspected anonymizing network + and belongs to a residential ISP. + """ + + is_tor_exit_node: bool + """This is true if the IP address is a Tor exit node.""" + + def __init__( + self, + ip_address: IPAddress, + *, + is_anonymous: bool = False, + is_anonymous_vpn: bool = False, + is_hosting_provider: bool = False, + is_public_proxy: bool = False, + is_residential_proxy: bool = False, + is_tor_exit_node: bool = False, + network: str | None = None, + prefix_len: int | None = None, + **_, + ) -> None: + super().__init__(ip_address, network, prefix_len) + self.is_anonymous = is_anonymous + self.is_anonymous_vpn = is_anonymous_vpn + self.is_hosting_provider = is_hosting_provider + self.is_public_proxy = is_public_proxy + self.is_residential_proxy = is_residential_proxy + self.is_tor_exit_node = is_tor_exit_node + + +class AnonymousPlus(AnonymousIP): + """Model class for the GeoIP Anonymous Plus.""" + + anonymizer_confidence: int | None + """A score ranging from 1 to 99 that is our percent confidence that the + network is currently part of an actively used VPN service. + """ + + network_last_seen: datetime.date | None + """The last day that the network was sighted in our analysis of anonymized + networks. + """ + + provider_name: str | None + """The name of the VPN provider (e.g., NordVPN, SurfShark, etc.) associated + with the network. + """ + + def __init__( + self, + ip_address: IPAddress, + *, + anonymizer_confidence: int | None = None, + is_anonymous: bool = False, + is_anonymous_vpn: bool = False, + is_hosting_provider: bool = False, + is_public_proxy: bool = False, + is_residential_proxy: bool = False, + is_tor_exit_node: bool = False, + network: str | None = None, + network_last_seen: str | None = None, + prefix_len: int | None = None, + provider_name: str | None = None, + **_, + ) -> None: + super().__init__( + is_anonymous=is_anonymous, + is_anonymous_vpn=is_anonymous_vpn, + is_hosting_provider=is_hosting_provider, + is_public_proxy=is_public_proxy, + is_residential_proxy=is_residential_proxy, + is_tor_exit_node=is_tor_exit_node, + ip_address=ip_address, + network=network, + prefix_len=prefix_len, + ) + self.anonymizer_confidence = anonymizer_confidence + if network_last_seen is not None: + self.network_last_seen = datetime.date.fromisoformat(network_last_seen) + self.provider_name = provider_name + + +class ASN(SimpleModel): + """Model class for the GeoLite2 ASN.""" + + autonomous_system_number: int | None + """The autonomous system number associated with the IP address.""" + + autonomous_system_organization: str | None + """The organization associated with the registered autonomous system number + for the IP address. + """ + + def __init__( + self, + ip_address: IPAddress, + *, + autonomous_system_number: int | None = None, + autonomous_system_organization: str | None = None, + network: str | None = None, + prefix_len: int | None = None, + **_, + ) -> None: + super().__init__(ip_address, network, prefix_len) + self.autonomous_system_number = autonomous_system_number + self.autonomous_system_organization = autonomous_system_organization + + +class ConnectionType(SimpleModel): + """Model class for the GeoIP2 Connection-Type.""" + + connection_type: str | None + """The connection type may take the following values: + + - Dialup + - Cable/DSL + - Corporate + - Cellular + - Satellite + + Additional values may be added in the future. + """ + + def __init__( + self, + ip_address: IPAddress, + *, + connection_type: str | None = None, + network: str | None = None, + prefix_len: int | None = None, + **_, + ) -> None: + super().__init__(ip_address, network, prefix_len) + self.connection_type = connection_type + + +class Domain(SimpleModel): + """Model class for the GeoIP2 Domain.""" + + domain: str | None + """The domain associated with the IP address.""" + + def __init__( + self, + ip_address: IPAddress, + *, + domain: str | None = None, + network: str | None = None, + prefix_len: int | None = None, + **_, + ) -> None: + super().__init__(ip_address, network, prefix_len) + self.domain = domain + + +class ISP(ASN): + """Model class for the GeoIP2 ISP.""" + + isp: str | None + """The name of the ISP associated with the IP address.""" + + mobile_country_code: str | None + """The `mobile country code (MCC) + `_ associated with the + IP address and ISP. + """ + + mobile_network_code: str | None + """The `mobile network code (MNC) + `_ associated with the + IP address and ISP. + """ + + organization: str | None + """The name of the organization associated with the IP address.""" + + def __init__( + self, + ip_address: IPAddress, + *, + autonomous_system_number: int | None = None, + autonomous_system_organization: str | None = None, + isp: str | None = None, + mobile_country_code: str | None = None, + mobile_network_code: str | None = None, + organization: str | None = None, + network: str | None = None, + prefix_len: int | None = None, + **_, + ) -> None: + super().__init__( + autonomous_system_number=autonomous_system_number, + autonomous_system_organization=autonomous_system_organization, + ip_address=ip_address, + network=network, + prefix_len=prefix_len, + ) + self.isp = isp + self.mobile_country_code = mobile_country_code + self.mobile_network_code = mobile_network_code + self.organization = organization diff --git a/geoip2/py.typed b/src/geoip2/py.typed similarity index 100% rename from geoip2/py.typed rename to src/geoip2/py.typed diff --git a/src/geoip2/records.py b/src/geoip2/records.py new file mode 100644 index 0000000..1405b69 --- /dev/null +++ b/src/geoip2/records.py @@ -0,0 +1,652 @@ +"""Record classes used within the response models.""" + +from __future__ import annotations + +import ipaddress +from abc import ABCMeta +from ipaddress import IPv4Address, IPv6Address +from typing import TYPE_CHECKING + +from geoip2._internal import Model + +if TYPE_CHECKING: + from collections.abc import Sequence + + from typing_extensions import Self + + from geoip2.types import IPAddress + + +class Record(Model, metaclass=ABCMeta): + """All records are subclasses of the abstract class ``Record``.""" + + def __repr__(self) -> str: + args = ", ".join(f"{k}={v!r}" for k, v in self.to_dict().items()) + return f"{self.__module__}.{self.__class__.__name__}({args})" + + +class PlaceRecord(Record, metaclass=ABCMeta): + """All records with :py:attr:`names` subclass :py:class:`PlaceRecord`.""" + + names: dict[str, str] + """A dictionary where the keys are locale codes and the values are names.""" + _locales: Sequence[str] + + def __init__( + self, + locales: Sequence[str] | None, + names: dict[str, str] | None, + ) -> None: + if locales is None: + locales = ["en"] + self._locales = locales + if names is None: + names = {} + self.names = names + + @property + def name(self) -> str | None: + """The name based on the locales list passed to the constructor.""" + return next((self.names.get(x) for x in self._locales if x in self.names), None) + + +class City(PlaceRecord): + """Contains data for the city record associated with an IP address. + + This class contains the city-level data associated with an IP address. + + This record is returned by ``city``, ``enterprise``, and ``insights``. + """ + + confidence: int | None + """A value from 0-100 indicating MaxMind's + confidence that the city is correct. This attribute is only available + from the Insights end point and the Enterprise database. + """ + geoname_id: int | None + """The GeoName ID for the city.""" + + def __init__( + self, + locales: Sequence[str] | None, + *, + confidence: int | None = None, + geoname_id: int | None = None, + names: dict[str, str] | None = None, + **_, + ) -> None: + self.confidence = confidence + self.geoname_id = geoname_id + super().__init__(locales, names) + + +class Continent(PlaceRecord): + """Contains data for the continent record associated with an IP address. + + This class contains the continent-level data associated with an IP + address. + """ + + code: str | None + """A two character continent code like "NA" (North America) + or "OC" (Oceania). + """ + geoname_id: int | None + """The GeoName ID for the continent.""" + + def __init__( + self, + locales: Sequence[str] | None, + *, + code: str | None = None, + geoname_id: int | None = None, + names: dict[str, str] | None = None, + **_, + ) -> None: + self.code = code + self.geoname_id = geoname_id + super().__init__(locales, names) + + +class Country(PlaceRecord): + """Contains data for the country record associated with an IP address. + + This class contains the country-level data associated with an IP address. + """ + + confidence: int | None + """A value from 0-100 indicating MaxMind's confidence that + the country is correct. This attribute is only available from the + Insights end point and the Enterprise database. + """ + geoname_id: int | None + """The GeoName ID for the country.""" + is_in_european_union: bool + """This is true if the country is a member state of the European Union.""" + iso_code: str | None + """The two-character `ISO 3166-1 + `_ alpha code for the + country. + """ + + def __init__( + self, + locales: Sequence[str] | None, + *, + confidence: int | None = None, + geoname_id: int | None = None, + is_in_european_union: bool = False, + iso_code: str | None = None, + names: dict[str, str] | None = None, + **_, + ) -> None: + self.confidence = confidence + self.geoname_id = geoname_id + self.is_in_european_union = is_in_european_union + self.iso_code = iso_code + super().__init__(locales, names) + + +class RepresentedCountry(Country): + """Contains data for the represented country associated with an IP address. + + This class contains the country-level data associated with an IP address + for the IP's represented country. The represented country is the country + represented by something like a military base. + """ + + type: str | None + """A string indicating the type of entity that is representing the + country. Currently we only return ``military`` but this could expand to + include other types in the future. + """ + + def __init__( + self, + locales: Sequence[str] | None, + *, + confidence: int | None = None, + geoname_id: int | None = None, + is_in_european_union: bool = False, + iso_code: str | None = None, + names: dict[str, str] | None = None, + type: str | None = None, # noqa: A002 + **_, + ) -> None: + self.type = type + super().__init__( + locales, + confidence=confidence, + geoname_id=geoname_id, + is_in_european_union=is_in_european_union, + iso_code=iso_code, + names=names, + ) + + +class Location(Record): + """Contains data for the location record associated with an IP address. + + This class contains the location data associated with an IP address. + + This record is returned by ``city``, ``enterprise``, and ``insights``. + """ + + average_income: int | None + """The average income in US dollars associated with the requested IP + address. This attribute is only available from the Insights end point. + """ + accuracy_radius: int | None + """The approximate accuracy radius in kilometers around the latitude and + longitude for the IP address. This is the radius where we have a 67% + confidence that the device using the IP address resides within the + circle centered at the latitude and longitude with the provided radius. + """ + latitude: float | None + """The approximate latitude of the location associated with the IP + address. This value is not precise and should not be used to identify a + particular address or household. + """ + longitude: float | None + """The approximate longitude of the location associated with the IP + address. This value is not precise and should not be used to identify a + particular address or household. + """ + metro_code: int | None + """The metro code is a no-longer-maintained code for targeting + advertisements in Google. + + .. deprecated:: 4.9.0 + """ + population_density: int | None + """The estimated population per square kilometer associated with the IP + address. This attribute is only available from the Insights end point. + """ + time_zone: str | None + """The time zone associated with location, as specified by the `IANA Time + Zone Database `_, e.g., + "America/New_York". + """ + + def __init__( + self, + *, + average_income: int | None = None, + accuracy_radius: int | None = None, + latitude: float | None = None, + longitude: float | None = None, + metro_code: int | None = None, + population_density: int | None = None, + time_zone: str | None = None, + **_, + ) -> None: + self.average_income = average_income + self.accuracy_radius = accuracy_radius + self.latitude = latitude + self.longitude = longitude + self.metro_code = metro_code + self.population_density = population_density + self.time_zone = time_zone + + +class MaxMind(Record): + """Contains data related to your MaxMind account.""" + + queries_remaining: int | None + """The number of remaining queries you have for the end point you are + calling. + """ + + def __init__(self, *, queries_remaining: int | None = None, **_) -> None: + self.queries_remaining = queries_remaining + + +class Postal(Record): + """Contains data for the postal record associated with an IP address. + + This class contains the postal data associated with an IP address. + + This attribute is returned by ``city``, ``enterprise``, and ``insights``. + """ + + code: str | None + """The postal code of the location. Postal codes are not available for + all countries. In some countries, this will only contain part of the + postal code. + """ + confidence: int | None + """A value from 0-100 indicating MaxMind's confidence that the postal code + is correct. This attribute is only available from the Insights end point + and the Enterprise database. + """ + + def __init__( + self, + *, + code: str | None = None, + confidence: int | None = None, + **_, + ) -> None: + self.code = code + self.confidence = confidence + + +class Subdivision(PlaceRecord): + """Contains data for the subdivisions associated with an IP address. + + This class contains the subdivision data associated with an IP address. + + This attribute is returned by ``city``, ``enterprise``, and ``insights``. + """ + + confidence: int | None + """This is a value from 0-100 indicating MaxMind's confidence that the + subdivision is correct. This attribute is only available from the Insights + end point and the Enterprise database. + """ + geoname_id: int | None + """This is a GeoName ID for the subdivision.""" + iso_code: str | None + """This is a string up to three characters long contain the subdivision + portion of the `ISO 3166-2 code `_. + """ + + def __init__( + self, + locales: Sequence[str] | None, + *, + confidence: int | None = None, + geoname_id: int | None = None, + iso_code: str | None = None, + names: dict[str, str] | None = None, + **_, + ) -> None: + self.confidence = confidence + self.geoname_id = geoname_id + self.iso_code = iso_code + super().__init__(locales, names) + + +class Subdivisions(tuple): # noqa: SLOT001 + """A tuple-like collection of subdivisions associated with an IP address. + + This class contains the subdivisions of the country associated with the + IP address from largest to smallest. + + For instance, the response for Oxford in the United Kingdom would have + England as the first element and Oxfordshire as the second element. + + This attribute is returned by ``city``, ``enterprise``, and ``insights``. + """ + + def __new__( + cls: type[Self], + locales: Sequence[str] | None, + *subdivisions: dict, + ) -> Self: + """Create a new Subdivisions instance. + + This method constructs the tuple with Subdivision objects created + from the provided dictionaries. + + Arguments: + cls: The class to instantiate (Subdivisions). + locales: A sequence of locale strings (e.g., ['en', 'fr']) + or None, passed to each Subdivision object. + *subdivisions: A variable number of dictionaries, where each + dictionary contains the data for a single :py:class:`Subdivision` + object (e.g., name, iso_code). + + Returns: + A new instance of Subdivisions containing :py:class:`Subdivision` objects. + + """ + subobjs = tuple(Subdivision(locales, **x) for x in subdivisions) + return super().__new__(cls, subobjs) + + def __init__( + self, + locales: Sequence[str] | None, + *_: dict, + ) -> None: + """Initialize the Subdivisions instance.""" + self._locales = locales + super().__init__() + + @property + def most_specific(self) -> Subdivision: + """The most specific (smallest) subdivision available. + + If there are no :py:class:`Subdivision` objects for the response, + this returns an empty :py:class:`Subdivision`. + """ + try: + return self[-1] + except IndexError: + return Subdivision(self._locales) + + +class Traits(Record): + """Contains data for the traits record associated with an IP address. + + This class contains the traits data associated with an IP address. + """ + + autonomous_system_number: int | None + """The `autonomous system + number `_ + associated with the IP address. This attribute is only available from + the City Plus and Insights web services and the Enterprise database. + """ + autonomous_system_organization: str | None + """The organization associated with the registered `autonomous system + number `_ for + the IP address. This attribute is only available from the City Plus and + Insights web service end points and the Enterprise database. + """ + connection_type: str | None + """The connection type may take the following values: + + - Dialup + - Cable/DSL + - Corporate + - Cellular + - Satellite + + Additional values may be added in the future. + + This attribute is only available from the City Plus and Insights web + service end points and the Enterprise database. + """ + domain: str | None + """The second level domain associated with the + IP address. This will be something like "example.com" or + "example.co.uk", not "foo.example.com". This attribute is only available + from the City Plus and Insights web service end points and the + Enterprise database. + """ + _ip_address: IPAddress | None + is_anonymous: bool + """This is true if the IP address belongs to any sort of anonymous network. + This attribute is only available from Insights. + """ + is_anonymous_proxy: bool + """This is true if the IP is an anonymous proxy. + + .. deprecated:: 2.2.0 + Use our `GeoIP2 Anonymous IP database + `_ + instead. + """ + is_anonymous_vpn: bool + """This is true if the IP address is registered to an anonymous VPN + provider. + + If a VPN provider does not register subnets under names associated with + them, we will likely only flag their IP ranges using the + ``is_hosting_provider`` attribute. + + This attribute is only available from Insights. + """ + is_anycast: bool + """This returns true if the IP address belongs to an + `anycast network `_. + This is available for the GeoIP2 Country, City Plus, and Insights + web services and the GeoIP2 Country, City, and Enterprise databases. + """ + is_hosting_provider: bool + """This is true if the IP address belongs to a hosting or VPN provider + (see description of ``is_anonymous_vpn`` attribute). + This attribute is only available from Insights. + """ + is_legitimate_proxy: bool + """This attribute is true if MaxMind believes this IP address to be a + legitimate proxy, such as an internal VPN used by a corporation. This + attribute is only available in the Enterprise database. + """ + is_public_proxy: bool + """This is true if the IP address belongs to a public proxy. This attribute + is only available from Insights. + """ + is_residential_proxy: bool + """This is true if the IP address is on a suspected anonymizing network + and belongs to a residential ISP. This attribute is only available from + Insights. + """ + is_satellite_provider: bool + """This is true if the IP address is from a satellite provider that + provides service to multiple countries. + + .. deprecated:: 2.2.0 + Due to the increased coverage by mobile carriers, very few + satellite providers now serve multiple countries. As a result, the + output does not provide sufficiently relevant data for us to maintain + it. + """ + is_tor_exit_node: bool + """This is true if the IP address is a Tor exit node. This attribute is + only available from Insights. + """ + isp: str | None + """The name of the ISP associated with the IP address. This attribute is + only available from the City Plus and Insights web services and the + Enterprise database. + """ + mobile_country_code: str | None + """The `mobile country code (MCC) + `_ associated with the + IP address and ISP. This attribute is available from the City Plus and + Insights web services and the Enterprise database. + """ + mobile_network_code: str | None + """The `mobile network code (MNC) + `_ associated with the + IP address and ISP. This attribute is available from the City Plus and + Insights web services and the Enterprise database. + """ + organization: str | None + """The name of the organization associated with the IP address. This + attribute is only available from the City Plus and Insights web services + and the Enterprise database. + """ + static_ip_score: float | None + """An indicator of how static or dynamic an IP address is. The value ranges + from 0 to 99.99 with higher values meaning a greater static association. + For example, many IP addresses with a user_type of cellular have a + lifetime under one. Static Cable/DSL IPs typically have a lifetime above + thirty. + + This indicator can be useful for deciding whether an IP address represents + the same user over time. This attribute is only available from + Insights. + """ + user_count: int | None + """The estimated number of users sharing the IP/network during the past 24 + hours. For IPv4, the count is for the individual IP. For IPv6, the count + is for the /64 network. This attribute is only available from + Insights. + """ + user_type: str | None + """The user type associated with the IP + address. This can be one of the following values: + + * business + * cafe + * cellular + * college + * consumer_privacy_network + * content_delivery_network + * dialup + * government + * hosting + * library + * military + * residential + * router + * school + * search_engine_spider + * traveler + + This attribute is only available from the Insights end point and the + Enterprise database. + """ + _network: ipaddress.IPv4Network | ipaddress.IPv6Network | None + _prefix_len: int | None + + def __init__( + self, + *, + autonomous_system_number: int | None = None, + autonomous_system_organization: str | None = None, + connection_type: str | None = None, + domain: str | None = None, + is_anonymous: bool = False, + is_anonymous_proxy: bool = False, + is_anonymous_vpn: bool = False, + is_hosting_provider: bool = False, + is_legitimate_proxy: bool = False, + is_public_proxy: bool = False, + is_residential_proxy: bool = False, + is_satellite_provider: bool = False, + is_tor_exit_node: bool = False, + isp: str | None = None, + ip_address: str | None = None, + network: str | None = None, + organization: str | None = None, + prefix_len: int | None = None, + static_ip_score: float | None = None, + user_count: int | None = None, + user_type: str | None = None, + mobile_country_code: str | None = None, + mobile_network_code: str | None = None, + is_anycast: bool = False, + **_, + ) -> None: + self.autonomous_system_number = autonomous_system_number + self.autonomous_system_organization = autonomous_system_organization + self.connection_type = connection_type + self.domain = domain + self.is_anonymous = is_anonymous + self.is_anonymous_proxy = is_anonymous_proxy + self.is_anonymous_vpn = is_anonymous_vpn + self.is_anycast = is_anycast + self.is_hosting_provider = is_hosting_provider + self.is_legitimate_proxy = is_legitimate_proxy + self.is_public_proxy = is_public_proxy + self.is_residential_proxy = is_residential_proxy + self.is_satellite_provider = is_satellite_provider + self.is_tor_exit_node = is_tor_exit_node + self.isp = isp + self.mobile_country_code = mobile_country_code + self.mobile_network_code = mobile_network_code + self.organization = organization + self.static_ip_score = static_ip_score + self.user_type = user_type + self.user_count = user_count + self._ip_address = ip_address + if network is None: + self._network = None + else: + self._network = ipaddress.ip_network(network, strict=False) + # We don't construct the network using prefix_len here as that is + # for database lookups. Customers using the database tend to be + # much more performance sensitive than web service users. + self._prefix_len = prefix_len + + @property + def ip_address(self) -> IPv4Address | IPv6Address | None: + """The IP address that the data in the model is for. + + If you performed a "me" lookup against the web service, this will be + the externally routable IP address for the system the code is running + on. If the system is behind a NAT, this may differ from the IP address + locally assigned to it. + """ + ip_address = self._ip_address + if ip_address is None: + return None + + if not isinstance(ip_address, (IPv4Address, IPv6Address)): + ip_address = ipaddress.ip_address(ip_address) + self._ip_address = ip_address + return ip_address + + @property + def network(self) -> ipaddress.IPv4Network | ipaddress.IPv6Network | None: + """The network associated with the record. + + In particular, this is the largest network where all of the fields besides + ip_address have the same value. + """ + # This code is duplicated for performance reasons + network = self._network + if network is not None: + return network + + ip_address = self.ip_address + prefix_len = self._prefix_len + if ip_address is None or prefix_len is None: + return None + network = ipaddress.ip_network(f"{ip_address}/{prefix_len}", strict=False) + self._network = network + return network diff --git a/geoip2/types.py b/src/geoip2/types.py similarity index 76% rename from geoip2/types.py rename to src/geoip2/types.py index ba6d2b5..d86f1c0 100644 --- a/geoip2/types.py +++ b/src/geoip2/types.py @@ -1,4 +1,4 @@ -"""Provides types used internally""" +"""Provides types used internally.""" from ipaddress import IPv4Address, IPv6Address from typing import Union diff --git a/geoip2/webservice.py b/src/geoip2/webservice.py similarity index 68% rename from geoip2/webservice.py rename to src/geoip2/webservice.py index c085df7..0316116 100644 --- a/geoip2/webservice.py +++ b/src/geoip2/webservice.py @@ -1,33 +1,31 @@ -""" -============================ -WebServices Client API -============================ +"""Client for GeoIP2 and GeoLite2 web services. -This class provides a client API for all the GeoIP2 Precision web service end -points. The end points are Country, City, and Insights. Each end point returns -a different set of data about an IP address, with Country returning the least +The web services are Country, City Plus, and Insights. Each service returns a +different set of data about an IP address, with Country returning the least data and Insights the most. -Each web service end point is represented by a different model class, and -these model classes in turn contain multiple record classes. The record -classes have attributes which contain data about the IP address. +Each service is represented by a different model class, and these model +classes in turn contain multiple record classes. The record classes have +attributes which contain data about the IP address. -If the web service does not return a particular piece of data for an IP -address, the associated attribute is not populated. +If the service does not return a particular piece of data for an IP address, +the associated attribute is not populated. -The web service may not return any information for an entire record, in which +The service may not return any information for an entire record, in which case all of the attributes for that record class will be empty. SSL --- -Requests to the GeoIP2 Precision web service are always made with SSL. +Requests to the web service are always made with SSL. """ +from __future__ import annotations + import ipaddress import json -from typing import Any, cast, List, Optional, Type, Union +from typing import TYPE_CHECKING, cast import aiohttp import aiohttp.http @@ -45,8 +43,14 @@ OutOfQueriesError, PermissionRequiredError, ) -from geoip2.models import City, Country, Insights -from geoip2.types import IPAddress + +if TYPE_CHECKING: + from collections.abc import Sequence + + from typing_extensions import Self + + from geoip2.models import City, Country, Insights + from geoip2.types import IPAddress _AIOHTTP_UA = ( f"GeoIP2-Python-Client/{geoip2.__version__} {aiohttp.http.SERVER_SOFTWARE}" @@ -57,11 +61,13 @@ ) -class BaseClient: # pylint: disable=missing-class-docstring, too-few-public-methods +class BaseClient: + """Base class for AsyncClient and Client.""" + _account_id: str _host: str _license_key: str - _locales: List[str] + _locales: Sequence[str] _timeout: float def __init__( @@ -69,11 +75,10 @@ def __init__( account_id: int, license_key: str, host: str, - locales: Optional[List[str]], + locales: Sequence[str] | None, timeout: float, ) -> None: """Construct a Client.""" - # pylint: disable=too-many-arguments if locales is None: locales = ["en"] @@ -93,7 +98,7 @@ def _uri(self, path: str, ip_address: IPAddress) -> str: return "/".join([self._base_uri, path, str(ip_address)]) @staticmethod - def _handle_success(body: str, uri: str) -> Any: + def _handle_success(body: str, uri: str) -> dict: try: return json.loads(body) except ValueError as ex: @@ -106,7 +111,11 @@ def _handle_success(body: str, uri: str) -> Any: ) from ex def _exception_for_error( - self, status: int, content_type: str, body: str, uri: str + self, + status: int, + content_type: str, + body: str, + uri: str, ) -> GeoIP2Error: if 400 <= status < 500: return self._exception_for_4xx_status(status, content_type, body, uri) @@ -115,7 +124,11 @@ def _exception_for_error( return self._exception_for_non_200_status(status, uri, body) def _exception_for_4xx_status( - self, status: int, content_type: str, body: str, uri: str + self, + status: int, + content_type: str, + body: str, + uri: str, ) -> GeoIP2Error: if not body: return HTTPError( @@ -135,35 +148,42 @@ def _exception_for_4xx_status( decoded_body = json.loads(body) except ValueError as ex: return HTTPError( - f"Received a {status} error for {uri} but it did not include " - + "the expected JSON body: " - + ", ".join(ex.args), + ( + f"Received a {status} error for {uri} but it did not include " + f"the expected JSON body: {', '.join(ex.args)}" + ), status, uri, body, ) - else: - if "code" in decoded_body and "error" in decoded_body: - return self._exception_for_web_service_error( - decoded_body.get("error"), decoded_body.get("code"), status, uri - ) - return HTTPError( - "Response contains JSON but it does not specify code or error keys", + + if "code" in decoded_body and "error" in decoded_body: + return self._exception_for_web_service_error( + decoded_body.get("error"), + decoded_body.get("code"), status, uri, - body, ) + return HTTPError( + "Response contains JSON but it does not specify code or error keys", + status, + uri, + body, + ) @staticmethod def _exception_for_web_service_error( - message: str, code: str, status: int, uri: str - ) -> Union[ - AuthenticationError, - AddressNotFoundError, - PermissionRequiredError, - OutOfQueriesError, - InvalidRequestError, - ]: + message: str, + code: str, + status: int, + uri: str, + ) -> ( + AuthenticationError + | AddressNotFoundError + | PermissionRequiredError + | OutOfQueriesError + | InvalidRequestError + ): if code in ("IP_ADDRESS_NOT_FOUND", "IP_ADDRESS_RESERVED"): return AddressNotFoundError(message) if code in ( @@ -184,7 +204,9 @@ def _exception_for_web_service_error( @staticmethod def _exception_for_5xx_status( - status: int, uri: str, body: Optional[str] + status: int, + uri: str, + body: str | None, ) -> HTTPError: return HTTPError( f"Received a server error ({status}) for {uri}", @@ -195,7 +217,9 @@ def _exception_for_5xx_status( @staticmethod def _exception_for_non_200_status( - status: int, uri: str, body: Optional[str] + status: int, + uri: str, + body: str | None, ) -> HTTPError: return HTTPError( f"Received a very surprising HTTP status ({status}) for {uri}", @@ -219,8 +243,11 @@ class AsyncClient(BaseClient): The following keyword arguments are also accepted: :param host: The hostname to make a request against. This defaults to - "geoip.maxmind.com". In most cases, you should not need to set this - explicitly. + "geoip.maxmind.com". To use the GeoLite2 web service instead of the + GeoIP2 web service, set this to "geolite.info". To use the Sandbox + GeoIP2 web service instead of the production GeoIP2 web service, set + this to "sandbox.maxmind.com". The sandbox allows you to experiment + with the API without affecting your production data. :param locales: This is list of locale codes. This argument will be passed on to record classes to use when their name properties are called. The default value is ['en']. @@ -245,22 +272,28 @@ class AsyncClient(BaseClient): * pt-BR -- Brazilian Portuguese * ru -- Russian * zh-CN -- Simplified Chinese. - :param timeout: The timeout in seconts to use when waiting on the request. + :param timeout: The timeout in seconds to use when waiting on the request. This sets both the connect timeout and the read timeout. The default is 60. + :param proxy: The URL of an HTTP proxy to use. It may optionally include + a basic auth username and password, e.g., + ``http://username:password@host:port``. """ _existing_session: aiohttp.ClientSession + _proxy: str | None - def __init__( # pylint: disable=too-many-arguments + def __init__( # noqa: PLR0913 self, account_id: int, license_key: str, host: str = "geoip.maxmind.com", - locales: Optional[List[str]] = None, + locales: Sequence[str] | None = None, timeout: float = 60, + proxy: str | None = None, ) -> None: + """Initialize AsyncClient.""" super().__init__( account_id, license_key, @@ -268,9 +301,10 @@ def __init__( # pylint: disable=too-many-arguments locales, timeout, ) + self._proxy = proxy async def city(self, ip_address: IPAddress = "me") -> City: - """Call GeoIP2 Precision City endpoint with the specified IP. + """Call City Plus endpoint with the specified IP. :param ip_address: IPv4 or IPv6 address as a string. If no address is provided, the address that the web service is @@ -280,7 +314,8 @@ async def city(self, ip_address: IPAddress = "me") -> City: """ return cast( - City, await self._response_for("city", geoip2.models.City, ip_address) + "City", + await self._response_for("city", geoip2.models.City, ip_address), ) async def country(self, ip_address: IPAddress = "me") -> Country: @@ -294,12 +329,15 @@ async def country(self, ip_address: IPAddress = "me") -> Country: """ return cast( - Country, + "Country", await self._response_for("country", geoip2.models.Country, ip_address), ) async def insights(self, ip_address: IPAddress = "me") -> Insights: - """Call the GeoIP2 Precision: Insights endpoint with the specified IP. + """Call the Insights endpoint with the specified IP. + + Insights is only supported by the GeoIP2 web service. The GeoLite2 web + service does not support it. :param ip_address: IPv4 or IPv6 address as a string. If no address is provided, the address that the web service is called from will @@ -309,7 +347,7 @@ async def insights(self, ip_address: IPAddress = "me") -> Insights: """ return cast( - Insights, + "Insights", await self._response_for("insights", geoip2.models.Insights, ip_address), ) @@ -326,32 +364,37 @@ async def _session(self) -> aiohttp.ClientSession: async def _response_for( self, path: str, - model_class: Union[Type[Insights], Type[City], Type[Country]], + model_class: type[City | Country | Insights], ip_address: IPAddress, - ) -> Union[Country, City, Insights]: + ) -> Country | City | Insights: uri = self._uri(path, ip_address) session = await self._session() - async with await session.get(uri) as response: + async with await session.get(uri, proxy=self._proxy) as response: status = response.status content_type = response.content_type body = await response.text() if status != 200: raise self._exception_for_error(status, content_type, body, uri) decoded_body = self._handle_success(body, uri) - return model_class(decoded_body, locales=self._locales) + return model_class(self._locales, **decoded_body) - async def close(self): - """Close underlying session + async def close(self) -> None: + """Close underlying session. This will close the session and any associated connections. """ if hasattr(self, "_existing_session"): await self._existing_session.close() - async def __aenter__(self) -> "AsyncClient": + async def __aenter__(self) -> Self: return self - async def __aexit__(self, exc_type: None, exc_value: None, traceback: None) -> None: + async def __aexit__( + self, + exc_type: object, + exc_value: object, + traceback: object, + ) -> None: await self.close() @@ -369,8 +412,11 @@ class Client(BaseClient): The following keyword arguments are also accepted: :param host: The hostname to make a request against. This defaults to - "geoip.maxmind.com". In most cases, you should not need to set this - explicitly. + "geoip.maxmind.com". To use the GeoLite2 web service instead of the + GeoIP2 web service, set this to "geolite.info". To use the Sandbox + GeoIP2 web service instead of the production GeoIP2 web service, set + this to "sandbox.maxmind.com". The sandbox allows you to experiment + with the API without affecting your production data. :param locales: This is list of locale codes. This argument will be passed on to record classes to use when their name properties are called. The default value is ['en']. @@ -395,30 +441,41 @@ class Client(BaseClient): * pt-BR -- Brazilian Portuguese * ru -- Russian * zh-CN -- Simplified Chinese. - :param timeout: The timeout in seconts to use when waiting on the request. + :param timeout: The timeout in seconds to use when waiting on the request. This sets both the connect timeout and the read timeout. The default is 60. + :param proxy: The URL of an HTTP proxy to use. It may optionally include + a basic auth username and password, e.g., + ``http://username:password@host:port``. + """ _session: requests.Session + _proxies: dict[str, str] | None - def __init__( # pylint: disable=too-many-arguments + def __init__( # noqa: PLR0913 self, account_id: int, license_key: str, host: str = "geoip.maxmind.com", - locales: Optional[List[str]] = None, + locales: Sequence[str] | None = None, timeout: float = 60, + proxy: str | None = None, ) -> None: + """Initialize Client.""" super().__init__(account_id, license_key, host, locales, timeout) self._session = requests.Session() self._session.auth = (self._account_id, self._license_key) self._session.headers["Accept"] = "application/json" self._session.headers["User-Agent"] = _REQUEST_UA + if proxy is None: + self._proxies = None + else: + self._proxies = {"https": proxy} def city(self, ip_address: IPAddress = "me") -> City: - """Call GeoIP2 Precision City endpoint with the specified IP. + """Call City Plus endpoint with the specified IP. :param ip_address: IPv4 or IPv6 address as a string. If no address is provided, the address that the web service is @@ -427,7 +484,7 @@ def city(self, ip_address: IPAddress = "me") -> City: :returns: :py:class:`geoip2.models.City` object """ - return cast(City, self._response_for("city", geoip2.models.City, ip_address)) + return cast("City", self._response_for("city", geoip2.models.City, ip_address)) def country(self, ip_address: IPAddress = "me") -> Country: """Call the GeoIP2 Country endpoint with the specified IP. @@ -440,11 +497,15 @@ def country(self, ip_address: IPAddress = "me") -> Country: """ return cast( - Country, self._response_for("country", geoip2.models.Country, ip_address) + "Country", + self._response_for("country", geoip2.models.Country, ip_address), ) def insights(self, ip_address: IPAddress = "me") -> Insights: - """Call the GeoIP2 Precision: Insights endpoint with the specified IP. + """Call the Insights endpoint with the specified IP. + + Insights is only supported by the GeoIP2 web service. The GeoLite2 web + service does not support it. :param ip_address: IPv4 or IPv6 address as a string. If no address is provided, the address that the web service is called from will @@ -454,34 +515,35 @@ def insights(self, ip_address: IPAddress = "me") -> Insights: """ return cast( - Insights, self._response_for("insights", geoip2.models.Insights, ip_address) + "Insights", + self._response_for("insights", geoip2.models.Insights, ip_address), ) def _response_for( self, path: str, - model_class: Union[Type[Insights], Type[City], Type[Country]], + model_class: type[City | Country | Insights], ip_address: IPAddress, - ) -> Union[Country, City, Insights]: + ) -> Country | City | Insights: uri = self._uri(path, ip_address) - response = self._session.get(uri, timeout=self._timeout) + response = self._session.get(uri, proxies=self._proxies, timeout=self._timeout) status = response.status_code content_type = response.headers["Content-Type"] body = response.text if status != 200: raise self._exception_for_error(status, content_type, body, uri) decoded_body = self._handle_success(body, uri) - return model_class(decoded_body, locales=self._locales) + return model_class(self._locales, **decoded_body) - def close(self): - """Close underlying session + def close(self) -> None: + """Close underlying session. This will close the session and any associated connections. """ self._session.close() - def __enter__(self) -> "Client": + def __enter__(self) -> Self: return self - def __exit__(self, exc_type: None, exc_value: None, traceback: None) -> None: + def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None: self.close() diff --git a/tests/data b/tests/data index cbaa463..b5ff09e 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit cbaa463dc6950ababbf678ca85fb3833b81c76d3 +Subproject commit b5ff09ebb67d959ae68118a058fe344a6994b046 diff --git a/tests/database_test.py b/tests/database_test.py index 4084ad4..2c831df 100644 --- a/tests/database_test.py +++ b/tests/database_test.py @@ -1,24 +1,23 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals - +import datetime import ipaddress import sys import unittest +from unittest.mock import MagicMock, patch sys.path.append("..") -import geoip2.database import maxminddb +import geoip2.database +import geoip2.errors + try: import maxminddb.extension except ImportError: - maxminddb.extension = None # type: ignore + maxminddb.extension = None # type: ignore[assignment] -class BaseTestReader(unittest.TestCase): +class TestReader(unittest.TestCase): def test_language_list(self) -> None: reader = geoip2.database.Reader( "tests/data/test-data/GeoIP2-Country-Test.mmdb", @@ -33,16 +32,28 @@ def test_unknown_address(self) -> None: reader = geoip2.database.Reader("tests/data/test-data/GeoIP2-City-Test.mmdb") with self.assertRaisesRegex( geoip2.errors.AddressNotFoundError, - "The address 10.10.10.10 is not in the " "database.", + "The address 10.10.10.10 is not in the database.", ): reader.city("10.10.10.10") reader.close() + def test_unknown_address_network(self) -> None: + reader = geoip2.database.Reader("tests/data/test-data/GeoIP2-City-Test.mmdb") + try: + reader.city("10.10.10.10") + self.fail("Expected AddressNotFoundError") + except geoip2.errors.AddressNotFoundError as e: + self.assertEqual(e.network, ipaddress.ip_network("10.0.0.0/8")) + except Exception as e: # noqa: BLE001 + self.fail(f"Expected AddressNotFoundError, got {type(e)}: {e!s}") + finally: + reader.close() + def test_wrong_database(self) -> None: reader = geoip2.database.Reader("tests/data/test-data/GeoIP2-City-Test.mmdb") with self.assertRaisesRegex( TypeError, - "The country method cannot be used with " "the GeoIP2-City database", + "The country method cannot be used with the GeoIP2-City database", ): reader.country("1.1.1.1") reader.close() @@ -50,14 +61,15 @@ def test_wrong_database(self) -> None: def test_invalid_address(self) -> None: reader = geoip2.database.Reader("tests/data/test-data/GeoIP2-City-Test.mmdb") with self.assertRaisesRegex( - ValueError, "u?'invalid' does not appear to be an " "IPv4 or IPv6 address" + ValueError, + "u?'invalid' does not appear to be an IPv4 or IPv6 address", ): reader.city("invalid") reader.close() def test_anonymous_ip(self) -> None: reader = geoip2.database.Reader( - "tests/data/test-data/GeoIP2-Anonymous-IP-Test.mmdb" + "tests/data/test-data/GeoIP2-Anonymous-IP-Test.mmdb", ) ip_address = "1.2.0.1" @@ -68,13 +80,33 @@ def test_anonymous_ip(self) -> None: self.assertEqual(record.is_public_proxy, False) self.assertEqual(record.is_residential_proxy, False) self.assertEqual(record.is_tor_exit_node, False) - self.assertEqual(record.ip_address, ip_address) + self.assertEqual(record.ip_address, ipaddress.ip_address(ip_address)) self.assertEqual(record.network, ipaddress.ip_network("1.2.0.0/16")) reader.close() + def test_anonymous_plus(self) -> None: + with geoip2.database.Reader( + "tests/data/test-data/GeoIP-Anonymous-Plus-Test.mmdb", + ) as reader: + ip_address = "1.2.0.1" + + record = reader.anonymous_plus(ip_address) + + self.assertEqual(record.anonymizer_confidence, 30) + self.assertEqual(record.is_anonymous, True) + self.assertEqual(record.is_anonymous_vpn, True) + self.assertEqual(record.is_hosting_provider, False) + self.assertEqual(record.is_public_proxy, False) + self.assertEqual(record.is_residential_proxy, False) + self.assertEqual(record.is_tor_exit_node, False) + self.assertEqual(record.ip_address, ipaddress.ip_address(ip_address)) + self.assertEqual(record.network, ipaddress.ip_network("1.2.0.1/32")) + self.assertEqual(record.network_last_seen, datetime.date(2025, 4, 14)) + self.assertEqual(record.provider_name, "foo") + def test_anonymous_ip_all_set(self) -> None: reader = geoip2.database.Reader( - "tests/data/test-data/GeoIP2-Anonymous-IP-Test.mmdb" + "tests/data/test-data/GeoIP2-Anonymous-IP-Test.mmdb", ) ip_address = "81.2.69.1" @@ -85,7 +117,7 @@ def test_anonymous_ip_all_set(self) -> None: self.assertEqual(record.is_public_proxy, True) self.assertEqual(record.is_residential_proxy, True) self.assertEqual(record.is_tor_exit_node, True) - self.assertEqual(record.ip_address, ip_address) + self.assertEqual(record.ip_address, ipaddress.ip_address(ip_address)) self.assertEqual(record.network, ipaddress.ip_network("81.2.69.0/24")) reader.close() @@ -95,11 +127,15 @@ def test_asn(self) -> None: ip_address = "1.128.0.0" record = reader.asn(ip_address) - self.assertEqual(record, eval(repr(record)), "ASN repr can be eval'd") + self.assertEqual( + record, + eval(repr(record)), # noqa: S307 + "ASN repr can be eval'd", + ) self.assertEqual(record.autonomous_system_number, 1221) self.assertEqual(record.autonomous_system_organization, "Telstra Pty Ltd") - self.assertEqual(record.ip_address, ip_address) + self.assertEqual(record.ip_address, ipaddress.ip_address(ip_address)) self.assertEqual(record.network, ipaddress.ip_network("1.128.0.0/11")) self.assertRegex( @@ -115,35 +151,45 @@ def test_city(self) -> None: record = reader.city("81.2.69.160") self.assertEqual( - record.country.name, "United Kingdom", "The default locale is en" + record.country.name, + "United Kingdom", + "The default locale is en", ) - self.assertEqual(record.country.is_in_european_union, True) + self.assertEqual(record.country.is_in_european_union, False) self.assertEqual( - record.location.accuracy_radius, 100, "The accuracy_radius is populated" + record.location.accuracy_radius, + 100, + "The accuracy_radius is populated", ) self.assertEqual(record.registered_country.is_in_european_union, False) + self.assertFalse(record.traits.is_anycast) + + record = reader.city("214.1.1.0") + self.assertTrue(record.traits.is_anycast) reader.close() def test_connection_type(self) -> None: reader = geoip2.database.Reader( - "tests/data/test-data/GeoIP2-Connection-Type-Test.mmdb" + "tests/data/test-data/GeoIP2-Connection-Type-Test.mmdb", ) ip_address = "1.0.1.0" record = reader.connection_type(ip_address) self.assertEqual( - record, eval(repr(record)), "ConnectionType repr can be eval'd" + record, + eval(repr(record)), # noqa: S307 + "ConnectionType repr can be eval'd", ) - self.assertEqual(record.connection_type, "Cable/DSL") - self.assertEqual(record.ip_address, ip_address) + self.assertEqual(record.connection_type, "Cellular") + self.assertEqual(record.ip_address, ipaddress.ip_address(ip_address)) self.assertEqual(record.network, ipaddress.ip_network("1.0.1.0/24")) self.assertRegex( str(record), - r"ConnectionType\(\{.*Cable/DSL.*\}\)", + r"ConnectionType\(.*Cellular.*\)", "ConnectionType str representation is reasonable", ) @@ -153,11 +199,18 @@ def test_country(self) -> None: reader = geoip2.database.Reader("tests/data/test-data/GeoIP2-Country-Test.mmdb") record = reader.country("81.2.69.160") self.assertEqual( - record.traits.ip_address, "81.2.69.160", "IP address is added to model" + record.traits.ip_address, + ipaddress.ip_address("81.2.69.160"), + "IP address is added to model", ) self.assertEqual(record.traits.network, ipaddress.ip_network("81.2.69.160/27")) - self.assertEqual(record.country.is_in_european_union, True) + self.assertEqual(record.country.is_in_european_union, False) self.assertEqual(record.registered_country.is_in_european_union, False) + self.assertFalse(record.traits.is_anycast) + + record = reader.country("214.1.1.0") + self.assertTrue(record.traits.is_anycast) + reader.close() def test_domain(self) -> None: @@ -166,15 +219,19 @@ def test_domain(self) -> None: ip_address = "1.2.0.0" record = reader.domain(ip_address) - self.assertEqual(record, eval(repr(record)), "Domain repr can be eval'd") + self.assertEqual( + record, + eval(repr(record)), # noqa: S307 + "Domain repr can be eval'd", + ) self.assertEqual(record.domain, "maxmind.com") - self.assertEqual(record.ip_address, ip_address) + self.assertEqual(record.ip_address, ipaddress.ip_address(ip_address)) self.assertEqual(record.network, ipaddress.ip_network("1.2.0.0/16")) self.assertRegex( str(record), - r"Domain\(\{.*maxmind.com.*\}\)", + r"Domain\(.*maxmind.com.*\)", "Domain str representation is reasonable", ) @@ -182,7 +239,7 @@ def test_domain(self) -> None: def test_enterprise(self) -> None: with geoip2.database.Reader( - "tests/data/test-data/GeoIP2-Enterprise-Test.mmdb" + "tests/data/test-data/GeoIP2-Enterprise-Test.mmdb", ) as reader: ip_address = "74.209.24.0" record = reader.enterprise(ip_address) @@ -194,61 +251,67 @@ def test_enterprise(self) -> None: self.assertEqual(record.registered_country.is_in_european_union, False) self.assertEqual(record.traits.connection_type, "Cable/DSL") self.assertTrue(record.traits.is_legitimate_proxy) - self.assertEqual(record.traits.ip_address, ip_address) + self.assertEqual(record.traits.ip_address, ipaddress.ip_address(ip_address)) self.assertEqual( - record.traits.network, ipaddress.ip_network("74.209.16.0/20") + record.traits.network, + ipaddress.ip_network("74.209.16.0/20"), ) + self.assertFalse(record.traits.is_anycast) - def test_isp(self) -> None: - reader = geoip2.database.Reader("tests/data/test-data/GeoIP2-ISP-Test.mmdb") + record = reader.enterprise("149.101.100.0") + self.assertEqual(record.traits.mobile_country_code, "310") + self.assertEqual(record.traits.mobile_network_code, "004") - ip_address = "1.128.0.0" - record = reader.isp(ip_address) - self.assertEqual(record, eval(repr(record)), "ISP repr can be eval'd") + record = reader.enterprise("214.1.1.0") + self.assertTrue(record.traits.is_anycast) - self.assertEqual(record.autonomous_system_number, 1221) - self.assertEqual(record.autonomous_system_organization, "Telstra Pty Ltd") - self.assertEqual(record.isp, "Telstra Internet") - self.assertEqual(record.organization, "Telstra Internet") - self.assertEqual(record.ip_address, ip_address) - self.assertEqual(record.network, ipaddress.ip_network("1.128.0.0/11")) + def test_isp(self) -> None: + with geoip2.database.Reader( + "tests/data/test-data/GeoIP2-ISP-Test.mmdb", + ) as reader: + ip_address = "1.128.0.0" + record = reader.isp(ip_address) + self.assertEqual( + record, + eval(repr(record)), # noqa: S307 + "ISP repr can be eval'd", + ) - self.assertRegex( - str(record), - r"ISP\(\{.*Telstra.*\}\)", - "ISP str representation is reasonable", - ) + self.assertEqual(record.autonomous_system_number, 1221) + self.assertEqual(record.autonomous_system_organization, "Telstra Pty Ltd") + self.assertEqual(record.isp, "Telstra Internet") + self.assertEqual(record.organization, "Telstra Internet") + self.assertEqual(record.ip_address, ipaddress.ip_address(ip_address)) + self.assertEqual(record.network, ipaddress.ip_network("1.128.0.0/11")) + + self.assertRegex( + str(record), + r"ISP\(.*Telstra.*\)", + "ISP str representation is reasonable", + ) - reader.close() + record = reader.isp("149.101.100.0") + + self.assertEqual(record.mobile_country_code, "310") + self.assertEqual(record.mobile_network_code, "004") def test_context_manager(self) -> None: with geoip2.database.Reader( - "tests/data/test-data/GeoIP2-Country-Test.mmdb" + "tests/data/test-data/GeoIP2-Country-Test.mmdb", ) as reader: record = reader.country("81.2.69.160") - self.assertEqual(record.traits.ip_address, "81.2.69.160") - - -@unittest.skipUnless(maxminddb.extension, "No C extension module found. Skipping tests") -class TestExtensionReader(BaseTestReader): - mode = geoip2.database.MODE_MMAP_EXT - - -class TestMMAPReader(BaseTestReader): - mode = geoip2.database.MODE_MMAP - - -class TestFileReader(BaseTestReader): - mode = geoip2.database.MODE_FILE - - -class TestMemoryReader(BaseTestReader): - mode = geoip2.database.MODE_MEMORY - - -class TestFDReader(unittest.TestCase): - mode = geoip2.database.MODE_FD + self.assertEqual( + record.traits.ip_address, + ipaddress.ip_address("81.2.69.160"), + ) + @patch("maxminddb.open_database") + def test_modes(self, mock_open: MagicMock) -> None: + mock_open.return_value = MagicMock() -class TestAutoReader(BaseTestReader): - mode = geoip2.database.MODE_AUTO + path = "tests/data/test-data/GeoIP2-Country-Test.mmdb" + with geoip2.database.Reader( + path, + mode=geoip2.database.MODE_MMAP_EXT, + ): + mock_open.assert_called_once_with(path, geoip2.database.MODE_MMAP_EXT) diff --git a/tests/models_test.py b/tests/models_test.py index 522a57b..7cce9a8 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -1,11 +1,7 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals - +import ipaddress import sys -from typing import Dict import unittest +from typing import ClassVar sys.path.append("..") @@ -13,6 +9,9 @@ class TestModels(unittest.TestCase): + def setUp(self) -> None: + self.maxDiff = 20_000 + def test_insights_full(self) -> None: raw = { "city": { @@ -72,18 +71,19 @@ def test_insights_full(self) -> None: "traits": { "autonomous_system_number": 1234, "autonomous_system_organization": "AS Organization", + "connection_type": "Cable/DSL", "domain": "example.com", "ip_address": "1.2.3.4", "is_anonymous": True, "is_anonymous_proxy": True, "is_anonymous_vpn": True, + "is_anycast": True, "is_hosting_provider": True, "is_public_proxy": True, "is_residential_proxy": True, "is_satellite_provider": True, "is_tor_exit_node": True, "isp": "Comcast", - "network_speed": "cable/DSL", "organization": "Blorg", "static_ip_score": 1.3, "user_count": 2, @@ -91,12 +91,16 @@ def test_insights_full(self) -> None: }, } - model = geoip2.models.Insights(raw) + model = geoip2.models.Insights(["en"], **raw) # type: ignore[arg-type] self.assertEqual( - type(model), geoip2.models.Insights, "geoip2.models.Insights object" + type(model), + geoip2.models.Insights, + "geoip2.models.Insights object", ) self.assertEqual( - type(model.city), geoip2.records.City, "geoip2.records.City object" + type(model.city), + geoip2.records.City, + "geoip2.records.City object", ) self.assertEqual( type(model.continent), @@ -104,7 +108,9 @@ def test_insights_full(self) -> None: "geoip2.records.Continent object", ) self.assertEqual( - type(model.country), geoip2.records.Country, "geoip2.records.Country object" + type(model.country), + geoip2.records.Country, + "geoip2.records.Country object", ) self.assertEqual( type(model.registered_country), @@ -127,23 +133,35 @@ def test_insights_full(self) -> None: "geoip2.records.Subdivision object", ) self.assertEqual( - type(model.traits), geoip2.records.Traits, "geoip2.records.Traits object" + type(model.traits), + geoip2.records.Traits, + "geoip2.records.Traits object", ) - self.assertEqual(model.raw, raw, "raw method returns raw input") + self.assertEqual(model.to_dict(), raw, "to_dict() method matches raw input") self.assertEqual( - model.subdivisions[0].iso_code, "MN", "div 1 has correct iso_code" + model.subdivisions[0].iso_code, + "MN", + "div 1 has correct iso_code", ) self.assertEqual( - model.subdivisions[0].confidence, 88, "div 1 has correct confidence" + model.subdivisions[0].confidence, + 88, + "div 1 has correct confidence", ) self.assertEqual( - model.subdivisions[0].geoname_id, 574635, "div 1 has correct geoname_id" + model.subdivisions[0].geoname_id, + 574635, + "div 1 has correct geoname_id", ) self.assertEqual( - model.subdivisions[0].names, {"en": "Minnesota"}, "div 1 names are correct" + model.subdivisions[0].names, + {"en": "Minnesota"}, + "div 1 names are correct", ) self.assertEqual( - model.subdivisions[1].name, "Hennepin", "div 2 has correct name" + model.subdivisions[1].name, + "Hennepin", + "div 2 has correct name", ) self.assertEqual( model.subdivisions.most_specific.iso_code, @@ -165,16 +183,22 @@ def test_insights_full(self) -> None: self.assertEqual(model.location.longitude, 93.2636, "correct longitude") self.assertEqual(model.location.metro_code, 765, "correct metro_code") self.assertEqual( - model.location.population_density, 1341, "correct population_density" + model.location.population_density, + 1341, + "correct population_density", ) self.assertRegex( str(model), - r"^geoip2.models.Insights\(\{.*geoname_id.*\}, \[.*en.*\]\)", + r"^geoip2.models.Insights\(\[.*en.*\]\, .*geoname_id.*\)", "Insights str representation looks reasonable", ) - self.assertEqual(model, eval(repr(model)), "Insights repr can be eval'd") + self.assertEqual( + model, + eval(repr(model)), # noqa: S307 + "Insights repr can be eval'd", + ) self.assertRegex( str(model.location), @@ -183,31 +207,44 @@ def test_insights_full(self) -> None: ) self.assertEqual( - model.location, eval(repr(model.location)), "Location repr can be eval'd" + model.location, + eval(repr(model.location)), # noqa: S307 + "Location repr can be eval'd", ) - self.assertIs(model.country.is_in_european_union, False) - self.assertIs(model.registered_country.is_in_european_union, False) - self.assertIs(model.represented_country.is_in_european_union, True) + self.assertIs(model.country.is_in_european_union, False) # noqa: FBT003 + self.assertIs( + model.registered_country.is_in_european_union, + False, # noqa: FBT003 + ) + self.assertIs( + model.represented_country.is_in_european_union, + True, # noqa: FBT003 + ) - self.assertIs(model.traits.is_anonymous, True) - self.assertIs(model.traits.is_anonymous_proxy, True) - self.assertIs(model.traits.is_anonymous_vpn, True) - self.assertIs(model.traits.is_hosting_provider, True) - self.assertIs(model.traits.is_public_proxy, True) - self.assertIs(model.traits.is_residential_proxy, True) - self.assertIs(model.traits.is_satellite_provider, True) - self.assertIs(model.traits.is_tor_exit_node, True) + self.assertIs(model.traits.is_anonymous, True) # noqa: FBT003 + self.assertIs(model.traits.is_anonymous_proxy, True) # noqa: FBT003 + self.assertIs(model.traits.is_anonymous_vpn, True) # noqa: FBT003 + self.assertIs(model.traits.is_anycast, True) # noqa: FBT003 + self.assertIs(model.traits.is_hosting_provider, True) # noqa: FBT003 + self.assertIs(model.traits.is_public_proxy, True) # noqa: FBT003 + self.assertIs(model.traits.is_residential_proxy, True) # noqa: FBT003 + self.assertIs(model.traits.is_satellite_provider, True) # noqa: FBT003 + self.assertIs(model.traits.is_tor_exit_node, True) # noqa: FBT003 self.assertEqual(model.traits.user_count, 2) self.assertEqual(model.traits.static_ip_score, 1.3) def test_insights_min(self) -> None: - model = geoip2.models.Insights({"traits": {"ip_address": "5.6.7.8"}}) + model = geoip2.models.Insights(["en"], traits={"ip_address": "5.6.7.8"}) self.assertEqual( - type(model), geoip2.models.Insights, "geoip2.models.Insights object" + type(model), + geoip2.models.Insights, + "geoip2.models.Insights object", ) self.assertEqual( - type(model.city), geoip2.records.City, "geoip2.records.City object" + type(model.city), + geoip2.records.City, + "geoip2.records.City object", ) self.assertEqual( type(model.continent), @@ -215,7 +252,9 @@ def test_insights_min(self) -> None: "geoip2.records.Continent object", ) self.assertEqual( - type(model.country), geoip2.records.Country, "geoip2.records.Country object" + type(model.country), + geoip2.records.Country, + "geoip2.records.Country object", ) self.assertEqual( type(model.registered_country), @@ -228,16 +267,19 @@ def test_insights_min(self) -> None: "geoip2.records.Location object", ) self.assertEqual( - type(model.traits), geoip2.records.Traits, "geoip2.records.Traits object" + type(model.traits), + geoip2.records.Traits, + "geoip2.records.Traits object", ) self.assertEqual( type(model.subdivisions.most_specific), geoip2.records.Subdivision, - "geoip2.records.Subdivision object returned even" - "when none are available.", + "geoip2.records.Subdivision object returned even when none are available.", ) self.assertEqual( - model.subdivisions.most_specific.names, {}, "Empty names hash returned" + model.subdivisions.most_specific.names, + {}, + "Empty names hash returned", ) def test_city_full(self) -> None: @@ -262,10 +304,12 @@ def test_city_full(self) -> None: "is_satellite_provider": True, }, } - model = geoip2.models.City(raw) + model = geoip2.models.City(["en"], **raw) # type: ignore[arg-type] self.assertEqual(type(model), geoip2.models.City, "geoip2.models.City object") self.assertEqual( - type(model.city), geoip2.records.City, "geoip2.records.City object" + type(model.city), + geoip2.records.City, + "geoip2.records.City object", ) self.assertEqual( type(model.continent), @@ -273,7 +317,9 @@ def test_city_full(self) -> None: "geoip2.records.Continent object", ) self.assertEqual( - type(model.country), geoip2.records.Country, "geoip2.records.Country object" + type(model.country), + geoip2.records.Country, + "geoip2.records.Country object", ) self.assertEqual( type(model.registered_country), @@ -286,16 +332,26 @@ def test_city_full(self) -> None: "geoip2.records.Location object", ) self.assertEqual( - type(model.traits), geoip2.records.Traits, "geoip2.records.Traits object" + type(model.traits), + geoip2.records.Traits, + "geoip2.records.Traits object", + ) + self.assertEqual( + model.to_dict(), + raw, + "to_dict method output matches raw input", ) - self.assertEqual(model.raw, raw, "raw method returns raw input") self.assertEqual(model.continent.geoname_id, 42, "continent geoname_id is 42") self.assertEqual(model.continent.code, "NA", "continent code is NA") self.assertEqual( - model.continent.names, {"en": "North America"}, "continent names is correct" + model.continent.names, + {"en": "North America"}, + "continent names is correct", ) self.assertEqual( - model.continent.name, "North America", "continent name is correct" + model.continent.name, + "North America", + "continent name is correct", ) self.assertEqual(model.country.geoname_id, 1, "country geoname_id is 1") self.assertEqual(model.country.iso_code, "US", "country iso_code is US") @@ -305,11 +361,15 @@ def test_city_full(self) -> None: "country names is correct", ) self.assertEqual( - model.country.name, "United States of America", "country name is correct" + model.country.name, + "United States of America", + "country name is correct", ) self.assertEqual(model.country.confidence, None, "country confidence is None") self.assertEqual( - model.registered_country.iso_code, "CA", "registered_country iso_code is CA" + model.registered_country.iso_code, + "CA", + "registered_country iso_code is CA", ) self.assertEqual( model.registered_country.names, @@ -326,67 +386,75 @@ def test_city_full(self) -> None: False, "traits is_anonymous_proxy returns False by default", ) + self.assertEqual( + model.traits.is_anycast, + False, + "traits is_anycast returns False by default", + ) self.assertEqual( model.traits.is_satellite_provider, True, "traits is_setellite_provider is True", ) - self.assertEqual(model.raw, raw, "raw method produces raw output") + self.assertEqual(model.to_dict(), raw, "to_dict method matches raw input") self.assertRegex( - str(model), r"^geoip2.models.City\(\{.*geoname_id.*\}, \[.*en.*\]\)" + str(model), + r"^geoip2.models.City\(\[.*en.*\], .*geoname_id.*\)", ) - self.assertFalse(model == True, "__eq__ does not blow up on weird input") + self.assertFalse(model is True, "__eq__ does not blow up on weird input") def test_unknown_keys(self) -> None: model = geoip2.models.City( - { - "city": {"invalid": 0}, - "continent": { - "invalid": 0, - "names": {"invalid": 0}, - }, - "country": { - "invalid": 0, - "names": {"invalid": 0}, - }, - "location": {"invalid": 0}, - "postal": {"invalid": 0}, - "subdivisions": [ - { - "invalid": 0, - "names": { - "invalid": 0, - }, - }, - ], - "registered_country": { + ["en"], + city={"invalid": 0}, + continent={ + "invalid": 0, + "names": {"invalid": 0}, + }, + country={ + "invalid": 0, + "names": {"invalid": 0}, + }, + location={"invalid": 0}, + postal={"invalid": 0}, + subdivisions=[ + { "invalid": 0, "names": { "invalid": 0, }, }, - "represented_country": { + ], + registered_country={ + "invalid": 0, + "names": { "invalid": 0, - "names": { - "invalid": 0, - }, }, - "traits": {"ip_address": "1.2.3.4", "invalid": "blah"}, - "unk_base": {"blah": 1}, - } + }, + represented_country={ + "invalid": 0, + "names": { + "invalid": 0, + }, + }, + traits={"ip_address": "1.2.3.4", "invalid": "blah"}, + unk_base={"blah": 1}, ) with self.assertRaises(AttributeError): - model.unk_base # type: ignore + model.unk_base # type: ignore[attr-defined] # noqa: B018 with self.assertRaises(AttributeError): - model.traits.invalid # type: ignore - self.assertEqual(model.traits.ip_address, "1.2.3.4", "correct ip") + model.traits.invalid # type: ignore[attr-defined] # noqa: B018 + self.assertEqual( + model.traits.ip_address, + ipaddress.ip_address("1.2.3.4"), + "correct ip", + ) class TestNames(unittest.TestCase): - - raw: Dict = { + raw: ClassVar[dict] = { "continent": { "code": "NA", "geoname_id": 42, @@ -415,7 +483,7 @@ class TestNames(unittest.TestCase): } def test_names(self) -> None: - model = geoip2.models.Country(self.raw, locales=["sq", "ar"]) + model = geoip2.models.Country(["sq", "ar"], **self.raw) self.assertEqual( model.continent.names, self.raw["continent"]["names"], @@ -428,7 +496,7 @@ def test_names(self) -> None: ) def test_three_locales(self) -> None: - model = geoip2.models.Country(self.raw, locales=["fr", "zh-CN", "en"]) + model = geoip2.models.Country(locales=["fr", "zh-CN", "en"], **self.raw) self.assertEqual( model.continent.name, "北美洲", @@ -437,27 +505,33 @@ def test_three_locales(self) -> None: self.assertEqual(model.country.name, "États-Unis", "country name is in French") def test_two_locales(self) -> None: - model = geoip2.models.Country(self.raw, locales=["ak", "fr"]) + model = geoip2.models.Country(locales=["ak", "fr"], **self.raw) self.assertEqual( model.continent.name, None, - "continent name is undef (no Akan or French " "available)", + "continent name is undef (no Akan or French available)", ) self.assertEqual(model.country.name, "États-Unis", "country name is in French") def test_unknown_locale(self) -> None: - model = geoip2.models.Country(self.raw, locales=["aa"]) + model = geoip2.models.Country(locales=["aa"], **self.raw) self.assertEqual( - model.continent.name, None, "continent name is undef (no Afar available)" + model.continent.name, + None, + "continent name is undef (no Afar available)", ) self.assertEqual( - model.country.name, None, "country name is in None (no Afar available)" + model.country.name, + None, + "country name is in None (no Afar available)", ) def test_german(self) -> None: - model = geoip2.models.Country(self.raw, locales=["de"]) + model = geoip2.models.Country(locales=["de"], **self.raw) self.assertEqual( - model.continent.name, "Nordamerika", "Correct german name for continent" + model.continent.name, + "Nordamerika", + "Correct german name for continent", ) diff --git a/tests/webservice_test.py b/tests/webservice_test.py index 8f3e6d1..f918753 100644 --- a/tests/webservice_test.py +++ b/tests/webservice_test.py @@ -1,20 +1,19 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +from __future__ import annotations import asyncio import copy import ipaddress -import json import sys -from typing import cast, Dict import unittest +from abc import ABC, abstractmethod +from collections import defaultdict +from typing import Callable, ClassVar, cast -sys.path.append("..") +import pytest +import pytest_httpserver +from pytest_httpserver import HeaderValueMatcher -# httpretty currently doesn't work, but mocket with the compat interface -# does. -from mocket import Mocket # type: ignore -from mocket.plugins.httpretty import httpretty, httprettified # type: ignore +sys.path.append("..") import geoip2 from geoip2.errors import ( AddressNotFoundError, @@ -28,9 +27,11 @@ from geoip2.webservice import AsyncClient, Client -class TestBaseClient(unittest.TestCase): - base_uri = "https://geoip.maxmind.com/geoip/v2.1/" - country = { +class TestBaseClient(unittest.TestCase, ABC): + client: AsyncClient | Client + client_class: Callable[[int, str], AsyncClient | Client] + + country: ClassVar = { "continent": {"code": "NA", "geoname_id": 42, "names": {"en": "North America"}}, "country": { "geoname_id": 1, @@ -44,49 +45,66 @@ class TestBaseClient(unittest.TestCase): "iso_code": "DE", "names": {"en": "Germany"}, }, - "traits": {"ip_address": "1.2.3.4", "network": "1.2.3.0/24"}, + "traits": { + "ip_address": "1.2.3.4", + "is_anycast": True, + "network": "1.2.3.0/24", + }, } # this is not a comprehensive representation of the # JSON from the server - insights = cast(Dict, copy.deepcopy(country)) + insights = cast("dict", copy.deepcopy(country)) insights["traits"]["user_count"] = 2 insights["traits"]["static_ip_score"] = 1.3 - def _content_type(self, endpoint): + @abstractmethod + def run_client(self, v): ... # noqa: ANN001 + + def _content_type(self, endpoint: str) -> str: return ( "application/vnd.maxmind.com-" + endpoint + "+json; charset=UTF-8; version=1.0" ) - @httprettified - def test_country_ok(self): - httpretty.register_uri( - httpretty.GET, - self.base_uri + "country/1.2.3.4", - body=json.dumps(self.country), + @pytest.fixture(autouse=True) + def setup_httpserver(self, httpserver: pytest_httpserver.HTTPServer) -> None: + self.httpserver = httpserver + + def test_country_ok(self) -> None: + self.httpserver.expect_request( + "/geoip/v2.1/country/1.2.3.4", + method="GET", + ).respond_with_json( + self.country, status=200, content_type=self._content_type("country"), ) country = self.run_client(self.client.country("1.2.3.4")) self.assertEqual( - type(country), geoip2.models.Country, "return value of client.country" + type(country), + geoip2.models.Country, + "return value of client.country", ) self.assertEqual(country.continent.geoname_id, 42, "continent geoname_id is 42") self.assertEqual(country.continent.code, "NA", "continent code is NA") self.assertEqual( - country.continent.name, "North America", "continent name is North America" + country.continent.name, + "North America", + "continent name is North America", ) self.assertEqual(country.country.geoname_id, 1, "country geoname_id is 1") self.assertIs( country.country.is_in_european_union, - False, + False, # noqa: FBT003 "country is_in_european_union is False", ) self.assertEqual(country.country.iso_code, "US", "country iso_code is US") self.assertEqual( - country.country.names, {"en": "United States of America"}, "country names" + country.country.names, + {"en": "United States of America"}, + "country names", ) self.assertEqual( country.country.name, @@ -94,30 +112,37 @@ def test_country_ok(self): "country name is United States of America", ) self.assertEqual( - country.maxmind.queries_remaining, 11, "queries_remaining is 11" + country.maxmind.queries_remaining, + 11, + "queries_remaining is 11", ) self.assertIs( country.registered_country.is_in_european_union, - True, + True, # noqa: FBT003 "registered_country is_in_european_union is True", ) self.assertEqual( - country.traits.network, ipaddress.ip_network("1.2.3.0/24"), "network" + country.traits.network, + ipaddress.ip_network("1.2.3.0/24"), + "network", ) - self.assertEqual(country.raw, self.country, "raw response is correct") - - @httprettified - def test_me(self): - httpretty.register_uri( - httpretty.GET, - self.base_uri + "country/me", - body=json.dumps(self.country), + self.assertTrue(country.traits.is_anycast) + self.assertEqual(country.to_dict(), self.country, "raw response is correct") + + def test_me(self) -> None: + self.httpserver.expect_request( + "/geoip/v2.1/country/me", + method="GET", + ).respond_with_json( + self.country, status=200, content_type=self._content_type("country"), ) implicit_me = self.run_client(self.client.country()) self.assertEqual( - type(implicit_me), geoip2.models.Country, "country() returns Country object" + type(implicit_me), + geoip2.models.Country, + "country() returns Country object", ) explicit_me = self.run_client(self.client.country()) self.assertEqual( @@ -126,262 +151,281 @@ def test_me(self): "country('me') returns Country object", ) - @httprettified - def test_200_error(self): - httpretty.register_uri( - httpretty.GET, - self.base_uri + "country/1.1.1.1", - body="", + def test_200_error(self) -> None: + self.httpserver.expect_request( + "/geoip/v2.1/country/1.1.1.1", + method="GET", + ).respond_with_data( + "", status=200, content_type=self._content_type("country"), ) + with self.assertRaisesRegex( - GeoIP2Error, "could not decode the response as JSON" + GeoIP2Error, + "could not decode the response as JSON", ): self.run_client(self.client.country("1.1.1.1")) - @httprettified - def test_bad_ip_address(self): + def test_bad_ip_address(self) -> None: with self.assertRaisesRegex( - ValueError, "'1.2.3' does not appear to be an IPv4 " "or IPv6 address" + ValueError, + "'1.2.3' does not appear to be an IPv4 or IPv6 address", ): self.run_client(self.client.country("1.2.3")) - @httprettified - def test_no_body_error(self): - httpretty.register_uri( - httpretty.GET, - self.base_uri + "country/" + "1.2.3.7", - body="", + def test_no_body_error(self) -> None: + self.httpserver.expect_request( + "/geoip/v2.1/country/1.2.3.7", + method="GET", + ).respond_with_data( + "", status=400, content_type=self._content_type("country"), ) with self.assertRaisesRegex( - HTTPError, "Received a 400 error for .* with no body" + HTTPError, + "Received a 400 error for .* with no body", ): self.run_client(self.client.country("1.2.3.7")) - @httprettified - def test_weird_body_error(self): - httpretty.register_uri( - httpretty.GET, - self.base_uri + "country/" + "1.2.3.8", - body='{"wierd": 42}', + def test_weird_body_error(self) -> None: + self.httpserver.expect_request( + "/geoip/v2.1/country/1.2.3.8", + method="GET", + ).respond_with_json( + {"wierd": 42}, status=400, content_type=self._content_type("country"), ) + with self.assertRaisesRegex( HTTPError, - "Response contains JSON but it does not " "specify code or error keys", + "Response contains JSON but it does not specify code or error keys", ): self.run_client(self.client.country("1.2.3.8")) - @httprettified - def test_bad_body_error(self): - httpretty.register_uri( - httpretty.GET, - self.base_uri + "country/" + "1.2.3.9", - body="bad body", + def test_bad_body_error(self) -> None: + self.httpserver.expect_request( + "/geoip/v2.1/country/1.2.3.9", + method="GET", + ).respond_with_data( + "bad body", status=400, content_type=self._content_type("country"), ) with self.assertRaisesRegex( - HTTPError, "it did not include the expected JSON body" + HTTPError, + "it did not include the expected JSON body", ): self.run_client(self.client.country("1.2.3.9")) - @httprettified - def test_500_error(self): - httpretty.register_uri( - httpretty.GET, self.base_uri + "country/" + "1.2.3.10", status=500 + def test_500_error(self) -> None: + self.httpserver.expect_request( + "/geoip/v2.1/country/1.2.3.10", + method="GET", + ).respond_with_data( + "", + status=500, + content_type=self._content_type("country"), ) with self.assertRaisesRegex(HTTPError, r"Received a server error \(500\) for"): self.run_client(self.client.country("1.2.3.10")) - @httprettified - def test_300_error(self): - httpretty.register_uri( - httpretty.GET, - self.base_uri + "country/" + "1.2.3.11", + def test_300_error(self) -> None: + self.httpserver.expect_request( + "/geoip/v2.1/country/1.2.3.11", + method="GET", + ).respond_with_data( + "", status=300, content_type=self._content_type("country"), ) with self.assertRaisesRegex( - HTTPError, r"Received a very surprising HTTP status \(300\) for" + HTTPError, + r"Received a very surprising HTTP status \(300\) for", ): - self.run_client(self.client.country("1.2.3.11")) - @httprettified - def test_ip_address_required(self): + def test_ip_address_required(self) -> None: self._test_error(400, "IP_ADDRESS_REQUIRED", InvalidRequestError) - @httprettified - def test_ip_address_not_found(self): + def test_ip_address_not_found(self) -> None: self._test_error(404, "IP_ADDRESS_NOT_FOUND", AddressNotFoundError) - @httprettified - def test_ip_address_reserved(self): + def test_ip_address_reserved(self) -> None: self._test_error(400, "IP_ADDRESS_RESERVED", AddressNotFoundError) - @httprettified - def test_permission_required(self): + def test_permission_required(self) -> None: self._test_error(403, "PERMISSION_REQUIRED", PermissionRequiredError) - @httprettified - def test_auth_invalid(self): + def test_auth_invalid(self) -> None: self._test_error(400, "AUTHORIZATION_INVALID", AuthenticationError) - @httprettified - def test_license_key_required(self): + def test_license_key_required(self) -> None: self._test_error(401, "LICENSE_KEY_REQUIRED", AuthenticationError) - @httprettified - def test_account_id_required(self): + def test_account_id_required(self) -> None: self._test_error(401, "ACCOUNT_ID_REQUIRED", AuthenticationError) - @httprettified - def test_user_id_required(self): + def test_user_id_required(self) -> None: self._test_error(401, "USER_ID_REQUIRED", AuthenticationError) - @httprettified - def test_account_id_unkown(self): + def test_account_id_unknown(self) -> None: self._test_error(401, "ACCOUNT_ID_UNKNOWN", AuthenticationError) - @httprettified - def test_user_id_unkown(self): + def test_user_id_unknown(self) -> None: self._test_error(401, "USER_ID_UNKNOWN", AuthenticationError) - @httprettified - def test_out_of_queries_error(self): + def test_out_of_queries_error(self) -> None: self._test_error(402, "OUT_OF_QUERIES", OutOfQueriesError) - def _test_error(self, status, error_code, error_class): + def _test_error( + self, + status: int, + error_code: str, + error_class: type[Exception], + ) -> None: msg = "Some error message" body = {"error": msg, "code": error_code} - httpretty.register_uri( - httpretty.GET, - self.base_uri + "country/1.2.3.18", - body=json.dumps(body), + self.httpserver.expect_request( + "/geoip/v2.1/country/1.2.3.18", + method="GET", + ).respond_with_json( + body, status=status, content_type=self._content_type("country"), ) - with self.assertRaisesRegex(error_class, msg): + with pytest.raises(error_class, match=msg): self.run_client(self.client.country("1.2.3.18")) - @httprettified - def test_unknown_error(self): + def test_unknown_error(self) -> None: msg = "Unknown error type" ip = "1.2.3.19" body = {"error": msg, "code": "UNKNOWN_TYPE"} - httpretty.register_uri( - httpretty.GET, - self.base_uri + "country/" + ip, - body=json.dumps(body), + self.httpserver.expect_request( + "/geoip/v2.1/country/" + ip, + method="GET", + ).respond_with_json( + body, status=400, content_type=self._content_type("country"), ) - with self.assertRaisesRegex(InvalidRequestError, msg): + with pytest.raises(InvalidRequestError, match=msg): self.run_client(self.client.country(ip)) - @httprettified - def test_request(self): - httpretty.register_uri( - httpretty.GET, - self.base_uri + "country/" + "1.2.3.4", - body=json.dumps(self.country), + def test_request(self) -> None: + def user_agent_compare(actual: str, _: str) -> bool: + if actual is None: + return False + return actual.startswith("GeoIP2-Python-Client/") + + self.httpserver.expect_request( + "/geoip/v2.1/country/1.2.3.4", + method="GET", + headers={ + "Accept": "application/json", + "Authorization": "Basic NDI6YWJjZGVmMTIzNDU2", + "User-Agent": "GeoIP2-Python-Client/", + }, + header_value_matcher=HeaderValueMatcher( + defaultdict( + lambda: HeaderValueMatcher.default_header_value_matcher, + {"User-Agent": user_agent_compare}, # type: ignore[dict-item] + ), + ), + ).respond_with_json( + self.country, status=200, content_type=self._content_type("country"), ) self.run_client(self.client.country("1.2.3.4")) - request = httpretty.last_request - self.assertEqual( - request.path, "/geoip/v2.1/country/1.2.3.4", "correct URI is used" - ) - self.assertEqual( - request.headers["Accept"], "application/json", "correct Accept header" - ) - self.assertRegex( - request.headers["User-Agent"], - "^GeoIP2-Python-Client/", - "Correct User-Agent", - ) - self.assertEqual( - request.headers["Authorization"], - "Basic NDI6YWJjZGVmMTIzNDU2", - "correct auth", - ) - - @httprettified - def test_city_ok(self): - httpretty.register_uri( - httpretty.GET, - self.base_uri + "city/" + "1.2.3.4", - body=json.dumps(self.country), + def test_city_ok(self) -> None: + self.httpserver.expect_request( + "/geoip/v2.1/city/1.2.3.4", + method="GET", + ).respond_with_json( + self.country, status=200, content_type=self._content_type("city"), ) city = self.run_client(self.client.city("1.2.3.4")) self.assertEqual(type(city), geoip2.models.City, "return value of client.city") self.assertEqual( - city.traits.network, ipaddress.ip_network("1.2.3.0/24"), "network" + city.traits.network, + ipaddress.ip_network("1.2.3.0/24"), + "network", ) - - @httprettified - def test_insights_ok(self): - httpretty.register_uri( - httpretty.GET, - self.base_uri + "insights/1.2.3.4", - body=json.dumps(self.insights), + self.assertTrue(city.traits.is_anycast) + + def test_insights_ok(self) -> None: + self.httpserver.expect_request( + "/geoip/v2.1/insights/1.2.3.4", + method="GET", + ).respond_with_json( + self.insights, status=200, - content_type=self._content_type("country"), + content_type=self._content_type("insights"), ) insights = self.run_client(self.client.insights("1.2.3.4")) self.assertEqual( - type(insights), geoip2.models.Insights, "return value of client.insights" + type(insights), + geoip2.models.Insights, + "return value of client.insights", ) self.assertEqual( - insights.traits.network, ipaddress.ip_network("1.2.3.0/24"), "network" + insights.traits.network, + ipaddress.ip_network("1.2.3.0/24"), + "network", ) + self.assertTrue(insights.traits.is_anycast) self.assertEqual(insights.traits.static_ip_score, 1.3, "static_ip_score is 1.3") self.assertEqual(insights.traits.user_count, 2, "user_count is 2") - def test_named_constructor_args(self): - id = 47 + def test_named_constructor_args(self) -> None: + account_id = 47 key = "1234567890ab" - client = self.client_class(account_id=id, license_key=key) - self.assertEqual(client._account_id, str(id)) - self.assertEqual(client._license_key, key) + client = self.client_class(account_id, key) + self.assertEqual(client._account_id, str(account_id)) # noqa: SLF001 + self.assertEqual(client._license_key, key) # noqa: SLF001 - def test_missing_constructor_args(self): + def test_missing_constructor_args(self) -> None: with self.assertRaises(TypeError): - self.client_class(license_key="1234567890ab") + self.client_class(license_key="1234567890ab") # type: ignore[call-arg] with self.assertRaises(TypeError): - self.client_class("47") + self.client_class("47") # type: ignore[call-arg,arg-type,misc] class TestClient(TestBaseClient): - def setUp(self): + client: Client + + def setUp(self) -> None: self.client_class = Client self.client = Client(42, "abcdef123456") + self.client._base_uri = self.httpserver.url_for("/geoip/v2.1") # noqa: SLF001 + self.maxDiff = 20_000 - def run_client(self, v): + def run_client(self, v): # noqa: ANN001 return v class TestAsyncClient(TestBaseClient): - def setUp(self): + client: AsyncClient + + def setUp(self) -> None: self._loop = asyncio.new_event_loop() self.client_class = AsyncClient self.client = AsyncClient(42, "abcdef123456") + self.client._base_uri = self.httpserver.url_for("/geoip/v2.1") # noqa: SLF001 + self.maxDiff = 20_000 - def tearDown(self): + def tearDown(self) -> None: self._loop.run_until_complete(self.client.close()) self._loop.close() - def run_client(self, v): + def run_client(self, v): # noqa: ANN001 return self._loop.run_until_complete(v) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..4251692 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1174 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.12.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/dc/ef9394bde9080128ad401ac7ede185267ed637df03b51f05d14d1c99ad67/aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc", size = 703921, upload-time = "2025-07-29T05:49:43.584Z" }, + { url = "https://files.pythonhosted.org/packages/8f/42/63fccfc3a7ed97eb6e1a71722396f409c46b60a0552d8a56d7aad74e0df5/aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af", size = 480288, upload-time = "2025-07-29T05:49:47.851Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a2/7b8a020549f66ea2a68129db6960a762d2393248f1994499f8ba9728bbed/aiohttp-3.12.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421", size = 468063, upload-time = "2025-07-29T05:49:49.789Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f5/d11e088da9176e2ad8220338ae0000ed5429a15f3c9dfd983f39105399cd/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79", size = 1650122, upload-time = "2025-07-29T05:49:51.874Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6b/b60ce2757e2faed3d70ed45dafee48cee7bfb878785a9423f7e883f0639c/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77", size = 1624176, upload-time = "2025-07-29T05:49:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/dd/de/8c9fde2072a1b72c4fadecf4f7d4be7a85b1d9a4ab333d8245694057b4c6/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c", size = 1696583, upload-time = "2025-07-29T05:49:55.338Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ad/07f863ca3d895a1ad958a54006c6dafb4f9310f8c2fdb5f961b8529029d3/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4", size = 1738896, upload-time = "2025-07-29T05:49:57.045Z" }, + { url = "https://files.pythonhosted.org/packages/20/43/2bd482ebe2b126533e8755a49b128ec4e58f1a3af56879a3abdb7b42c54f/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6", size = 1643561, upload-time = "2025-07-29T05:49:58.762Z" }, + { url = "https://files.pythonhosted.org/packages/23/40/2fa9f514c4cf4cbae8d7911927f81a1901838baf5e09a8b2c299de1acfe5/aiohttp-3.12.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2", size = 1583685, upload-time = "2025-07-29T05:50:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c3/94dc7357bc421f4fb978ca72a201a6c604ee90148f1181790c129396ceeb/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d", size = 1627533, upload-time = "2025-07-29T05:50:02.306Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3f/1f8911fe1844a07001e26593b5c255a685318943864b27b4e0267e840f95/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb", size = 1638319, upload-time = "2025-07-29T05:50:04.282Z" }, + { url = "https://files.pythonhosted.org/packages/4e/46/27bf57a99168c4e145ffee6b63d0458b9c66e58bb70687c23ad3d2f0bd17/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5", size = 1613776, upload-time = "2025-07-29T05:50:05.863Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7e/1d2d9061a574584bb4ad3dbdba0da90a27fdc795bc227def3a46186a8bc1/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b", size = 1693359, upload-time = "2025-07-29T05:50:07.563Z" }, + { url = "https://files.pythonhosted.org/packages/08/98/bee429b52233c4a391980a5b3b196b060872a13eadd41c3a34be9b1469ed/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065", size = 1716598, upload-time = "2025-07-29T05:50:09.33Z" }, + { url = "https://files.pythonhosted.org/packages/57/39/b0314c1ea774df3392751b686104a3938c63ece2b7ce0ba1ed7c0b4a934f/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1", size = 1644940, upload-time = "2025-07-29T05:50:11.334Z" }, + { url = "https://files.pythonhosted.org/packages/1b/83/3dacb8d3f8f512c8ca43e3fa8a68b20583bd25636ffa4e56ee841ffd79ae/aiohttp-3.12.15-cp310-cp310-win32.whl", hash = "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a", size = 429239, upload-time = "2025-07-29T05:50:12.803Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f9/470b5daba04d558c9673ca2034f28d067f3202a40e17804425f0c331c89f/aiohttp-3.12.15-cp310-cp310-win_amd64.whl", hash = "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830", size = 452297, upload-time = "2025-07-29T05:50:14.266Z" }, + { url = "https://files.pythonhosted.org/packages/20/19/9e86722ec8e835959bd97ce8c1efa78cf361fa4531fca372551abcc9cdd6/aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117", size = 711246, upload-time = "2025-07-29T05:50:15.937Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/0a31fcb1a7d4629ac9d8f01f1cb9242e2f9943f47f5d03215af91c3c1a26/aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe", size = 483515, upload-time = "2025-07-29T05:50:17.442Z" }, + { url = "https://files.pythonhosted.org/packages/62/6c/94846f576f1d11df0c2e41d3001000527c0fdf63fce7e69b3927a731325d/aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9", size = 471776, upload-time = "2025-07-29T05:50:19.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6c/f766d0aaafcee0447fad0328da780d344489c042e25cd58fde566bf40aed/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5", size = 1741977, upload-time = "2025-07-29T05:50:21.665Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/fb779a05ba6ff44d7bc1e9d24c644e876bfff5abe5454f7b854cace1b9cc/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728", size = 1690645, upload-time = "2025-07-29T05:50:23.333Z" }, + { url = "https://files.pythonhosted.org/packages/37/4e/a22e799c2035f5d6a4ad2cf8e7c1d1bd0923192871dd6e367dafb158b14c/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16", size = 1789437, upload-time = "2025-07-29T05:50:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/28/e5/55a33b991f6433569babb56018b2fb8fb9146424f8b3a0c8ecca80556762/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0", size = 1828482, upload-time = "2025-07-29T05:50:26.693Z" }, + { url = "https://files.pythonhosted.org/packages/c6/82/1ddf0ea4f2f3afe79dffed5e8a246737cff6cbe781887a6a170299e33204/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b", size = 1730944, upload-time = "2025-07-29T05:50:28.382Z" }, + { url = "https://files.pythonhosted.org/packages/1b/96/784c785674117b4cb3877522a177ba1b5e4db9ce0fd519430b5de76eec90/aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd", size = 1668020, upload-time = "2025-07-29T05:50:30.032Z" }, + { url = "https://files.pythonhosted.org/packages/12/8a/8b75f203ea7e5c21c0920d84dd24a5c0e971fe1e9b9ebbf29ae7e8e39790/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8", size = 1716292, upload-time = "2025-07-29T05:50:31.983Z" }, + { url = "https://files.pythonhosted.org/packages/47/0b/a1451543475bb6b86a5cfc27861e52b14085ae232896a2654ff1231c0992/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50", size = 1711451, upload-time = "2025-07-29T05:50:33.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/fd/793a23a197cc2f0d29188805cfc93aa613407f07e5f9da5cd1366afd9d7c/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676", size = 1691634, upload-time = "2025-07-29T05:50:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bf/23a335a6670b5f5dfc6d268328e55a22651b440fca341a64fccf1eada0c6/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7", size = 1785238, upload-time = "2025-07-29T05:50:37.597Z" }, + { url = "https://files.pythonhosted.org/packages/57/4f/ed60a591839a9d85d40694aba5cef86dde9ee51ce6cca0bb30d6eb1581e7/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7", size = 1805701, upload-time = "2025-07-29T05:50:39.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/e0/444747a9455c5de188c0f4a0173ee701e2e325d4b2550e9af84abb20cdba/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685", size = 1718758, upload-time = "2025-07-29T05:50:41.292Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/1006278d1ffd13a698e5dd4bfa01e5878f6bddefc296c8b62649753ff249/aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b", size = 428868, upload-time = "2025-07-29T05:50:43.063Z" }, + { url = "https://files.pythonhosted.org/packages/10/97/ad2b18700708452400278039272032170246a1bf8ec5d832772372c71f1a/aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d", size = 453273, upload-time = "2025-07-29T05:50:44.613Z" }, + { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333, upload-time = "2025-07-29T05:50:46.507Z" }, + { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948, upload-time = "2025-07-29T05:50:48.067Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787, upload-time = "2025-07-29T05:50:49.669Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590, upload-time = "2025-07-29T05:50:51.368Z" }, + { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241, upload-time = "2025-07-29T05:50:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335, upload-time = "2025-07-29T05:50:55.394Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491, upload-time = "2025-07-29T05:50:57.202Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929, upload-time = "2025-07-29T05:50:59.192Z" }, + { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733, upload-time = "2025-07-29T05:51:01.394Z" }, + { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790, upload-time = "2025-07-29T05:51:03.657Z" }, + { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245, upload-time = "2025-07-29T05:51:05.911Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899, upload-time = "2025-07-29T05:51:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459, upload-time = "2025-07-29T05:51:09.56Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434, upload-time = "2025-07-29T05:51:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045, upload-time = "2025-07-29T05:51:13.689Z" }, + { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591, upload-time = "2025-07-29T05:51:15.452Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266, upload-time = "2025-07-29T05:51:17.239Z" }, + { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741, upload-time = "2025-07-29T05:51:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407, upload-time = "2025-07-29T05:51:21.165Z" }, + { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703, upload-time = "2025-07-29T05:51:22.948Z" }, + { url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532, upload-time = "2025-07-29T05:51:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794, upload-time = "2025-07-29T05:51:27.145Z" }, + { url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865, upload-time = "2025-07-29T05:51:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238, upload-time = "2025-07-29T05:51:31.285Z" }, + { url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566, upload-time = "2025-07-29T05:51:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270, upload-time = "2025-07-29T05:51:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294, upload-time = "2025-07-29T05:51:37.215Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958, upload-time = "2025-07-29T05:51:39.328Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553, upload-time = "2025-07-29T05:51:41.356Z" }, + { url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688, upload-time = "2025-07-29T05:51:43.452Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157, upload-time = "2025-07-29T05:51:45.643Z" }, + { url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050, upload-time = "2025-07-29T05:51:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647, upload-time = "2025-07-29T05:51:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067, upload-time = "2025-07-29T05:51:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/18/8d/da08099af8db234d1cd43163e6ffc8e9313d0e988cee1901610f2fa5c764/aiohttp-3.12.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:691d203c2bdf4f4637792efbbcdcd157ae11e55eaeb5e9c360c1206fb03d4d98", size = 706829, upload-time = "2025-07-29T05:51:54.434Z" }, + { url = "https://files.pythonhosted.org/packages/4e/94/8eed385cfb60cf4fdb5b8a165f6148f3bebeb365f08663d83c35a5f273ef/aiohttp-3.12.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e995e1abc4ed2a454c731385bf4082be06f875822adc4c6d9eaadf96e20d406", size = 481806, upload-time = "2025-07-29T05:51:56.355Z" }, + { url = "https://files.pythonhosted.org/packages/38/68/b13e1a34584fbf263151b3a72a084e89f2102afe38df1dce5a05a15b83e9/aiohttp-3.12.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bd44d5936ab3193c617bfd6c9a7d8d1085a8dc8c3f44d5f1dcf554d17d04cf7d", size = 469205, upload-time = "2025-07-29T05:51:58.277Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/3d7348bf53aa4af54416bc64cbef3a2ac5e8b9bfa97cc45f1cf9a94d9c8d/aiohttp-3.12.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46749be6e89cd78d6068cdf7da51dbcfa4321147ab8e4116ee6678d9a056a0cf", size = 1644174, upload-time = "2025-07-29T05:52:00.23Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ed/fd9b5b22b0f6ca1a85c33bb4868cbcc6ae5eae070a0f4c9c5cad003c89d7/aiohttp-3.12.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c643f4d75adea39e92c0f01b3fb83d57abdec8c9279b3078b68a3a52b3933b6", size = 1618672, upload-time = "2025-07-29T05:52:02.272Z" }, + { url = "https://files.pythonhosted.org/packages/39/f7/f6530ab5f8c8c409e44a63fcad35e839c87aabecdfe5b8e96d671ed12f64/aiohttp-3.12.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a23918fedc05806966a2438489dcffccbdf83e921a1170773b6178d04ade142", size = 1692295, upload-time = "2025-07-29T05:52:04.546Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/3cf483bb0106566dc97ebaa2bb097f5e44d4bc4ab650a6f107151cd7b193/aiohttp-3.12.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74bdd8c864b36c3673741023343565d95bfbd778ffe1eb4d412c135a28a8dc89", size = 1731609, upload-time = "2025-07-29T05:52:06.552Z" }, + { url = "https://files.pythonhosted.org/packages/de/a4/fd04bf807851197077d9cac9381d58f86d91c95c06cbaf9d3a776ac4467a/aiohttp-3.12.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a146708808c9b7a988a4af3821379e379e0f0e5e466ca31a73dbdd0325b0263", size = 1637852, upload-time = "2025-07-29T05:52:08.975Z" }, + { url = "https://files.pythonhosted.org/packages/98/03/29d626ca3bcdcafbd74b45d77ca42645a5c94d396f2ee3446880ad2405fb/aiohttp-3.12.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7011a70b56facde58d6d26da4fec3280cc8e2a78c714c96b7a01a87930a9530", size = 1572852, upload-time = "2025-07-29T05:52:11.508Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cd/b4777a9e204f4e01091091027e5d1e2fa86decd0fee5067bc168e4fa1e76/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3bdd6e17e16e1dbd3db74d7f989e8af29c4d2e025f9828e6ef45fbdee158ec75", size = 1620813, upload-time = "2025-07-29T05:52:13.891Z" }, + { url = "https://files.pythonhosted.org/packages/ae/26/1a44a6e8417e84057beaf8c462529b9e05d4b53b8605784f1eb571f0ff68/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57d16590a351dfc914670bd72530fd78344b885a00b250e992faea565b7fdc05", size = 1630951, upload-time = "2025-07-29T05:52:15.955Z" }, + { url = "https://files.pythonhosted.org/packages/dd/7f/10c605dbd01c40e2b27df7ef9004bec75d156f0705141e11047ecdfe264d/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bc9a0f6569ff990e0bbd75506c8d8fe7214c8f6579cca32f0546e54372a3bb54", size = 1607595, upload-time = "2025-07-29T05:52:18.089Z" }, + { url = "https://files.pythonhosted.org/packages/66/f6/2560dcb01731c1d7df1d34b64de95bc4b3ed02bb78830fd82299c1eb314e/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:536ad7234747a37e50e7b6794ea868833d5220b49c92806ae2d7e8a9d6b5de02", size = 1695194, upload-time = "2025-07-29T05:52:20.255Z" }, + { url = "https://files.pythonhosted.org/packages/e7/02/ee105ae82dc2b981039fd25b0cf6eaa52b493731960f9bc861375a72b463/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f0adb4177fa748072546fb650d9bd7398caaf0e15b370ed3317280b13f4083b0", size = 1710872, upload-time = "2025-07-29T05:52:22.769Z" }, + { url = "https://files.pythonhosted.org/packages/88/16/70c4e42ed6a04f78fb58d1a46500a6ce560741d13afde2a5f33840746a5f/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:14954a2988feae3987f1eb49c706bff39947605f4b6fa4027c1d75743723eb09", size = 1640539, upload-time = "2025-07-29T05:52:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1d/a7eb5fa8a6967117c5c0ad5ab4b1dec0d21e178c89aa08bc442a0b836392/aiohttp-3.12.15-cp39-cp39-win32.whl", hash = "sha256:b784d6ed757f27574dca1c336f968f4e81130b27595e458e69457e6878251f5d", size = 430164, upload-time = "2025-07-29T05:52:27.905Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/e0cf8793aedc41c6d7f2aad646a27e27bdacafe3b402bb373d7651c94d73/aiohttp-3.12.15-cp39-cp39-win_amd64.whl", hash = "sha256:86ceded4e78a992f835209e236617bffae649371c4a50d5e5a3987f237db84b8", size = 453370, upload-time = "2025-07-29T05:52:29.936Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "certifi" +version = "2025.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671, upload-time = "2025-05-02T08:34:12.696Z" }, + { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744, upload-time = "2025-05-02T08:34:14.665Z" }, + { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993, upload-time = "2025-05-02T08:34:17.134Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382, upload-time = "2025-05-02T08:34:19.081Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536, upload-time = "2025-05-02T08:34:21.073Z" }, + { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349, upload-time = "2025-05-02T08:34:23.193Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365, upload-time = "2025-05-02T08:34:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499, upload-time = "2025-05-02T08:34:27.359Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735, upload-time = "2025-05-02T08:34:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786, upload-time = "2025-05-02T08:34:31.858Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203, upload-time = "2025-05-02T08:34:33.88Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436, upload-time = "2025-05-02T08:34:35.907Z" }, + { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772, upload-time = "2025-05-02T08:34:37.935Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[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, 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, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/36/0da0a49409f6b47cc2d060dc8c9040b897b5902a8a4e37d9bc1deb11f680/frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a", size = 81304, upload-time = "2025-06-09T22:59:46.226Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/77c11d13d39513b298e267b22eb6cb559c103d56f155aa9a49097221f0b6/frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61", size = 47735, upload-time = "2025-06-09T22:59:48.133Z" }, + { url = "https://files.pythonhosted.org/packages/37/12/9d07fa18971a44150593de56b2f2947c46604819976784bcf6ea0d5db43b/frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d", size = 46775, upload-time = "2025-06-09T22:59:49.564Z" }, + { url = "https://files.pythonhosted.org/packages/70/34/f73539227e06288fcd1f8a76853e755b2b48bca6747e99e283111c18bcd4/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e", size = 224644, upload-time = "2025-06-09T22:59:51.35Z" }, + { url = "https://files.pythonhosted.org/packages/fb/68/c1d9c2f4a6e438e14613bad0f2973567586610cc22dcb1e1241da71de9d3/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9", size = 222125, upload-time = "2025-06-09T22:59:52.884Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d0/98e8f9a515228d708344d7c6986752be3e3192d1795f748c24bcf154ad99/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c", size = 233455, upload-time = "2025-06-09T22:59:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/79/df/8a11bcec5600557f40338407d3e5bea80376ed1c01a6c0910fcfdc4b8993/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981", size = 227339, upload-time = "2025-06-09T22:59:56.187Z" }, + { url = "https://files.pythonhosted.org/packages/50/82/41cb97d9c9a5ff94438c63cc343eb7980dac4187eb625a51bdfdb7707314/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615", size = 212969, upload-time = "2025-06-09T22:59:57.604Z" }, + { url = "https://files.pythonhosted.org/packages/13/47/f9179ee5ee4f55629e4f28c660b3fdf2775c8bfde8f9c53f2de2d93f52a9/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50", size = 222862, upload-time = "2025-06-09T22:59:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/df81e41ec6b953902c8b7e3a83bee48b195cb0e5ec2eabae5d8330c78038/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa", size = 222492, upload-time = "2025-06-09T23:00:01.026Z" }, + { url = "https://files.pythonhosted.org/packages/84/17/30d6ea87fa95a9408245a948604b82c1a4b8b3e153cea596421a2aef2754/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577", size = 238250, upload-time = "2025-06-09T23:00:03.401Z" }, + { url = "https://files.pythonhosted.org/packages/8f/00/ecbeb51669e3c3df76cf2ddd66ae3e48345ec213a55e3887d216eb4fbab3/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59", size = 218720, upload-time = "2025-06-09T23:00:05.282Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c0/c224ce0e0eb31cc57f67742071bb470ba8246623c1823a7530be0e76164c/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e", size = 232585, upload-time = "2025-06-09T23:00:07.962Z" }, + { url = "https://files.pythonhosted.org/packages/55/3c/34cb694abf532f31f365106deebdeac9e45c19304d83cf7d51ebbb4ca4d1/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd", size = 234248, upload-time = "2025-06-09T23:00:09.428Z" }, + { url = "https://files.pythonhosted.org/packages/98/c0/2052d8b6cecda2e70bd81299e3512fa332abb6dcd2969b9c80dfcdddbf75/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718", size = 221621, upload-time = "2025-06-09T23:00:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bf/7dcebae315436903b1d98ffb791a09d674c88480c158aa171958a3ac07f0/frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e", size = 39578, upload-time = "2025-06-09T23:00:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/8f/5f/f69818f017fa9a3d24d1ae39763e29b7f60a59e46d5f91b9c6b21622f4cd/frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464", size = 43830, upload-time = "2025-06-09T23:00:14.98Z" }, + { url = "https://files.pythonhosted.org/packages/34/7e/803dde33760128acd393a27eb002f2020ddb8d99d30a44bfbaab31c5f08a/frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a", size = 82251, upload-time = "2025-06-09T23:00:16.279Z" }, + { url = "https://files.pythonhosted.org/packages/75/a9/9c2c5760b6ba45eae11334db454c189d43d34a4c0b489feb2175e5e64277/frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750", size = 48183, upload-time = "2025-06-09T23:00:17.698Z" }, + { url = "https://files.pythonhosted.org/packages/47/be/4038e2d869f8a2da165f35a6befb9158c259819be22eeaf9c9a8f6a87771/frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd", size = 47107, upload-time = "2025-06-09T23:00:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/79/26/85314b8a83187c76a37183ceed886381a5f992975786f883472fcb6dc5f2/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2", size = 237333, upload-time = "2025-06-09T23:00:20.275Z" }, + { url = "https://files.pythonhosted.org/packages/1f/fd/e5b64f7d2c92a41639ffb2ad44a6a82f347787abc0c7df5f49057cf11770/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f", size = 231724, upload-time = "2025-06-09T23:00:21.705Z" }, + { url = "https://files.pythonhosted.org/packages/20/fb/03395c0a43a5976af4bf7534759d214405fbbb4c114683f434dfdd3128ef/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30", size = 245842, upload-time = "2025-06-09T23:00:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/d0/15/c01c8e1dffdac5d9803507d824f27aed2ba76b6ed0026fab4d9866e82f1f/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98", size = 239767, upload-time = "2025-06-09T23:00:25.103Z" }, + { url = "https://files.pythonhosted.org/packages/14/99/3f4c6fe882c1f5514b6848aa0a69b20cb5e5d8e8f51a339d48c0e9305ed0/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86", size = 224130, upload-time = "2025-06-09T23:00:27.061Z" }, + { url = "https://files.pythonhosted.org/packages/4d/83/220a374bd7b2aeba9d0725130665afe11de347d95c3620b9b82cc2fcab97/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae", size = 235301, upload-time = "2025-06-09T23:00:29.02Z" }, + { url = "https://files.pythonhosted.org/packages/03/3c/3e3390d75334a063181625343e8daab61b77e1b8214802cc4e8a1bb678fc/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8", size = 234606, upload-time = "2025-06-09T23:00:30.514Z" }, + { url = "https://files.pythonhosted.org/packages/23/1e/58232c19608b7a549d72d9903005e2d82488f12554a32de2d5fb59b9b1ba/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31", size = 248372, upload-time = "2025-06-09T23:00:31.966Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/e4a567e01702a88a74ce8a324691e62a629bf47d4f8607f24bf1c7216e7f/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7", size = 229860, upload-time = "2025-06-09T23:00:33.375Z" }, + { url = "https://files.pythonhosted.org/packages/73/a6/63b3374f7d22268b41a9db73d68a8233afa30ed164c46107b33c4d18ecdd/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5", size = 245893, upload-time = "2025-06-09T23:00:35.002Z" }, + { url = "https://files.pythonhosted.org/packages/6d/eb/d18b3f6e64799a79673c4ba0b45e4cfbe49c240edfd03a68be20002eaeaa/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898", size = 246323, upload-time = "2025-06-09T23:00:36.468Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f5/720f3812e3d06cd89a1d5db9ff6450088b8f5c449dae8ffb2971a44da506/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56", size = 233149, upload-time = "2025-06-09T23:00:37.963Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/03efbf545e217d5db8446acfd4c447c15b7c8cf4dbd4a58403111df9322d/frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7", size = 39565, upload-time = "2025-06-09T23:00:39.753Z" }, + { url = "https://files.pythonhosted.org/packages/58/17/fe61124c5c333ae87f09bb67186d65038834a47d974fc10a5fadb4cc5ae1/frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d", size = 44019, upload-time = "2025-06-09T23:00:40.988Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload-time = "2025-06-09T23:00:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload-time = "2025-06-09T23:00:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload-time = "2025-06-09T23:00:44.793Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload-time = "2025-06-09T23:00:46.125Z" }, + { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload-time = "2025-06-09T23:00:47.73Z" }, + { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload-time = "2025-06-09T23:00:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload-time = "2025-06-09T23:00:51.352Z" }, + { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload-time = "2025-06-09T23:00:52.855Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload-time = "2025-06-09T23:00:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload-time = "2025-06-09T23:00:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload-time = "2025-06-09T23:00:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload-time = "2025-06-09T23:01:00.015Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload-time = "2025-06-09T23:01:01.474Z" }, + { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload-time = "2025-06-09T23:01:02.961Z" }, + { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload-time = "2025-06-09T23:01:05.095Z" }, + { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload-time = "2025-06-09T23:01:06.54Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload-time = "2025-06-09T23:01:07.752Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" }, + { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" }, + { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload-time = "2025-06-09T23:01:13.641Z" }, + { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload-time = "2025-06-09T23:01:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload-time = "2025-06-09T23:01:16.752Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload-time = "2025-06-09T23:01:18.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload-time = "2025-06-09T23:01:19.649Z" }, + { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload-time = "2025-06-09T23:01:21.175Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload-time = "2025-06-09T23:01:23.098Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload-time = "2025-06-09T23:01:24.808Z" }, + { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload-time = "2025-06-09T23:01:26.28Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload-time = "2025-06-09T23:01:27.887Z" }, + { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload-time = "2025-06-09T23:01:29.524Z" }, + { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload-time = "2025-06-09T23:01:31.287Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload-time = "2025-06-09T23:01:35.503Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload-time = "2025-06-09T23:01:36.784Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload-time = "2025-06-09T23:01:38.295Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload-time = "2025-06-09T23:01:39.887Z" }, + { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload-time = "2025-06-09T23:01:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload-time = "2025-06-09T23:01:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload-time = "2025-06-09T23:01:44.166Z" }, + { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload-time = "2025-06-09T23:01:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload-time = "2025-06-09T23:01:47.234Z" }, + { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload-time = "2025-06-09T23:01:48.819Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload-time = "2025-06-09T23:01:50.394Z" }, + { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload-time = "2025-06-09T23:01:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload-time = "2025-06-09T23:01:53.788Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload-time = "2025-06-09T23:01:55.769Z" }, + { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload-time = "2025-06-09T23:01:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload-time = "2025-06-09T23:01:58.936Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload-time = "2025-06-09T23:02:00.493Z" }, + { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload-time = "2025-06-09T23:02:02.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload-time = "2025-06-09T23:02:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b1/ee59496f51cd244039330015d60f13ce5a54a0f2bd8d79e4a4a375ab7469/frozenlist-1.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cea3dbd15aea1341ea2de490574a4a37ca080b2ae24e4b4f4b51b9057b4c3630", size = 82434, upload-time = "2025-06-09T23:02:05.195Z" }, + { url = "https://files.pythonhosted.org/packages/75/e1/d518391ce36a6279b3fa5bc14327dde80bcb646bb50d059c6ca0756b8d05/frozenlist-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7d536ee086b23fecc36c2073c371572374ff50ef4db515e4e503925361c24f71", size = 48232, upload-time = "2025-06-09T23:02:07.728Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8d/a0d04f28b6e821a9685c22e67b5fb798a5a7b68752f104bfbc2dccf080c4/frozenlist-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dfcebf56f703cb2e346315431699f00db126d158455e513bd14089d992101e44", size = 47186, upload-time = "2025-06-09T23:02:09.243Z" }, + { url = "https://files.pythonhosted.org/packages/93/3a/a5334c0535c8b7c78eeabda1579179e44fe3d644e07118e59a2276dedaf1/frozenlist-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974c5336e61d6e7eb1ea5b929cb645e882aadab0095c5a6974a111e6479f8878", size = 226617, upload-time = "2025-06-09T23:02:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/0a/67/8258d971f519dc3f278c55069a775096cda6610a267b53f6248152b72b2f/frozenlist-1.7.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c70db4a0ab5ab20878432c40563573229a7ed9241506181bba12f6b7d0dc41cb", size = 224179, upload-time = "2025-06-09T23:02:12.603Z" }, + { url = "https://files.pythonhosted.org/packages/fc/89/8225905bf889b97c6d935dd3aeb45668461e59d415cb019619383a8a7c3b/frozenlist-1.7.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1137b78384eebaf70560a36b7b229f752fb64d463d38d1304939984d5cb887b6", size = 235783, upload-time = "2025-06-09T23:02:14.678Z" }, + { url = "https://files.pythonhosted.org/packages/54/6e/ef52375aa93d4bc510d061df06205fa6dcfd94cd631dd22956b09128f0d4/frozenlist-1.7.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e793a9f01b3e8b5c0bc646fb59140ce0efcc580d22a3468d70766091beb81b35", size = 229210, upload-time = "2025-06-09T23:02:16.313Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/62c87d1a6547bfbcd645df10432c129100c5bd0fd92a384de6e3378b07c1/frozenlist-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74739ba8e4e38221d2c5c03d90a7e542cb8ad681915f4ca8f68d04f810ee0a87", size = 215994, upload-time = "2025-06-09T23:02:17.9Z" }, + { url = "https://files.pythonhosted.org/packages/45/d2/263fea1f658b8ad648c7d94d18a87bca7e8c67bd6a1bbf5445b1bd5b158c/frozenlist-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e63344c4e929b1a01e29bc184bbb5fd82954869033765bfe8d65d09e336a677", size = 225122, upload-time = "2025-06-09T23:02:19.479Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/7145e35d12fb368d92124f679bea87309495e2e9ddf14c6533990cb69218/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ea2a7369eb76de2217a842f22087913cdf75f63cf1307b9024ab82dfb525938", size = 224019, upload-time = "2025-06-09T23:02:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/44/1e/7dae8c54301beb87bcafc6144b9a103bfd2c8f38078c7902984c9a0c4e5b/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:836b42f472a0e006e02499cef9352ce8097f33df43baaba3e0a28a964c26c7d2", size = 239925, upload-time = "2025-06-09T23:02:22.466Z" }, + { url = "https://files.pythonhosted.org/packages/4b/1e/99c93e54aa382e949a98976a73b9b20c3aae6d9d893f31bbe4991f64e3a8/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e22b9a99741294b2571667c07d9f8cceec07cb92aae5ccda39ea1b6052ed4319", size = 220881, upload-time = "2025-06-09T23:02:24.521Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9c/ca5105fa7fb5abdfa8837581be790447ae051da75d32f25c8f81082ffc45/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:9a19e85cc503d958abe5218953df722748d87172f71b73cf3c9257a91b999890", size = 234046, upload-time = "2025-06-09T23:02:26.206Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4d/e99014756093b4ddbb67fb8f0df11fe7a415760d69ace98e2ac6d5d43402/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f22dac33bb3ee8fe3e013aa7b91dc12f60d61d05b7fe32191ffa84c3aafe77bd", size = 235756, upload-time = "2025-06-09T23:02:27.79Z" }, + { url = "https://files.pythonhosted.org/packages/8b/72/a19a40bcdaa28a51add2aaa3a1a294ec357f36f27bd836a012e070c5e8a5/frozenlist-1.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ccec739a99e4ccf664ea0775149f2749b8a6418eb5b8384b4dc0a7d15d304cb", size = 222894, upload-time = "2025-06-09T23:02:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/08/49/0042469993e023a758af81db68c76907cd29e847d772334d4d201cbe9a42/frozenlist-1.7.0-cp39-cp39-win32.whl", hash = "sha256:b3950f11058310008a87757f3eee16a8e1ca97979833239439586857bc25482e", size = 39848, upload-time = "2025-06-09T23:02:31.413Z" }, + { url = "https://files.pythonhosted.org/packages/5a/45/827d86ee475c877f5f766fbc23fb6acb6fada9e52f1c9720e2ba3eae32da/frozenlist-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:43a82fce6769c70f2f5a06248b614a7d268080a9d20f7457ef10ecee5af82b63", size = 44102, upload-time = "2025-06-09T23:02:32.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, +] + +[[package]] +name = "geoip2" +version = "5.1.0" +source = { editable = "." } +dependencies = [ + { name = "aiohttp" }, + { name = "maxminddb" }, + { name = "requests" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-httpserver" }, + { name = "types-requests" }, +] +lint = [ + { name = "mypy" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", specifier = ">=3.6.2,<4.0.0" }, + { name = "maxminddb", specifier = ">=2.7.0,<3.0.0" }, + { name = "requests", specifier = ">=2.24.0,<3.0.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-httpserver", specifier = ">=1.0.10" }, + { name = "types-requests", specifier = ">=2.32.0.20250328" }, +] +lint = [ + { name = "mypy", specifier = ">=1.15.0" }, + { name = "ruff", specifier = ">=0.11.6" }, +] + +[[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, 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, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +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/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 = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +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 = "maxminddb" +version = "2.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/9c/5af549744e7a1e986bddd119c0bbca7f7fa7fb72590b554cb860a0c3acb1/maxminddb-2.8.2.tar.gz", hash = "sha256:26a8e536228d8cc28c5b8f574a571a2704befce3b368ceca593a76d56b6590f9", size = 194388, upload-time = "2025-07-25T20:32:05.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f5/0f66cee71b252934bbdffc7b93de56f83a9f0a85b46d73d3595d39108206/maxminddb-2.8.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3db07d41644fbb712f31d8837feb3109a8b73f42f7ef1be32b3eb84af96f062b", size = 52245, upload-time = "2025-07-25T20:29:55.016Z" }, + { url = "https://files.pythonhosted.org/packages/5b/78/738d0b5d6fd6070175a1a0c7158ffc2615764d21c3b6402ce0ff731fc1c3/maxminddb-2.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb7797d3cf35160f5ed54e12e7bddb12ec011e838bedc9201f7c2987ea284a3c", size = 35194, upload-time = "2025-07-25T20:29:56.445Z" }, + { url = "https://files.pythonhosted.org/packages/b0/bc/a07567c1ae7b60c79fcdeb704e7cf0d87292dd557062a7ee4fdc401bf6b7/maxminddb-2.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:08df1edfb85bd2e30e8f7a2c512be15c5c169492e5972afd3ddab7c498b5aad2", size = 35004, upload-time = "2025-07-25T20:29:58.002Z" }, + { url = "https://files.pythonhosted.org/packages/88/3c/2d009b59b89fad5a3017f2185ef55f59a31fe2a591c2a3ec8d3c27943bdc/maxminddb-2.8.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c671d56b95543a28ec05628fa139d9db9f43f53f09f466b6b2d0dae09adddb", size = 94719, upload-time = "2025-07-25T20:29:59.487Z" }, + { url = "https://files.pythonhosted.org/packages/ce/32/c075774a6873451cbf0afcbb4c4fdba7e9a8c406ec5dc100c1550fbc7529/maxminddb-2.8.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f7453048c0f20750a77091eb38443abf1e30f6d6e41de3b8358ea6e7cd73730", size = 92316, upload-time = "2025-07-25T20:30:01.579Z" }, + { url = "https://files.pythonhosted.org/packages/97/5a/791016f1d4474b17698f6d2145d0336d2f017bf705c480ce12f2c6208833/maxminddb-2.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:990b7993503e77e44baed17f2c7cd1006112f54bd132af354ef4640c6d83a68b", size = 92259, upload-time = "2025-07-25T20:30:03.253Z" }, + { url = "https://files.pythonhosted.org/packages/02/6c/1936c7f43a84676c8f2b02d27cd6199645c35c26e21f36beb92e5d0df086/maxminddb-2.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:027a8bc9e622532196cb84f14f8b18d555b0937a3e0a6e95805db215f98c451b", size = 90418, upload-time = "2025-07-25T20:30:04.364Z" }, + { url = "https://files.pythonhosted.org/packages/95/39/b6192b11d0605c09e9dcb5626bf0a4996f644893adb2b0272433852d7601/maxminddb-2.8.2-cp310-cp310-win32.whl", hash = "sha256:883e17e942631a3b99747a4dc8d55c3e20ac2e342696e828a961d9dcd1811cbb", size = 34598, upload-time = "2025-07-25T20:30:05.531Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3e/e3316093c73da362c3ae921d8b05a1ff2da46917a488c4ed3adb88c3452d/maxminddb-2.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:472d6c61c5c1994989fbdefc7a17adec245330f3e9a11021b9460c5b9f27bcd1", size = 36680, upload-time = "2025-07-25T20:30:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/08/5e/b66837faf2bcc398af6d5b7d51cc7ea30ae46c2870ee13ab580e9328c6b8/maxminddb-2.8.2-cp310-cp310-win_arm64.whl", hash = "sha256:67828addad0cb0ef21fd37549db58a16f219cc1e9c6243b089a726dfe8dfcd34", size = 33035, upload-time = "2025-07-25T20:30:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2a/e61a2544d69ef0d0f31dec9afe943d4e28d2667f9293f490b843620b426b/maxminddb-2.8.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c6d18662c285bb5dfa3b8f2b222c5f77d2521f1d9260a025d8c8b8ec87916f4", size = 52246, upload-time = "2025-07-25T20:30:09.735Z" }, + { url = "https://files.pythonhosted.org/packages/de/c7/429492073b45d50d2a636b890abe54661f3e84c844711f9d57246b7e9739/maxminddb-2.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fd06457cee79e465e72cf21a46c78d5a8574dfeed98b54c106f14f47d237009", size = 35194, upload-time = "2025-07-25T20:30:10.995Z" }, + { url = "https://files.pythonhosted.org/packages/27/b1/a27b00e554ce461c7a4031c6f236a2110e0dc2540c10c2e166d67a82bd45/maxminddb-2.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:711beeb8fda0169c379e77758499f4b7feb56a89327e894fff57bf35d9fe35d5", size = 35006, upload-time = "2025-07-25T20:30:12.085Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/255c7eebcaee9784665b7d73075b3aa60dc72e420db63264f0789e29e774/maxminddb-2.8.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc0eaef5f5a371484542503d70b979e14dd2efded78a19029e78c4e016d7d694", size = 94909, upload-time = "2025-07-25T20:30:13.26Z" }, + { url = "https://files.pythonhosted.org/packages/5b/df/debe55bf6edc34ed0572ea716d9c58c5e42d76df028cda63c86f54445fff/maxminddb-2.8.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a38f213e887c273ba14f563980f15b620bf600576d3ba530dd12416004dcd33", size = 92498, upload-time = "2025-07-25T20:30:14.747Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cc/b0ee8e3807e5adeb7cb9cea6d59f5e3fe63001ca70b9a96ab5bdc7964160/maxminddb-2.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a3fbf0d36cb3fad3743cd2c522855577209c533a782c7176b4d54550928f6935", size = 92466, upload-time = "2025-07-25T20:30:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ca/7bfabf900ff7cadd5b8d5a259619bcb43d8fce4ef482c4d1a79c0e6f9998/maxminddb-2.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b516e113564228ed1965a2454bba901a85984aef599b61e98ce743ce94c22a07", size = 90598, upload-time = "2025-07-25T20:30:17.81Z" }, + { url = "https://files.pythonhosted.org/packages/48/d1/70dfb4cec8190e426f7576384d3adc64ef3bff5b3fd51805c2d49334434c/maxminddb-2.8.2-cp311-cp311-win32.whl", hash = "sha256:c7fc5b3ea6b9a664712544738f14da256981031d0a951e590508a79f4d4a37d1", size = 34595, upload-time = "2025-07-25T20:30:19.362Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0c/3633d901e0bd90933cde5b2b7200ea22f52becb882a474babd9a10031432/maxminddb-2.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:590399b8c6b41aaf42385da412bb0c0690c3db2720fb3a6e7d6967aecc4342ad", size = 36671, upload-time = "2025-07-25T20:30:20.734Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f3/810af19728d1f834d42e7b585301f4842f386c0baa5c61d9c99ee18772da/maxminddb-2.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:f63d07b6a6d402548f153e0cc31fd21ddd7825a457d4da6205fef6b9211361d8", size = 33037, upload-time = "2025-07-25T20:30:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/58/45/ff56248fbaaca9383d18d73aee60a544f0282d71e54af0bf0dea4128fda5/maxminddb-2.8.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bcfb9bc5e31875dd6c1e2de9d748ce403ca5d5d4bc6167973bb0b1bd294bf8d7", size = 52615, upload-time = "2025-07-25T20:30:23.369Z" }, + { url = "https://files.pythonhosted.org/packages/79/44/2703121c2dbba7d03c37294dd407cca2e31dc4542543b93808dd26fd144b/maxminddb-2.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e12bec7f672af46e2177e7c1cd5d330eb969f0dc42f672e250b3d5d72e61778d", size = 35394, upload-time = "2025-07-25T20:30:24.55Z" }, + { url = "https://files.pythonhosted.org/packages/c2/25/99e999e630b1a44936c5261827cc94def5eec82ae57a667a76d641b93925/maxminddb-2.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b23103a754ff1e795d6e107ae23bf9b3360bce9e9bff08c58e388dc2f3fd85ad", size = 35177, upload-time = "2025-07-25T20:30:26.105Z" }, + { url = "https://files.pythonhosted.org/packages/41/21/05c8f50c1b4138516f2bde2810d32c97b84c6d0aefe7e1a1b41635241041/maxminddb-2.8.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c4a10cb799ed3449d063883df962b76b55fdfe0756dfa82eed9765d95e8fd6e", size = 96062, upload-time = "2025-07-25T20:30:27.33Z" }, + { url = "https://files.pythonhosted.org/packages/66/7a/ba7995d1f6b405c057e6f4bd5751fe667535b0ba84f65ee6eb1493bccb80/maxminddb-2.8.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6315977c0512cb7d982bc2eb869355a168f12ef6d2bd5a4f2c93148bc3c03fdc", size = 94208, upload-time = "2025-07-25T20:30:28.932Z" }, + { url = "https://files.pythonhosted.org/packages/99/6f/11cc4b0f1d7f98965ef3304bd9bf2c587f5e84b99aeac27891f5661565cb/maxminddb-2.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b24594f04d03855687b8166ee2c7b788f1e1836b4c5fef2e55fc19327f507ac", size = 93448, upload-time = "2025-07-25T20:30:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/31664be079b71b30895875d6781ae08f871d67de04e518c64422271a8b25/maxminddb-2.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b07b72d9297179c74344aaecad48c88dfdea4422e16721b5955015800d865da2", size = 92240, upload-time = "2025-07-25T20:30:31.658Z" }, + { url = "https://files.pythonhosted.org/packages/a4/19/a5931bb077ccb7e719b8a602fb3ffcd577cdd4954cae3d2b9201272cd462/maxminddb-2.8.2-cp312-cp312-win32.whl", hash = "sha256:51d9717354ee7aa02d52c15115fec2d29bb33f31d6c9f5a8a5aaa2c25dc66e63", size = 34751, upload-time = "2025-07-25T20:30:32.883Z" }, + { url = "https://files.pythonhosted.org/packages/63/50/25720ed19f2d62440b94a1333656cccf6c3c1ce2527ed9abf7b35e2557e1/maxminddb-2.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:18132ccd77ad68863b9022451655cbe1e8fc3c973bafcad66a252eff2732a5c1", size = 36782, upload-time = "2025-07-25T20:30:34.378Z" }, + { url = "https://files.pythonhosted.org/packages/9f/30/1c3121365114678d8df4c02fd416d7520c86b1e37708cc7134ccc3c06e78/maxminddb-2.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:59934eb00274f8b7860927f470a2b9b049842f91e2524a24ade99e16755320f2", size = 33040, upload-time = "2025-07-25T20:30:35.474Z" }, + { url = "https://files.pythonhosted.org/packages/bb/33/06d8d8eb2e422bbff372628c23ce09a2d51f50b9283449c5d8cef0225fe3/maxminddb-2.8.2-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b32a8b61e0dae09c80f41dcd6dc4a442a3cc94b7874a18931daecfea274f640c", size = 36642, upload-time = "2025-07-25T20:30:36.627Z" }, + { url = "https://files.pythonhosted.org/packages/41/c1/dca3608b85d3889760bdf98e931ac66e236f9b8da640f47461c8549fe931/maxminddb-2.8.2-cp313-cp313-android_21_x86_64.whl", hash = "sha256:5f12674cee687cd41c9be1c9ab806bd6a777864e762d5f34ec57c0afa9a21411", size = 37052, upload-time = "2025-07-25T20:30:37.912Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e0/3af26974a2c267939c394d6481723021bdb67af570f948cf510f80e6aeb1/maxminddb-2.8.2-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:995a506a02f70a33ba5ee9f73ce737ef8cdb219bfca3177db79622ebc5624057", size = 34381, upload-time = "2025-07-25T20:30:39.363Z" }, + { url = "https://files.pythonhosted.org/packages/28/ce/26e06d888f057f98b4bc269ee0f8d0ede3dad9684d38e4033acc444b08e5/maxminddb-2.8.2-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5ef9b7f106a1e9ee08f47cd98f7ae80fa40fc0fd40d97cf0d011266738847b52", size = 34918, upload-time = "2025-07-25T20:30:40.512Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a2/0e23f5c33461d1d43d201f2c741c6318d658907833d22cec4ee475d6fab8/maxminddb-2.8.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:adeceeb591755b36a0dc544b92f6d80fc5c112519f5ed8211c34d2ad796bfac0", size = 52619, upload-time = "2025-07-25T20:30:41.645Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ec/3a69a57a9ba4c7d62105fe235642f744bf4ef7cd057f8019a14b1b8eea6d/maxminddb-2.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5c8df08cbdafaa04f7d36a0506e342e4cd679587b56b0fad065b4777e94c8065", size = 35399, upload-time = "2025-07-25T20:30:42.804Z" }, + { url = "https://files.pythonhosted.org/packages/30/b3/b904e778e347ed40e5c82717609e1ecdcdff6c7d7ea2f844a6a20578daef/maxminddb-2.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3e982112e239925c2d8739f834c71539947e54747e56e66c6d960ac356432f32", size = 35165, upload-time = "2025-07-25T20:30:45.534Z" }, + { url = "https://files.pythonhosted.org/packages/34/da/685eeae2ad155d970efabad5ca86ed745665a2ff7576d8fa3d9b9bdb7f8a/maxminddb-2.8.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ef30c32af0107e6b0b9d53f9ae949cf74ddb6882025054bd7500a7b1eb02ec0", size = 96127, upload-time = "2025-07-25T20:30:46.716Z" }, + { url = "https://files.pythonhosted.org/packages/fd/24/a7f54b2b6d808cc4dd485adc004fcd66e103d0aacbf448afd419c0c18380/maxminddb-2.8.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685df893f44606dcb1353b31762b18a2a9537015f1b9e7c0bb3ae74c9fbced32", size = 94250, upload-time = "2025-07-25T20:30:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/6e/cb/bbc5c11201497d7dd42d3240141a8ec484ff704afdf6dff7a7a2de5a6291/maxminddb-2.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3dc27c443cf27b35d4d77ff90fbc6caf1c4e28cffd967775b11cf993af5b9d1", size = 93399, upload-time = "2025-07-25T20:30:50.052Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e6/521c750ea7480fbe362b7bb2821937544313fd3b697f30f4c1975b85c816/maxminddb-2.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:742e857b4411ae3d59c555c2aa96856f72437374cf668c3bed18647092584af6", size = 92250, upload-time = "2025-07-25T20:30:51.259Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4b/9a522ba96a48882c7a954636411f05994573af2eed4b93b511ca6ea3d023/maxminddb-2.8.2-cp313-cp313-win32.whl", hash = "sha256:1fba9c16f5e492eee16362e8204aaec30241167a3466874ca9b0521dec32d63e", size = 34759, upload-time = "2025-07-25T20:30:52.936Z" }, + { url = "https://files.pythonhosted.org/packages/e8/4a/e0d7451b56821fe0ec794a917cceb67efac8510013783cc5713b733d5ff4/maxminddb-2.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:cfbfee615d2566124cb6232401d89f15609f5297eb4f022f1f6a14205c091df6", size = 36771, upload-time = "2025-07-25T20:30:54.076Z" }, + { url = "https://files.pythonhosted.org/packages/71/27/abffb686514905994ef26191971ca30765c45e391d82ee2ea6b2ecfe1bad/maxminddb-2.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:2ade954d94087039fc45de99eeae0e2f0480d69a767abd417bd0742bf5d177ab", size = 33041, upload-time = "2025-07-25T20:30:55.567Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/844530632ef917f622742d6d5beae5c3ebed7d424af02bf428b639e42a41/maxminddb-2.8.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7d5db6d4f8caaf7b753a0f6782765ea5352409ef6d430196b0dc7c61c0a8c72b", size = 34384, upload-time = "2025-07-25T20:30:57.046Z" }, + { url = "https://files.pythonhosted.org/packages/ee/6c/ff9555963983d99a201a5068ab037c92583cd8422046d7064e2cab92c09f/maxminddb-2.8.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:bda6015f617b4ec6f1a49ae74b1a36c10d997602d3e9141514ef11983e6ddf8d", size = 34929, upload-time = "2025-07-25T20:30:58.194Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c2/8d093e973edb1ca0ad54a80f124b4e8d1db5508a00c0f98765d0df6bd4d5/maxminddb-2.8.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:4e32f5608af05bc0b6cee91edd0698f6a310ae9dd0f3cebfb524a6b444c003a2", size = 52616, upload-time = "2025-07-25T20:30:59.294Z" }, + { url = "https://files.pythonhosted.org/packages/5d/85/8442162353c28ff0679f348d2099f24d9be9b84f9ffa1ed21e8ecafe64dc/maxminddb-2.8.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5abf18c51f3a3e5590ea77d43bff159a9f88cec1f95a7e3fc2a39a21fc8f9e7c", size = 35405, upload-time = "2025-07-25T20:31:00.821Z" }, + { url = "https://files.pythonhosted.org/packages/14/df/f37d5b2605ae0f1d3f87d45ddbab032f36b2cae29f80f02c390001b35677/maxminddb-2.8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3c8d57063ff2c6d0690e5d907a10b5b6ba64e0ab5e6d8661b6075fbda854e97d", size = 35174, upload-time = "2025-07-25T20:31:02.112Z" }, + { url = "https://files.pythonhosted.org/packages/32/12/5d562de6243b8631f9480b7deac92cb62ec5ae8aecd4e3ccdaecfc177c24/maxminddb-2.8.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73d603c7202e1338bdbb3ead8a3db4f74825e419ecc8733ef8a76c14366800d2", size = 96060, upload-time = "2025-07-25T20:31:03.318Z" }, + { url = "https://files.pythonhosted.org/packages/3a/95/04c8c2526e4c0c0d2894052c7d07f39c9b8d1185bd2da5752de2effc287a/maxminddb-2.8.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:acca37ed0372efa01251da32db1a5d81189369449bc4b943d3087ebc9e30e814", size = 94013, upload-time = "2025-07-25T20:31:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/c7/98/7870de3e5cf362c567c0a9cf7a8834d3699fe0a52e601fc352c902d3ebc7/maxminddb-2.8.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1e1e3ef04a686cf7d893a8274ddc0081bd40121ac4923b67e8caa902094ac111", size = 93350, upload-time = "2025-07-25T20:31:05.815Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ef/7eb25529011cf0e18fb529792ad5225b402a3e80728cfbd7604e53c5ada3/maxminddb-2.8.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6657615038d8fe106acccd2bf4fe073d07f72886ee893725c74649687635a1a", size = 92036, upload-time = "2025-07-25T20:31:07.03Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9d/12926eac198a920a2c4f9ce6e57de33d47a6c40ccb1637362abfd268f017/maxminddb-2.8.2-cp314-cp314-win32.whl", hash = "sha256:af058500ab3448b709c43f1aefd3d9f7c5f1773af07611d589502ea78bf2b9dc", size = 35403, upload-time = "2025-07-25T20:31:08.221Z" }, + { url = "https://files.pythonhosted.org/packages/c6/eb/48636b611f604bb072b26be16e6990694bbfdd57553622a784b17c1999c7/maxminddb-2.8.2-cp314-cp314-win_amd64.whl", hash = "sha256:b5982d1b53b50b96a9afcf4f7f49db0a842501f9cf58c4c16c0d62c1b0d22840", size = 37559, upload-time = "2025-07-25T20:31:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/05/4a/27e53d1b9b7b168f259bbfccec1d1383d51c07e112d7bd24e543042e07a1/maxminddb-2.8.2-cp314-cp314-win_arm64.whl", hash = "sha256:48c9f7e182c6e970a412c02e7438c2a66197c0664d0c7da81b951bff86519dd5", size = 33614, upload-time = "2025-07-25T20:31:10.555Z" }, + { url = "https://files.pythonhosted.org/packages/eb/43/e49927eb381fb44c9a06a5ac06da039951fde90bf47f100b495f082d6b37/maxminddb-2.8.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b40ed2ec586a5a479d08bd39838fbfbdff84d7deb57089317f312609f1357384", size = 53708, upload-time = "2025-07-25T20:31:11.642Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d0/ff081ac508358b3a9ca1f0b39d5bf74904aa644b45d2d6d8b9112ad9566e/maxminddb-2.8.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1ba4036f823a8e6418af0d69734fb176e3d1edd0432e218f3be8362564b53ea5", size = 35925, upload-time = "2025-07-25T20:31:12.804Z" }, + { url = "https://files.pythonhosted.org/packages/bc/30/f94d3acca0314f038a4f1cb83ccbdf0a56b9f13454bab9667af0506ecca0/maxminddb-2.8.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:96531e18bddff9639061ee543417f941a2fd41efc7b1699e1e18aba4157b0b03", size = 35757, upload-time = "2025-07-25T20:31:14.322Z" }, + { url = "https://files.pythonhosted.org/packages/b0/21/5710a5aa7f83453fcf36cee11ed113c110a53cdc5a4ecf82904be797101b/maxminddb-2.8.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb77ad5c585d6255001d701eafc4758e2d28953ba47510d9f54cc2a9e469c6b6", size = 104991, upload-time = "2025-07-25T20:31:15.542Z" }, + { url = "https://files.pythonhosted.org/packages/47/0c/8cf559f850c3e43e6f490fad458293fdb0b70debbe3fcbf7d7713558044f/maxminddb-2.8.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bfd950af416ef4133bc04b059f29ac4d4b356927fa4a500048220d65ec4c6ac", size = 101935, upload-time = "2025-07-25T20:31:16.83Z" }, + { url = "https://files.pythonhosted.org/packages/02/47/104ef451772d1cd852dea2334c2dfb02d6de7caf8d31e1358f10b9af6769/maxminddb-2.8.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bf73612f8fbfa9181ba62fa88fb3d732bdc775017bdb3725e24cdd1a0da92d4", size = 101653, upload-time = "2025-07-25T20:31:18.104Z" }, + { url = "https://files.pythonhosted.org/packages/60/03/139791f82e3857d4d0638494647f74d997a2abded7048ab4ed4622a089ad/maxminddb-2.8.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:74361fbddb0566970af38cff0a6256ec3f445cb5031da486d0cee6f19ccb9e2e", size = 99517, upload-time = "2025-07-25T20:31:19.764Z" }, + { url = "https://files.pythonhosted.org/packages/4c/45/c625fc2b84b8dcf2181eb411f130729446164215409c8e0c8fd01a53f388/maxminddb-2.8.2-cp314-cp314t-win32.whl", hash = "sha256:6bfb41c3a560a60fc20d0d87cb400003974fbb833b44571250476c2d9cb4d407", size = 36349, upload-time = "2025-07-25T20:31:21.004Z" }, + { url = "https://files.pythonhosted.org/packages/27/8d/46c202be273fd8ec985686e1fdd84ad55c7234dc66d82d6d59e5caf438e4/maxminddb-2.8.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ec6bba1b1f0fd0846aac5b0af1f84804c67702e873aa9d79c9965794a635ada8", size = 38595, upload-time = "2025-07-25T20:31:22.185Z" }, + { url = "https://files.pythonhosted.org/packages/62/33/09601f476fd9d494e967f15c1e05aa1e35bdf5ee54555596e05e5c9ec8c9/maxminddb-2.8.2-cp314-cp314t-win_arm64.whl", hash = "sha256:929a00528db82ffa5aa928a9cd1a972e8f93c36243609c25574dfd920c21533b", size = 33990, upload-time = "2025-07-25T20:31:23.367Z" }, + { url = "https://files.pythonhosted.org/packages/39/e3/238393797fd82c34c54990c4d4546ae34315735c9219fe7e0c8d2a3d74ee/maxminddb-2.8.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9b27485e54eee7c251846cfc3b3277b1fdbdae6b6bbc26015c360de7ce78ae33", size = 52244, upload-time = "2025-07-25T20:31:24.521Z" }, + { url = "https://files.pythonhosted.org/packages/85/87/c9c1d53a8b23cc00ce310c803bd54dfda3f10544f04f3faf2c4d1f0321c3/maxminddb-2.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c335db4abdd79e3846deb2aa72374284eae78bb2622a82a29c5fd7dd42741a11", size = 35199, upload-time = "2025-07-25T20:31:25.782Z" }, + { url = "https://files.pythonhosted.org/packages/53/b9/0b119b8ca2b0116d7f09efb24d8cf680ef20943d7995d804acf179b89b38/maxminddb-2.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c6ff6b84327bb4521068ab6e62f6b537641d106b1acabbdc6436ab7a74ce1328", size = 35002, upload-time = "2025-07-25T20:31:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/27/3d/6a97e72bebc2d2947554b69a68203fa352c0868aa7f2fff0b98736217bc2/maxminddb-2.8.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dccb69b63aac9b9b7c5f251e9abc0c945c9bd1681869ca72b7e6f512009b541", size = 94459, upload-time = "2025-07-25T20:31:28.613Z" }, + { url = "https://files.pythonhosted.org/packages/2f/1b/3576d131f6d77288036a314551511b66d0ae0d56a1cba0fc86b145d7a419/maxminddb-2.8.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9efa8a04f546f3c91a235256d61f2985f0a45bb1ec3559bbb551906c015d9464", size = 92076, upload-time = "2025-07-25T20:31:29.919Z" }, + { url = "https://files.pythonhosted.org/packages/84/84/636a728c0df7de1a1df21ae55512b421e9c156c27c48bfe3f96e727038ba/maxminddb-2.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5853b9f1fb4fc2b394b6ddce33a0be6711b80c8df86498a6e9e90057f0e7276f", size = 91981, upload-time = "2025-07-25T20:31:31.587Z" }, + { url = "https://files.pythonhosted.org/packages/14/da/c98f2e60398f1c0070fa5ac134230014cd6b9a05080316474add4d2ad88a/maxminddb-2.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0d39044f19696a3bca319539c8cd159c3c5af99d1ee381da6e4b273b6a27c728", size = 90179, upload-time = "2025-07-25T20:31:33.914Z" }, + { url = "https://files.pythonhosted.org/packages/b7/16/36012be72ac75910c93dd07c85c983e51d1f558da8064c38888e49b7f74c/maxminddb-2.8.2-cp39-cp39-win32.whl", hash = "sha256:56a84983debc7b8d9874c9c739106b860f9d4f120b0179085ffb500704c31266", size = 34603, upload-time = "2025-07-25T20:31:35.131Z" }, + { url = "https://files.pythonhosted.org/packages/a9/79/62d637834c86c15d98a813c76df5c6839c3445d19f90f6ffa8cf489dbf5c/maxminddb-2.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:2f754550d51c25233853cdcbae1ee384a2af9e3e422b54b992bd4cef6332f894", size = 36682, upload-time = "2025-07-25T20:31:36.385Z" }, + { url = "https://files.pythonhosted.org/packages/d4/96/4780cd9f6caa3c60f8d0d11fc102ef5f3283af656eec2cd581244ae96b8c/maxminddb-2.8.2-cp39-cp39-win_arm64.whl", hash = "sha256:1c319d257fa3e8225ec2eece0043687ad64bf3968de9432187376eb97c2ac6da", size = 33025, upload-time = "2025-07-25T20:31:37.565Z" }, + { url = "https://files.pythonhosted.org/packages/b9/46/741e1945fc64f7cf5a5d399a15c673d5d30899480db17ddaea270c41f120/maxminddb-2.8.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ed8d6742e66b119e66a658307bba5da32ba3f7e4e99a35a770dcf924e51326a5", size = 34209, upload-time = "2025-07-25T20:31:38.681Z" }, + { url = "https://files.pythonhosted.org/packages/24/13/78361b264ccc275c7e64a3ba29951560d0231990bf64d03cd9cc6a561e67/maxminddb-2.8.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:464b6e4269b9feea12c63eb1561038fac5f1b449a14b78be250ad081b560ff3c", size = 33806, upload-time = "2025-07-25T20:31:39.804Z" }, + { url = "https://files.pythonhosted.org/packages/84/dc/9e4578ba5a44057d8cc843aa139bf70f2a4d6b3a2d2be5eb6b5848836346/maxminddb-2.8.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:833247b194d86bc62e16d36169336daebba777414821fd0003b1ecfc6bb3f1a7", size = 38143, upload-time = "2025-07-25T20:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/73/19/f7922739c61aed246f5d6e032e7d3df4239c33ffb090a8eee5a644c80d35/maxminddb-2.8.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d8d30c6038bdc7ad0458598e4b8c54f19cb052853ac84a0be8902c7af3a009f", size = 36974, upload-time = "2025-07-25T20:31:42.21Z" }, + { url = "https://files.pythonhosted.org/packages/be/54/28bddcd972a665244f6714a1979b7bea01fb4f689e4fa178e28b65d4fbb9/maxminddb-2.8.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f6da4d844f176b7a662446107dd09b987759126c2d8c266918fe7f0186d41538", size = 36719, upload-time = "2025-07-25T20:31:43.444Z" }, + { url = "https://files.pythonhosted.org/packages/55/a9/50aa454bdf8aa76c7c8cf8343b039461203d4b53d5c3f4eecdb180574981/maxminddb-2.8.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:28205d215b426c31c35ecc2e71f6ee22ebf12a9a7560ed1efec3709e343d720b", size = 34139, upload-time = "2025-07-25T20:31:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/a2/af/610036e75aa0aebc67e47f89aea73cc2fa92288eb72f4141cf061e0e5673/maxminddb-2.8.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:88b7be82d81a4de2ea40e9bd1f39074ac2d127268a328ad524500c3c210eced1", size = 33735, upload-time = "2025-07-25T20:31:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/b8f8748405c344c03684b12267ec7d8e99c33d8c610da76892ce9a1827f2/maxminddb-2.8.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9a37c151ccdff7ae0be86eff1c464db02237e428f079300b3efc07277762334", size = 38140, upload-time = "2025-07-25T20:31:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/46/ec/25a20b61cf43b2fab1524817f59116132e40c5a272a0dfca1c465ed66324/maxminddb-2.8.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ff2045eadfad106824ff4fe2045e7f8ca737405e3201a9adfa646e2e6cdfad7", size = 36975, upload-time = "2025-07-25T20:31:49.181Z" }, + { url = "https://files.pythonhosted.org/packages/d5/10/8ed5b99189eb380bf7166fd38594f9457c5ba587a3300cc1ec64ddc4a0a6/maxminddb-2.8.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:869add1b2c9c48008e13c8db204b681a82cbe815c5f58ab8267205b522c852c0", size = 36718, upload-time = "2025-07-25T20:31:51.976Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f5/9b51102f1e07f891330040a2b6628706eed87d7d9df7164867dd726355a3/maxminddb-2.8.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8d85e20807ee11494fce001cffdb1364729e154041739813fb261f866865522c", size = 34205, upload-time = "2025-07-25T20:31:53.772Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a0/df86f19ba49863bb264f4f34655c8b7727979ab0b792061a93bb47603774/maxminddb-2.8.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:622fde1542a4753a39253d138438e1f543edb8455fd70a8f4afbe0a0bc04fe1e", size = 33807, upload-time = "2025-07-25T20:31:55.743Z" }, + { url = "https://files.pythonhosted.org/packages/db/a8/6bd38cf4e40f6144c21b48952a20e9f4d90c43de740939652939b0b93ce2/maxminddb-2.8.2-pp39-pypy39_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79492896ec7f6e029c2aa92c4cc10ad0347a03b866025bd26a6f415982a833de", size = 38142, upload-time = "2025-07-25T20:31:59.374Z" }, + { url = "https://files.pythonhosted.org/packages/01/61/a92ba49c681ac2c039a06d07847c255bbfd4956f849242107f9b0fd85307/maxminddb-2.8.2-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd42526b902755d383108bf2ba38fb9a946ec369faeead3cbe8ffc034a0462e0", size = 36976, upload-time = "2025-07-25T20:32:01.43Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9b/2444b0dd5adba12b6ea33065afa4e4abc89e08b64339dded64d3b3964929/maxminddb-2.8.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:40e113e56ae90d3410bbfc20f5510308c29aa6815964f59859aff4187d21db8c", size = 36723, upload-time = "2025-07-25T20:32:03.127Z" }, +] + +[[package]] +name = "multidict" +version = "6.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/b5/59f27b4ce9951a4bce56b88ba5ff5159486797ab18863f2b4c1c5e8465bd/multidict-6.5.0.tar.gz", hash = "sha256:942bd8002492ba819426a8d7aefde3189c1b87099cdf18aaaefefcf7f3f7b6d2", size = 98512, upload-time = "2025-06-17T14:15:56.556Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/88/f8354ef1cb1121234c3461ff3d11eac5f4fe115f00552d3376306275c9ab/multidict-6.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e118a202904623b1d2606d1c8614e14c9444b59d64454b0c355044058066469", size = 73858, upload-time = "2025-06-17T14:13:21.451Z" }, + { url = "https://files.pythonhosted.org/packages/49/04/634b49c7abe71bd1c61affaeaa0c2a46b6be8d599a07b495259615dbdfe0/multidict-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a42995bdcaff4e22cb1280ae7752c3ed3fbb398090c6991a2797a4a0e5ed16a9", size = 43186, upload-time = "2025-06-17T14:13:23.615Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ff/091ff4830ec8f96378578bfffa7f324a9dd16f60274cec861ae65ba10be3/multidict-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2261b538145723ca776e55208640fffd7ee78184d223f37c2b40b9edfe0e818a", size = 43031, upload-time = "2025-06-17T14:13:24.725Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/1b4137845f8b8dbc2332af54e2d7761c6a29c2c33c8d47a0c8c70676bac1/multidict-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e5b19f8cd67235fab3e195ca389490415d9fef5a315b1fa6f332925dc924262", size = 233588, upload-time = "2025-06-17T14:13:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/c3/77/cbe9a1f58c6d4f822663788e414637f256a872bc352cedbaf7717b62db58/multidict-6.5.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:177b081e4dec67c3320b16b3aa0babc178bbf758553085669382c7ec711e1ec8", size = 222714, upload-time = "2025-06-17T14:13:27.482Z" }, + { url = "https://files.pythonhosted.org/packages/6c/37/39e1142c2916973818515adc13bbdb68d3d8126935e3855200e059a79bab/multidict-6.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d30a2cc106a7d116b52ee046207614db42380b62e6b1dd2a50eba47c5ca5eb1", size = 242741, upload-time = "2025-06-17T14:13:28.92Z" }, + { url = "https://files.pythonhosted.org/packages/a3/aa/60c3ef0c87ccad3445bf01926a1b8235ee24c3dde483faef1079cc91706d/multidict-6.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a72933bc308d7a64de37f0d51795dbeaceebdfb75454f89035cdfc6a74cfd129", size = 235008, upload-time = "2025-06-17T14:13:30.587Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5e/f7e0fd5f5b8a7b9a75b0f5642ca6b6dde90116266920d8cf63b513f3908b/multidict-6.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96d109e663d032280ef8ef62b50924b2e887d5ddf19e301844a6cb7e91a172a6", size = 226627, upload-time = "2025-06-17T14:13:31.831Z" }, + { url = "https://files.pythonhosted.org/packages/b7/74/1bc0a3c6a9105051f68a6991fe235d7358836e81058728c24d5bbdd017cb/multidict-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b555329c9894332401f03b9a87016f0b707b6fccd4706793ec43b4a639e75869", size = 228232, upload-time = "2025-06-17T14:13:33.402Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/37118291cdc31f4cc680d54047cdea9b520e9a724a643919f71f8c2a2aeb/multidict-6.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6994bad9d471ef2156f2b6850b51e20ee409c6b9deebc0e57be096be9faffdce", size = 246616, upload-time = "2025-06-17T14:13:34.964Z" }, + { url = "https://files.pythonhosted.org/packages/ff/89/e2c08d6bdb21a1a55be4285510d058ace5f5acabe6b57900432e863d4c70/multidict-6.5.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:b15f817276c96cde9060569023808eec966bd8da56a97e6aa8116f34ddab6534", size = 235007, upload-time = "2025-06-17T14:13:36.428Z" }, + { url = "https://files.pythonhosted.org/packages/89/1e/e39a98e8e1477ec7a871b3c17265658fbe6d617048059ae7fa5011b224f3/multidict-6.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b4bf507c991db535a935b2127cf057a58dbc688c9f309c72080795c63e796f58", size = 244824, upload-time = "2025-06-17T14:13:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ba/63e11edd45c31e708c5a1904aa7ac4de01e13135a04cfe96bc71eb359b85/multidict-6.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:60c3f8f13d443426c55f88cf3172547bbc600a86d57fd565458b9259239a6737", size = 257229, upload-time = "2025-06-17T14:13:39.554Z" }, + { url = "https://files.pythonhosted.org/packages/0f/00/bdcceb6af424936adfc8b92a79d3a95863585f380071393934f10a63f9e3/multidict-6.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a10227168a24420c158747fc201d4279aa9af1671f287371597e2b4f2ff21879", size = 247118, upload-time = "2025-06-17T14:13:40.795Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a0/4aa79e991909cca36ca821a9ba5e8e81e4cd5b887c81f89ded994e0f49df/multidict-6.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e3b1425fe54ccfde66b8cfb25d02be34d5dfd2261a71561ffd887ef4088b4b69", size = 243948, upload-time = "2025-06-17T14:13:42.477Z" }, + { url = "https://files.pythonhosted.org/packages/21/8b/e45e19ce43afb31ff6b0fd5d5816b4fcc1fcc2f37e8a82aefae06c40c7a6/multidict-6.5.0-cp310-cp310-win32.whl", hash = "sha256:b4e47ef51237841d1087e1e1548071a6ef22e27ed0400c272174fa585277c4b4", size = 40433, upload-time = "2025-06-17T14:13:43.972Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6e/96e0ba4601343d9344e69503fca072ace19c35f7d4ca3d68401e59acdc8f/multidict-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:63b3b24fadc7067282c88fae5b2f366d5b3a7c15c021c2838de8c65a50eeefb4", size = 44423, upload-time = "2025-06-17T14:13:44.991Z" }, + { url = "https://files.pythonhosted.org/packages/eb/4a/9befa919d7a390f13a5511a69282b7437782071160c566de6e0ebf712c9f/multidict-6.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:8b2d61afbafc679b7eaf08e9de4fa5d38bd5dc7a9c0a577c9f9588fb49f02dbb", size = 41481, upload-time = "2025-06-17T14:13:49.389Z" }, + { url = "https://files.pythonhosted.org/packages/75/ba/484f8e96ee58ec4fef42650eb9dbbedb24f9bc155780888398a4725d2270/multidict-6.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8b4bf6bb15a05796a07a248084e3e46e032860c899c7a9b981030e61368dba95", size = 73283, upload-time = "2025-06-17T14:13:50.406Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/01d62ea6199d76934c87746695b3ed16aeedfdd564e8d89184577037baac/multidict-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46bb05d50219655c42a4b8fcda9c7ee658a09adbb719c48e65a20284e36328ea", size = 42937, upload-time = "2025-06-17T14:13:51.45Z" }, + { url = "https://files.pythonhosted.org/packages/da/cf/bb462d920f26d9e2e0aff8a78aeb06af1225b826e9a5468870c57591910a/multidict-6.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:54f524d73f4d54e87e03c98f6af601af4777e4668a52b1bd2ae0a4d6fc7b392b", size = 42748, upload-time = "2025-06-17T14:13:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b1/d5c11ea0fdad68d3ed45f0e2527de6496d2fac8afe6b8ca6d407c20ad00f/multidict-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529b03600466480ecc502000d62e54f185a884ed4570dee90d9a273ee80e37b5", size = 236448, upload-time = "2025-06-17T14:13:53.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/69/c3ceb264994f5b338c812911a8d660084f37779daef298fc30bd817f75c7/multidict-6.5.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69ad681ad7c93a41ee7005cc83a144b5b34a3838bcf7261e2b5356057b0f78de", size = 228695, upload-time = "2025-06-17T14:13:54.775Z" }, + { url = "https://files.pythonhosted.org/packages/81/3d/c23dcc0d34a35ad29974184db2878021d28fe170ecb9192be6bfee73f1f2/multidict-6.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fe9fada8bc0839466b09fa3f6894f003137942984843ec0c3848846329a36ae", size = 247434, upload-time = "2025-06-17T14:13:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/06/b3/06cf7a049129ff52525a859277abb5648e61d7afae7fb7ed02e3806be34e/multidict-6.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f94c6ea6405fcf81baef1e459b209a78cda5442e61b5b7a57ede39d99b5204a0", size = 239431, upload-time = "2025-06-17T14:13:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/8a/72/b2fe2fafa23af0c6123aebe23b4cd23fdad01dfe7009bb85624e4636d0dd/multidict-6.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ca75ad8a39ed75f079a8931435a5b51ee4c45d9b32e1740f99969a5d1cc2ee", size = 231542, upload-time = "2025-06-17T14:13:58.597Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c9/a52ca0a342a02411a31b6af197a6428a5137d805293f10946eeab614ec06/multidict-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be4c08f3a2a6cc42b414496017928d95898964fed84b1b2dace0c9ee763061f9", size = 233069, upload-time = "2025-06-17T14:13:59.834Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/a3328a3929b8e131e2678d5e65f552b0a6874fab62123e31f5a5625650b0/multidict-6.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:046a7540cfbb4d5dc846a1fd9843f3ba980c6523f2e0c5b8622b4a5c94138ae6", size = 250596, upload-time = "2025-06-17T14:14:01.178Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b8/aa3905a38a8287013aeb0a54c73f79ccd8b32d2f1d53e5934643a36502c2/multidict-6.5.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:64306121171d988af77d74be0d8c73ee1a69cf6f96aea7fa6030c88f32a152dd", size = 237858, upload-time = "2025-06-17T14:14:03.232Z" }, + { url = "https://files.pythonhosted.org/packages/d3/eb/f11d5af028014f402e5dd01ece74533964fa4e7bfae4af4824506fa8c398/multidict-6.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b4ac1dd5eb0ecf6f7351d5a9137f30a83f7182209c5d37f61614dfdce5714853", size = 249175, upload-time = "2025-06-17T14:14:04.561Z" }, + { url = "https://files.pythonhosted.org/packages/ac/57/d451905a62e5ef489cb4f92e8190d34ac5329427512afd7f893121da4e96/multidict-6.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bab4a8337235365f4111a7011a1f028826ca683834ebd12de4b85e2844359c36", size = 259532, upload-time = "2025-06-17T14:14:05.798Z" }, + { url = "https://files.pythonhosted.org/packages/d3/90/ff82b5ac5cabe3c79c50cf62a62f3837905aa717e67b6b4b7872804f23c8/multidict-6.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a05b5604c5a75df14a63eeeca598d11b2c3745b9008539b70826ea044063a572", size = 250554, upload-time = "2025-06-17T14:14:07.382Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5a/0cabc50d4bc16e61d8b0a8a74499a1409fa7b4ef32970b7662a423781fc7/multidict-6.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67c4a640952371c9ca65b6a710598be246ef3be5ca83ed38c16a7660d3980877", size = 248159, upload-time = "2025-06-17T14:14:08.65Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1d/adeabae0771544f140d9f42ab2c46eaf54e793325999c36106078b7f6600/multidict-6.5.0-cp311-cp311-win32.whl", hash = "sha256:fdeae096ca36c12d8aca2640b8407a9d94e961372c68435bef14e31cce726138", size = 40357, upload-time = "2025-06-17T14:14:09.91Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/bbd85ae65c96de5c9910c332ee1f4b7be0bf0fb21563895167bcb6502a1f/multidict-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:e2977ef8b7ce27723ee8c610d1bd1765da4f3fbe5a64f9bf1fd3b4770e31fbc0", size = 44432, upload-time = "2025-06-17T14:14:11.013Z" }, + { url = "https://files.pythonhosted.org/packages/96/af/f9052d9c4e65195b210da9f7afdea06d3b7592b3221cc0ef1b407f762faa/multidict-6.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:82d0cf0ea49bae43d9e8c3851e21954eff716259ff42da401b668744d1760bcb", size = 41408, upload-time = "2025-06-17T14:14:12.112Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fa/18f4950e00924f7e84c8195f4fc303295e14df23f713d64e778b8fa8b903/multidict-6.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1bb986c8ea9d49947bc325c51eced1ada6d8d9b4c5b15fd3fcdc3c93edef5a74", size = 73474, upload-time = "2025-06-17T14:14:13.528Z" }, + { url = "https://files.pythonhosted.org/packages/6c/66/0392a2a8948bccff57e4793c9dde3e5c088f01e8b7f8867ee58a2f187fc5/multidict-6.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:03c0923da300120830fc467e23805d63bbb4e98b94032bd863bc7797ea5fa653", size = 43741, upload-time = "2025-06-17T14:14:15.188Z" }, + { url = "https://files.pythonhosted.org/packages/98/3e/f48487c91b2a070566cfbab876d7e1ebe7deb0a8002e4e896a97998ae066/multidict-6.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4c78d5ec00fdd35c91680ab5cf58368faad4bd1a8721f87127326270248de9bc", size = 42143, upload-time = "2025-06-17T14:14:16.612Z" }, + { url = "https://files.pythonhosted.org/packages/3f/49/439c6cc1cd00365cf561bdd3579cc3fa1a0d38effb3a59b8d9562839197f/multidict-6.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadc3cb78be90a887f8f6b73945b840da44b4a483d1c9750459ae69687940c97", size = 239303, upload-time = "2025-06-17T14:14:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/c4/24/491786269e90081cb536e4d7429508725bc92ece176d1204a4449de7c41c/multidict-6.5.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b02e1ca495d71e07e652e4cef91adae3bf7ae4493507a263f56e617de65dafc", size = 236913, upload-time = "2025-06-17T14:14:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/e8/76/bbe2558b820ebeca8a317ab034541790e8160ca4b1e450415383ac69b339/multidict-6.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7fe92a62326eef351668eec4e2dfc494927764a0840a1895cff16707fceffcd3", size = 250752, upload-time = "2025-06-17T14:14:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e3/3977f2c1123f553ceff9f53cd4de04be2c1912333c6fabbcd51531655476/multidict-6.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7673ee4f63879ecd526488deb1989041abcb101b2d30a9165e1e90c489f3f7fb", size = 243937, upload-time = "2025-06-17T14:14:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b8/7a6e9c13c79709cdd2f22ee849f058e6da76892d141a67acc0e6c30d845c/multidict-6.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa097ae2a29f573de7e2d86620cbdda5676d27772d4ed2669cfa9961a0d73955", size = 237419, upload-time = "2025-06-17T14:14:23.215Z" }, + { url = "https://files.pythonhosted.org/packages/84/9d/8557f5e88da71bc7e7a8ace1ada4c28197f3bfdc2dd6e51d3b88f2e16e8e/multidict-6.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:300da0fa4f8457d9c4bd579695496116563409e676ac79b5e4dca18e49d1c308", size = 237222, upload-time = "2025-06-17T14:14:24.516Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3b/8f023ad60e7969cb6bc0683738d0e1618f5ff5723d6d2d7818dc6df6ad3d/multidict-6.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a19bd108c35877b57393243d392d024cfbfdefe759fd137abb98f6fc910b64c", size = 247861, upload-time = "2025-06-17T14:14:25.839Z" }, + { url = "https://files.pythonhosted.org/packages/af/1c/9cf5a099ce7e3189906cf5daa72c44ee962dcb4c1983659f3a6f8a7446ab/multidict-6.5.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f32a1777465a35c35ddbbd7fc1293077938a69402fcc59e40b2846d04a120dd", size = 243917, upload-time = "2025-06-17T14:14:27.164Z" }, + { url = "https://files.pythonhosted.org/packages/6c/bb/88ee66ebeef56868044bac58feb1cc25658bff27b20e3cfc464edc181287/multidict-6.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9cc1e10c14ce8112d1e6d8971fe3cdbe13e314f68bea0e727429249d4a6ce164", size = 249214, upload-time = "2025-06-17T14:14:28.795Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/a90e88cc4a1309f33088ab1cdd5c0487718f49dfb82c5ffc845bb17c1973/multidict-6.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e95c5e07a06594bdc288117ca90e89156aee8cb2d7c330b920d9c3dd19c05414", size = 258682, upload-time = "2025-06-17T14:14:30.066Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/16dd69a6811920a31f4e06114ebe67b1cd922c8b05c9c82b050706d0b6fe/multidict-6.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:40ff26f58323795f5cd2855e2718a1720a1123fb90df4553426f0efd76135462", size = 254254, upload-time = "2025-06-17T14:14:31.323Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a8/90193a5f5ca1bdbf92633d69a25a2ef9bcac7b412b8d48c84d01a2732518/multidict-6.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76803a29fd71869a8b59c2118c9dcfb3b8f9c8723e2cce6baeb20705459505cf", size = 247741, upload-time = "2025-06-17T14:14:32.717Z" }, + { url = "https://files.pythonhosted.org/packages/cd/43/29c7a747153c05b41d1f67455426af39ed88d6de3f21c232b8f2724bde13/multidict-6.5.0-cp312-cp312-win32.whl", hash = "sha256:df7ecbc65a53a2ce1b3a0c82e6ad1a43dcfe7c6137733f9176a92516b9f5b851", size = 41049, upload-time = "2025-06-17T14:14:33.941Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e8/8f3fc32b7e901f3a2719764d64aeaf6ae77b4ba961f1c3a3cf3867766636/multidict-6.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ec1c3fbbb0b655a6540bce408f48b9a7474fd94ed657dcd2e890671fefa7743", size = 44700, upload-time = "2025-06-17T14:14:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/24/e4/e250806adc98d524d41e69c8d4a42bc3513464adb88cb96224df12928617/multidict-6.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:2d24a00d34808b22c1f15902899b9d82d0faeca9f56281641c791d8605eacd35", size = 41703, upload-time = "2025-06-17T14:14:36.168Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c9/092c4e9402b6d16de761cff88cb842a5c8cc50ccecaf9c4481ba53264b9e/multidict-6.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:53d92df1752df67a928fa7f884aa51edae6f1cf00eeb38cbcf318cf841c17456", size = 73486, upload-time = "2025-06-17T14:14:37.238Z" }, + { url = "https://files.pythonhosted.org/packages/08/f9/6f7ddb8213f5fdf4db48d1d640b78e8aef89b63a5de8a2313286db709250/multidict-6.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:680210de2c38eef17ce46b8df8bf2c1ece489261a14a6e43c997d49843a27c99", size = 43745, upload-time = "2025-06-17T14:14:38.32Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a7/b9be0163bfeee3bb08a77a1705e24eb7e651d594ea554107fac8a1ca6a4d/multidict-6.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e279259bcb936732bfa1a8eec82b5d2352b3df69d2fa90d25808cfc403cee90a", size = 42135, upload-time = "2025-06-17T14:14:39.897Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/93c8203f943a417bda3c573a34d5db0cf733afdfffb0ca78545c7716dbd8/multidict-6.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1c185fc1069781e3fc8b622c4331fb3b433979850392daa5efbb97f7f9959bb", size = 238585, upload-time = "2025-06-17T14:14:41.332Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fe/2582b56a1807604774f566eeef183b0d6b148f4b89d1612cd077567b2e1e/multidict-6.5.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6bb5f65ff91daf19ce97f48f63585e51595539a8a523258b34f7cef2ec7e0617", size = 236174, upload-time = "2025-06-17T14:14:42.602Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c4/d8b66d42d385bd4f974cbd1eaa8b265e6b8d297249009f312081d5ded5c7/multidict-6.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8646b4259450c59b9286db280dd57745897897284f6308edbdf437166d93855", size = 250145, upload-time = "2025-06-17T14:14:43.944Z" }, + { url = "https://files.pythonhosted.org/packages/bc/64/62feda5093ee852426aae3df86fab079f8bf1cdbe403e1078c94672ad3ec/multidict-6.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d245973d4ecc04eea0a8e5ebec7882cf515480036e1b48e65dffcfbdf86d00be", size = 243470, upload-time = "2025-06-17T14:14:45.343Z" }, + { url = "https://files.pythonhosted.org/packages/67/dc/9f6fa6e854625cf289c0e9f4464b40212a01f76b2f3edfe89b6779b4fb93/multidict-6.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a133e7ddc9bc7fb053733d0ff697ce78c7bf39b5aec4ac12857b6116324c8d75", size = 236968, upload-time = "2025-06-17T14:14:46.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/ae/4b81c6e3745faee81a156f3f87402315bdccf04236f75c03e37be19c94ff/multidict-6.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80d696fa38d738fcebfd53eec4d2e3aeb86a67679fd5e53c325756682f152826", size = 236575, upload-time = "2025-06-17T14:14:47.929Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fa/4089d7642ea344226e1bfab60dd588761d4791754f8072e911836a39bedf/multidict-6.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:20d30c9410ac3908abbaa52ee5967a754c62142043cf2ba091e39681bd51d21a", size = 247632, upload-time = "2025-06-17T14:14:49.525Z" }, + { url = "https://files.pythonhosted.org/packages/16/ee/a353dac797de0f28fb7f078cc181c5f2eefe8dd16aa11a7100cbdc234037/multidict-6.5.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c65068cc026f217e815fa519d8e959a7188e94ec163ffa029c94ca3ef9d4a73", size = 243520, upload-time = "2025-06-17T14:14:50.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/ec/560deb3d2d95822d6eb1bcb1f1cb728f8f0197ec25be7c936d5d6a5d133c/multidict-6.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e355ac668a8c3e49c2ca8daa4c92f0ad5b705d26da3d5af6f7d971e46c096da7", size = 248551, upload-time = "2025-06-17T14:14:52.229Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/ddf277e67c78205f6695f2a7639be459bca9cc353b962fd8085a492a262f/multidict-6.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:08db204213d0375a91a381cae0677ab95dd8c67a465eb370549daf6dbbf8ba10", size = 258362, upload-time = "2025-06-17T14:14:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/02/fc/d64ee1df9b87c5210f2d4c419cab07f28589c81b4e5711eda05a122d0614/multidict-6.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ffa58e3e215af8f6536dc837a990e456129857bb6fd546b3991be470abd9597a", size = 253862, upload-time = "2025-06-17T14:14:55.323Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7c/a2743c00d9e25f4826d3a77cc13d4746398872cf21c843eef96bb9945665/multidict-6.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3e86eb90015c6f21658dbd257bb8e6aa18bdb365b92dd1fba27ec04e58cdc31b", size = 247391, upload-time = "2025-06-17T14:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/9b/03/7773518db74c442904dbd349074f1e7f2a854cee4d9529fc59e623d3949e/multidict-6.5.0-cp313-cp313-win32.whl", hash = "sha256:f34a90fbd9959d0f857323bd3c52b3e6011ed48f78d7d7b9e04980b8a41da3af", size = 41115, upload-time = "2025-06-17T14:14:59.33Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9a/6fc51b1dc11a7baa944bc101a92167d8b0f5929d376a8c65168fc0d35917/multidict-6.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fcb2aa79ac6aef8d5b709bbfc2fdb1d75210ba43038d70fbb595b35af470ce06", size = 44768, upload-time = "2025-06-17T14:15:00.427Z" }, + { url = "https://files.pythonhosted.org/packages/82/2d/0d010be24b663b3c16e3d3307bbba2de5ae8eec496f6027d5c0515b371a8/multidict-6.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:6dcee5e7e92060b4bb9bb6f01efcbb78c13d0e17d9bc6eec71660dd71dc7b0c2", size = 41770, upload-time = "2025-06-17T14:15:01.854Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/a71711a5f32f84b7b036e82182e3250b949a0ce70d51a2c6a4079e665449/multidict-6.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:cbbc88abea2388fde41dd574159dec2cda005cb61aa84950828610cb5010f21a", size = 80450, upload-time = "2025-06-17T14:15:02.968Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a2/953a9eede63a98fcec2c1a2c1a0d88de120056219931013b871884f51b43/multidict-6.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70b599f70ae6536e5976364d3c3cf36f40334708bd6cebdd1e2438395d5e7676", size = 46971, upload-time = "2025-06-17T14:15:04.149Z" }, + { url = "https://files.pythonhosted.org/packages/44/61/60250212953459edda2c729e1d85130912f23c67bd4f585546fe4bdb1578/multidict-6.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:828bab777aa8d29d59700018178061854e3a47727e0611cb9bec579d3882de3b", size = 45548, upload-time = "2025-06-17T14:15:05.666Z" }, + { url = "https://files.pythonhosted.org/packages/11/b6/e78ee82e96c495bc2582b303f68bed176b481c8d81a441fec07404fce2ca/multidict-6.5.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9695fc1462f17b131c111cf0856a22ff154b0480f86f539d24b2778571ff94d", size = 238545, upload-time = "2025-06-17T14:15:06.88Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0f/6132ca06670c8d7b374c3a4fd1ba896fc37fbb66b0de903f61db7d1020ec/multidict-6.5.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b5ac6ebaf5d9814b15f399337ebc6d3a7f4ce9331edd404e76c49a01620b68d", size = 229931, upload-time = "2025-06-17T14:15:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/c0/63/d9957c506e6df6b3e7a194f0eea62955c12875e454b978f18262a65d017b/multidict-6.5.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84a51e3baa77ded07be4766a9e41d977987b97e49884d4c94f6d30ab6acaee14", size = 248181, upload-time = "2025-06-17T14:15:09.907Z" }, + { url = "https://files.pythonhosted.org/packages/43/3f/7d5490579640db5999a948e2c41d4a0efd91a75989bda3e0a03a79c92be2/multidict-6.5.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8de67f79314d24179e9b1869ed15e88d6ba5452a73fc9891ac142e0ee018b5d6", size = 241846, upload-time = "2025-06-17T14:15:11.596Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/252b1ce949ece52bba4c0de7aa2e3a3d5964e800bce71fb778c2e6c66f7c/multidict-6.5.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17f78a52c214481d30550ec18208e287dfc4736f0c0148208334b105fd9e0887", size = 232893, upload-time = "2025-06-17T14:15:12.946Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/0070bfd48c16afc26e056f2acce49e853c0d604a69c7124bc0bbdb1bcc0a/multidict-6.5.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2966d0099cb2e2039f9b0e73e7fd5eb9c85805681aa2a7f867f9d95b35356921", size = 228567, upload-time = "2025-06-17T14:15:14.267Z" }, + { url = "https://files.pythonhosted.org/packages/2a/31/90551c75322113ebf5fd9c5422e8641d6952f6edaf6b6c07fdc49b1bebdd/multidict-6.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:86fb42ed5ed1971c642cc52acc82491af97567534a8e381a8d50c02169c4e684", size = 246188, upload-time = "2025-06-17T14:15:15.985Z" }, + { url = "https://files.pythonhosted.org/packages/cc/e2/aa4b02a55e7767ff292871023817fe4db83668d514dab7ccbce25eaf7659/multidict-6.5.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:4e990cbcb6382f9eae4ec720bcac6a1351509e6fc4a5bb70e4984b27973934e6", size = 235178, upload-time = "2025-06-17T14:15:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5c/f67e726717c4b138b166be1700e2b56e06fbbcb84643d15f9a9d7335ff41/multidict-6.5.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d99a59d64bb1f7f2117bec837d9e534c5aeb5dcedf4c2b16b9753ed28fdc20a3", size = 243422, upload-time = "2025-06-17T14:15:18.939Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/15fa318285e26a50aa3fa979bbcffb90f9b4d5ec58882d0590eda067d0da/multidict-6.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:e8ef15cc97c9890212e1caf90f0d63f6560e1e101cf83aeaf63a57556689fb34", size = 254898, upload-time = "2025-06-17T14:15:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3d/d6c6d1c2e9b61ca80313912d30bb90d4179335405e421ef0a164eac2c0f9/multidict-6.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:b8a09aec921b34bd8b9f842f0bcfd76c6a8c033dc5773511e15f2d517e7e1068", size = 247129, upload-time = "2025-06-17T14:15:21.665Z" }, + { url = "https://files.pythonhosted.org/packages/29/15/1568258cf0090bfa78d44be66247cfdb16e27dfd935c8136a1e8632d3057/multidict-6.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff07b504c23b67f2044533244c230808a1258b3493aaf3ea2a0785f70b7be461", size = 243841, upload-time = "2025-06-17T14:15:23.38Z" }, + { url = "https://files.pythonhosted.org/packages/65/57/64af5dbcfd61427056e840c8e520b502879d480f9632fbe210929fd87393/multidict-6.5.0-cp313-cp313t-win32.whl", hash = "sha256:9232a117341e7e979d210e41c04e18f1dc3a1d251268df6c818f5334301274e1", size = 46761, upload-time = "2025-06-17T14:15:24.733Z" }, + { url = "https://files.pythonhosted.org/packages/26/a8/cac7f7d61e188ff44f28e46cb98f9cc21762e671c96e031f06c84a60556e/multidict-6.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:44cb5c53fb2d4cbcee70a768d796052b75d89b827643788a75ea68189f0980a1", size = 52112, upload-time = "2025-06-17T14:15:25.906Z" }, + { url = "https://files.pythonhosted.org/packages/51/9f/076533feb1b5488d22936da98b9c217205cfbf9f56f7174e8c5c86d86fe6/multidict-6.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:51d33fafa82640c0217391d4ce895d32b7e84a832b8aee0dcc1b04d8981ec7f4", size = 44358, upload-time = "2025-06-17T14:15:27.117Z" }, + { url = "https://files.pythonhosted.org/packages/68/0b/b024da30f18241e03a400aebdc3ca1bcbdc0561f9d48019cbe66549aea3e/multidict-6.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c0078358470da8dc90c37456f4a9cde9f86200949a048d53682b9cd21e5bbf2b", size = 73804, upload-time = "2025-06-17T14:15:28.305Z" }, + { url = "https://files.pythonhosted.org/packages/a3/8f/5e69092bb8a75b95dd27ed4d21220641ede7e127d8a0228cd5e1d5f2150e/multidict-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5cc7968b7d1bf8b973c307d38aa3a2f2c783f149bcac855944804252f1df5105", size = 43161, upload-time = "2025-06-17T14:15:29.47Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d9/51968d296800285343055d482b65001bda4fa4950aad5575afe17906f16f/multidict-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad73a60e11aa92f1f2c9330efdeaac4531b719fc568eb8d312fd4112f34cc18", size = 42996, upload-time = "2025-06-17T14:15:30.622Z" }, + { url = "https://files.pythonhosted.org/packages/38/1c/19ce336cf8af2b7c530ea890496603eb9bbf0da4e3a8e0fcc3669ad30c21/multidict-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3233f21abdcd180b2624eb6988a1e1287210e99bca986d8320afca5005d85844", size = 231051, upload-time = "2025-06-17T14:15:32.296Z" }, + { url = "https://files.pythonhosted.org/packages/73/9b/2cf6eff5b30ff8a67ca231a741053c8cc8269fd860cac2c0e16b376de89d/multidict-6.5.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bee5c0b79fca78fd2ab644ca4dc831ecf793eb6830b9f542ee5ed2c91bc35a0e", size = 219511, upload-time = "2025-06-17T14:15:33.602Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ac/43c89a11d710ce6e5c824ece7b570fd79839e3d25a6a7d3b2526a77b290c/multidict-6.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e053a4d690f4352ce46583080fefade9a903ce0fa9d820db1be80bdb9304fa2f", size = 240287, upload-time = "2025-06-17T14:15:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/16/94/1896d424324618f2e2adbf9acb049aeef8da3f31c109e37ffda63b58d1b5/multidict-6.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42bdee30424c1f4dcda96e07ac60e2a4ede8a89f8ae2f48b5e4ccc060f294c52", size = 232748, upload-time = "2025-06-17T14:15:36.576Z" }, + { url = "https://files.pythonhosted.org/packages/e1/43/2f852c12622bda304a2e0c4419250de3cd0345776ae2e699416cbdc15c9f/multidict-6.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58b2ded1a7982cf7b8322b0645713a0086b2b3cf5bb9f7c01edfc1a9f98d20dc", size = 224910, upload-time = "2025-06-17T14:15:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/31/68/9c32a0305a11aec71a85f354d739011221507bce977a3be8d9fa248763e7/multidict-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f805b8b951d1fadc5bc18c3c93e509608ac5a883045ee33bc22e28806847c20", size = 225773, upload-time = "2025-06-17T14:15:39.645Z" }, + { url = "https://files.pythonhosted.org/packages/bc/81/488054827b644e615f59211fc26fd64b28a1366143e4985326802f18773b/multidict-6.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2540395b63723da748f850568357a39cd8d8d4403ca9439f9fcdad6dd423c780", size = 244097, upload-time = "2025-06-17T14:15:41.164Z" }, + { url = "https://files.pythonhosted.org/packages/9f/71/b9d96548da768dd7284c1f21187129a48906f526d5ed4f71bb050476d91f/multidict-6.5.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:c96aedff25f4e47b6697ba048b2c278f7caa6df82c7c3f02e077bcc8d47b4b76", size = 232831, upload-time = "2025-06-17T14:15:42.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/45/0c57c9bf9be7808252269f0d3964c1495413bcee36a7a7e836fdb778a578/multidict-6.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e80de5ad995de210fd02a65c2350649b8321d09bd2e44717eaefb0f5814503e8", size = 242201, upload-time = "2025-06-17T14:15:44.286Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d4/2441e56b32f7d25c917557641b35a89e0142a7412bc57182c80330975b8d/multidict-6.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6cb9bcedd9391b313e5ec2fb3aa07c03e050550e7b9e4646c076d5c24ba01532", size = 254479, upload-time = "2025-06-17T14:15:45.718Z" }, + { url = "https://files.pythonhosted.org/packages/0d/93/acbc2fed235c7a7b2b21fe8c6ac1b612f7fee79dbddd9c73d42b1a65599c/multidict-6.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a7d130ed7a112e25ab47309962ecafae07d073316f9d158bc7b3936b52b80121", size = 244179, upload-time = "2025-06-17T14:15:47.174Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b2/07ce91400ee2b296de2d6d55f1d948d88d148182b35a3edcc480ddb0f99a/multidict-6.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:95750a9a9741cd1855d1b6cb4c6031ae01c01ad38d280217b64bfae986d39d56", size = 241173, upload-time = "2025-06-17T14:15:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/a0/09/61c0b044065a1d2e1329b0e4f0f2afa992d3bb319129b63dd63c54c2cc15/multidict-6.5.0-cp39-cp39-win32.whl", hash = "sha256:7f78caf409914f108f4212b53a9033abfdc2cbab0647e9ac3a25bb0f21ab43d2", size = 40467, upload-time = "2025-06-17T14:15:50.285Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/48c2837046222ea6800824d576f110d7622c4048b3dd252ef62c51a0969b/multidict-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:220c74009507e847a3a6fc5375875f2a2e05bd9ce28cf607be0e8c94600f4472", size = 44449, upload-time = "2025-06-17T14:15:51.84Z" }, + { url = "https://files.pythonhosted.org/packages/d2/4e/b61b006e75c6e071fac1bd0f32696ad1b052772493c4e9d0121ba604b215/multidict-6.5.0-cp39-cp39-win_arm64.whl", hash = "sha256:d98f4ac9c1ede7e9d04076e2e6d967e15df0079a6381b297270f6bcab661195e", size = 41477, upload-time = "2025-06-17T14:15:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/44/d8/45e8fc9892a7386d074941429e033adb4640e59ff0780d96a8cf46fe788e/multidict-6.5.0-py3-none-any.whl", hash = "sha256:5634b35f225977605385f56153bd95a7133faffc0ffe12ad26e10517537e8dfc", size = 12181, upload-time = "2025-06-17T14:15:55.156Z" }, +] + +[[package]] +name = "mypy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299, upload-time = "2025-07-31T07:54:06.425Z" }, + { url = "https://files.pythonhosted.org/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451, upload-time = "2025-07-31T07:53:52.974Z" }, + { url = "https://files.pythonhosted.org/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211, upload-time = "2025-07-31T07:53:18.879Z" }, + { url = "https://files.pythonhosted.org/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687, upload-time = "2025-07-31T07:53:30.544Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322, upload-time = "2025-07-31T07:53:50.74Z" }, + { url = "https://files.pythonhosted.org/packages/a3/87/8e3e9c2c8bd0d7e071a89c71be28ad088aaecbadf0454f46a540bda7bca6/mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b", size = 9507962, upload-time = "2025-07-31T07:53:08.431Z" }, + { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, + { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" }, + { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, + { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, + { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, + { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" }, + { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" }, + { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, + { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, + { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" }, + { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" }, + { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" }, + { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" }, + { url = "https://files.pythonhosted.org/packages/29/cb/673e3d34e5d8de60b3a61f44f80150a738bff568cd6b7efb55742a605e98/mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9", size = 10992466, upload-time = "2025-07-31T07:53:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d0/fe1895836eea3a33ab801561987a10569df92f2d3d4715abf2cfeaa29cb2/mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99", size = 10117638, upload-time = "2025-07-31T07:53:34.256Z" }, + { url = "https://files.pythonhosted.org/packages/97/f3/514aa5532303aafb95b9ca400a31054a2bd9489de166558c2baaeea9c522/mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8", size = 11915673, upload-time = "2025-07-31T07:52:59.361Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c3/c0805f0edec96fe8e2c048b03769a6291523d509be8ee7f56ae922fa3882/mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8", size = 12649022, upload-time = "2025-07-31T07:53:45.92Z" }, + { url = "https://files.pythonhosted.org/packages/45/3e/d646b5a298ada21a8512fa7e5531f664535a495efa672601702398cea2b4/mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259", size = 12895536, upload-time = "2025-07-31T07:53:06.17Z" }, + { url = "https://files.pythonhosted.org/packages/14/55/e13d0dcd276975927d1f4e9e2ec4fd409e199f01bdc671717e673cc63a22/mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d", size = 9512564, upload-time = "2025-07-31T07:53:12.346Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +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/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 = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/14/510deed325e262afeb8b360043c5d7c960da7d3ecd6d6f9496c9c56dc7f4/propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770", size = 73178, upload-time = "2025-06-09T22:53:40.126Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4e/ad52a7925ff01c1325653a730c7ec3175a23f948f08626a534133427dcff/propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3", size = 43133, upload-time = "2025-06-09T22:53:41.965Z" }, + { url = "https://files.pythonhosted.org/packages/63/7c/e9399ba5da7780871db4eac178e9c2e204c23dd3e7d32df202092a1ed400/propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3", size = 43039, upload-time = "2025-06-09T22:53:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/22/e1/58da211eb8fdc6fc854002387d38f415a6ca5f5c67c1315b204a5d3e9d7a/propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e", size = 201903, upload-time = "2025-06-09T22:53:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0a/550ea0f52aac455cb90111c8bab995208443e46d925e51e2f6ebdf869525/propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220", size = 213362, upload-time = "2025-06-09T22:53:46.707Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/9893b7d878deda9bb69fcf54600b247fba7317761b7db11fede6e0f28bd0/propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb", size = 210525, upload-time = "2025-06-09T22:53:48.547Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bb/38fd08b278ca85cde36d848091ad2b45954bc5f15cce494bb300b9285831/propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614", size = 198283, upload-time = "2025-06-09T22:53:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/78/8c/9fe55bd01d362bafb413dfe508c48753111a1e269737fa143ba85693592c/propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50", size = 191872, upload-time = "2025-06-09T22:53:51.438Z" }, + { url = "https://files.pythonhosted.org/packages/54/14/4701c33852937a22584e08abb531d654c8bcf7948a8f87ad0a4822394147/propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339", size = 199452, upload-time = "2025-06-09T22:53:53.229Z" }, + { url = "https://files.pythonhosted.org/packages/16/44/447f2253d859602095356007657ee535e0093215ea0b3d1d6a41d16e5201/propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0", size = 191567, upload-time = "2025-06-09T22:53:54.541Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b3/e4756258749bb2d3b46defcff606a2f47410bab82be5824a67e84015b267/propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2", size = 193015, upload-time = "2025-06-09T22:53:56.44Z" }, + { url = "https://files.pythonhosted.org/packages/1e/df/e6d3c7574233164b6330b9fd697beeac402afd367280e6dc377bb99b43d9/propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7", size = 204660, upload-time = "2025-06-09T22:53:57.839Z" }, + { url = "https://files.pythonhosted.org/packages/b2/53/e4d31dd5170b4a0e2e6b730f2385a96410633b4833dc25fe5dffd1f73294/propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b", size = 206105, upload-time = "2025-06-09T22:53:59.638Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fe/74d54cf9fbe2a20ff786e5f7afcfde446588f0cf15fb2daacfbc267b866c/propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c", size = 196980, upload-time = "2025-06-09T22:54:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/c469c9d59dada8a7679625e0440b544fe72e99311a4679c279562051f6fc/propcache-0.3.2-cp310-cp310-win32.whl", hash = "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70", size = 37679, upload-time = "2025-06-09T22:54:03.003Z" }, + { url = "https://files.pythonhosted.org/packages/38/35/07a471371ac89d418f8d0b699c75ea6dca2041fbda360823de21f6a9ce0a/propcache-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9", size = 41459, upload-time = "2025-06-09T22:54:04.134Z" }, + { url = "https://files.pythonhosted.org/packages/80/8d/e8b436717ab9c2cfc23b116d2c297305aa4cd8339172a456d61ebf5669b8/propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", size = 74207, upload-time = "2025-06-09T22:54:05.399Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/1e34000e9766d112171764b9fa3226fa0153ab565d0c242c70e9945318a7/propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", size = 43648, upload-time = "2025-06-09T22:54:08.023Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/1ad5af0df781e76988897da39b5f086c2bf0f028b7f9bd1f409bb05b6874/propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", size = 43496, upload-time = "2025-06-09T22:54:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ce/e96392460f9fb68461fabab3e095cb00c8ddf901205be4eae5ce246e5b7e/propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf", size = 217288, upload-time = "2025-06-09T22:54:10.466Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2a/866726ea345299f7ceefc861a5e782b045545ae6940851930a6adaf1fca6/propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9", size = 227456, upload-time = "2025-06-09T22:54:11.828Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/07d992ccb6d930398689187e1b3c718339a1c06b8b145a8d9650e4726166/propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66", size = 225429, upload-time = "2025-06-09T22:54:13.823Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/116ba39448753b1330f48ab8ba927dcd6cf0baea8a0ccbc512dfb49ba670/propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df", size = 213472, upload-time = "2025-06-09T22:54:15.232Z" }, + { url = "https://files.pythonhosted.org/packages/a6/85/f01f5d97e54e428885a5497ccf7f54404cbb4f906688a1690cd51bf597dc/propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2", size = 204480, upload-time = "2025-06-09T22:54:17.104Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/7bf5ab9033b8b8194cc3f7cf1aaa0e9c3256320726f64a3e1f113a812dce/propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7", size = 214530, upload-time = "2025-06-09T22:54:18.512Z" }, + { url = "https://files.pythonhosted.org/packages/31/0b/bd3e0c00509b609317df4a18e6b05a450ef2d9a963e1d8bc9c9415d86f30/propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95", size = 205230, upload-time = "2025-06-09T22:54:19.947Z" }, + { url = "https://files.pythonhosted.org/packages/7a/23/fae0ff9b54b0de4e819bbe559508da132d5683c32d84d0dc2ccce3563ed4/propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e", size = 206754, upload-time = "2025-06-09T22:54:21.716Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/ad6a3c22630aaa5f618b4dc3c3598974a72abb4c18e45a50b3cdd091eb2f/propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e", size = 218430, upload-time = "2025-06-09T22:54:23.17Z" }, + { url = "https://files.pythonhosted.org/packages/5b/2c/ba4f1c0e8a4b4c75910742f0d333759d441f65a1c7f34683b4a74c0ee015/propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf", size = 223884, upload-time = "2025-06-09T22:54:25.539Z" }, + { url = "https://files.pythonhosted.org/packages/88/e4/ebe30fc399e98572019eee82ad0caf512401661985cbd3da5e3140ffa1b0/propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e", size = 211480, upload-time = "2025-06-09T22:54:26.892Z" }, + { url = "https://files.pythonhosted.org/packages/96/0a/7d5260b914e01d1d0906f7f38af101f8d8ed0dc47426219eeaf05e8ea7c2/propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897", size = 37757, upload-time = "2025-06-09T22:54:28.241Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2d/89fe4489a884bc0da0c3278c552bd4ffe06a1ace559db5ef02ef24ab446b/propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39", size = 41500, upload-time = "2025-06-09T22:54:29.4Z" }, + { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload-time = "2025-06-09T22:54:30.551Z" }, + { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload-time = "2025-06-09T22:54:32.296Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload-time = "2025-06-09T22:54:33.929Z" }, + { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload-time = "2025-06-09T22:54:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload-time = "2025-06-09T22:54:36.708Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload-time = "2025-06-09T22:54:38.062Z" }, + { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload-time = "2025-06-09T22:54:39.634Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload-time = "2025-06-09T22:54:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload-time = "2025-06-09T22:54:43.038Z" }, + { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload-time = "2025-06-09T22:54:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload-time = "2025-06-09T22:54:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload-time = "2025-06-09T22:54:47.63Z" }, + { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload-time = "2025-06-09T22:54:48.982Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload-time = "2025-06-09T22:54:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload-time = "2025-06-09T22:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload-time = "2025-06-09T22:54:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload-time = "2025-06-09T22:54:58.975Z" }, + { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload-time = "2025-06-09T22:55:00.471Z" }, + { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload-time = "2025-06-09T22:55:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload-time = "2025-06-09T22:55:03.199Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload-time = "2025-06-09T22:55:04.518Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload-time = "2025-06-09T22:55:05.942Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload-time = "2025-06-09T22:55:07.792Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload-time = "2025-06-09T22:55:09.173Z" }, + { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload-time = "2025-06-09T22:55:10.62Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload-time = "2025-06-09T22:55:12.029Z" }, + { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload-time = "2025-06-09T22:55:13.45Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload-time = "2025-06-09T22:55:15.284Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload-time = "2025-06-09T22:55:16.445Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" }, + { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload-time = "2025-06-09T22:55:21.5Z" }, + { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload-time = "2025-06-09T22:55:22.918Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload-time = "2025-06-09T22:55:24.651Z" }, + { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload-time = "2025-06-09T22:55:26.049Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload-time = "2025-06-09T22:55:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload-time = "2025-06-09T22:55:28.747Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload-time = "2025-06-09T22:55:30.184Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload-time = "2025-06-09T22:55:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload-time = "2025-06-09T22:55:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload-time = "2025-06-09T22:55:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload-time = "2025-06-09T22:55:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload-time = "2025-06-09T22:55:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload-time = "2025-06-09T22:55:39.687Z" }, + { url = "https://files.pythonhosted.org/packages/6c/39/8ea9bcfaaff16fd0b0fc901ee522e24c9ec44b4ca0229cfffb8066a06959/propcache-0.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a7fad897f14d92086d6b03fdd2eb844777b0c4d7ec5e3bac0fbae2ab0602bbe5", size = 74678, upload-time = "2025-06-09T22:55:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/d3/85/cab84c86966e1d354cf90cdc4ba52f32f99a5bca92a1529d666d957d7686/propcache-0.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1f43837d4ca000243fd7fd6301947d7cb93360d03cd08369969450cc6b2ce3b4", size = 43829, upload-time = "2025-06-09T22:55:42.417Z" }, + { url = "https://files.pythonhosted.org/packages/23/f7/9cb719749152d8b26d63801b3220ce2d3931312b2744d2b3a088b0ee9947/propcache-0.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:261df2e9474a5949c46e962065d88eb9b96ce0f2bd30e9d3136bcde84befd8f2", size = 43729, upload-time = "2025-06-09T22:55:43.651Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a2/0b2b5a210ff311260002a315f6f9531b65a36064dfb804655432b2f7d3e3/propcache-0.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e514326b79e51f0a177daab1052bc164d9d9e54133797a3a58d24c9c87a3fe6d", size = 204483, upload-time = "2025-06-09T22:55:45.327Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e0/7aff5de0c535f783b0c8be5bdb750c305c1961d69fbb136939926e155d98/propcache-0.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a996adb6904f85894570301939afeee65f072b4fd265ed7e569e8d9058e4ec", size = 217425, upload-time = "2025-06-09T22:55:46.729Z" }, + { url = "https://files.pythonhosted.org/packages/92/1d/65fa889eb3b2a7d6e4ed3c2b568a9cb8817547a1450b572de7bf24872800/propcache-0.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76cace5d6b2a54e55b137669b30f31aa15977eeed390c7cbfb1dafa8dfe9a701", size = 214723, upload-time = "2025-06-09T22:55:48.342Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e2/eecf6989870988dfd731de408a6fa366e853d361a06c2133b5878ce821ad/propcache-0.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31248e44b81d59d6addbb182c4720f90b44e1efdc19f58112a3c3a1615fb47ef", size = 200166, upload-time = "2025-06-09T22:55:49.775Z" }, + { url = "https://files.pythonhosted.org/packages/12/06/c32be4950967f18f77489268488c7cdc78cbfc65a8ba8101b15e526b83dc/propcache-0.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abb7fa19dbf88d3857363e0493b999b8011eea856b846305d8c0512dfdf8fbb1", size = 194004, upload-time = "2025-06-09T22:55:51.335Z" }, + { url = "https://files.pythonhosted.org/packages/46/6c/17b521a6b3b7cbe277a4064ff0aa9129dd8c89f425a5a9b6b4dd51cc3ff4/propcache-0.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d81ac3ae39d38588ad0549e321e6f773a4e7cc68e7751524a22885d5bbadf886", size = 203075, upload-time = "2025-06-09T22:55:52.681Z" }, + { url = "https://files.pythonhosted.org/packages/62/cb/3bdba2b736b3e45bc0e40f4370f745b3e711d439ffbffe3ae416393eece9/propcache-0.3.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:cc2782eb0f7a16462285b6f8394bbbd0e1ee5f928034e941ffc444012224171b", size = 195407, upload-time = "2025-06-09T22:55:54.048Z" }, + { url = "https://files.pythonhosted.org/packages/29/bd/760c5c6a60a4a2c55a421bc34a25ba3919d49dee411ddb9d1493bb51d46e/propcache-0.3.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:db429c19a6c7e8a1c320e6a13c99799450f411b02251fb1b75e6217cf4a14fcb", size = 196045, upload-time = "2025-06-09T22:55:55.485Z" }, + { url = "https://files.pythonhosted.org/packages/76/58/ced2757a46f55b8c84358d6ab8de4faf57cba831c51e823654da7144b13a/propcache-0.3.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:21d8759141a9e00a681d35a1f160892a36fb6caa715ba0b832f7747da48fb6ea", size = 208432, upload-time = "2025-06-09T22:55:56.884Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ec/d98ea8d5a4d8fe0e372033f5254eddf3254344c0c5dc6c49ab84349e4733/propcache-0.3.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2ca6d378f09adb13837614ad2754fa8afaee330254f404299611bce41a8438cb", size = 210100, upload-time = "2025-06-09T22:55:58.498Z" }, + { url = "https://files.pythonhosted.org/packages/56/84/b6d8a7ecf3f62d7dd09d9d10bbf89fad6837970ef868b35b5ffa0d24d9de/propcache-0.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:34a624af06c048946709f4278b4176470073deda88d91342665d95f7c6270fbe", size = 200712, upload-time = "2025-06-09T22:55:59.906Z" }, + { url = "https://files.pythonhosted.org/packages/bf/32/889f4903ddfe4a9dc61da71ee58b763758cf2d608fe1decede06e6467f8d/propcache-0.3.2-cp39-cp39-win32.whl", hash = "sha256:4ba3fef1c30f306b1c274ce0b8baaa2c3cdd91f645c48f06394068f37d3837a1", size = 38187, upload-time = "2025-06-09T22:56:01.212Z" }, + { url = "https://files.pythonhosted.org/packages/67/74/d666795fb9ba1dc139d30de64f3b6fd1ff9c9d3d96ccfdb992cd715ce5d2/propcache-0.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:7a2368eed65fc69a7a7a40b27f22e85e7627b74216f0846b04ba5c116e191ec9", size = 42025, upload-time = "2025-06-09T22:56:02.875Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +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/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 = "pytest" +version = "8.4.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 = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "pytest-httpserver" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/d8/def15ba33bd696dd72dd4562a5287c0cba4d18a591eeb82e0b08ab385afc/pytest_httpserver-1.1.3.tar.gz", hash = "sha256:af819d6b533f84b4680b9416a5b3f67f1df3701f1da54924afd4d6e4ba5917ec", size = 68870, upload-time = "2025-04-10T08:17:15.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/d2/dfc2f25f3905921c2743c300a48d9494d29032f1389fc142e718d6978fb2/pytest_httpserver-1.1.3-py3-none-any.whl", hash = "sha256:5f84757810233e19e2bb5287f3826a71c97a3740abe3a363af9155c0f82fdbb9", size = 21000, upload-time = "2025-04-10T08:17:13.906Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/45/2e403fa7007816b5fbb324cb4f8ed3c7402a927a0a0cb2b6279879a8bfdc/ruff-0.12.9.tar.gz", hash = "sha256:fbd94b2e3c623f659962934e52c2bea6fc6da11f667a427a368adaf3af2c866a", size = 5254702, upload-time = "2025-08-14T16:08:55.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/20/53bf098537adb7b6a97d98fcdebf6e916fcd11b2e21d15f8c171507909cc/ruff-0.12.9-py3-none-linux_armv6l.whl", hash = "sha256:fcebc6c79fcae3f220d05585229463621f5dbf24d79fdc4936d9302e177cfa3e", size = 11759705, upload-time = "2025-08-14T16:08:12.968Z" }, + { url = "https://files.pythonhosted.org/packages/20/4d/c764ee423002aac1ec66b9d541285dd29d2c0640a8086c87de59ebbe80d5/ruff-0.12.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aed9d15f8c5755c0e74467731a007fcad41f19bcce41cd75f768bbd687f8535f", size = 12527042, upload-time = "2025-08-14T16:08:16.54Z" }, + { url = "https://files.pythonhosted.org/packages/8b/45/cfcdf6d3eb5fc78a5b419e7e616d6ccba0013dc5b180522920af2897e1be/ruff-0.12.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b15ea354c6ff0d7423814ba6d44be2807644d0c05e9ed60caca87e963e93f70", size = 11724457, upload-time = "2025-08-14T16:08:18.686Z" }, + { url = "https://files.pythonhosted.org/packages/72/e6/44615c754b55662200c48bebb02196dbb14111b6e266ab071b7e7297b4ec/ruff-0.12.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d596c2d0393c2502eaabfef723bd74ca35348a8dac4267d18a94910087807c53", size = 11949446, upload-time = "2025-08-14T16:08:21.059Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d1/9b7d46625d617c7df520d40d5ac6cdcdf20cbccb88fad4b5ecd476a6bb8d/ruff-0.12.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b15599931a1a7a03c388b9c5df1bfa62be7ede6eb7ef753b272381f39c3d0ff", size = 11566350, upload-time = "2025-08-14T16:08:23.433Z" }, + { url = "https://files.pythonhosted.org/packages/59/20/b73132f66f2856bc29d2d263c6ca457f8476b0bbbe064dac3ac3337a270f/ruff-0.12.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d02faa2977fb6f3f32ddb7828e212b7dd499c59eb896ae6c03ea5c303575756", size = 13270430, upload-time = "2025-08-14T16:08:25.837Z" }, + { url = "https://files.pythonhosted.org/packages/a2/21/eaf3806f0a3d4c6be0a69d435646fba775b65f3f2097d54898b0fd4bb12e/ruff-0.12.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:17d5b6b0b3a25259b69ebcba87908496e6830e03acfb929ef9fd4c58675fa2ea", size = 14264717, upload-time = "2025-08-14T16:08:27.907Z" }, + { url = "https://files.pythonhosted.org/packages/d2/82/1d0c53bd37dcb582b2c521d352fbf4876b1e28bc0d8894344198f6c9950d/ruff-0.12.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72db7521860e246adbb43f6ef464dd2a532ef2ef1f5dd0d470455b8d9f1773e0", size = 13684331, upload-time = "2025-08-14T16:08:30.352Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2f/1c5cf6d8f656306d42a686f1e207f71d7cebdcbe7b2aa18e4e8a0cb74da3/ruff-0.12.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a03242c1522b4e0885af63320ad754d53983c9599157ee33e77d748363c561ce", size = 12739151, upload-time = "2025-08-14T16:08:32.55Z" }, + { url = "https://files.pythonhosted.org/packages/47/09/25033198bff89b24d734e6479e39b1968e4c992e82262d61cdccaf11afb9/ruff-0.12.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fc83e4e9751e6c13b5046d7162f205d0a7bac5840183c5beebf824b08a27340", size = 12954992, upload-time = "2025-08-14T16:08:34.816Z" }, + { url = "https://files.pythonhosted.org/packages/52/8e/d0dbf2f9dca66c2d7131feefc386523404014968cd6d22f057763935ab32/ruff-0.12.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:881465ed56ba4dd26a691954650de6ad389a2d1fdb130fe51ff18a25639fe4bb", size = 12899569, upload-time = "2025-08-14T16:08:36.852Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b614d7c08515b1428ed4d3f1d4e3d687deffb2479703b90237682586fa66/ruff-0.12.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:43f07a3ccfc62cdb4d3a3348bf0588358a66da756aa113e071b8ca8c3b9826af", size = 11751983, upload-time = "2025-08-14T16:08:39.314Z" }, + { url = "https://files.pythonhosted.org/packages/58/d6/383e9f818a2441b1a0ed898d7875f11273f10882f997388b2b51cb2ae8b5/ruff-0.12.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:07adb221c54b6bba24387911e5734357f042e5669fa5718920ee728aba3cbadc", size = 11538635, upload-time = "2025-08-14T16:08:41.297Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/56f869d314edaa9fc1f491706d1d8a47747b9d714130368fbd69ce9024e9/ruff-0.12.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f5cd34fabfdea3933ab85d72359f118035882a01bff15bd1d2b15261d85d5f66", size = 12534346, upload-time = "2025-08-14T16:08:43.39Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4b/d8b95c6795a6c93b439bc913ee7a94fda42bb30a79285d47b80074003ee7/ruff-0.12.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6be1d2ca0686c54564da8e7ee9e25f93bdd6868263805f8c0b8fc6a449db6d7", size = 13017021, upload-time = "2025-08-14T16:08:45.889Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c1/5f9a839a697ce1acd7af44836f7c2181cdae5accd17a5cb85fcbd694075e/ruff-0.12.9-py3-none-win32.whl", hash = "sha256:cc7a37bd2509974379d0115cc5608a1a4a6c4bff1b452ea69db83c8855d53f93", size = 11734785, upload-time = "2025-08-14T16:08:48.062Z" }, + { url = "https://files.pythonhosted.org/packages/fa/66/cdddc2d1d9a9f677520b7cfc490d234336f523d4b429c1298de359a3be08/ruff-0.12.9-py3-none-win_amd64.whl", hash = "sha256:6fb15b1977309741d7d098c8a3cb7a30bc112760a00fb6efb7abc85f00ba5908", size = 12840654, upload-time = "2025-08-14T16:08:50.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fd/669816bc6b5b93b9586f3c1d87cd6bc05028470b3ecfebb5938252c47a35/ruff-0.12.9-py3-none-win_arm64.whl", hash = "sha256:63c8c819739d86b96d500cce885956a1a48ab056bbcbc61b747ad494b2485089", size = 11949623, upload-time = "2025-08-14T16:08:52.233Z" }, +] + +[[package]] +name = "tomli" +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-requests" +version = "2.32.4.20250809" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/b0/9355adb86ec84d057fea765e4c49cce592aaf3d5117ce5609a95a7fc3dac/types_requests-2.32.4.20250809.tar.gz", hash = "sha256:d8060de1c8ee599311f56ff58010fb4902f462a1470802cf9f6ed27bc46c4df3", size = 23027, upload-time = "2025-08-09T03:17:10.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/6f/ec0012be842b1d888d46884ac5558fd62aeae1f0ec4f7a581433d890d4b5/types_requests-2.32.4.20250809-py3-none-any.whl", hash = "sha256:f73d1832fb519ece02c85b1f09d5f0dd3108938e7d47e7f94bbfa18a6782b163", size = 20644, upload-time = "2025-08-09T03:17:09.716Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" }, +] + +[[package]] +name = "yarl" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/65/7fed0d774abf47487c64be14e9223749468922817b5e8792b8a64792a1bb/yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4", size = 132910, upload-time = "2025-06-10T00:42:31.108Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7b/988f55a52da99df9e56dc733b8e4e5a6ae2090081dc2754fc8fd34e60aa0/yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a", size = 90644, upload-time = "2025-06-10T00:42:33.851Z" }, + { url = "https://files.pythonhosted.org/packages/f7/de/30d98f03e95d30c7e3cc093759982d038c8833ec2451001d45ef4854edc1/yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed", size = 89322, upload-time = "2025-06-10T00:42:35.688Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7a/f2f314f5ebfe9200724b0b748de2186b927acb334cf964fd312eb86fc286/yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e", size = 323786, upload-time = "2025-06-10T00:42:37.817Z" }, + { url = "https://files.pythonhosted.org/packages/15/3f/718d26f189db96d993d14b984ce91de52e76309d0fd1d4296f34039856aa/yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73", size = 319627, upload-time = "2025-06-10T00:42:39.937Z" }, + { url = "https://files.pythonhosted.org/packages/a5/76/8fcfbf5fa2369157b9898962a4a7d96764b287b085b5b3d9ffae69cdefd1/yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e", size = 339149, upload-time = "2025-06-10T00:42:42.627Z" }, + { url = "https://files.pythonhosted.org/packages/3c/95/d7fc301cc4661785967acc04f54a4a42d5124905e27db27bb578aac49b5c/yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8", size = 333327, upload-time = "2025-06-10T00:42:44.842Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/e21269718349582eee81efc5c1c08ee71c816bfc1585b77d0ec3f58089eb/yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23", size = 326054, upload-time = "2025-06-10T00:42:47.149Z" }, + { url = "https://files.pythonhosted.org/packages/32/ae/8616d1f07853704523519f6131d21f092e567c5af93de7e3e94b38d7f065/yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70", size = 315035, upload-time = "2025-06-10T00:42:48.852Z" }, + { url = "https://files.pythonhosted.org/packages/48/aa/0ace06280861ef055855333707db5e49c6e3a08840a7ce62682259d0a6c0/yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb", size = 338962, upload-time = "2025-06-10T00:42:51.024Z" }, + { url = "https://files.pythonhosted.org/packages/20/52/1e9d0e6916f45a8fb50e6844f01cb34692455f1acd548606cbda8134cd1e/yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2", size = 335399, upload-time = "2025-06-10T00:42:53.007Z" }, + { url = "https://files.pythonhosted.org/packages/f2/65/60452df742952c630e82f394cd409de10610481d9043aa14c61bf846b7b1/yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30", size = 338649, upload-time = "2025-06-10T00:42:54.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f5/6cd4ff38dcde57a70f23719a838665ee17079640c77087404c3d34da6727/yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309", size = 358563, upload-time = "2025-06-10T00:42:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/d1/90/c42eefd79d0d8222cb3227bdd51b640c0c1d0aa33fe4cc86c36eccba77d3/yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24", size = 357609, upload-time = "2025-06-10T00:42:59.055Z" }, + { url = "https://files.pythonhosted.org/packages/03/c8/cea6b232cb4617514232e0f8a718153a95b5d82b5290711b201545825532/yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13", size = 350224, upload-time = "2025-06-10T00:43:01.248Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a3/eaa0ab9712f1f3d01faf43cf6f1f7210ce4ea4a7e9b28b489a2261ca8db9/yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8", size = 81753, upload-time = "2025-06-10T00:43:03.486Z" }, + { url = "https://files.pythonhosted.org/packages/8f/34/e4abde70a9256465fe31c88ed02c3f8502b7b5dead693a4f350a06413f28/yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16", size = 86817, upload-time = "2025-06-10T00:43:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b1/18/893b50efc2350e47a874c5c2d67e55a0ea5df91186b2a6f5ac52eff887cd/yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", size = 133833, upload-time = "2025-06-10T00:43:07.393Z" }, + { url = "https://files.pythonhosted.org/packages/89/ed/b8773448030e6fc47fa797f099ab9eab151a43a25717f9ac043844ad5ea3/yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", size = 91070, upload-time = "2025-06-10T00:43:09.538Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e3/409bd17b1e42619bf69f60e4f031ce1ccb29bd7380117a55529e76933464/yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", size = 89818, upload-time = "2025-06-10T00:43:11.575Z" }, + { url = "https://files.pythonhosted.org/packages/f8/77/64d8431a4d77c856eb2d82aa3de2ad6741365245a29b3a9543cd598ed8c5/yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4", size = 347003, upload-time = "2025-06-10T00:43:14.088Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d2/0c7e4def093dcef0bd9fa22d4d24b023788b0a33b8d0088b51aa51e21e99/yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1", size = 336537, upload-time = "2025-06-10T00:43:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f3/fc514f4b2cf02cb59d10cbfe228691d25929ce8f72a38db07d3febc3f706/yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833", size = 362358, upload-time = "2025-06-10T00:43:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/a313ac8d8391381ff9006ac05f1d4331cee3b1efaa833a53d12253733255/yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d", size = 357362, upload-time = "2025-06-10T00:43:20.888Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/8f78a95d6935a70263d46caa3dd18e1f223cf2f2ff2037baa01a22bc5b22/yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8", size = 348979, upload-time = "2025-06-10T00:43:23.169Z" }, + { url = "https://files.pythonhosted.org/packages/cb/05/42773027968968f4f15143553970ee36ead27038d627f457cc44bbbeecf3/yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf", size = 337274, upload-time = "2025-06-10T00:43:27.111Z" }, + { url = "https://files.pythonhosted.org/packages/05/be/665634aa196954156741ea591d2f946f1b78ceee8bb8f28488bf28c0dd62/yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e", size = 363294, upload-time = "2025-06-10T00:43:28.96Z" }, + { url = "https://files.pythonhosted.org/packages/eb/90/73448401d36fa4e210ece5579895731f190d5119c4b66b43b52182e88cd5/yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389", size = 358169, upload-time = "2025-06-10T00:43:30.701Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b0/fce922d46dc1eb43c811f1889f7daa6001b27a4005587e94878570300881/yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f", size = 362776, upload-time = "2025-06-10T00:43:32.51Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0d/b172628fce039dae8977fd22caeff3eeebffd52e86060413f5673767c427/yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845", size = 381341, upload-time = "2025-06-10T00:43:34.543Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9b/5b886d7671f4580209e855974fe1cecec409aa4a89ea58b8f0560dc529b1/yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1", size = 379988, upload-time = "2025-06-10T00:43:36.489Z" }, + { url = "https://files.pythonhosted.org/packages/73/be/75ef5fd0fcd8f083a5d13f78fd3f009528132a1f2a1d7c925c39fa20aa79/yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e", size = 371113, upload-time = "2025-06-10T00:43:38.592Z" }, + { url = "https://files.pythonhosted.org/packages/50/4f/62faab3b479dfdcb741fe9e3f0323e2a7d5cd1ab2edc73221d57ad4834b2/yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773", size = 81485, upload-time = "2025-06-10T00:43:41.038Z" }, + { url = "https://files.pythonhosted.org/packages/f0/09/d9c7942f8f05c32ec72cd5c8e041c8b29b5807328b68b4801ff2511d4d5e/yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e", size = 86686, upload-time = "2025-06-10T00:43:42.692Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload-time = "2025-06-10T00:43:44.369Z" }, + { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload-time = "2025-06-10T00:43:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload-time = "2025-06-10T00:43:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload-time = "2025-06-10T00:43:49.924Z" }, + { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload-time = "2025-06-10T00:43:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload-time = "2025-06-10T00:43:53.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload-time = "2025-06-10T00:43:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload-time = "2025-06-10T00:43:58.056Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload-time = "2025-06-10T00:43:59.773Z" }, + { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload-time = "2025-06-10T00:44:02.051Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload-time = "2025-06-10T00:44:04.196Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload-time = "2025-06-10T00:44:06.527Z" }, + { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload-time = "2025-06-10T00:44:08.379Z" }, + { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload-time = "2025-06-10T00:44:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload-time = "2025-06-10T00:44:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload-time = "2025-06-10T00:44:14.731Z" }, + { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, + { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload-time = "2025-06-10T00:44:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload-time = "2025-06-10T00:44:26.167Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload-time = "2025-06-10T00:44:27.915Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload-time = "2025-06-10T00:44:30.041Z" }, + { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload-time = "2025-06-10T00:44:32.171Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload-time = "2025-06-10T00:44:34.494Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload-time = "2025-06-10T00:44:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload-time = "2025-06-10T00:44:39.141Z" }, + { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload-time = "2025-06-10T00:44:40.934Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload-time = "2025-06-10T00:44:42.854Z" }, + { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload-time = "2025-06-10T00:44:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload-time = "2025-06-10T00:44:47.31Z" }, + { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload-time = "2025-06-10T00:44:49.164Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload-time = "2025-06-10T00:44:51.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" }, + { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload-time = "2025-06-10T00:44:59.071Z" }, + { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload-time = "2025-06-10T00:45:01.605Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload-time = "2025-06-10T00:45:03.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload-time = "2025-06-10T00:45:05.992Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload-time = "2025-06-10T00:45:08.227Z" }, + { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload-time = "2025-06-10T00:45:10.11Z" }, + { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload-time = "2025-06-10T00:45:12.055Z" }, + { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload-time = "2025-06-10T00:45:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload-time = "2025-06-10T00:45:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload-time = "2025-06-10T00:45:18.399Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload-time = "2025-06-10T00:45:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload-time = "2025-06-10T00:45:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload-time = "2025-06-10T00:45:25.793Z" }, + { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" }, + { url = "https://files.pythonhosted.org/packages/01/75/0d37402d208d025afa6b5b8eb80e466d267d3fd1927db8e317d29a94a4cb/yarl-1.20.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e42ba79e2efb6845ebab49c7bf20306c4edf74a0b20fc6b2ccdd1a219d12fad3", size = 134259, upload-time = "2025-06-10T00:45:29.882Z" }, + { url = "https://files.pythonhosted.org/packages/73/84/1fb6c85ae0cf9901046f07d0ac9eb162f7ce6d95db541130aa542ed377e6/yarl-1.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:41493b9b7c312ac448b7f0a42a089dffe1d6e6e981a2d76205801a023ed26a2b", size = 91269, upload-time = "2025-06-10T00:45:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9c/eae746b24c4ea29a5accba9a06c197a70fa38a49c7df244e0d3951108861/yarl-1.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5a5928ff5eb13408c62a968ac90d43f8322fd56d87008b8f9dabf3c0f6ee983", size = 89995, upload-time = "2025-06-10T00:45:35.066Z" }, + { url = "https://files.pythonhosted.org/packages/fb/30/693e71003ec4bc1daf2e4cf7c478c417d0985e0a8e8f00b2230d517876fc/yarl-1.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30c41ad5d717b3961b2dd785593b67d386b73feca30522048d37298fee981805", size = 325253, upload-time = "2025-06-10T00:45:37.052Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a2/5264dbebf90763139aeb0b0b3154763239398400f754ae19a0518b654117/yarl-1.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:59febc3969b0781682b469d4aca1a5cab7505a4f7b85acf6db01fa500fa3f6ba", size = 320897, upload-time = "2025-06-10T00:45:39.962Z" }, + { url = "https://files.pythonhosted.org/packages/e7/17/77c7a89b3c05856489777e922f41db79ab4faf58621886df40d812c7facd/yarl-1.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2b6fb3622b7e5bf7a6e5b679a69326b4279e805ed1699d749739a61d242449e", size = 340696, upload-time = "2025-06-10T00:45:41.915Z" }, + { url = "https://files.pythonhosted.org/packages/6d/55/28409330b8ef5f2f681f5b478150496ec9cf3309b149dab7ec8ab5cfa3f0/yarl-1.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:749d73611db8d26a6281086f859ea7ec08f9c4c56cec864e52028c8b328db723", size = 335064, upload-time = "2025-06-10T00:45:43.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/58/cb0257cbd4002828ff735f44d3c5b6966c4fd1fc8cc1cd3cd8a143fbc513/yarl-1.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9427925776096e664c39e131447aa20ec738bdd77c049c48ea5200db2237e000", size = 327256, upload-time = "2025-06-10T00:45:46.393Z" }, + { url = "https://files.pythonhosted.org/packages/53/f6/c77960370cfa46f6fb3d6a5a79a49d3abfdb9ef92556badc2dcd2748bc2a/yarl-1.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff70f32aa316393eaf8222d518ce9118148eddb8a53073c2403863b41033eed5", size = 316389, upload-time = "2025-06-10T00:45:48.358Z" }, + { url = "https://files.pythonhosted.org/packages/64/ab/be0b10b8e029553c10905b6b00c64ecad3ebc8ace44b02293a62579343f6/yarl-1.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c7ddf7a09f38667aea38801da8b8d6bfe81df767d9dfc8c88eb45827b195cd1c", size = 340481, upload-time = "2025-06-10T00:45:50.663Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c3/3f327bd3905a4916029bf5feb7f86dcf864c7704f099715f62155fb386b2/yarl-1.20.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57edc88517d7fc62b174fcfb2e939fbc486a68315d648d7e74d07fac42cec240", size = 336941, upload-time = "2025-06-10T00:45:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/d1/42/040bdd5d3b3bb02b4a6ace4ed4075e02f85df964d6e6cb321795d2a6496a/yarl-1.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dab096ce479d5894d62c26ff4f699ec9072269d514b4edd630a393223f45a0ee", size = 339936, upload-time = "2025-06-10T00:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1c/911867b8e8c7463b84dfdc275e0d99b04b66ad5132b503f184fe76be8ea4/yarl-1.20.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14a85f3bd2d7bb255be7183e5d7d6e70add151a98edf56a770d6140f5d5f4010", size = 360163, upload-time = "2025-06-10T00:45:56.87Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/8c389f6c6ca0379b57b2da87f1f126c834777b4931c5ee8427dd65d0ff6b/yarl-1.20.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c89b5c792685dd9cd3fa9761c1b9f46fc240c2a3265483acc1565769996a3f8", size = 359108, upload-time = "2025-06-10T00:45:58.869Z" }, + { url = "https://files.pythonhosted.org/packages/7f/09/ae4a649fb3964324c70a3e2b61f45e566d9ffc0affd2b974cbf628957673/yarl-1.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:69e9b141de5511021942a6866990aea6d111c9042235de90e08f94cf972ca03d", size = 351875, upload-time = "2025-06-10T00:46:01.45Z" }, + { url = "https://files.pythonhosted.org/packages/8d/43/bbb4ed4c34d5bb62b48bf957f68cd43f736f79059d4f85225ab1ef80f4b9/yarl-1.20.1-cp39-cp39-win32.whl", hash = "sha256:b5f307337819cdfdbb40193cad84978a029f847b0a357fbe49f712063cfc4f06", size = 82293, upload-time = "2025-06-10T00:46:03.763Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/ce185848a7dba68ea69e932674b5c1a42a1852123584bccc5443120f857c/yarl-1.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:eae7bfe2069f9c1c5b05fc7fe5d612e5bbc089a39309904ee8b829e322dcad00", size = 87385, upload-time = "2025-06-10T00:46:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, +]