Skip to content

Commit c33a6e7

Browse files
committed
MPT-14401 Decouple client and httpx
1 parent e1346f4 commit c33a6e7

File tree

14 files changed

+461
-122
lines changed

14 files changed

+461
-122
lines changed
Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
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
4+
from httpx import (
5+
USE_CLIENT_DEFAULT,
6+
AsyncClient,
7+
AsyncHTTPTransport,
8+
HTTPError,
9+
HTTPStatusError,
10+
)
11+
from httpx._client import UseClientDefault
12+
from httpx._types import AuthTypes as HttpxAuthTypes
13+
14+
from mpt_api_client.exceptions import MPTError, transform_http_status_exception
15+
from mpt_api_client.http.types import (
716
AuthTypes,
8-
CookieTypes,
17+
ContentType,
918
HeaderTypes,
10-
QueryParamTypes,
11-
RequestContent,
19+
QueryParam,
1220
RequestData,
13-
RequestExtensions,
1421
RequestFiles,
15-
TimeoutTypes,
22+
Response,
1623
)
1724

18-
from mpt_api_client.exceptions import MPTError, transform_http_status_exception
19-
2025

21-
class AsyncHTTPClient(AsyncClient):
26+
class AsyncHTTPClient:
2227
"""Async HTTP client for interacting with SoftwareOne Marketplace Platform API."""
2328

