Skip to content

Commit b2f6952

Browse files
vdusekclaude
andcommitted
fix: update _models.py to align with apify-client-python typed models
Sync SDK models with the new fully typed Pydantic models from apify-client-python PR #604. Key changes: - Add CommonActorPricingInfo base class with shared pricing fields - Add new models: ActorRunUsageUsd, Metamorph, PricingModel enum, GeneralAccessEnum - Add missing fields: migration_count, reboot_count, max_items, general_access, metamorphs, client_ip, user_agent, schedule_id, scheduled_at - Fix __model_config__ → model_config (was not applying config) - Add extra='allow' for forward-compatibility with new API fields - Change Decimal → float for pricing fields to match API types - Change timedelta → int/float for duration/timeout fields - Change container_url from required to optional - Separate usage vs usage_usd into distinct models - Update _charging.py for Decimal→float type conversions - Update timeout field references in integration tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cf36cc2 commit b2f6952

File tree

3 files changed

+135
-67
lines changed

3 files changed

+135
-67
lines changed

src/apify/_charging.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ async def __aenter__(self) -> None:
164164
if pricing_info and pricing_info.pricing_model == 'PAY_PER_EVENT':
165165
for event_name, event_pricing in pricing_info.pricing_per_event.actor_charge_events.items(): # ty:ignore[possibly-missing-attribute]
166166
self._pricing_info[event_name] = PricingInfoItem(
167-
price=event_pricing.event_price_usd,
167+
price=Decimal(str(event_pricing.event_price_usd)),
168168
title=event_pricing.event_title,
169169
)
170170

@@ -355,10 +355,11 @@ async def _fetch_pricing_info(self) -> _FetchedPricingInfoDict:
355355
if run is None:
356356
raise RuntimeError('Actor run not found')
357357

358+
max_charge = run.options.max_total_charge_usd
358359
return _FetchedPricingInfoDict(
359360
pricing_info=run.pricing_info,
360361
charged_event_counts=run.charged_event_counts or {},
361-
max_total_charge_usd=run.options.max_total_charge_usd or Decimal('inf'),
362+
max_total_charge_usd=Decimal(str(max_charge)) if max_charge is not None else Decimal('inf'),
362363
)
363364

364365
# Local development without environment variables

src/apify/_models.py

Lines changed: 120 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,38 @@
11
from __future__ import annotations
22

3-
from datetime import datetime, timedelta
4-
from decimal import Decimal
5-
from typing import TYPE_CHECKING, Annotated, Literal
3+
from datetime import datetime
4+
from enum import Enum
5+
from typing import Annotated, Literal
66

77
from pydantic import BaseModel, BeforeValidator, ConfigDict, Field
88

99
from apify_shared.consts import ActorJobStatus, MetaOrigin, WebhookEventType
10-
from crawlee._utils.models import timedelta_ms
1110
from crawlee._utils.urls import validate_http_url
1211

1312
from apify._utils import docs_group
1413

15-
if TYPE_CHECKING:
16-
from typing import TypeAlias
14+
15+
class PricingModel(str, Enum):
16+
"""Pricing model for an Actor."""
17+
18+
PAY_PER_EVENT = 'PAY_PER_EVENT'
19+
PRICE_PER_DATASET_ITEM = 'PRICE_PER_DATASET_ITEM'
20+
FLAT_PRICE_PER_MONTH = 'FLAT_PRICE_PER_MONTH'
21+
FREE = 'FREE'
22+
23+
24+
class GeneralAccessEnum(str, Enum):
25+
"""Defines the general access level for the resource."""
26+
27+
ANYONE_WITH_ID_CAN_READ = 'ANYONE_WITH_ID_CAN_READ'
28+
ANYONE_WITH_NAME_CAN_READ = 'ANYONE_WITH_NAME_CAN_READ'
29+
FOLLOW_USER_SETTING = 'FOLLOW_USER_SETTING'
30+
RESTRICTED = 'RESTRICTED'
1731

1832

1933
@docs_group('Actor')
2034
class Webhook(BaseModel):
21-
__model_config__ = ConfigDict(populate_by_name=True)
35+
model_config = ConfigDict(populate_by_name=True, extra='allow')
2236

