Skip to content

Commit 2fc3eb8

Browse files
author
Tom Hendrikx
committed
Implement a new iterator-only handler for result lists.
Since the full list of results is unknown, and results that already have been iterated might no longer be available, methods such as `__len__()` and `__getitem__()` as found in `ObjectList` are no longer available. `__getitem__()` (e.g `result_list['tr_1234']`) replacement: use `Resource.get(resource_id)`. `__len__()` (e.g. `len(result_list)`) replacement: exhaust the iterator and count the results. TODO: - More tests - Reversed() iterator, hoe does it work? - Add logic to return an ResultListIterator where now an ObjectList is hardcoded - Empty list results?
1 parent e8e559b commit 2fc3eb8

File tree

8 files changed

+269
-16
lines changed

8 files changed

+269
-16
lines changed

mollie/api/objects/list.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Type
2+
13
from .base import ObjectBase
24

5+
if TYPE_CHECKING:
6+
from ..client import Client
7+
from ..resources.base import ResourceBase
8+
39

410
class UnknownObject(ObjectBase):
511
"""Mock object for empty lists."""
@@ -97,3 +103,87 @@ def get_previous(self):
97103
resource = self.object_type.get_resource_class(self.client)
98104
resp = resource.perform_api_call(resource.REST_READ, url)
99105
return ObjectList(resp, self.object_type, self.client)
106+
107+
108+
class ResultListIterator:
109+
"""
110+
An iterator for result lists from the API.
111+
112+
You can iterate through the results. If the initial result indocates pagination,
113+
a new result page is automatically fetched from the API when the current result page
114+
is exhausted.
115+
116+
Note: This iterator should preferably replace the ObjectList as the default
117+
return value for the Resource.list() method in the future.
118+
"""
119+
120+
_last: int
121+
_client: "Client"
122+
next_uri: str
123+
list_data: List[Dict[str, Any]]
124+
result_class: Type[ObjectBase]
125+
resource_class: Type["ResourceBase"]
126+
127+
def __init__(
128+
self,
129+
client: "Client",
130+
data: Dict[str, Any],
131+
resource_class: Type["ResourceBase"],
132+
) -> None:
133+
self._client = client
134+
self.resource_class = resource_class
135+
136+
# Next line is a bit klunky
137+
self.result_class = self.resource_class(self._client).get_resource_object({}).__class__
138+
self.list_data, self.next_uri = self._parse_data(data)
139+
140+
self._last = -1
141+
142+
def __iter__(self):
143+
"""Return the iterator."""
144+
return self
145+
146+
def __next__(self) -> ObjectBase:
147+
"""
148+
Return the next result.
149+
150+
If the result data is exhausted, but a link to further paginated results
151+
is available, we fetch that and return the first result of that.
152+
"""
153+
current = self._last + 1
154+
try:
155+
object_data = self.list_data[current]
156+
self._last = current
157+
except IndexError:
158+
if self.next_uri:
159+
self._reinit_from_uri(self.next_uri)
160+
return next(self)
161+
else:
162+
raise StopIteration
163+
164+
return self.result_class(object_data, self._client)
165+
166+
def _parse_data(self, data: Dict[str, Any]) -> Tuple[List[Dict[str, Any]], str]:
167+
"""
168+
Extract useful data from the payload.
169+
170+
We are interested in the following parts:
171+
- the actual list data, unwrapped
172+
- links to next results, when results are paginated
173+
"""
174+
try:
175+
next_uri = data["_links"]["next"]["href"]
176+
except TypeError:
177+
next_uri = ""
178+
179+
resource_name = self.result_class.get_object_name()
180+
list_data = data["_embedded"][resource_name]
181+
182+
return list_data, next_uri
183+
184+
def _reinit_from_uri(self, uri: str) -> None:
185+
"""Fetch additional results from the API, and feed the iterator with the data."""
186+
187+
result = self.resource_class(self._client).perform_api_call(self.resource_class.REST_READ, uri)
188+
self.list_data, self.next_uri = self._parse_data(result)
189+
self._last = -1

mollie/api/resources/base.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from typing import TYPE_CHECKING, Any, Dict, Optional
1+
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
22

33
from ..error import IdentifierError, ResponseError, ResponseHandlingError
4-
from ..objects.list import ObjectList
4+
from ..objects.list import ObjectList, ResultListIterator
55

66
if TYPE_CHECKING:
77
from ..client import Client
@@ -96,10 +96,17 @@ def from_url(self, url: str, params: Optional[Dict[str, Any]] = None) -> Any:
9696

9797

9898
class ResourceListMixin(ResourceBase):
99-
def list(self, **params: Optional[Dict[str, Any]]) -> ObjectList:
99+
def list(self, **params: Optional[Dict[str, Any]]) -> Union[ObjectList, ResultListIterator]:
100+
use_iterator = params.pop("use_iterator", False)
101+
100102
path = self.get_resource_path()
101103
result = self.perform_api_call(self.REST_LIST, path, params=params)
102-
return ObjectList(result, self.get_resource_object({}).__class__, self.client)
104+
105+
if use_iterator:
106+
resource_class = self.__class__
107+
return ResultListIterator(self.client, result, resource_class)
108+
else:
109+
return ObjectList(result, self.get_resource_object({}).__class__, self.client)
103110

