Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New handler for list results #285

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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 mollie/api/objects/balance.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from typing import Any
from typing import Any, Union

from .balance_report import BalanceReport
from .base import ObjectBase
from .list import ObjectList
from .list import ObjectList, ResultListIterator


class Balance(ObjectBase):
Expand Down Expand Up @@ -65,7 +65,7 @@ def get_report(self, **params: Any) -> BalanceReport:

return BalanceReports(self.client, self).get_report(params=params)

def get_transactions(self, **params: Any) -> ObjectList:
def get_transactions(self, **params: Any) -> Union[ObjectList, ResultListIterator]:
from ..resources import BalanceTransactions

return BalanceTransactions(self.client, self).list(params=params)
85 changes: 85 additions & 0 deletions mollie/api/objects/list.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from typing import TYPE_CHECKING, Any, Dict, List, Tuple

from ..utils import get_class_from_dotted_path
from .base import ObjectBase

if TYPE_CHECKING:
from ..resources.base import ResourceBase


class UnknownObject(ObjectBase):
"""Mock object for empty lists."""
Expand Down Expand Up @@ -97,3 +103,82 @@ def get_previous(self):
resource = self.object_type.get_resource_class(self.client)
resp = resource.perform_api_call(resource.REST_READ, url)
return ObjectList(resp, self.object_type, self.client)


class ResultListIterator:
"""
An iterator for result lists from the API.

You can iterate through the results. If the initial result indocates pagination,
a new result page is automatically fetched from the API when the current result page
is exhausted.

Note: This iterator should preferably replace the ObjectList as the default
return value for the ResourceBase.list() method in the future.
"""

_last: int
resource: "ResourceBase"
next_uri: str
list_data: List[Dict[str, Any]]

def __init__(self, resource: "ResourceBase", data: Dict[str, Any]) -> None:
self.resource = resource
self.list_data, self.next_uri = self._parse_data(data)
self._last = -1

def __iter__(self):
"""Return the iterator."""
return self

def __next__(self) -> ObjectBase:
"""
Return the next result.

If the list data is exhausted, but a link to further paginated results
is available, we fetch those results and return the first result of that.
"""
current = self._last + 1
try:
object_data = self.list_data[current]

except IndexError:
if self.next_uri:
# Fetch new results and return the first entry
self._reinit_from_uri(self.next_uri)
return next(self)

else:
# No more results to return, nor to fetch: this iterator is really exhausted
raise StopIteration

else:
# Return the next result
self._last = current
return self.resource.get_resource_object(object_data)

def _parse_data(self, data: Dict[str, Any]) -> Tuple[List[Dict[str, Any]], str]:
"""
Extract useful data from the payload.

We are interested in the following parts:
- the actual list data, unwrapped
- links to next results, when results are paginated
"""
try:
next_uri = data["_links"]["next"]["href"]
except TypeError:
next_uri = ""

result_class = get_class_from_dotted_path(self.resource.RESULT_CLASS_PATH)
resource_name = result_class.get_object_name()
list_data = data["_embedded"][resource_name]

return list_data, next_uri

def _reinit_from_uri(self, uri: str) -> None:
"""Fetch additional results from the API, and feed the iterator with the data."""

result = self.resource.perform_api_call(self.resource.REST_READ, uri)
self.list_data, self.next_uri = self._parse_data(result)
self._last = -1
30 changes: 18 additions & 12 deletions mollie/api/resources/base.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import logging
import uuid
from typing import TYPE_CHECKING, Any, Dict, Optional
from typing import TYPE_CHECKING, Any, Dict, Optional, Union

from ..error import IdentifierError, ResponseError, ResponseHandlingError
from ..objects.list import ObjectList
from ..objects.list import ObjectList, ResultListIterator
from ..utils import get_class_from_dotted_path

if TYPE_CHECKING:
from ..client import Client
Expand All @@ -20,17 +21,17 @@ class ResourceBase:

