Skip to content

Commit f980e12

Browse files
committed
Add for sync collection clients
1 parent 0e311ae commit f980e12

14 files changed

+140
-72
lines changed

src/apify_client/clients/base/resource_collection_client.py

Lines changed: 88 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Generator
3+
from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Generator, Iterable, Iterator
44
from typing import Any, Generic, Protocol, TypeVar
55

66
from apify_client._utils import parse_date_fields, pluck_data
@@ -52,6 +52,37 @@ def _list(self, **kwargs: Any) -> ListPage:
5252

5353
return ListPage(parse_date_fields(pluck_data(response.json())))
5454

55+
56+
def _list_iterable(self, **kwargs: Any) -> IterableListPage[T]:
57+
"""Return object can be awaited or iterated over."""
58+
chunk_size = kwargs.pop('chunk_size', None)
59+
60+
list_page = self._list(**{**kwargs, 'limit': _min_for_limit_param(kwargs.get('limit'), chunk_size)})
61+
62+
def iterator() -> Iterator[T]:
63+
current_page = list_page
64+
for item in current_page.items:
65+
yield item
66+
67+
offset = kwargs.get('offset') or 0
68+
limit = min(kwargs.get('limit') or current_page.total, current_page.total)
69+
70+
current_offset = offset + len(current_page.items)
71+
remaining_items = min(current_page.total - offset, limit) - len(current_page.items)
72+
while current_page.items and remaining_items > 0:
73+
new_kwargs = {
74+
**kwargs,
75+
'offset': current_offset,
76+
'limit': _min_for_limit_param(remaining_items, chunk_size),
77+
}
78+
current_page = self._list(**new_kwargs)
79+
for item in current_page.items:
80+
yield item
81+
current_offset += len(current_page.items)
82+
remaining_items -= len(current_page.items)
83+
84+
return IterableListPage[T](list_page, iterator())
85+
5586
def _create(self, resource: dict) -> dict:
5687
response = self.http_client.call(
5788
url=self._url(),
@@ -85,24 +116,11 @@ async def _list(self, **kwargs: Any) -> ListPage:
85116

86117
return ListPage(parse_date_fields(pluck_data(response.json())))
87118

88-
def _list_iterable(self, **kwargs: Any) -> ListPageProtocol[T]:
119+
def _list_iterable(self, **kwargs: Any) -> ListPageProtocolAsync[T]:
89120
"""Return object can be awaited or iterated over."""
90-
91-
def min_for_limit_param(a: int | None, b: int | None) -> int | None:
92-
# API treats 0 as None for limit parameter, in this context API understands 0 as infinity.
93-
if a == 0:
94-
a = None
95-
if b == 0:
96-
b = None
97-
if a is None:
98-
return b
99-
if b is None:
100-
return a
101-
return min(a, b)
102-
103121
chunk_size = kwargs.pop('chunk_size', None)
104122

105-
list_page_awaitable = self._list(**{**kwargs, 'limit': min_for_limit_param(kwargs.get('limit'), chunk_size)})
123+
list_page_awaitable = self._list(**{**kwargs, 'limit': _min_for_limit_param(kwargs.get('limit'), chunk_size)})
106124

107125
async def async_iterator() -> AsyncIterator[T]:
108126
current_page = await list_page_awaitable
@@ -118,15 +136,15 @@ async def async_iterator() -> AsyncIterator[T]:
118136
new_kwargs = {
119137
**kwargs,
120138
'offset': current_offset,
121-
'limit': min_for_limit_param(remaining_items, chunk_size),
139+
'limit': _min_for_limit_param(remaining_items, chunk_size),
122140
}
123141
current_page = await self._list(**new_kwargs)
124142
for item in current_page.items:
125143
yield item
126144
current_offset += len(current_page.items)
127145
remaining_items -= len(current_page.items)
128146

129-
return IterableListPage[T](list_page_awaitable, async_iterator())
147+
return IterableListPageAsync[T](list_page_awaitable, async_iterator())
130148

131149
async def _create(self, resource: dict) -> dict:
132150
response = await self.http_client.call(
@@ -153,11 +171,47 @@ async def _get_or_create(
153171
return parse_date_fields(pluck_data(response.json()))
154172

155173

156-
class ListPageProtocol(Protocol[T], AsyncIterable[T], Awaitable[ListPage[T]]):
174+
class ListPageProtocol(Protocol[T], Iterable[T]):
157175
"""Protocol for an object that can be both awaited and asynchronously iterated over."""
158176

177+
items: list[T]
178+
"""List of returned objects on this page"""
159179

160-
class IterableListPage(Generic[T]):
180+
count: int
181+
"""Count of the returned objects on this page"""
182+
183+
offset: int
184+
"""The limit on the number of returned objects offset specified in the API call"""
185+
186+
limit: int
187+
"""The offset of the first object specified in the API call"""
188+
189+
total: int
190+
"""Total number of objects matching the API call criteria"""
191+
192+
desc: bool
193+
"""Whether the listing is descending or not"""
194+
195+
class IterableListPage(Generic[T], ListPage[T]):
196+
"""Can be called to get ListPage with items or iterated over to get individual items."""
197+
198+
def __init__(self, list_page: ListPage[T], iterator: Iterator[T]) -> None:
199+
self.items = list_page.items
200+
self.offset = list_page.offset
201+
self.limit = list_page.limit
202+
self.count = list_page.count
203+
self.total = list_page.total
204+
self.desc = list_page.desc
205+
self._iterator = iterator
206+
207+
def __iter__(self) -> Iterator[T]:
208+
"""Return an iterator over the items from API, possibly doing multiple API calls."""
209+
return self._iterator
210+
211+
class ListPageProtocolAsync(Protocol[T], AsyncIterable[T], Awaitable[ListPage[T]]):
212+
"""Protocol for an object that can be both awaited and asynchronously iterated over."""
213+
214+
class IterableListPageAsync(Generic[T]):
161215
"""Can be awaited to get ListPage with items or asynchronously iterated over to get individual items."""
162216

163217
def __init__(self, awaitable: Awaitable[ListPage[T]], async_iterator: AsyncIterator[T]) -> None:
@@ -171,3 +225,17 @@ def __aiter__(self) -> AsyncIterator[T]:
171225
def __await__(self) -> Generator[Any, Any, ListPage[T]]:
172226
"""Return an awaitable that resolves to the ListPage doing exactly one API call."""
173227
return self._awaitable.__await__()
228+
229+
230+
def _min_for_limit_param(a: int | None, b: int | None) -> int | None:
231+
"""Return minimum of two limit parameters, treating None or 0 as infinity. Return None for infinity."""
232+
# API treats 0 as None for limit parameter, in this context API understands 0 as infinity.
233+
if a == 0:
234+
a = None
235+
if b == 0:
236+
b = None
237+
if a is None:
238+
return b
239+
if b is None:
240+
return a
241+
return min(a, b)

src/apify_client/clients/resource_clients/actor_collection.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from apify_client.clients.resource_clients.actor import get_actor_representation
88

99
if TYPE_CHECKING:
10-
from apify_client.clients.base.resource_collection_client import ListPage, ListPageProtocol
10+
from apify_client.clients.base.resource_collection_client import ListPageProtocol, ListPageProtocolAsync
1111

1212

1313
class ActorCollectionClient(ResourceCollectionClient):
@@ -25,7 +25,7 @@ def list(
2525
offset: int | None = None,
2626
desc: bool | None = None,
2727
sort_by: Literal['createdAt', 'stats.lastRunStartedAt'] | None = 'createdAt',
28-
) -> ListPage[dict]:
28+
) -> ListPageProtocol[dict]:
2929
"""List the Actors the user has created or used.
3030
3131
https://docs.apify.com/api/v2#/reference/actors/actor-collection/get-list-of-actors
@@ -40,7 +40,7 @@ def list(
4040
Returns:
4141
The list of available Actors matching the specified filters.
4242
"""
43-
return self._list(my=my, limit=limit, offset=offset, desc=desc, sortBy=sort_by)
43+
return self._list_iterable(my=my, limit=limit, offset=offset, desc=desc, sortBy=sort_by)
4444

4545
def create(
4646
self,
@@ -150,7 +150,7 @@ def list(
150150
offset: int | None = None,
151151
desc: bool | None = None,
152152
sort_by: Literal['createdAt', 'stats.lastRunStartedAt'] | None = 'createdAt',
153-
) -> ListPageProtocol[dict]:
153+
) -> ListPageProtocolAsync[dict]:
154154
"""List the Actors the user has created or used.
155155
156156
https://docs.apify.com/api/v2#/reference/actors/actor-collection/get-list-of-actors

src/apify_client/clients/resource_clients/actor_env_var_collection.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from apify_client.clients.resource_clients.actor_env_var import get_actor_env_var_representation
88

99
if TYPE_CHECKING:
10-
from apify_client.clients.base.resource_collection_client import ListPage, ListPageProtocol
10+
from apify_client.clients.base.resource_collection_client import ListPageProtocol, ListPageProtocolAsync
1111

1212

1313
class ActorEnvVarCollectionClient(ResourceCollectionClient):
@@ -17,15 +17,15 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
1717
resource_path = kwargs.pop('resource_path', 'env-vars')
1818
super().__init__(*args, resource_path=resource_path, **kwargs)
1919

20-
def list(self) -> ListPage[dict]:
20+
def list(self) -> ListPageProtocol[dict]:
2121
"""List the available actor environment variables.
2222
2323
https://docs.apify.com/api/v2#/reference/actors/environment-variable-collection/get-list-of-environment-variables
2424
2525
Returns:
2626
The list of available actor environment variables.
2727
"""
28-
return self._list()
28+
return self._list_iterable()
2929

3030
def create(
3131
self,
@@ -62,7 +62,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
6262
resource_path = kwargs.pop('resource_path', 'env-vars')
6363
super().__init__(*args, resource_path=resource_path, **kwargs)
6464

65-
def list(self) -> ListPageProtocol[dict]:
65+
def list(self) -> ListPageProtocolAsync[dict]:
6666
"""List the available actor environment variables.
6767
6868
https://docs.apify.com/api/v2#/reference/actors/environment-variable-collection/get-list-of-environment-variables

src/apify_client/clients/resource_clients/actor_version_collection.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
if TYPE_CHECKING:
1010
from apify_shared.consts import ActorSourceType
1111

12-
from apify_client.clients.base.resource_collection_client import ListPage, ListPageProtocol
12+
from apify_client.clients.base.resource_collection_client import ListPageProtocol, ListPageProtocolAsync
1313

1414

1515
class ActorVersionCollectionClient(ResourceCollectionClient):
@@ -19,15 +19,15 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
1919
resource_path = kwargs.pop('resource_path', 'versions')
2020
super().__init__(*args, resource_path=resource_path, **kwargs)
2121

22-
def list(self) -> ListPage[dict]:
22+
def list(self) -> ListPageProtocol[dict]:
2323
"""List the available Actor versions.
2424
2525
https://docs.apify.com/api/v2#/reference/actors/version-collection/get-list-of-versions
2626
2727
Returns:
2828
The list of available Actor versions.
2929
"""
30-
return self._list()
30+
return self._list_iterable()
3131

3232
def create(
3333
self,
@@ -88,7 +88,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
8888
resource_path = kwargs.pop('resource_path', 'versions')
8989
super().__init__(*args, resource_path=resource_path, **kwargs)
9090

91-
def list(self) -> ListPageProtocol[dict]:
91+
def list(self) -> ListPageProtocolAsync[dict]:
9292
"""List the available Actor versions.
9393
9494
https://docs.apify.com/api/v2#/reference/actors/version-collection/get-list-of-versions

src/apify_client/clients/resource_clients/build_collection.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from apify_client.clients.base import ResourceCollectionClient, ResourceCollectionClientAsync
66

77
if TYPE_CHECKING:
8-
from apify_client.clients.base.resource_collection_client import ListPage, ListPageProtocol
8+
from apify_client.clients.base.resource_collection_client import ListPageProtocol, ListPageProtocolAsync
99

1010

1111
class BuildCollectionClient(ResourceCollectionClient):
@@ -21,7 +21,7 @@ def list(
2121
limit: int | None = None,
2222
offset: int | None = None,
2323
desc: bool | None = None,
24-
) -> ListPage[dict]:
24+
) -> ListPageProtocol[dict]:
2525
"""List all Actor builds.
2626
2727
List all Actor builds, either of a single Actor, or all user's Actors, depending on where this client
@@ -38,7 +38,7 @@ def list(
3838
Returns:
3939
The retrieved Actor builds.
4040
"""
41-
return self._list(limit=limit, offset=offset, desc=desc)
41+
return self._list_iterable(limit=limit, offset=offset, desc=desc)
4242

4343

4444
class BuildCollectionClientAsync(ResourceCollectionClientAsync):
@@ -54,7 +54,7 @@ def list(
5454
limit: int | None = None,
5555
offset: int | None = None,
5656
desc: bool | None = None,
57-
) -> ListPageProtocol[dict]:
57+
) -> ListPageProtocolAsync[dict]:
5858
"""List all Actor builds.
5959
6060
List all Actor builds, either of a single Actor, or all user's Actors, depending on where this client

src/apify_client/clients/resource_clients/dataset_collection.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from apify_client.clients.base import ResourceCollectionClient, ResourceCollectionClientAsync
77

88
if TYPE_CHECKING:
9-
from apify_client.clients.base.resource_collection_client import ListPage, ListPageProtocol
9+
from apify_client.clients.base.resource_collection_client import ListPageProtocol, ListPageProtocolAsync
1010

1111

1212
class DatasetCollectionClient(ResourceCollectionClient):
@@ -23,7 +23,7 @@ def list(
2323
limit: int | None = None,
2424
offset: int | None = None,
2525
desc: bool | None = None,
26-
) -> ListPage[dict]:
26+
) -> ListPageProtocol[dict]:
2727
"""List the available datasets.
2828
2929
https://docs.apify.com/api/v2#/reference/datasets/dataset-collection/get-list-of-datasets
@@ -37,7 +37,7 @@ def list(
3737
Returns:
3838
The list of available datasets matching the specified filters.
3939
"""
40-
return self._list(unnamed=unnamed, limit=limit, offset=offset, desc=desc)
40+
return self._list_iterable(unnamed=unnamed, limit=limit, offset=offset, desc=desc)
4141

4242
def get_or_create(self, *, name: str | None = None, schema: dict | None = None) -> dict:
4343
"""Retrieve a named dataset, or create a new one when it doesn't exist.
@@ -68,7 +68,7 @@ def list(
6868
limit: int | None = None,
6969
offset: int | None = None,
7070
desc: bool | None = None,
71-
) -> ListPageProtocol[dict]:
71+
) -> ListPageProtocolAsync[dict]:
7272
"""List the available datasets.
7373
7474
https://docs.apify.com/api/v2#/reference/datasets/dataset-collection/get-list-of-datasets

src/apify_client/clients/resource_clients/key_value_store_collection.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from apify_client.clients.base import ResourceCollectionClient, ResourceCollectionClientAsync
77

88
if TYPE_CHECKING:
9-
from apify_client.clients.base.resource_collection_client import ListPage, ListPageProtocol
9+
from apify_client.clients.base.resource_collection_client import ListPageProtocol, ListPageProtocolAsync
1010

1111

1212
class KeyValueStoreCollectionClient(ResourceCollectionClient):
@@ -23,7 +23,7 @@ def list(
2323
limit: int | None = None,
2424
offset: int | None = None,
2525
desc: bool | None = None,
26-
) -> ListPage[dict]:
26+
) -> ListPageProtocol[dict]:
2727
"""List the available key-value stores.
2828
2929
https://docs.apify.com/api/v2#/reference/key-value-stores/store-collection/get-list-of-key-value-stores
@@ -37,7 +37,7 @@ def list(
3737
Returns:
3838
The list of available key-value stores matching the specified filters.
3939
"""
40-
return self._list(unnamed=unnamed, limit=limit, offset=offset, desc=desc)
40+
return self._list_iterable(unnamed=unnamed, limit=limit, offset=offset, desc=desc)
4141

4242
def get_or_create(
4343
self,
@@ -73,7 +73,7 @@ def list(
7373
limit: int | None = None,
7474
offset: int | None = None,
7575
desc: bool | None = None,
76-
) -> ListPageProtocol[dict]:
76+
) -> ListPageProtocolAsync[dict]:
7777
"""List the available key-value stores.
7878
7979
https://docs.apify.com/api/v2#/reference/key-value-stores/store-collection/get-list-of-key-value-stores

0 commit comments

Comments
 (0)