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
4 changes: 4 additions & 0 deletions mpt_api_client/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from mpt_api_client.mptclient import MPTClient
from mpt_api_client.rql import RQLQuery

__all__ = ["MPTClient", "RQLQuery"] # noqa: WPS410
2 changes: 1 addition & 1 deletion mpt_api_client/http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import httpx


class MPTClient(httpx.Client):
class HTTPClient(httpx.Client):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this name better? Before it was a bit more specific

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mpt_api_client/http/client.py: HTTPClient is the class that manages the http connection (Auth and headers).
Now with this PR we have a new MPTClient to be used in mpt_api_client/mptclient.py which gives you access to a higher level of abstraction layer to access the MPT API.

"""A client for interacting with SoftwareOne Marketplace Platform API."""

def __init__(
Expand Down
43 changes: 29 additions & 14 deletions mpt_api_client/http/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand Down
55 changes: 43 additions & 12 deletions mpt_api_client/http/resource.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -35,21 +37,52 @@ 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.

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:
Expand All @@ -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_

Expand Down Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions mpt_api_client/models/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down
3 changes: 0 additions & 3 deletions mpt_api_client/modules/__init__.py

This file was deleted.

16 changes: 0 additions & 16 deletions mpt_api_client/modules/order.py

This file was deleted.

13 changes: 6 additions & 7 deletions mpt_api_client/mpt.py → mpt_api_client/mptclient.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
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__(
self,
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]
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion mpt_api_client/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from mpt_api_client.http.collection import CollectionBaseClient

ItemType = type[CollectionBaseClient[Any]]
ItemType = type[CollectionBaseClient[Any, Any]]


class Registry:
Expand Down
3 changes: 3 additions & 0 deletions mpt_api_client/resources/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from mpt_api_client.resources.order import Order, OrderCollectionClient, OrderResourceClient

__all__ = ["Order", "OrderCollectionClient", "OrderResourceClient"] # noqa: WPS410
Loading