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
73 changes: 41 additions & 32 deletions mpt_api_client/http/async_client.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import os
from typing import Any, override
from typing import Any

from httpx import URL, AsyncClient, AsyncHTTPTransport, HTTPError, HTTPStatusError, Response
from httpx._client import USE_CLIENT_DEFAULT, UseClientDefault # noqa: PLC2701
from httpx._types import ( # noqa: WPS235
AuthTypes,
CookieTypes,
HeaderTypes,
QueryParamTypes,
RequestContent,
RequestData,
RequestExtensions,
RequestFiles,
TimeoutTypes,
from httpx import (
AsyncClient,
AsyncHTTPTransport,
HTTPError,
HTTPStatusError,
)

from mpt_api_client.exceptions import MPTError, transform_http_status_exception
from mpt_api_client.http.types import (
HeaderTypes,
QueryParam,
RequestFiles,
Response,
)


class AsyncHTTPClient(AsyncClient):
class AsyncHTTPClient:
"""Async HTTP client for interacting with SoftwareOne Marketplace Platform API."""

def __init__(
Expand Down Expand Up @@ -49,43 +48,49 @@ def __init__(
"Authorization": f"Bearer {api_token}",
"Accept": "application/json",
}
super().__init__(
self.httpx_client = AsyncClient(
base_url=base_url,
headers=base_headers,
timeout=timeout,
transport=AsyncHTTPTransport(retries=retries),
)

@override
async def request( # noqa: WPS211
self,
method: str,
url: URL | str,
url: str,
*,
content: RequestContent | None = None, # noqa: WPS110
data: RequestData | None = None, # noqa: WPS110
files: RequestFiles | None = None,
json: Any | None = None,
params: QueryParamTypes | None = None, # noqa: WPS110
query_params: QueryParam | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
extensions: RequestExtensions | None = None,
) -> Response:
"""Perform an HTTP request.

Args:
method: HTTP method.
url: URL to send the request to.
files: Request files.
json: Request JSON data.
query_params: Query parameters.
headers: Request headers.

Returns:
Response object.

Raises:
MPTError: If the request fails.
MPTApiError: If the response contains an error.
MPTHttpError: If the response contains an HTTP error.
"""
try:
response = await super().request(
response = await self.httpx_client.request(
method,
url,
content=content,
data=data,
files=files,
json=json,
params=params,
params=query_params,
headers=headers,
cookies=cookies,
auth=auth,
)
except HTTPError as err:
raise MPTError(f"HTTP Error: {err}") from err
Expand All @@ -94,4 +99,8 @@ async def request( # noqa: WPS211
response.raise_for_status()
except HTTPStatusError as http_status_exception:
raise transform_http_status_exception(http_status_exception) from http_status_exception
return response
return Response(
headers=dict(response.headers),
status_code=response.status_code,
content=response.content,
)
14 changes: 6 additions & 8 deletions mpt_api_client/http/async_service.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
from collections.abc import AsyncIterator
from urllib.parse import urljoin

import httpx

from mpt_api_client.http.async_client import AsyncHTTPClient
from mpt_api_client.http.base_service import ServiceBase
from mpt_api_client.http.types import QueryParam
from mpt_api_client.http.types import QueryParam, Response
from mpt_api_client.models import Collection, ResourceData
from mpt_api_client.models import Model as BaseModel
from mpt_api_client.models.collection import ResourceList
Expand Down Expand Up @@ -77,17 +75,17 @@ async def iterate(self, batch_size: int = 100) -> AsyncIterator[Model]:
break
offset = items_collection.meta.pagination.next_offset()

async def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> httpx.Response:
async def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> Response:
"""Fetch one page of resources.

Returns:
httpx.Response object.
Response object.

Raises:
HTTPStatusError: if the response status code is not 200.
"""
pagination_params: dict[str, int] = {"limit": limit, "offset": offset}
return await self.http_client.get(self.build_url(pagination_params))
return await self.http_client.request("get", self.build_url(pagination_params))

async def _resource_do_request( # noqa: WPS211
self,
Expand All @@ -97,7 +95,7 @@ async def _resource_do_request( # noqa: WPS211
json: ResourceData | ResourceList | None = None,
query_params: QueryParam | None = None,
headers: dict[str, str] | None = None,
) -> httpx.Response:
) -> Response:
"""Perform an action on a specific resource using.

Request with action: `HTTP_METHOD /endpoint/{resource_id}/{action}`.
Expand All @@ -117,7 +115,7 @@ async def _resource_do_request( # noqa: WPS211
resource_url = urljoin(f"{self.endpoint}/", resource_id)
url = urljoin(f"{resource_url}/", action) if action else resource_url
return await self.http_client.request(
method, url, json=json, params=query_params, headers=headers
method, url, json=json, query_params=query_params, headers=headers
)

async def _resource_action(
Expand Down
5 changes: 2 additions & 3 deletions mpt_api_client/http/base_service.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import copy
from typing import Any, Self

import httpx

from mpt_api_client.http.types import Response
from mpt_api_client.models import Collection, Meta
from mpt_api_client.models import Model as BaseModel
from mpt_api_client.rql import RQLQuery
Expand Down Expand Up @@ -122,7 +121,7 @@ def select(self, *fields: str) -> Self:
return new_client

@classmethod
def _create_collection(cls, response: httpx.Response) -> Collection[Model]:
def _create_collection(cls, response: Response) -> Collection[Model]:
meta = Meta.from_response(response)
return Collection(
resources=[
Expand Down
71 changes: 36 additions & 35 deletions mpt_api_client/http/client.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,26 @@
import os
from typing import Any, override
from typing import Any

from httpx import (
URL,
USE_CLIENT_DEFAULT,
Client,
HTTPError,
HTTPStatusError,
HTTPTransport,
Response,
)
from httpx._client import UseClientDefault
from httpx._types import (
AuthTypes,
CookieTypes,
HeaderTypes,
QueryParamTypes,
RequestContent,
RequestData,
RequestExtensions,
TimeoutTypes,
)
from respx.types import RequestFiles

from mpt_api_client.exceptions import (
MPTError,
transform_http_status_exception,
)
from mpt_api_client.http.types import (
HeaderTypes,
QueryParam,
RequestFiles,
Response,
)


class HTTPClient(Client):
class HTTPClient:
"""Sync HTTP client for interacting with SoftwareOne Marketplace Platform API."""

def __init__(
Expand Down Expand Up @@ -60,43 +51,49 @@ def __init__(
"Authorization": f"Bearer {api_token}",
"content-type": "application/json",
}
super().__init__(
self.httpx_client = Client(
base_url=base_url,
headers=base_headers,
timeout=timeout,
transport=HTTPTransport(retries=retries),
)

@override
def request( # noqa: WPS211
self,
method: str,
url: URL | str,
url: str,
*,
content: RequestContent | None = None, # noqa: WPS110
data: RequestData | None = None, # noqa: WPS110
files: RequestFiles | None = None,
json: Any | None = None,
params: QueryParamTypes | None = None, # noqa: WPS110
query_params: QueryParam | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
extensions: RequestExtensions | None = None,
) -> Response:
"""Perform an HTTP request.

Args:
method: HTTP method.
url: URL to send the request to.
files: Request files.
json: Request JSON data.
query_params: Query parameters.
headers: Request headers.

Returns:
Response object.

Raises:
MPTError: If the request fails.
MPTApiError: If the response contains an error.
MPTHttpError: If the response contains an HTTP error.
"""
try:
response = super().request(
response = self.httpx_client.request(
method,
url,
content=content,
data=data,
files=files,
json=json,
params=params,
params=query_params,
headers=headers,
cookies=cookies,
auth=auth,
)
except HTTPError as err:
raise MPTError(f"HTTP Error: {err}") from err
Expand All @@ -105,4 +102,8 @@ def request( # noqa: WPS211
response.raise_for_status()
except HTTPStatusError as http_status_exception:
raise transform_http_status_exception(http_status_exception) from http_status_exception
return response
return Response(
headers=dict(response.headers),
status_code=response.status_code,
content=response.content,
)
14 changes: 6 additions & 8 deletions mpt_api_client/http/mixins.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import json
from urllib.parse import urljoin

from httpx import Response
from httpx._types import FileTypes

from mpt_api_client.http.types import FileTypes, Response
from mpt_api_client.models import FileModel, ResourceData


Expand All @@ -22,7 +20,7 @@ def create(self, resource_data: ResourceData) -> Model:
Returns:
New resource created.
"""
response = self.http_client.post(self.endpoint, json=resource_data) # type: ignore[attr-defined]
response = self.http_client.request("post", self.endpoint, json=resource_data) # type: ignore[attr-defined]

return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]

Expand Down Expand Up @@ -84,7 +82,7 @@ def create(
"application/json",
)

response = self.http_client.post(self.endpoint, files=files) # type: ignore[attr-defined]
response = self.http_client.request("post", self.endpoint, files=files) # type: ignore[attr-defined]

return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]

Expand Down Expand Up @@ -112,7 +110,7 @@ async def create(self, resource_data: ResourceData) -> Model:
Returns:
New resource created.
"""
response = await self.http_client.post(self.endpoint, json=resource_data) # type: ignore[attr-defined]
response = await self.http_client.request("post", self.endpoint, json=resource_data) # type: ignore[attr-defined]

return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]

Expand All @@ -127,7 +125,7 @@ async def delete(self, resource_id: str) -> None:
resource_id: Resource ID.
"""
url = urljoin(f"{self.endpoint}/", resource_id) # type: ignore[attr-defined]
await self.http_client.delete(url) # type: ignore[attr-defined]
await self.http_client.request("delete", url) # type: ignore[attr-defined]


class AsyncUpdateMixin[Model]:
Expand Down Expand Up @@ -175,7 +173,7 @@ async def create(
"application/json",
)

response = await self.http_client.post(self.endpoint, files=files) # type: ignore[attr-defined]
response = await self.http_client.request("post", self.endpoint, files=files) # type: ignore[attr-defined]

return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]

Expand Down
Loading