2337
event_types: Annotated[
2438
list[WebhookEventType],
@@ -31,22 +45,28 @@ class Webhook(BaseModel):
3145
]
3246
payload_template: Annotated[
3347
str | None,
34-
Field(description='Template for the payload sent by the webook'),
48+
Field(description='Template for the payload sent by the webhook'),
3549
] = None
3650

3751

3852
@docs_group('Actor')
3953
class ActorRunMeta(BaseModel):
40-
__model_config__ = ConfigDict(populate_by_name=True)
54+
model_config = ConfigDict(populate_by_name=True, extra='allow')
4155

4256
origin: Annotated[MetaOrigin, Field()]
57+
client_ip: Annotated[str | None, Field(alias='clientIp')] = None
58+
user_agent: Annotated[str | None, Field(alias='userAgent')] = None
59+
schedule_id: Annotated[str | None, Field(alias='scheduleId')] = None
60+
scheduled_at: Annotated[datetime | None, Field(alias='scheduledAt')] = None
4361

4462

4563
@docs_group('Actor')
4664
class ActorRunStats(BaseModel):
47-
__model_config__ = ConfigDict(populate_by_name=True)
65+
model_config = ConfigDict(populate_by_name=True, extra='allow')
4866

4967
input_body_len: Annotated[int | None, Field(alias='inputBodyLen')] = None
68+
migration_count: Annotated[int | None, Field(alias='migrationCount')] = None
69+
reboot_count: Annotated[int | None, Field(alias='rebootCount')] = None
5070
restart_count: Annotated[int, Field(alias='restartCount')]
5171
resurrect_count: Annotated[int, Field(alias='resurrectCount')]
5272
mem_avg_bytes: Annotated[float | None, Field(alias='memAvgBytes')] = None
@@ -57,26 +77,47 @@ class ActorRunStats(BaseModel):
5777
cpu_current_usage: Annotated[float | None, Field(alias='cpuCurrentUsage')] = None
5878
net_rx_bytes: Annotated[int | None, Field(alias='netRxBytes')] = None
5979
net_tx_bytes: Annotated[int | None, Field(alias='netTxBytes')] = None
60-
duration: Annotated[timedelta_ms | None, Field(alias='durationMillis')] = None
61-
run_time: Annotated[timedelta | None, Field(alias='runTimeSecs')] = None
80+
duration_millis: Annotated[int | None, Field(alias='durationMillis')] = None
81+
run_time_secs: Annotated[float | None, Field(alias='runTimeSecs')] = None
6282
metamorph: Annotated[int | None, Field(alias='metamorph')] = None
6383
compute_units: Annotated[float, Field(alias='computeUnits')]
6484

6585

6686
@docs_group('Actor')
6787
class ActorRunOptions(BaseModel):
68-
__model_config__ = ConfigDict(populate_by_name=True)
88+
model_config = ConfigDict(populate_by_name=True, extra='allow')
6989

7090
build: str
71-
timeout: Annotated[timedelta, Field(alias='timeoutSecs')]
91+
timeout_secs: Annotated[int, Field(alias='timeoutSecs')]
7292
memory_mbytes: Annotated[int, Field(alias='memoryMbytes')]
7393
disk_mbytes: Annotated[int, Field(alias='diskMbytes')]
74-
max_total_charge_usd: Annotated[Decimal | None, Field(alias='maxTotalChargeUsd')] = None
94+
max_items: Annotated[int | None, Field(alias='maxItems')] = None
95+
max_total_charge_usd: Annotated[float | None, Field(alias='maxTotalChargeUsd')] = None
7596

7697