RESOURCE_ID_PREFIX: str = ""

# Dotted path to the result class
RESULT_CLASS_PATH: str = ""

def __init__(self, client: "Client") -> None:
self.client = client

def get_resource_object(self, result: dict) -> Any:
"""
Return an instantiated result class for this resource. Should be overriden by a subclass.
def get_resource_object(self, result: Dict[str, Any]) -> Any:
"""Return an instantiated result class for this resource."""
result_class = get_class_from_dotted_path(self.RESULT_CLASS_PATH)

:param result: The API response that the object should hold.
:type result: dict
"""
raise NotImplementedError() # pragma: no cover
return result_class(result, self.client)

def get_resource_path(self) -> str:
"""Return the base URL path in the API for this resource."""
Expand All @@ -44,7 +45,6 @@ def perform_api_call(
params: Optional[Dict[str, Any]] = None,
idempotency_key: str = "",
) -> Dict[str, Any]:

resp = self.client.perform_http_call(http_method, path, data, params, idempotency_key)
if "application/hal+json" in resp.headers.get("Content-Type", ""):
# set the content type according to the media type definition
Expand Down Expand Up @@ -113,10 +113,16 @@ def from_url(self, url: str, params: Optional[Dict[str, Any]] = None) -> Any:


class ResourceListMixin(ResourceBase):
def list(self, **params: Any) -> ObjectList:
def list(self, **params: Any) -> Union[ObjectList, ResultListIterator]:
return_iterator = params.pop("return_iterator", False)
path = self.get_resource_path()
result = self.perform_api_call(self.REST_LIST, path, params=params)
return ObjectList(result, self.get_resource_object({}).__class__, self.client)

if return_iterator:
resource = self
return ResultListIterator(resource, result)
else:
return ObjectList(result, self.get_resource_object({}).__class__, self.client)


class ResourceUpdateMixin(ResourceBase):
Expand Down
4 changes: 1 addition & 3 deletions mollie/api/resources/captures.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@

class CapturesBase(ResourceBase):
RESOURCE_ID_PREFIX: str = "cpt_"

def get_resource_object(self, result: dict) -> Capture:
return Capture(result, self.client)
RESULT_CLASS_PATH: str = "mollie.api.objects.capture.Capture"


class PaymentCaptures(CapturesBase, ResourceGetMixin, ResourceListMixin):
Expand Down
10 changes: 4 additions & 6 deletions mollie/api/resources/chargebacks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Union

from ..objects.chargeback import Chargeback
from ..objects.list import ObjectList
from ..objects.list import ObjectList, ResultListIterator
from .base import ResourceBase, ResourceGetMixin, ResourceListMixin

if TYPE_CHECKING:
Expand All @@ -20,9 +20,7 @@

class ChargebacksBase(ResourceBase):
RESOURCE_ID_PREFIX: str = "chb_"

def get_resource_object(self, result: dict) -> Chargeback:
return Chargeback(result, self.client)
RESULT_CLASS_PATH: str = "mollie.api.objects.chargeback.Chargeback"


class Chargebacks(ChargebacksBase, ResourceListMixin):
Expand Down Expand Up @@ -74,7 +72,7 @@ def __init__(self, client: "Client", profile: "Profile") -> None:
self._profile = profile
super().__init__(client)

