apify-client 3.2.1b1__tar.gz → 3.2.1b2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/CHANGELOG.md +4 -0
  2. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/PKG-INFO +1 -1
  3. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/pyproject.toml +1 -1
  4. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/pyproject.toml.orig +1 -1
  5. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_pagination.py +54 -26
  6. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/dataset.py +11 -9
  7. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/CONTRIBUTING.md +0 -0
  8. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/LICENSE +0 -0
  9. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/README.md +0 -0
  10. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/__init__.py +0 -0
  11. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_apify_client.py +0 -0
  12. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_client_registry.py +0 -0
  13. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_consts.py +0 -0
  14. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_docs.py +0 -0
  15. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_literals.py +0 -0
  16. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_logging.py +0 -0
  17. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_models.py +0 -0
  18. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/__init__.py +0 -0
  19. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/_resource_client.py +0 -0
  20. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/actor.py +0 -0
  21. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/actor_collection.py +0 -0
  22. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/actor_env_var.py +0 -0
  23. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/actor_env_var_collection.py +0 -0
  24. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/actor_version.py +0 -0
  25. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/actor_version_collection.py +0 -0
  26. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/build.py +0 -0
  27. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/build_collection.py +0 -0
  28. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/dataset_collection.py +0 -0
  29. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/key_value_store.py +0 -0
  30. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/key_value_store_collection.py +0 -0
  31. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/log.py +0 -0
  32. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/request_queue.py +0 -0
  33. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/request_queue_collection.py +0 -0
  34. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/run.py +0 -0
  35. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/run_collection.py +0 -0
  36. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/schedule.py +0 -0
  37. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/schedule_collection.py +0 -0
  38. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/store_collection.py +0 -0
  39. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/task.py +0 -0
  40. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/task_collection.py +0 -0
  41. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/user.py +0 -0
  42. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/webhook.py +0 -0
  43. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/webhook_collection.py +0 -0
  44. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/webhook_dispatch.py +0 -0
  45. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_resource_clients/webhook_dispatch_collection.py +0 -0
  46. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_statistics.py +0 -0
  47. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_status_message_watcher.py +0 -0
  48. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_streamed_log.py +0 -0
  49. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_typeddicts.py +0 -0
  50. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_utils/__init__.py +0 -0
  51. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_utils/crypto.py +0 -0
  52. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_utils/encoding.py +0 -0
  53. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_utils/errors.py +0 -0
  54. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_utils/http.py +0 -0
  55. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_utils/time.py +0 -0
  56. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/_utils/try_import.py +0 -0
  57. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/errors.py +0 -0
  58. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/http_clients/__init__.py +0 -0
  59. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/http_clients/_base.py +0 -0
  60. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/http_clients/_httpx2.py +0 -0
  61. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/http_clients/_impit.py +0 -0
  62. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/http_compressors/__init__.py +0 -0
  63. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/http_compressors/_base.py +0 -0
  64. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/http_compressors/_brotli.py +0 -0
  65. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/http_compressors/_gzip.py +0 -0
  66. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/http_compressors/_resolve.py +0 -0
  67. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/py.typed +0 -0
  68. {apify_client-3.2.1b1 → apify_client-3.2.1b2}/src/apify_client/types.py +0 -0
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
5
5
  <!-- git-cliff-unreleased-start -->
6
6
  ## 3.2.1 - **not yet released**
7
7
 