7798
@docs_group('Actor')
7899
class ActorRunUsage(BaseModel):
79-
__model_config__ = ConfigDict(populate_by_name=True)
100+
model_config = ConfigDict(populate_by_name=True, extra='allow')
101+
102+
actor_compute_units: Annotated[float | None, Field(alias='ACTOR_COMPUTE_UNITS')] = None
103+
dataset_reads: Annotated[int | None, Field(alias='DATASET_READS')] = None
104+
dataset_writes: Annotated[int | None, Field(alias='DATASET_WRITES')] = None
105+
key_value_store_reads: Annotated[int | None, Field(alias='KEY_VALUE_STORE_READS')] = None
106+
key_value_store_writes: Annotated[int | None, Field(alias='KEY_VALUE_STORE_WRITES')] = None
107+
key_value_store_lists: Annotated[int | None, Field(alias='KEY_VALUE_STORE_LISTS')] = None
108+
request_queue_reads: Annotated[int | None, Field(alias='REQUEST_QUEUE_READS')] = None
109+
request_queue_writes: Annotated[int | None, Field(alias='REQUEST_QUEUE_WRITES')] = None
110+
data_transfer_internal_gbytes: Annotated[float | None, Field(alias='DATA_TRANSFER_INTERNAL_GBYTES')] = None
111+
data_transfer_external_gbytes: Annotated[float | None, Field(alias='DATA_TRANSFER_EXTERNAL_GBYTES')] = None
112+
proxy_residential_transfer_gbytes: Annotated[float | None, Field(alias='PROXY_RESIDENTIAL_TRANSFER_GBYTES')] = None
113+
proxy_serps: Annotated[int | None, Field(alias='PROXY_SERPS')] = None
114+
115+
116+
@docs_group('Actor')
117+
class ActorRunUsageUsd(BaseModel):
118+
"""Resource usage costs in USD."""
119+
120+
model_config = ConfigDict(populate_by_name=True, extra='allow')
80121

81122
actor_compute_units: Annotated[float | None, Field(alias='ACTOR_COMPUTE_UNITS')] = None
82123
dataset_reads: Annotated[float | None, Field(alias='DATASET_READS')] = None
@@ -92,9 +133,67 @@ class ActorRunUsage(BaseModel):
92133
proxy_serps: Annotated[float | None, Field(alias='PROXY_SERPS')] = None
93134

94135

136+
class Metamorph(BaseModel):
137+
"""Information about a metamorph event that occurred during the run."""
138+
139+
model_config = ConfigDict(populate_by_name=True, extra='allow')
140+
141+
created_at: Annotated[datetime, Field(alias='createdAt')]
142+
actor_id: Annotated[str, Field(alias='actorId')]
143+
build_id: Annotated[str, Field(alias='buildId')]
144+
input_key: Annotated[str | None, Field(alias='inputKey')] = None
145+
146+
147+
class CommonActorPricingInfo(BaseModel):
148+
model_config = ConfigDict(populate_by_name=True, extra='allow')
149+
150+
apify_margin_percentage: Annotated[float | None, Field(alias='apifyMarginPercentage')] = None
151+
created_at: Annotated[datetime | None, Field(alias='createdAt')] = None
152+
started_at: Annotated[datetime | None, Field(alias='startedAt')] = None
153+
notified_about_future_change_at: Annotated[datetime | None, Field(alias='notifiedAboutFutureChangeAt')] = None
154+
notified_about_change_at: Annotated[datetime | None, Field(alias='notifiedAboutChangeAt')] = None
155+
reason_for_change: Annotated[str | None, Field(alias='reasonForChange')] = None
156+
157+
158+
class FreeActorPricingInfo(CommonActorPricingInfo):
159+
pricing_model: Annotated[Literal['FREE'], Field(alias='pricingModel')]
160+
161+
162+
class FlatPricePerMonthActorPricingInfo(CommonActorPricingInfo):
163+
pricing_model: Annotated[Literal['FLAT_PRICE_PER_MONTH'], Field(alias='pricingModel')]
164+
trial_minutes: Annotated[int, Field(alias='trialMinutes')]
165+
price_per_unit_usd: Annotated[float, Field(alias='pricePerUnitUsd')]
166+
167+
168+
class PricePerDatasetItemActorPricingInfo(CommonActorPricingInfo):
169+
pricing_model: Annotated[Literal['PRICE_PER_DATASET_ITEM'], Field(alias='pricingModel')]
170+
unit_name: Annotated[str, Field(alias='unitName')]
171+
price_per_unit_usd: Annotated[float, Field(alias='pricePerUnitUsd')]
172+
173+
174+
class ActorChargeEvent(BaseModel):
175+
model_config = ConfigDict(populate_by_name=True, extra='allow')
176+
177+
event_price_usd: Annotated[float, Field(alias='eventPriceUsd')]
178+
event_title: Annotated[str, Field(alias='eventTitle')]
179+
event_description: Annotated[str | None, Field(alias='eventDescription')] = None
180+
181+
182+
class PricingPerEvent(BaseModel):
183+
model_config = ConfigDict(populate_by_name=True, extra='allow')
184+
185+
actor_charge_events: Annotated[dict[str, ActorChargeEvent] | None, Field(alias='actorChargeEvents')] = None
186+
187+
188+
class PayPerEventActorPricingInfo(CommonActorPricingInfo):
189+
pricing_model: Annotated[Literal['PAY_PER_EVENT'], Field(alias='pricingModel')]
190+
pricing_per_event: Annotated[PricingPerEvent, Field(alias='pricingPerEvent')]
191+
minimal_max_total_charge_usd: Annotated[float | None, Field(alias='minimalMaxTotalChargeUsd')] = None
192+
193+
95194
@docs_group('Actor')
96195
class ActorRun(BaseModel):
97-
__model_config__ = ConfigDict(populate_by_name=True)
196+
model_config = ConfigDict(populate_by_name=True, extra='allow')
98197

