Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions mpt_api_client/http/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions mpt_api_client/modules/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from mpt_api_client.modules.order import Order, OrderCollectionClient

__all__ = ["Order", "OrderCollectionClient"] # noqa: WPS410
16 changes: 16 additions & 0 deletions mpt_api_client/modules/order.py
Original file line number Diff line number Diff line change
@@ -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]
54 changes: 54 additions & 0 deletions mpt_api_client/mpt.py
Original file line number Diff line number Diff line change
@@ -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]
74 changes: 74 additions & 0 deletions mpt_api_client/registry.py
Original file line number Diff line number Diff line change
@@ -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()
File renamed without changes.
2 changes: 1 addition & 1 deletion tests/http/collection/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]):
Expand Down
2 changes: 1 addition & 1 deletion tests/http/resource/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]):
Expand Down
2 changes: 1 addition & 1 deletion tests/http/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
12 changes: 12 additions & 0 deletions tests/modules/test_order.py
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions tests/test_mpt.py
Original file line number Diff line number Diff line change
@@ -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
72 changes: 72 additions & 0 deletions tests/test_registry.py
Original file line number Diff line number Diff line change
@@ -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