Skip to content

Commit c846719

Browse files
committed
fix: align SDK models with the API behavior
Update Pydantic models in _models.py and storage client models to match the typed models from apify-client-python. This adds missing fields to Webhook, ActorRunMeta, ActorRunStats, ActorRunOptions, ActorRunUsage, and ActorRun models, introduces new helper models (PricingModel enum, WebhookCondition, WebhookStats, Metamorph, ActorRunUsageUsd), adds extra='allow' to all models for forward compatibility, and fixes type inconsistencies (Decimal vs float, timedelta vs raw int/float fields).
1 parent 678b4db commit c846719

File tree

4 files changed

+194
-77
lines changed

4 files changed

+194
-77
lines changed

src/apify/_charging.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,15 +156,16 @@ async def __aenter__(self) -> None:
156156

157157
# Set pricing model
158158
if self._configuration.test_pay_per_event:
159-
self._pricing_model = 'PAY_PER_EVENT'
159+
self._pricing_model = PricingModel.PAY_PER_EVENT
160160
else:
161-
self._pricing_model = pricing_info.pricing_model if pricing_info is not None else None
161+
self._pricing_model = PricingModel(pricing_info.pricing_model) if pricing_info else None
162162

163163
# Load per-event pricing information
164164
if pricing_info is not None and isinstance(pricing_info, PayPerEventActorPricingInfo):
165-
for event_name, event_pricing in pricing_info.pricing_per_event.actor_charge_events.items():
165+
actor_charge_events = pricing_info.pricing_per_event.actor_charge_events or {}
166+
for event_name, event_pricing in actor_charge_events.items():
166167
self._pricing_info[event_name] = PricingInfoItem(
167-
price=event_pricing.event_price_usd,
168+
price=Decimal(str(event_pricing.event_price_usd)),
168169
title=event_pricing.event_title,
169170
)
170171

@@ -355,10 +356,11 @@ async def _fetch_pricing_info(self) -> _FetchedPricingInfoDict:
355356
if run is None:
356357
raise RuntimeError('Actor run not found')
357358

359+
max_charge = run.options.max_total_charge_usd
358360
return _FetchedPricingInfoDict(
359361
pricing_info=run.pricing_info,
360362
charged_event_counts=run.charged_event_counts or {},
361-
max_total_charge_usd=run.options.max_total_charge_usd or Decimal('inf'),
363+
max_total_charge_usd=Decimal(str(max_charge)) if max_charge is not None else Decimal('inf'),
362364
)
363365

364366
# Local development without environment variables

src/apify/_models.py

Lines changed: 170 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,120 @@
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'
31+
32+
33+
class WebhookCondition(BaseModel):
34+
"""Condition for triggering a webhook."""
35+
36+
model_config = ConfigDict(populate_by_name=True, extra='allow')
37+
38+
actor_id: Annotated[str | None, Field(alias='actorId')] = None
39+
actor_task_id: Annotated[str | None, Field(alias='actorTaskId')] = None
40+
actor_run_id: Annotated[str | None, Field(alias='actorRunId')] = None
41+
42+
43+
class WebhookDispatchStatus(str, Enum):
44+
"""Status of a webhook dispatch."""
45+
46+
ACTIVE = 'ACTIVE'
47+
SUCCEEDED = 'SUCCEEDED'
48+
FAILED = 'FAILED'
49+
50+
51+
class ExampleWebhookDispatch(BaseModel):
52+
"""Information about a webhook dispatch."""
53+
54+
model_config = ConfigDict(populate_by_name=True, extra='allow')
55+
56+
status: WebhookDispatchStatus
57+
finished_at: Annotated[datetime, Field(alias='finishedAt')]
58+
59+
60+
class WebhookStats(BaseModel):
61+
"""Statistics about webhook dispatches."""
62+
63+
model_config = ConfigDict(populate_by_name=True, extra='allow')
64+
65+
total_dispatches: Annotated[int, Field(alias='totalDispatches')]
1766

1867

1968
@docs_group('Actor')
2069
class Webhook(BaseModel):
21-
__model_config__ = ConfigDict(populate_by_name=True)
70+
model_config = ConfigDict(populate_by_name=True, extra='allow')
2271

