Skip to content
Merged
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
38 changes: 38 additions & 0 deletions docs/04_upgrading/upgrading_to_v3.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,44 @@ except NotFoundError:

Direct `.get()` also now swallows every 404 regardless of the `error.type` string in the response body (previously only `record-not-found` and `record-or-token-not-found` types were swallowed). If your code needs to distinguish between "resource missing" and "404 with an unexpected type", inspect `.type` on a caught <ApiLink to="class/NotFoundError">`NotFoundError`</ApiLink> from a non-`.get()` call path.

## Keyword-only arguments for secondary parameters

Several methods and utility functions had additional `*` separators inserted into their signatures, so optional/secondary parameters can no longer be passed positionally. The "subject" arguments (e.g. `key` on KVS record methods, `event_name` on `charge()`) remain positional; only the parameters that follow them are affected.

### Affected APIs

<ApiLink to="class/KeyValueStoreClient">`KeyValueStoreClient`</ApiLink> / <ApiLink to="class/KeyValueStoreClientAsync">`KeyValueStoreClientAsync`</ApiLink>:

- `get_record(key, *, signature=None, timeout='long')`
- `get_record_as_bytes(key, *, signature=None, timeout='long')`
- `stream_record(key, *, signature=None, timeout='long')`
- `set_record(key, value, *, content_type=None, timeout='long')`

<ApiLink to="class/RunClient">`RunClient`</ApiLink> / <ApiLink to="class/RunClientAsync">`RunClientAsync`</ApiLink>:

- `charge(event_name, *, count=1, idempotency_key=None, timeout='short')`
- `get_status_message_watcher(*, to_logger=None, check_period=..., timeout='long')`

<ApiLink to="class/ApifyApiError">`ApifyApiError`</ApiLink> constructor:

- `ApifyApiError(response, attempt, *, method='GET')`

### Migration

Before (v2):

```python
client.key_value_store('my-store').set_record('my-key', {'data': 1}, 'application/json')
client.run('my-run').charge('my-event', 5, 'my-idempotency-key')
```

After (v3):

```python
client.key_value_store('my-store').set_record('my-key', {'data': 1}, content_type='application/json')
client.run('my-run').charge('my-event', count=5, idempotency_key='my-idempotency-key')
```

## Snake_case `sort_by` values on `actors().list()`

The `sort_by` parameter of <ApiLink to="class/ActorCollectionClient#list">`ActorCollectionClient.list()`</ApiLink> and <ApiLink to="class/ActorCollectionClientAsync#list">`ActorCollectionClientAsync.list()`</ApiLink> now accepts pythonic snake_case values instead of the raw camelCase values used by the API.
Expand Down
5 changes: 3 additions & 2 deletions src/apify_client/_http_clients/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def _parse_params(params: dict[str, Any] | None) -> dict[str, Any] | None:

return parsed_params