8
+ ### 🐛 Bug Fixes
9
+
10
+ - Stop dataset iterators from skipping items when unwind is used ([#1059](https://github.com/apify/apify-client-python/pull/1059)) ([8e30197](https://github.com/apify/apify-client-python/commit/8e301977ecb80b7c01251cacf071a3f6201469ba)) by [@vdusek](https://github.com/vdusek), closes [#1058](https://github.com/apify/apify-client-python/issues/1058)
11
+
8
12
 
9
13
  <!-- git-cliff-unreleased-end -->
10
14
  ## [3.2.0](https://github.com/apify/apify-client-python/releases/tag/v3.2.0) (2026-09-03)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apify_client
3
- Version: 3.2.1b1
3
+ Version: 3.2.1b2
4
4
  Summary: Apify API client for Python
5
5
  Keywords: apify,api,client,automation,crawling,scraping
6
6
  Author: Apify Technologies s.r.o.
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "apify_client"
7
- version = "3.2.1b1"
7
+ version = "3.2.1b2"
8
8
  description = "Apify API client for Python"
9
9
  license = "Apache-2.0"
10
10
  license-files = ["LICENSE"]
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "apify_client"
7
- version = "3.2.1b1"
7
+ version = "3.2.1b2"
8
8
  description = "Apify API client for Python"
9
9
  authors = [{ name = "Apify Technologies s.r.o.", email = "support@apify.com" }]
10
10
  license = "Apache-2.0"
@@ -19,9 +19,10 @@ The value of 1000 keeps backwards compatibility with the previous fixed cache si
19
19
  class HasItems(Protocol[T]):
20
20
  """Structural contract for a single page of results from a paginated API endpoint.
21
21
 
22
- Implementations must expose `items`. They may optionally expose `count` - the number of items scanned by the API for
23
- this page, which can exceed `len(items)` when filters drop items from the response. The iterator helpers consult
24
- `count` opportunistically via `getattr` for offset bookkeeping and fall back to `len(items)` when it is absent.
22
+ Implementations must expose `items`. They may optionally expose `count` - the number of rows the API scanned to
23
+ produce this page, which `len(items)` can land below (filters drop items) or above (`unwind` splits one row into
24
+ several items). The iterator helpers consult `count` opportunistically via `getattr` for offset bookkeeping and
25
+ fall back to `len(items)` when it is absent.
25
26
  """
26
27
 
27
28
  items: list[T]
@@ -38,20 +39,19 @@ def get_items_iterator(
38
39
 
39
40
  The `callback` is invoked lazily to fetch each page from the API. It must accept `limit` and `offset` keyword
40
41
  arguments and return an object whose `items` attribute is a list. If the object also exposes a `count` attribute, it
41
- is used for offset bookkeeping (the Apify API's `count` reflects items scanned, which can exceed items returned when
42
- filters are applied).
42
+ is used for offset bookkeeping - `_page_scanned_rows` describes how the next offset is derived.
43
43
 
44
- Iteration stops when a page scans no items (`count` is `0`, or `items` is empty when `count` is absent) or when the
45
- user-requested `limit` is reached. A page can scan items while returning none - filters like `clean` drop items from
46
- `items` but still count toward `count` - so terminating on scanned rather than returned items keeps the iterator
47
- advancing across fully-filtered pages. The `total` field is intentionally not consulted, because it can change
48
- between calls.
44
+ Iteration stops when a page scans no rows or when the user-requested `limit` is reached. A page can scan rows while
45
+ returning no items - filters like `clean` drop items from `items` but still count toward `count` - so terminating on
46
+ scanned rather than returned rows keeps the iterator advancing across fully-filtered pages. The `total` field is
47
+ intentionally not consulted, because it can change between calls.
49
48
 
50
49
  Args:
51
50
  callback: Function returning a single page of items.
52
- limit: Maximum total number of items to yield across all pages. `None` or `0` means no limit.
51
+ limit: Maximum total number of rows scanned across all pages. On the dataset items endpoint `unwind` can
52
+ turn one row into several items, so more items than this can be yielded. `None` or `0` means no limit.
53
53
  offset: Starting offset for the first page.
54
- chunk_size: Maximum number of items requested per API call. `None` or `0` lets the API decide.
54
+ chunk_size: Per-page cap, sent to the API as its `limit`. `None` or `0` lets the API decide.
55
55
  """
56
56
  effective_chunk = chunk_size or 0
57
57
  initial_offset = offset or 0
@@ -59,13 +59,14 @@ def get_items_iterator(
59
59
  fetched_items = 0
60
60
 
61
61
  while True:
62
+ page_limit = _next_page_limit(initial_limit, fetched_items, effective_chunk)
62
63
  current_page = callback(
63
- limit=_next_page_limit(initial_limit, fetched_items, effective_chunk),
64
+ limit=page_limit,
64
65
  offset=initial_offset + fetched_items,
65
66
  )
66
67
  yield from current_page.items
67
68
 
68
- page_scanned = max(getattr(current_page, 'count', 0), len(current_page.items))
69
+ page_scanned = _page_scanned_rows(current_page, page_limit)
69
70
  fetched_items += page_scanned
70
71
 
71
72
  if not page_scanned or (initial_limit and fetched_items >= initial_limit):
@@ -89,14 +90,15 @@ async def get_items_iterator_async(
89
90
  fetched_items = 0
90
91
 
91
92
  while True:
93
+ page_limit = _next_page_limit(initial_limit, fetched_items, effective_chunk)
92
94
  current_page = await callback(
93
- limit=_next_page_limit(initial_limit, fetched_items, effective_chunk),
95
+ limit=page_limit,
94
96
  offset=initial_offset + fetched_items,
95
97
  )
96
98
  for item in current_page.items:
97
99
  yield item
98
100
 
99
- page_scanned = max(getattr(current_page, 'count', 0), len(current_page.items))
101
+ page_scanned = _page_scanned_rows(current_page, page_limit)
100
102
  fetched_items += page_scanned
101
103
 
102
104
  if not page_scanned or (initial_limit and fetched_items >= initial_limit):
@@ -126,20 +128,30 @@ def get_cursor_iterator(
126
128
  limit: int | None = None,
127
129
  chunk_size: int | None = None,
128
130
  ) -> Iterator[KeyValueStoreKey] | Iterator[Request]:
129
- """Yield individual items from cursor-paginated API responses.
131
+ """Yield individual items from a cursor-paginated API response.
132
+
133
+ This iterator supports the two API responses that use cursor pagination. `ListOfKeys` is used for key-value store
134
+ keys, while `ListOfRequests` is used for request queue requests.
135
+
136
+ Pagination continues until either:
137
+
138
+ - the API returns no next cursor, or
139
+ - the requested `limit` is reached.
140
+
141
+ An empty page does not explicitly stop the iteration. In practice, both supported endpoints return a next cursor
142
+ only when the current page contains items, so an empty page always has a `None` cursor and naturally ends the
143
+ iteration.
130
144
 
131
- Cursor pagination is restricted to the two API responses that expose it: `ListOfKeys` (for key-value store keys) and
132
- `ListOfRequests` (for request queue requests). Iteration ends when the next cursor is `None` or the user-requested
133
- `limit` is reached. Emptiness alone does not stop iteration: server-side filters (such as the request-queue state
134
- `filter`) can drop every item on a page while a live cursor still points at more data, so termination relies on the
135
- cursor, not on whether a page returned items. Unlike offset responses, cursor responses expose no scanned-item
136
- `count`, so `count` cannot be used to detect a fully-filtered page here.
145
+ The endpoints determine the next cursor differently:
146
+
147
+ - For key-value store keys, the cursor is the last key returned on the current page.
148
+ - For request queue requests, a cursor is returned only when the current page is full.
137
149
 
138
150
  Args:
139
- callback: Function returning a single page of items. Receives `cursor` and `limit` kwargs.
140
- cursor: Value of the cursor for the first request, or `None` to start from the beginning.
151
+ callback: Function that returns one page of items and accepts `cursor` and `limit` keyword arguments.
152
+ cursor: Cursor to use for the first request. If `None`, iteration starts from the beginning.
141
153
  limit: Maximum total number of items to yield across all pages.
142
- chunk_size: Maximum number of items requested per API call.
154
+ chunk_size: Maximum number of items to request in a single API call.
143
155
  """
144
156
  effective_chunk = chunk_size or 0
145
157
  initial_limit = limit or 0
@@ -218,3 +230,19 @@ def _next_page_limit(initial_limit: int, fetched_items: int, effective_chunk: in
218
230
  if not effective_chunk:
219
231
  return remaining
220
232
  return min(remaining, effective_chunk)
233
+
234
+
235
+ def _page_scanned_rows(page: HasItems[T], requested_limit: int) -> int:
236
+ """Compute how far the offset advances past `page`, in dataset rows.
237
+
238
+ Neither reported number is right on its own. `count` follows the rows the API scanned, but it is derived from a
239
+ dataset's item count, which is incremented by a throttled write and so lags a fresh push. `len(items)` counts the
240
+ items the API shaped out of those rows: filters (`clean`, `skip_empty`, `skip_hidden`) drop some, and `unwind`
241
+ splits one row into several. The larger of the two absorbs a `count` that lags behind the items returned, and
242
+ capping it at the rows the call asked for keeps an unwound page from advancing past rows the next call would then
243
+ never read. The cap is a valid bound because the endpoint applies the `limit` it is sent verbatim; on a page
244
+ covering fewer rows than that, the advance can still overshoot into rows a concurrent push appends afterwards. A
245
+ `requested_limit` of `0` means the call sent no limit, leaving the advance unbounded.
246
+ """
247
+ scanned_rows = max(getattr(page, 'count', 0), len(page.items))
248
+ return min(scanned_rows, requested_limit) if requested_limit else scanned_rows
@@ -40,7 +40,7 @@ class DatasetItemsPage:
40
40
  """The offset of the first item in this page."""
41
41
 
42
42
  count: int
43
- """Number of items in this page."""
43
+ """Number of dataset rows the API scanned for this page, or the number of items returned when that is larger."""
44
44
 
45
45
  limit: int
46
46
  """The limit that was used for this request."""
@@ -204,8 +204,8 @@ class DatasetClient(ResourceClient):
204
204
  items=items,
205
205
  total=int(response.headers['x-apify-pagination-total']),
206
206
  offset=int(response.headers['x-apify-pagination-offset']),
207
- # x-apify-pagination-count returns count of processed items, not count of returned items
208
- # This makes difference when items were filtered using hidden/empty
207
+ # The header counts the rows the API scanned, which `unwind` and a lagging dataset item count can
208
+ # both leave below the number of items returned.
209
209
  count=max(int(response.headers['x-apify-pagination-count']), len(items)),
210
210
  # API returns 999999999999 when no limit is used
211
211
  limit=int(response.headers['x-apify-pagination-limit']),
@@ -237,7 +237,8 @@ class DatasetClient(ResourceClient):
237
237
 
238
238
  Args:
239
239
  offset: Number of items that should be skipped at the start. The default value is 0.
240
- limit: Maximum number of items to return. By default there is no limit.
240
+ limit: Maximum number of dataset rows to scan. Fewer items are yielded when filters drop some, more
241
+ when `unwind` splits a row into several. By default there is no limit.
241
242
  desc: By default, results are returned in the same order as they were stored. To reverse the order,
242
243
  set this parameter to True.
243
244
  clean: If True, returns only non-empty items and skips hidden fields (i.e. fields starting with
@@ -260,7 +261,7 @@ class DatasetClient(ResourceClient):
260
261
  skip_hidden: If True, then hidden fields are skipped from the output, i.e. fields starting with
261
262
  the # character.
262
263
  signature: Signature used to access the items.
263
- chunk_size: Maximum number of items requested per API call when iterating across pages.
264
+ chunk_size: Maximum number of dataset rows requested per API call when iterating across pages.
264
265
  timeout: Timeout for the API HTTP request.
265
266
 
266
267
  Yields:
@@ -763,8 +764,8 @@ class DatasetClientAsync(ResourceClientAsync):
763
764
  items=items,
764
765
  total=int(response.headers['x-apify-pagination-total']),
765
766
  offset=int(response.headers['x-apify-pagination-offset']),
766
- # x-apify-pagination-count returns count of processed items, not count of returned items
767
- # This makes difference when items were filtered using hidden/empty
767
+ # The header counts the rows the API scanned, which `unwind` and a lagging dataset item count can
768
+ # both leave below the number of items returned.
768
769
  count=max(int(response.headers['x-apify-pagination-count']), len(items)),
769
770
  # API returns 999999999999 when no limit is used
770
771
  limit=int(response.headers['x-apify-pagination-limit']),
@@ -796,7 +797,8 @@ class DatasetClientAsync(ResourceClientAsync):
796
797
 
797
798
  Args:
798
799
  offset: Number of items that should be skipped at the start. The default value is 0.
799
- limit: Maximum number of items to return. By default there is no limit.
800
+ limit: Maximum number of dataset rows to scan. Fewer items are yielded when filters drop some, more
801
+ when `unwind` splits a row into several. By default there is no limit.
800
802
  desc: By default, results are returned in the same order as they were stored. To reverse the order,
801
803
  set this parameter to True.
802
804
  clean: If True, returns only non-empty items and skips hidden fields (i.e. fields starting with
@@ -819,7 +821,7 @@ class DatasetClientAsync(ResourceClientAsync):
819
821
  skip_hidden: If True, then hidden fields are skipped from the output, i.e. fields starting with
820
822
  the # character.
821
823
  signature: Signature used to access the items.
822
- chunk_size: Maximum number of items requested per API call when iterating across pages.
824
+ chunk_size: Maximum number of dataset rows requested per API call when iterating across pages.
823
825
  timeout: Timeout for the API HTTP request.
824
826
 
825
827
  Yields:
File without changes
File without changes