99198
id: Annotated[str, Field(alias='id')]
100199
act_id: Annotated[str, Field(alias='actId')]
@@ -110,16 +209,17 @@ class ActorRun(BaseModel):
110209
options: Annotated[ActorRunOptions, Field(alias='options')]
111210
build_id: Annotated[str, Field(alias='buildId')]
112211
exit_code: Annotated[int | None, Field(alias='exitCode')] = None
212+
general_access: Annotated[str | None, Field(alias='generalAccess')] = None
113213
default_key_value_store_id: Annotated[str, Field(alias='defaultKeyValueStoreId')]
114214
default_dataset_id: Annotated[str, Field(alias='defaultDatasetId')]
115215
default_request_queue_id: Annotated[str, Field(alias='defaultRequestQueueId')]
116216
build_number: Annotated[str | None, Field(alias='buildNumber')] = None
117-
container_url: Annotated[str, Field(alias='containerUrl')]
217+
container_url: Annotated[str | None, Field(alias='containerUrl')] = None
118218
is_container_server_ready: Annotated[bool | None, Field(alias='isContainerServerReady')] = None
119219
git_branch_name: Annotated[str | None, Field(alias='gitBranchName')] = None
120220
usage: Annotated[ActorRunUsage | None, Field(alias='usage')] = None
121221
usage_total_usd: Annotated[float | None, Field(alias='usageTotalUsd')] = None
122-
usage_usd: Annotated[ActorRunUsage | None, Field(alias='usageUsd')] = None
222+
usage_usd: Annotated[ActorRunUsageUsd | None, Field(alias='usageUsd')] = None
123223
pricing_info: Annotated[
124224
FreeActorPricingInfo
125225
| FlatPricePerMonthActorPricingInfo
@@ -132,43 +232,4 @@ class ActorRun(BaseModel):
132232
dict[str, int] | None,
133233
Field(alias='chargedEventCounts'),
134234
] = None
135-
136-
137-
class FreeActorPricingInfo(BaseModel):
138-
pricing_model: Annotated[Literal['FREE'], Field(alias='pricingModel')]
139-
140-
141-
class FlatPricePerMonthActorPricingInfo(BaseModel):
142-
pricing_model: Annotated[Literal['FLAT_PRICE_PER_MONTH'], Field(alias='pricingModel')]
143-
trial_minutes: Annotated[int | None, Field(alias='trialMinutes')] = None
144-
price_per_unit_usd: Annotated[Decimal, Field(alias='pricePerUnitUsd')]
145-
146-
147-
class PricePerDatasetItemActorPricingInfo(BaseModel):
148-
pricing_model: Annotated[Literal['PRICE_PER_DATASET_ITEM'], Field(alias='pricingModel')]
149-
unit_name: Annotated[str | None, Field(alias='unitName')] = None
150-
price_per_unit_usd: Annotated[Decimal, Field(alias='pricePerUnitUsd')]
151-
152-
153-
class ActorChargeEvent(BaseModel):
154-
event_price_usd: Annotated[Decimal, Field(alias='eventPriceUsd')]
155-
event_title: Annotated[str, Field(alias='eventTitle')]
156-
event_description: Annotated[str | None, Field(alias='eventDescription')] = None
157-
158-
159-
class PricingPerEvent(BaseModel):
160-
actor_charge_events: Annotated[dict[str, ActorChargeEvent], Field(alias='actorChargeEvents')]
161-
162-
163-
class PayPerEventActorPricingInfo(BaseModel):
164-
pricing_model: Annotated[Literal['PAY_PER_EVENT'], Field(alias='pricingModel')]
165-
pricing_per_event: Annotated[PricingPerEvent, Field(alias='pricingPerEvent')]
166-
minimal_max_total_charge_usd: Annotated[Decimal | None, Field(alias='minimalMaxTotalChargeUsd')] = None
167-
168-
169-
PricingModel: TypeAlias = Literal[
170-
'FREE',
171-
'FLAT_PRICE_PER_MONTH',
172-
'PRICE_PER_DATASET_ITEM',
173-
'PAY_PER_EVENT',
174-
]
235+
metamorphs: Annotated[list[Metamorph] | None, Field(alias='metamorphs')] = None

