Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions mpt_api_client/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ def __init__(self, status_code: int, message: str, body: str):
super().__init__(f"HTTP {status_code}: {message}")


class MPTMaxRetryError(MPTError):
"""Represents an error when maximum retry attempts are exceeded."""

def __init__(self, message: str, attempts: int):
super().__init__(f"{message} error after {attempts} retry attempts.")


class MPTAPIError(MPTHttpError):
"""Represents an API error."""

Expand Down
36 changes: 18 additions & 18 deletions mpt_api_client/http/async_client.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
import os
from typing import Any

from httpx import (
AsyncClient,
AsyncHTTPTransport,
HTTPError,
HTTPStatusError,
)
from httpx import AsyncClient, HTTPError, RequestError
from httpx_retries import Retry, RetryTransport

from mpt_api_client.constants import APPLICATION_JSON
from mpt_api_client.exceptions import MPTError, transform_http_status_exception
from mpt_api_client.exceptions import MPTError, MPTMaxRetryError
from mpt_api_client.http.client import json_to_file_payload
from mpt_api_client.http.client_utils import get_query_params, validate_base_url
from mpt_api_client.http.query_options import QueryOptions
from mpt_api_client.http.types import (
HeaderTypes,
QueryParam,
RequestFiles,
Response,
)
from mpt_api_client.http.request_response_utils import handle_response_http_error
from mpt_api_client.http.types import HeaderTypes, QueryParam, RequestFiles, Response


class AsyncHTTPClient:
Expand All @@ -32,6 +24,13 @@ def __init__(
timeout: float = 20.0,
retries: int = 5,
):
self._retries = retries
retry = Retry(
total=retries,
allowed_methods={"DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH"},
)
transport = RetryTransport(retry=retry)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
api_token = api_token or os.getenv("MPT_API_TOKEN")
if not api_token:
raise ValueError(
Expand All @@ -49,7 +48,7 @@ def __init__(
base_url=base_url,
headers=base_headers,
timeout=timeout,
transport=AsyncHTTPTransport(retries=retries),
transport=transport,
follow_redirects=True,
)

Expand Down Expand Up @@ -86,6 +85,7 @@ async def request( # noqa: WPS211
MPTError: If the request fails.
MPTApiError: If the response contains an error.
MPTHttpError: If the response contains an HTTP error.
MPTMaxRetryError: If the request fails after maximum retry attempts.
"""
files = dict(files or {})
if force_multipart or (files and json):
Expand All @@ -101,13 +101,13 @@ async def request( # noqa: WPS211
params=params_str or None,
headers=headers,
)
except RequestError as err:
raise MPTMaxRetryError(str(err), self._retries + 1) from err
except HTTPError as err:
raise MPTError(f"HTTP Error: {err}") from err

try:
response.raise_for_status()
except HTTPStatusError as http_status_exception:
raise transform_http_status_exception(http_status_exception) from http_status_exception
handle_response_http_error(response)

return Response(
headers=dict(response.headers),
status_code=response.status_code,
Expand Down
38 changes: 17 additions & 21 deletions mpt_api_client/http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,15 @@
import os
from typing import Any

from httpx import (
Client,
HTTPError,
HTTPStatusError,
HTTPTransport,
)
from httpx import Client, HTTPError, RequestError
from httpx_retries import Retry, RetryTransport

from mpt_api_client.constants import APPLICATION_JSON
from mpt_api_client.exceptions import (
MPTError,
transform_http_status_exception,
)
from mpt_api_client.exceptions import MPTError, MPTMaxRetryError
from mpt_api_client.http.client_utils import get_query_params, validate_base_url
from mpt_api_client.http.query_options import QueryOptions
from mpt_api_client.http.types import (
HeaderTypes,
QueryParam,
RequestFiles,
Response,
)
from mpt_api_client.http.request_response_utils import handle_response_http_error
from mpt_api_client.http.types import HeaderTypes, QueryParam, RequestFiles, Response
from mpt_api_client.models import ResourceData


Expand All @@ -45,6 +34,13 @@ def __init__(
timeout: float = 20.0,
retries: int = 5,
):
self._retries = retries
retry = Retry(
total=self._retries,
allowed_methods={"DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH"},
)
transport = RetryTransport(retry=retry)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
api_token = api_token or os.getenv("MPT_API_TOKEN")
if not api_token:
raise ValueError(
Expand All @@ -62,7 +58,7 @@ def __init__(
base_url=base_url,
headers=base_headers,
timeout=timeout,
transport=HTTPTransport(retries=retries),
transport=transport,
follow_redirects=True,
)

Expand Down Expand Up @@ -114,13 +110,13 @@ def request( # noqa: WPS211
params=params_str or None,
headers=headers,
)
except RequestError as err:
raise MPTMaxRetryError(str(err), self._retries + 1) from err
except HTTPError as err:
raise MPTError(f"HTTP Error: {err}") from err

try:
response.raise_for_status()
except HTTPStatusError as http_status_exception:
raise transform_http_status_exception(http_status_exception) from http_status_exception
handle_response_http_error(response)

return Response(
headers=dict(response.headers),
status_code=response.status_code,
Expand Down
12 changes: 12 additions & 0 deletions mpt_api_client/http/request_response_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from httpx import HTTPStatusError
from httpx import Response as HTTPXResponse

from mpt_api_client.exceptions import transform_http_status_exception


def handle_response_http_error(response: HTTPXResponse) -> None:
"""Handles HTTP response error by raising a transformed HTTPStatusError exception."""
try:
response.raise_for_status()
except HTTPStatusError as http_status_exception:
raise transform_http_status_exception(http_status_exception) from http_status_exception
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ classifiers = [
]
dependencies = [
"httpx==0.28.*",
"httpx-retries==0.5.*",
]

[dependency-groups]
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/http/test_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ async def test_async_http_call_success(async_http_client, mock_response):
async def test_async_http_call_failure(async_http_client):
timeout_route = respx.get(f"{API_URL}/timeout").mock(side_effect=ConnectTimeout("Mock Timeout"))

with pytest.raises(MPTError, match="HTTP Error: Mock Timeout"):
with pytest.raises(MPTError, match=r"Mock Timeout error after 6 retry attempts."):
await async_http_client.request("GET", "/timeout")

assert timeout_route.called
assert timeout_route.call_count == 6


async def test_http_call_with_json_and_files(mocker, async_http_client, mock_httpx_response): # noqa: WPS210
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/http/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import respx
from httpx import ConnectTimeout, Response, codes

from mpt_api_client.exceptions import MPTError
from mpt_api_client.exceptions import MPTMaxRetryError
from mpt_api_client.http.client import HTTPClient
from mpt_api_client.http.query_options import QueryOptions
from tests.unit.conftest import API_TOKEN, API_URL
Expand Down Expand Up @@ -71,10 +71,10 @@ def test_http_call_success(http_client):
def test_http_call_failure(http_client):
timeout_route = respx.get(f"{API_URL}/timeout").mock(side_effect=ConnectTimeout("Mock Timeout"))

with pytest.raises(MPTError, match="HTTP Error: Mock Timeout"):
with pytest.raises(MPTMaxRetryError, match=r"Mock Timeout error after 6 retry attempts."):
http_client.request("GET", "/timeout")

assert timeout_route.called
assert timeout_route.call_count == 6


def test_http_call_with_json_and_files(mocker, http_client, mock_httpx_response):
Expand Down
18 changes: 17 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.