Skip to content

Commit 371d943

Browse files
committed
MPT-12840 Add agreements assets (WIP)
1 parent 6403e88 commit 371d943

File tree

13 files changed

+274
-13
lines changed

13 files changed

+274
-13
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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ async def _resource_do_request(
126126
action: str | None = None,
127127
json: ResourceData | ResourceList | None = None,
128128
query_params: QueryParam | None = None,
129+
headers: dict[str, str] | None = None,
129130
) -> httpx.Response:
130131
"""Perform an action on a specific resource using.
131132
@@ -138,13 +139,16 @@ async def _resource_do_request(
138139
action: The action name to use.
139140
json: The updated resource data.
140141
query_params: Additional query parameters.
142+
headers: Additional headers.
141143
142144
Raises:
143145
HTTPError: If the action fails.
144146
"""
145147
resource_url = urljoin(f"{self._endpoint}/", resource_id)
146148
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)
149+
response = await self.http_client.request(
150+
method, url, json=json, params=query_params, headers=headers
151+
)
148152
response.raise_for_status()
149153
return response
150154

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: 3 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]
@@ -40,7 +40,7 @@ async def create(self, resource_data: ResourceData) -> Model:
4040
Returns:
4141
New resource created.
4242
"""
43-
response = await self.http_client.post(self._endpoint, json=resource_data) # type: ignore[attr-defined]
43+
response = await self.http_client.post(self.endpoint, json=resource_data) # type: ignore[attr-defined]
4444
response.raise_for_status()
4545

4646
return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]
@@ -55,6 +55,6 @@ async def delete(self, resource_id: str) -> None:
5555
Args:
5656
resource_id: Resource ID.
5757
"""
58-
url = urljoin(f"{self._endpoint}/", resource_id) # type: ignore[attr-defined]
58+
url = urljoin(f"{self.endpoint}/", resource_id) # type: ignore[attr-defined]
5959
response = await self.http_client.delete(url) # type: ignore[attr-defined]
6060
response.raise_for_status()

mpt_api_client/http/service.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def _resource_do_request(
126126
action: str | None = None,
127127
json: ResourceData | ResourceList | None = None,
128128
query_params: QueryParam | None = None,
129+
headers: dict[str, str] | None = None,
129130
) -> httpx.Response:
130131
"""Perform an action on a specific resource using `HTTP_METHOD /endpoint/{resource_id}`.
131132
@@ -135,16 +136,19 @@ def _resource_do_request(
135136
action: The action name to use.
136137
json: The updated resource data.
137138
query_params: Additional query parameters.
139+
headers: Additional headers.
138140
139141
Returns:
140142
HTTP response object.
141143
142144
Raises:
143145
HTTPError: If the action fails.
144146
"""
145-
resource_url = urljoin(f"{self._endpoint}/", resource_id)
147+
resource_url = urljoin(f"{self.endpoint}/", resource_id)
146148
url = urljoin(f"{resource_url}/", action) if action else resource_url
147-
response = self.http_client.request(method, url, json=json, params=query_params)
149+
response = self.http_client.request(
150+
method, url, json=json, params=query_params, headers=headers
151+
)
148152
response.raise_for_status()
149153
return response
150154

