Skip to content

Commit 2c38c91

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

File tree

14 files changed

+460
-119
lines changed

14 files changed

+460
-119
lines changed
Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
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+
URL,
6+
USE_CLIENT_DEFAULT,
7+
AsyncClient,
8+
AsyncHTTPTransport,
9+
HTTPError,
10+
HTTPStatusError,
11+
)
12+
from httpx._client import UseClientDefault
13+
from httpx._types import AuthTypes as HttpxAuthTypes
14+
15+
from mpt_api_client.exceptions import MPTError, transform_http_status_exception
16+
from mpt_api_client.http.types import (
717
AuthTypes,
8-
CookieTypes,
18+
ContentType,
919
HeaderTypes,
10-
QueryParamTypes,
11-
RequestContent,
20+
QueryParam,
1221
RequestData,
13-
RequestExtensions,
1422
RequestFiles,
15-
TimeoutTypes,
23+
Response,
1624
)
1725

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

21-
class AsyncHTTPClient(AsyncClient):
27+
class AsyncHTTPClient:
2228
"""Async HTTP client for interacting with SoftwareOne Marketplace Platform API."""
2329

2430
def __init__(
@@ -49,33 +55,55 @@ def __init__(
4955
"Authorization": f"Bearer {api_token}",
5056
"Accept": "application/json",
5157
}
52-
super().__init__(
58+
self.httpx_client = AsyncClient(
5359
base_url=base_url,
5460
headers=base_headers,
5561
timeout=timeout,
5662
transport=AsyncHTTPTransport(retries=retries),
5763
)
5864

59-
@override
6065
async def request( # noqa: WPS211
6166
self,
6267
method: str,
6368
url: URL | str,
6469
*,
65-
content: RequestContent | None = None, # noqa: WPS110
70+
content: ContentType | None = None, # noqa: WPS110
6671
data: RequestData | None = None, # noqa: WPS110
6772
files: RequestFiles | None = None,
6873
json: Any | None = None,
69-
params: QueryParamTypes | None = None, # noqa: WPS110
74+
params: QueryParam | None = None, # noqa: WPS110
7075
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,
76+
auth: AuthTypes | bool | None = None,
7677
) -> Response:
78+
"""Perform an HTTP request.
79+
80+
Args:
81+
method: HTTP method.
82+
url: URL to send the request to.
83+
content: Request content.
84+
data: Request data.
85+
files: Request files.
86+
json: Request JSON data.
87+
params: Query parameters.
88+
headers: Request headers.
89+
auth: Authentication.
90+
91+
Returns:
92+
Response object.
93+
94+
Raises:
95+
MPTError: If the request fails.
96+
MPTApiError: If the response contains an error.
97+
MPTHttpError: If the response contains an HTTP error.
98+
"""
99+
httpx_auth: HttpxAuthTypes | UseClientDefault | None = auth # type: ignore[assignment]
100+
if auth is None:
101+
httpx_auth = USE_CLIENT_DEFAULT
102+
elif auth is False:
103+
httpx_auth = None
104+
77105
try:
78-
response = await super().request(
106+
response = await self.httpx_client.request(
79107
method,
80108
url,
81109
content=content,
@@ -84,8 +112,7 @@ async def request( # noqa: WPS211
84112
json=json,
85113
params=params,
86114
headers=headers,
87-
cookies=cookies,
88-
auth=auth,
115+
auth=httpx_auth,
89116
)
90117
except HTTPError as err:
91118
raise MPTError(f"HTTP Error: {err}") from err
@@ -94,4 +121,12 @@ async def request( # noqa: WPS211
94121
response.raise_for_status()
95122
except HTTPStatusError as http_status_exception:
96123
raise transform_http_status_exception(http_status_exception) from http_status_exception
97-
return response
124+
return Response(
125+
headers=dict(response.headers),
126+
status_code=response.status_code,
127+
content=response.content,
128+
)
129+
130+
async def close(self) -> None:
131+
"""Close transport and proxies."""
132+
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: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os
2-
from typing import Any, override
2+
from typing import Any
33

44
from httpx import (
55
URL,
@@ -8,28 +8,26 @@
88
HTTPError,
99
HTTPStatusError,
1010
HTTPTransport,
11-
Response,
1211
)
1312
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
13+
from httpx._types import AuthTypes as HttpxAuthTypes
2514

2615
from mpt_api_client.exceptions import (
2716
MPTError,
2817
transform_http_status_exception,
2918
)
19+
from mpt_api_client.http.types import (
20+
AuthTypes,
21+
ContentType,
22+
HeaderTypes,
23+
QueryParam,
24+
RequestData,
25+
RequestFiles,
26+
Response,
27+
)
3028

3129

32-
class HTTPClient(Client):
30+
class HTTPClient:
3331
"""Sync HTTP client for interacting with SoftwareOne Marketplace Platform API."""
3432

3533
def __init__(
@@ -60,33 +58,55 @@ def __init__(
6058
"Authorization": f"Bearer {api_token}",
6159
"content-type": "application/json",
6260
}
63-
super().__init__(
61+
self.httpx_client = Client(
6462
base_url=base_url,
6563
headers=base_headers,
6664
timeout=timeout,
6765
transport=HTTPTransport(retries=retries),
6866
)
6967

70-
@override
7168
def request( # noqa: WPS211
7269
self,
7370
method: str,
7471
url: URL | str,
7572
*,
76-
content: RequestContent | None = None, # noqa: WPS110
73+
content: ContentType | None = None, # noqa: WPS110
7774
data: RequestData | None = None, # noqa: WPS110
7875
files: RequestFiles | None = None,
7976
json: Any | None = None,
80-
params: QueryParamTypes | None = None, # noqa: WPS110
77+
params: QueryParam | None = None, # noqa: WPS110
8178
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,
79+
auth: AuthTypes | bool | None = None,
8780
) -> Response:
81+
"""Perform an HTTP request.
82+
83+
Args:
84+
method: HTTP method.
85+
url: URL to send the request to.
86+
content: Request content.
87+
data: Request data.
88+
files: Request files.
89+
json: Request JSON data.
90+
params: Query parameters.
91+
headers: Request headers.
92+
auth: Authentication.
93+
94+
Returns:
95+
Response object.
96+
97+
Raises:
98+
MPTError: If the request fails.
99+
MPTApiError: If the response contains an error.
100+
MPTHttpError: If the response contains an HTTP error.
101+
"""
102+
httpx_auth: HttpxAuthTypes | UseClientDefault | None = auth # type: ignore[assignment]
103+
if auth is None:
104+
httpx_auth = USE_CLIENT_DEFAULT
105+
elif auth is False:
106+
httpx_auth = None
107+
88108
try:
89-
response = super().request(
109+
response = self.httpx_client.request(
90110
method,
91111
url,
92112
content=content,
@@ -95,8 +115,7 @@ def request( # noqa: WPS211
95115
json=json,
96116
params=params,
97117
headers=headers,
98-
cookies=cookies,
99-
auth=auth,
118+
auth=httpx_auth,
100119
)
101120
except HTTPError as err:
102121
raise MPTError(f"HTTP Error: {err}") from err
@@ -105,4 +124,12 @@ def request( # noqa: WPS211
105124
response.raise_for_status()
106125
except HTTPStatusError as http_status_exception:
107126
raise transform_http_status_exception(http_status_exception) from http_status_exception
108-
return response
127+
return Response(
128+
headers=dict(response.headers),
129+
status_code=response.status_code,
130+
content=response.content,
131+
)
132+
133+
def close(self) -> None:
134+
"""Close transport and proxies."""
135+
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)