def list(self, **params: Any) -> ObjectList:
def list(self, **params: Any) -> Union[ObjectList, ResultListIterator]:
# Set the profileId in the query params
params.update({"profileId": self._profile.id})
return Chargebacks(self.client).list(**params)
4 changes: 1 addition & 3 deletions mollie/api/resources/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ class Clients(ResourceListMixin, ResourceGetMixin):
"""

RESOURCE_ID_PREFIX: str = "org_"

def get_resource_object(self, result: dict) -> Client:
return Client(result, self.client)
RESULT_CLASS_PATH: str = "mollie.api.objects.client.Client"

def get(self, resource_id: str, **params: Any) -> Client:
"""Retrieve a single client, linked to your partner account, by its ID."""
Expand Down
4 changes: 1 addition & 3 deletions mollie/api/resources/customers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ class Customers(ResourceCreateMixin, ResourceDeleteMixin, ResourceGetMixin, Reso
"""Resource handler for the `/customers` endpoint."""

RESOURCE_ID_PREFIX: str = "cst_"

def get_resource_object(self, result: dict) -> Customer:
return Customer(result, self.client)
RESULT_CLASS_PATH: str = "mollie.api.objects.customer.Customer"

def get(self, resource_id: str, **params: Any) -> Customer:
self.validate_resource_id(resource_id, "customer ID")
Expand Down
4 changes: 1 addition & 3 deletions mollie/api/resources/invoices.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ class Invoices(ResourceGetMixin, ResourceListMixin):
"""Resource handler for the `/invoices` endpoint."""

RESOURCE_ID_PREFIX: str = "inv_"

def get_resource_object(self, result: dict) -> Invoice:
return Invoice(result, self.client)
RESULT_CLASS_PATH: str = "mollie.api.objects.invoice.Invoice"

def get(self, resource_id: str, **params: Any) -> Invoice:
self.validate_resource_id(resource_id, "invoice ID")
Expand Down
4 changes: 1 addition & 3 deletions mollie/api/resources/mandates.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class CustomerMandates(ResourceCreateMixin, ResourceDeleteMixin, ResourceGetMixi
"""Resource handler for the `/customers/:customer_id:/mandates` endpoint."""

RESOURCE_ID_PREFIX = "mdt_"
RESULT_CLASS_PATH: str = "mollie.api.objects.mandate.Mandate"

_customer: Customer

Expand All @@ -27,9 +28,6 @@ def __init__(self, client: "Client", customer: Customer) -> None:
def get_resource_path(self) -> str:
return f"customers/{self._customer.id}/mandates"

def get_resource_object(self, result: dict) -> Mandate:
return Mandate(result, self.client)

def get(self, resource_id: str, **params: Any) -> Mandate:
self.validate_resource_id(resource_id, "mandate ID")
return super().get(resource_id, **params)
Expand Down
10 changes: 5 additions & 5 deletions mollie/api/resources/methods.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from typing import TYPE_CHECKING, Any, Dict, List, Optional
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union

from ..error import IdentifierError
from ..objects.issuer import Issuer
from ..objects.list import ObjectList
from ..objects.list import ObjectList, ResultListIterator
from ..objects.method import Method
from .base import ResourceBase, ResourceGetMixin, ResourceListMixin

Expand All @@ -18,8 +18,8 @@


class MethodsBase(ResourceBase):
def get_resource_object(self, result: dict) -> Method:
return Method(result, self.client)

RESULT_CLASS_PATH: str = "mollie.api.objects.method.Method"


class Methods(MethodsBase, ResourceGetMixin, ResourceListMixin):
Expand Down Expand Up @@ -85,7 +85,7 @@ def disable(self, method_id: str, **params: Any) -> Method:
result = self.perform_api_call(self.REST_DELETE, path, params=params)
return self.get_resource_object(result)

def list(self, **params: Any) -> ObjectList:
def list(self, **params: Any) -> Union[ObjectList, ResultListIterator]:
"""List the payment methods for the profile."""
params.update({"profileId": self._profile.id})
# Divert the API call to the general Methods resource
Expand Down
3 changes: 1 addition & 2 deletions mollie/api/resources/onboarding.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
class Onboarding(ResourceGetMixin):
"""Resource handler for the `/onboarding` endpoint."""

def get_resource_object(self, result: dict) -> OnboardingObject:
return OnboardingObject(result, self.client)
RESULT_CLASS_PATH: str = "mollie.api.objects.onboarding.Onboarding"

def get(self, resource_id: str, **params: Any) -> OnboardingObject:
if resource_id != "me":
Expand Down
4 changes: 1 addition & 3 deletions mollie/api/resources/order_lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class OrderLines(ResourceBase):
"""