def _compute_timeout(self, timeout: Timeout, attempt: int) -> int | float | None:
def _compute_timeout(self, timeout: Timeout, *, attempt: int) -> int | float | None:
"""Resolve a timeout tier and compute the timeout for a request attempt with exponential increase.

For `no_timeout`, returns `None` to indicate no timeout. For tier literals and explicit `timedelta` values,
Expand Down Expand Up @@ -197,6 +197,7 @@ def _compute_timeout(self, timeout: Timeout, attempt: int) -> int | float | None

def _prepare_request_call(
self,
*,
headers: dict[str, str] | None = None,
params: dict[str, Any] | None = None,
data: str | bytes | bytearray | None = None,
Expand All @@ -221,7 +222,7 @@ def _prepare_request_call(

return (headers, self._parse_params(params), data)

def _build_url_with_params(self, url: str, params: dict[str, Any] | None = None) -> str:
def _build_url_with_params(self, url: str, *, params: dict[str, Any] | None = None) -> str:
"""Build a URL with query parameters appended. List values are expanded into multiple key=value pairs."""
if not params:
return url
Expand Down
22 changes: 16 additions & 6 deletions src/apify_client/_http_clients/_impit.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,12 @@ def call(

self._statistics.calls += 1

prepared_headers, prepared_params, content = self._prepare_request_call(headers, params, data, json)
prepared_headers, prepared_params, content = self._prepare_request_call(
headers=headers,
params=params,
data=data,
json=json,
)

return self._retry_with_exp_backoff(
lambda stop_retrying, attempt: self._make_request(
Expand Down Expand Up @@ -198,12 +203,12 @@ def _make_request(
self._statistics.requests += 1

try:
url_with_params = self._build_url_with_params(url, params)
url_with_params = self._build_url_with_params(url, params=params)

# Impit treats timeout=None as "use client default (30s)", not "no timeout".
# Use a large value (24 hours) to effectively disable the timeout.
# This can be removed once impit updates its behaviour: https://github.com/apify/impit/issues/401
computed_timeout = self._compute_timeout(timeout, attempt)
computed_timeout = self._compute_timeout(timeout, attempt=attempt)
impit_timeout = 86_400 if computed_timeout is None else computed_timeout

response = self._impit_client.request(
Expand Down Expand Up @@ -384,7 +389,12 @@ async def call(

self._statistics.calls += 1

prepared_headers, prepared_params, content = self._prepare_request_call(headers, params, data, json)
prepared_headers, prepared_params, content = self._prepare_request_call(
headers=headers,
params=params,
data=data,
json=json,
)

return await self._retry_with_exp_backoff(
lambda stop_retrying, attempt: self._make_request(
Expand Down Expand Up @@ -440,12 +450,12 @@ async def _make_request(
self._statistics.requests += 1

try:
url_with_params = self._build_url_with_params(url, params)
url_with_params = self._build_url_with_params(url, params=params)

# Impit treats timeout=None as "use client default (30s)", not "no timeout".
# Use a large value (24 hours) to effectively disable the timeout.
# This can be removed once impit updates its behaviour: https://github.com/apify/impit/issues/401
computed_timeout = self._compute_timeout(timeout, attempt)
computed_timeout = self._compute_timeout(timeout, attempt=attempt)
impit_timeout = 86_400 if computed_timeout is None else computed_timeout

response = await self._impit_async_client.request(
Expand Down
8 changes: 4 additions & 4 deletions src/apify_client/_resource_clients/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ def start(
Returns:
The run object.
"""
run_input, content_type = encode_key_value_store_record_value(run_input, content_type)
run_input, content_type = encode_key_value_store_record_value(run_input, content_type=content_type)

