Skip to content

Commit 4ca99fd

Browse files
authored
refactor!: Mark secondary arguments as keyword-only (#766)
## Summary Reshapes function/method signatures across the public API so that secondary parameters must be passed as keyword arguments. Primary "subject" arguments (e.g. `key`, `value`, `event_name`, `response`) stay positional. Affected APIs: - `KeyValueStoreClient.{get_record, get_record_as_bytes, stream_record, set_record}` (sync + async) — `signature` / `content_type` are now keyword-only. - `RunClient.{charge, get_status_message_watcher}` (sync + async) — `count`, `idempotency_key`, `to_logger`, `check_period`, `timeout` are now keyword-only. - `ApifyApiError(response, attempt, *, method='GET')` — `method` is now keyword-only. - Several internal helpers received the same treatment for consistency. ## Why Keyword-only parameters at API boundaries make call sites self-documenting and prevent breakage when new options are added between existing arguments.
1 parent cec286c commit 4ca99fd

19 files changed

Lines changed: 125 additions & 75 deletions

docs/04_upgrading/upgrading_to_v3.mdx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,44 @@ except NotFoundError:
249249

250250
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.
251251

252+
## Keyword-only arguments for secondary parameters
253+
254+
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.
255+
256+
### Affected APIs
257+
258+
<ApiLink to="class/KeyValueStoreClient">`KeyValueStoreClient`</ApiLink> / <ApiLink to="class/KeyValueStoreClientAsync">`KeyValueStoreClientAsync`</ApiLink>:
259+
260+
- `get_record(key, *, signature=None, timeout='long')`
261+
- `get_record_as_bytes(key, *, signature=None, timeout='long')`
262+
- `stream_record(key, *, signature=None, timeout='long')`
263+
- `set_record(key, value, *, content_type=None, timeout='long')`
264+
265+
<ApiLink to="class/RunClient">`RunClient`</ApiLink> / <ApiLink to="class/RunClientAsync">`RunClientAsync`</ApiLink>:
266+
267+
- `charge(event_name, *, count=1, idempotency_key=None, timeout='short')`
268+
- `get_status_message_watcher(*, to_logger=None, check_period=..., timeout='long')`
269+
270+
<ApiLink to="class/ApifyApiError">`ApifyApiError`</ApiLink> constructor:
271+
272+
- `ApifyApiError(response, attempt, *, method='GET')`
273+
274+
### Migration
275+
276+
Before (v2):
277+
278+
```python
279+
client.key_value_store('my-store').set_record('my-key', {'data': 1}, 'application/json')
280+
client.run('my-run').charge('my-event', 5, 'my-idempotency-key')
281+
```
282+
283+
After (v3):
284+
285+
```python
286+
client.key_value_store('my-store').set_record('my-key', {'data': 1}, content_type='application/json')
287+
client.run('my-run').charge('my-event', count=5, idempotency_key='my-idempotency-key')
288+
```
289+
252290
## Snake_case `sort_by` values on `actors().list()`
253291

254292
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.

src/apify_client/_http_clients/_base.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def _parse_params(params: dict[str, Any] | None) -> dict[str, Any] | None:
167167

168168
return parsed_params
169169

170-
def _compute_timeout(self, timeout: Timeout, attempt: int) -> int | float | None:
170+
def _compute_timeout(self, timeout: Timeout, *, attempt: int) -> int | float | None:
171171
"""Resolve a timeout tier and compute the timeout for a request attempt with exponential increase.
172172
173173
For `no_timeout`, returns `None` to indicate no timeout. For tier literals and explicit `timedelta` values,
@@ -197,6 +197,7 @@ def _compute_timeout(self, timeout: Timeout, attempt: int) -> int | float | None
197197

198198
def _prepare_request_call(
199199
self,
200+
*,
200201
headers: dict[str, str] | None = None,
201202
params: dict[str, Any] | None = None,
202203
data: str | bytes | bytearray | None = None,
@@ -221,7 +222,7 @@ def _prepare_request_call(
221222

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

224-
def _build_url_with_params(self, url: str, params: dict[str, Any] | None = None) -> str:
225+
def _build_url_with_params(self, url: str, *, params: dict[str, Any] | None = None) -> str:
225226
"""Build a URL with query parameters appended. List values are expanded into multiple key=value pairs."""
226227
if not params:
227228
return url

src/apify_client/_http_clients/_impit.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,12 @@ def call(
142142

143143
self._statistics.calls += 1
144144

145-
prepared_headers, prepared_params, content = self._prepare_request_call(headers, params, data, json)
145+
prepared_headers, prepared_params, content = self._prepare_request_call(
146+
headers=headers,
147+
params=params,
148+
data=data,
149+
json=json,
150+
)
146151

147152
return self._retry_with_exp_backoff(
148153
lambda stop_retrying, attempt: self._make_request(
@@ -198,12 +203,12 @@ def _make_request(
198203
self._statistics.requests += 1
199204

200205
try:
201-
url_with_params = self._build_url_with_params(url, params)
206+
url_with_params = self._build_url_with_params(url, params=params)
202207

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

209214
response = self._impit_client.request(
@@ -384,7 +389,12 @@ async def call(
384389

385390
self._statistics.calls += 1
386391

387-
prepared_headers, prepared_params, content = self._prepare_request_call(headers, params, data, json)
392+
prepared_headers, prepared_params, content = self._prepare_request_call(
393+
headers=headers,
394+
params=params,
395+
data=data,
396+
json=json,
397+
)
388398

389399
return await self._retry_with_exp_backoff(
390400
lambda stop_retrying, attempt: self._make_request(
@@ -440,12 +450,12 @@ async def _make_request(
440450
self._statistics.requests += 1
441451

442452
try:
443-
url_with_params = self._build_url_with_params(url, params)
453+
url_with_params = self._build_url_with_params(url, params=params)
444454

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

451461
response = await self._impit_async_client.request(

src/apify_client/_resource_clients/actor.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ def start(
266266
Returns:
267267
The run object.
268268
"""
269-
run_input, content_type = encode_key_value_store_record_value(run_input, content_type)
269+
run_input, content_type = encode_key_value_store_record_value(run_input, content_type=content_type)
270270

271271
request_params = self._build_params(
272272
build=build,
@@ -543,7 +543,7 @@ def validate_input(
543543
Returns:
544544
True if the input is valid, else raise an exception with validation error details.
545545
"""
546-
run_input, content_type = encode_key_value_store_record_value(run_input, content_type)
546+
run_input, content_type = encode_key_value_store_record_value(run_input, content_type=content_type)
547547

548548
self._http_client.call(
549549
url=self._build_url('validate-input'),
@@ -762,7 +762,7 @@ async def start(
762762
Returns:
763763
The run object.
764764
"""
765-
run_input, content_type = encode_key_value_store_record_value(run_input, content_type)
765+
run_input, content_type = encode_key_value_store_record_value(run_input, content_type=content_type)
766766

767767
request_params = self._build_params(
768768
build=build,
@@ -1043,7 +1043,7 @@ async def validate_input(
10431043
Returns:
10441044
True if the input is valid, else raise an exception with validation error details.
10451045
"""
1046-
run_input, content_type = encode_key_value_store_record_value(run_input, content_type)
1046+
run_input, content_type = encode_key_value_store_record_value(run_input, content_type=content_type)
10471047

10481048
await self._http_client.call(
10491049
url=self._build_url('validate-input'),

src/apify_client/_resource_clients/dataset.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -714,8 +714,8 @@ def create_items_public_url(
714714

715715
if dataset and dataset.url_signing_secret_key:
716716
signature = create_storage_content_signature(
717-
resource_id=dataset.id,
718-
url_signing_secret_key=dataset.url_signing_secret_key,
717+
dataset.id,
718+
dataset.url_signing_secret_key,
719719
expires_in=expires_in,
720720
)
721721
request_params['signature'] = signature
@@ -1292,8 +1292,8 @@ async def create_items_public_url(
12921292

12931293
if dataset and dataset.url_signing_secret_key:
12941294
signature = create_storage_content_signature(
1295-
resource_id=dataset.id,
1296-
url_signing_secret_key=dataset.url_signing_secret_key,
1295+
dataset.id,
1296+
dataset.url_signing_secret_key,
12971297
expires_in=expires_in,
12981298
)
12991299
request_params['signature'] = signature

src/apify_client/_resource_clients/key_value_store.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ def iterate_keys(
231231

232232
exclusive_start_key = current_keys_page.next_exclusive_start_key
233233

234-
def get_record(self, key: str, signature: str | None = None, *, timeout: Timeout = 'long') -> dict | None:
234+
def get_record(self, key: str, *, signature: str | None = None, timeout: Timeout = 'long') -> dict | None:
235235
"""Retrieve the given record from the key-value store.
236236
237237
https://docs.apify.com/api/v2#/reference/key-value-stores/record/get-record
@@ -290,7 +290,7 @@ def record_exists(self, key: str, *, timeout: Timeout = 'long') -> bool:
290290

291291
return response.status_code == HTTPStatus.OK
292292

293-
def get_record_as_bytes(self, key: str, signature: str | None = None, *, timeout: Timeout = 'long') -> dict | None:
293+
def get_record_as_bytes(self, key: str, *, signature: str | None = None, timeout: Timeout = 'long') -> dict | None:
294294
"""Retrieve the given record from the key-value store, without parsing it.
295295
296296
https://docs.apify.com/api/v2#/reference/key-value-stores/record/get-record
@@ -324,7 +324,7 @@ def get_record_as_bytes(self, key: str, signature: str | None = None, *, timeout
324324

325325
@contextmanager
326326
def stream_record(
327-
self, key: str, signature: str | None = None, *, timeout: Timeout = 'long'
327+
self, key: str, *, signature: str | None = None, timeout: Timeout = 'long'
328328
) -> Iterator[dict | None]:
329329
"""Retrieve the given record from the key-value store, as a stream.
330330
@@ -365,8 +365,8 @@ def set_record(
365365
self,
366366
key: str,
367367
value: Any,
368-
content_type: str | None = None,
369368
*,
369+
content_type: str | None = None,
370370
timeout: Timeout = 'long',
371371
) -> None:
372372
"""Set a value to the given record in the key-value store.
@@ -379,7 +379,7 @@ def set_record(
379379
content_type: The content type of the saved value.
380380
timeout: Timeout for the API HTTP request.
381381
"""
382-
value, content_type = encode_key_value_store_record_value(value, content_type)
382+
value, content_type = encode_key_value_store_record_value(value, content_type=content_type)
383383

384384
headers = {'content-type': content_type}
385385

@@ -482,8 +482,8 @@ def create_keys_public_url(
482482

483483
if metadata and metadata.url_signing_secret_key:
484484
signature = create_storage_content_signature(
485-
resource_id=metadata.id,
486-
url_signing_secret_key=metadata.url_signing_secret_key,
485+
metadata.id,
486+
metadata.url_signing_secret_key,
487487
expires_in=expires_in,
488488
)
489489
request_params['signature'] = signature
@@ -662,7 +662,7 @@ async def iterate_keys(
662662

663663
exclusive_start_key = current_keys_page.next_exclusive_start_key
664664

665-
async def get_record(self, key: str, signature: str | None = None, *, timeout: Timeout = 'long') -> dict | None:
665+
async def get_record(self, key: str, *, signature: str | None = None, timeout: Timeout = 'long') -> dict | None:
666666
"""Retrieve the given record from the key-value store.
667667
668668
https://docs.apify.com/api/v2#/reference/key-value-stores/record/get-record
@@ -722,7 +722,7 @@ async def record_exists(self, key: str, *, timeout: Timeout = 'long') -> bool:
722722
return response.status_code == HTTPStatus.OK
723723

724724
async def get_record_as_bytes(
725-
self, key: str, signature: str | None = None, *, timeout: Timeout = 'long'
725+
self, key: str, *, signature: str | None = None, timeout: Timeout = 'long'
726726
) -> dict | None:
727727
"""Retrieve the given record from the key-value store, without parsing it.
728728
@@ -757,7 +757,7 @@ async def get_record_as_bytes(
757757

758758
@asynccontextmanager
759759
async def stream_record(
760-
self, key: str, signature: str | None = None, *, timeout: Timeout = 'long'
760+
self, key: str, *, signature: str | None = None, timeout: Timeout = 'long'
761761
) -> AsyncIterator[dict | None]:
762762
"""Retrieve the given record from the key-value store, as a stream.
763763
@@ -798,8 +798,8 @@ async def set_record(
798798
self,
799799
key: str,
800800
value: Any,
801-
content_type: str | None = None,
802801
*,
802+
content_type: str | None = None,
803803
timeout: Timeout = 'long',
804804
) -> None:
805805
"""Set a value to the given record in the key-value store.
@@ -812,7 +812,7 @@ async def set_record(
812812
content_type: The content type of the saved value.
813813
timeout: Timeout for the API HTTP request.
814814
"""
815-
value, content_type = encode_key_value_store_record_value(value, content_type)
815+
value, content_type = encode_key_value_store_record_value(value, content_type=content_type)
816816

817817
headers = {'content-type': content_type}
818818

@@ -915,8 +915,8 @@ async def create_keys_public_url(
915915

916916
if metadata and metadata.url_signing_secret_key:
917917
signature = create_storage_content_signature(
918-
resource_id=metadata.id,
919-
url_signing_secret_key=metadata.url_signing_secret_key,
918+
metadata.id,
919+
metadata.url_signing_secret_key,
920920
expires_in=expires_in,
921921
)
922922
request_params['signature'] = signature

src/apify_client/_resource_clients/request_queue.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,7 @@ async def delete_request_lock(
876876

877877
async def _batch_add_requests_worker(
878878
self,
879+
*,
879880
queue: asyncio.Queue[Iterable[dict]],
880881
request_params: dict,
881882
timeout: Timeout,
@@ -992,7 +993,9 @@ async def batch_add_requests(
992993
async with asyncio.TaskGroup() as tg:
993994
workers = [
994995
tg.create_task(
995-
self._batch_add_requests_worker(asyncio_queue, request_params, timeout),
996+
self._batch_add_requests_worker(
997+
queue=asyncio_queue, request_params=request_params, timeout=timeout
998+
),
996999
name=f'batch_add_requests_worker_{i}',
9971000
)
9981001
for i in range(max_parallel)

src/apify_client/_resource_clients/run.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ def metamorph(
185185
Returns:
186186
The Actor run data.
187187
"""
188-
run_input, content_type = encode_key_value_store_record_value(run_input, content_type)
188+
run_input, content_type = encode_key_value_store_record_value(run_input, content_type=content_type)
189189

190190
safe_target_actor_id = to_safe_id(target_actor_id)
191191

@@ -375,6 +375,7 @@ def get_streamed_log(
375375
def charge(
376376
self,
377377
event_name: str,
378+
*,
378379
count: int = 1,
379380
idempotency_key: str | None = None,
380381
timeout: Timeout = 'short',
@@ -419,9 +420,9 @@ def charge(
419420

420421
def get_status_message_watcher(
421422
self,
423+
*,
422424
to_logger: logging.Logger | None = None,
423425
check_period: timedelta = timedelta(seconds=1),
424-
*,
425426
timeout: Timeout = 'long',
426427
) -> StatusMessageWatcher:
427428
"""Get `StatusMessageWatcher` instance that can be used to redirect status and status messages to logs.
@@ -608,7 +609,7 @@ async def metamorph(
608609
Returns:
609610
The Actor run data.
610611
"""
611-
run_input, content_type = encode_key_value_store_record_value(run_input, content_type)
612+
run_input, content_type = encode_key_value_store_record_value(run_input, content_type=content_type)
612613

613614
safe_target_actor_id = to_safe_id(target_actor_id)
614615

@@ -801,6 +802,7 @@ async def get_streamed_log(
801802
async def charge(
802803
self,
803804
event_name: str,
805+
*,
804806
count: int = 1,
805807
idempotency_key: str | None = None,
806808
timeout: Timeout = 'short',
@@ -845,9 +847,9 @@ async def charge(
845847

846848
async def get_status_message_watcher(
847849
self,
850+
*,
848851
to_logger: logging.Logger | None = None,
849852
check_period: timedelta = timedelta(seconds=1),
850-
*,
851853
timeout: Timeout = 'long',
852854
) -> StatusMessageWatcherAsync:
853855
"""Get `StatusMessageWatcher` instance that can be used to redirect status and status messages to logs.

src/apify_client/_utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def catch_not_found_for_resource_or_throw(exc: ApifyApiError, resource_id: str |
7878
catch_not_found_or_throw(exc)
7979

8080

81-
def encode_key_value_store_record_value(value: Any, content_type: str | None = None) -> tuple[Any, str]:
81+
def encode_key_value_store_record_value(value: Any, *, content_type: str | None = None) -> tuple[Any, str]:
8282
"""Encode a value for storage in a key-value store record.
8383
8484
Args:
@@ -227,6 +227,7 @@ def create_hmac_signature(secret_key: str, message: str) -> str:
227227
def create_storage_content_signature(
228228
resource_id: str,
229229
url_signing_secret_key: str,
230+
*,
230231
expires_in: timedelta | None = None,
231232
version: int = 0,
232233
) -> str:

0 commit comments

Comments
 (0)