diff --git a/mpt_api_client/http/client.py b/mpt_api_client/http/client.py index 915298d8..ee8aa05e 100644 --- a/mpt_api_client/http/client.py +++ b/mpt_api_client/http/client.py @@ -49,7 +49,6 @@ def __init__( base_headers = { "User-Agent": "swo-marketplace-client/1.0", "Authorization": f"Bearer {api_token}", - "content-type": "application/json", } self.httpx_client = Client( base_url=base_url, diff --git a/mpt_api_client/http/mixins.py b/mpt_api_client/http/mixins.py index fb09a6be..375d32a7 100644 --- a/mpt_api_client/http/mixins.py +++ b/mpt_api_client/http/mixins.py @@ -86,7 +86,6 @@ def create( _json_to_file_payload(resource_data), "application/json", ) - response = self.http_client.request("post", self.path, files=files) # type: ignore[attr-defined] return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return] diff --git a/mpt_api_client/models/model.py b/mpt_api_client/models/model.py index c43da7a2..60b82e14 100644 --- a/mpt_api_client/models/model.py +++ b/mpt_api_client/models/model.py @@ -52,6 +52,11 @@ def from_response(cls, response: Response) -> Self: meta = Meta.from_response(response) return cls.new(response_data, meta) + @property + def id(self) -> str: + """Returns the resource ID.""" + return str(self._resource_data.get("id", "")) # type: ignore[no-untyped-call] + def to_dict(self) -> dict[str, Any]: """Returns the resource as a dictionary.""" return self._resource_data.to_dict() diff --git a/mpt_api_client/resources/catalog/products.py b/mpt_api_client/resources/catalog/products.py index f6ef8791..cece131f 100644 --- a/mpt_api_client/resources/catalog/products.py +++ b/mpt_api_client/resources/catalog/products.py @@ -1,10 +1,13 @@ +import json + from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, - AsyncManagedResourceMixin, + AsyncModifiableResourceMixin, CollectionMixin, - ManagedResourceMixin, + ModifiableResourceMixin, ) +from mpt_api_client.http.types import FileTypes from mpt_api_client.models import Model, ResourceData from mpt_api_client.resources.catalog.mixins import ( AsyncPublishableMixin, @@ -54,13 +57,38 @@ class ProductsServiceConfig: class ProductsService( PublishableMixin[Product], - ManagedResourceMixin[Product], + ModifiableResourceMixin[Product], CollectionMixin[Product], Service[Product], ProductsServiceConfig, ): """Products service.""" + def create( + self, + resource_data: ResourceData, + icon: FileTypes, + ) -> Product: + """Create product with icon. + + Args: + resource_data: Product data. + icon: Icon image in jpg, png, GIF, etc. + + Returns: + Created resource. + """ + files: dict[str, FileTypes] = {} + files["product"] = ( + None, + json.dumps(resource_data), + "application/json", + ) + files["icon"] = icon + response = self.http_client.request("post", self.path, files=files) + + return self._model_class.from_response(response) + def item_groups(self, product_id: str) -> ItemGroupsService: """Return item_groups service.""" return ItemGroupsService( @@ -108,13 +136,37 @@ def update_settings(self, product_id: str, settings: ResourceData) -> Product: class AsyncProductsService( AsyncPublishableMixin[Product], - AsyncManagedResourceMixin[Product], + AsyncModifiableResourceMixin[Product], AsyncCollectionMixin[Product], AsyncService[Product], ProductsServiceConfig, ): """Products service.""" + async def create( + self, + resource_data: ResourceData, + icon: FileTypes, + ) -> Product: + """Create product with icon. + + Args: + resource_data: Product data. + icon: Icon image in jpg, png, GIF, etc. + + Returns: + Created resource. + """ + files: dict[str, FileTypes] = {} + files["product"] = ( + None, + json.dumps(resource_data), + "application/json", + ) + files["icon"] = icon + response = await self.http_client.request("post", self.path, files=files) + return self._model_class.from_response(response) + def item_groups(self, product_id: str) -> AsyncItemGroupsService: """Return item_groups service.""" return AsyncItemGroupsService( diff --git a/tests/unit/http/test_client.py b/tests/unit/http/test_client.py index 3970ea93..026b1844 100644 --- a/tests/unit/http/test_client.py +++ b/tests/unit/http/test_client.py @@ -19,7 +19,6 @@ def test_http_initialization(mocker): headers={ "User-Agent": "swo-marketplace-client/1.0", "Authorization": "Bearer test-token", - "content-type": "application/json", }, timeout=5.0, transport=mocker.ANY, @@ -38,7 +37,6 @@ def test_env_initialization(monkeypatch, mocker): headers={ "User-Agent": "swo-marketplace-client/1.0", "Authorization": f"Bearer {API_TOKEN}", - "content-type": "application/json", }, timeout=5.0, transport=mocker.ANY, diff --git a/tests/unit/http/test_mixins.py b/tests/unit/http/test_mixins.py index f401d8c8..227525bb 100644 --- a/tests/unit/http/test_mixins.py +++ b/tests/unit/http/test_mixins.py @@ -154,6 +154,7 @@ async def test_async_file_create_with_data(async_media_service): b"Content-Type: image/jpeg\r\n\r\n" b"Image content\r\n" in request.content ) + assert "multipart/form-data" in request.headers["Content-Type"] assert new_media.to_dict() == media_data diff --git a/tests/unit/models/resource/test_resource.py b/tests/unit/models/resource/test_resource.py index 4ab2ae38..1f504096 100644 --- a/tests/unit/models/resource/test_resource.py +++ b/tests/unit/models/resource/test_resource.py @@ -35,7 +35,7 @@ def test_attribute_getter(mocker, meta_data): resource = Model.from_response(response) - assert resource.id == 1 + assert resource.id == "1" assert resource.name.given == "Albert" @@ -43,10 +43,10 @@ def test_attribute_setter(): resource_data = {"id": 1, "name": {"given": "Albert", "family": "Einstein"}} resource = Model(resource_data) - resource.id = 2 + resource.id = "2" resource.name.given = "John" - assert resource.id == 2 + assert resource.id == "2" assert resource.name.given == "John" diff --git a/tests/unit/models/resource/test_resource_custom_key.py b/tests/unit/models/resource/test_resource_custom_key.py index f43aea22..e8fc3e55 100644 --- a/tests/unit/models/resource/test_resource_custom_key.py +++ b/tests/unit/models/resource/test_resource_custom_key.py @@ -13,5 +13,5 @@ def test_custom_data_key(): resource = ChargeResourceMock.from_response(response) - assert resource.id == 1 + assert resource.id == "1" assert resource.amount == 100 diff --git a/tests/unit/resources/catalog/test_products.py b/tests/unit/resources/catalog/test_products.py index 4c87a38a..f68d65e5 100644 --- a/tests/unit/resources/catalog/test_products.py +++ b/tests/unit/resources/catalog/test_products.py @@ -135,3 +135,47 @@ async def test_async_update_settings(async_products_service): assert request.method == "PUT" assert request.url.path == f"/public/v1/catalog/products/{product_id}/settings" assert product.to_dict() == expected_response + + +def test_product_create(products_service, tmp_path): + """Test creating a product (sync).""" + product_data = {"name": "New Product", "category": "Books"} + expected_response = {"id": "PRD-123", "name": "New Product", "category": "Books"} + + # Create a temporary icon file + icon_path = tmp_path / "icon.png" + icon_path.write_bytes(b"fake image data") + with icon_path.open("rb") as icon_file, respx.mock: + mock_route = respx.post("https://api.example.com/public/v1/catalog/products").mock( + return_value=httpx.Response(httpx.codes.CREATED, json=expected_response) + ) + + product = products_service.create(product_data, icon=icon_file) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + assert request.method == "POST" + assert request.url.path == "/public/v1/catalog/products" + assert product.to_dict() == expected_response + + +async def test_async_product_create(async_products_service, tmp_path): + """Test creating a product (async).""" + product_data = {"name": "Async Product", "category": "Music"} + expected_response = {"id": "PRD-456", "name": "Async Product", "category": "Music"} + + # Create a temporary icon file + icon_path = tmp_path / "icon.png" + icon_path.write_bytes(b"fake image data") + with icon_path.open("rb") as icon_file, respx.mock: + mock_route = respx.post("https://api.example.com/public/v1/catalog/products").mock( + return_value=httpx.Response(httpx.codes.CREATED, json=expected_response) + ) + + product = await async_products_service.create(product_data, icon=icon_file) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + assert request.method == "POST" + assert request.url.path == "/public/v1/catalog/products" + assert product.to_dict() == expected_response