Skip to content

Commit 1ea5ae8

Browse files
committed
Separate public and private interface of CharginManager
1 parent 872f04c commit 1ea5ae8

File tree

3 files changed

+87
-51
lines changed

3 files changed

+87
-51
lines changed

src/apify/_actor.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
EventSystemInfoData,
2525
)
2626

27-
from apify._charging import ChargeResult, ChargingManager
27+
from apify._charging import ChargeResult, ChargingManager, ChargingManagerImplementation
2828
from apify._configuration import Configuration
2929
from apify._consts import EVENT_LISTENERS_TIMEOUT
3030
from apify._crypto import decrypt_input_secrets, load_private_key
@@ -98,7 +98,7 @@ def __init__(
9898
)
9999
)
100100

101-
self._charging_manager = ChargingManager(self._configuration, self._apify_client)
101+
self._charging_manager = ChargingManagerImplementation(self._configuration, self._apify_client)
102102

103103
self._is_initialized = False
104104

@@ -232,7 +232,7 @@ async def init(self) -> None:
232232
await self._event_manager.__aenter__()
233233
self.log.debug('Event manager initialized')
234234

235-
await self._charging_manager.init()
235+
await self._charging_manager.__aenter__()
236236
self.log.debug('Charging manager initialized')
237237

238238
self._is_initialized = True
@@ -276,6 +276,7 @@ async def finalize() -> None:
276276
await self._event_manager.wait_for_all_listeners_to_complete(timeout=event_listeners_timeout)
277277

278278
await self._event_manager.__aexit__(None, None, None)
279+
await self._charging_manager.__aexit__(None, None, None)
279280

280281
await asyncio.wait_for(finalize(), cleanup_timeout.total_seconds())
281282
self._is_initialized = False

src/apify/_charging.py

Lines changed: 82 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@
44
from dataclasses import dataclass
55
from datetime import datetime, timezone
66
from decimal import Decimal
7-
from typing import TYPE_CHECKING, Union
7+
from typing import TYPE_CHECKING, Protocol, Union
88

99
from pydantic import TypeAdapter
1010

11+
from apify_shared.utils import ignore_docs
12+
1113
from apify._models import ActorRun, PricingModel
1214
from apify._utils import docs_group
1315
from apify.log import logger
1416
from apify.storages import Dataset
1517

1618
if TYPE_CHECKING:
19+
from types import TracebackType
20+
1721
from apify_client import ApifyClientAsync
1822

1923
from apify._configuration import Configuration
@@ -22,8 +26,74 @@
2226
run_validator: TypeAdapter[ActorRun | None] = TypeAdapter(Union[ActorRun, None])
2327

2428

25-
@docs_group('Classes')
26-
class ChargingManager:
29+
@docs_group('Interfaces')
30+
class ChargingManager(Protocol):
31+
"""Provides fine-grained access to pay-per-event functionality."""
32+
33+
async def charge(self, event_name: str, count: int = 1) -> ChargeResult:
34+
"""Charge for a specified number of events - sub-operations of the Actor.
35+
36+
This is relevant only for the pay-per-event pricing model.
37+
38+
Args:
39+
event_name: Name of the event to be charged for.
40+
count: Number of events to charge for.
41+
"""
42+
43+
def calculate_total_charged_amount(self) -> Decimal:
44+
"""Calculate the total amount of money charged for pay-per-event events so far."""
45+
46+
def calculate_max_event_charge_count_within_limit(self, event_name: str) -> int | None:
47+
"""Calculate how many instances of an event can be charged before we reach the configured limit.
48+
49+
Args:
50+
event_name: Name of the inspected event.
51+
"""
52+
53+
def get_pricing_info(self) -> ActorPricingInfo:
54+
"""Retrieve detailed information about the effective pricing of the current Actor run.
55+
56+
This can be used for instance when your code needs to support multiple pricing models in transition periods.
57+
"""
58+
59+
60+
@docs_group('Data structures')
61+
@dataclass(frozen=True)
62+
class ChargeResult:
63+
"""Result of the `ChargingManager.charge` method."""
64+
65+
event_charge_limit_reached: bool
66+
"""If true, no more events of this type can be charged within the limit."""
67+
68+
charged_count: int
69+
"""Total amount of charged events - may be lower than the requested amount."""
70+
71+
chargeable_within_limit: dict[str, int | None]
72+
"""How many events of each known type can still be charged within the limit."""
73+
74+
75+
@docs_group('Data structures')
76+
@dataclass
77+
class ActorPricingInfo:
78+
"""Result of the `ChargingManager.get_pricing_info` method."""
79+
80+
pricing_model: PricingModel | None
81+
"""The currently effective pricing model."""
82+
83+
max_total_charge_usd: Decimal
84+
"""A configured limit for the total charged amount - if you exceed it, you won't receive more money than this."""
85+
86+
is_pay_per_event: bool
87+
"""A shortcut - true if the Actor runs with the pay-per-event pricing model."""
88+
89+
per_event_prices: dict[str, Decimal]
90+
"""Price of every known event type."""
91+
92+
93+
@ignore_docs
94+
class ChargingManagerImplementation(ChargingManager):
95+
"""Implementation of the `ChargingManager` Protocol - this is only meant to be instantiated internally."""
96+
2797
LOCAL_CHARGING_LOG_DATASET_NAME = 'charging_log'
2898

