From 291171e0f1cede6a52473299af87181362fc1ebf Mon Sep 17 00:00:00 2001 From: Tom Hendrikx Date: Fri, 25 Nov 2022 01:47:41 +0100 Subject: [PATCH 1/3] Implement a new iterator-only handler for result lists. The current result list wrapping class `ObjectList` is intended for list handling, but supports a mix of list- and dictionary-related protocols: iteration, slices, getitem (by key, not by position), etc. One feature that you'd expect from a list that isn't supported, is getting quickly from the start to the end of the list. You need to use a combination of `.has_next()` and `.get_next()` to work through all (paginated) list items. The old `Objectlist` supports key-based access to its members, but since the data is paged, you can never be sure that the data is actually available in this page, or in the next/previous one). Getting list length is also supported, but it only returns the length of the current page, not a grand total. Both features are treacherous. To improve on this, a new simple iterator-based approach was used, which transparently fetches additional paginated data from the API. Things like `getitem` and `len()` are no longer available, if you need these, they're easy to implement in custom code: ```python payments = client.payments.list() length = 0 for item in payments: length += 1 # Equivalent of payments["tr_12345"] if item.id == "tr_12345": break # Equivalent of len(payments) print(length) # An even better replacement for getting a specific payment would be: client.payments.get("tr_12345") ``` TODO: - More tests - Reversed() iterator, hoe does it work? - Add logic to return an ResultListIterator where now an ObjectList is hardcoded - Empty list results? --- mollie/api/objects/balance.py | 6 +- mollie/api/objects/list.py | 92 +++++++++++++ mollie/api/resources/base.py | 15 ++- mollie/api/resources/chargebacks.py | 6 +- mollie/api/resources/methods.py | 6 +- mollie/api/resources/payments.py | 6 +- mollie/api/resources/refunds.py | 6 +- tests/responses/payments_list_with_next.json | 130 +++++++++++++++++++ tests/test_list.py | 26 ++++ 9 files changed, 273 insertions(+), 20 deletions(-) create mode 100644 tests/responses/payments_list_with_next.json diff --git a/mollie/api/objects/balance.py b/mollie/api/objects/balance.py index 501908d8..e5a25d99 100644 --- a/mollie/api/objects/balance.py +++ b/mollie/api/objects/balance.py @@ -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): @@ -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) diff --git a/mollie/api/objects/list.py b/mollie/api/objects/list.py index a1919553..066201ec 100644 --- a/mollie/api/objects/list.py +++ b/mollie/api/objects/list.py @@ -1,5 +1,10 @@ +from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Type + from .base import ObjectBase +if TYPE_CHECKING: + from ..resources.base import ResourceBase + class UnknownObject(ObjectBase): """Mock object for empty lists.""" @@ -97,3 +102,90 @@ 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" + result_class: Type[ObjectBase] + 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 = "" + + if not hasattr(self, "result_class"): + # A bit klunky: need to instantiate the class with fake data, only to get its type. + # + # This could be improved if we define the result class (or its dotted path) as a + # class constant on the resource. Additionaly, that could help when making + # get_resource_object() a generic method on ResourceBase. + self.result_class = type(self.resource.get_resource_object({})) + + resource_name = self.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 diff --git a/mollie/api/resources/base.py b/mollie/api/resources/base.py index b4b2de0d..37f37eb2 100644 --- a/mollie/api/resources/base.py +++ b/mollie/api/resources/base.py @@ -1,9 +1,9 @@ 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 if TYPE_CHECKING: from ..client import Client @@ -44,7 +44,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 @@ -113,10 +112,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): diff --git a/mollie/api/resources/chargebacks.py b/mollie/api/resources/chargebacks.py index 7e8cb166..6895bf6e 100644 --- a/mollie/api/resources/chargebacks.py +++ b/mollie/api/resources/chargebacks.py @@ -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: @@ -74,7 +74,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) diff --git a/mollie/api/resources/methods.py b/mollie/api/resources/methods.py index 2b1c064e..75dc2a6a 100644 --- a/mollie/api/resources/methods.py +++ b/mollie/api/resources/methods.py @@ -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 @@ -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 diff --git a/mollie/api/resources/payments.py b/mollie/api/resources/payments.py index fe005048..8d3129a3 100644 --- a/mollie/api/resources/payments.py +++ b/mollie/api/resources/payments.py @@ -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 @@ -147,7 +147,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) diff --git a/mollie/api/resources/refunds.py b/mollie/api/resources/refunds.py index 3d25eccc..d5665cb1 100644 --- a/mollie/api/resources/refunds.py +++ b/mollie/api/resources/refunds.py @@ -1,6 +1,6 @@ -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional, Union -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 @@ -99,7 +99,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 Refunds(self.client).list(**params) diff --git a/tests/responses/payments_list_with_next.json b/tests/responses/payments_list_with_next.json new file mode 100644 index 00000000..fc9d91e2 --- /dev/null +++ b/tests/responses/payments_list_with_next.json @@ -0,0 +1,130 @@ +{ + "_embedded": { + "payments": [ + { + "resource": "payment", + "id": "tr_gHTfdq4xAB", + "mode": "test", + "createdAt": "2018-07-19T09:49:46+00:00", + "amount": { + "value": "50.00", + "currency": "EUR" + }, + "description": "My first iDEAL API payment", + "method": "ideal", + "metadata": { + "order_id": 1531993786 + }, + "status": "open", + "isCancelable": false, + "expiresAt": "2018-07-19T10:05:16+00:00", + "profileId": "pfl_gh5wrNQ6fx", + "sequenceType": "oneoff", + "redirectUrl": "https://webshop.example.org/order/1531993786", + "webhookUrl": "https://webshop.example.org/payments/webhook/", + "settlementAmount": { + "value": "50.00", + "currency": "EUR" + }, + "_links": { + "self": { + "href": "https://api.mollie.com/v2/payments/tr_gM5hTq4x4J", + "type": "application/hal+json" + }, + "checkout": { + "href": "https://www.mollie.com/paymentscreen/testmode/?method=ideal&token=spyye1", + "type": "text/html" + } + } + }, + { + "resource": "payment", + "id": "tr_9uhYN1zuCD", + "mode": "test", + "createdAt": "2018-07-19T09:49:35+00:00", + "amount": { + "value": "10.00", + "currency": "GBP" + }, + "description": "My first iDEAL API payment", + "method": "ideal", + "metadata": { + "order_id": 1531993773 + }, + "status": "open", + "isCancelable": false, + "expiresAt": "2018-07-19T10:05:05+00:00", + "profileId": "pfl_gh5wrNQ6fx", + "sequenceType": "oneoff", + "redirectUrl": "https://webshop.example.org/order/1531993773", + "webhookUrl": "https://webshop.example.org/payments/webhook/", + "settlementAmount": { + "value": "50.00", + "currency": "EUR" + }, + "_links": { + "self": { + "href": "https://api.mollie.com/v2/payments/tr_7UhSN1zuXS", + "type": "application/hal+json" + }, + "checkout": { + "href": "https://www.mollie.com/paymentscreen/testmode/?method=ideal&token=xyrvjf", + "type": "text/html" + } + } + }, + { + "resource": "payment", + "id": "tr_47HgTDE9EF", + "mode": "test", + "createdAt": "2018-07-19T09:49:37+00:00", + "amount": { + "value": "100.00", + "currency": "EUR" + }, + "description": "My first iDEAL API payment", + "method": "ideal", + "metadata": { + "order_id": 1531993778 + }, + "status": "open", + "isCancelable": false, + "expiresAt": "2018-07-19T10:05:08+00:00", + "profileId": "pfl_gh5wrNQ6rt", + "sequenceType": "oneoff", + "redirectUrl": "https://webshop.example.org/order/1531993778", + "webhookUrl": "https://webshop.example.org/payments/webhook/", + "settlementAmount": { + "value": "50.00", + "currency": "EUR" + }, + "_links": { + "self": { + "href": "https://api.mollie.com/v2/payments/tr_45xyzDE9v9", + "type": "application/hal+json" + }, + "checkout": { + "href": "https://www.mollie.com/paymentscreen/testmode/?method=ideal&token=xasfjf", + "type": "text/html" + } + } + } + ] + }, + "count": 3, + "_links": { + "documentation": { + "href": "https://docs.mollie.com/reference/v2/payments-api/list-payments", + "type": "text/html" + }, + "self": { + "href": "https://api.mollie.com/v2/payments?limit=3", + "type": "application/hal+json" + }, + "previous": null, + "next": { + "href": "https://api.mollie.com/v2/payments?from=tr_gM5hTq4x4J&limit=3", + "type": "application/hal+json" + } + } +} diff --git a/tests/test_list.py b/tests/test_list.py index 8c410730..05424c37 100644 --- a/tests/test_list.py +++ b/tests/test_list.py @@ -1,4 +1,5 @@ import pytest +from responses import matchers from mollie.api.objects.list import ObjectList from mollie.api.objects.method import Method @@ -6,6 +7,31 @@ from .utils import assert_list_object +def test_list_payments_use_new_iterator(client, response): + """Retrieve a list of payments using the new result iterator.""" + response.get( + "https://api.mollie.com/v2/payments", + "payments_list_with_next", + match=[matchers.query_string_matcher("limit=3")], + ) + response.get( + "https://api.mollie.com/v2/payments", + "payments_list", + match=[matchers.query_string_matcher("from=tr_gM5hTq4x4J&limit=3")], + ) + + payments = client.payments.list(limit=3, return_iterator=True) + payment_ids = [p.id for p in payments] + assert payment_ids == [ + "tr_gHTfdq4xAB", + "tr_9uhYN1zuCD", + "tr_47HgTDE9EF", + "tr_gM5hTq4x4J", + "tr_7UhSN1zuXS", + "tr_45xyzDE9v9", + ] + + def test_list_iterator_behaviour(client, response): """Verify the behaviour of the List object in iterator circumstances.""" response.get("https://api.mollie.com/v2/methods", "methods_list") From 91beaab5b7f929b83f958e266f5d641465096f80 Mon Sep 17 00:00:00 2001 From: Tom Hendrikx Date: Sun, 4 Dec 2022 02:37:59 +0100 Subject: [PATCH 2/3] Improve ResultListIterator code using a dotted path constant on the resource class. --- mollie/api/objects/list.py | 15 ++++----------- mollie/api/resources/base.py | 3 +++ mollie/api/resources/payments.py | 1 + mollie/api/utils.py | 9 +++++++++ 4 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 mollie/api/utils.py diff --git a/mollie/api/objects/list.py b/mollie/api/objects/list.py index 066201ec..7e395e48 100644 --- a/mollie/api/objects/list.py +++ b/mollie/api/objects/list.py @@ -1,5 +1,6 @@ -from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Type +from typing import TYPE_CHECKING, Any, Dict, List, Tuple +from ..utils import get_class_from_dotted_path from .base import ObjectBase if TYPE_CHECKING: @@ -118,7 +119,6 @@ class ResultListIterator: _last: int resource: "ResourceBase" - result_class: Type[ObjectBase] next_uri: str list_data: List[Dict[str, Any]] @@ -170,15 +170,8 @@ def _parse_data(self, data: Dict[str, Any]) -> Tuple[List[Dict[str, Any]], str]: except TypeError: next_uri = "" - if not hasattr(self, "result_class"): - # A bit klunky: need to instantiate the class with fake data, only to get its type. - # - # This could be improved if we define the result class (or its dotted path) as a - # class constant on the resource. Additionaly, that could help when making - # get_resource_object() a generic method on ResourceBase. - self.result_class = type(self.resource.get_resource_object({})) - - resource_name = self.result_class.get_object_name() + 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 diff --git a/mollie/api/resources/base.py b/mollie/api/resources/base.py index 37f37eb2..dc0ff8e9 100644 --- a/mollie/api/resources/base.py +++ b/mollie/api/resources/base.py @@ -20,6 +20,9 @@ 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 diff --git a/mollie/api/resources/payments.py b/mollie/api/resources/payments.py index 8d3129a3..381499bb 100644 --- a/mollie/api/resources/payments.py +++ b/mollie/api/resources/payments.py @@ -31,6 +31,7 @@ class PaymentsBase(ResourceBase): RESOURCE_ID_PREFIX: str = "tr_" + RESULT_CLASS_PATH: str = "mollie.api.objects.payment.Payment" def get_resource_object(self, result: dict) -> Payment: from ..objects.payment import Payment diff --git a/mollie/api/utils.py b/mollie/api/utils.py new file mode 100644 index 00000000..ab58974f --- /dev/null +++ b/mollie/api/utils.py @@ -0,0 +1,9 @@ +import importlib +from typing import Any, Type + + +def get_class_from_dotted_path(dotted_path: str) -> Type[Any]: + module_path, class_name = dotted_path.rsplit(".", 1) + module = importlib.import_module(module_path) + class_ = getattr(module, class_name) + return class_ From 4eaf7102b4c069b67be18cfaafa954745ff2a45b Mon Sep 17 00:00:00 2001 From: Tom Hendrikx Date: Sun, 4 Dec 2022 02:44:40 +0100 Subject: [PATCH 3/3] Remove duplicate code and replace it with a generic method and a constant --- mollie/api/resources/base.py | 12 +++++------- mollie/api/resources/captures.py | 4 +--- mollie/api/resources/chargebacks.py | 4 +--- mollie/api/resources/clients.py | 4 +--- mollie/api/resources/customers.py | 4 +--- mollie/api/resources/invoices.py | 4 +--- mollie/api/resources/mandates.py | 4 +--- mollie/api/resources/methods.py | 4 ++-- mollie/api/resources/onboarding.py | 3 +-- mollie/api/resources/order_lines.py | 4 +--- mollie/api/resources/orders.py | 4 +--- mollie/api/resources/organizations.py | 4 +--- mollie/api/resources/payment_links.py | 4 +--- mollie/api/resources/payments.py | 5 ----- mollie/api/resources/permissions.py | 3 +-- mollie/api/resources/profiles.py | 4 +--- mollie/api/resources/refunds.py | 4 +--- mollie/api/resources/settlements.py | 4 +--- mollie/api/resources/shipments.py | 4 +--- mollie/api/resources/subscriptions.py | 4 +--- 20 files changed, 24 insertions(+), 63 deletions(-) diff --git a/mollie/api/resources/base.py b/mollie/api/resources/base.py index dc0ff8e9..e32bd28e 100644 --- a/mollie/api/resources/base.py +++ b/mollie/api/resources/base.py @@ -4,6 +4,7 @@ from ..error import IdentifierError, ResponseError, ResponseHandlingError from ..objects.list import ObjectList, ResultListIterator +from ..utils import get_class_from_dotted_path if TYPE_CHECKING: from ..client import Client @@ -26,14 +27,11 @@ class ResourceBase: 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.""" diff --git a/mollie/api/resources/captures.py b/mollie/api/resources/captures.py index cd1c7417..6d1de2e3 100644 --- a/mollie/api/resources/captures.py +++ b/mollie/api/resources/captures.py @@ -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): diff --git a/mollie/api/resources/chargebacks.py b/mollie/api/resources/chargebacks.py index 6895bf6e..a0a5c15e 100644 --- a/mollie/api/resources/chargebacks.py +++ b/mollie/api/resources/chargebacks.py @@ -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): diff --git a/mollie/api/resources/clients.py b/mollie/api/resources/clients.py index f48c466f..8b669a78 100644 --- a/mollie/api/resources/clients.py +++ b/mollie/api/resources/clients.py @@ -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.""" diff --git a/mollie/api/resources/customers.py b/mollie/api/resources/customers.py index 3eddd945..4b9c871d 100644 --- a/mollie/api/resources/customers.py +++ b/mollie/api/resources/customers.py @@ -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") diff --git a/mollie/api/resources/invoices.py b/mollie/api/resources/invoices.py index 69bec70b..fb2271f9 100644 --- a/mollie/api/resources/invoices.py +++ b/mollie/api/resources/invoices.py @@ -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") diff --git a/mollie/api/resources/mandates.py b/mollie/api/resources/mandates.py index 779be3f8..d938d213 100644 --- a/mollie/api/resources/mandates.py +++ b/mollie/api/resources/mandates.py @@ -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 @@ -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) diff --git a/mollie/api/resources/methods.py b/mollie/api/resources/methods.py index 75dc2a6a..3bed379d 100644 --- a/mollie/api/resources/methods.py +++ b/mollie/api/resources/methods.py @@ -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): diff --git a/mollie/api/resources/onboarding.py b/mollie/api/resources/onboarding.py index 97d2fc8f..b0a84386 100644 --- a/mollie/api/resources/onboarding.py +++ b/mollie/api/resources/onboarding.py @@ -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": diff --git a/mollie/api/resources/order_lines.py b/mollie/api/resources/order_lines.py index e99ab010..29f95d2c 100644 --- a/mollie/api/resources/order_lines.py +++ b/mollie/api/resources/order_lines.py @@ -25,6 +25,7 @@ class OrderLines(ResourceBase): """ RESOURCE_ID_PREFIX: str = "odl_" + RESULT_CLASS_PATH: str = "mollie.api.objects.order_line.OrderLine" _order: "Order" @@ -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. diff --git a/mollie/api/resources/orders.py b/mollie/api/resources/orders.py index 70bac22d..281b6c09 100644 --- a/mollie/api/resources/orders.py +++ b/mollie/api/resources/orders.py @@ -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") diff --git a/mollie/api/resources/organizations.py b/mollie/api/resources/organizations.py index a64d9eba..7728d74f 100644 --- a/mollie/api/resources/organizations.py +++ b/mollie/api/resources/organizations.py @@ -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": diff --git a/mollie/api/resources/payment_links.py b/mollie/api/resources/payment_links.py index 94579e8d..77cafb47 100644 --- a/mollie/api/resources/payment_links.py +++ b/mollie/api/resources/payment_links.py @@ -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) diff --git a/mollie/api/resources/payments.py b/mollie/api/resources/payments.py index 381499bb..c37edcac 100644 --- a/mollie/api/resources/payments.py +++ b/mollie/api/resources/payments.py @@ -33,11 +33,6 @@ class PaymentsBase(ResourceBase): RESOURCE_ID_PREFIX: str = "tr_" RESULT_CLASS_PATH: str = "mollie.api.objects.payment.Payment" - def get_resource_object(self, result: dict) -> Payment: - from ..objects.payment import Payment - - return Payment(result, self.client) - class Payments( PaymentsBase, ResourceCreateMixin, ResourceDeleteMixin, ResourceGetMixin, ResourceListMixin, ResourceUpdateMixin diff --git a/mollie/api/resources/permissions.py b/mollie/api/resources/permissions.py index 0ba84425..fa08e8d3 100644 --- a/mollie/api/resources/permissions.py +++ b/mollie/api/resources/permissions.py @@ -13,8 +13,7 @@ class Permissions(ResourceGetMixin, ResourceListMixin): """Resource handler for the `/permissions` endpoint.""" - def get_resource_object(self, result: dict) -> Permission: - return Permission(result, self.client) + RESULT_CLASS_PATH: str = "mollie.api.objects.permission.Permission" @staticmethod def validate_permission_id(permission_id: str) -> None: diff --git a/mollie/api/resources/profiles.py b/mollie/api/resources/profiles.py index 85e7f36e..e49d8ffc 100644 --- a/mollie/api/resources/profiles.py +++ b/mollie/api/resources/profiles.py @@ -12,9 +12,7 @@ class Profiles(ResourceCreateMixin, ResourceDeleteMixin, ResourceGetMixin, Resou """Resource handler for the `/profiles` endpoint.""" RESOURCE_ID_PREFIX: str = "pfl_" - - def get_resource_object(self, result: dict) -> Profile: - return Profile(result, self.client) + RESULT_CLASS_PATH: str = "mollie.api.objects.profile.Profile" def get(self, resource_id: str, **params: Any) -> Profile: if resource_id != "me": diff --git a/mollie/api/resources/refunds.py b/mollie/api/resources/refunds.py index d5665cb1..5a8a5536 100644 --- a/mollie/api/resources/refunds.py +++ b/mollie/api/resources/refunds.py @@ -22,9 +22,7 @@ class RefundsBase(ResourceBase): RESOURCE_ID_PREFIX: str = "re_" - - def get_resource_object(self, result: dict) -> Refund: - return Refund(result, self.client) + RESULT_CLASS_PATH: str = "mollie.api.objects.refund.Refund" class Refunds(RefundsBase, ResourceListMixin): diff --git a/mollie/api/resources/settlements.py b/mollie/api/resources/settlements.py index 7961d3c3..567b9e07 100644 --- a/mollie/api/resources/settlements.py +++ b/mollie/api/resources/settlements.py @@ -9,6 +9,7 @@ class Settlements(ResourceGetMixin, ResourceListMixin): """Resource handler for the `/settlements` endpoint.""" RESOURCE_ID_PREFIX: str = "stl_" + RESULT_CLASS_PATH: str = "mollie.api.objects.settlement.Settlement" # According to Mollie, the bank reference is formatted as: # - The Mollie customer ID, 4 to 7 digits. @@ -17,9 +18,6 @@ class Settlements(ResourceGetMixin, ResourceListMixin): # The components are separated by a dot. BANK_REFERENCE_REGEX: Pattern[str] = re.compile(r"^\d{4,7}\.\d{4}\.\d{2}$", re.ASCII) - def get_resource_object(self, result: dict) -> Settlement: - return Settlement(result, self.client) - @classmethod def validate_resource_id(cls, resource_id: str, name: str = "", message: str = "") -> None: """ diff --git a/mollie/api/resources/shipments.py b/mollie/api/resources/shipments.py index f1c9ca6e..1fb09531 100644 --- a/mollie/api/resources/shipments.py +++ b/mollie/api/resources/shipments.py @@ -16,6 +16,7 @@ class OrderShipments(ResourceCreateMixin, ResourceGetMixin, ResourceListMixin, R """Resource handler for the `/orders/:order_id:/shipments` endpoint.""" RESOURCE_ID_PREFIX: str = "shp_" + RESULT_CLASS_PATH: str = "mollie.api.objects.shipment.Shipment" _order: Order @@ -23,9 +24,6 @@ def __init__(self, client: "Client", order: Order) -> None: self._order = order super().__init__(client) - def get_resource_object(self, result: dict) -> Shipment: - return Shipment(result, self.client) - def get_resource_path(self) -> str: return f"orders/{self._order.id}/shipments" diff --git a/mollie/api/resources/subscriptions.py b/mollie/api/resources/subscriptions.py index 2d9dd93a..2082146d 100644 --- a/mollie/api/resources/subscriptions.py +++ b/mollie/api/resources/subscriptions.py @@ -22,9 +22,7 @@ class SubscriptionsBase(ResourceBase): RESOURCE_ID_PREFIX: str = "sub_" - - def get_resource_object(self, result: dict) -> Subscription: - return Subscription(result, self.client) + RESULT_CLASS_PATH: str = "mollie.api.objects.subscription.Subscription" class Subscriptions(SubscriptionsBase, ResourceListMixin):