Skip to content

Commit eb94c99

Browse files
authored
refactor!: Mark secondary arguments as keyword-only (#917)
## Summary Closes #881. Reshapes function/method signatures across the SDK public API so that secondary parameters must be passed as keyword arguments. Primary "subject" arguments (e.g. `key`, `data`, `event_name`) stay positional. Mirrors what was done in the client: apify/apify-client-python#766. ### Affected APIs `Actor`: - `get_value(key, *, default_value=None)` - `push_data(data, *, charged_event_name=None)` - `charge(event_name, *, count=1)` - `use_state(default_value=None, *, key=None, kvs_name=None)` `ChargingManager` / `ChargingManagerImplementation` (returned by `Actor.get_charging_manager()`): - `charge(event_name, *, count=1)` Crawlee-overriding methods (e.g. `ProxyConfiguration.new_proxy_info`, the storage clients) were intentionally left untouched — Crawlee's base signatures are positional, so making the overrides keyword-only would diverge from the base class. ## Why Keyword-only parameters at API boundaries make call sites self-documenting and prevent breakage when new options are added between existing arguments. **BREAKING CHANGE** for v4.0 — see `docs/04_upgrading/upgrading_to_v4.md` for the migration guide.
1 parent 24a6edb commit eb94c99

9 files changed

Lines changed: 42 additions & 21 deletions

File tree

docs/02_concepts/code/11_actor_charge.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ async def main() -> None:
2020
]
2121
# highlight-start
2222
# Shortcut for charging for each pushed dataset item
23-
await Actor.push_data(result, 'result-item')
23+
await Actor.push_data(result, charged_event_name='result-item')
2424
# highlight-end
2525

2626
# highlight-start

docs/02_concepts/code/11_charge_limit_check.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ async def main() -> None:
1717

1818
# highlight-start
1919
# push_data returns a ChargeResult - check it to know if the budget ran out
20-
charge_result = await Actor.push_data(result, 'result-item')
20+
charge_result = await Actor.push_data(
21+
result, charged_event_name='result-item'
22+
)
2123

2224
if charge_result.event_charge_limit_reached:
2325
Actor.log.info('Charge limit reached, stopping the Actor')

docs/02_concepts/code/11_conditional_actor_charge.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ async def main() -> None:
1414
# highlight-start
1515
if Actor.get_charging_manager().get_pricing_info().is_pay_per_event:
1616
# highlight-end
17-
await Actor.push_data({'hello': 'world'}, 'dataset-item')
17+
await Actor.push_data({'hello': 'world'}, charged_event_name='dataset-item')
1818
elif charged_items < (Actor.configuration.max_paid_dataset_items or 0):
1919
await Actor.push_data({'hello': 'world'})
2020
charged_items += 1

docs/04_upgrading/upgrading_to_v4.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ This guide lists the breaking changes between Apify Python SDK v3.x and v4.0.
1010

1111
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.
1212

13+
## Keyword-only arguments
14+
15+
Secondary parameters in these signatures can no longer be passed positionally:
16+
- `Actor``get_value`, `push_data`, `charge`, `use_state`.
17+
- `ChargingManager``charge`.
18+
19+
```python
20+
# Before (v3)
21+
value = await Actor.get_value('my-key', default_value)
22+
await Actor.push_data(data, 'my-event')
23+
await Actor.charge('my-event', 5)
24+
25+
# After (v4)
26+
value = await Actor.get_value('my-key', default_value=default_value)
27+
await Actor.push_data(data, charged_event_name='my-event')
28+
await Actor.charge('my-event', count=5)
29+
```
30+
1331
## Removal of deprecated APIs
1432

1533
Methods and arguments that had been deprecated in v3 are removed in v4.

src/apify/_actor.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,7 @@ async def open_request_queue(
624624
)
625625

