Skip to content

Commit aba702e

Browse files
committed
MPT-14401 Decouple client and httpx
1 parent 7ea6995 commit aba702e

File tree

14 files changed

+241
-135
lines changed

14 files changed

+241
-135
lines changed
Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
import os
2-
from typing import Any, override
2+
from typing import Any
33

4-
from httpx import URL, AsyncClient, AsyncHTTPTransport, HTTPError, HTTPStatusError, Response
5-
from httpx._client import USE_CLIENT_DEFAULT, UseClientDefault # noqa: PLC2701
6-
from httpx._types import ( # noqa: WPS235
7-
AuthTypes,
8-
CookieTypes,
9-
HeaderTypes,
10-
QueryParamTypes,
11-
RequestContent,
12-
RequestData,
13-
RequestExtensions,
14-
RequestFiles,
15-
TimeoutTypes,
4+
from httpx import (
5+
AsyncClient,
6+
AsyncHTTPTransport,
7+
HTTPError,
8+
HTTPStatusError,
169
)
1710

1811
from mpt_api_client.exceptions import MPTError, transform_http_status_exception
12+
from mpt_api_client.http.types import (
13+
HeaderTypes,
14+
QueryParam,
15+
RequestFiles,
16+
Response,
17+
)
1918

2019

21-
class AsyncHTTPClient(AsyncClient):
20+
class AsyncHTTPClient:
2221
"""Async HTTP client for interacting with SoftwareOne Marketplace Platform API."""
2322

2423
def __init__(
@@ -49,43 +48,49 @@ def __init__(
4948
"Authorization": f"Bearer {api_token}",
5049
"Accept": "application/json",
5150
}
52-
super().__init__(
51+
self.httpx_client = AsyncClient(
5352
base_url=base_url,
5453
headers=base_headers,
5554
timeout=timeout,
5655
transport=AsyncHTTPTransport(retries=retries),
5756
)
5857

59-
@override
6058
async def request( # noqa: WPS211
6159
self,
6260
method: str,
63-
url: URL | str,
61+
url: str,
6462
*,
65-
content: RequestContent | None = None, # noqa: WPS110
66-
data: RequestData | None = None, # noqa: WPS110
6763
files: RequestFiles | None = None,
6864
json: Any | None = None,
69-
params: QueryParamTypes | None = None, # noqa: WPS110
65+
query_params: QueryParam | None = None,
7066
headers: HeaderTypes | None = None,
71-
cookies: CookieTypes | None = None,
72-
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
73-
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
74-
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
75-
extensions: RequestExtensions | None = None,
7667
) -> Response:
68+
"""Perform an HTTP request.
69+
70+
Args:
71+
method: HTTP method.
72+
url: URL to send the request to.
73+
files: Request files.
74+
json: Request JSON data.
75+
query_params: Query parameters.
76+
headers: Request headers.
77+
78+
Returns:
79+
Response object.
80+
81+
Raises:
82+
MPTError: If the request fails.
83+
MPTApiError: If the response contains an error.
84+
MPTHttpError: If the response contains an HTTP error.
85+
"""
7786
try:
78-
response = await super().request(
87+
response = await self.httpx_client.request(
7988
method,
8089
url,
81-
content=content,
82-
data=data,
8390
files=files,
8491
json=json,
85-
params=params,
92+
params=query_params,
8693
headers=headers,
87-
cookies=cookies,
88-
auth=auth,
8994
)
9095
except HTTPError as err:
9196
raise MPTError(f"HTTP Error: {err}") from err
@@ -94,4 +99,8 @@ async def request( # noqa: WPS211
9499
response.raise_for_status()
95100
except HTTPStatusError as http_status_exception:
96101
raise transform_http_status_exception(http_status_exception) from http_status_exception
97-
return response
102+
return Response(
103+
headers=dict(response.headers),
104+
status_code=response.status_code,
105+
content=response.content,
106+
)

mpt_api_client/http/async_service.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
from collections.abc import AsyncIterator
22
from urllib.parse import urljoin
33

4-
import httpx
5-
64
from mpt_api_client.http.async_client import AsyncHTTPClient
75
from mpt_api_client.http.base_service import ServiceBase
8-
from mpt_api_client.http.types import QueryParam
6+
from mpt_api_client.http.types import QueryParam, Response
97
from mpt_api_client.models import Collection, ResourceData
108
from mpt_api_client.models import Model as BaseModel
119
from mpt_api_client.models.collection import ResourceList
@@ -77,17 +75,17 @@ async def iterate(self, batch_size: int = 100) -> AsyncIterator[Model]:
7775
break
7876
offset = items_collection.meta.pagination.next_offset()
7977

80-
async def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> httpx.Response:
78+
async def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> Response:
8179
"""Fetch one page of resources.
8280
8381
Returns:
84-
httpx.Response object.
82+
Response object.
8583
8684
Raises:
8785
HTTPStatusError: if the response status code is not 200.
8886
"""
8987
pagination_params: dict[str, int] = {"limit": limit, "offset": offset}
90-
return await self.http_client.get(self.build_url(pagination_params))
88+
return await self.http_client.request("get", self.build_url(pagination_params))
9189

9290
async def _resource_do_request( # noqa: WPS211
9391
self,
@@ -97,7 +95,7 @@ async def _resource_do_request( # noqa: WPS211
9795
json: ResourceData | ResourceList | None = None,
9896
query_params: QueryParam | None = None,
9997
headers: dict[str, str] | None = None,
100-
) -> httpx.Response:
98+
) -> Response:
10199
"""Perform an action on a specific resource using.
102100
103101
Request with action: `HTTP_METHOD /endpoint/{resource_id}/{action}`.
@@ -117,7 +115,7 @@ async def _resource_do_request( # noqa: WPS211
117115
resource_url = urljoin(f"{self.endpoint}/", resource_id)
118116
url = urljoin(f"{resource_url}/", action) if action else resource_url
119117
return await self.http_client.request(
120-
method, url, json=json, params=query_params, headers=headers
118+
method, url, json=json, query_params=query_params, headers=headers
121119
)
122120

123121
async def _resource_action(

mpt_api_client/http/base_service.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import copy
22
from typing import Any, Self
33

4-
import httpx
5-
4+
from mpt_api_client.http.types import Response
65
from mpt_api_client.models import Collection, Meta
76
from mpt_api_client.models import Model as BaseModel
87
from mpt_api_client.rql import RQLQuery
@@ -122,7 +121,7 @@ def select(self, *fields: str) -> Self:
122121
return new_client
123122

124123
@classmethod
125-
def _create_collection(cls, response: httpx.Response) -> Collection[Model]:
124+
def _create_collection(cls, response: Response) -> Collection[Model]:
126125
meta = Meta.from_response(response)
127126
return Collection(
128127
resources=[

mpt_api_client/http/client.py

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,26 @@
11
import os
2-
from typing import Any, override
2+
from typing import Any
33

44
from httpx import (
5-
URL,
6-
USE_CLIENT_DEFAULT,
75
Client,
86
HTTPError,
97
HTTPStatusError,
108
HTTPTransport,
11-
Response,
12-
)
13-
from httpx._client import UseClientDefault
14-
from httpx._types import (
15-
AuthTypes,
16-
CookieTypes,
17-
HeaderTypes,
18-
QueryParamTypes,
19-
RequestContent,
20-
RequestData,
21-
RequestExtensions,
22-
TimeoutTypes,
239
)
24-
from respx.types import RequestFiles
2510

2611
from mpt_api_client.exceptions import (
2712
MPTError,
2813
transform_http_status_exception,
2914
)
15+
from mpt_api_client.http.types import (
16+
HeaderTypes,
17+
QueryParam,
18+
RequestFiles,
19+
Response,
20+
)
3021

3122

32-
class HTTPClient(Client):
23+
class HTTPClient:
3324
"""Sync HTTP client for interacting with SoftwareOne Marketplace Platform API."""
3425

3526
def __init__(
@@ -60,43 +51,49 @@ def __init__(
6051
"Authorization": f"Bearer {api_token}",
6152
"content-type": "application/json",
6253
}
63-
super().__init__(
54+
self.httpx_client = Client(
6455
base_url=base_url,
6556
headers=base_headers,
6657
timeout=timeout,
6758
transport=HTTPTransport(retries=retries),
6859
)
6960

70-
@override
7161
def request( # noqa: WPS211
7262
self,
7363
method: str,
74-
url: URL | str,
64+
url: str,
7565
*,
76-
content: RequestContent | None = None, # noqa: WPS110
77-
data: RequestData | None = None, # noqa: WPS110
7866
files: RequestFiles | None = None,
7967
json: Any | None = None,
80-
params: QueryParamTypes | None = None, # noqa: WPS110
68+
query_params: QueryParam | None = None,
8169
headers: HeaderTypes | None = None,
82-
cookies: CookieTypes | None = None,
83-
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
84-
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
85-
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
86-
extensions: RequestExtensions | None = None,
8770
) -> Response:
71+
"""Perform an HTTP request.
72+
73+
Args:
74+
method: HTTP method.
75+
url: URL to send the request to.
76+
files: Request files.
77+
json: Request JSON data.
78+
query_params: Query parameters.
79+
headers: Request headers.
80+
81+
Returns:
82+
Response object.
83+
84+
Raises:
85+
MPTError: If the request fails.
86+
MPTApiError: If the response contains an error.
87+
MPTHttpError: If the response contains an HTTP error.
88+
"""
8889
try:
89-
response = super().request(
90+
response = self.httpx_client.request(
9091
method,
9192
url,
92-
content=content,
93-
data=data,
9493
files=files,
9594
json=json,
96-
params=params,
95+
params=query_params,
9796
headers=headers,
98-
cookies=cookies,
99-
auth=auth,
10097
)
10198
except HTTPError as err:
10299
raise MPTError(f"HTTP Error: {err}") from err
@@ -105,4 +102,8 @@ def request( # noqa: WPS211
105102
response.raise_for_status()
106103
except HTTPStatusError as http_status_exception:
107104
raise transform_http_status_exception(http_status_exception) from http_status_exception
108-
return response
105+
return Response(
106+
headers=dict(response.headers),
107+
status_code=response.status_code,
108+
content=response.content,
109+
)

mpt_api_client/http/mixins.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import json
22
from urllib.parse import urljoin
33

4-
from httpx import Response
5-
from httpx._types import FileTypes
6-
4+
from mpt_api_client.http.types import FileTypes, Response
75
from mpt_api_client.models import FileModel, ResourceData
86

97

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

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

@@ -84,7 +82,7 @@ def create(
8482
"application/json",
8583
)
8684

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

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

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

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

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

132130

133131
class AsyncUpdateMixin[Model]:
@@ -175,7 +173,7 @@ async def create(
175173
"application/json",
176174
)
177175

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

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

0 commit comments

Comments
 (0)