Skip to content

Commit a8656c6

Browse files
vdusekclaude
andcommitted
fix: adapt Actor.start/call/call_task to apify-client v3 run_timeout and tolerate platform pricing-info env var omissions
apify-client v3 split the single `timeout` kwarg on `actor.start/call` and `task.call` into `run_timeout` (server-side Actor-run timeout) and `timeout` (HTTP request timeout). The SDK kept passing the inherited remaining-time to `timeout=`, so the new run received the default platform timeout, breaking the `inherit` timeout propagation. The v3 pricing models also require `apifyMarginPercentage`, `createdAt`, `startedAt`, and per-event `eventDescription`, but the platform's `APIFY_ACTOR_PRICING_INFO` env var does not include them, so Configuration() raised ValidationError on Actor startup. Inject safe defaults in a BeforeValidator so validation succeeds on the Actor side. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5862f42 commit a8656c6

5 files changed

Lines changed: 57 additions & 7 deletions

File tree

src/apify/_actor.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -927,7 +927,7 @@ async def start(
927927
content_type=content_type,
928928
build=build,
929929
memory_mbytes=memory_mbytes,
930-
timeout=actor_start_timeout if actor_start_timeout is not None else 'medium',
930+
run_timeout=actor_start_timeout,
931931
wait_for_finish=wait_for_finish,
932932
webhooks=serialized_webhooks,
933933
)
@@ -1050,7 +1050,7 @@ async def call(
10501050
content_type=content_type,
10511051
build=build,
10521052
memory_mbytes=memory_mbytes,
1053-
timeout=actor_call_timeout if actor_call_timeout is not None else 'no_timeout',
1053+
run_timeout=actor_call_timeout,
10541054
webhooks=serialized_webhooks,
10551055
wait_duration=wait,
10561056
logger=logger,
@@ -1125,7 +1125,7 @@ async def call_task(
11251125
task_input=task_input,
11261126
build=build,
11271127
memory_mbytes=memory_mbytes,
1128-
timeout=task_call_timeout if task_call_timeout is not None else 'no_timeout',
1128+
run_timeout=task_call_timeout,
11291129
webhooks=serialized_webhooks,
11301130
wait_duration=wait,
11311131
)

src/apify/_configuration.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,34 @@ def _load_storage_keys(data: None | str | ActorStorages) -> ActorStorages | None
7171
}
7272

7373

74+
def _normalize_actor_pricing_info(data: Any) -> Any:
75+
"""Parse and normalize the `APIFY_ACTOR_PRICING_INFO` env var for the apify-client pricing models.
76+
77+
The platform-provided env var omits some fields that are required by the apify-client pydantic models
78+
(`apifyMarginPercentage`, `createdAt`, `startedAt`, and per-event `eventDescription`). Inject safe
79+
defaults for those so validation succeeds on the Actor side.
80+
"""
81+
if data is None or data == '':
82+
return None
83+
pricing_info = json.loads(data) if isinstance(data, str) else data
84+
if not isinstance(pricing_info, dict):
85+
return pricing_info
86+
87+
pricing_info.setdefault('apifyMarginPercentage', 0.0)
88+
pricing_info.setdefault('createdAt', '1970-01-01T00:00:00.000Z')
89+
pricing_info.setdefault('startedAt', '1970-01-01T00:00:00.000Z')
90+
91+
pricing_per_event = pricing_info.get('pricingPerEvent')
92+
if isinstance(pricing_per_event, dict):
93+
actor_charge_events = pricing_per_event.get('actorChargeEvents')
94+
if isinstance(actor_charge_events, dict):
95+
for event in actor_charge_events.values():
96+
if isinstance(event, dict):
97+
event.setdefault('eventDescription', '')
98+
99+
return pricing_info
100+
101+
74102
@docs_group('Configuration')
75103
class Configuration(CrawleeConfiguration):
76104
"""A class for specifying the configuration of an Actor.
@@ -471,7 +499,7 @@ class Configuration(CrawleeConfiguration):
471499
description='JSON string with prising info of the actor',
472500
discriminator='pricing_model',
473501
),
474-
BeforeValidator(lambda data: json.loads(data) if isinstance(data, str) else data or None),
502+
BeforeValidator(_normalize_actor_pricing_info),
475503
] = None
476504

477505
charged_event_counts: Annotated[

tests/unit/actor/test_actor_helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ async def test_remote_method_with_timedelta_timeout(
268268
calls = apify_client_async_patcher.calls[client_resource][client_method]
269269
assert len(calls) == 1
270270
_, kwargs = calls[0][0], calls[0][1]
271-
assert kwargs.get('timeout') == timedelta(seconds=120)
271+
assert kwargs.get('run_timeout') == timedelta(seconds=120)
272272

273273

274274
async def test_call_actor_with_remaining_time_deprecation(

tests/unit/actor/test_configuration.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,28 @@ def test_actor_pricing_info_from_json_env_var(monkeypatch: pytest.MonkeyPatch) -
340340
assert config.actor_pricing_info.pricing_model == 'PAY_PER_EVENT'
341341

342342

343+
def test_actor_pricing_info_env_var_tolerates_platform_omissions(monkeypatch: pytest.MonkeyPatch) -> None:
344+
"""The platform env var may omit fields that apify-client models require; they should be injected with defaults."""
345+
346+
pricing_json = json.dumps(
347+
{
348+
'pricingModel': 'PAY_PER_EVENT',
349+
'pricingPerEvent': {
350+
'actorChargeEvents': {
351+
'search': {
352+
'eventPriceUsd': '0.01',
353+
'eventTitle': 'Search event',
354+
}
355+
}
356+
},
357+
}
358+
)
359+
monkeypatch.setenv('APIFY_ACTOR_PRICING_INFO', pricing_json)
360+
config = ApifyConfiguration()
361+
assert config.actor_pricing_info is not None
362+
assert config.actor_pricing_info.pricing_model == 'PAY_PER_EVENT'
363+
364+
343365
def test_actor_storage_json_env_var(monkeypatch: pytest.MonkeyPatch) -> None:
344366
"""Test that actor_storages_json is parsed from JSON env var."""
345367
datasets = {'default': 'default_dataset_id', 'custom': 'custom_dataset_id'}

uv.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)