Skip to content

Commit 2c3600c

Browse files
committed
MPT-12840 Add agreements attachments
1 parent 468c07b commit 2c3600c

21 files changed

+841
-176
lines changed

mpt_api_client/http/async_client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def __init__(
3232
base_headers = {
3333
"User-Agent": "swo-marketplace-client/1.0",
3434
"Authorization": f"Bearer {api_token}",
35+
"Accept": "application/json",
3536
}
3637
super().__init__(
3738
base_url=base_url,

mpt_api_client/http/async_service.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -91,19 +91,6 @@ async def get(self, resource_id: str, select: list[str] | str | None = None) ->
9191
select = ",".join(select) if select else None
9292
return await self._resource_action(resource_id=resource_id, query_params={"select": select})
9393

94-
async def update(self, resource_id: str, resource_data: ResourceData) -> Model:
95-
"""Update a resource using `PUT /endpoint/{resource_id}`.
96-
97-
Args:
98-
resource_id: Resource ID.
99-
resource_data: Resource data.
100-
101-
Returns:
102-
Resource object.
103-
104-
"""
105-
return await self._resource_action(resource_id, "PUT", json=resource_data)
106-
10794
async def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> httpx.Response:
10895
"""Fetch one page of resources.
10996
@@ -119,13 +106,14 @@ async def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> ht
119106

120107
return response
121108

122-
async def _resource_do_request(
109+
async def _resource_do_request( # noqa: WPS211
123110
self,
124111
resource_id: str,
125112
method: str = "GET",
126113
action: str | None = None,
127114
json: ResourceData | ResourceList | None = None,
128115
query_params: QueryParam | None = None,
116+
headers: dict[str, str] | None = None,
129117
) -> httpx.Response:
130118
"""Perform an action on a specific resource using.
131119
@@ -138,13 +126,16 @@ async def _resource_do_request(
138126
action: The action name to use.
139127
json: The updated resource data.
140128
query_params: Additional query parameters.
129+
headers: Additional headers.
141130
142131
Raises:
143132
HTTPError: If the action fails.
144133
"""
145-
resource_url = urljoin(f"{self._endpoint}/", resource_id)
134+
resource_url = urljoin(f"{self.endpoint}/", resource_id)
146135
url = urljoin(f"{resource_url}/", action) if action else resource_url
147-
response = await self.http_client.request(method, url, json=json, params=query_params)
136+
response = await self.http_client.request(
137+
method, url, json=json, params=query_params, headers=headers
138+
)
148139
response.raise_for_status()
149140
return response
150141

mpt_api_client/http/base_service.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from mpt_api_client.rql import RQLQuery
99

1010

11-
class ServiceBase[Client, Model: BaseModel]:
11+
class ServiceBase[Client, Model: BaseModel]: # noqa: WPS214
1212
"""Service base with agnostic HTTP client."""
1313

1414
_endpoint: str
@@ -22,11 +22,13 @@ def __init__(
2222
query_rql: RQLQuery | None = None,
2323
query_order_by: list[str] | None = None,
2424
query_select: list[str] | None = None,
25+
endpoint_params: dict[str, str] | None = None,
2526
) -> None:
2627
self.http_client = http_client
2728
self.query_rql: RQLQuery | None = query_rql
2829
self.query_order_by = query_order_by
2930
self.query_select = query_select
31+
self.endpoint_params = endpoint_params or {}
3032

3133
def clone(self) -> Self:
3234
"""Create a copy of collection client for immutable operations.
@@ -39,30 +41,40 @@ def clone(self) -> Self:
3941
query_rql=self.query_rql,
4042
query_order_by=copy.copy(self.query_order_by) if self.query_order_by else None,
4143
query_select=copy.copy(self.query_select) if self.query_select else None,
44+
endpoint_params=self.endpoint_params,
4245
)
4346

44-
def build_url(self, query_params: dict[str, Any] | None = None) -> str: # noqa: WPS210
47+
@property
48+
def endpoint(self) -> str:
49+
"""Service endpoint URL."""
50+
return self._endpoint.format(**self.endpoint_params)
51+
52+
def build_url(
53+
self,
54+
query_params: dict[str, Any] | None = None,
55+
) -> str: # noqa: WPS210
4556
"""Builds the endpoint URL with all the query parameters.
4657
4758
Returns:
4859
Partial URL with query parameters.
4960
"""
5061
query_params = query_params or {}
62+
if self.query_order_by:
63+
query_params.update({"order": ",".join(self.query_order_by)})
64+
if self.query_select:
65+
query_params.update({"select": ",".join(self.query_select)})
66+
5167
query_parts = [
5268
f"{param_key}={param_value}" for param_key, param_value in query_params.items()
5369
]
54-
if self.query_order_by:
55-
str_order_by = ",".join(self.query_order_by)
56-
query_parts.append(f"order={str_order_by}")
57-
if self.query_select:
58-
str_query_select = ",".join(self.query_select)
59-
query_parts.append(f"select={str_query_select}")
70+
6071
if self.query_rql:
6172
query_parts.append(str(self.query_rql))
73+
6274
if query_parts:
6375
query = "&".join(query_parts)
64-
return f"{self._endpoint}?{query}"
65-
return self._endpoint
76+
return f"{self.endpoint}?{query}"
77+
return self.endpoint
6678

6779
def order_by(self, *fields: str) -> Self:
6880
"""Returns new collection with ordering setup.
@@ -109,12 +121,13 @@ def select(self, *fields: str) -> Self:
109121
new_client.query_select = list(fields)
110122
return new_client
111123

112-
def _create_collection(self, response: httpx.Response) -> Collection[Model]:
124+
@classmethod
125+
def _create_collection(cls, response: httpx.Response) -> Collection[Model]:
113126
meta = Meta.from_response(response)
114127
return Collection(
115128
resources=[
116-
self._model_class.new(resource, meta)
117-
for resource in response.json().get(self._collection_key)
129+
cls._model_class.new(resource, meta)
130+
for resource in response.json().get(cls._collection_key)
118131
],
119132
meta=meta,
120133
)

mpt_api_client/http/client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def __init__(
3232
base_headers = {
3333
"User-Agent": "swo-marketplace-client/1.0",
3434
"Authorization": f"Bearer {api_token}",
35+
"content-type": "application/json",
3536
}
3637
super().__init__(
3738
base_url=base_url,

mpt_api_client/http/mixins.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def create(self, resource_data: ResourceData) -> Model:
1212
Returns:
1313
New resource created.
1414
"""
15-
response = self.http_client.post(self._endpoint, json=resource_data) # type: ignore[attr-defined]
15+
response = self.http_client.post(self.endpoint, json=resource_data) # type: ignore[attr-defined]
1616
response.raise_for_status()
1717

1818
return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]
@@ -31,6 +31,23 @@ def delete(self, resource_id: str) -> None:
3131
response.raise_for_status()
3232

3333

34+
class UpdateMixin[Model]:
35+
"""Update resource mixin."""
36+
37+
def update(self, resource_id: str, resource_data: ResourceData) -> Model:
38+
"""Update a resource using `PUT /endpoint/{resource_id}`.
39+
40+
Args:
41+
resource_id: Resource ID.
42+
resource_data: Resource data.
43+
44+
Returns:
45+
Resource object.
46+
47+
"""
48+
return self._resource_action(resource_id, "PUT", json=resource_data) # type: ignore[attr-defined, no-any-return]
49+
50+
3451
class AsyncCreateMixin[Model]:
3552
"""Create resource mixin."""
3653

@@ -40,7 +57,7 @@ async def create(self, resource_data: ResourceData) -> Model:
4057
Returns:
4158
New resource created.
4259
"""
43-
response = await self.http_client.post(self._endpoint, json=resource_data) # type: ignore[attr-defined]
60+
response = await self.http_client.post(self.endpoint, json=resource_data) # type: ignore[attr-defined]
4461
response.raise_for_status()
4562

4663
return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]
@@ -55,6 +72,23 @@ async def delete(self, resource_id: str) -> None:
5572
Args:
5673
resource_id: Resource ID.
5774
"""
58-
url = urljoin(f"{self._endpoint}/", resource_id) # type: ignore[attr-defined]
75+
url = urljoin(f"{self.endpoint}/", resource_id) # type: ignore[attr-defined]
5976
response = await self.http_client.delete(url) # type: ignore[attr-defined]
6077
response.raise_for_status()
78+
79+
80+
class AsyncUpdateMixin[Model]:
81+
"""Update resource mixin."""
82+
83+
async def update(self, resource_id: str, resource_data: ResourceData) -> Model:
84+
"""Update a resource using `PUT /endpoint/{resource_id}`.
85+
86+
Args:
87+
resource_id: Resource ID.
88+
resource_data: Resource data.
89+
90+
Returns:
91+
Resource object.
92+
93+
"""
94+
return await self._resource_action(resource_id, "PUT", json=resource_data) # type: ignore[attr-defined, no-any-return]

mpt_api_client/http/service.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from mpt_api_client.models.collection import ResourceList
1212

1313

14-
class Service[Model: BaseModel](ServiceBase[HTTPClient, Model]):
14+
class Service[Model: BaseModel](ServiceBase[HTTPClient, Model]): # noqa: WPS214
1515
"""Immutable service for RESTful resource collections.
1616
1717
Examples:
@@ -91,19 +91,6 @@ def get(self, resource_id: str, select: list[str] | str | None = None) -> Model:
9191

9292
return self._resource_action(resource_id=resource_id, query_params={"select": select})
9393

94-
def update(self, resource_id: str, resource_data: ResourceData) -> Model:
95-
"""Update a resource using `PUT /endpoint/{resource_id}`.
96-
97-
Args:
98-
resource_id: Resource ID.
99-
resource_data: Resource data.
100-
101-
Returns:
102-
Resource object.
103-
104-
"""
105-
return self._resource_action(resource_id, "PUT", json=resource_data)
106-
10794
def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> httpx.Response:
10895
"""Fetch one page of resources.
10996
@@ -119,13 +106,14 @@ def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> httpx.Re
119106

120107
return response
121108

122-
def _resource_do_request(
109+
def _resource_do_request( # noqa: WPS211
123110
self,
124111
resource_id: str,
125112
method: str = "GET",
126113
action: str | None = None,
127114
json: ResourceData | ResourceList | None = None,
128115
query_params: QueryParam | None = None,
116+
headers: dict[str, str] | None = None,
129117
) -> httpx.Response:
130118
"""Perform an action on a specific resource using `HTTP_METHOD /endpoint/{resource_id}`.
131119
@@ -135,16 +123,19 @@ def _resource_do_request(
135123
action: The action name to use.
136124
json: The updated resource data.
137125
query_params: Additional query parameters.
126+
headers: Additional headers.
138127
139128
Returns:
140129
HTTP response object.
141130
142131
Raises:
143132
HTTPError: If the action fails.
144133
"""
145-
resource_url = urljoin(f"{self._endpoint}/", resource_id)
134+
resource_url = urljoin(f"{self.endpoint}/", resource_id)
146135
url = urljoin(f"{resource_url}/", action) if action else resource_url
147-
response = self.http_client.request(method, url, json=json, params=query_params)
136+
response = self.http_client.request(
137+
method, url, json=json, params=query_params, headers=headers
138+
)
148139
response.raise_for_status()
149140
return response
150141

@@ -166,6 +157,11 @@ def _resource_action(
166157
query_params: Additional query parameters.
167158
"""
168159
response = self._resource_do_request(
169-
resource_id, method, action, json=json, query_params=query_params
160+
resource_id,
161+
method,
162+
action,
163+
json=json,
164+
query_params=query_params,
165+
headers={"Accept": "application/json"},
170166
)
171167
return self._model_class.from_response(response)

mpt_api_client/models/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from mpt_api_client.models.collection import Collection
2+
from mpt_api_client.models.file_model import FileModel
23
from mpt_api_client.models.meta import Meta, Pagination
34
from mpt_api_client.models.model import Model, ResourceData
45

5-
__all__ = ["Collection", "Meta", "Model", "Pagination", "ResourceData"] # noqa: WPS410
6+
__all__ = ["Collection", "FileModel", "Meta", "Model", "Pagination", "ResourceData"] # noqa: WPS410
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import re
2+
3+
from httpx import Response
4+
5+
6+
class FileModel:
7+
"""File resource."""
8+
9+
def __init__(self, response: Response):
10+
self.response = response
11+
12+
@property
13+
def filename(self) -> str | None:
14+
"""Filename from Content-Disposition header.
15+
16+
Returns:
17+
The filename if found in the Content-Disposition header, None otherwise.
18+
"""
19+
content_disposition = self.response.headers.get("content-disposition")
20+
if not content_disposition:
21+
return None
22+
23+
filename_match = re.search(
24+
r'filename\*=(?:UTF-8\'\')?([^;]+)|filename=(?:"([^"]+)"|([^;]+))',
25+
content_disposition,
26+
re.IGNORECASE,
27+
)
28+
29+
if filename_match:
30+
return filename_match.group(1) or filename_match.group(2) or filename_match.group(3)
31+
32+
return None
33+
34+
@property
35+
def file_contents(self) -> bytes:
36+
"""Returns the content of the attachment.
37+
38+
Returns:
39+
The content of the attachment in bytes
40+
41+
Raises:
42+
ResponseNotRead()
43+
44+
"""
45+
return self.response.content
46+
47+
@property
48+
def content_type(self) -> str | None:
49+
"""Returns the content type of the attachment.
50+
51+
Returns:
52+
The content type of the attachment.
53+
"""
54+
ctype = self.response.headers.get("content-type", "")
55+
return str(ctype)

0 commit comments

Comments
 (0)