2372
event_types: Annotated[
2473
list[WebhookEventType],
25-
Field(description='Event types that should trigger the webhook'),
74+
Field(alias='eventTypes', description='Event types that should trigger the webhook'),
2675
]
2776
request_url: Annotated[
2877
str,
29-
Field(description='URL that the webhook should call'),
78+
Field(alias='requestUrl', description='URL that the webhook should call'),
3079
BeforeValidator(validate_http_url),
3180
]
81+
id: Annotated[str | None, Field(alias='id')] = None
82+
created_at: Annotated[datetime | None, Field(alias='createdAt')] = None
83+
modified_at: Annotated[datetime | None, Field(alias='modifiedAt')] = None
84+
user_id: Annotated[str | None, Field(alias='userId')] = None
85+
is_ad_hoc: Annotated[bool | None, Field(alias='isAdHoc')] = None
86+
should_interpolate_strings: Annotated[bool | None, Field(alias='shouldInterpolateStrings')] = None
87+
condition: Annotated[WebhookCondition | None, Field(alias='condition')] = None
88+
ignore_ssl_errors: Annotated[bool | None, Field(alias='ignoreSslErrors')] = None
89+
do_not_retry: Annotated[bool | None, Field(alias='doNotRetry')] = None
3290
payload_template: Annotated[
3391
str | None,
34-
Field(description='Template for the payload sent by the webook'),
92+
Field(alias='payloadTemplate', description='Template for the payload sent by the webhook'),
3593
] = None
94+
headers_template: Annotated[str | None, Field(alias='headersTemplate')] = None
95+
description: Annotated[str | None, Field(alias='description')] = None
96+
last_dispatch: Annotated[ExampleWebhookDispatch | None, Field(alias='lastDispatch')] = None
97+
stats: Annotated[WebhookStats | None, Field(alias='stats')] = None
3698

3799

38100
@docs_group('Actor')
39101
class ActorRunMeta(BaseModel):
40-
__model_config__ = ConfigDict(populate_by_name=True)
102+
model_config = ConfigDict(populate_by_name=True, extra='allow')
41103

42104
origin: Annotated[MetaOrigin, Field()]
105+
client_ip: Annotated[str | None, Field(alias='clientIp')] = None
106+
user_agent: Annotated[str | None, Field(alias='userAgent')] = None
107+
schedule_id: Annotated[str | None, Field(alias='scheduleId')] = None
108+
scheduled_at: Annotated[datetime | None, Field(alias='scheduledAt')] = None
43109

44110

45111
@docs_group('Actor')
46112
class ActorRunStats(BaseModel):
47-
__model_config__ = ConfigDict(populate_by_name=True)
113+
model_config = ConfigDict(populate_by_name=True, extra='allow')
48114

49115
input_body_len: Annotated[int | None, Field(alias='inputBodyLen')] = None
116+
migration_count: Annotated[int | None, Field(alias='migrationCount')] = None
117+
reboot_count: Annotated[int | None, Field(alias='rebootCount')] = None
50118
restart_count: Annotated[int, Field(alias='restartCount')]
51119
resurrect_count: Annotated[int, Field(alias='resurrectCount')]
52120
mem_avg_bytes: Annotated[float | None, Field(alias='memAvgBytes')] = None
@@ -57,26 +125,47 @@ class ActorRunStats(BaseModel):
57125
cpu_current_usage: Annotated[float | None, Field(alias='cpuCurrentUsage')] = None
58126
net_rx_bytes: Annotated[int | None, Field(alias='netRxBytes')] = None
59127
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
128+
duration_millis: Annotated[int | None, Field(alias='durationMillis')] = None
129+
run_time_secs: Annotated[float | None, Field(alias='runTimeSecs')] = None
62130
metamorph: Annotated[int | None, Field(alias='metamorph')] = None
63131
compute_units: Annotated[float, Field(alias='computeUnits')]
64132

65133

66134
@docs_group('Actor')
67135
class ActorRunOptions(BaseModel):
68-
__model_config__ = ConfigDict(populate_by_name=True)
136+
model_config = ConfigDict(populate_by_name=True, extra='allow')
69137

70138
build: str
71-
timeout: Annotated[timedelta, Field(alias='timeoutSecs')]
139+
timeout_secs: Annotated[int, Field(alias='timeoutSecs')]
72140
memory_mbytes: Annotated[int, Field(alias='memoryMbytes')]
73141
disk_mbytes: Annotated[int, Field(alias='diskMbytes')]
74-
max_total_charge_usd: Annotated[Decimal | None, Field(alias='maxTotalChargeUsd')] = None
142+
max_items: Annotated[int | None, Field(alias='maxItems')] = None
143+
max_total_charge_usd: Annotated[float | None, Field(alias='maxTotalChargeUsd')] = None
75144

76145

