Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/04_upgrading/upgrading_to_v4.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
40 changes: 11 additions & 29 deletions src/apify/_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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=}'
)
Expand Down
27 changes: 1 addition & 26 deletions src/apify/_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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),
Expand Down
11 changes: 0 additions & 11 deletions src/apify/storage_clients/_apify/_dataset_client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

import asyncio
import warnings
from logging import getLogger
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -42,7 +41,6 @@ def __init__(
self,
*,
api_client: DatasetClientAsync,
api_public_base_url: str,
lock: asyncio.Lock,
) -> None:
"""Initialize a new instance.
Expand All @@ -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()
Expand Down Expand Up @@ -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(),
)

Expand Down
11 changes: 0 additions & 11 deletions src/apify/storage_clients/_apify/_key_value_store_client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

import asyncio
import warnings
from logging import getLogger
from typing import TYPE_CHECKING, Any

Expand Down Expand Up @@ -30,7 +29,6 @@ def __init__(
self,
*,
api_client: KeyValueStoreClientAsync,
api_public_base_url: str,
lock: asyncio.Lock,
) -> None:
"""Initialize a new instance.
Expand All @@ -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()
Expand Down Expand Up @@ -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(),
)

Expand Down
18 changes: 1 addition & 17 deletions tests/unit/actor/test_actor_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import asyncio
import logging
import warnings
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
12 changes: 0 additions & 12 deletions tests/unit/storage_clients/test_apify_dataset_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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(),
)
12 changes: 0 additions & 12 deletions tests/unit/storage_clients/test_apify_kvs_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ def _make_kvs_client(

return ApifyKeyValueStoreClient(
api_client=api_client,
api_public_base_url='',
lock=asyncio.Lock(),
**kwargs,
), api_client
Expand Down Expand Up @@ -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(),
)