2999
def __init__(self, configuration: Configuration, client: ApifyClientAsync) -> None:
@@ -50,7 +120,7 @@ def __init__(self, configuration: Configuration, client: ApifyClientAsync) -> No
50120

51121
self._not_ppe_warning_printed = False
52122

53-
async def init(self) -> None:
123+
async def __aenter__(self) -> None:
54124
"""Initialize the charging manager - this is called by the `Actor` class and shouldn't be invoked manually."""
55125
self._charging_state = {}
56126

@@ -93,11 +163,15 @@ async def init(self) -> None:
93163

94164
self._charging_log_dataset = await Dataset.open(name=self.LOCAL_CHARGING_LOG_DATASET_NAME)
95165

96-
async def charge(self, event_name: str, count: int = 1) -> ChargeResult:
97-
"""Charge for a specified number of events - sub-operations of the Actor.
166+
async def __aexit__(
167+
self,
168+
exc_type: type[BaseException] | None,
169+
exc_value: BaseException | None,
170+
exc_traceback: TracebackType | None,
171+
) -> None:
172+
pass
98173

99-
This is relevant only for the pay-per-event pricing model.
100-
"""
174+
async def charge(self, event_name: str, count: int = 1) -> ChargeResult:
101175
if self._charging_state is None:
102176
raise RuntimeError('Charging manager is not initialized')
103177

@@ -192,7 +266,6 @@ def calculate_chargeable() -> dict[str, int | None]:
192266
)
193267

194268
def calculate_total_charged_amount(self) -> Decimal:
195-
"""Calculate the total amount of money charged for pay-per-event events so far."""
196269
if self._charging_state is None:
197270
raise RuntimeError('Charging manager is not initialized')
198271

@@ -202,7 +275,6 @@ def calculate_total_charged_amount(self) -> Decimal:
202275
)
203276

204277
def calculate_max_event_charge_count_within_limit(self, event_name: str) -> int | None:
205-
"""Calculate how many instances of an event can be charged before we reach the configured limit."""
206278
if self._charging_state is None:
207279
raise RuntimeError('Charging manager is not initialized')
208280

@@ -222,10 +294,6 @@ def calculate_max_event_charge_count_within_limit(self, event_name: str) -> int
222294
return math.floor(result) if result.is_finite() else None
223295

224296
def get_pricing_info(self) -> ActorPricingInfo:
225-
"""Retrieve detailed information about the effective pricing of the current Actor run.
226-
227-
This can be used for instance when your code needs to support multiple pricing models in transition periods.
228-
"""
229297
if self._charging_state is None:
230298
raise RuntimeError('Charging manager is not initialized')
231299

@@ -241,39 +309,6 @@ def get_pricing_info(self) -> ActorPricingInfo:
241309
)
242310

243311

244-
@docs_group('Data structures')
245-
@dataclass(frozen=True)
246-
class ChargeResult:
247-
"""Result of the `ChargingManager.charge` method."""
248-
249-
event_charge_limit_reached: bool
250-
"""If true, no more events of this type can be charged within the limit."""
251-
252-
charged_count: int
253-
"""Total amount of charged events - may be lower than the requested amount."""
254-
255-
chargeable_within_limit: dict[str, int | None]
256-
"""How many events of each known type can still be charged within the limit."""
257-
258-
259-
@docs_group('Data structures')
260-
@dataclass
261-
class ActorPricingInfo:
262-
"""Result of the `ChargingManager.get_pricing_info` method."""
263-
264-
pricing_model: PricingModel | None
265-
"""The currently effective pricing model."""
266-
267-
max_total_charge_usd: Decimal
268-
"""A configured limit for the total charged amount - if you exceed it, you won't receive more money than this."""
269-
270-
is_pay_per_event: bool
271-
"""A shortcut - true if the Actor runs with the pay-per-event pricing model."""
272-
273-
per_event_prices: dict[str, Decimal]
274-
"""Price of every known event type."""
275-
276-
277312
@dataclass
278313
class ChargingStateItem:
279314
charge_count: int

src/apify/_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def is_running_in_ipython() -> bool:
2727
return getattr(builtins, '__IPYTHON__', False)
2828

2929

30-
GroupName = Literal['Classes', 'Abstract classes', 'Data structures', 'Errors', 'Functions']
30+
GroupName = Literal['Classes', 'Abstract classes', 'Interfaces', 'Data structures', 'Errors', 'Functions']
3131

3232

3333
def docs_group(group_name: GroupName) -> Callable: # noqa: ARG001

0 commit comments

Comments
 (0)