2429
def __init__(
@@ -49,33 +54,55 @@ def __init__(
4954
"Authorization": f"Bearer {api_token}",
5055
"Accept": "application/json",
5156
}
52-
super().__init__(
57+
self.httpx_client = AsyncClient(
5358
base_url=base_url,
5459
headers=base_headers,
5560
timeout=timeout,
5661
transport=AsyncHTTPTransport(retries=retries),
5762
)
5863

59-
@override
6064
async def request( # noqa: WPS211
6165
self,
6266
method: str,
63-
url: URL | str,
67+
url: str,
6468
*,
65-
content: RequestContent | None = None, # noqa: WPS110
69+
content: ContentType | None = None, # noqa: WPS110
6670
data: RequestData | None = None, # noqa: WPS110
6771
files: RequestFiles | None = None,
6872
json: Any | None = None,
69-
params: QueryParamTypes | None = None, # noqa: WPS110
73+
params: QueryParam | None = None, # noqa: WPS110
7074
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,
75+
auth: AuthTypes | bool | None = None,
7676
) -> Response:
77+
"""Perform an HTTP request.
78+
79+
Args:
80+
method: HTTP method.
81+
url: URL to send the request to.
82+
content: Request content.
83+
data: Request data.
84+
files: Request files.
85+
json: Request JSON data.
86+
params: Query parameters.
87+
headers: Request headers.
88+
auth: Authentication.
89+
90+
Returns:
91+
Response object.
92+
93+
Raises:
94+
MPTError: If the request fails.
95+
MPTApiError: If the response contains an error.
96+
MPTHttpError: If the response contains an HTTP error.
97+
"""
98+
httpx_auth: HttpxAuthTypes | UseClientDefault | None = auth # type: ignore[assignment]
99+
if auth is None:
100+
httpx_auth = USE_CLIENT_DEFAULT
101+
elif auth is False:
102+
httpx_auth = None
103+
77104
try:
78-
response = await super().request(
105+
response = await self.httpx_client.request(
79106
method,
80107
url,
81108
content=content,
@@ -84,8 +111,7 @@ async def request( # noqa: WPS211
84111
json=json,
85112
params=params,
86113
headers=headers,
87-
cookies=cookies,
88-
auth=auth,
114+
auth=httpx_auth,
89115
)
90116
except HTTPError as err:
91117
raise MPTError(f"HTTP Error: {err}") from err
@@ -94,4 +120,12 @@ async def request( # noqa: WPS211
94120
response.raise_for_status()
95121
except HTTPStatusError as http_status_exception:
96122
raise transform_http_status_exception(http_status_exception) from http_status_exception
97-
return response
123+
return Response(
124+
headers=dict(response.headers),
125+
status_code=response.status_code,
126+
content=response.content,
127+
)
128+
129+
async def close(self) -> None:
130+
"""Close transport and proxies."""
131+
await self.httpx_client.aclose()

mpt_api_client/http/async_service.py

Lines changed: 5 additions & 7 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
@@ -91,17 +89,17 @@ async def get(self, resource_id: str, select: list[str] | str | None = None) ->
9189
select = ",".join(select) if select else None
9290
return await self._resource_action(resource_id=resource_id, query_params={"select": select})
9391

94-
async def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> httpx.Response:
92+
async def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> Response:
9593
"""Fetch one page of resources.
9694
9795
Returns:
98-
httpx.Response object.
96+
Response object.
9997
10098
Raises:
10199
HTTPStatusError: if the response status code is not 200.
102100
"""
103101
pagination_params: dict[str, int] = {"limit": limit, "offset": offset}
104-
return await self.http_client.get(self.build_url(pagination_params))
102+
return await self.http_client.request("get", self.build_url(pagination_params))
105103

106104
async def _resource_do_request( # noqa: WPS211
107105
self,
@@ -111,7 +109,7 @@ async def _resource_do_request( # noqa: WPS211
111109
json: ResourceData | ResourceList | None = None,
112110
query_params: QueryParam | None = None,
113111
headers: dict[str, str] | None = None,
114-
) -> httpx.Response:
112+
) -> Response:
115113
"""Perform an action on a specific resource using.
116114
117115
Request with action: `HTTP_METHOD /endpoint/{resource_id}/{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: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,32 @@
11
import os
2-
from typing import Any, override
2+
from typing import Any
33

44
from httpx import (
5-
URL,
65
USE_CLIENT_DEFAULT,
76
Client,
87
HTTPError,
98
HTTPStatusError,
109
HTTPTransport,
11-
Response,
1210
)
1311
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,
23-
)
24-
from respx.types import RequestFiles
12+
from httpx._types import AuthTypes as HttpxAuthTypes
2513

2614
from mpt_api_client.exceptions import (
2715
MPTError,
2816
transform_http_status_exception,
2917
)
18+
from mpt_api_client.http.types import (
19+
AuthTypes,
20+
ContentType,
21+
HeaderTypes,
22+
QueryParam,
23+
RequestData,
24+
RequestFiles,
25+
Response,
26+
)
3027

3128

32-
class HTTPClient(Client):
29+
class HTTPClient:
3330
"""Sync HTTP client for interacting with SoftwareOne Marketplace Platform API."""
3431

3532
def __init__(
@@ -60,33 +57,55 @@ def __init__(
6057
"Authorization": f"Bearer {api_token}",
6158
"content-type": "application/json",
6259
}
63-
super().__init__(
60+
self.httpx_client = Client(
6461
base_url=base_url,
6562
headers=base_headers,
6663
timeout=timeout,
6764
transport=HTTPTransport(retries=retries),
6865
)
6966

70-
@override
7167
def request( # noqa: WPS211
7268
self,
7369
method: str,
74-
url: URL | str,
70+
url: str,
7571
*,
76-
content: RequestContent | None = None, # noqa: WPS110
72+
content: ContentType | None = None, # noqa: WPS110
7773
data: RequestData | None = None, # noqa: WPS110
7874
files: RequestFiles | None = None,
7975
json: Any | None = None,
80-
params: QueryParamTypes | None = None, # noqa: WPS110
76+
params: QueryParam | None = None, # noqa: WPS110
8177
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,
78+
auth: AuthTypes | bool | None = None,
8779
) -> Response:
80+
"""Perform an HTTP request.
81+
82+
Args:
83+
method: HTTP method.
84+
url: URL to send the request to.
85+
content: Request content.
86+
data: Request data.
87+
files: Request files.
88+
json: Request JSON data.
89+
params: Query parameters.
90+
headers: Request headers.
91+
auth: Authentication.
92+
93+
Returns:
94+
Response object.
95+
96+
Raises:
97+
MPTError: If the request fails.
98+
MPTApiError: If the response contains an error.
99+
MPTHttpError: If the response contains an HTTP error.
100+
"""
101+
httpx_auth: HttpxAuthTypes | UseClientDefault | None = auth # type: ignore[assignment]
102+
if auth is None:
103+
httpx_auth = USE_CLIENT_DEFAULT
104+
elif auth is False:
105+
httpx_auth = None
106+
88107
try:
89-
response = super().request(
108+
response = self.httpx_client.request(
90109
method,
91110
url,
92111
content=content,
@@ -95,8 +114,7 @@ def request( # noqa: WPS211
95114
json=json,
96115
params=params,
97116
headers=headers,
98-
cookies=cookies,
99-
auth=auth,
117+
auth=httpx_auth,
100118
)
101119
except HTTPError as err:
102120
raise MPTError(f"HTTP Error: {err}") from err
@@ -105,4 +123,12 @@ def request( # noqa: WPS211
105123
response.raise_for_status()
106124
except HTTPStatusError as http_status_exception:
107125
raise transform_http_status_exception(http_status_exception) from http_status_exception
108-
return response
126+
return Response(
127+
headers=dict(response.headers),
128+
status_code=response.status_code,
129+
content=response.content,
130+
)
131+
132+
def close(self) -> None:
133+
"""Close transport and proxies."""
134+
self.httpx_client.close()

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)