@@ -166,6 +170,11 @@ def _resource_action(
166170
query_params: Additional query parameters.
167171
"""
168172
response = self._resource_do_request(
169-
resource_id, method, action, json=json, query_params=query_params
173+
resource_id,
174+
method,
175+
action,
176+
json=json,
177+
query_params=query_params,
178+
headers={"Accept": "application/json"},
170179
)
171180
return self._model_class.from_response(response)

mpt_api_client/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from mpt_api_client.models.collection import Collection
22
from mpt_api_client.models.meta import Meta, Pagination
33
from mpt_api_client.models.model import Model, ResourceData
4+
from mpt_api_client.models.file import File
45

56
__all__ = ["Collection", "Meta", "Model", "Pagination", "ResourceData"] # noqa: WPS410

mpt_api_client/models/file.py

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 File:
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 content(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) if ctype else None

mpt_api_client/resources/commerce/agreements.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
from mpt_api_client.http import AsyncService, Service
22
from mpt_api_client.models import Model
3+
from mpt_api_client.resources.commerce.agreements_assets import (
4+
AgreementsAttachmentService,
5+
AsyncAgreementsAttachmentService,
6+
)
37

48

59
class Agreement(Model):
@@ -29,6 +33,20 @@ def template(self, agreement_id: str) -> str:
2933
response = self._resource_do_request(agreement_id, action="template")
3034
return response.text
3135

36+
def attachments(self, agreement_id: str) -> AgreementsAttachmentService:
37+
"""Get the attachments service for the given Agreement id.
38+
39+
Args:
40+
agreement_id: Agreement ID.
41+
42+
Returns:
43+
Agreements Attachment service.
44+
"""
45+
return AgreementsAttachmentService(
46+
http_client=self.http_client,
47+
endpoint_params={"agreement_id": agreement_id},
48+
)
49+
3250

3351
class AsyncAgreementsService(AsyncService[Agreement], AgreementsServiceConfig):
3452
"""Agreements service."""
@@ -44,3 +62,17 @@ async def template(self, agreement_id: str) -> str:
4462
"""
4563
response = await self._resource_do_request(agreement_id, action="template")
4664
return response.text
65+
66+
def attachments(self, agreement_id: str) -> AsyncAgreementsAttachmentService:
67+
"""Get the attachments service for the given Agreement id.
68+
69+
Args:
70+
agreement_id: Agreement ID.
71+
72+
Returns:
73+
Agreements Attachment service.
74+
"""
75+
return AsyncAgreementsAttachmentService(
76+
http_client=self.http_client,
77+
endpoint_params={"agreement_id": agreement_id},
78+
)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from typing import AsyncIterator, Iterator, Optional
2+
import re
3+
4+
from httpx import Response
5+
from httpx._types import RequestFiles
6+
7+
from mpt_api_client.http import AsyncService, Service
8+
from mpt_api_client.models import File, Model, ResourceData
9+
10+
11+
class AgreementAttachment(Model):
12+
"""Agreement attachment resource."""
13+
14+
15+
class AgreementsAttachmentServiceConfig:
16+
"""Orders service config."""
17+
18+
_endpoint = "/public/v1/commerce/agreements/{agreement_id}/attachments"
19+
_model_class = AgreementAttachment
20+
_collection_key = "data"
21+
22+
23+
class AgreementsAttachmentService(Service[AgreementAttachment], AgreementsAttachmentServiceConfig):
24+
"""Attachments service."""
25+
26+
def create(
27+
self, resource_data: ResourceData, files: RequestFiles | None = None
28+
) -> AgreementAttachment:
29+
response = self.http_client.post(self.endpoint, json=resource_data, files=files)
30+
response.raise_for_status()
31+
return AgreementAttachment.from_response(response)
32+
33+
def download(self, agreement_id: str) -> File:
34+
"""Renders the template for the given Agreement id.
35+
36+
Args:
37+
agreement_id: Agreement ID.
38+
39+
Returns:
40+
Agreement template.
41+
"""
42+
response: Response = self._resource_do_request(
43+
agreement_id,
44+
method="GET",
45+
)
46+
return File(response)
47+
48+
49+
class AsyncAgreementsAttachmentService(
50+
AsyncService[AgreementAttachment], AgreementsAttachmentServiceConfig
51+
):
52+
"""Attachments service."""
53+
54+
async def download(self, agreement_id: str) -> File:
55+
"""Renders the template for the given Agreement id.
56+
57+
Args:
58+
agreement_id: Agreement ID.
59+
60+
Returns:
61+
Agreement template.
62+
"""
63+
response = await self._resource_do_request(
64+
agreement_id, method="GET", headers={"Accept": "*"}
65+
)
66+
return File(response)

tests/http/test_async_service.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,14 @@ async def test_async_update_resource(async_dummy_service): # noqa: WPS210
263263
async def test_async_get(async_dummy_service):
264264
resource_data = {"id": "RES-123", "name": "Test Resource"}
265265
with respx.mock:
266-
respx.get("https://api.example.com/api/v1/test/RES-123").mock(
267-
return_value=httpx.Response(httpx.codes.OK, json=resource_data)
268-
)
266+
mock_route = respx.get(
267+
"https://api.example.com/api/v1/test/RES-123", params={"select": "id,name"}
268+
).mock(return_value=httpx.Response(httpx.codes.OK, json=resource_data))
269269

270270
resource = await async_dummy_service.get("RES-123", select=["id", "name"])
271+
272+
request = mock_route.calls[0].request
273+
accept_header = (b"Accept", b"application/json")
274+
assert accept_header in request.headers.raw
271275
assert isinstance(resource, DummyModel)
272276
assert resource.to_dict() == resource_data

0 commit comments

Comments
 (0)