request_params = self._build_params(
build=build,
Expand Down Expand Up @@ -543,7 +543,7 @@ def validate_input(
Returns:
True if the input is valid, else raise an exception with validation error details.
"""
run_input, content_type = encode_key_value_store_record_value(run_input, content_type)
run_input, content_type = encode_key_value_store_record_value(run_input, content_type=content_type)

self._http_client.call(
url=self._build_url('validate-input'),
Expand Down Expand Up @@ -762,7 +762,7 @@ async def start(
Returns:
The run object.
"""
run_input, content_type = encode_key_value_store_record_value(run_input, content_type)
run_input, content_type = encode_key_value_store_record_value(run_input, content_type=content_type)

request_params = self._build_params(
build=build,
Expand Down Expand Up @@ -1043,7 +1043,7 @@ async def validate_input(
Returns:
True if the input is valid, else raise an exception with validation error details.
"""
run_input, content_type = encode_key_value_store_record_value(run_input, content_type)
run_input, content_type = encode_key_value_store_record_value(run_input, content_type=content_type)

await self._http_client.call(
url=self._build_url('validate-input'),
Expand Down
8 changes: 4 additions & 4 deletions src/apify_client/_resource_clients/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,8 +714,8 @@ def create_items_public_url(

if dataset and dataset.url_signing_secret_key:
signature = create_storage_content_signature(
resource_id=dataset.id,
url_signing_secret_key=dataset.url_signing_secret_key,
dataset.id,
dataset.url_signing_secret_key,
expires_in=expires_in,
)
request_params['signature'] = signature
Expand Down Expand Up @@ -1292,8 +1292,8 @@ async def create_items_public_url(

if dataset and dataset.url_signing_secret_key:
signature = create_storage_content_signature(
resource_id=dataset.id,
url_signing_secret_key=dataset.url_signing_secret_key,
dataset.id,
dataset.url_signing_secret_key,
expires_in=expires_in,
)
request_params['signature'] = signature
Expand Down
28 changes: 14 additions & 14 deletions src/apify_client/_resource_clients/key_value_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ def iterate_keys(

exclusive_start_key = current_keys_page.next_exclusive_start_key

def get_record(self, key: str, signature: str | None = None, *, timeout: Timeout = 'long') -> dict | None:
def get_record(self, key: str, *, signature: str | None = None, timeout: Timeout = 'long') -> dict | None:
"""Retrieve the given record from the key-value store.

https://docs.apify.com/api/v2#/reference/key-value-stores/record/get-record
Expand Down Expand Up @@ -290,7 +290,7 @@ def record_exists(self, key: str, *, timeout: Timeout = 'long') -> bool:

return response.status_code == HTTPStatus.OK

def get_record_as_bytes(self, key: str, signature: str | None = None, *, timeout: Timeout = 'long') -> dict | None:
def get_record_as_bytes(self, key: str, *, signature: str | None = None, timeout: Timeout = 'long') -> dict | None:
"""Retrieve the given record from the key-value store, without parsing it.

https://docs.apify.com/api/v2#/reference/key-value-stores/record/get-record
Expand Down Expand Up @@ -324,7 +324,7 @@ def get_record_as_bytes(self, key: str, signature: str | None = None, *, timeout

@contextmanager
def stream_record(
self, key: str, signature: str | None = None, *, timeout: Timeout = 'long'
self, key: str, *, signature: str | None = None, timeout: Timeout = 'long'
) -> Iterator[dict | None]:
"""Retrieve the given record from the key-value store, as a stream.

Expand Down Expand Up @@ -365,8 +365,8 @@ def set_record(
self,
key: str,
value: Any,
content_type: str | None = None,
*,
content_type: str | None = None,
timeout: Timeout = 'long',
) -> None:
"""Set a value to the given record in the key-value store.
Expand All @@ -379,7 +379,7 @@ def set_record(
content_type: The content type of the saved value.
timeout: Timeout for the API HTTP request.
"""
value, content_type = encode_key_value_store_record_value(value, content_type)
value, content_type = encode_key_value_store_record_value(value, content_type=content_type)

headers = {'content-type': content_type}

Expand Down Expand Up @@ -482,8 +482,8 @@ def create_keys_public_url(

if metadata and metadata.url_signing_secret_key:
signature = create_storage_content_signature(
resource_id=metadata.id,
url_signing_secret_key=metadata.url_signing_secret_key,
metadata.id,
metadata.url_signing_secret_key,
expires_in=expires_in,
)
request_params['signature'] = signature
Expand Down Expand Up @@ -662,7 +662,7 @@ async def iterate_keys(

exclusive_start_key = current_keys_page.next_exclusive_start_key

async def get_record(self, key: str, signature: str | None = None, *, timeout: Timeout = 'long') -> dict | None:
async def get_record(self, key: str, *, signature: str | None = None, timeout: Timeout = 'long') -> dict | None:
"""Retrieve the given record from the key-value store.

https://docs.apify.com/api/v2#/reference/key-value-stores/record/get-record
Expand Down Expand Up @@ -722,7 +722,7 @@ async def record_exists(self, key: str, *, timeout: Timeout = 'long') -> bool:
return response.status_code == HTTPStatus.OK

async def get_record_as_bytes(
self, key: str, signature: str | None = None, *, timeout: Timeout = 'long'
self, key: str, *, signature: str | None = None, timeout: Timeout = 'long'
) -> dict | None:
"""Retrieve the given record from the key-value store, without parsing it.

Expand Down Expand Up @@ -757,7 +757,7 @@ async def get_record_as_bytes(

@asynccontextmanager
async def stream_record(
self, key: str, signature: str | None = None, *, timeout: Timeout = 'long'
self, key: str, *, signature: str | None = None, timeout: Timeout = 'long'
) -> AsyncIterator[dict | None]:
"""Retrieve the given record from the key-value store, as a stream.

Expand Down Expand Up @@ -798,8 +798,8 @@ async def set_record(
self,
key: str,
value: Any,
content_type: str | None = None,
*,
content_type: str | None = None,
timeout: Timeout = 'long',
) -> None:
"""Set a value to the given record in the key-value store.
Expand All @@ -812,7 +812,7 @@ async def set_record(
content_type: The content type of the saved value.
timeout: Timeout for the API HTTP request.
"""
value, content_type = encode_key_value_store_record_value(value, content_type)
value, content_type = encode_key_value_store_record_value(value, content_type=content_type)

headers = {'content-type': content_type}

Expand Down Expand Up @@ -915,8 +915,8 @@ async def create_keys_public_url(

if metadata and metadata.url_signing_secret_key:
signature = create_storage_content_signature(
resource_id=metadata.id,
url_signing_secret_key=metadata.url_signing_secret_key,
metadata.id,
metadata.url_signing_secret_key,
expires_in=expires_in,
)
request_params['signature'] = signature
Expand Down
5 changes: 4 additions & 1 deletion src/apify_client/_resource_clients/request_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,7 @@ async def delete_request_lock(

async def _batch_add_requests_worker(
self,
*,
queue: asyncio.Queue[Iterable[dict]],
request_params: dict,
timeout: Timeout,
Expand Down Expand Up @@ -992,7 +993,9 @@ async def batch_add_requests(
async with asyncio.TaskGroup() as tg:
workers = [
tg.create_task(
self._batch_add_requests_worker(asyncio_queue, request_params, timeout),
self._batch_add_requests_worker(
queue=asyncio_queue, request_params=request_params, timeout=timeout
),
name=f'batch_add_requests_worker_{i}',
)
for i in range(max_parallel)
Expand Down
10 changes: 6 additions & 4 deletions src/apify_client/_resource_clients/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def metamorph(
Returns:
The Actor run data.
"""
run_input, content_type = encode_key_value_store_record_value(run_input, content_type)
run_input, content_type = encode_key_value_store_record_value(run_input, content_type=content_type)

safe_target_actor_id = to_safe_id(target_actor_id)

Expand Down Expand Up @@ -375,6 +375,7 @@ def get_streamed_log(
def charge(
self,
event_name: str,
*,
count: int = 1,
idempotency_key: str | None = None,
timeout: Timeout = 'short',
Expand Down Expand Up @@ -419,9 +420,9 @@ def charge(

def get_status_message_watcher(
self,
*,
to_logger: logging.Logger | None = None,
check_period: timedelta = timedelta(seconds=1),
*,
timeout: Timeout = 'long',
) -> StatusMessageWatcher:
"""Get `StatusMessageWatcher` instance that can be used to redirect status and status messages to logs.
Expand Down Expand Up @@ -608,7 +609,7 @@ async def metamorph(
Returns:
The Actor run data.
"""
run_input, content_type = encode_key_value_store_record_value(run_input, content_type)
run_input, content_type = encode_key_value_store_record_value(run_input, content_type=content_type)

safe_target_actor_id = to_safe_id(target_actor_id)

Expand Down Expand Up @@ -801,6 +802,7 @@ async def get_streamed_log(
async def charge(
self,
event_name: str,
*,
count: int = 1,
idempotency_key: str | None = None,
timeout: Timeout = 'short',
Expand Down Expand Up @@ -845,9 +847,9 @@ async def charge(

async def get_status_message_watcher(
self,
*,
to_logger: logging.Logger | None = None,
check_period: timedelta = timedelta(seconds=1),
*,
timeout: Timeout = 'long',
) -> StatusMessageWatcherAsync:
"""Get `StatusMessageWatcher` instance that can be used to redirect status and status messages to logs.
Expand Down
3 changes: 2 additions & 1 deletion src/apify_client/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def catch_not_found_for_resource_or_throw(exc: ApifyApiError, resource_id: str |
catch_not_found_or_throw(exc)


def encode_key_value_store_record_value(value: Any, content_type: str | None = None) -> tuple[Any, str]:
def encode_key_value_store_record_value(value: Any, *, content_type: str | None = None) -> tuple[Any, str]:
"""Encode a value for storage in a key-value store record.

Args:
Expand Down Expand Up @@ -227,6 +227,7 @@ def create_hmac_signature(secret_key: str, message: str) -> str:
def create_storage_content_signature(
resource_id: str,
url_signing_secret_key: str,
*,
expires_in: timedelta | None = None,
version: int = 0,
) -> str:
Expand Down
Loading