Skip to content

Commit 447099e

Browse files
committed
MPT-13322 Add catalog products media
1 parent e9db3ed commit 447099e

File tree

7 files changed

+508
-1
lines changed

7 files changed

+508
-1
lines changed

mpt_api_client/resources/catalog/products.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@
1111
AsyncPublishableMixin,
1212
PublishableMixin,
1313
)
14+
from mpt_api_client.resources.catalog.products_item_groups import (
15+
AsyncItemGroupsService,
16+
ItemGroupsService,
17+
)
18+
from mpt_api_client.resources.catalog.products_media import (
19+
AsyncMediaService,
20+
MediaService,
21+
)
1422
from mpt_api_client.resources.catalog.products_parameter_groups import (
1523
AsyncParameterGroupsService,
1624
ParameterGroupsService,
@@ -43,12 +51,24 @@ class ProductsService(
4351
):
4452
"""Products service."""
4553

54+
def item_groups(self, product_id: str) -> ItemGroupsService:
55+
"""Return item_groups service."""
56+
return ItemGroupsService(
57+
http_client=self.http_client, endpoint_params={"product_id": product_id}
58+
)
59+
4660
def parameter_groups(self, product_id: str) -> ParameterGroupsService:
4761
"""Return parameter_groups service."""
4862
return ParameterGroupsService(
4963
http_client=self.http_client, endpoint_params={"product_id": product_id}
5064
)
5165

66+
def media(self, product_id: str) -> MediaService:
67+
"""Return media service."""
68+
return MediaService(
69+
http_client=self.http_client, endpoint_params={"product_id": product_id}
70+
)
71+
5272
def product_parameters(self, product_id: str) -> ParametersService:
5373
"""Return product_parameters service."""
5474
return ParametersService(
@@ -66,12 +86,24 @@ class AsyncProductsService(
6686
):
6787
"""Products service."""
6888

89+
def item_groups(self, product_id: str) -> AsyncItemGroupsService:
90+
"""Return item_groups service."""
91+
return AsyncItemGroupsService(
92+
http_client=self.http_client, endpoint_params={"product_id": product_id}
93+
)
94+
6995
def parameter_groups(self, product_id: str) -> AsyncParameterGroupsService:
7096
"""Return parameter_groups service."""
7197
return AsyncParameterGroupsService(
7298
http_client=self.http_client, endpoint_params={"product_id": product_id}
7399
)
74100

101+
def media(self, product_id: str) -> AsyncMediaService:
102+
"""Return media service."""
103+
return AsyncMediaService(
104+
http_client=self.http_client, endpoint_params={"product_id": product_id}
105+
)
106+
75107
def product_parameters(self, product_id: str) -> AsyncParametersService:
76108
"""Return product_parameters service."""
77109
return AsyncParametersService(
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from mpt_api_client.http import AsyncService, CreateMixin, DeleteMixin, Service
2+
from mpt_api_client.http.mixins import (
3+
AsyncCreateMixin,
4+
AsyncDeleteMixin,
5+
AsyncUpdateMixin,
6+
UpdateMixin,
7+
)
8+
from mpt_api_client.models import Model
9+
10+
11+
class ItemGroup(Model):
12+
"""Item Group resource."""
13+
14+
15+
class ItemGroupsServiceConfig:
16+
"""Item Groups service configuration."""
17+
18+
_endpoint = "/public/v1/catalog/products/{product_id}/item-groups"
19+
_model_class = ItemGroup
20+
_collection_key = "data"
21+
22+
23+
class ItemGroupsService(
24+
CreateMixin[ItemGroup],
25+
DeleteMixin,
26+
UpdateMixin[ItemGroup],
27+
Service[ItemGroup],
28+
ItemGroupsServiceConfig,
29+
):
30+
"""Item Groups service."""
31+
32+
33+
class AsyncItemGroupsService(
34+
AsyncCreateMixin[ItemGroup],
35+
AsyncDeleteMixin,
36+
AsyncUpdateMixin[ItemGroup],
37+
AsyncService[ItemGroup],
38+
ItemGroupsServiceConfig,
39+
):
40+
"""Item Groups service."""
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import json
2+
from typing import override
3+
4+
from httpx import Response
5+
from httpx._types import FileTypes
6+
7+
from mpt_api_client.http import AsyncService, CreateMixin, DeleteMixin, Service
8+
from mpt_api_client.http.mixins import (
9+
AsyncCreateMixin,
10+
AsyncDeleteMixin,
11+
AsyncUpdateMixin,
12+
UpdateMixin,
13+
)
14+
from mpt_api_client.models import FileModel, Model, ResourceData
15+
16+
17+
def _json_to_file_payload(resource_data: ResourceData) -> bytes:
18+
return json.dumps(
19+
resource_data, ensure_ascii=False, separators=(",", ":"), allow_nan=False
20+
).encode("utf-8")
21+
22+
23+
class Media(Model):
24+
"""Media resource."""
25+
26+
27+
class MediaServiceConfig:
28+
"""Media service configuration."""
29+
30+
_endpoint = "/public/v1/catalog/products/{product_id}/media"
31+
_model_class = Media
32+
_collection_key = "data"
33+
34+
35+
class MediaService(
36+
CreateMixin[Media],
37+
DeleteMixin,
38+
UpdateMixin[Media],
39+
Service[Media],
40+
MediaServiceConfig,
41+
):
42+
"""Media service."""
43+
44+
@override
45+
def create(
46+
self,
47+
resource_data: ResourceData | None = None,
48+
files: dict[str, FileTypes] | None = None, # noqa: WPS221
49+
) -> Media:
50+
"""Create Media resource.
51+
52+
Currently are two types of media resources available image and video.
53+
54+
Video:
55+
resource_data:
56+
{
57+
"name": "SomeMediaFile",
58+
"description":"Some media description",
59+
"mediaType": "Video",
60+
"url": http://www.somemedia.com/somevideo.avi,
61+
"displayOrder": 1
62+
}
63+
files: Add an image with the video thumbnail
64+
65+
Image:
66+
resource_data:
67+
{
68+
"name": "SomeMediaFile",
69+
"description":"Some media description",
70+
"mediaType": "Video",
71+
"displayOrder": 1
72+
}
73+
files: The image itself
74+
75+
Args:
76+
resource_data: Resource data.
77+
files: Files data.
78+
79+
Returns:
80+
Media resource.
81+
"""
82+
files = files or {}
83+
84+
# Note: This is a workaround to fulfill MPT API request format
85+
#
86+
# HTTPx does not support sending json and files in the same call
87+
# currently only supports sending form-data and files in the same call.
88+
# https://www.python-httpx.org/quickstart/#sending-multipart-file-uploads
89+
#
90+
# MPT API expects files and data to be submitted in a multipart form-data upload.
91+
#
92+
# Current workaround is to send the json data as an unnamed file.
93+
# This ends adding the json as payload multipart data.
94+
#
95+
# json.dumps is setup using the same params of httpx json encoder to produce the same
96+
# encodings.
97+
98+
if resource_data:
99+
files["_media_data"] = (
100+
None,
101+
_json_to_file_payload(resource_data),
102+
"application/json",
103+
)
104+
105+
response = self.http_client.post(self.endpoint, files=files)
106+
response.raise_for_status()
107+
return Media.from_response(response)
108+
109+
def download(self, media_id: str) -> FileModel:
110+
"""Download the media file for the given media ID.
111+
112+
Args:
113+
media_id: Media ID.
114+
115+
Returns:
116+
Media file.
117+
"""
118+
response: Response = self._resource_do_request(
119+
media_id, method="GET", headers={"Accept": "*"}
120+
)
121+
return FileModel(response)
122+
123+
124+
class AsyncMediaService(
125+
AsyncCreateMixin[Media],
126+
AsyncDeleteMixin,
127+
AsyncUpdateMixin[Media],
128+
AsyncService[Media],
129+
MediaServiceConfig,
130+
):
131+
"""Media service."""
132+
133+
@override
134+
async def create(
135+
self,
136+
resource_data: ResourceData | None = None,
137+
files: dict[str, FileTypes] | None = None, # noqa: WPS221
138+
) -> Media:
139+
"""Create Media resource.
140+
141+
Args:
142+
resource_data: Resource data.
143+
files: Files data.
144+
145+
Returns:
146+
Media resource.
147+
"""
148+
files = files or {}
149+
150+
# Note: This is a workaround to fulfill MPT API request format
151+
#
152+
# HTTPx does not support sending json and files in the same call
153+
# currently only supports sending form-data and files in the same call.
154+
# https://www.python-httpx.org/quickstart/#sending-multipart-file-uploads
155+
#
156+
# MPT API expects files and data to be submitted in a multipart form-data upload.
157+
#
158+
# Current workaround is to send the json data as an unnamed file.
159+
# This ends adding the json as payload multipart data.
160+
#
161+
# json.dumps is setup using the same params of httpx json encoder to produce the same
162+
# encodings.
163+
164+
if resource_data:
165+
files["_media_data"] = (
166+
None,
167+
_json_to_file_payload(resource_data),
168+
"application/json",
169+
)
170+
171+
response = await self.http_client.post(self.endpoint, files=files)
172+
response.raise_for_status()
173+
return Media.from_response(response)
174+
175+
async def download(self, media_id: str) -> FileModel:
176+
"""Download the media file for the given media ID.
177+
178+
Args:
179+
media_id: Media ID.
180+
181+
Returns:
182+
Media file.
183+
"""
184+
response = await self._resource_do_request(media_id, method="GET", headers={"Accept": "*"})
185+
return FileModel(response)

setup.cfg

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ extend-ignore =
3333

3434
per-file-ignores =
3535
mpt_api_client/rql/query_builder.py: WPS110 WPS115 WPS210 WPS214
36-
mpt_api_client/resources/catalog/products.py: WPS215
36+
mpt_api_client/resources/catalog/products.py: WPS204 WPS215
3737
mpt_api_client/resources/catalog/items.py: WPS215
38+
mpt_api_client/resources/catalog/products_item_groups.py: WPS215
3839
mpt_api_client/resources/catalog/products_parameter_groups.py: WPS215
3940
mpt_api_client/resources/catalog/products_parameters.py: WPS215
41+
mpt_api_client/resources/catalog/products_media.py: WPS215
4042
tests/http/test_async_service.py: WPS204 WPS202
4143
tests/http/test_service.py: WPS204 WPS202
4244

tests/resources/catalog/test_products.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import pytest
22

33
from mpt_api_client.resources.catalog.products import AsyncProductsService, ProductsService
4+
from mpt_api_client.resources.catalog.products_item_groups import (
5+
AsyncItemGroupsService,
6+
ItemGroupsService,
7+
)
8+
from mpt_api_client.resources.catalog.products_media import (
9+
AsyncMediaService,
10+
MediaService,
11+
)
412
from mpt_api_client.resources.catalog.products_parameter_groups import (
513
AsyncParameterGroupsService,
614
ParameterGroupsService,
@@ -38,7 +46,9 @@ def test_async_mixins_present(async_products_service, method):
3846
@pytest.mark.parametrize(
3947
("service_method", "expected_service_class"),
4048
[
49+
("item_groups", ItemGroupsService),
4150
("parameter_groups", ParameterGroupsService),
51+
("media", MediaService),
4252
("product_parameters", ParametersService),
4353
],
4454
)
@@ -52,7 +62,9 @@ def test_property_services(products_service, service_method, expected_service_cl
5262
@pytest.mark.parametrize(
5363
("service_method", "expected_service_class"),
5464
[
65+
("item_groups", AsyncItemGroupsService),
5566
("parameter_groups", AsyncParameterGroupsService),
67+
("media", AsyncMediaService),
5668
("product_parameters", AsyncParametersService),
5769
],
5870
)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import pytest
2+
3+
from mpt_api_client.resources.catalog.products_item_groups import (
4+
AsyncItemGroupsService,
5+
ItemGroupsService,
6+
)
7+
8+
9+
@pytest.fixture
10+
def item_groups_service(http_client):
11+
return ItemGroupsService(http_client=http_client, endpoint_params={"product_id": "PRD-001"})
12+
13+
14+
@pytest.fixture
15+
def async_item_groups_service(async_http_client):
16+
return AsyncItemGroupsService(
17+
http_client=async_http_client, endpoint_params={"product_id": "PRD-001"}
18+
)
19+
20+
21+
def test_endpoint(item_groups_service):
22+
assert item_groups_service.endpoint == "/public/v1/catalog/products/PRD-001/item-groups"
23+
24+
25+
def test_async_endpoint(async_item_groups_service):
26+
assert async_item_groups_service.endpoint == "/public/v1/catalog/products/PRD-001/item-groups"
27+
28+
29+
@pytest.mark.parametrize("method", ["get", "create", "delete", "update"])
30+
def test_methods_present(item_groups_service, method):
31+
assert hasattr(item_groups_service, method)
32+
33+
34+
@pytest.mark.parametrize("method", ["get", "create", "delete", "update"])
35+
def test_async_methods_present(async_item_groups_service, method):
36+
assert hasattr(async_item_groups_service, method)

0 commit comments

Comments
 (0)