77146
@docs_group('Actor')
78147
class ActorRunUsage(BaseModel):
79-
__model_config__ = ConfigDict(populate_by_name=True)
148+
model_config = ConfigDict(populate_by_name=True, extra='allow')
149+
150+
actor_compute_units: Annotated[float | None, Field(alias='ACTOR_COMPUTE_UNITS')] = None
151+
dataset_reads: Annotated[int | None, Field(alias='DATASET_READS')] = None
152+
dataset_writes: Annotated[int | None, Field(alias='DATASET_WRITES')] = None
153+
key_value_store_reads: Annotated[int | None, Field(alias='KEY_VALUE_STORE_READS')] = None
154+
key_value_store_writes: Annotated[int | None, Field(alias='KEY_VALUE_STORE_WRITES')] = None
155+
key_value_store_lists: Annotated[int | None, Field(alias='KEY_VALUE_STORE_LISTS')] = None
156+
request_queue_reads: Annotated[int | None, Field(alias='REQUEST_QUEUE_READS')] = None
157+
request_queue_writes: Annotated[int | None, Field(alias='REQUEST_QUEUE_WRITES')] = None
158+
data_transfer_internal_gbytes: Annotated[float | None, Field(alias='DATA_TRANSFER_INTERNAL_GBYTES')] = None
159+
data_transfer_external_gbytes: Annotated[float | None, Field(alias='DATA_TRANSFER_EXTERNAL_GBYTES')] = None
160+
proxy_residential_transfer_gbytes: Annotated[float | None, Field(alias='PROXY_RESIDENTIAL_TRANSFER_GBYTES')] = None
161+
proxy_serps: Annotated[int | None, Field(alias='PROXY_SERPS')] = None
162+
163+
164+
@docs_group('Actor')
165+
class ActorRunUsageUsd(BaseModel):
166+
"""Resource usage costs in USD."""
167+
168+
model_config = ConfigDict(populate_by_name=True, extra='allow')
80169

81170
actor_compute_units: Annotated[float | None, Field(alias='ACTOR_COMPUTE_UNITS')] = None
82171
dataset_reads: Annotated[float | None, Field(alias='DATASET_READS')] = None
@@ -92,9 +181,67 @@ class ActorRunUsage(BaseModel):
92181
proxy_serps: Annotated[float | None, Field(alias='PROXY_SERPS')] = None
93182

94183

184+
class Metamorph(BaseModel):
185+
"""Information about a metamorph event that occurred during the run."""
186+
187+
model_config = ConfigDict(populate_by_name=True, extra='allow')
188+
189+
created_at: Annotated[datetime, Field(alias='createdAt')]
190+
actor_id: Annotated[str, Field(alias='actorId')]
191+
build_id: Annotated[str, Field(alias='buildId')]
192+
input_key: Annotated[str | None, Field(alias='inputKey')] = None
193+
194+
195+
class CommonActorPricingInfo(BaseModel):
196+
model_config = ConfigDict(populate_by_name=True, extra='allow')
197+
198+
apify_margin_percentage: Annotated[float | None, Field(alias='apifyMarginPercentage')] = None
199+
created_at: Annotated[datetime | None, Field(alias='createdAt')] = None
200+
started_at: Annotated[datetime | None, Field(alias='startedAt')] = None
201+
notified_about_future_change_at: Annotated[datetime | None, Field(alias='notifiedAboutFutureChangeAt')] = None
202+
notified_about_change_at: Annotated[datetime | None, Field(alias='notifiedAboutChangeAt')] = None
203+
reason_for_change: Annotated[str | None, Field(alias='reasonForChange')] = None
204+
205+
206+
class FreeActorPricingInfo(CommonActorPricingInfo):
207+
pricing_model: Annotated[Literal['FREE'], Field(alias='pricingModel')]
208+
209+
210+
class FlatPricePerMonthActorPricingInfo(CommonActorPricingInfo):
211+
pricing_model: Annotated[Literal['FLAT_PRICE_PER_MONTH'], Field(alias='pricingModel')]
212+
trial_minutes: Annotated[int, Field(alias='trialMinutes')]
213+
price_per_unit_usd: Annotated[float, Field(alias='pricePerUnitUsd')]
214+
215+
216+
class PricePerDatasetItemActorPricingInfo(CommonActorPricingInfo):
217+
pricing_model: Annotated[Literal['PRICE_PER_DATASET_ITEM'], Field(alias='pricingModel')]
218+
unit_name: Annotated[str, Field(alias='unitName')]
219+
price_per_unit_usd: Annotated[float, Field(alias='pricePerUnitUsd')]
220+
221+
222+
class ActorChargeEvent(BaseModel):
223+
model_config = ConfigDict(populate_by_name=True, extra='allow')
224+
225+
event_price_usd: Annotated[float, Field(alias='eventPriceUsd')]
226+
event_title: Annotated[str, Field(alias='eventTitle')]
227+
event_description: Annotated[str | None, Field(alias='eventDescription')] = None
228+
229+
230+
class PricingPerEvent(BaseModel):
231+
model_config = ConfigDict(populate_by_name=True, extra='allow')
232+
233+
actor_charge_events: Annotated[dict[str, ActorChargeEvent] | None, Field(alias='actorChargeEvents')] = None
234+
235+
236+
class PayPerEventActorPricingInfo(CommonActorPricingInfo):
237+
pricing_model: Annotated[Literal['PAY_PER_EVENT'], Field(alias='pricingModel')]
238+
pricing_per_event: Annotated[PricingPerEvent, Field(alias='pricingPerEvent')]
239+
minimal_max_total_charge_usd: Annotated[float | None, Field(alias='minimalMaxTotalChargeUsd')] = None
240+
241+
95242
@docs_group('Actor')
96243
class ActorRun(BaseModel):
97-
__model_config__ = ConfigDict(populate_by_name=True)
244+
model_config = ConfigDict(populate_by_name=True, extra='allow')
98245