104111

105112
class ResourceUpdateMixin(ResourceBase):

mollie/api/resources/chargebacks.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from typing import TYPE_CHECKING, Any, Dict, Optional
1+
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
22

33
from ..objects.chargeback import Chargeback
4-
from ..objects.list import ObjectList
4+
from ..objects.list import ObjectList, ResultListIterator
55
from .base import ResourceBase, ResourceGetMixin, ResourceListMixin
66

77
if TYPE_CHECKING:
@@ -74,7 +74,7 @@ def __init__(self, client: "Client", profile: "Profile") -> None:
7474
self._profile = profile
7575
super().__init__(client)
7676

77-
def list(self, **params: Optional[Dict[str, Any]]) -> ObjectList:
77+
def list(self, **params: Optional[Dict[str, Any]]) -> Union[ObjectList, ResultListIterator]:
7878
# Set the profileId in the query params
7979
params.update({"profileId": self._profile.id})
8080
return Chargebacks(self.client).list(**params)

mollie/api/resources/methods.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from typing import TYPE_CHECKING, Any, Dict, List, Optional
1+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
22

33
from ..error import IdentifierError
44
from ..objects.issuer import Issuer
5-
from ..objects.list import ObjectList
5+
from ..objects.list import ObjectList, ResultListIterator
66
from ..objects.method import Method
77
from .base import ResourceBase, ResourceGetMixin, ResourceListMixin
88

@@ -87,7 +87,7 @@ def disable(self, method_id: str, **params: Optional[Dict[str, Any]]) -> Method:
8787
result = self.perform_api_call(self.REST_DELETE, path, params=params)
8888
return self.get_resource_object(result)
8989

90-
def list(self, **params: Optional[Dict[str, Any]]) -> ObjectList:
90+
def list(self, **params: Optional[Dict[str, Any]]) -> Union[ObjectList, ResultListIterator]:
9191
"""List the payment methods for the profile."""
9292
params.update({"profileId": self._profile.id})
9393
# Divert the API call to the general Methods resource

mollie/api/resources/payments.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from typing import TYPE_CHECKING, Any, Dict, Optional
1+
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
22

33
from ..objects.customer import Customer
4-
from ..objects.list import ObjectList
4+
from ..objects.list import ObjectList, ResultListIterator
55
from ..objects.order import Order
66
from ..objects.payment import Payment
77
from ..objects.profile import Profile
@@ -147,7 +147,7 @@ def __init__(self, client: "Client", profile: Profile) -> None:
147147
self._profile = profile
148148
super().__init__(client)
149149

150-
def list(self, **params: Optional[Dict[str, Any]]) -> ObjectList:
150+
def list(self, **params: Optional[Dict[str, Any]]) -> Union[ObjectList, ResultListIterator]:
151151
# Set the profileId in the query params
152152
params.update({"profileId": self._profile.id})
153153
return Payments(self.client).list(**params)

mollie/api/resources/refunds.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from typing import TYPE_CHECKING, Any, Dict, Optional
1+
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
22

3-
from ..objects.list import ObjectList
3+
from ..objects.list import ObjectList, ResultListIterator
44
from ..objects.order import Order
55
from ..objects.payment import Payment
66
from ..objects.profile import Profile
@@ -99,7 +99,7 @@ def __init__(self, client: "Client", profile: Profile) -> None:
9999
self._profile = profile
100100
super().__init__(client)
101101

