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
2 changes: 1 addition & 1 deletion docs/02_concepts/code/11_actor_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ async def main() -> None:
]
# highlight-start
# Shortcut for charging for each pushed dataset item
await Actor.push_data(result, 'result-item')
await Actor.push_data(result, charged_event_name='result-item')
# highlight-end

# highlight-start
Expand Down
4 changes: 3 additions & 1 deletion docs/02_concepts/code/11_charge_limit_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ async def main() -> None:

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

if charge_result.event_charge_limit_reached:
Actor.log.info('Charge limit reached, stopping the Actor')
Expand Down
2 changes: 1 addition & 1 deletion docs/02_concepts/code/11_conditional_actor_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ async def main() -> None:
# highlight-start
if Actor.get_charging_manager().get_pricing_info().is_pay_per_event:
# highlight-end
await Actor.push_data({'hello': 'world'}, 'dataset-item')
await Actor.push_data({'hello': 'world'}, charged_event_name='dataset-item')
elif charged_items < (Actor.configuration.max_paid_dataset_items or 0):
await Actor.push_data({'hello': 'world'})
charged_items += 1
Expand Down
21 changes: 21 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,24 @@ 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.

## Keyword-only arguments

Secondary parameters on these methods can no longer be passed positionally.

```python
# Before
value = await Actor.get_value('my-key', default_value)
await Actor.push_data(data, 'my-event')
await Actor.charge('my-event', 5)

# After
value = await Actor.get_value('my-key', default_value=default_value)
await Actor.push_data(data, charged_event_name='my-event')
await Actor.charge('my-event', count=5)
```

Affected signatures:

- `Actor` — `get_value`, `push_data`, `charge`, `use_state`.
- `ChargingManager` — `charge`.
9 changes: 5 additions & 4 deletions src/apify/_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ async def open_request_queue(
)

@_ensure_context
async def push_data(self, data: dict | list[dict], charged_event_name: str | None = None) -> ChargeResult:
async def push_data(self, data: dict | list[dict], *, charged_event_name: str | None = None) -> ChargeResult:
"""Store an object or a list of objects to the default dataset of the current Actor run.

Args:
Expand Down Expand Up @@ -701,7 +701,7 @@ async def get_input(self) -> Any:
return input_value

@_ensure_context
async def get_value(self, key: str, default_value: Any = None) -> Any:
async def get_value(self, key: str, *, default_value: Any = None) -> Any:
"""Get a value from the default key-value store associated with the current Actor run.

Args:
Expand Down Expand Up @@ -735,7 +735,7 @@ def get_charging_manager(self) -> ChargingManager:
return self._charging_manager_implementation

@_ensure_context
async def charge(self, event_name: str, count: int = 1) -> ChargeResult:
async def charge(self, event_name: str, *, count: int = 1) -> ChargeResult:
"""Charge for a specified number of events - sub-operations of the Actor.

This is relevant only for the pay-per-event pricing model.
Expand All @@ -746,7 +746,7 @@ async def charge(self, event_name: str, count: int = 1) -> ChargeResult:
"""
# charging_manager.charge() acquires charge_lock internally.
charging_manager = self.get_charging_manager()
return await charging_manager.charge(event_name, count)
return await charging_manager.charge(event_name, count=count)

@overload
def on(
Expand Down Expand Up @@ -1397,6 +1397,7 @@ async def create_proxy_configuration(
async def use_state(
self,
default_value: dict[str, JsonSerializable] | None = None,
*,
key: str | None = None,
kvs_name: str | None = None,
) -> MutableMapping[str, JsonSerializable]:
Expand Down
6 changes: 3 additions & 3 deletions src/apify/_charging.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class ChargingManager(Protocol):
charge_lock: ReentrantLock
"""Lock to synchronize charge operations. Prevents race conditions between `charge` and `push_data` calls."""

async def charge(self, event_name: str, count: int = 1) -> ChargeResult:
async def charge(self, event_name: str, *, count: int = 1) -> ChargeResult:
"""Charge for a specified number of events - sub-operations of the Actor.

This is relevant only for the pay-per-event pricing model.
Expand Down Expand Up @@ -250,7 +250,7 @@ async def __aexit__(
self.active = False

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

Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/test_actor_api_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ async def test_actor_reboots_successfully(
async def main() -> None:
async with Actor:
print('Starting...')
cnt = await Actor.get_value('reboot_counter', 0)
cnt = await Actor.get_value('reboot_counter', default_value=0)

if cnt < 2:
print(f'Rebooting (cnt = {cnt})...')
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/test_actor_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ async def main() -> None:
async with Actor:
await Actor.push_data(
[{'id': i} for i in range(5)],
'push-item',
charged_event_name='push-item',
)

actor_client = await make_actor('ppe-push-data', main_func=main)
Expand Down
18 changes: 9 additions & 9 deletions tests/unit/actor/test_actor_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ async def setup_mocked_charging(
setup.charging_mgr._pricing_info['event'] = PricingInfoItem(Decimal('1.0'), 'Event')

result = await Actor.charge('event', count=1)
setup.mock_charge.assert_called_once_with('event', 1)
setup.mock_charge.assert_called_once_with('event', count=1)
"""
# Mock the ApifyClientAsync
mock_client = Mock()
Expand Down Expand Up @@ -76,14 +76,14 @@ async def test_actor_charge_push_data_with_no_remaining_budget() -> None:
result1 = await Actor.charge('some-event', count=1) # Costs $1, leaving $0.5

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

assert result1.charged_count == 1

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

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

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


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

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

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

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

Expand Down Expand Up @@ -254,10 +254,10 @@ async def test_charge_with_overdrawn_budget() -> None:
)

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

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

setup.mock_charge.assert_not_called()
Loading