Skip to content

Commit a2528ed

Browse files
committed
Use Apify-provided environment variables to obtain PPE pricing information
1 parent 77bf101 commit a2528ed

File tree

2 files changed

+109
-39
lines changed

2 files changed

+109
-39
lines changed

src/apify/_charging.py

Lines changed: 93 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
from __future__ import annotations
22

3+
import json
34
import math
45
from dataclasses import dataclass
56
from datetime import datetime, timezone
67
from decimal import Decimal
7-
from typing import TYPE_CHECKING, Protocol
8+
from typing import TYPE_CHECKING, Any, Protocol
89

910
from pydantic import TypeAdapter
1011

1112
from 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+
)
1422
from apify._utils import docs_group
1523
from apify.log import logger
1624
from 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
333387
class ChargingStateItem:

src/apify/_configuration.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,22 @@ class Configuration(CrawleeConfiguration):
409409
),
410410
] = None
411411

412+
actor_pricing_info: Annotated[
413+
str | None,
414+
Field(
415+
alias='apify_actor_pricing_info',
416+
description='JSON string with prising info of the actor',
417+
),
418+
] = None
419+
420+
charged_event_counts: Annotated[
421+
str | None,
422+
Field(
423+
alias='apify_charged_actor_event_counts',
424+
description='Counts of events that were charged for the actor',
425+
),
426+
] = None
427+
412428
@model_validator(mode='after')
413429
def disable_browser_sandbox_on_platform(self) -> Self:
414430
"""Disable the browser sandbox mode when running on the Apify platform.

0 commit comments

Comments
 (0)