626626
@_ensure_context
627-
async def push_data(self, data: dict | list[dict], charged_event_name: str | None = None) -> ChargeResult:
627+
async def push_data(self, data: dict | list[dict], *, charged_event_name: str | None = None) -> ChargeResult:
628628
"""Store an object or a list of objects to the default dataset of the current Actor run.
629629
630630
Args:
@@ -700,7 +700,7 @@ async def get_input(self) -> Any:
700700
return input_value
701701

702702
@_ensure_context
703-
async def get_value(self, key: str, default_value: Any = None) -> Any:
703+
async def get_value(self, key: str, *, default_value: Any = None) -> Any:
704704
"""Get a value from the default key-value store associated with the current Actor run.
705705
706706
Args:
@@ -734,7 +734,7 @@ def get_charging_manager(self) -> ChargingManager:
734734
return self._charging_manager_implementation
735735

736736
@_ensure_context
737-
async def charge(self, event_name: str, count: int = 1) -> ChargeResult:
737+
async def charge(self, event_name: str, *, count: int = 1) -> ChargeResult:
738738
"""Charge for a specified number of events - sub-operations of the Actor.
739739
740740
This is relevant only for the pay-per-event pricing model.
@@ -745,7 +745,7 @@ async def charge(self, event_name: str, count: int = 1) -> ChargeResult:
745745
"""
746746
# charging_manager.charge() acquires charge_lock internally.
747747
charging_manager = self.get_charging_manager()
748-
return await charging_manager.charge(event_name, count)
748+
return await charging_manager.charge(event_name, count=count)
749749

750750
@overload
751751
def on(
@@ -1379,6 +1379,7 @@ async def create_proxy_configuration(
13791379
async def use_state(
13801380
self,
13811381
default_value: dict[str, JsonSerializable] | None = None,
1382+
*,
13821383
key: str | None = None,
13831384
kvs_name: str | None = None,
13841385
) -> MutableMapping[str, JsonSerializable]:

src/apify/_charging.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class ChargingManager(Protocol):
5555
charge_lock: ReentrantLock
5656
"""Lock to synchronize charge operations. Prevents race conditions between `charge` and `push_data` calls."""
5757

58-
async def charge(self, event_name: str, count: int = 1) -> ChargeResult:
58+
async def charge(self, event_name: str, *, count: int = 1) -> ChargeResult:
5959
"""Charge for a specified number of events - sub-operations of the Actor.
6060
6161
This is relevant only for the pay-per-event pricing model.
@@ -250,7 +250,7 @@ async def __aexit__(
250250
self.active = False
251251

252252
@_ensure_context
253-
async def charge(self, event_name: str, count: int = 1) -> ChargeResult:
253+
async def charge(self, event_name: str, *, count: int = 1) -> ChargeResult:
254254
# For runs that do not use the pay-per-event pricing model, just print a warning and return
255255
if self._pricing_model != 'PAY_PER_EVENT':
256256
if not self._not_ppe_warning_printed:
@@ -308,7 +308,7 @@ async def charge(self, event_name: str, count: int = 1) -> ChargeResult:
308308
# the platform handles them automatically based on dataset writes.
309309
pass
310310
elif event_name in self._pricing_info:
311-
await self._client.run(self._actor_run_id).charge(event_name, charged_count)
311+
await self._client.run(self._actor_run_id).charge(event_name, count=charged_count)
312312
else:
313313
logger.warning(f"Attempting to charge for an unknown event '{event_name}'")
314314

tests/e2e/test_actor_api_helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ async def test_actor_reboots_successfully(
360360
async def main() -> None:
361361
async with Actor:
362362
print('Starting...')
363-
cnt = await Actor.get_value('reboot_counter', 0)
363+
cnt = await Actor.get_value('reboot_counter', default_value=0)
364364

365365
if cnt < 2:
366366
print(f'Rebooting (cnt = {cnt})...')

tests/e2e/test_actor_charge.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ async def main() -> None:
2626
async with Actor:
2727
await Actor.push_data(
2828
[{'id': i} for i in range(5)],
29-
'push-item',
29+
charged_event_name='push-item',
3030
)
3131

3232
actor_client = await make_actor('ppe-push-data', main_func=main)

tests/unit/actor/test_actor_charge.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ async def setup_mocked_charging(
3232
setup.charging_mgr._pricing_info['event'] = PricingInfoItem(Decimal('1.0'), 'Event')
3333
3434
result = await Actor.charge('event', count=1)
35-
setup.mock_charge.assert_called_once_with('event', 1)
35+
setup.mock_charge.assert_called_once_with('event', count=1)
3636
"""
3737
# Mock the ApifyClientAsync
3838
mock_client = Mock()
@@ -76,14 +76,14 @@ async def test_actor_charge_push_data_with_no_remaining_budget() -> None:
7676
result1 = await Actor.charge('some-event', count=1) # Costs $1, leaving $0.5
7777

7878
# Verify the first charge call was made correctly
79-
setup.mock_charge.assert_called_once_with('some-event', 1)
79+
setup.mock_charge.assert_called_once_with('some-event', count=1)
8080
setup.mock_charge.reset_mock()
8181

8282
assert result1.charged_count == 1
8383

8484
# Now try to push data - we can't afford even 1 more event
8585
# This will call charge(event_name, count=0) because max_charged_count=0
86-
result = await Actor.push_data([{'hello': 'world'} for _ in range(10)], 'another-event')
86+
result = await Actor.push_data([{'hello': 'world'} for _ in range(10)], charged_event_name='another-event')
8787

8888
# The API should NOT be called when count=0
8989
setup.mock_charge.assert_not_called()
@@ -111,7 +111,7 @@ async def test_actor_charge_api_call_verification() -> None:
111111

112112
# Call charge with count=1 - this SHOULD call the API
113113
result2 = await Actor.charge('test-event', count=1)
114-
setup.mock_charge.assert_called_once_with('test-event', 1)
114+
setup.mock_charge.assert_called_once_with('test-event', count=1)
115115
assert result2.charged_count == 1
116116

117117

@@ -154,7 +154,7 @@ async def test_push_data_combined_price_limits_items() -> None:
154154
{'scrape': Decimal('1.00'), 'apify-default-dataset-item': Decimal('1.00')},
155155
):
156156
data = [{'id': i} for i in range(5)]
157-
result = await Actor.push_data(data, 'scrape')
157+
result = await Actor.push_data(data, charged_event_name='scrape')
158158

159159
assert result is not None
160160
assert result.charged_count == 1
@@ -172,7 +172,7 @@ async def test_push_data_charges_synthetic_event_for_default_dataset() -> None:
172172
{'test': Decimal('0.10'), 'apify-default-dataset-item': Decimal('0.05')},
173173
) as setup:
174174
data = [{'id': i} for i in range(3)]
175-
result = await Actor.push_data(data, 'test')
175+
result = await Actor.push_data(data, charged_event_name='test')
176176