102-
def list(self, **params: Optional[Dict[str, Any]]) -> ObjectList:
102+
def list(self, **params: Optional[Dict[str, Any]]) -> Union[ObjectList, ResultListIterator]:
103103
# Set the profileId in the query params
104104
params.update({"profileId": self._profile.id})
105105
return Refunds(self.client).list(**params)
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
{
2+
"_embedded": {
3+
"payments": [
4+
{
5+
"resource": "payment",
6+
"id": "tr_gHTfdq4xAB",
7+
"mode": "test",
8+
"createdAt": "2018-07-19T09:49:46+00:00",
9+
"amount": {
10+
"value": "50.00",
11+
"currency": "EUR"
12+
},
13+
"description": "My first iDEAL API payment",
14+
"method": "ideal",
15+
"metadata": {
16+
"order_id": 1531993786
17+
},
18+
"status": "open",
19+
"isCancelable": false,
20+
"expiresAt": "2018-07-19T10:05:16+00:00",
21+
"profileId": "pfl_gh5wrNQ6fx",
22+
"sequenceType": "oneoff",
23+
"redirectUrl": "https://webshop.example.org/order/1531993786",
24+
"webhookUrl": "https://webshop.example.org/payments/webhook/",
25+
"settlementAmount": {
26+
"value": "50.00",
27+
"currency": "EUR"
28+
},
29+
"_links": {
30+
"self": {
31+
"href": "https://api.mollie.com/v2/payments/tr_gM5hTq4x4J",
32+
"type": "application/hal+json"
33+
},
34+
"checkout": {
35+
"href": "https://www.mollie.com/paymentscreen/testmode/?method=ideal&token=spyye1",
36+
"type": "text/html"
37+
}
38+
}
39+
},
40+
{
41+
"resource": "payment",
42+
"id": "tr_9uhYN1zuCD",
43+
"mode": "test",
44+
"createdAt": "2018-07-19T09:49:35+00:00",
45+
"amount": {
46+
"value": "10.00",
47+
"currency": "GBP"
48+
},
49+
"description": "My first iDEAL API payment",
50+
"method": "ideal",
51+
"metadata": {
52+
"order_id": 1531993773
53+
},
54+
"status": "open",
55+
"isCancelable": false,
56+
"expiresAt": "2018-07-19T10:05:05+00:00",
57+
"profileId": "pfl_gh5wrNQ6fx",
58+
"sequenceType": "oneoff",
59+
"redirectUrl": "https://webshop.example.org/order/1531993773",
60+
"webhookUrl": "https://webshop.example.org/payments/webhook/",
61+
"settlementAmount": {
62+
"value": "50.00",
63+
"currency": "EUR"
64+
},
65+
"_links": {
66+
"self": {
67+
"href": "https://api.mollie.com/v2/payments/tr_7UhSN1zuXS",
68+
"type": "application/hal+json"
69+
},
70+
"checkout": {
71+
"href": "https://www.mollie.com/paymentscreen/testmode/?method=ideal&token=xyrvjf",
72+
"type": "text/html"
73+
}
74+
}
75+
},
76+
{
77+
"resource": "payment",
78+
"id": "tr_47HgTDE9EF",
79+
"mode": "test",
80+
"createdAt": "2018-07-19T09:49:37+00:00",
81+
"amount": {
82+
"value": "100.00",
83+
"currency": "EUR"
84+
},
85+
"description": "My first iDEAL API payment",
86+
"method": "ideal",
87+
"metadata": {
88+
"order_id": 1531993778
89+
},
90+
"status": "open",
91+
"isCancelable": false,
92+
"expiresAt": "2018-07-19T10:05:08+00:00",
93+
"profileId": "pfl_gh5wrNQ6rt",
94+
"sequenceType": "oneoff",
95+
"redirectUrl": "https://webshop.example.org/order/1531993778",
96+
"webhookUrl": "https://webshop.example.org/payments/webhook/",
97+
"settlementAmount": {
98+
"value": "50.00",
99+
"currency": "EUR"
100+
},
101+
"_links": {
102+
"self": {
103+
"href": "https://api.mollie.com/v2/payments/tr_45xyzDE9v9",
104+
"type": "application/hal+json"
105+
},
106+
"checkout": {
107+
"href": "https://www.mollie.com/paymentscreen/testmode/?method=ideal&token=xasfjf",
108+
"type": "text/html"
109+
}
110+
}
111+
}
112+
]
113+
},
114+
"count": 3,
115+
"_links": {
116+
"documentation": {
117+
"href": "https://docs.mollie.com/reference/v2/payments-api/list-payments",
118+
"type": "text/html"
119+
},
120+
"self": {
121+
"href": "https://api.mollie.com/v2/payments?limit=50",
122+
"type": "application/hal+json"
123+
},
124+
"previous": null,
125+
"next": {
126+
"href": "https://api.mollie.com/v2/payments?from=tr_gM5hTq4x4J&limit=3",
127+
"type": "application/hal+json"
128+
}
129+
}
130+
}

tests/test_payments.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
from responses import matchers
23

34
from mollie.api.error import IdentifierError
45
from mollie.api.objects.capture import Capture
@@ -32,6 +33,31 @@ def test_list_payments(client, response):
3233
assert_list_object(payments, Payment)
3334

3435

36+
def test_list_payments_use_iterator(client, response):
37+
"""Retrieve a list of payments using the new object list."""
38+
response.get(
39+
"https://api.mollie.com/v2/payments",
40+
"payments_list_with_next",
41+
match=[matchers.query_string_matcher("limit=3")],
42+
)
43+
response.get(
44+
"https://api.mollie.com/v2/payments",
45+
"payments_list",
46+
match=[matchers.query_string_matcher("from=tr_gM5hTq4x4J&limit=3")],
47+
)
48+
49+
payments = client.payments.list(use_iterator=True, limit=3)
50+
payment_ids = [p.id for p in payments]
51+
assert payment_ids == [
52+
"tr_gHTfdq4xAB",
53+
"tr_9uhYN1zuCD",
54+
"tr_47HgTDE9EF",
55+
"tr_gM5hTq4x4J",
56+
"tr_7UhSN1zuXS",
57+
"tr_45xyzDE9v9",
58+
]
59+
60+
3561
def test_create_payment(client, response):
3662
"""Create a new payment."""
3763
response.post("https://api.mollie.com/v2/payments", "payment_single")

0 commit comments

Comments
 (0)