diff --git a/docs/02_concepts/code/11_actor_charge.py b/docs/02_concepts/code/11_actor_charge.py index fc8a4433..6f511915 100644 --- a/docs/02_concepts/code/11_actor_charge.py +++ b/docs/02_concepts/code/11_actor_charge.py @@ -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 diff --git a/docs/02_concepts/code/11_charge_limit_check.py b/docs/02_concepts/code/11_charge_limit_check.py index 7f946a23..20bca7cd 100644 --- a/docs/02_concepts/code/11_charge_limit_check.py +++ b/docs/02_concepts/code/11_charge_limit_check.py @@ -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') diff --git a/docs/02_concepts/code/11_conditional_actor_charge.py b/docs/02_concepts/code/11_conditional_actor_charge.py index 193284fd..e0110737 100644 --- a/docs/02_concepts/code/11_conditional_actor_charge.py +++ b/docs/02_concepts/code/11_conditional_actor_charge.py @@ -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 diff --git a/docs/04_upgrading/upgrading_to_v4.md b/docs/04_upgrading/upgrading_to_v4.md index d7606598..e62ae02b 100644 --- a/docs/04_upgrading/upgrading_to_v4.md +++ b/docs/04_upgrading/upgrading_to_v4.md @@ -10,6 +10,24 @@ This guide lists the breaking changes between Apify Python SDK v3.x and v4.0. 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 in these signatures can no longer be passed positionally: +- `Actor` — `get_value`, `push_data`, `charge`, `use_state`. +- `ChargingManager` — `charge`. + +```python +# Before (v3) +value = await Actor.get_value('my-key', default_value) +await Actor.push_data(data, 'my-event') +await Actor.charge('my-event', 5) + +# After (v4) +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) +``` + ## Removal of deprecated APIs Methods and arguments that had been deprecated in v3 are removed in v4. diff --git a/src/apify/_actor.py b/src/apify/_actor.py index 3f4c0428..8f563f46 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -624,7 +624,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: @@ -700,7 +700,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: @@ -734,7 +734,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. @@ -745,7 +745,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( @@ -1379,6 +1379,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]: diff --git a/src/apify/_charging.py b/src/apify/_charging.py index 1ba09e14..09abee98 100644 --- a/src/apify/_charging.py +++ b/src/apify/_charging.py @@ -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. @@ -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: @@ -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}'") diff --git a/tests/e2e/test_actor_api_helpers.py b/tests/e2e/test_actor_api_helpers.py index 3747dd3b..33ae81ac 100644 --- a/tests/e2e/test_actor_api_helpers.py +++ b/tests/e2e/test_actor_api_helpers.py @@ -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})...') diff --git a/tests/e2e/test_actor_charge.py b/tests/e2e/test_actor_charge.py index f8b1f393..edf1b766 100644 --- a/tests/e2e/test_actor_charge.py +++ b/tests/e2e/test_actor_charge.py @@ -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) diff --git a/tests/unit/actor/test_actor_charge.py b/tests/unit/actor/test_actor_charge.py index 4e452e78..aad901d4 100644 --- a/tests/unit/actor/test_actor_charge.py +++ b/tests/unit/actor/test_actor_charge.py @@ -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() @@ -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() @@ -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 @@ -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 @@ -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 @@ -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)]), ) @@ -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()