177177
assert result is not None
178178
assert result.charged_count == 3
@@ -192,7 +192,7 @@ async def test_charge_lock_concurrent_actor_and_dataset_push() -> None:
192192

193193
# Run concurrent pushes - Actor.push_data and direct dataset.push_data
194194
await asyncio.gather(
195-
Actor.push_data([{'source': 'actor', 'id': i} for i in range(5)], 'event'),
195+
Actor.push_data([{'source': 'actor', 'id': i} for i in range(5)], charged_event_name='event'),
196196
dataset.push_data([{'source': 'dataset', 'id': i} for i in range(5)]),
197197
)
198198

@@ -254,10 +254,10 @@ async def test_charge_with_overdrawn_budget() -> None:
254254
)
255255

256256
async with setup_mocked_charging(configuration, {}) as setup:
257-
charge_result = await Actor.charge('event', 1)
257+
charge_result = await Actor.charge('event', count=1)
258258
assert charge_result.charged_count == 0 # The budget doesn't allow another event
259259

260-
push_result = await Actor.push_data([{'hello': 'world'}], 'event')
260+
push_result = await Actor.push_data([{'hello': 'world'}], charged_event_name='event')
261261
assert push_result.charged_count == 0 # Nor does the budget allow this
262262

263263
setup.mock_charge.assert_not_called()

0 commit comments

Comments
 (0)