pax_global_header 0000666 0000000 0000000 00000000064 15000473553 0014514 g ustar 00root root 0000000 0000000 52 comment=3eae1c74e06a59a4d04147146be1bafbd53fd1c1
aioxmlrpc-0.10.0/ 0000775 0000000 0000000 00000000000 15000473553 0013570 5 ustar 00root root 0000000 0000000 aioxmlrpc-0.10.0/.editorconfig 0000664 0000000 0000000 00000000267 15000473553 0016252 0 ustar 00root root 0000000 0000000 [*.py]
indent_style = space
indent_size = 4
[*.rst]
indent_style = space
indent_size = 3
[*.toml]
indent_style = space
indent_size = 2
[*.yml]
indent_style = space
indent_size = 2
aioxmlrpc-0.10.0/.github/ 0000775 0000000 0000000 00000000000 15000473553 0015130 5 ustar 00root root 0000000 0000000 aioxmlrpc-0.10.0/.github/workflows/ 0000775 0000000 0000000 00000000000 15000473553 0017165 5 ustar 00root root 0000000 0000000 aioxmlrpc-0.10.0/.github/workflows/build-artifacts.yml 0000664 0000000 0000000 00000002323 15000473553 0022765 0 ustar 00root root 0000000 0000000 # Build envsub tarballs for supported python.
name: "Build artifact"
on:
workflow_call:
inputs:
release-version:
required: true
type: string
dry-run:
required: true
type: boolean
python-version:
required: true
type: string
pull_request:
paths:
# When we change pyproject.toml, we want to ensure that the maturin builds still work.
- pyproject.toml
# And when we change this workflow itself...
- .github/workflows/build-artifacts.yml
concurrency:
group: sdist-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
sdist:
name: Build artifact for ${{ inputs.release-version }} ${{ inputs.dry-run && '(dry-run)' || '' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v3
- name: Install the project
run: uv sync
- name: Build tarball
run: uv build
- name: "Upload sdist"
uses: actions/upload-artifact@v4
with:
name: pypi_files
path: dist/*
aioxmlrpc-0.10.0/.github/workflows/publish-pypi.yml 0000664 0000000 0000000 00000001616 15000473553 0022341 0 ustar 00root root 0000000 0000000 # Publish a release to PyPI.
#
name: "Publish to PyPI"
on:
workflow_call:
inputs:
release-version:
required: true
type: string
dry-run:
required: true
type: boolean
jobs:
pypi-publish:
name: Upload to PyPI ${{ inputs.release-version }} ${{ inputs.dry-run && '(dry-run)' || '' }}
runs-on: ubuntu-latest
if: ${{ !inputs.dry-run }}
permissions:
# This permission is needed for private repositories.
contents: read
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: pypi_files
path: dist
merge-multiple: true
- uses: pdm-project/setup-pdm@v4
with:
python-version: 3.12
- name: Publish package distributions to PyPI
run: pdm publish --no-build
aioxmlrpc-0.10.0/.github/workflows/release.yml 0000664 0000000 0000000 00000004403 15000473553 0021331 0 ustar 00root root 0000000 0000000 name: Release
on:
push:
tags:
- 'v*' # Automatically trigger on version tags
- 'dry-run'
workflow_dispatch:
inputs:
tag:
description: "Release Tag"
required: true
default: "dry-run"
type: string
env:
PYTHON_VERSION: "3.12"
jobs:
plan:
runs-on: ubuntu-latest
outputs:
release_version: ${{ steps.release-version.outputs.release_version }}
dry-run: ${{ steps.release-version.outputs.dry_run }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set Release Version
id: release-version
run: |
if [ "${{ github.event_name }}" == "push" ]; then
echo "release_version=${{ github.ref_name }}" >> $GITHUB_OUTPUT
if [ "${{ github.ref_name }}" == "dry-run" ]; then
echo "dry_run=true" >> $GITHUB_OUTPUT
else
echo "dry_run=false" >> $GITHUB_OUTPUT
fi
else
version="${{ github.event.inputs.tag || 'dry-run' }}"
if [ "${version}" == "dry-run" ]; then
echo "release_version=latest" >> $GITHUB_OUTPUT
echo "dry_run=true" >> $GITHUB_OUTPUT
else
echo "release_version=${version}" >> $GITHUB_OUTPUT
echo "dry_run=false" >> $GITHUB_OUTPUT
fi
fi
- name: Display Release Version
run: echo "The release version is ${{ steps.release-version.outputs.release_version }}"
unit-tests:
uses: ./.github/workflows/tests.yml
build-artifacts:
needs:
- plan
- unit-tests
uses: ./.github/workflows/build-artifacts.yml
with:
release-version: ${{ needs.plan.outputs.release_version }}
dry-run: ${{ needs.plan.outputs.dry-run == 'true' }}
python-version: '3.12'
tests-artifacts:
needs:
- plan
- build-artifacts
uses: ./.github/workflows/tests-artifacts.yml
with:
release-version: ${{ needs.plan.outputs.release_version }}
dry-run: ${{ needs.plan.outputs.dry-run == 'true' }}
publish-pypi:
needs:
- plan
- tests-artifacts
uses: ./.github/workflows/publish-pypi.yml
with:
release-version: ${{ needs.plan.outputs.release_version }}
dry-run: ${{ needs.plan.outputs.dry-run == 'true' }}
aioxmlrpc-0.10.0/.github/workflows/tests-artifacts.yml 0000664 0000000 0000000 00000002155 15000473553 0023033 0 ustar 00root root 0000000 0000000 name: tests artifacts
# Controls when the workflow will run
on:
# Allows you to run this workflow manually from the Actions tab
workflow_call:
inputs:
release-version:
required: true
type: string
description: "release number"
dry-run:
required: true
type: boolean
description: "blank run means that the release will not be pushed"
jobs:
test-sdist:
name: test tarball archive of ${{ inputs.release-version }} ${{ inputs.dry-run && '(dry-run)' || '' }}
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: actions/download-artifact@v4
with:
pattern: pypi_files
path: dist
merge-multiple: true
- name: "Install"
run: |
pip install dist/aioxmlrpc-*.whl --force-reinstall
- name: "Test sdist"
run: |
python -c "from aioxmlrpc import __version__; print(__version__, end='')"
aioxmlrpc-0.10.0/.github/workflows/tests.yml 0000664 0000000 0000000 00000003051 15000473553 0021051 0 ustar 00root root 0000000 0000000 name: tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
workflow_call:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: chartboost/ruff-action@v1
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v3
- name: Install the project
run: uv sync --group dev
- name: Check types
run: |
uv run mypy src/aioxmlrpc/
- name: Run tests
run: |
uv run pytest tests --junitxml=junit/test-results-${{ matrix.python-version }}.xml --cov=aioxmlrpc --cov-report=xml --cov-report=html
- name: Upload pytest test results
uses: actions/upload-artifact@v4
with:
name: pytest-results-${{ matrix.python-version }}
path: junit/test-results-${{ matrix.python-version }}.xml
- name: Upload test results to Codecov
if: ${{ !cancelled() }} && matrix.python-version == '3.12' && github.event_name != 'workflow_dispatch'
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Codecov
if: matrix.python-version == '3.12' && github.event_name != 'workflow_dispatch'
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
aioxmlrpc-0.10.0/.gitignore 0000664 0000000 0000000 00000001015 15000473553 0015555 0 ustar 00root root 0000000 0000000 # Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
bin/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
venv*
*.log
# Sphinx documentation
docs/_build/
# pycharm
.idea/
aioxmlrpc-0.10.0/CHANGELOG.rst 0000664 0000000 0000000 00000003772 15000473553 0015622 0 ustar 00root root 0000000 0000000 0.10.0 - Released on 2025-04-18
--------------------------------
* Add support of aioxmlrpc.server
* Add support of MultiCall
* Fix httpx warning
0.9.1 - Released on 2024-12-06
-------------------------------
* Fix for httpx 0.28 (Ondra Geršl)
0.9.0 - Released on 2024-11-06
-------------------------------
* Add typing.
* Breaking change: drop support of python 3.7 and python 3.8.
* Switch packaging to uv / pdm.
* CI update
* Update LICENSE to MIT
0.8.1 - Released on 2024-04-25
-------------------------------
* Add PEP-517 metadata in python package
0.8.0 - Released on 2024-04-09
-------------------------------
* Update dependencies
0.7.0 - Released on 2023-05-08
------------------------------
* New feature: Add missing context kwargs argument for ServerProxy compatibility.
* Breaking change: Argument headers must be a kwargs like in the standard library.
* Breaking change: Non standard arguments timeout and session must be kwargs too.
* Update dependencies
0.6.5 - Released on 2023-04-27
------------------------------
* Update dependencies
0.6.4 - Released on 2022-06-02
------------------------------
* Update dependencies
0.6.3 released on 2021-12-15
----------------------------
* Fix ProtocolError
0.6.2 released on 2021-12-13
----------------------------
* Update httpx to ^0.21.1
* Switch CI to github action
0.6.1 released on 2021-10-06
----------------------------
* Switch to httpx
0.5 released on 2017-09-10
--------------------------
* Remove compatibility with aiohttp < 1.0 (Ovv)
0.4 released on 2017-03-30
--------------------------
* Fix NXDOMAIN Exception handling (Vladimir Rutsky)
* Fix cancel of futures handling (Gustavo Tavares Cabral)
0.3 released on 2016-06-16
--------------------------
* Fix socket closing issue
0.2 released on 2016-05-26
--------------------------
* Update compatibility for aiohttp >= 0.20
.. important::
This break the compatibility of python 3.3
0.1 released on 2014-05-17
--------------------------
* Initial version implementing ``aioxmlrpc.client``
aioxmlrpc-0.10.0/CONTRIBUTORS.rst 0000664 0000000 0000000 00000000224 15000473553 0016255 0 ustar 00root root 0000000 0000000 Contributors
============
A. Jesse Jiryu Davis
Gustavo Tavares Cabral
Vladimir Rutsky
nibrag
sayoun
Ondra Geršl
Romuald Brunet
Sylvain Peyrefitte
aioxmlrpc-0.10.0/Justfile 0000664 0000000 0000000 00000002635 15000473553 0015306 0 ustar 00root root 0000000 0000000 package := 'aioxmlrpc'
default_unittest_suite := 'tests/unittests'
default_functest_suite := 'tests/functionals'
install:
uv sync --group dev
test: lint typecheck unittest
lint:
uv run ruff check .
typecheck:
uv run mypy src/ tests/
unittest test_suite=default_unittest_suite:
uv run pytest -sxv {{test_suite}}
lf:
uv run pytest -sxvvv --lf
cov test_suite=default_unittest_suite:
rm -f .coverage
rm -rf htmlcov
uv run pytest --cov-report=html --cov={{package}} {{test_suite}}
xdg-open htmlcov/index.html
fmt:
uv run ruff check --fix .
uv run ruff format src tests
release major_minor_patch: test && changelog
#! /bin/bash
# Try to bump the version first
if ! uvx pdm bump {{major_minor_patch}}; then
# If it fails, check if pdm-bump is installed
if ! uvx pdm self list | grep -q pdm-bump; then
# If not installed, add pdm-bump
uvx pdm self add pdm-bump
fi
# Attempt to bump the version again
uvx pdm bump {{major_minor_patch}}
fi
uv sync
changelog:
uv run python scripts/write_changelog.py
cat CHANGELOG.rst >> CHANGELOG.rst.new
rm CHANGELOG.rst
mv CHANGELOG.rst.new CHANGELOG.rst
$EDITOR CHANGELOG.rst
publish:
git commit -am "Release $(uv run scripts/get_version.py)"
git tag "v$(uv run scripts/get_version.py)"
git push origin "v$(uv run scripts/get_version.py)"
aioxmlrpc-0.10.0/LICENSE 0000664 0000000 0000000 00000002110 15000473553 0014567 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2014-2024, Guillaume Gauvrit and Contibutors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
aioxmlrpc-0.10.0/README.rst 0000664 0000000 0000000 00000004352 15000473553 0015263 0 ustar 00root root 0000000 0000000 =========
aioxmlrpc
=========
.. image:: https://github.com/mardiros/aioxmlrpc/actions/workflows/tests.yml/badge.svg
:target: https://github.com/mardiros/aioxmlrpc/actions/workflows/tests.yml
.. image:: https://codecov.io/gh/mardiros/aioxmlrpc/branch/master/graph/badge.svg?token=BR3KttC9uJ
:target: https://codecov.io/gh/mardiros/aioxmlrpc
Asyncio version of the standard lib ``xmlrpc``
``aioxmlrpc.client``, which works like ``xmlrpc.client`` but uses coroutines,
has been implemented.
``aioxmlrpc.client`` is based on ``httpx`` for the transport, and just patch
the necessary from the python standard library to get it working.
``aioxmlrpc.server``, which works much like ``xmlrpc.server``, but runs on the asyncio
event loop and handles remote procedure calls (RPC) using both regular functions and coroutines.
``aioxmlrpc.server`` is based on ``starlette`` and ``uvicorn`` for handling HTTP.
Installation
------------
aioxmlrpc is available on PyPI, it can simply be installed with your favorite
tool, example with pip here.
::
pip install aioxmlrpc
The server dependencies is installed using the extra syntax.
::
pip install "aioxmlrpc[server]"
Getting Started
---------------
Client
~~~~~~
This example show how to print the current version of the Gandi XML-RPC api.
::
import asyncio
from aioxmlrpc.client import ServerProxy
async def print_gandi_api_version():
api = ServerProxy('https://rpc.gandi.net/xmlrpc/')
result = await api.version.info()
print(result)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(print_gandi_api_version())
loop.stop()
Run the example
::
uv run examples/gandi_api_version.py
Server
~~~~~~
This example show an exemple of the server side.
::
import asyncio
from aioxmlrpc.server import SimpleXMLRPCServer
class Api:
def info(self):
return "1.0.0"
async def sleep(self):
await asyncio.sleep(1)
return "done"
async def main():
server = SimpleXMLRPCServer(("0.0.0.0", 8080))
server.register_instance(Api(), allow_dotted_names=True)
await server.serve_forever()
if __name__ == "__main__":
asyncio.run(main())
aioxmlrpc-0.10.0/examples/ 0000775 0000000 0000000 00000000000 15000473553 0015406 5 ustar 00root root 0000000 0000000 aioxmlrpc-0.10.0/examples/gandi_api_version.py 0000775 0000000 0000000 00000000516 15000473553 0021445 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
import asyncio
from aioxmlrpc.client import ServerProxy
ENDPOINT = "https://rpc.gandi.net/xmlrpc/"
async def main():
client = ServerProxy(ENDPOINT, allow_none=True)
for _ in range(5):
data = await client.version.info()
print(data)
if __name__ == "__main__":
asyncio.run(main())
aioxmlrpc-0.10.0/examples/server.py 0000664 0000000 0000000 00000000642 15000473553 0017270 0 ustar 00root root 0000000 0000000 import asyncio
from aioxmlrpc.server import SimpleXMLRPCServer
class Api:
def info(self):
return "1.0.0"
async def sleep(self):
await asyncio.sleep(1)
return "done"
async def main():
server = SimpleXMLRPCServer(("0.0.0.0", 8080))
server.register_instance(Api(), allow_dotted_names=True)
await server.serve_forever()
if __name__ == "__main__":
asyncio.run(main())
aioxmlrpc-0.10.0/pyproject.toml 0000664 0000000 0000000 00000004264 15000473553 0016512 0 ustar 00root root 0000000 0000000 [project]
name = "aioxmlrpc"
description = "XML-RPC client for asyncio"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Framework :: AsyncIO",
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"License :: OSI Approved :: MIT License",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Software Development :: Libraries",
"Topic :: System :: Networking",
"Typing :: Typed",
"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",
]
version = "0.10.0"
readme = "README.rst"
license = { text = "MIT License" }
authors = [{ name = "Guillaume Gauvrit", email = "guillaume@gauvr.it" }]
requires-python = ">=3.9"
dependencies = ["httpx >=0.24, <1"]
[project.optional-dependencies]
server = ["starlette>=0.46.2", "uvicorn>=0.34.1"]
[dependency-groups]
dev = [
"mypy>=1.13.0,<2",
"pytest >=8.3.3,<9",
"pytest-asyncio >=0.24.0",
"pytest-cov >=6.0.0,<7",
"starlette>=0.46.2",
"uvicorn>=0.34.1",
]
[tool.pdm.build]
includes = ["src", "CHANGELOG.rst", "CONTRIBUTORS.rst"]
excludes = ["tests"]
[project.urls]
Homepage = "https://github.com/mardiros/aioxmlrpc"
Documentation = "https://github.com/mardiros/aioxmlrpc/blob/main/README.rst"
Repository = "https://github.com/mardiros/aioxmlrpc.git"
Issues = "https://github.com/mardiros/aioxmlrpc/issues"
Changelog = "https://mardiros.github.io/aioxmlrpc/user/changelog.html"
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
[[tool.mypy.overrides]]
disallow_any_generics = true
disallow_untyped_defs = true
module = "aioxmlrpc.*"
[tool.pyright]
ignore = ["examples"]
include = ["src", "tests"]
reportPrivateUsage = false
reportUnknownMemberType = false
reportUnknownParameterType = false
reportUnknownVariableType = false
reportShadowedImports = false
typeCheckingMode = "strict"
venvPath = ".venv"
[tool.coverage.report]
exclude_lines = [
"if TYPE_CHECKING:",
"except ImportError:",
"\\s+\\.\\.\\.$",
"# coverage: ignore",
]
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
aioxmlrpc-0.10.0/scripts/ 0000775 0000000 0000000 00000000000 15000473553 0015257 5 ustar 00root root 0000000 0000000 aioxmlrpc-0.10.0/scripts/get_version.py 0000664 0000000 0000000 00000000161 15000473553 0020153 0 ustar 00root root 0000000 0000000 import aioxmlrpc
__version__ = aioxmlrpc.__version__
if __name__ == "__main__":
print(__version__, end="")
aioxmlrpc-0.10.0/scripts/write_changelog.py 0000664 0000000 0000000 00000000627 15000473553 0020777 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import datetime
from importlib.metadata import version
header = (
f"{version('aioxmlrpc')} - "
f"Released on {datetime.datetime.now().date().isoformat()}"
)
with open("CHANGELOG.rst.new", "w") as changelog:
changelog.write(header)
changelog.write("\n")
changelog.write("-" * len(header))
changelog.write("\n")
changelog.write("* please write here \n\n")
aioxmlrpc-0.10.0/src/ 0000775 0000000 0000000 00000000000 15000473553 0014357 5 ustar 00root root 0000000 0000000 aioxmlrpc-0.10.0/src/aioxmlrpc/ 0000775 0000000 0000000 00000000000 15000473553 0016355 5 ustar 00root root 0000000 0000000 aioxmlrpc-0.10.0/src/aioxmlrpc/__init__.py 0000664 0000000 0000000 00000000166 15000473553 0020471 0 ustar 00root root 0000000 0000000 """
XML-RPC Protocol for ``asyncio``
"""
from importlib import metadata
__version__ = metadata.version("aioxmlrpc")
aioxmlrpc-0.10.0/src/aioxmlrpc/client.py 0000664 0000000 0000000 00000014347 15000473553 0020216 0 ustar 00root root 0000000 0000000 """
XML-RPC Client with asyncio.
This module adapt the ``xmlrpc.client`` module of the standard library to
work with asyncio.
"""
import asyncio
import logging
import ssl
from typing import (
Any,
Awaitable,
Callable,
Optional,
Union,
cast,
)
from xmlrpc import client as xmlrpc
import httpx
__ALL__ = ["ServerProxy", "Fault", "ProtocolError", "MultiCall"]
RPCResult = Any
RPCParameters = Any
# you don't have to import xmlrpc.client from your code
Fault = xmlrpc.Fault
ProtocolError = xmlrpc.ProtocolError
log = logging.getLogger(__name__)
class _Method:
# some magic to bind an XML-RPC method to an RPC server.
# supports "nested" methods (e.g. examples.getStateName)
def __init__(
self, send: Callable[[str, RPCParameters], Awaitable[RPCResult]], name: str
) -> None:
self.__send = send
self.__name = name
def __getattr__(self, name: str) -> "_Method":
return _Method(self.__send, "%s.%s" % (self.__name, name))
async def __call__(self, *args: RPCParameters) -> RPCResult:
ret = await self.__send(self.__name, args)
return ret
class AioTransport(xmlrpc.Transport):
"""
``xmlrpc.Transport`` subclass for asyncio support
"""
def __init__(
self,
session: httpx.AsyncClient,
use_https: bool,
*,
use_datetime: bool = False,
use_builtin_types: bool = False,
auth: Optional[httpx._types.AuthTypes] = None,
timeout: Optional[httpx._types.TimeoutTypes] = None,
):
super().__init__(use_datetime, use_builtin_types)
self.use_https = use_https
self._session = session
self.auth = auth or httpx.USE_CLIENT_DEFAULT
self.timeout = timeout
async def request( # type: ignore
self,
host: str,
handler: str,
request_body: bytes,
verbose: bool = False,
) -> RPCResult:
"""
Send the XML-RPC request, return the response.
This method is a coroutine.
"""
url = self._build_url(host, handler)
response = None
try:
response = await self._session.post(
url,
content=request_body,
auth=self.auth,
timeout=self.timeout,
)
body = response.text
if response.status_code != 200:
raise ProtocolError(
url,
response.status_code,
body,
# response.headers is a case insensitive dict from httpx,
# the ProtocolError is typed as simple dict
cast(dict[str, str], response.headers),
)
except asyncio.CancelledError:
raise
except ProtocolError:
raise
except Exception as exc:
log.error("Unexpected error", exc_info=True)
if response is not None:
errcode = response.status_code
headers = cast(dict[str, str], response.headers) # coverage: ignore
else:
errcode = 0
headers = {}
raise ProtocolError(url, errcode, str(exc), headers)
return self.parse_response(body)
def parse_response( # type: ignore
self,
body: str,
) -> RPCResult:
"""
Parse the xmlrpc response.
"""
p, u = self.getparser()
p.feed(body)
p.close()
return u.close()
def _build_url(self, host: str, handler: str) -> str:
"""
Build a url for our request based on the host, handler and use_http
property
"""
scheme = "https" if self.use_https else "http"
return f"{scheme}://{host}{handler}"
class ServerProxy(xmlrpc.ServerProxy):
"""
``xmlrpc.ServerProxy`` subclass for asyncio support
"""
def __init__(
self,
uri: str,
encoding: Optional[str] = None,
verbose: bool = False,
allow_none: bool = False,
use_datetime: bool = False,
use_builtin_types: bool = False,
auth: Optional[httpx._types.AuthTypes] = None,
*,
headers: Optional[dict[str, Any]] = None,
context: Optional[Union[bool, ssl.SSLContext]] = None,
timeout: httpx._types.TimeoutTypes = 5.0,
session: Optional[httpx.AsyncClient] = None,
) -> None:
if not headers:
headers = {
"User-Agent": "python/aioxmlrpc",
"Accept": "text/xml",
"Content-Type": "text/xml",
}
if context is None:
context = True
self._session = session or httpx.AsyncClient(headers=headers, verify=context)
transport = AioTransport(
use_https=uri.startswith("https://"),
session=self._session,
auth=auth,
timeout=timeout,
use_datetime=use_datetime,
use_builtin_types=use_builtin_types,
)
super().__init__(
uri,
transport,
encoding,
verbose,
allow_none,
use_datetime,
use_builtin_types,
)
async def __request( # type: ignore
self,
methodname: str,
params: RPCParameters,
) -> RPCResult:
# call a method on the remote server
request = xmlrpc.dumps(
params, methodname, encoding=self.__encoding, allow_none=self.__allow_none
).encode(self.__encoding)
response = await self.__transport.request( # type: ignore
self.__host, self.__handler, request, verbose=self.__verbose
)
if len(response) == 1: # type: ignore
response = response[0]
return response
def __getattr__(self, name: str) -> _Method: # type: ignore
return _Method(self.__request, name)
class MultiCall(xmlrpc.MultiCall):
__server: ServerProxy
async def __call__(self) -> xmlrpc.MultiCallIterator: # type: ignore
marshalled_list = []
for name, args in self.__call_list:
marshalled_list.append({"methodName": name, "params": args})
return xmlrpc.MultiCallIterator(
await self.__server.system.multicall(marshalled_list)
)
aioxmlrpc-0.10.0/src/aioxmlrpc/py.typed 0000664 0000000 0000000 00000000000 15000473553 0020042 0 ustar 00root root 0000000 0000000 aioxmlrpc-0.10.0/src/aioxmlrpc/server.py 0000664 0000000 0000000 00000014323 15000473553 0020240 0 ustar 00root root 0000000 0000000 """
XML-RPC Server with asyncio.
This module adapt the ``xmlrpc.server`` module of the standard library to
work with asyncio.
Handle RPC of classic and coroutine functions
"""
import asyncio
import inspect
from types import TracebackType
from typing import (
Any,
Awaitable,
Callable,
Coroutine,
Iterable,
Optional,
Tuple,
overload,
)
from xmlrpc import server
from xmlrpc.client import loads, dumps, Fault
import uvicorn
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import Response
from starlette.routing import Route
__all__ = ["SimpleXMLRPCDispatcher", "SimpleXMLRPCServer"]
_Marshallable = Any
class SimpleXMLRPCDispatcher(server.SimpleXMLRPCDispatcher):
async def _marshaled_dispatch(self, data: str) -> bytes: # type: ignore
"""
Override function from SimpleXMLRPCDispatcher to handle coroutines RPC case
"""
try:
params, method = loads(data, use_builtin_types=self.use_builtin_types)
if method is None:
raise ValueError("Invalid")
response = await self._dispatch(method, params)
# wrap response in a singleton tuple
response = (response,)
response = dumps(
response,
methodresponse=True,
allow_none=self.allow_none,
encoding=self.encoding,
)
except Fault as fault:
response = dumps(fault, allow_none=self.allow_none, encoding=self.encoding)
except Exception as exc:
# report exception back to server
response = dumps(
Fault(1, "%s:%s" % (type(exc), exc)),
encoding=self.encoding,
allow_none=self.allow_none,
)
return response.encode(self.encoding, "xmlcharrefreplace")
async def _dispatch( # type: ignore
self, method: str, params: Iterable[_Marshallable]
) -> _Marshallable: # type: ignore
"""
Override function from SimpleXMLRPCDispatcher to handle coroutine
RPC call
"""
func = None
try:
# check to see if a matching function has been registered
func = self.funcs[method]
except KeyError:
if self.instance is not None:
# check for a _dispatch method
if hasattr(self.instance, "_dispatch"):
resp = await self.instance._dispatch(method, params)
if inspect.iscoroutine(self.instance._dispatch):
return await resp
else:
return resp
else:
# call instance method directly
try:
func = server.resolve_dotted_attribute(
self.instance,
method,
getattr(self, "allow_dotted_names", True),
)
except AttributeError:
pass
if func is not None:
result = func(*params)
if inspect.iscoroutine(result):
return await result
else:
return result
else:
raise Exception('method "%s" is not supported' % method)
async def system_multicall(self, call_list: list[dict[str, _Marshallable]]): # type: ignore
async def handle_call(call: dict[str, _Marshallable]) -> _Marshallable:
method_name = call["methodName"]
params = call["params"]
try:
result = await self._dispatch(method_name, params)
return [result]
except Fault as fault:
return {"faultCode": fault.faultCode, "faultString": fault.faultString}
except BaseException as exc:
return {"faultCode": 1, "faultString": f"{type(exc).__name__}:{exc}"}
return await asyncio.gather(*(handle_call(call) for call in call_list))
class SimpleXMLRPCServer(SimpleXMLRPCDispatcher):
rpc_paths = ["/", "/RPC2", "/xmlrpc"]
def __init__(
self,
addr: Tuple[str, int],
logRequests: bool = True,
allow_none: bool = False,
encoding: Optional[str] = None,
use_builtin_types: bool = False,
) -> None:
super().__init__(allow_none, encoding, use_builtin_types)
self.host, self.port = addr
self.logRequests = logRequests
self.app = Starlette(
routes=[
Route(route, self.handle_xmlrpc, methods=["POST"])
for route in self.rpc_paths
],
)
async def handle_xmlrpc(self, request: Request) -> Response:
body = await request.body()
response = await self._marshaled_dispatch(body.decode())
return Response(response, media_type="text/xml")
def serve_forever(self) -> asyncio.Task[Any]:
config = uvicorn.Config(
self.app, host=self.host, port=self.port, log_level="error", loop="asyncio"
)
self.server = uvicorn.Server(config)
return asyncio.create_task(self.server.serve())
@overload # type: ignore
def register_function(
self, function: Callable[..., _Marshallable], name: Optional[str] = None
) -> Callable[..., _Marshallable]: ...
@overload
def register_function(
self,
function: Coroutine[Awaitable[_Marshallable], Any, Any],
name: Optional[str] = None,
) -> Coroutine[Awaitable[_Marshallable], Any, Any]: ...
def register_function( # type: ignore
self,
function: Any,
name: Optional[str] = None,
) -> Any:
super().register_function(function, name)
async def __aenter__(self) -> "SimpleXMLRPCServer":
return self
def __enter__(self) -> "SimpleXMLRPCServer":
return self
def __exit__(
self,
exc_type: Optional[type[BaseException]],
exc: Optional[BaseException],
tb: Optional[TracebackType],
) -> None: ...
async def __aexit__(
self,
exc_type: Optional[type[BaseException]],
exc: Optional[BaseException],
tb: Optional[TracebackType],
) -> None: ...
aioxmlrpc-0.10.0/tests/ 0000775 0000000 0000000 00000000000 15000473553 0014732 5 ustar 00root root 0000000 0000000 aioxmlrpc-0.10.0/tests/functionals/ 0000775 0000000 0000000 00000000000 15000473553 0017257 5 ustar 00root root 0000000 0000000 aioxmlrpc-0.10.0/tests/functionals/conftest.py 0000664 0000000 0000000 00000003301 15000473553 0021453 0 ustar 00root root 0000000 0000000 import asyncio
from datetime import datetime
import socket
from math import pow
import pytest
from aioxmlrpc.client import ServerProxy
from aioxmlrpc.server import SimpleXMLRPCServer
async def wait_for_socket(
host: str, port: int, timeout: int = 5, poll_time: float = 0.1
):
"""Wait until the socket is open, or raise an error if the timeout is exceeded."""
for _ in range(timeout * int(1 / poll_time)):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
if sock.connect_ex((host, port)) == 0:
break
await asyncio.sleep(poll_time)
else:
raise RuntimeError(f"Server on {host}:{port} did not start in time.")
async def multiply(a: int, b: int) -> int:
await asyncio.sleep(0)
return a * b
class ExampleService:
def get_data(self):
return "42"
class dt:
@staticmethod
def now():
return datetime.today()
@pytest.fixture
async def server():
addr = ("localhost", 8000)
async with SimpleXMLRPCServer(addr) as server:
server.register_function(pow)
server.register_function(multiply)
server.register_function(lambda x, y: x + y, "add") # type: ignore
@server.register_function
async def substract(x: int, y: int) -> int:
return x - y
server.register_instance(ExampleService(), allow_dotted_names=True)
server.register_multicall_functions()
task = server.serve_forever()
await wait_for_socket(*addr)
yield "http://localhost:8000/RPC2"
server.server.should_exit = True
await task
@pytest.fixture
async def client(server: str) -> ServerProxy:
return ServerProxy(server)
aioxmlrpc-0.10.0/tests/functionals/test_functionals.py 0000664 0000000 0000000 00000001431 15000473553 0023214 0 ustar 00root root 0000000 0000000 from datetime import datetime
from aioxmlrpc.client import ServerProxy, MultiCall
async def test_method(client: ServerProxy):
assert await client.pow(4, 2) == 16
async def test_lambda(client: ServerProxy):
assert await client.add(4, 2) == 6
async def test_coroutine(client: ServerProxy):
assert await client.multiply(4, 2) == 8
async def test_decorator(client: ServerProxy):
assert await client.substract(16, 2) == 14
async def test_dotted(client: ServerProxy):
assert await client.get_data() == "42"
assert await client.dt.now() == datetime.today()
async def test_multicall(client: ServerProxy):
multicall = MultiCall(client)
multicall.pow(4, 2)
multicall.add(4, 2)
resp = await multicall()
assert resp[0] == 16
assert resp[1] == 6
aioxmlrpc-0.10.0/tests/unittests/ 0000775 0000000 0000000 00000000000 15000473553 0016774 5 ustar 00root root 0000000 0000000 aioxmlrpc-0.10.0/tests/unittests/__init__.py 0000664 0000000 0000000 00000000054 15000473553 0021104 0 ustar 00root root 0000000 0000000 """
Test suite for the aioxmlrpc module
"""
aioxmlrpc-0.10.0/tests/unittests/test_client.py 0000664 0000000 0000000 00000007405 15000473553 0021671 0 ustar 00root root 0000000 0000000 import ssl
import pytest
from httpx import Request, Response
from aioxmlrpc.client import Fault, MultiCall, ProtocolError, ServerProxy
RESPONSES = {
"http://localhost/test_xmlrpc_ok": {
"status": 200,
"body": """
1
""",
},
"http://localhost/test_xmlrpc_multi_ok": {
"status": 200,
"body": """
1
2
""",
},
"http://localhost/test_xmlrpc_fault": {
"status": 200,
"body": """
faultCode
4
faultString
You are not lucky
""",
},
"http://localhost/test_http_500": {
"status": 500,
"body": """
I am really broken
""",
},
}
class DummyAsyncClient:
async def post(self, url, *args, **kwargs):
response = RESPONSES[url]
return Response(
status_code=response["status"],
headers={},
text=response["body"],
request=Request("POST", url),
)
async def test_xmlrpc_ok():
client = ServerProxy("http://localhost/test_xmlrpc_ok", session=DummyAsyncClient())
response = await client.name.space.proxfyiedcall()
assert response == 1
async def test_xmlrpc_fault():
client = ServerProxy(
"http://localhost/test_xmlrpc_fault", session=DummyAsyncClient()
)
with pytest.raises(Fault):
await client.name.space.proxfyiedcall()
async def test_http_500():
client = ServerProxy("http://localhost/test_http_500", session=DummyAsyncClient())
with pytest.raises(ProtocolError):
await client.name.space.proxfyiedcall()
async def test_network_error():
client = ServerProxy("http://nonexistent/nonexistent")
with pytest.raises(ProtocolError):
await client.name.space.proxfyiedcall()
def test_context_default():
client = ServerProxy("http://nonexistent/nonexistent")
ctx = client._session._transport._pool._ssl_context # type: ignore
assert ctx.verify_mode is ssl.VerifyMode.CERT_REQUIRED
def test_context_disable():
client = ServerProxy("http://nonexistent/nonexistent", context=False)
ctx = client._session._transport._pool._ssl_context # type: ignore
assert ctx.verify_mode is ssl.VerifyMode.CERT_NONE
def test_context_custom():
ctx = ssl.create_default_context()
client = ServerProxy("http://nonexistent/nonexistent", context=ctx)
assert client._session._transport._pool._ssl_context is ctx # type: ignore
async def test_multicall():
client = ServerProxy(
"http://localhost/test_xmlrpc_multi_ok", session=DummyAsyncClient()
)
mc = MultiCall(client)
mc.name.space.proxfyiedcall()
mc.name.space.proxfyiedcall()
response = await mc()
assert response[0] == 1
assert response[1] == 2
aioxmlrpc-0.10.0/tests/unittests/test_server.py 0000664 0000000 0000000 00000003751 15000473553 0021721 0 ustar 00root root 0000000 0000000 import pytest
from aioxmlrpc.server import SimpleXMLRPCDispatcher
RPC_CALL = """
division
{0}
{1}
"""
RPC_RESPONSE = """
{0}
"""
RPC_FAULT = """
faultCode
1
faultString
{0}
"""
async def test_marshall_unregister():
d = SimpleXMLRPCDispatcher()
resp = await d._marshaled_dispatch(RPC_CALL.format(8, 2))
assert resp.decode() == RPC_FAULT.format(
"<class 'Exception'>:method \"division\" is not supported"
)
@pytest.mark.parametrize(
"params,expected",
[
pytest.param(RPC_CALL.format(8, 2), RPC_RESPONSE.format("4.0"), id="ok"),
pytest.param(
RPC_CALL.format(8, 0),
RPC_FAULT.format("<class 'ZeroDivisionError'>:division by zero"),
id="fault",
),
],
)
async def test_marshall(params: str, expected: str):
d = SimpleXMLRPCDispatcher()
d.register_function(lambda x, y: x / y, "division")
resp = await d._marshaled_dispatch(params)
assert resp.decode() == expected
async def test_multicall():
d = SimpleXMLRPCDispatcher()
d.register_function(lambda x, y: x / y, "division")
resp = await d.system_multicall(
[
{"methodName": "division", "params": [8, 2]},
{"methodName": "division", "params": [8, 0]},
],
)
assert resp == [
[
4.0,
],
{
"faultCode": 1,
"faultString": "ZeroDivisionError:division by zero",
},
]
aioxmlrpc-0.10.0/uv.lock 0000664 0000000 0000000 00000117234 15000473553 0015104 0 ustar 00root root 0000000 0000000 version = 1
revision = 1
requires-python = ">=3.9"
[[package]]
name = "aioxmlrpc"
version = "0.10.0"
source = { editable = "." }
dependencies = [
{ name = "httpx" },
]
[package.optional-dependencies]
server = [
{ name = "starlette" },
{ name = "uvicorn" },
]
[package.dev-dependencies]
dev = [
{ name = "mypy" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-cov" },
{ name = "starlette" },
{ name = "uvicorn" },
]
[package.metadata]
requires-dist = [
{ name = "httpx", specifier = ">=0.24,<1" },
{ name = "starlette", marker = "extra == 'server'", specifier = ">=0.46.2" },
{ name = "uvicorn", marker = "extra == 'server'", specifier = ">=0.34.1" },
]
provides-extras = ["server"]
[package.metadata.requires-dev]
dev = [
{ name = "mypy", specifier = ">=1.13.0,<2" },
{ name = "pytest", specifier = ">=8.3.3,<9" },
{ name = "pytest-asyncio", specifier = ">=0.24.0" },
{ name = "pytest-cov", specifier = ">=6.0.0,<7" },
{ name = "starlette", specifier = ">=0.46.2" },
{ name = "uvicorn", specifier = ">=0.34.1" },
]
[[package]]
name = "anyio"
version = "4.6.2.post1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 },
]
[[package]]
name = "certifi"
version = "2024.8.30"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
]
[[package]]
name = "click"
version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "coverage"
version = "7.6.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/52/12/3669b6382792783e92046730ad3327f53b2726f0603f4c311c4da4824222/coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", size = 798716 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/93/4ad92f71e28ece5c0326e5f4a6630aa4928a8846654a65cfff69b49b95b9/coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07", size = 206713 },
{ url = "https://files.pythonhosted.org/packages/01/ae/747a580b1eda3f2e431d87de48f0604bd7bc92e52a1a95185a4aa585bc47/coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0", size = 207149 },
{ url = "https://files.pythonhosted.org/packages/07/1a/1f573f8a6145f6d4c9130bbc120e0024daf1b24cf2a78d7393fa6eb6aba7/coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72", size = 235584 },
{ url = "https://files.pythonhosted.org/packages/40/42/c8523f2e4db34aa9389caee0d3688b6ada7a84fcc782e943a868a7f302bd/coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51", size = 233486 },
{ url = "https://files.pythonhosted.org/packages/8d/95/565c310fffa16ede1a042e9ea1ca3962af0d8eb5543bc72df6b91dc0c3d5/coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491", size = 234649 },
{ url = "https://files.pythonhosted.org/packages/d5/81/3b550674d98968ec29c92e3e8650682be6c8b1fa7581a059e7e12e74c431/coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b", size = 233744 },
{ url = "https://files.pythonhosted.org/packages/0d/70/d66c7f51b3e33aabc5ea9f9624c1c9d9655472962270eb5e7b0d32707224/coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea", size = 232204 },
{ url = "https://files.pythonhosted.org/packages/23/2d/2b3a2dbed7a5f40693404c8a09e779d7c1a5fbed089d3e7224c002129ec8/coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a", size = 233335 },
{ url = "https://files.pythonhosted.org/packages/5a/4f/92d1d2ad720d698a4e71c176eacf531bfb8e0721d5ad560556f2c484a513/coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa", size = 209435 },
{ url = "https://files.pythonhosted.org/packages/c7/b9/cdf158e7991e2287bcf9082670928badb73d310047facac203ff8dcd5ff3/coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172", size = 210243 },
{ url = "https://files.pythonhosted.org/packages/87/31/9c0cf84f0dfcbe4215b7eb95c31777cdc0483c13390e69584c8150c85175/coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", size = 206819 },
{ url = "https://files.pythonhosted.org/packages/53/ed/a38401079ad320ad6e054a01ec2b61d270511aeb3c201c80e99c841229d5/coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", size = 207263 },
{ url = "https://files.pythonhosted.org/packages/20/e7/c3ad33b179ab4213f0d70da25a9c214d52464efa11caeab438592eb1d837/coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", size = 239205 },
{ url = "https://files.pythonhosted.org/packages/36/91/fc02e8d8e694f557752120487fd982f654ba1421bbaa5560debf96ddceda/coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", size = 236612 },
{ url = "https://files.pythonhosted.org/packages/cc/57/cb08f0eda0389a9a8aaa4fc1f9fec7ac361c3e2d68efd5890d7042c18aa3/coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", size = 238479 },
{ url = "https://files.pythonhosted.org/packages/d5/c9/2c7681a9b3ca6e6f43d489c2e6653a53278ed857fd6e7010490c307b0a47/coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", size = 237405 },
{ url = "https://files.pythonhosted.org/packages/b5/4e/ebfc6944b96317df8b537ae875d2e57c27b84eb98820bc0a1055f358f056/coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", size = 236038 },
{ url = "https://files.pythonhosted.org/packages/13/f2/3a0bf1841a97c0654905e2ef531170f02c89fad2555879db8fe41a097871/coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", size = 236812 },
{ url = "https://files.pythonhosted.org/packages/b9/9c/66bf59226b52ce6ed9541b02d33e80a6e816a832558fbdc1111a7bd3abd4/coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", size = 209400 },
{ url = "https://files.pythonhosted.org/packages/2a/a0/b0790934c04dfc8d658d4a62acb8f7ca0efdf3818456fcad757b11c6479d/coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", size = 210243 },
{ url = "https://files.pythonhosted.org/packages/7d/e7/9291de916d084f41adddfd4b82246e68d61d6a75747f075f7e64628998d2/coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", size = 207013 },
{ url = "https://files.pythonhosted.org/packages/27/03/932c2c5717a7fa80cd43c6a07d3177076d97b79f12f40f882f9916db0063/coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", size = 207251 },
{ url = "https://files.pythonhosted.org/packages/d5/3f/0af47dcb9327f65a45455fbca846fe96eb57c153af46c4754a3ba678938a/coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", size = 240268 },
{ url = "https://files.pythonhosted.org/packages/8a/3c/37a9d81bbd4b23bc7d46ca820e16174c613579c66342faa390a271d2e18b/coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", size = 237298 },
{ url = "https://files.pythonhosted.org/packages/c0/70/6b0627e5bd68204ee580126ed3513140b2298995c1233bd67404b4e44d0e/coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", size = 239367 },
{ url = "https://files.pythonhosted.org/packages/3c/eb/634d7dfab24ac3b790bebaf9da0f4a5352cbc125ce6a9d5c6cf4c6cae3c7/coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", size = 238853 },
{ url = "https://files.pythonhosted.org/packages/d9/0d/8e3ed00f1266ef7472a4e33458f42e39492e01a64281084fb3043553d3f1/coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", size = 237160 },
{ url = "https://files.pythonhosted.org/packages/ce/9c/4337f468ef0ab7a2e0887a9c9da0e58e2eada6fc6cbee637a4acd5dfd8a9/coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", size = 238824 },
{ url = "https://files.pythonhosted.org/packages/5e/09/3e94912b8dd37251377bb02727a33a67ee96b84bbbe092f132b401ca5dd9/coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", size = 209639 },
{ url = "https://files.pythonhosted.org/packages/01/69/d4f3a4101171f32bc5b3caec8ff94c2c60f700107a6aaef7244b2c166793/coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", size = 210428 },
{ url = "https://files.pythonhosted.org/packages/c2/4d/2dede4f7cb5a70fb0bb40a57627fddf1dbdc6b9c1db81f7c4dcdcb19e2f4/coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", size = 207039 },
{ url = "https://files.pythonhosted.org/packages/3f/f9/d86368ae8c79e28f1fb458ebc76ae9ff3e8bd8069adc24e8f2fed03c58b7/coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", size = 207298 },
{ url = "https://files.pythonhosted.org/packages/64/c5/b4cc3c3f64622c58fbfd4d8b9a7a8ce9d355f172f91fcabbba1f026852f6/coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", size = 239813 },
{ url = "https://files.pythonhosted.org/packages/8a/86/14c42e60b70a79b26099e4d289ccdfefbc68624d096f4481163085aa614c/coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", size = 236959 },
{ url = "https://files.pythonhosted.org/packages/7f/f8/4436a643631a2fbab4b44d54f515028f6099bfb1cd95b13cfbf701e7f2f2/coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", size = 238950 },
{ url = "https://files.pythonhosted.org/packages/49/50/1571810ddd01f99a0a8be464a4ac8b147f322cd1e8e296a1528984fc560b/coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", size = 238610 },
{ url = "https://files.pythonhosted.org/packages/f3/8c/6312d241fe7cbd1f0cade34a62fea6f333d1a261255d76b9a87074d8703c/coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", size = 236697 },
{ url = "https://files.pythonhosted.org/packages/ce/5f/fef33dfd05d87ee9030f614c857deb6df6556b8f6a1c51bbbb41e24ee5ac/coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", size = 238541 },
{ url = "https://files.pythonhosted.org/packages/a9/64/6a984b6e92e1ea1353b7ffa08e27f707a5e29b044622445859200f541e8c/coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", size = 209707 },
{ url = "https://files.pythonhosted.org/packages/5c/60/ce5a9e942e9543783b3db5d942e0578b391c25cdd5e7f342d854ea83d6b7/coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", size = 210439 },
{ url = "https://files.pythonhosted.org/packages/78/53/6719677e92c308207e7f10561a1b16ab8b5c00e9328efc9af7cfd6fb703e/coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", size = 207784 },
{ url = "https://files.pythonhosted.org/packages/fa/dd/7054928930671fcb39ae6a83bb71d9ab5f0afb733172543ced4b09a115ca/coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", size = 208058 },
{ url = "https://files.pythonhosted.org/packages/b5/7d/fd656ddc2b38301927b9eb3aae3fe827e7aa82e691923ed43721fd9423c9/coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", size = 250772 },
{ url = "https://files.pythonhosted.org/packages/90/d0/eb9a3cc2100b83064bb086f18aedde3afffd7de6ead28f69736c00b7f302/coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", size = 246490 },
{ url = "https://files.pythonhosted.org/packages/45/44/3f64f38f6faab8a0cfd2c6bc6eb4c6daead246b97cf5f8fc23bf3788f841/coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", size = 248848 },
{ url = "https://files.pythonhosted.org/packages/5d/11/4c465a5f98656821e499f4b4619929bd5a34639c466021740ecdca42aa30/coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", size = 248340 },
{ url = "https://files.pythonhosted.org/packages/f1/96/ebecda2d016cce9da812f404f720ca5df83c6b29f65dc80d2000d0078741/coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", size = 246229 },
{ url = "https://files.pythonhosted.org/packages/16/d9/3d820c00066ae55d69e6d0eae11d6149a5ca7546de469ba9d597f01bf2d7/coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", size = 247510 },
{ url = "https://files.pythonhosted.org/packages/8f/c3/4fa1eb412bb288ff6bfcc163c11700ff06e02c5fad8513817186e460ed43/coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", size = 210353 },
{ url = "https://files.pythonhosted.org/packages/7e/77/03fc2979d1538884d921c2013075917fc927f41cd8526909852fe4494112/coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", size = 211502 },
{ url = "https://files.pythonhosted.org/packages/fb/27/7efede2355bd1417137246246ab0980751b3ba6065102518a2d1eba6a278/coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3", size = 206714 },
{ url = "https://files.pythonhosted.org/packages/f3/94/594af55226676d078af72b329372e2d036f9ba1eb6bcf1f81debea2453c7/coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c", size = 207146 },
{ url = "https://files.pythonhosted.org/packages/d5/13/19de1c5315b22795dd67dbd9168281632424a344b648d23d146572e42c2b/coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076", size = 235180 },
{ url = "https://files.pythonhosted.org/packages/db/26/8fba01ce9f376708c7efed2761cea740f50a1b4138551886213797a4cecd/coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376", size = 233100 },
{ url = "https://files.pythonhosted.org/packages/74/66/4db60266551b89e820b457bc3811a3c5eaad3c1324cef7730c468633387a/coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0", size = 234231 },
{ url = "https://files.pythonhosted.org/packages/2a/9b/7b33f0892fccce50fc82ad8da76c7af1731aea48ec71279eef63a9522db7/coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858", size = 233383 },
{ url = "https://files.pythonhosted.org/packages/91/49/6ff9c4e8a67d9014e1c434566e9169965f970350f4792a0246cd0d839442/coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111", size = 231863 },
{ url = "https://files.pythonhosted.org/packages/81/f9/c9d330dec440676b91504fcceebca0814718fa71c8498cf29d4e21e9dbfc/coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901", size = 232854 },
{ url = "https://files.pythonhosted.org/packages/ee/d9/605517a023a0ba8eb1f30d958f0a7ff3a21867b07dcb42618f862695ca0e/coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09", size = 209437 },
{ url = "https://files.pythonhosted.org/packages/aa/79/2626903efa84e9f5b9c8ee6972de8338673fdb5bb8d8d2797740bf911027/coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f", size = 210209 },
{ url = "https://files.pythonhosted.org/packages/cc/56/e1d75e8981a2a92c2a777e67c26efa96c66da59d645423146eb9ff3a851b/coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e", size = 198954 },
]
[package.optional-dependencies]
toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]]
name = "exceptiongroup"
version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
]
[[package]]
name = "h11"
version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
]
[[package]]
name = "httpcore"
version = "1.0.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b6/44/ed0fa6a17845fb033bd885c03e842f08c1b9406c86a2e60ac1ae1b9206a6/httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f", size = 85180 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/06/89/b161908e2f51be56568184aeb4a880fd287178d176fd1c860d2217f41106/httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", size = 78011 },
]
[[package]]
name = "httpx"
version = "0.27.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
]
[[package]]
name = "mypy"
version = "1.13.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/8c/206de95a27722b5b5a8c85ba3100467bd86299d92a4f71c6b9aa448bfa2f/mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a", size = 11020731 },
{ url = "https://files.pythonhosted.org/packages/ab/bb/b31695a29eea76b1569fd28b4ab141a1adc9842edde080d1e8e1776862c7/mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80", size = 10184276 },
{ url = "https://files.pythonhosted.org/packages/a5/2d/4a23849729bb27934a0e079c9c1aad912167d875c7b070382a408d459651/mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7", size = 12587706 },
{ url = "https://files.pythonhosted.org/packages/5c/c3/d318e38ada50255e22e23353a469c791379825240e71b0ad03e76ca07ae6/mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f", size = 13105586 },
{ url = "https://files.pythonhosted.org/packages/4a/25/3918bc64952370c3dbdbd8c82c363804678127815febd2925b7273d9482c/mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372", size = 9632318 },
{ url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027 },
{ url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699 },
{ url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263 },
{ url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688 },
{ url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 },
{ url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900 },
{ url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818 },
{ url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 },
{ url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 },
{ url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 },
{ url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 },
{ url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 },
{ url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 },
{ url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 },
{ url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 },
{ url = "https://files.pythonhosted.org/packages/5f/d4/b33ddd40dad230efb317898a2d1c267c04edba73bc5086bf77edeb410fb2/mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", size = 11013906 },
{ url = "https://files.pythonhosted.org/packages/f4/e6/f414bca465b44d01cd5f4a82761e15044bedd1bf8025c5af3cc64518fac5/mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732", size = 10180657 },
{ url = "https://files.pythonhosted.org/packages/38/e9/fc3865e417722f98d58409770be01afb961e2c1f99930659ff4ae7ca8b7e/mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc", size = 12586394 },
{ url = "https://files.pythonhosted.org/packages/2e/35/f4d8b6d2cb0b3dad63e96caf159419dda023f45a358c6c9ac582ccaee354/mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d", size = 13103591 },
{ url = "https://files.pythonhosted.org/packages/22/1d/80594aef135f921dd52e142fa0acd19df197690bd0cde42cea7b88cf5aa2/mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24", size = 9634690 },
{ url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 },
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
]
[[package]]
name = "packaging"
version = "24.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 },
]
[[package]]
name = "pluggy"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
]
[[package]]
name = "pytest"
version = "8.3.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
]
[[package]]
name = "pytest-asyncio"
version = "0.24.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024 },
]
[[package]]
name = "pytest-cov"
version = "6.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage", extra = ["toml"] },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]]
name = "starlette"
version = "0.46.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 },
]
[[package]]
name = "tomli"
version = "2.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237 },
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
[[package]]
name = "uvicorn"
version = "0.34.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/86/37/dd92f1f9cedb5eaf74d9999044306e06abe65344ff197864175dbbd91871/uvicorn-0.34.1.tar.gz", hash = "sha256:af981725fc4b7ffc5cb3b0e9eda6258a90c4b52cb2a83ce567ae0a7ae1757afc", size = 76755 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/38/a5801450940a858c102a7ad9e6150146a25406a119851c993148d56ab041/uvicorn-0.34.1-py3-none-any.whl", hash = "sha256:984c3a8c7ca18ebaad15995ee7401179212c59521e67bfc390c07fa2b8d2e065", size = 62404 },
]