99246
id: Annotated[str, Field(alias='id')]
100247
act_id: Annotated[str, Field(alias='actId')]
@@ -110,16 +257,17 @@ class ActorRun(BaseModel):
110257
options: Annotated[ActorRunOptions, Field(alias='options')]
111258
build_id: Annotated[str, Field(alias='buildId')]
112259
exit_code: Annotated[int | None, Field(alias='exitCode')] = None
260+
general_access: Annotated[str | None, Field(alias='generalAccess')] = None
113261
default_key_value_store_id: Annotated[str, Field(alias='defaultKeyValueStoreId')]
114262
default_dataset_id: Annotated[str, Field(alias='defaultDatasetId')]
115263
default_request_queue_id: Annotated[str, Field(alias='defaultRequestQueueId')]
116264
build_number: Annotated[str | None, Field(alias='buildNumber')] = None
117-
container_url: Annotated[str, Field(alias='containerUrl')]
265+
container_url: Annotated[str | None, Field(alias='containerUrl')] = None
118266
is_container_server_ready: Annotated[bool | None, Field(alias='isContainerServerReady')] = None
119267
git_branch_name: Annotated[str | None, Field(alias='gitBranchName')] = None
120268
usage: Annotated[ActorRunUsage | None, Field(alias='usage')] = None
121269
usage_total_usd: Annotated[float | None, Field(alias='usageTotalUsd')] = None
122-
usage_usd: Annotated[ActorRunUsage | None, Field(alias='usageUsd')] = None
270+
usage_usd: Annotated[ActorRunUsageUsd | None, Field(alias='usageUsd')] = None
123271
pricing_info: Annotated[
124272
FreeActorPricingInfo
125273
| FlatPricePerMonthActorPricingInfo
@@ -132,43 +280,4 @@ class ActorRun(BaseModel):
132280
dict[str, int] | None,
133281
Field(alias='chargedEventCounts'),
134282
] = 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-
]
283+
metamorphs: Annotated[list[Metamorph] | None, Field(alias='metamorphs')] = None

src/apify/storage_clients/_apify/_models.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class ApifyKeyValueStoreMetadata(KeyValueStoreMetadata):
2626
class ProlongRequestLockResponse(BaseModel):
2727
"""Response to prolong request lock calls."""
2828

29-
model_config = ConfigDict(populate_by_name=True)
29+
model_config = ConfigDict(populate_by_name=True, extra='allow')
3030

3131
lock_expires_at: Annotated[datetime, Field(alias='lockExpiresAt')]
3232

@@ -39,7 +39,7 @@ class RequestQueueHead(BaseModel):
3939
including metadata about the queue's state and lock information for the requests.
4040
"""
4141

42-
model_config = ConfigDict(populate_by_name=True)
42+
model_config = ConfigDict(populate_by_name=True, extra='allow')
4343

4444
limit: Annotated[int | None, Field(alias='limit', default=None)]
4545
"""The maximum number of requests that were requested from the queue."""
@@ -66,7 +66,7 @@ class KeyValueStoreKeyInfo(BaseModel):
6666
Only internal structure.
6767
"""
6868

69-
model_config = ConfigDict(populate_by_name=True)
69+
model_config = ConfigDict(populate_by_name=True, extra='allow')
7070

7171
key: Annotated[str, Field(alias='key')]
7272
size: Annotated[int, Field(alias='size')]
@@ -78,7 +78,7 @@ class KeyValueStoreListKeysPage(BaseModel):
7878
Only internal structure.
7979
"""
8080

81-
model_config = ConfigDict(populate_by_name=True)
81+
model_config = ConfigDict(populate_by_name=True, extra='allow')
8282

8383
count: Annotated[int, Field(alias='count')]
8484
limit: Annotated[int, Field(alias='limit')]
@@ -108,7 +108,7 @@ class CachedRequest(BaseModel):
108108

109109

110110
class RequestQueueStats(BaseModel):
111-
model_config = ConfigDict(populate_by_name=True)
111+
model_config = ConfigDict(populate_by_name=True, extra='allow')
112112

113113
delete_count: Annotated[int, Field(alias='deleteCount', default=0)]
114114
""""The number of request queue deletes."""

0 commit comments

Comments
 (0)