From c5027f5f27761f807ed4c64d7a4ac869b608b462 Mon Sep 17 00:00:00 2001 From: Albert Sola Date: Tue, 19 Aug 2025 14:04:44 +0100 Subject: [PATCH] MPT-12330 Order resource client --- mpt_api_client/__init__.py | 4 + mpt_api_client/http/client.py | 2 +- mpt_api_client/http/collection.py | 43 +++++--- mpt_api_client/http/resource.py | 55 ++++++++--- mpt_api_client/models/resource.py | 8 +- mpt_api_client/modules/__init__.py | 3 - mpt_api_client/modules/order.py | 16 --- mpt_api_client/{mpt.py => mptclient.py} | 13 ++- mpt_api_client/registry.py | 2 +- mpt_api_client/resources/__init__.py | 3 + mpt_api_client/resources/order.py | 85 ++++++++++++++++ setup.cfg | 2 + tests/conftest.py | 6 +- tests/http/collection/conftest.py | 16 --- .../collection/test_collection_client_init.py | 11 +-- tests/http/collection/test_collection_get.py | 4 + tests/http/conftest.py | 44 +++++++++ tests/http/resource/conftest.py | 30 ------ tests/http/test_client.py | 10 +- tests/models/resource/test_resource.py | 9 +- .../orders/test_order_collection_client.py} | 8 +- .../orders/test_order_resource_client.py | 97 +++++++++++++++++++ tests/test_mpt.py | 10 +- tests/test_registry.py | 3 +- 24 files changed, 351 insertions(+), 133 deletions(-) delete mode 100644 mpt_api_client/modules/__init__.py delete mode 100644 mpt_api_client/modules/order.py rename mpt_api_client/{mpt.py => mptclient.py} (82%) create mode 100644 mpt_api_client/resources/__init__.py create mode 100644 mpt_api_client/resources/order.py delete mode 100644 tests/http/collection/conftest.py create mode 100644 tests/http/collection/test_collection_get.py create mode 100644 tests/http/conftest.py delete mode 100644 tests/http/resource/conftest.py rename tests/{modules/test_order.py => resources/orders/test_order_collection_client.py} (50%) create mode 100644 tests/resources/orders/test_order_resource_client.py diff --git a/mpt_api_client/__init__.py b/mpt_api_client/__init__.py index e69de29b..2afb1505 100644 --- a/mpt_api_client/__init__.py +++ b/mpt_api_client/__init__.py @@ -0,0 +1,4 @@ +from mpt_api_client.mptclient import MPTClient +from mpt_api_client.rql import RQLQuery + +__all__ = ["MPTClient", "RQLQuery"] # noqa: WPS410 diff --git a/mpt_api_client/http/client.py b/mpt_api_client/http/client.py index 3d2ee7d8..6dbc95e7 100644 --- a/mpt_api_client/http/client.py +++ b/mpt_api_client/http/client.py @@ -3,7 +3,7 @@ import httpx -class MPTClient(httpx.Client): +class HTTPClient(httpx.Client): """A client for interacting with SoftwareOne Marketplace Platform API.""" def __init__( diff --git a/mpt_api_client/http/collection.py b/mpt_api_client/http/collection.py index 95512cd6..38e5be4c 100644 --- a/mpt_api_client/http/collection.py +++ b/mpt_api_client/http/collection.py @@ -5,12 +5,15 @@ import httpx -from mpt_api_client.http.client import MPTClient +from mpt_api_client.http.client import HTTPClient +from mpt_api_client.http.resource import ResourceBaseClient from mpt_api_client.models import Collection, Resource from mpt_api_client.rql.query_builder import RQLQuery -class CollectionBaseClient[ResourceType: Resource](ABC): # noqa: WPS214 +class CollectionBaseClient[ResourceModel: Resource, ResourceClient: ResourceBaseClient[Resource]]( # noqa: WPS214 + ABC +): """Immutable Base client for RESTful resource collections. Examples: @@ -23,21 +26,24 @@ class CollectionBaseClient[ResourceType: Resource](ABC): # noqa: WPS214 """ _endpoint: str - _resource_class: type[ResourceType] - _collection_class: type[Collection[ResourceType]] + _resource_class: type[ResourceModel] + _resource_client_class: type[ResourceClient] + _collection_class: type[Collection[ResourceModel]] def __init__( self, query_rql: RQLQuery | None = None, - client: MPTClient | None = None, + client: HTTPClient | None = None, ) -> None: - self.mpt_client = client or MPTClient() + self.mpt_client = client or HTTPClient() self.query_rql: RQLQuery | None = query_rql self.query_order_by: list[str] | None = None self.query_select: list[str] | None = None @classmethod - def clone(cls, collection_client: "CollectionBaseClient[ResourceType]") -> Self: + def clone( + cls, collection_client: "CollectionBaseClient[ResourceModel, ResourceClient]" + ) -> Self: """Create a copy of collection client for immutable operations. Returns: @@ -122,7 +128,7 @@ def select(self, *fields: str) -> Self: new_client.query_select = list(fields) return new_client - def fetch_page(self, limit: int = 100, offset: int = 0) -> Collection[ResourceType]: + def fetch_page(self, limit: int = 100, offset: int = 0) -> Collection[ResourceModel]: """Fetch one page of resources. Returns: @@ -131,7 +137,7 @@ def fetch_page(self, limit: int = 100, offset: int = 0) -> Collection[ResourceTy response = self._fetch_page_as_response(limit=limit, offset=offset) return Collection.from_response(response) - def fetch_one(self) -> ResourceType: + def fetch_one(self) -> ResourceModel: """Fetch one page, expect exactly one result. Returns: @@ -141,7 +147,7 @@ def fetch_one(self) -> ResourceType: ValueError: If the total matching records are not exactly one. """ response = self._fetch_page_as_response(limit=1, offset=0) - resource_list: Collection[ResourceType] = Collection.from_response(response) + resource_list: Collection[ResourceModel] = Collection.from_response(response) total_records = len(resource_list) if resource_list.meta: total_records = resource_list.meta.pagination.total @@ -152,18 +158,23 @@ def fetch_one(self) -> ResourceType: return resource_list[0] - def iterate(self) -> Iterator[ResourceType]: + def iterate(self, batch_size: int = 100) -> Iterator[ResourceModel]: """Iterate over all resources, yielding GenericResource objects. + Args: + batch_size: Number of resources to fetch per request + Returns: Iterator of resources. """ offset = 0 - limit = 100 # Default page size + limit = batch_size # Default page size while True: response = self._fetch_page_as_response(limit=limit, offset=offset) - items_collection: Collection[ResourceType] = Collection.from_response(response) + items_collection: Collection[ResourceModel] = self._collection_class.from_response( + response + ) yield from items_collection if not items_collection.meta: @@ -172,7 +183,11 @@ def iterate(self) -> Iterator[ResourceType]: break offset = items_collection.meta.pagination.next_offset() - def create(self, resource_data: dict[str, Any]) -> ResourceType: + def get(self, resource_id: str) -> ResourceClient: + """Get resource by resource_id.""" + return self._resource_client_class(client=self.mpt_client, resource_id=resource_id) + + def create(self, resource_data: dict[str, Any]) -> ResourceModel: """Create a new resource using `POST /endpoint`. Returns: diff --git a/mpt_api_client/http/resource.py b/mpt_api_client/http/resource.py index 51fb4805..16677f7b 100644 --- a/mpt_api_client/http/resource.py +++ b/mpt_api_client/http/resource.py @@ -1,18 +1,20 @@ from abc import ABC from typing import Any, ClassVar, Self, override -from mpt_api_client.http.client import MPTClient +from httpx import Response + +from mpt_api_client.http.client import HTTPClient from mpt_api_client.models import Resource -class ResourceBaseClient[ResourceType: Resource](ABC): # noqa: WPS214 +class ResourceBaseClient[ResourceModel: Resource](ABC): # noqa: WPS214 """Client for RESTful resources.""" _endpoint: str - _resource_class: type[Resource] + _resource_class: type[ResourceModel] _safe_attributes: ClassVar[set[str]] = {"mpt_client_", "resource_id_", "resource_"} - def __init__(self, client: MPTClient, resource_id: str) -> None: + def __init__(self, client: HTTPClient, resource_id: str) -> None: self.mpt_client_ = client # noqa: WPS120 self.resource_id_ = resource_id # noqa: WPS120 self.resource_: Resource | None = None # noqa: WPS120 @@ -35,7 +37,7 @@ def __setattr__(self, attribute: str, attribute_value: Any) -> None: self._ensure_resource_is_fetched() self.resource_.__setattr__(attribute, attribute_value) - def fetch(self) -> Resource: + def fetch(self) -> ResourceModel: """Fetch a specific resource using `GET /endpoint/{resource_id}`. It fetches and caches the resource. @@ -43,13 +45,44 @@ def fetch(self) -> Resource: Returns: The fetched resource. """ - response = self.mpt_client_.get(self.resource_url) - response.raise_for_status() + response = self.do_action("GET") self.resource_ = self._resource_class.from_response(response) # noqa: WPS120 return self.resource_ - def update(self, resource_data: dict[str, Any]) -> Resource: + def resource_action( + self, + method: str = "GET", + url: str | None = None, + json: dict[str, Any] | list[Any] | None = None, # noqa: WPS221 + ) -> ResourceModel: + """Perform an action on a specific resource using `HTTP_METHOD /endpoint/{resource_id}`.""" + response = self.do_action(method, url, json=json) + self.resource_ = self._resource_class.from_response(response) # noqa: WPS120 + return self.resource_ + + def do_action( + self, + method: str = "GET", + url: str | None = None, + json: dict[str, Any] | list[Any] | None = None, # noqa: WPS221 + ) -> Response: + """Perform an action on a specific resource using `HTTP_METHOD /endpoint/{resource_id}`. + + Args: + method: The HTTP method to use. + url: The action name to use. + json: The updated resource data. + + Raises: + HTTPError: If the action fails. + """ + url = f"{self.resource_url}/{url}" if url else self.resource_url + response = self.mpt_client_.request(method, url, json=json) + response.raise_for_status() + return response + + def update(self, resource_data: dict[str, Any]) -> ResourceModel: """Update a specific in the API and catches the result as a current resource. Args: @@ -63,9 +96,7 @@ def update(self, resource_data: dict[str, Any]) -> Resource: """ - response = self.mpt_client_.put(self.resource_url, json=resource_data) - response.raise_for_status() - + response = self.do_action("PUT", json=resource_data) self.resource_ = self._resource_class.from_response(response) # noqa: WPS120 return self.resource_ @@ -94,7 +125,7 @@ def delete(self) -> None: Examples: contact.delete() """ - response = self.mpt_client_.delete(self.resource_url) + response = self.do_action("DELETE") response.raise_for_status() self.resource_ = None # noqa: WPS120 diff --git a/mpt_api_client/models/resource.py b/mpt_api_client/models/resource.py index 6a7ea089..89a02ef7 100644 --- a/mpt_api_client/models/resource.py +++ b/mpt_api_client/models/resource.py @@ -10,7 +10,7 @@ class Resource(BaseResource): """Provides a resource to interact with api data using fluent interfaces.""" - _data_key: ClassVar[str] = "data" + _data_key: ClassVar[str | None] = None _safe_attributes: ClassVar[list[str]] = ["meta", "_resource_data"] def __init__(self, resource_data: ResourceData | None = None, meta: Meta | None = None) -> None: @@ -37,7 +37,11 @@ def __setattr__(self, attribute: str, attribute_value: Any) -> None: @classmethod @override def from_response(cls, response: Response) -> Self: - response_data = response.json().get(cls._data_key) + response_data = response.json() + if isinstance(response_data, dict): + response_data.pop("$meta", None) + if cls._data_key: + response_data = response_data.get(cls._data_key) if not isinstance(response_data, dict): raise TypeError("Response data must be a dict.") meta = Meta.from_response(response) diff --git a/mpt_api_client/modules/__init__.py b/mpt_api_client/modules/__init__.py deleted file mode 100644 index cdd6302c..00000000 --- a/mpt_api_client/modules/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from mpt_api_client.modules.order import Order, OrderCollectionClient - -__all__ = ["Order", "OrderCollectionClient"] # noqa: WPS410 diff --git a/mpt_api_client/modules/order.py b/mpt_api_client/modules/order.py deleted file mode 100644 index 8571acf5..00000000 --- a/mpt_api_client/modules/order.py +++ /dev/null @@ -1,16 +0,0 @@ -from mpt_api_client.http.collection import CollectionBaseClient -from mpt_api_client.models import Collection, Resource -from mpt_api_client.registry import commerce - - -class Order(Resource): - """Order resource.""" - - -@commerce("orders") -class OrderCollectionClient(CollectionBaseClient[Order]): - """Orders client.""" - - _endpoint = "/public/v1/commerce/orders" - _resource_class = Order - _collection_class = Collection[Order] diff --git a/mpt_api_client/mpt.py b/mpt_api_client/mptclient.py similarity index 82% rename from mpt_api_client/mpt.py rename to mpt_api_client/mptclient.py index bc2de75d..f54c7fd9 100644 --- a/mpt_api_client/mpt.py +++ b/mpt_api_client/mptclient.py @@ -1,9 +1,9 @@ -from mpt_api_client.http.client import MPTClient -from mpt_api_client.modules import OrderCollectionClient +from mpt_api_client.http.client import HTTPClient from mpt_api_client.registry import Registry, commerce +from mpt_api_client.resources import OrderCollectionClient -class MPT: +class MPTClient: """MPT API Client.""" def __init__( @@ -11,10 +11,9 @@ def __init__( base_url: str | None = None, api_key: str | None = None, registry: Registry | None = None, - mpt_client: MPTClient | None = None, + mpt_client: HTTPClient | None = None, ): - - self.mpt_client = mpt_client or MPTClient(base_url=base_url, api_token=api_key) + self.mpt_client = mpt_client or HTTPClient(base_url=base_url, api_token=api_key) self.registry: Registry = registry or Registry() def __getattr__(self, name): # type: ignore[no-untyped-def] @@ -31,7 +30,7 @@ def commerce(self) -> "CommerceMpt": return CommerceMpt(mpt_client=self.mpt_client, registry=commerce) -class CommerceMpt(MPT): +class CommerceMpt(MPTClient): """Commerce MPT API Client.""" @property diff --git a/mpt_api_client/registry.py b/mpt_api_client/registry.py index 1c2d2e6f..cefef24a 100644 --- a/mpt_api_client/registry.py +++ b/mpt_api_client/registry.py @@ -3,7 +3,7 @@ from mpt_api_client.http.collection import CollectionBaseClient -ItemType = type[CollectionBaseClient[Any]] +ItemType = type[CollectionBaseClient[Any, Any]] class Registry: diff --git a/mpt_api_client/resources/__init__.py b/mpt_api_client/resources/__init__.py new file mode 100644 index 00000000..89e8ed47 --- /dev/null +++ b/mpt_api_client/resources/__init__.py @@ -0,0 +1,3 @@ +from mpt_api_client.resources.order import Order, OrderCollectionClient, OrderResourceClient + +__all__ = ["Order", "OrderCollectionClient", "OrderResourceClient"] # noqa: WPS410 diff --git a/mpt_api_client/resources/order.py b/mpt_api_client/resources/order.py new file mode 100644 index 00000000..5be89616 --- /dev/null +++ b/mpt_api_client/resources/order.py @@ -0,0 +1,85 @@ +from typing import Any + +from mpt_api_client.http.collection import CollectionBaseClient +from mpt_api_client.http.resource import ResourceBaseClient +from mpt_api_client.models import Collection, Resource +from mpt_api_client.registry import commerce + + +class Order(Resource): + """Order resource.""" + + +class OrderResourceClient(ResourceBaseClient[Order]): + """Order resource client.""" + + _endpoint = "/public/v1/commerce/orders" + _resource_class = Order + + def validate(self, order: dict[str, Any] | None = None) -> Order: + """Switch order to validate state. + + Args: + order: Order data will be updated + """ + response = self.do_action("POST", "validate", json=order) + return self._resource_class.from_response(response) + + def process(self, order: dict[str, Any] | None = None) -> Order: + """Switch order to process state. + + Args: + order: Order data will be updated + """ + return self.resource_action("POST", "process", json=order) + + def query(self, order: dict[str, Any] | None = None) -> Order: + """Switch order to query state. + + Args: + order: Order data will be updated + """ + return self.resource_action("POST", "query", json=order) + + def complete(self, order: dict[str, Any] | None = None) -> Order: + """Switch order to complete state. + + Args: + order: Order data will be updated + """ + return self.resource_action("POST", "complete", json=order) + + def fail(self, order: dict[str, Any] | None = None) -> Order: + """Switch order to fail state. + + Args: + order: Order data will be updated + """ + return self.resource_action("POST", "fail", json=order) + + def notify(self, user: dict[str, Any]) -> None: + """Notify user about order status. + + Args: + user: User data + """ + self.do_action("POST", "notify", json=user) + + def template(self) -> str: + """Render order template. + + Returns: + Order template text in markdown format. + """ + response = self.do_action("GET", "template") + return response.text + + +@commerce("orders") +class OrderCollectionClient(CollectionBaseClient[Order, OrderResourceClient]): + """Orders client.""" + + _endpoint = "/public/v1/commerce/orders" + _resource_class = Order + _resource_client_class = OrderResourceClient + _collection_class = Collection[Order] diff --git a/setup.cfg b/setup.cfg index 6ca09d8b..9e8f2fd8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,4 +48,6 @@ per-file-ignores = tests/*: # Allow magic strings WPS432 + # Found too many modules members + WPS202 diff --git a/tests/conftest.py b/tests/conftest.py index e2840523..6d7c6c7e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,6 @@ import pytest -from mpt_api_client.http.client import MPTClient +from mpt_api_client.http.client import HTTPClient from mpt_api_client.models import Resource API_TOKEN = "test-token" @@ -10,7 +10,9 @@ class DummyResource(Resource): """Dummy resource for testing.""" + _data_key = "data" + @pytest.fixture def mpt_client(): - return MPTClient(base_url=API_URL, api_token=API_TOKEN) + return HTTPClient(base_url=API_URL, api_token=API_TOKEN) diff --git a/tests/http/collection/conftest.py b/tests/http/collection/conftest.py deleted file mode 100644 index 696ff550..00000000 --- a/tests/http/collection/conftest.py +++ /dev/null @@ -1,16 +0,0 @@ -import pytest - -from mpt_api_client.http.collection import CollectionBaseClient -from mpt_api_client.models import Collection -from tests.conftest import DummyResource - - -class DummyCollectionClient(CollectionBaseClient[DummyResource]): - _endpoint = "/api/v1/test" - _resource_class = DummyResource - _collection_class = Collection[DummyResource] - - -@pytest.fixture -def collection_client(mpt_client): - return DummyCollectionClient(client=mpt_client) diff --git a/tests/http/collection/test_collection_client_init.py b/tests/http/collection/test_collection_client_init.py index 37a03a4f..24260376 100644 --- a/tests/http/collection/test_collection_client_init.py +++ b/tests/http/collection/test_collection_client_init.py @@ -1,18 +1,13 @@ import pytest -from mpt_api_client.http.client import MPTClient -from mpt_api_client.models import Resource +from mpt_api_client.http.client import HTTPClient from mpt_api_client.rql.query_builder import RQLQuery -from tests.http.collection.conftest import DummyCollectionClient - - -class DummyResource(Resource): - """Dummy resource for testing.""" +from tests.http.conftest import DummyCollectionClient @pytest.fixture def mock_mpt_client(api_url, api_token): - return MPTClient(base_url=api_url, api_token=api_token) + return HTTPClient(base_url=api_url, api_token=api_token) @pytest.fixture diff --git a/tests/http/collection/test_collection_get.py b/tests/http/collection/test_collection_get.py new file mode 100644 index 00000000..db5076be --- /dev/null +++ b/tests/http/collection/test_collection_get.py @@ -0,0 +1,4 @@ +def test_get(collection_client): + resource = collection_client.get("RES-123") + assert resource.resource_id_ == "RES-123" + assert isinstance(resource, collection_client._resource_client_class) # noqa: SLF001 diff --git a/tests/http/conftest.py b/tests/http/conftest.py new file mode 100644 index 00000000..08a5ee72 --- /dev/null +++ b/tests/http/conftest.py @@ -0,0 +1,44 @@ +import pytest + +from mpt_api_client.http.client import HTTPClient +from mpt_api_client.http.collection import CollectionBaseClient +from mpt_api_client.http.resource import ResourceBaseClient +from mpt_api_client.models import Collection +from tests.conftest import DummyResource + + +class DummyResourceClient(ResourceBaseClient[DummyResource]): + _endpoint = "/api/v1/test-resource" + _resource_class = DummyResource + + +class DummyCollectionClient(CollectionBaseClient[DummyResource, DummyResourceClient]): + _endpoint = "/api/v1/test" + _resource_class = DummyResource + _resource_client_class = DummyResourceClient + _collection_class = Collection[DummyResource] + + +@pytest.fixture +def api_url(): + return "https://api.example.com" + + +@pytest.fixture +def api_token(): + return "test-token" + + +@pytest.fixture +def mpt_client(api_url, api_token): + return HTTPClient(base_url=api_url, api_token=api_token) + + +@pytest.fixture +def resource_client(mpt_client): + return DummyResourceClient(client=mpt_client, resource_id="RES-123") + + +@pytest.fixture +def collection_client(mpt_client) -> DummyCollectionClient: + return DummyCollectionClient(client=mpt_client) diff --git a/tests/http/resource/conftest.py b/tests/http/resource/conftest.py deleted file mode 100644 index 59143d94..00000000 --- a/tests/http/resource/conftest.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest - -from mpt_api_client.http.client import MPTClient -from mpt_api_client.http.resource import ResourceBaseClient -from tests.conftest import DummyResource - - -class DummyResourceClient(ResourceBaseClient[DummyResource]): - _endpoint = "/api/v1/test-resource" - _resource_class = DummyResource - - -@pytest.fixture -def api_url(): - return "https://api.example.com" - - -@pytest.fixture -def api_token(): - return "test-token" - - -@pytest.fixture -def mpt_client(api_url, api_token): - return MPTClient(base_url=api_url, api_token=api_token) - - -@pytest.fixture -def resource_client(mpt_client): - return DummyResourceClient(client=mpt_client, resource_id="RES-123") diff --git a/tests/http/test_client.py b/tests/http/test_client.py index 5bb42913..1a482bc6 100644 --- a/tests/http/test_client.py +++ b/tests/http/test_client.py @@ -2,12 +2,12 @@ import respx from httpx import ConnectTimeout, Response, codes -from mpt_api_client.http.client import MPTClient +from mpt_api_client.http.client import HTTPClient from tests.conftest import API_TOKEN, API_URL def test_mpt_client_initialization(): - client = MPTClient(base_url=API_URL, api_token=API_TOKEN) + client = HTTPClient(base_url=API_URL, api_token=API_TOKEN) assert client.base_url == API_URL assert client.headers["Authorization"] == "Bearer test-token" @@ -18,7 +18,7 @@ def test_env_initialization(monkeypatch): monkeypatch.setenv("MPT_TOKEN", API_TOKEN) monkeypatch.setenv("MPT_URL", API_URL) - client = MPTClient() + client = HTTPClient() assert client.base_url == API_URL assert client.headers["Authorization"] == f"Bearer {API_TOKEN}" @@ -26,12 +26,12 @@ def test_env_initialization(monkeypatch): def test_mpt_client_without_token(): with pytest.raises(ValueError): - MPTClient(base_url=API_URL) + HTTPClient(base_url=API_URL) def test_mpt_client_without_url(): with pytest.raises(ValueError): - MPTClient(api_token=API_TOKEN) + HTTPClient(api_token=API_TOKEN) @respx.mock diff --git a/tests/models/resource/test_resource.py b/tests/models/resource/test_resource.py index a6d08337..dc0817bb 100644 --- a/tests/models/resource/test_resource.py +++ b/tests/models/resource/test_resource.py @@ -18,7 +18,7 @@ def test_resource_empty(): def test_from_response(meta_data): record_data = {"id": 1, "name": {"given": "Albert", "family": "Einstein"}} - response = Response(200, json={"data": record_data, "$meta": meta_data}) + response = Response(200, json=record_data | {"$meta": meta_data}) expected_meta = Meta.from_response(response) resource = Resource.from_response(response) @@ -29,7 +29,9 @@ def test_from_response(meta_data): def test_attribute_getter(mocker, meta_data): resource_data = {"id": 1, "name": {"given": "Albert", "family": "Einstein"}} - response = Response(200, json={"data": resource_data, "$meta": meta_data}) + response_data = resource_data | {"$meta": meta_data} + + response = Response(200, json=response_data) resource = Resource.from_response(response) @@ -49,5 +51,6 @@ def test_attribute_setter(): def test_wrong_data_type(): + response = Response(200, json=1) with pytest.raises(TypeError, match=r"Response data must be a dict."): - Resource.from_response(Response(200, json={"data": 1})) + Resource.from_response(response) diff --git a/tests/modules/test_order.py b/tests/resources/orders/test_order_collection_client.py similarity index 50% rename from tests/modules/test_order.py rename to tests/resources/orders/test_order_collection_client.py index 5f9e212c..073b0fbc 100644 --- a/tests/modules/test_order.py +++ b/tests/resources/orders/test_order_collection_client.py @@ -1,12 +1,6 @@ - -from mpt_api_client.modules.order import Order, OrderCollectionClient +from mpt_api_client.resources.order import OrderCollectionClient def test_order_collection_client(mpt_client): order_cc = OrderCollectionClient(client=mpt_client) assert order_cc.query_rql is None - - -def test_order(): - order = Order() - assert order is not None diff --git a/tests/resources/orders/test_order_resource_client.py b/tests/resources/orders/test_order_resource_client.py new file mode 100644 index 00000000..0639d612 --- /dev/null +++ b/tests/resources/orders/test_order_resource_client.py @@ -0,0 +1,97 @@ +from unittest.mock import Mock, patch + +import httpx +import pytest + +from mpt_api_client.resources.order import Order, OrderResourceClient + + +@pytest.fixture +def order_response(): + return httpx.Response( + status_code=200, + headers={"content-type": "application/json"}, + content='{"id": "order123", "status": "completed", "$meta": {"total": 1}}', + request=httpx.Request("POST", "https://api.example.com/orders"), + ) + + +@pytest.fixture +def order_client(): + with patch.object(OrderResourceClient, "do_action"): + client = OrderResourceClient(Mock(), "order123") + yield client + + +def test_validate(order_client, order_response): + order_client.do_action.return_value = order_response + order_data = {"id": "order123", "status": "draft"} + + order = order_client.validate(order_data) + + order_client.do_action.assert_called_once_with("POST", "validate", json=order_data) + assert isinstance(order, Order) + + +def test_process(order_client, order_response): + order_client.do_action.return_value = order_response + order_data = {"id": "order123", "status": "validated"} + + order = order_client.process(order_data) + + order_client.do_action.assert_called_once_with("POST", "process", json=order_data) + assert isinstance(order, Order) + + +def test_query(order_client, order_response): + order_client.do_action.return_value = order_response + order_data = {"id": "order123", "status": "processing"} + + order = order_client.query(order_data) + + order_client.do_action.assert_called_once_with("POST", "query", json=order_data) + assert isinstance(order, Order) + + +def test_complete(order_client, order_response): + order_client.do_action.return_value = order_response + order_data = {"id": "order123", "status": "processing"} + + order = order_client.complete(order_data) + + order_client.do_action.assert_called_once_with("POST", "complete", json=order_data) + assert isinstance(order, Order) + + +def test_fail(order_client, order_response): + order_client.do_action.return_value = order_response + order_data = {"id": "order123", "status": "processing"} + + order = order_client.fail(order_data) + + order_client.do_action.assert_called_once_with("POST", "fail", json=order_data) + assert isinstance(order, Order) + + +def test_notify(order_client, order_response): + order_client.do_action.return_value = order_response + user_data = {"email": "user@example.com", "name": "John Doe"} + + order_client.notify(user_data) + + order_client.do_action.assert_called_once_with("POST", "notify", json=user_data) + + +def test_template(order_client): + template_response = httpx.Response( + status_code=200, + headers={"content-type": "text/markdown"}, + content="# Order Template\n\nThis is a markdown template.", + request=httpx.Request("GET", "https://api.example.com/orders/template"), + ) + order_client.do_action.return_value = template_response + + markdown_template = order_client.template() + + order_client.do_action.assert_called_once_with("GET", "template") + assert markdown_template == "# Order Template\n\nThis is a markdown template." diff --git a/tests/test_mpt.py b/tests/test_mpt.py index 7a685434..3afd59f0 100644 --- a/tests/test_mpt.py +++ b/tests/test_mpt.py @@ -1,12 +1,12 @@ from unittest.mock import Mock -from mpt_api_client.modules import OrderCollectionClient -from mpt_api_client.mpt import MPT +from mpt_api_client.mptclient import MPTClient +from mpt_api_client.resources import OrderCollectionClient def test_mapped_module() -> None: mock_registry = Mock() - mpt = MPT(base_url="https://test.example.com", api_key="test-key", registry=mock_registry) + mpt = MPTClient(base_url="https://test.example.com", api_key="test-key", registry=mock_registry) mpt.orders # noqa: B018 @@ -15,7 +15,7 @@ def test_mapped_module() -> None: def test_not_mapped_module() -> None: mock_registry = Mock() - mpt = MPT(base_url="https://test.example.com", api_key="test-key", registry=mock_registry) + mpt = MPTClient(base_url="https://test.example.com", api_key="test-key", registry=mock_registry) mpt.non_existing_module # noqa: B018 @@ -23,7 +23,7 @@ def test_not_mapped_module() -> None: def test_subclient_orders_module(): - mpt = MPT(base_url="https://test.example.com", api_key="test-key") + mpt = MPTClient(base_url="https://test.example.com", api_key="test-key") orders_client = mpt.commerce.orders diff --git a/tests/test_registry.py b/tests/test_registry.py index 165c2b48..f2db59ec 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -1,6 +1,7 @@ import pytest from mpt_api_client.http.collection import CollectionBaseClient +from mpt_api_client.http.resource import ResourceBaseClient from mpt_api_client.models import Resource from mpt_api_client.registry import Registry @@ -63,7 +64,7 @@ def test_registry_as_decorator(): registry = Registry() @registry("test_call") - class TestCallClient(CollectionBaseClient[DummyResource]): # noqa: WPS431 + class TestCallClient(CollectionBaseClient[DummyResource, ResourceBaseClient[DummyResource]]): # noqa: WPS431 _endpoint = "/api/v1/test-call" _resource_class = DummyResource