From 6480edd6f5c91a3091ed52795d6bfde91fb935a3 Mon Sep 17 00:00:00 2001 From: Albert Sola Date: Tue, 19 Aug 2025 11:21:19 +0100 Subject: [PATCH] MPT-12329 Order collection client --- mpt_api_client/http/collection.py | 6 +-- mpt_api_client/modules/__init__.py | 3 ++ mpt_api_client/modules/order.py | 16 +++++++ mpt_api_client/mpt.py | 54 ++++++++++++++++++++++ mpt_api_client/registry.py | 74 ++++++++++++++++++++++++++++++ tests/{http => }/conftest.py | 0 tests/http/collection/conftest.py | 2 +- tests/http/resource/conftest.py | 2 +- tests/http/test_client.py | 2 +- tests/modules/test_order.py | 12 +++++ tests/test_mpt.py | 31 +++++++++++++ tests/test_registry.py | 72 +++++++++++++++++++++++++++++ 12 files changed, 268 insertions(+), 6 deletions(-) create mode 100644 mpt_api_client/modules/__init__.py create mode 100644 mpt_api_client/modules/order.py create mode 100644 mpt_api_client/mpt.py create mode 100644 mpt_api_client/registry.py rename tests/{http => }/conftest.py (100%) create mode 100644 tests/modules/test_order.py create mode 100644 tests/test_mpt.py create mode 100644 tests/test_registry.py diff --git a/mpt_api_client/http/collection.py b/mpt_api_client/http/collection.py index 6750df8b..95512cd6 100644 --- a/mpt_api_client/http/collection.py +++ b/mpt_api_client/http/collection.py @@ -23,8 +23,8 @@ class CollectionBaseClient[ResourceType: Resource](ABC): # noqa: WPS214 """ _endpoint: str - _resource_class: type[Resource] - _collection_class: type[Collection[Resource]] + _resource_class: type[ResourceType] + _collection_class: type[Collection[ResourceType]] def __init__( self, @@ -181,7 +181,7 @@ def create(self, resource_data: dict[str, Any]) -> ResourceType: response = self.mpt_client.post(self._endpoint, json=resource_data) response.raise_for_status() - return self._resource_class.from_response(response) # type: ignore[return-value] + return self._resource_class.from_response(response) def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> httpx.Response: """Fetch one page of resources. diff --git a/mpt_api_client/modules/__init__.py b/mpt_api_client/modules/__init__.py new file mode 100644 index 00000000..cdd6302c --- /dev/null +++ b/mpt_api_client/modules/__init__.py @@ -0,0 +1,3 @@ +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 new file mode 100644 index 00000000..8571acf5 --- /dev/null +++ b/mpt_api_client/modules/order.py @@ -0,0 +1,16 @@ +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/mpt.py new file mode 100644 index 00000000..bc2de75d --- /dev/null +++ b/mpt_api_client/mpt.py @@ -0,0 +1,54 @@ +from mpt_api_client.http.client import MPTClient +from mpt_api_client.modules import OrderCollectionClient +from mpt_api_client.registry import Registry, commerce + + +class MPT: + """MPT API Client.""" + + def __init__( + self, + base_url: str | None = None, + api_key: str | None = None, + registry: Registry | None = None, + mpt_client: MPTClient | None = None, + ): + + self.mpt_client = mpt_client or MPTClient(base_url=base_url, api_token=api_key) + self.registry: Registry = registry or Registry() + + def __getattr__(self, name): # type: ignore[no-untyped-def] + return self.registry.get(name)(client=self.mpt_client) + + @property + def commerce(self) -> "CommerceMpt": + """Commerce MPT API Client. + + The Commerce API provides a comprehensive set of endpoints + for managing agreements, requests, subscriptions, and orders + within a vendor-client-ops ecosystem. + """ + return CommerceMpt(mpt_client=self.mpt_client, registry=commerce) + + +class CommerceMpt(MPT): + """Commerce MPT API Client.""" + + @property + def orders(self) -> OrderCollectionClient: + """Orders MPT API collection. + + The Orders API provides a comprehensive set of endpoints + for creating, updating, and retrieving orders. + + + + Returns: Order collection + + Examples: + active=RQLQuery("status=active") + for order in mpt.orders.filter(active).iterate(): + [...] + + """ + return self.registry.get("orders")(client=self.mpt_client) # type: ignore[return-value] diff --git a/mpt_api_client/registry.py b/mpt_api_client/registry.py new file mode 100644 index 00000000..1c2d2e6f --- /dev/null +++ b/mpt_api_client/registry.py @@ -0,0 +1,74 @@ +from collections.abc import Callable +from typing import Any + +from mpt_api_client.http.collection import CollectionBaseClient + +ItemType = type[CollectionBaseClient[Any]] + + +class Registry: + """Registry for MPT collection clients.""" + + def __init__(self) -> None: + self.items: dict[str, ItemType] = {} # noqa: WPS110 + + def __call__(self, keyname: str) -> Callable[[ItemType], ItemType]: + """Decorator to register a CollectionBaseClient class. + + Args: + keyname: The key to register the class under + + Returns: + The decorator function + + Examples: + registry = Registry() + @registry("orders") + class OrderCollectionClient(CollectionBaseClient): + _endpoint = "/api/v1/orders" + _resource_class = Order + + registry.get("orders") == OrderCollectionClient + """ + + def decorator(cls: ItemType) -> ItemType: + self.register(keyname, cls) + return cls + + return decorator + + def register(self, keyname: str, item: ItemType) -> None: # noqa: WPS110 + """Register a collection client class with a keyname. + + Args: + keyname: The key to register the client under + item: The collection client class to register + """ + self.items[keyname] = item + + def get(self, keyname: str) -> ItemType: + """Get a registered collection client class by keyname. + + Args: + keyname: The key to look up + + Returns: + The registered collection client class + + Raises: + KeyError: If keyname is not registered + """ + if keyname not in self.items: + raise KeyError(f"No collection client registered with keyname: {keyname}") + return self.items[keyname] + + def list_keys(self) -> list[str]: + """Get all registered keynames. + + Returns: + List of all registered keynames + """ + return list(self.items.keys()) + + +commerce = Registry() diff --git a/tests/http/conftest.py b/tests/conftest.py similarity index 100% rename from tests/http/conftest.py rename to tests/conftest.py diff --git a/tests/http/collection/conftest.py b/tests/http/collection/conftest.py index dc211554..696ff550 100644 --- a/tests/http/collection/conftest.py +++ b/tests/http/collection/conftest.py @@ -2,7 +2,7 @@ from mpt_api_client.http.collection import CollectionBaseClient from mpt_api_client.models import Collection -from tests.http.conftest import DummyResource +from tests.conftest import DummyResource class DummyCollectionClient(CollectionBaseClient[DummyResource]): diff --git a/tests/http/resource/conftest.py b/tests/http/resource/conftest.py index c969cb17..59143d94 100644 --- a/tests/http/resource/conftest.py +++ b/tests/http/resource/conftest.py @@ -2,7 +2,7 @@ from mpt_api_client.http.client import MPTClient from mpt_api_client.http.resource import ResourceBaseClient -from tests.http.conftest import DummyResource +from tests.conftest import DummyResource class DummyResourceClient(ResourceBaseClient[DummyResource]): diff --git a/tests/http/test_client.py b/tests/http/test_client.py index b372cf64..5bb42913 100644 --- a/tests/http/test_client.py +++ b/tests/http/test_client.py @@ -3,7 +3,7 @@ from httpx import ConnectTimeout, Response, codes from mpt_api_client.http.client import MPTClient -from tests.http.conftest import API_TOKEN, API_URL +from tests.conftest import API_TOKEN, API_URL def test_mpt_client_initialization(): diff --git a/tests/modules/test_order.py b/tests/modules/test_order.py new file mode 100644 index 00000000..5f9e212c --- /dev/null +++ b/tests/modules/test_order.py @@ -0,0 +1,12 @@ + +from mpt_api_client.modules.order import Order, 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/test_mpt.py b/tests/test_mpt.py new file mode 100644 index 00000000..7a685434 --- /dev/null +++ b/tests/test_mpt.py @@ -0,0 +1,31 @@ +from unittest.mock import Mock + +from mpt_api_client.modules import OrderCollectionClient +from mpt_api_client.mpt import MPT + + +def test_mapped_module() -> None: + mock_registry = Mock() + mpt = MPT(base_url="https://test.example.com", api_key="test-key", registry=mock_registry) + + mpt.orders # noqa: B018 + + mock_registry.get.assert_called_once_with("orders") + + +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.non_existing_module # noqa: B018 + + mock_registry.get.assert_called_once_with("non_existing_module") + + +def test_subclient_orders_module(): + mpt = MPT(base_url="https://test.example.com", api_key="test-key") + + orders_client = mpt.commerce.orders + + assert isinstance(orders_client, OrderCollectionClient) + assert orders_client.mpt_client == mpt.mpt_client diff --git a/tests/test_registry.py b/tests/test_registry.py new file mode 100644 index 00000000..165c2b48 --- /dev/null +++ b/tests/test_registry.py @@ -0,0 +1,72 @@ +import pytest + +from mpt_api_client.http.collection import CollectionBaseClient +from mpt_api_client.models import Resource +from mpt_api_client.registry import Registry + + +class DummyResource(Resource): + """Dummy resource for testing.""" + + +class DummyCollectionClient(CollectionBaseClient): + _endpoint = "/api/v1/dummy" + _resource_class = DummyResource + + +def test_register_collection_client_successfully(): + registry = Registry() + keyname = "test_collection" + + registry.register(keyname, DummyCollectionClient) + + assert keyname in registry.items + assert registry.items[keyname] == DummyCollectionClient + assert registry.get(keyname) == DummyCollectionClient + + +def test_get_registered_client_successfully(): + registry = Registry() + keyname = "orders" + + registry.register(keyname, DummyCollectionClient) + + retrieved_client = registry.get(keyname) + + assert retrieved_client == DummyCollectionClient + + +def test_get_raise_exception(): + registry = Registry() + unregistered_keyname = "nonexistent_client" + + with pytest.raises( + KeyError, match="No collection client registered with keyname: nonexistent_client" + ): + registry.get(unregistered_keyname) + + +def test_list_keys(): + registry = Registry() + expected_keys = ["orders", "customers", "products"] + + for keyname in expected_keys: + registry.register(keyname, DummyCollectionClient) + + registry_keys = registry.list_keys() + + assert sorted(registry_keys) == sorted(expected_keys) + assert len(registry_keys) == 3 + + +def test_registry_as_decorator(): + registry = Registry() + + @registry("test_call") + class TestCallClient(CollectionBaseClient[DummyResource]): # noqa: WPS431 + _endpoint = "/api/v1/test-call" + _resource_class = DummyResource + + registered_client = registry.get("test_call") + + assert registered_client == TestCallClient