Skip to content

Commit 5a1f491

Browse files
authored
MPT-15036 Unable to create product with icon due to wrong header (#100)
When uploading files api is expecting the request ContentType to be a multipart but currently it was forced to be application/json
2 parents 419d0d5 + 9ccb577 commit 5a1f491

File tree

9 files changed

+110
-12
lines changed

9 files changed

+110
-12
lines changed

mpt_api_client/http/client.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ def __init__(
4949
base_headers = {
5050
"User-Agent": "swo-marketplace-client/1.0",
5151
"Authorization": f"Bearer {api_token}",
52-
"content-type": "application/json",
5352
}
5453
self.httpx_client = Client(
5554
base_url=base_url,

mpt_api_client/http/mixins.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ def create(
8686
_json_to_file_payload(resource_data),
8787
"application/json",
8888
)
89-
9089
response = self.http_client.request("post", self.path, files=files) # type: ignore[attr-defined]
9190

9291
return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]

mpt_api_client/models/model.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ def from_response(cls, response: Response) -> Self:
5252
meta = Meta.from_response(response)
5353
return cls.new(response_data, meta)
5454

55+
@property
56+
def id(self) -> str:
57+
"""Returns the resource ID."""
58+
return str(self._resource_data.get("id", "")) # type: ignore[no-untyped-call]
59+
5560
def to_dict(self) -> dict[str, Any]:
5661
"""Returns the resource as a dictionary."""
5762
return self._resource_data.to_dict()

mpt_api_client/resources/catalog/products.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import json
2+
13
from mpt_api_client.http import AsyncService, Service
24
from mpt_api_client.http.mixins import (
35
AsyncCollectionMixin,
4-
AsyncManagedResourceMixin,
6+
AsyncModifiableResourceMixin,
57
CollectionMixin,
6-
ManagedResourceMixin,
8+
ModifiableResourceMixin,
79
)
10+
from mpt_api_client.http.types import FileTypes
811
from mpt_api_client.models import Model, ResourceData
912
from mpt_api_client.resources.catalog.mixins import (
1013
AsyncPublishableMixin,
@@ -54,13 +57,38 @@ class ProductsServiceConfig:
5457

5558
class ProductsService(
5659
PublishableMixin[Product],
57-
ManagedResourceMixin[Product],
60+
ModifiableResourceMixin[Product],
5861
CollectionMixin[Product],
5962
Service[Product],
6063
ProductsServiceConfig,
6164
):
6265
"""Products service."""
6366

67+
def create(
68+
self,
69+
resource_data: ResourceData,
70+
icon: FileTypes,
71+
) -> Product:
72+
"""Create product with icon.
73+
74+
Args:
75+
resource_data: Product data.
76+
icon: Icon image in jpg, png, GIF, etc.
77+
78+
Returns:
79+
Created resource.
80+
"""
81+
files: dict[str, FileTypes] = {}
82+
files["product"] = (
83+
None,
84+
json.dumps(resource_data),
85+
"application/json",
86+
)
87+
files["icon"] = icon
88+
response = self.http_client.request("post", self.path, files=files)
89+
90+
return self._model_class.from_response(response)
91+
6492
def item_groups(self, product_id: str) -> ItemGroupsService:
6593
"""Return item_groups service."""
6694
return ItemGroupsService(
@@ -108,13 +136,37 @@ def update_settings(self, product_id: str, settings: ResourceData) -> Product:
108136

109137
class AsyncProductsService(
110138
AsyncPublishableMixin[Product],
111-
AsyncManagedResourceMixin[Product],
139+
AsyncModifiableResourceMixin[Product],
112140
AsyncCollectionMixin[Product],
113141
AsyncService[Product],
114142
ProductsServiceConfig,
115143
):
116144
"""Products service."""
117145

146+
async def create(
147+
self,
148+
resource_data: ResourceData,
149+
icon: FileTypes,
150+
) -> Product:
151+
"""Create product with icon.
152+
153+
Args:
154+
resource_data: Product data.
155+
icon: Icon image in jpg, png, GIF, etc.
156+
157+
Returns:
158+
Created resource.
159+
"""
160+
files: dict[str, FileTypes] = {}
161+
files["product"] = (
162+
None,
163+
json.dumps(resource_data),
164+
"application/json",
165+
)
166+
files["icon"] = icon
167+
response = await self.http_client.request("post", self.path, files=files)
168+
return self._model_class.from_response(response)
169+
118170
def item_groups(self, product_id: str) -> AsyncItemGroupsService:
119171
"""Return item_groups service."""
120172
return AsyncItemGroupsService(

tests/unit/http/test_client.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ def test_http_initialization(mocker):
1919
headers={
2020
"User-Agent": "swo-marketplace-client/1.0",
2121
"Authorization": "Bearer test-token",
22-
"content-type": "application/json",
2322
},
2423
timeout=5.0,
2524
transport=mocker.ANY,
@@ -38,7 +37,6 @@ def test_env_initialization(monkeypatch, mocker):
3837
headers={
3938
"User-Agent": "swo-marketplace-client/1.0",
4039
"Authorization": f"Bearer {API_TOKEN}",
41-
"content-type": "application/json",
4240
},
4341
timeout=5.0,
4442
transport=mocker.ANY,

tests/unit/http/test_mixins.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ async def test_async_file_create_with_data(async_media_service):
154154
b"Content-Type: image/jpeg\r\n\r\n"
155155
b"Image content\r\n" in request.content
156156
)
157+
assert "multipart/form-data" in request.headers["Content-Type"]
157158
assert new_media.to_dict() == media_data
158159

159160

tests/unit/models/resource/test_resource.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,18 @@ def test_attribute_getter(mocker, meta_data):
3535

3636
resource = Model.from_response(response)
3737

38-
assert resource.id == 1
38+
assert resource.id == "1"
3939
assert resource.name.given == "Albert"
4040

4141

4242
def test_attribute_setter():
4343
resource_data = {"id": 1, "name": {"given": "Albert", "family": "Einstein"}}
4444
resource = Model(resource_data)
4545

46-
resource.id = 2
46+
resource.id = "2"
4747
resource.name.given = "John"
4848

49-
assert resource.id == 2
49+
assert resource.id == "2"
5050
assert resource.name.given == "John"
5151

5252

tests/unit/models/resource/test_resource_custom_key.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ def test_custom_data_key():
1313

1414
resource = ChargeResourceMock.from_response(response)
1515

16-
assert resource.id == 1
16+
assert resource.id == "1"
1717
assert resource.amount == 100

tests/unit/resources/catalog/test_products.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,47 @@ async def test_async_update_settings(async_products_service):
135135
assert request.method == "PUT"
136136
assert request.url.path == f"/public/v1/catalog/products/{product_id}/settings"
137137
assert product.to_dict() == expected_response
138+
139+
140+
def test_product_create(products_service, tmp_path):
141+
"""Test creating a product (sync)."""
142+
product_data = {"name": "New Product", "category": "Books"}
143+
expected_response = {"id": "PRD-123", "name": "New Product", "category": "Books"}
144+
145+
# Create a temporary icon file
146+
icon_path = tmp_path / "icon.png"
147+
icon_path.write_bytes(b"fake image data")
148+
with icon_path.open("rb") as icon_file, respx.mock:
149+
mock_route = respx.post("https://api.example.com/public/v1/catalog/products").mock(
150+
return_value=httpx.Response(httpx.codes.CREATED, json=expected_response)
151+
)
152+
153+
product = products_service.create(product_data, icon=icon_file)
154+
155+
assert mock_route.call_count == 1
156+
request = mock_route.calls[0].request
157+
assert request.method == "POST"
158+
assert request.url.path == "/public/v1/catalog/products"
159+
assert product.to_dict() == expected_response
160+
161+
162+
async def test_async_product_create(async_products_service, tmp_path):
163+
"""Test creating a product (async)."""
164+
product_data = {"name": "Async Product", "category": "Music"}
165+
expected_response = {"id": "PRD-456", "name": "Async Product", "category": "Music"}
166+
167+
# Create a temporary icon file
168+
icon_path = tmp_path / "icon.png"
169+
icon_path.write_bytes(b"fake image data")
170+
with icon_path.open("rb") as icon_file, respx.mock:
171+
mock_route = respx.post("https://api.example.com/public/v1/catalog/products").mock(
172+
return_value=httpx.Response(httpx.codes.CREATED, json=expected_response)
173+
)
174+
175+
product = await async_products_service.create(product_data, icon=icon_file)
176+
177+
assert mock_route.call_count == 1
178+
request = mock_route.calls[0].request
179+
assert request.method == "POST"
180+
assert request.url.path == "/public/v1/catalog/products"
181+
assert product.to_dict() == expected_response

0 commit comments

Comments
 (0)