diff --git a/docs/04_upgrading/upgrading_to_v4.md b/docs/04_upgrading/upgrading_to_v4.md index ffb422c2..f35717a4 100644 --- a/docs/04_upgrading/upgrading_to_v4.md +++ b/docs/04_upgrading/upgrading_to_v4.md @@ -9,3 +9,50 @@ This guide lists the breaking changes between Apify Python SDK v3.x and v4.0. ## Python 3.11+ required Support for Python 3.10 has been dropped. The Apify Python SDK v4.x now requires Python 3.11 or later — make sure your environment is on a compatible version before upgrading. + +## Removal of deprecated APIs + +Methods and arguments that had been deprecated in v3 are removed in v4. + +### `api_public_base_url` argument of storage clients + +The deprecated `api_public_base_url` argument has been removed from `ApifyDatasetClient` and `ApifyKeyValueStoreClient`. It had no effect already in v3 — passing it only emitted a `DeprecationWarning`. Drop it from your call sites. The public base URL is taken from `Configuration.api_public_base_url`, which is unchanged. + +Before (v3): + +```python +client = ApifyDatasetClient( + api_client=api_client, + api_public_base_url='https://api.apify.com', + lock=lock, +) +``` + +After (v4): + +```python +client = ApifyDatasetClient( + api_client=api_client, + lock=lock, +) +``` + +### `Actor.start` and `Actor.call`: `RemainingTime` + +The deprecated `RemainingTime` value of the `timeout` argument has been removed from `Actor.start()` and `Actor.call()`. Use `inherit` instead — the signature and behavior are identical. + +Before (v3): + +```python +run = await Actor.call('user/actor', timeout='RemainingTime') +``` + +After (v4): + +```python +run = await Actor.call('user/actor', timeout='inherit') +``` + +### Deprecated `Configuration` fields + +The deprecated `latest_sdk_version`, `log_format`, and `standby_port` fields have been removed from `Configuration`. Use `web_server_port` in place of `standby_port`; the other two have no replacement — SDK version checking is not supported for the Python SDK, and the log format should be adjusted in code instead. diff --git a/src/apify/_actor.py b/src/apify/_actor.py index e52483e2..3f4c0428 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -2,7 +2,6 @@ import asyncio import sys -import warnings from contextlib import suppress from datetime import UTC, datetime, timedelta from functools import cached_property @@ -867,7 +866,7 @@ async def start( max_total_charge_usd: Decimal | None = None, restart_on_error: bool | None = None, memory_mbytes: int | None = None, - timeout: timedelta | None | Literal['inherit', 'RemainingTime'] = None, + timeout: timedelta | None | Literal['inherit'] = None, force_permission_level: ActorPermissionLevel | None = None, wait_for_finish: int | None = None, webhooks: list[Webhook] | None = None, @@ -889,8 +888,8 @@ async def start( memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified in the default run configuration for the Actor. timeout: Optional timeout for the run, in seconds. By default, the run uses timeout specified in - the default run configuration for the Actor. Using `inherit` or `RemainingTime` will set timeout of the - other Actor to the time remaining from this Actor timeout. + the default run configuration for the Actor. Using `inherit` will set timeout of the other Actor + to the time remaining from this Actor timeout. force_permission_level: Override the Actor's permissions for this run. If not set, the Actor will run with permissions configured in the Actor settings. wait_for_finish: The maximum number of seconds the server waits for the run to finish. By default, @@ -911,22 +910,14 @@ async def start( else: serialized_webhooks = None - if timeout in {'inherit', 'RemainingTime'}: - if timeout == 'RemainingTime': - warnings.warn( - '`RemainingTime` is deprecated and will be removed in version 4.0.0. Use `inherit` instead.', - DeprecationWarning, - stacklevel=2, - ) + if timeout == 'inherit': actor_start_timeout = self._get_remaining_time() elif timeout is None: actor_start_timeout = None elif isinstance(timeout, timedelta): actor_start_timeout = timeout else: - raise ValueError( - f'Invalid timeout {timeout!r}: expected `None`, `"inherit"`, `"RemainingTime"`, or a `timedelta`.' - ) + raise ValueError(f'Invalid timeout {timeout!r}: expected `None`, `"inherit"`, or a `timedelta`.') api_result = await client.actor(actor_id).start( run_input=run_input, @@ -988,7 +979,7 @@ async def call( max_total_charge_usd: Decimal | None = None, restart_on_error: bool | None = None, memory_mbytes: int | None = None, - timeout: timedelta | None | Literal['inherit', 'RemainingTime'] = None, + timeout: timedelta | None | Literal['inherit'] = None, force_permission_level: ActorPermissionLevel | None = None, webhooks: list[Webhook] | None = None, wait: timedelta | None = None, @@ -1011,8 +1002,8 @@ async def call( memory_mbytes: Memory limit for the run, in megabytes. By default, the run uses a memory limit specified in the default run configuration for the Actor. timeout: Optional timeout for the run, in seconds. By default, the run uses timeout specified in - the default run configuration for the Actor. Using `inherit` or `RemainingTime` will set timeout of the - other Actor to the time remaining from this Actor timeout. + the default run configuration for the Actor. Using `inherit` will set timeout of the other Actor + to the time remaining from this Actor timeout. force_permission_level: Override the Actor's permissions for this run. If not set, the Actor will run with permissions configured in the Actor settings. webhooks: Optional webhooks (https://docs.apify.com/webhooks) associated with the Actor run, which can @@ -1036,23 +1027,14 @@ async def call( else: serialized_webhooks = None - if timeout in {'inherit', 'RemainingTime'}: - if timeout == 'RemainingTime': - warnings.warn( - '`RemainingTime` is deprecated and will be removed in version 4.0.0. Use `inherit` instead.', - DeprecationWarning, - stacklevel=2, - ) - + if timeout == 'inherit': actor_call_timeout = self._get_remaining_time() elif timeout is None: actor_call_timeout = None elif isinstance(timeout, timedelta): actor_call_timeout = timeout else: - raise ValueError( - f'Invalid timeout {timeout!r}: expected `None`, `"inherit"`, `"RemainingTime"`, or a `timedelta`.' - ) + raise ValueError(f'Invalid timeout {timeout!r}: expected `None`, `"inherit"`, or a `timedelta`.') api_result = await client.actor(actor_id).call( run_input=run_input, @@ -1450,7 +1432,7 @@ def _get_remaining_time(self) -> timedelta | None: return max(self.configuration.timeout_at - datetime.now(tz=UTC), timedelta(0)) self.log.warning( - 'Using `inherit` or `RemainingTime` argument is only possible when the Actor' + 'Using `inherit` argument is only possible when the Actor' ' is running on the Apify platform and when the timeout for the Actor run is set. ' f'{self.is_at_home()=}, {self.configuration.timeout_at=}' ) diff --git a/src/apify/_configuration.py b/src/apify/_configuration.py index 97dc2d05..9c476951 100644 --- a/src/apify/_configuration.py +++ b/src/apify/_configuration.py @@ -8,7 +8,7 @@ from typing import Annotated, Any, Self from pydantic import AliasChoices, BeforeValidator, Field, model_validator -from typing_extensions import TypedDict, deprecated +from typing_extensions import TypedDict from crawlee import service_locator from crawlee._utils.models import timedelta_ms @@ -273,22 +273,6 @@ class Configuration(CrawleeConfiguration): ), ] = False - latest_sdk_version: Annotated[ - str | None, - Field( - alias='apify_sdk_latest_version', - description='Specifies the most recent release version of the Apify SDK for Javascript. Used for ' - 'checking for updates.', - ), - deprecated('SDK version checking is not supported for the Python SDK'), - ] = None - - log_format: Annotated[ - str | None, - Field(alias='apify_log_format'), - deprecated('Adjust the log format in code instead'), - ] = None - max_paid_dataset_items: Annotated[ int | None, Field( @@ -386,15 +370,6 @@ class Configuration(CrawleeConfiguration): BeforeValidator(lambda val: val if val != '' else None), # We should accept empty environment variables as well ] = None - standby_port: Annotated[ - int, - Field( - alias='actor_standby_port', - description='TCP port for the Actor to start an HTTP server to receive messages in the Actor Standby mode', - ), - deprecated('Use `web_server_port` instead'), - ] = 4321 - standby_url: Annotated[ str, BeforeValidator(validate_http_url), diff --git a/src/apify/storage_clients/_apify/_dataset_client.py b/src/apify/storage_clients/_apify/_dataset_client.py index fc809646..ce538e02 100644 --- a/src/apify/storage_clients/_apify/_dataset_client.py +++ b/src/apify/storage_clients/_apify/_dataset_client.py @@ -1,7 +1,6 @@ from __future__ import annotations import asyncio -import warnings from logging import getLogger from typing import TYPE_CHECKING @@ -42,7 +41,6 @@ def __init__( self, *, api_client: DatasetClientAsync, - api_public_base_url: str, lock: asyncio.Lock, ) -> None: """Initialize a new instance. @@ -58,14 +56,6 @@ def __init__( self._lock = lock """A lock to ensure that only one operation is performed at a time.""" - if api_public_base_url: - # Remove in version 4.0, https://github.com/apify/apify-sdk-python/issues/635 - warnings.warn( - 'api_public_base_url argument is deprecated and will be removed in version 4.0.0', - DeprecationWarning, - stacklevel=2, - ) - @override async def get_metadata(self) -> DatasetMetadata: metadata = await self._api_client.get() @@ -114,7 +104,6 @@ async def open( dataset_client = cls( api_client=api_client, - api_public_base_url='', # Remove in version 4.0, https://github.com/apify/apify-sdk-python/issues/635 lock=asyncio.Lock(), ) diff --git a/src/apify/storage_clients/_apify/_key_value_store_client.py b/src/apify/storage_clients/_apify/_key_value_store_client.py index b422b464..cd11a1c2 100644 --- a/src/apify/storage_clients/_apify/_key_value_store_client.py +++ b/src/apify/storage_clients/_apify/_key_value_store_client.py @@ -1,7 +1,6 @@ from __future__ import annotations import asyncio -import warnings from logging import getLogger from typing import TYPE_CHECKING, Any @@ -30,7 +29,6 @@ def __init__( self, *, api_client: KeyValueStoreClientAsync, - api_public_base_url: str, lock: asyncio.Lock, ) -> None: """Initialize a new instance. @@ -43,14 +41,6 @@ def __init__( self._lock = lock """A lock to ensure that only one operation is performed at a time.""" - if api_public_base_url: - # Remove in version 4.0, https://github.com/apify/apify-sdk-python/issues/635 - warnings.warn( - 'api_public_base_url argument is deprecated and will be removed in version 4.0.0', - DeprecationWarning, - stacklevel=2, - ) - @override async def get_metadata(self) -> ApifyKeyValueStoreMetadata: metadata = await self._api_client.get() @@ -98,7 +88,6 @@ async def open( ) return cls( api_client=api_client, - api_public_base_url='', # Remove in version 4.0, https://github.com/apify/apify-sdk-python/issues/635 lock=asyncio.Lock(), ) diff --git a/tests/unit/actor/test_actor_helpers.py b/tests/unit/actor/test_actor_helpers.py index b90a5d13..bafee17a 100644 --- a/tests/unit/actor/test_actor_helpers.py +++ b/tests/unit/actor/test_actor_helpers.py @@ -2,7 +2,6 @@ import asyncio import logging -import warnings from datetime import UTC, datetime, timedelta from typing import TYPE_CHECKING @@ -267,21 +266,6 @@ async def test_remote_method_with_timedelta_timeout( assert kwargs.get('timeout_secs') == 120 -async def test_call_actor_with_remaining_time_deprecation( - apify_client_async_patcher: ApifyClientAsyncPatcher, fake_actor_run: dict -) -> None: - """Test that call() with RemainingTime emits deprecation warning.""" - apify_client_async_patcher.patch('actor', 'call', return_value=fake_actor_run) - - async with Actor: - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') - await Actor.call('some-actor-id', timeout='RemainingTime') - deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)] - assert len(deprecation_warnings) == 1 - assert 'RemainingTime' in str(deprecation_warnings[0].message) - - @pytest.mark.parametrize(('client_resource', 'client_method', 'actor_method_name', 'entity_id'), _ACTOR_REMOTE_METHODS) async def test_remote_method_with_invalid_timeout( apify_client_async_patcher: ApifyClientAsyncPatcher, @@ -321,7 +305,7 @@ async def test_get_remaining_time_warns_when_not_at_home(caplog: pytest.LogCaptu # Actor is not at home, so _get_remaining_time should return None and log warning result = Actor._get_remaining_time() assert result is None - assert any('inherit' in msg or 'RemainingTime' in msg for msg in caplog.messages) + assert any('inherit' in msg for msg in caplog.messages) async def test_get_remaining_time_clamps_negative_to_zero() -> None: diff --git a/tests/unit/storage_clients/test_apify_dataset_client.py b/tests/unit/storage_clients/test_apify_dataset_client.py index 5fd6b170..263d30e5 100644 --- a/tests/unit/storage_clients/test_apify_dataset_client.py +++ b/tests/unit/storage_clients/test_apify_dataset_client.py @@ -15,7 +15,6 @@ def _make_dataset_client(api_client: AsyncMock | None = None) -> tuple[ApifyData return ApifyDatasetClient( api_client=api_client, - api_public_base_url='', lock=asyncio.Lock(), ), api_client @@ -32,14 +31,3 @@ async def test_drop_calls_api_delete() -> None: client, api_client = _make_dataset_client() await client.drop() api_client.delete.assert_awaited_once() - - -async def test_deprecated_api_public_base_url() -> None: - """Test that passing api_public_base_url triggers deprecation warning.""" - api_client = AsyncMock() - with pytest.warns(DeprecationWarning, match='api_public_base_url argument is deprecated'): - ApifyDatasetClient( - api_client=api_client, - api_public_base_url='https://api.apify.com', - lock=asyncio.Lock(), - ) diff --git a/tests/unit/storage_clients/test_apify_kvs_client.py b/tests/unit/storage_clients/test_apify_kvs_client.py index 4e5b4c6b..8ccc36f7 100644 --- a/tests/unit/storage_clients/test_apify_kvs_client.py +++ b/tests/unit/storage_clients/test_apify_kvs_client.py @@ -19,7 +19,6 @@ def _make_kvs_client( return ApifyKeyValueStoreClient( api_client=api_client, - api_public_base_url='', lock=asyncio.Lock(), **kwargs, ), api_client @@ -119,14 +118,3 @@ async def test_purge_raises_not_implemented() -> None: client, _ = _make_kvs_client() with pytest.raises(NotImplementedError, match='Purging key-value stores is not supported'): await client.purge() - - -async def test_deprecated_api_public_base_url() -> None: - """Test that passing api_public_base_url triggers deprecation warning.""" - api_client = AsyncMock() - with pytest.warns(DeprecationWarning, match='api_public_base_url argument is deprecated'): - ApifyKeyValueStoreClient( - api_client=api_client, - api_public_base_url='https://api.apify.com', - lock=asyncio.Lock(), - )