tests/integration/actor/test_actor_call_timeouts.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ async def test_actor_start_inherit_timeout(
1919
Timeout should be the remaining time of the first Actor run calculated at the moment of the other Actor start."""
2020

2121
async def main() -> None:
22-
from datetime import datetime, timezone
22+
from datetime import datetime, timedelta, timezone
2323

2424
async with Actor:
2525
actor_input = (await Actor.get_input()) or {}
@@ -44,8 +44,11 @@ async def main() -> None:
4444

4545
remaining_time_after_actor_start = Actor.configuration.timeout_at - datetime.now(tz=timezone.utc)
4646

47-
assert other_run_data.options.timeout > remaining_time_after_actor_start
48-
assert other_run_data.options.timeout < Actor.configuration.timeout_at - Actor.configuration.started_at
47+
other_timeout = timedelta(seconds=other_run_data.options.timeout_secs)
48+
total_timeout = Actor.configuration.timeout_at - Actor.configuration.started_at
49+
50+
assert other_timeout > remaining_time_after_actor_start
51+
assert other_timeout < total_timeout
4952
finally:
5053
# Make sure the other actor run is aborted
5154
await Actor.apify_client.run(other_run_data.id).abort()
@@ -66,7 +69,7 @@ async def test_actor_call_inherit_timeout(
6669
Timeout should be the remaining time of the first Actor run calculated at the moment of the other Actor call."""
6770

6871
async def main() -> None:
69-
from datetime import datetime, timezone
72+
from datetime import datetime, timedelta, timezone
7073

7174
async with Actor:
7275
actor_input = (await Actor.get_input()) or {}
@@ -93,8 +96,11 @@ async def main() -> None:
9396

9497
remaining_time_after_actor_start = Actor.configuration.timeout_at - datetime.now(tz=timezone.utc)
9598

96-
assert other_run_data.options.timeout > remaining_time_after_actor_start
97-
assert other_run_data.options.timeout < Actor.configuration.timeout_at - Actor.configuration.started_at
99+
other_timeout = timedelta(seconds=other_run_data.options.timeout_secs)
100+
total_timeout = Actor.configuration.timeout_at - Actor.configuration.started_at
101+
102+
assert other_timeout > remaining_time_after_actor_start
103+
assert other_timeout < total_timeout
98104
finally:
99105
# Make sure the other actor run is aborted
100106
await Actor.apify_client.run(other_run_data.id).abort()

0 commit comments

Comments
 (0)