RESOURCE_ID_PREFIX: str = "odl_"
RESULT_CLASS_PATH: str = "mollie.api.objects.order_line.OrderLine"

_order: "Order"

Expand All @@ -35,9 +36,6 @@ def __init__(self, client: "Client", order: "Order") -> None:
def get_resource_path(self) -> str:
return f"orders/{self._order.id}/lines"

def get_resource_object(self, result: dict) -> OrderLine:
return OrderLine(result, self.client)

def delete_lines(self, data: Optional[Dict[str, Any]] = None, **params: Any) -> dict:
"""
Cancel multiple orderlines.
Expand Down
4 changes: 1 addition & 3 deletions mollie/api/resources/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ class Orders(ResourceCreateMixin, ResourceDeleteMixin, ResourceGetMixin, Resourc
"""Resource handler for the `/orders` endpoint."""

RESOURCE_ID_PREFIX: str = "ord_"

def get_resource_object(self, result: dict) -> Order:
return Order(result, self.client)
RESULT_CLASS_PATH: str = "mollie.api.objects.order.Order"

def get(self, resource_id: str, **params: Any) -> Order:
self.validate_resource_id(resource_id, "order ID")
Expand Down
4 changes: 1 addition & 3 deletions mollie/api/resources/organizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ class Organizations(ResourceGetMixin):
"""Resource handler for the `/organizations` endpoint."""

RESOURCE_ID_PREFIX: str = "org_"

def get_resource_object(self, result: dict) -> Organization:
return Organization(result, self.client)
RESULT_CLASS_PATH: str = "mollie.api.objects.organization.Organization"

def get(self, resource_id: str, **params: Any) -> Organization:
if resource_id != "me":
Expand Down
4 changes: 1 addition & 3 deletions mollie/api/resources/payment_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ class PaymentLinks(ResourceCreateMixin, ResourceGetMixin, ResourceListMixin):
"""Resource handler for the `/payment_links` endpoint."""

RESOURCE_ID_PREFIX: str = "pl_"
RESULT_CLASS_PATH: str = "mollie.api.objects.payment_link.PaymentLink"

def get_resource_path(self) -> str:
return "payment-links"

def get_resource_object(self, result: dict) -> PaymentLink:
return PaymentLink(result, self.client)

def get(self, resource_id: str, **params: Any) -> PaymentLink:
self.validate_resource_id(resource_id, "payment link ID")
return super().get(resource_id, **params)
12 changes: 4 additions & 8 deletions mollie/api/resources/payments.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import TYPE_CHECKING, Any, Dict, Optional
from typing import TYPE_CHECKING, Any, Dict, Optional, Union

from ..objects.customer import Customer
from ..objects.list import ObjectList
from ..objects.list import ObjectList, ResultListIterator
from ..objects.order import Order
from ..objects.payment import Payment
from ..objects.profile import Profile
Expand Down Expand Up @@ -31,11 +31,7 @@

class PaymentsBase(ResourceBase):
RESOURCE_ID_PREFIX: str = "tr_"

def get_resource_object(self, result: dict) -> Payment:
from ..objects.payment import Payment

return Payment(result, self.client)
RESULT_CLASS_PATH: str = "mollie.api.objects.payment.Payment"


class Payments(
Expand Down Expand Up @@ -147,7 +143,7 @@ def __init__(self, client: "Client", profile: Profile) -> None:
self._profile = profile
super().__init__(client)

def list(self, **params: Any) -> ObjectList:
def list(self, **params: Any) -> Union[ObjectList, ResultListIterator]:
# Set the profileId in the query params
params.update({"profileId": self._profile.id})
return Payments(self.client).list(**params)
Loading