diff --git a/kickerde_api_client/api.py b/kickerde_api_client/api.py index f7968d9..5ed4250 100644 --- a/kickerde_api_client/api.py +++ b/kickerde_api_client/api.py @@ -5,6 +5,7 @@ from typing import Any import datetype +import httpx from .mapping.league_list import league_list_home_to_dict from .mapping.league_season_info import league_season_info_to_dict @@ -29,11 +30,16 @@ class Api: """The primary API client and entry point for all requests.""" + _provider: ResponseProvider + def __init__( self, + http_client: httpx.AsyncClient | None = None, provider: ResponseProvider | None = None, ) -> None: - self._provider = provider or DefaultResponseProvider() + self._provider = provider or DefaultResponseProvider( + http_client=http_client + ) async def leagues(self) -> dict[LeagueId, League]: """Returns all leagues and tournaments known to the system. diff --git a/kickerde_api_client/provider.py b/kickerde_api_client/provider.py index 526d639..0839d33 100644 --- a/kickerde_api_client/provider.py +++ b/kickerde_api_client/provider.py @@ -2,9 +2,9 @@ from abc import ABC, abstractmethod -import requests +import httpx -from .settings import DEFAULT_ENDPOINT_URL, REQUEST_TIMEOUT_SEC +from .settings import DEFAULT_ENDPOINT_URL class ResponseProvider(ABC): # pylint: disable=too-few-public-methods @@ -25,18 +25,50 @@ async def get(self, path: str) -> str: class DefaultResponseProvider(ResponseProvider): # pylint: disable=too-few-public-methods """The default provider, which issues actual HTTP requests.""" - def __init__(self, base_url: str = DEFAULT_ENDPOINT_URL): + # Internally re-use a single HTTP client for all object instances + # who do not bring their own + _internal_client: httpx.AsyncClient | None = None + + _httpx_client: httpx.AsyncClient + + def __init__( + self, + http_client: httpx.AsyncClient | None = None, + base_url: str = DEFAULT_ENDPOINT_URL, + ): """ + :param http_client: + An optional HTTP client to re-use. + The default value is `None`, which means that an internal + HTTP client will be used. + :param base_url: The Kicker API endpoint to connect to. The default value is the public production endpoint. """ self._base_url = base_url + if ( + http_client is None + and DefaultResponseProvider._internal_client is None + ): + DefaultResponseProvider._internal_client = ( + httpx.AsyncClient() + ) + existing_client = ( + http_client or DefaultResponseProvider._internal_client + ) + assert existing_client is not None + self._httpx_client = existing_client + async def get(self, path: str) -> str: - response = requests.get( + response = await self._httpx_client.get( f'{self._base_url}/{path}', - timeout=REQUEST_TIMEOUT_SEC, + follow_redirects=False, + headers={ + 'Accept': 'application/xml', + 'Cache-Control': 'no-cache', + }, ) response.raise_for_status() return response.text diff --git a/kickerde_api_client/settings.py b/kickerde_api_client/settings.py index 40d1d81..47390d3 100644 --- a/kickerde_api_client/settings.py +++ b/kickerde_api_client/settings.py @@ -10,6 +10,5 @@ DEFAULT_ENDPOINT_URL = ( 'https://ovsyndication.kicker.de/API/universal/3.0' ) -REQUEST_TIMEOUT_SEC = 30 debugMode = bool(os.getenv('KICKERDE_API_CLIENT_DEBUG')) diff --git a/poetry.lock b/poetry.lock index 25feacc..4c9f0d8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,6 +11,26 @@ files = [ {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] +[[package]] +name = "anyio" +version = "4.6.2.post1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +files = [ + {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, + {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] + [[package]] name = "astroid" version = "3.3.5" @@ -223,6 +243,63 @@ files = [ {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, ] +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.6" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f"}, + {file = "httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.27.2" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "idna" version = "3.10" @@ -675,6 +752,17 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + [[package]] name = "snowballstemmer" version = "2.2.0" @@ -875,20 +963,6 @@ files = [ {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] -[[package]] -name = "types-requests" -version = "2.32.0.20241016" -description = "Typing stubs for requests" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"}, - {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"}, -] - -[package.dependencies] -urllib3 = ">=2" - [[package]] name = "types-xmltodict" version = "0.14.0.20241009" @@ -942,4 +1016,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.12" -content-hash = "204a565ca53a77f6580874abee1d5da34c571b0037f41f9a7903dfd4fa5ff23e" +content-hash = "8da92d610d4bbc85ecb902d8faa0fb14c864acea234e326de5bd6d265a1d699b" diff --git a/pyproject.toml b/pyproject.toml index 1043974..39e9864 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ repository = "https://github.com/claui/kickerde-api-client" [tool.poetry.dependencies] python = ">=3.12" datetype = "*" -requests = "^2.32.3" +httpx = "^0.27.2" xmltodict = "^0.14.2" [tool.poetry.group.dev.dependencies] @@ -50,7 +50,6 @@ poethepoet = ">=0.24" pylint = ">=3.0" pytest = "*" pytest-asyncio = "*" -types-requests = "*" types-xmltodict = "*" [tool.poetry.group.doc.dependencies]