11from __future__ import annotations
22
3+ import json
34import math
45from dataclasses import dataclass
56from datetime import datetime , timezone
67from decimal import Decimal
7- from typing import TYPE_CHECKING , Protocol
8+ from typing import TYPE_CHECKING , Any , Protocol
89
910from pydantic import TypeAdapter
1011
1112from crawlee ._utils .context import ensure_context
1213
13- from apify ._models import ActorRun , PricingModel
14+ from apify ._models import (
15+ ActorRun ,
16+ FlatPricePerMonthActorPricingInfo ,
17+ FreeActorPricingInfo ,
18+ PayPerEventActorPricingInfo ,
19+ PricePerDatasetItemActorPricingInfo ,
20+ PricingModel ,
21+ )
1422from apify ._utils import docs_group
1523from apify .log import logger
1624from apify .storages import Dataset
@@ -115,20 +123,12 @@ class ChargingManagerImplementation(ChargingManager):
115123
116124 def __init__ (self , configuration : Configuration , client : ApifyClientAsync ) -> None :
117125 self ._max_total_charge_usd = configuration .max_total_charge_usd or Decimal ('inf' )
126+ self ._configuration = configuration
118127 self ._is_at_home = configuration .is_at_home
119128 self ._actor_run_id = configuration .actor_run_id
120129 self ._purge_charging_log_dataset = configuration .purge_on_start
121130 self ._pricing_model : PricingModel | None = None
122131
123- if configuration .test_pay_per_event :
124- if self ._is_at_home :
125- raise ValueError (
126- 'Using the ACTOR_TEST_PAY_PER_EVENT environment variable is only supported '
127- 'in a local development environment'
128- )
129-
130- self ._pricing_model = 'PAY_PER_EVENT'
131-
132132 self ._client = client
133133 self ._charging_log_dataset : Dataset | None = None
134134
@@ -140,41 +140,47 @@ def __init__(self, configuration: Configuration, client: ApifyClientAsync) -> No
140140
141141 async def __aenter__ (self ) -> None :
142142 """Initialize the charging manager - this is called by the `Actor` class and shouldn't be invoked manually."""
143- self .active = True
144-
145- if self ._is_at_home :
146- # Running on the Apify platform - fetch pricing info for the current run.
147-
148- if self ._actor_run_id is None :
149- raise RuntimeError ('Actor run ID not found even though the Actor is running on Apify' )
143+ # Validate config
144+ if self ._configuration .test_pay_per_event and self ._is_at_home :
145+ raise ValueError (
146+ 'Using the ACTOR_TEST_PAY_PER_EVENT environment variable is only supported '
147+ 'in a local development environment'
148+ )
150149
151- run = run_validator .validate_python (await self ._client .run (self ._actor_run_id ).get ())
152- if run is None :
153- raise RuntimeError ('Actor run not found' )
150+ self .active = True
154151
155- if run .pricing_info is not None :
156- self ._pricing_model = run .pricing_info .pricing_model
152+ # Retrieve pricing information from env vars or API
153+ pricing_data = await self ._fetch_pricing_info ()
154+ pricing_info = pricing_data ['pricing_info' ]
155+ charged_event_counts = pricing_data ['charged_event_counts' ]
156+ max_total_charge_usd = pricing_data ['max_total_charge_usd' ]
157157
158- if run .pricing_info .pricing_model == 'PAY_PER_EVENT' :
159- for event_name , event_pricing in run .pricing_info .pricing_per_event .actor_charge_events .items ():
160- self ._pricing_info [event_name ] = PricingInfoItem (
161- price = event_pricing .event_price_usd ,
162- title = event_pricing .event_title ,
163- )
158+ # Set pricing model
159+ if self ._configuration .test_pay_per_event :
160+ self ._pricing_model = 'PAY_PER_EVENT'
161+ else :
162+ self ._pricing_model = pricing_info .pricing_model if pricing_info else None
163+
164+ # Load per-event pricing information
165+ if pricing_info and pricing_info .pricing_model == 'PAY_PER_EVENT' :
166+ for event_name , event_pricing in pricing_info .pricing_per_event .actor_charge_events .items ():
167+ self ._pricing_info [event_name ] = PricingInfoItem (
168+ price = event_pricing .event_price_usd ,
169+ title = event_pricing .event_title ,
170+ )
164171
165- self ._max_total_charge_usd = run . options . max_total_charge_usd or self . _max_total_charge_usd
172+ self ._max_total_charge_usd = max_total_charge_usd
166173
167- for event_name , count in (run .charged_event_counts or {}).items ():
168- price = self ._pricing_info .get (event_name , PricingInfoItem (Decimal (), title = '' )).price
169- self ._charging_state [event_name ] = ChargingStateItem (
170- charge_count = count ,
171- total_charged_amount = count * price ,
172- )
174+ # Load charged event counts
175+ for event_name , count in charged_event_counts .items ():
176+ price = self ._pricing_info .get (event_name , PricingInfoItem (Decimal (), title = '' )).price
177+ self ._charging_state [event_name ] = ChargingStateItem (
178+ charge_count = count ,
179+ total_charged_amount = count * price ,
180+ )
173181
182+ # Set up charging log dataset for local development
174183 if not self ._is_at_home and self ._pricing_model == 'PAY_PER_EVENT' :
175- # We are not running on the Apify platform, but PPE is enabled for testing - open a dataset that
176- # will contain a log of all charge calls for debugging purposes.
177-
178184 if self ._purge_charging_log_dataset :
179185 dataset = await Dataset .open (name = self .LOCAL_CHARGING_LOG_DATASET_NAME )
180186 await dataset .drop ()
@@ -328,6 +334,54 @@ def get_charged_event_count(self, event_name: str) -> int:
328334 def get_max_total_charge_usd (self ) -> Decimal :
329335 return self ._max_total_charge_usd
330336
337+ async def _fetch_pricing_info (self ) -> dict [str , Any ]:
338+ """Fetch pricing information from environment variables or API."""
339+ # Check if pricing info is available via environment variables
340+ if self ._configuration .actor_pricing_info and self ._configuration .charged_event_counts :
341+ charged_counts = json .loads (self ._configuration .charged_event_counts )
342+
343+ # Validate pricing info with proper discriminator support
344+ pricing_info_adapter : TypeAdapter [
345+ FreeActorPricingInfo
346+ | FlatPricePerMonthActorPricingInfo
347+ | PricePerDatasetItemActorPricingInfo
348+ | PayPerEventActorPricingInfo
349+ ] = TypeAdapter (
350+ FreeActorPricingInfo
351+ | FlatPricePerMonthActorPricingInfo
352+ | PricePerDatasetItemActorPricingInfo
353+ | PayPerEventActorPricingInfo
354+ )
355+ pricing_info = pricing_info_adapter .validate_json (self ._configuration .actor_pricing_info )
356+
357+ return {
358+ 'pricing_info' : pricing_info ,
359+ 'charged_event_counts' : charged_counts ,
360+ 'max_total_charge_usd' : self ._configuration .max_total_charge_usd or Decimal ('inf' ),
361+ }
362+
363+ # Fall back to API call
364+ if self ._is_at_home :
365+ if self ._actor_run_id is None :
366+ raise RuntimeError ('Actor run ID not found even though the Actor is running on Apify' )
367+
368+ run = run_validator .validate_python (await self ._client .run (self ._actor_run_id ).get ())
369+ if run is None :
370+ raise RuntimeError ('Actor run not found' )
371+
372+ return {
373+ 'pricing_info' : run .pricing_info ,
374+ 'charged_event_counts' : run .charged_event_counts or {},
375+ 'max_total_charge_usd' : run .options .max_total_charge_usd or Decimal ('inf' ),
376+ }
377+
378+ # Local development without environment variables
379+ return {
380+ 'pricing_info' : None ,
381+ 'charged_event_counts' : {},
382+ 'max_total_charge_usd' : self ._configuration .max_total_charge_usd or Decimal ('inf' ),
383+ }
384+
331385
332386@dataclass
333387class ChargingStateItem :
0 commit comments