From fdc829c68cb125264a425804f19e4e074a6656b1 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 8 Oct 2026 13:14:56 +0200 Subject: [PATCH 1/7] chore: bump apify-client to 3.2.2b11 --- pyproject.toml | 2 +- src/apify/storage_clients/_apify/_utils.py | 6 ++--- tests/integration/test_request_queue.py | 4 ++-- .../test_apify_request_queue_client.py | 22 +++++++++---------- uv.lock | 8 +++---- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8c6d53f08..1287f8b8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ keywords = [ "scraping", ] dependencies = [ - "apify-client[brotli]>=3.1.0,<4.0.0", + "apify-client[brotli]>=3.2.2b11,<4.0.0", "crawlee>=1.8.0,<2.0.0,!=1.10.1", "cachetools>=5.5.0", "cryptography>=42.0.0", diff --git a/src/apify/storage_clients/_apify/_utils.py b/src/apify/storage_clients/_apify/_utils.py index 21363431b..e66023fa3 100644 --- a/src/apify/storage_clients/_apify/_utils.py +++ b/src/apify/storage_clients/_apify/_utils.py @@ -14,8 +14,8 @@ if TYPE_CHECKING: from collections.abc import Iterable - from apify_client._models import HeadRequest, LockedHeadRequest - from apify_client._models import Request as ClientRequest + from apify_client._models import LockedRequestQueueHeadItem, RequestQueueHeadItem + from apify_client._models import RequestResource as ClientRequest from crawlee.storage_clients.models import AddRequestsResponse from apify import Configuration @@ -51,7 +51,7 @@ def hash_api_public_base_url_and_token(configuration: Configuration) -> str: return compute_short_hash(f'{configuration.api_public_base_url}{configuration.token}'.encode()) -def to_crawlee_request(client_request: ClientRequest | HeadRequest | LockedHeadRequest) -> Request: +def to_crawlee_request(client_request: ClientRequest | RequestQueueHeadItem | LockedRequestQueueHeadItem) -> Request: """Convert an Apify API client's `Request` model to a Crawlee's `Request` model. Args: diff --git a/tests/integration/test_request_queue.py b/tests/integration/test_request_queue.py index 6702937a3..b80a61b73 100644 --- a/tests/integration/test_request_queue.py +++ b/tests/integration/test_request_queue.py @@ -9,7 +9,7 @@ import pytest -from apify_client._models import BatchAddResult, RequestDraft +from apify_client._models import BatchAddResult, UnprocessedRequest from crawlee import service_locator from crawlee.crawlers import BasicCrawler @@ -1290,7 +1290,7 @@ async def test_request_queue_deduplication_unprocessed_requests( def return_unprocessed_requests(requests: list[dict], *_: Any, **__: Any) -> BatchAddResult: """Simulate API returning unprocessed requests.""" unprocessed_requests = [ - RequestDraft.model_construct( + UnprocessedRequest.model_construct( url=request['url'], unique_key=request['uniqueKey'], method=request['method'], diff --git a/tests/unit/storage_clients/test_apify_request_queue_client.py b/tests/unit/storage_clients/test_apify_request_queue_client.py index 4d086cb10..34d26a5d9 100644 --- a/tests/unit/storage_clients/test_apify_request_queue_client.py +++ b/tests/unit/storage_clients/test_apify_request_queue_client.py @@ -12,16 +12,16 @@ from apify_client._models import ( AddedRequest, BatchAddResult, - HeadRequest, - LockedHeadRequest, LockedRequestQueueHead, - RequestDraft, + LockedRequestQueueHeadItem, RequestLockInfo, RequestQueueHead, + RequestQueueHeadItem, RequestQueueStats, RequestRegistration, + UnprocessedRequest, ) -from apify_client._models import Request as ClientRequest +from apify_client._models import RequestResource as ClientRequest from crawlee.storage_clients.models import AddRequestsResponse, ProcessedRequest, RequestQueueMetadata from apify import Request @@ -66,7 +66,7 @@ def _batch_result( for request in processed ], unprocessed_requests=[ - RequestDraft.model_construct( + UnprocessedRequest.model_construct( unique_key=request.unique_key, url=request.url, method=request.method, @@ -589,9 +589,9 @@ async def test_shared_is_finished_does_not_refetch_requests_confirmed_in_an_unfi api_client.get_request.assert_awaited_once_with(straggler_id) -def _locked_item(request: Request, *, lock_expires_at: datetime) -> LockedHeadRequest: +def _locked_item(request: Request, *, lock_expires_at: datetime) -> LockedRequestQueueHeadItem: """Build a single locked head entry for `request` with the given lock expiry.""" - return LockedHeadRequest( + return LockedRequestQueueHeadItem( id=unique_key_to_request_id(request.unique_key), unique_key=request.unique_key, url=request.url, @@ -602,7 +602,7 @@ def _locked_item(request: Request, *, lock_expires_at: datetime) -> LockedHeadRe def _locked_head( - items: Sequence[LockedHeadRequest], + items: Sequence[LockedRequestQueueHeadItem], *, queue_has_locked_requests: bool = True, ) -> LockedRequestQueueHead: @@ -850,9 +850,9 @@ async def test_reclaim_request_frees_in_progress() -> None: assert second.unique_key == request.unique_key -def _head_item(request: Request) -> HeadRequest: +def _head_item(request: Request) -> RequestQueueHeadItem: """Build a `list_head` item for the given request.""" - return HeadRequest( + return RequestQueueHeadItem( id=unique_key_to_request_id(request.unique_key), unique_key=request.unique_key, url=request.url, @@ -861,7 +861,7 @@ def _head_item(request: Request) -> HeadRequest: ) -def _head(*items: HeadRequest) -> RequestQueueHead: +def _head(*items: RequestQueueHeadItem) -> RequestQueueHead: """Build a `list_head` response wrapping the given items.""" return RequestQueueHead( limit=200, diff --git a/uv.lock b/uv.lock index ee2b192df..f3236c790 100644 --- a/uv.lock +++ b/uv.lock @@ -225,7 +225,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "apify-client", extras = ["brotli"], specifier = ">=3.1.0,<4.0.0" }, + { name = "apify-client", extras = ["brotli"], specifier = ">=3.2.2b11,<4.0.0" }, { name = "cachetools", specifier = ">=5.5.0" }, { name = "crawlee", specifier = ">=1.8.0,!=1.10.1,<2.0.0" }, { name = "cryptography", specifier = ">=42.0.0" }, @@ -265,7 +265,7 @@ dev = [ [[package]] name = "apify-client" -version = "3.2.1" +version = "3.2.2b11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, @@ -274,9 +274,9 @@ dependencies = [ { name = "pydantic", extra = ["email"] }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/37/0e7374e97e7cdb0749f8a0b1e9d84b1036b0e08b1ff636fb140463246084/apify_client-3.2.1.tar.gz", hash = "sha256:604561af6eada8ca2ed4f3b6afb62f22e5bf6e00f8f4c46f6f51636301412215", size = 144669, upload-time = "2026-09-25T12:55:14.751Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/2a/c42e3a68e94b316b31a6c53b3e3c784996cbc6c98744098a3b136674cfd7/apify_client-3.2.2b11.tar.gz", hash = "sha256:ba9e893240d11e2a2da683bd1c79fda5e149bc738a1d5ca371b6da55445ddeea", size = 158694, upload-time = "2026-10-08T11:09:39.034Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/05/9dff8e2767335f159fb9506926888a990b3f3855dfbef34dada70be96762/apify_client-3.2.1-py3-none-any.whl", hash = "sha256:b464064ae14e1f96b0fc253529a093bcf4ccb8ff3112647c326d11011b2454ea", size = 160238, upload-time = "2026-09-25T12:55:12.808Z" }, + { url = "https://files.pythonhosted.org/packages/29/c4/5c150ac89fc7684846e679fc2f54e387a73d414ec18a32212977322a5f37/apify_client-3.2.2b11-py3-none-any.whl", hash = "sha256:9b43ee0316e2e96530c3cf05b1a93ffe8d9e50a30db121bbbb1d8ddbac7c49bc", size = 174392, upload-time = "2026-10-08T11:09:37.326Z" }, ] [package.optional-dependencies] From 25213337dc0c14732d950389a0c8bfc190cbb433 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 8 Oct 2026 13:27:28 +0200 Subject: [PATCH 2/7] chore: bump apify-client to 3.3.0 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1287f8b8f..0c7de4148 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ keywords = [ "scraping", ] dependencies = [ - "apify-client[brotli]>=3.2.2b11,<4.0.0", + "apify-client[brotli]>=3.3.0,<4.0.0", "crawlee>=1.8.0,<2.0.0,!=1.10.1", "cachetools>=5.5.0", "cryptography>=42.0.0", diff --git a/uv.lock b/uv.lock index f3236c790..d84116f92 100644 --- a/uv.lock +++ b/uv.lock @@ -225,7 +225,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "apify-client", extras = ["brotli"], specifier = ">=3.2.2b11,<4.0.0" }, + { name = "apify-client", extras = ["brotli"], specifier = ">=3.3.0,<4.0.0" }, { name = "cachetools", specifier = ">=5.5.0" }, { name = "crawlee", specifier = ">=1.8.0,!=1.10.1,<2.0.0" }, { name = "cryptography", specifier = ">=42.0.0" }, @@ -265,7 +265,7 @@ dev = [ [[package]] name = "apify-client" -version = "3.2.2b11" +version = "3.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, @@ -274,9 +274,9 @@ dependencies = [ { name = "pydantic", extra = ["email"] }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/2a/c42e3a68e94b316b31a6c53b3e3c784996cbc6c98744098a3b136674cfd7/apify_client-3.2.2b11.tar.gz", hash = "sha256:ba9e893240d11e2a2da683bd1c79fda5e149bc738a1d5ca371b6da55445ddeea", size = 158694, upload-time = "2026-10-08T11:09:39.034Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/eb/e99684fd036fd63f9b3f95ebeafa0e706e02b7c7066ac091444683cb0471/apify_client-3.3.0.tar.gz", hash = "sha256:4adb1d2d424ceb72f7382bd51b9372a687e5cc1ea54f1bc252228e0eb9316687", size = 158619, upload-time = "2026-10-08T11:22:35.225Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/c4/5c150ac89fc7684846e679fc2f54e387a73d414ec18a32212977322a5f37/apify_client-3.2.2b11-py3-none-any.whl", hash = "sha256:9b43ee0316e2e96530c3cf05b1a93ffe8d9e50a30db121bbbb1d8ddbac7c49bc", size = 174392, upload-time = "2026-10-08T11:09:37.326Z" }, + { url = "https://files.pythonhosted.org/packages/a4/30/0ef4c0a26ef68c491f502dbe149478f97ec68d56bcc097d3505ee6cd660d/apify_client-3.3.0-py3-none-any.whl", hash = "sha256:57edb1382e44b54a58f0ca2f62fe884c09994fcfad94ff327bcbed490022d708", size = 174351, upload-time = "2026-10-08T11:22:33.826Z" }, ] [package.optional-dependencies] From 440f25dca0baa95302407fb5e85fb25b644e1ab6 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 8 Oct 2026 15:29:41 +0200 Subject: [PATCH 3/7] feat: add max_total_charge_usd to Actor.start_task and Actor.call_task --- src/apify/_actor.py | 6 ++++++ tests/unit/actor/test_actor_helpers.py | 27 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index 1bb9857c1..f9dbb3f29 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -1161,6 +1161,7 @@ async def start_task( *, build: str | None = None, max_items: int | None = None, + max_total_charge_usd: Decimal | None = None, restart_on_error: bool | None = None, memory_mbytes: int | None = None, timeout: timedelta | Literal['inherit'] | None = None, @@ -1183,6 +1184,7 @@ async def start_task( the run uses the build specified in the default run configuration for the Actor (typically latest). max_items: Maximum number of dataset items you are charged for, for pay-per-result Actors. It caps the charge, not the output, so the run can return fewer or more items than this. + max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. restart_on_error: If true, the Task run process will be restarted whenever it exits with a non-zero status code. memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified @@ -1203,6 +1205,7 @@ async def start_task( task_input=task_input, build=build, max_items=max_items, + max_total_charge_usd=max_total_charge_usd, restart_on_error=restart_on_error, memory_mbytes=memory_mbytes, run_timeout=self._resolve_run_timeout(timeout), @@ -1217,6 +1220,7 @@ async def call_task( *, build: str | None = None, max_items: int | None = None, + max_total_charge_usd: Decimal | None = None, restart_on_error: bool | None = None, memory_mbytes: int | None = None, timeout: timedelta | Literal['inherit'] | None = None, @@ -1239,6 +1243,7 @@ async def call_task( the run uses the build specified in the default run configuration for the Actor (typically latest). max_items: Maximum number of dataset items you are charged for, for pay-per-result Actors. It caps the charge, not the output, so the run can return fewer or more items than this. + max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. restart_on_error: If true, the Task run process will be restarted whenever it exits with a non-zero status code. memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified @@ -1261,6 +1266,7 @@ async def call_task( task_input=task_input, build=build, max_items=max_items, + max_total_charge_usd=max_total_charge_usd, restart_on_error=restart_on_error, memory_mbytes=memory_mbytes, run_timeout=self._resolve_run_timeout(timeout), diff --git a/tests/unit/actor/test_actor_helpers.py b/tests/unit/actor/test_actor_helpers.py index a5b678358..7f1833d4d 100644 --- a/tests/unit/actor/test_actor_helpers.py +++ b/tests/unit/actor/test_actor_helpers.py @@ -158,6 +158,33 @@ async def test_max_items_forwarded_to_client( assert apify_client_async_patcher.calls[client_resource][client_method][0][1]['max_items'] == 42 +@pytest.mark.parametrize( + ('client_resource', 'client_method', 'sdk_method'), + [ + pytest.param('actor', 'start', 'start', id='start'), + pytest.param('actor', 'call', 'call', id='call'), + pytest.param('task', 'start', 'start_task', id='start_task'), + pytest.param('task', 'call', 'call_task', id='call_task'), + ], +) +async def test_max_total_charge_usd_forwarded_to_client( + apify_client_async_patcher: ApifyClientAsyncPatcher, + fake_actor_run: Run, + client_resource: str, + client_method: str, + sdk_method: str, +) -> None: + """`max_total_charge_usd` passed to any of the run-starting helpers reaches the API client.""" + apify_client_async_patcher.patch(client_resource, client_method, return_value=fake_actor_run) + + async with Actor: + await getattr(Actor, sdk_method)('some-id', max_total_charge_usd=Decimal('2.5')) + + assert apify_client_async_patcher.calls[client_resource][client_method][0][1]['max_total_charge_usd'] == Decimal( + '2.5' + ) + + async def test_abort_actor_run(apify_client_async_patcher: ApifyClientAsyncPatcher, fake_actor_run: Run) -> None: apify_client_async_patcher.patch('run', 'abort', return_value=fake_actor_run) run_id = 'some-run-id' From c351095ce607439d36107e14241df7e7cb4e41c9 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 8 Oct 2026 15:31:23 +0200 Subject: [PATCH 4/7] feat: deprecate max_items in favor of max_total_charge_usd --- src/apify/_actor.py | 62 +++++++++++++++++---- tests/unit/actor/test_actor_helpers.py | 77 ++++++++++++++++++++++---- 2 files changed, 119 insertions(+), 20 deletions(-) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index f9dbb3f29..27a72d6b0 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -66,6 +66,17 @@ _ensure_context = ensure_context('_active') +def _warn_max_items_deprecated() -> None: + warnings.warn( + '`max_items` is deprecated and will be removed in version 5.0.0. It only limits the charge of Actors that ' + 'charge per dataset item and has no effect on other Actors. To cap the cost of a run, use ' + '`max_total_charge_usd`, which works for every pay-per-event Actor.', + FutureWarning, + # This helper, the SDK method, and its `ensure_context` wrapper sit between the warning and the caller. + stacklevel=4, + ) + + @docs_name('Actor') @docs_group('Actor') class _ActorType: @@ -956,8 +967,11 @@ async def start( content_type: The content type of the input. build: Specifies the Actor build to run. It can be either a build tag or build number. By default, the run uses the build specified in the default run configuration for the Actor (typically latest). - max_items: Maximum number of dataset items you are charged for, for pay-per-result Actors. It caps the - charge, not the output, so the run can return fewer or more items than this. + max_items: Deprecated, use `max_total_charge_usd` instead. Will be removed in version 5.0.0. A legacy limit + from the pay-per-result pricing model. When `max_total_charge_usd` is not set, the platform turns it + into a charge limit for a pay-per-event Actor, priced by the Actor's `apify-default-dataset-item` event + or, failing that, by its last pay-per-result price. Otherwise it has no effect. It caps the charge, not + the output, so the run can return more items than this. max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. restart_on_error: If true, the Actor run process will be restarted whenever it exits with a non-zero status code. @@ -975,6 +989,9 @@ async def start( Returns: Info about the started Actor run """ + if max_items is not None: + _warn_max_items_deprecated() + client = self.new_client(token=token) if token else self.apify_client actor_client = client.actor(actor_id) @@ -1056,8 +1073,12 @@ async def resurrect( timeout: New timeout for the resurrected run. By default, the resurrected run uses the same timeout as before. Using `inherit` will set timeout of the resurrected run to the time remaining from this Actor timeout. - max_items: Maximum number of items that the resurrected pay-per-result run will return. By default, the - resurrected run uses the same limit as before. The limit can only be increased. + max_items: Deprecated, use `max_total_charge_usd` instead. Will be removed in version 5.0.0. A legacy limit + from the pay-per-result pricing model. When `max_total_charge_usd` is not set, the platform turns it + into a charge limit for a pay-per-event Actor, priced by the Actor's `apify-default-dataset-item` event + or, failing that, by its last pay-per-result price. Otherwise it has no effect. It caps the charge, not + the output, so the run can return more items than this. + By default, the resurrected run uses the same limit as before. The limit can only be increased. max_total_charge_usd: Maximum cost for the resurrected pay-per-event run in USD. By default, the resurrected run uses the same limit as before. The limit can only be increased. restart_on_error: If true, the resurrected run process will be restarted whenever it exits with a non-zero @@ -1066,6 +1087,9 @@ async def resurrect( Returns: Info about the resurrected Actor run. """ + if max_items is not None: + _warn_max_items_deprecated() + client = self.new_client(token=token) if token else self.apify_client return await client.run(run_id).resurrect( @@ -1107,8 +1131,11 @@ async def call( content_type: The content type of the input. build: Specifies the Actor build to run. It can be either a build tag or build number. By default, the run uses the build specified in the default run configuration for the Actor (typically latest). - max_items: Maximum number of dataset items you are charged for, for pay-per-result Actors. It caps the - charge, not the output, so the run can return fewer or more items than this. + max_items: Deprecated, use `max_total_charge_usd` instead. Will be removed in version 5.0.0. A legacy limit + from the pay-per-result pricing model. When `max_total_charge_usd` is not set, the platform turns it + into a charge limit for a pay-per-event Actor, priced by the Actor's `apify-default-dataset-item` event + or, failing that, by its last pay-per-result price. Otherwise it has no effect. It caps the charge, not + the output, so the run can return more items than this. max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. restart_on_error: If true, the Actor run process will be restarted whenever it exits with a non-zero status code. @@ -1130,6 +1157,9 @@ async def call( Returns: Info about the started Actor run. """ + if max_items is not None: + _warn_max_items_deprecated() + client = self.new_client(token=token) if token else self.apify_client actor_client = client.actor(actor_id) @@ -1182,8 +1212,11 @@ async def start_task( token: The Apify API token to use for this request (defaults to the `APIFY_TOKEN` environment variable). build: Specifies the Actor build to run. It can be either a build tag or build number. By default, the run uses the build specified in the default run configuration for the Actor (typically latest). - max_items: Maximum number of dataset items you are charged for, for pay-per-result Actors. It caps the - charge, not the output, so the run can return fewer or more items than this. + max_items: Deprecated, use `max_total_charge_usd` instead. Will be removed in version 5.0.0. A legacy limit + from the pay-per-result pricing model. When `max_total_charge_usd` is not set, the platform turns it + into a charge limit for a pay-per-event Actor, priced by the Actor's `apify-default-dataset-item` event + or, failing that, by its last pay-per-result price. Otherwise it has no effect. It caps the charge, not + the output, so the run can return more items than this. max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. restart_on_error: If true, the Task run process will be restarted whenever it exits with a non-zero status code. @@ -1199,6 +1232,9 @@ async def start_task( Returns: Info about the started Actor run. """ + if max_items is not None: + _warn_max_items_deprecated() + client = self.new_client(token=token) if token else self.apify_client task_client = client.task(task_id) return await task_client.start( @@ -1241,8 +1277,11 @@ async def call_task( token: The Apify API token to use for this request (defaults to the `APIFY_TOKEN` environment variable). build: Specifies the Actor build to run. It can be either a build tag or build number. By default, the run uses the build specified in the default run configuration for the Actor (typically latest). - max_items: Maximum number of dataset items you are charged for, for pay-per-result Actors. It caps the - charge, not the output, so the run can return fewer or more items than this. + max_items: Deprecated, use `max_total_charge_usd` instead. Will be removed in version 5.0.0. A legacy limit + from the pay-per-result pricing model. When `max_total_charge_usd` is not set, the platform turns it + into a charge limit for a pay-per-event Actor, priced by the Actor's `apify-default-dataset-item` event + or, failing that, by its last pay-per-result price. Otherwise it has no effect. It caps the charge, not + the output, so the run can return more items than this. max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. restart_on_error: If true, the Task run process will be restarted whenever it exits with a non-zero status code. @@ -1259,6 +1298,9 @@ async def call_task( Returns: Info about the started Actor run. """ + if max_items is not None: + _warn_max_items_deprecated() + client = self.new_client(token=token) if token else self.apify_client task_client = client.task(task_id) diff --git a/tests/unit/actor/test_actor_helpers.py b/tests/unit/actor/test_actor_helpers.py index 7f1833d4d..06948df0c 100644 --- a/tests/unit/actor/test_actor_helpers.py +++ b/tests/unit/actor/test_actor_helpers.py @@ -2,6 +2,7 @@ import asyncio import logging +import warnings from datetime import UTC, datetime, timedelta from decimal import Decimal from typing import TYPE_CHECKING @@ -153,11 +154,66 @@ async def test_max_items_forwarded_to_client( apify_client_async_patcher.patch(client_resource, client_method, return_value=fake_actor_run) async with Actor: - await getattr(Actor, sdk_method)('some-id', max_items=42) + with pytest.warns(FutureWarning): + await getattr(Actor, sdk_method)('some-id', max_items=42) assert apify_client_async_patcher.calls[client_resource][client_method][0][1]['max_items'] == 42 +@pytest.mark.parametrize( + ('client_resource', 'client_method', 'sdk_method'), + [ + pytest.param('actor', 'start', 'start', id='start'), + pytest.param('actor', 'call', 'call', id='call'), + pytest.param('task', 'start', 'start_task', id='start_task'), + pytest.param('task', 'call', 'call_task', id='call_task'), + pytest.param('run', 'resurrect', 'resurrect', id='resurrect'), + ], +) +async def test_max_items_warns_deprecated( + apify_client_async_patcher: ApifyClientAsyncPatcher, + fake_actor_run: Run, + client_resource: str, + client_method: str, + sdk_method: str, +) -> None: + """Passing `max_items` emits a `FutureWarning` that recommends `max_total_charge_usd` and points at the caller.""" + apify_client_async_patcher.patch(client_resource, client_method, return_value=fake_actor_run) + + async with Actor: + with pytest.warns(FutureWarning, match='`max_total_charge_usd`') as record: + await getattr(Actor, sdk_method)('some-id', max_items=42) + + assert len(record) == 1 + assert record[0].filename == __file__ + + +@pytest.mark.parametrize( + ('client_resource', 'client_method', 'sdk_method'), + [ + pytest.param('actor', 'start', 'start', id='start'), + pytest.param('actor', 'call', 'call', id='call'), + pytest.param('task', 'start', 'start_task', id='start_task'), + pytest.param('task', 'call', 'call_task', id='call_task'), + pytest.param('run', 'resurrect', 'resurrect', id='resurrect'), + ], +) +async def test_no_max_items_warning_without_max_items( + apify_client_async_patcher: ApifyClientAsyncPatcher, + fake_actor_run: Run, + client_resource: str, + client_method: str, + sdk_method: str, +) -> None: + """The `max_items` deprecation warning is not emitted when `max_items` is not passed.""" + apify_client_async_patcher.patch(client_resource, client_method, return_value=fake_actor_run) + + async with Actor: + with warnings.catch_warnings(): + warnings.simplefilter('error', FutureWarning) + await getattr(Actor, sdk_method)('some-id') + + @pytest.mark.parametrize( ('client_resource', 'client_method', 'sdk_method'), [ @@ -202,15 +258,16 @@ async def test_resurrect_actor_run(apify_client_async_patcher: ApifyClientAsyncP run_id = 'some-run-id' async with Actor: - run = await Actor.resurrect( - run_id, - build='beta', - memory_mbytes=1024, - timeout=timedelta(seconds=120), - max_items=100, - max_total_charge_usd=Decimal('2.5'), - restart_on_error=True, - ) + with pytest.warns(FutureWarning): + run = await Actor.resurrect( + run_id, + build='beta', + memory_mbytes=1024, + timeout=timedelta(seconds=120), + max_items=100, + max_total_charge_usd=Decimal('2.5'), + restart_on_error=True, + ) assert run is fake_actor_run calls = apify_client_async_patcher.calls['run']['resurrect'] From 377bf3a8de40b838bfb8404a394106f14d5ee7a1 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 8 Oct 2026 15:39:14 +0200 Subject: [PATCH 5/7] test: simplify max_total_charge_usd forwarding assertion --- tests/unit/actor/test_actor_helpers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit/actor/test_actor_helpers.py b/tests/unit/actor/test_actor_helpers.py index 7f1833d4d..a30b2fb73 100644 --- a/tests/unit/actor/test_actor_helpers.py +++ b/tests/unit/actor/test_actor_helpers.py @@ -180,9 +180,8 @@ async def test_max_total_charge_usd_forwarded_to_client( async with Actor: await getattr(Actor, sdk_method)('some-id', max_total_charge_usd=Decimal('2.5')) - assert apify_client_async_patcher.calls[client_resource][client_method][0][1]['max_total_charge_usd'] == Decimal( - '2.5' - ) + kwargs = apify_client_async_patcher.calls[client_resource][client_method][0][1] + assert kwargs['max_total_charge_usd'] == Decimal('2.5') async def test_abort_actor_run(apify_client_async_patcher: ApifyClientAsyncPatcher, fake_actor_run: Run) -> None: From 00d43b68966c4e895147d1125f7894f5bd86112a Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 8 Oct 2026 15:41:07 +0200 Subject: [PATCH 6/7] docs: clarify max_items is the charge cap for pay-per-result Actors --- src/apify/_actor.py | 52 ++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index 27a72d6b0..8d4a88c02 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -967,11 +967,11 @@ async def start( content_type: The content type of the input. build: Specifies the Actor build to run. It can be either a build tag or build number. By default, the run uses the build specified in the default run configuration for the Actor (typically latest). - max_items: Deprecated, use `max_total_charge_usd` instead. Will be removed in version 5.0.0. A legacy limit - from the pay-per-result pricing model. When `max_total_charge_usd` is not set, the platform turns it - into a charge limit for a pay-per-event Actor, priced by the Actor's `apify-default-dataset-item` event - or, failing that, by its last pay-per-result price. Otherwise it has no effect. It caps the charge, not - the output, so the run can return more items than this. + max_items: Deprecated, use `max_total_charge_usd` instead. Will be removed in version 5.0.0. Caps the charge + of a pay-per-result Actor at this many dataset items. For a pay-per-event Actor without + `max_total_charge_usd`, the platform turns it into a charge limit, priced by the Actor's + `apify-default-dataset-item` event or, failing that, by its last pay-per-result price. Otherwise it has + no effect. It caps the charge, not the output, so the run can return more items than this. max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. restart_on_error: If true, the Actor run process will be restarted whenever it exits with a non-zero status code. @@ -1073,12 +1073,12 @@ async def resurrect( timeout: New timeout for the resurrected run. By default, the resurrected run uses the same timeout as before. Using `inherit` will set timeout of the resurrected run to the time remaining from this Actor timeout. - max_items: Deprecated, use `max_total_charge_usd` instead. Will be removed in version 5.0.0. A legacy limit - from the pay-per-result pricing model. When `max_total_charge_usd` is not set, the platform turns it - into a charge limit for a pay-per-event Actor, priced by the Actor's `apify-default-dataset-item` event - or, failing that, by its last pay-per-result price. Otherwise it has no effect. It caps the charge, not - the output, so the run can return more items than this. - By default, the resurrected run uses the same limit as before. The limit can only be increased. + max_items: Deprecated, use `max_total_charge_usd` instead. Will be removed in version 5.0.0. Caps the charge + of a pay-per-result Actor at this many dataset items. For a pay-per-event Actor without + `max_total_charge_usd`, the platform turns it into a charge limit, priced by the Actor's + `apify-default-dataset-item` event or, failing that, by its last pay-per-result price. Otherwise it has + no effect. It caps the charge, not the output, so the run can return more items than this. By default, + the resurrected run uses the same limit as before. The limit can only be increased. max_total_charge_usd: Maximum cost for the resurrected pay-per-event run in USD. By default, the resurrected run uses the same limit as before. The limit can only be increased. restart_on_error: If true, the resurrected run process will be restarted whenever it exits with a non-zero @@ -1131,11 +1131,11 @@ async def call( content_type: The content type of the input. build: Specifies the Actor build to run. It can be either a build tag or build number. By default, the run uses the build specified in the default run configuration for the Actor (typically latest). - max_items: Deprecated, use `max_total_charge_usd` instead. Will be removed in version 5.0.0. A legacy limit - from the pay-per-result pricing model. When `max_total_charge_usd` is not set, the platform turns it - into a charge limit for a pay-per-event Actor, priced by the Actor's `apify-default-dataset-item` event - or, failing that, by its last pay-per-result price. Otherwise it has no effect. It caps the charge, not - the output, so the run can return more items than this. + max_items: Deprecated, use `max_total_charge_usd` instead. Will be removed in version 5.0.0. Caps the charge + of a pay-per-result Actor at this many dataset items. For a pay-per-event Actor without + `max_total_charge_usd`, the platform turns it into a charge limit, priced by the Actor's + `apify-default-dataset-item` event or, failing that, by its last pay-per-result price. Otherwise it has + no effect. It caps the charge, not the output, so the run can return more items than this. max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. restart_on_error: If true, the Actor run process will be restarted whenever it exits with a non-zero status code. @@ -1212,11 +1212,11 @@ async def start_task( token: The Apify API token to use for this request (defaults to the `APIFY_TOKEN` environment variable). build: Specifies the Actor build to run. It can be either a build tag or build number. By default, the run uses the build specified in the default run configuration for the Actor (typically latest). - max_items: Deprecated, use `max_total_charge_usd` instead. Will be removed in version 5.0.0. A legacy limit - from the pay-per-result pricing model. When `max_total_charge_usd` is not set, the platform turns it - into a charge limit for a pay-per-event Actor, priced by the Actor's `apify-default-dataset-item` event - or, failing that, by its last pay-per-result price. Otherwise it has no effect. It caps the charge, not - the output, so the run can return more items than this. + max_items: Deprecated, use `max_total_charge_usd` instead. Will be removed in version 5.0.0. Caps the charge + of a pay-per-result Actor at this many dataset items. For a pay-per-event Actor without + `max_total_charge_usd`, the platform turns it into a charge limit, priced by the Actor's + `apify-default-dataset-item` event or, failing that, by its last pay-per-result price. Otherwise it has + no effect. It caps the charge, not the output, so the run can return more items than this. max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. restart_on_error: If true, the Task run process will be restarted whenever it exits with a non-zero status code. @@ -1277,11 +1277,11 @@ async def call_task( token: The Apify API token to use for this request (defaults to the `APIFY_TOKEN` environment variable). build: Specifies the Actor build to run. It can be either a build tag or build number. By default, the run uses the build specified in the default run configuration for the Actor (typically latest). - max_items: Deprecated, use `max_total_charge_usd` instead. Will be removed in version 5.0.0. A legacy limit - from the pay-per-result pricing model. When `max_total_charge_usd` is not set, the platform turns it - into a charge limit for a pay-per-event Actor, priced by the Actor's `apify-default-dataset-item` event - or, failing that, by its last pay-per-result price. Otherwise it has no effect. It caps the charge, not - the output, so the run can return more items than this. + max_items: Deprecated, use `max_total_charge_usd` instead. Will be removed in version 5.0.0. Caps the charge + of a pay-per-result Actor at this many dataset items. For a pay-per-event Actor without + `max_total_charge_usd`, the platform turns it into a charge limit, priced by the Actor's + `apify-default-dataset-item` event or, failing that, by its last pay-per-result price. Otherwise it has + no effect. It caps the charge, not the output, so the run can return more items than this. max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. restart_on_error: If true, the Task run process will be restarted whenever it exits with a non-zero status code. From ab83502061a2b6e3cdc3204780dbaeed68b515b5 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Fri, 9 Oct 2026 13:14:16 +0200 Subject: [PATCH 7/7] docs: clarify that max_total_charge_usd applies to every Actor --- src/apify/_actor.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index f9dbb3f29..11ebe78da 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -958,7 +958,8 @@ async def start( the run uses the build specified in the default run configuration for the Actor (typically latest). max_items: Maximum number of dataset items you are charged for, for pay-per-result Actors. It caps the charge, not the output, so the run can return fewer or more items than this. - max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. + max_total_charge_usd: A limit on the total charged amount, in USD. Once the run exceeds it, the platform + aborts the run, which takes a few seconds, so the final charge can slightly overshoot the limit. restart_on_error: If true, the Actor run process will be restarted whenever it exits with a non-zero status code. memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified @@ -1058,8 +1059,10 @@ async def resurrect( timeout. max_items: Maximum number of items that the resurrected pay-per-result run will return. By default, the resurrected run uses the same limit as before. The limit can only be increased. - max_total_charge_usd: Maximum cost for the resurrected pay-per-event run in USD. By default, the resurrected - run uses the same limit as before. The limit can only be increased. + max_total_charge_usd: A limit on the total charged amount of the resurrected run, in USD. Once the run + exceeds it, the platform aborts the run, which takes a few seconds, so the final charge can slightly + overshoot the limit. By default, the resurrected run uses the same limit as before. The limit can only + be increased. restart_on_error: If true, the resurrected run process will be restarted whenever it exits with a non-zero status code. By default, the resurrected run uses the same setting as before. @@ -1109,7 +1112,8 @@ async def call( the run uses the build specified in the default run configuration for the Actor (typically latest). max_items: Maximum number of dataset items you are charged for, for pay-per-result Actors. It caps the charge, not the output, so the run can return fewer or more items than this. - max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. + max_total_charge_usd: A limit on the total charged amount, in USD. Once the run exceeds it, the platform + aborts the run, which takes a few seconds, so the final charge can slightly overshoot the limit. restart_on_error: If true, the Actor run process will be restarted whenever it exits with a non-zero status code. memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified @@ -1184,7 +1188,8 @@ async def start_task( the run uses the build specified in the default run configuration for the Actor (typically latest). max_items: Maximum number of dataset items you are charged for, for pay-per-result Actors. It caps the charge, not the output, so the run can return fewer or more items than this. - max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. + max_total_charge_usd: A limit on the total charged amount, in USD. Once the run exceeds it, the platform + aborts the run, which takes a few seconds, so the final charge can slightly overshoot the limit. restart_on_error: If true, the Task run process will be restarted whenever it exits with a non-zero status code. memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified @@ -1243,7 +1248,8 @@ async def call_task( the run uses the build specified in the default run configuration for the Actor (typically latest). max_items: Maximum number of dataset items you are charged for, for pay-per-result Actors. It caps the charge, not the output, so the run can return fewer or more items than this. - max_total_charge_usd: A limit on the total charged amount for pay-per-event Actors. + max_total_charge_usd: A limit on the total charged amount, in USD. Once the run exceeds it, the platform + aborts the run, which takes a few seconds, so the final charge can slightly overshoot the limit. restart_on_error: If true, the Task run process will be restarted whenever it exits with a non-zero status code. memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified