Skip to content

Commit d15fadb

Browse files
committed
Merge remote-tracking branch 'origin/master' into alias-storage-configuration-mapping
2 parents e2df853 + 9e0aa56 commit d15fadb

File tree

9 files changed

+254
-123
lines changed

9 files changed

+254
-123
lines changed

.github/workflows/_tests.yaml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,7 @@ jobs:
8585
(github.event_name == 'push' && github.ref == 'refs/heads/master')
8686
}}
8787
88-
# E2E tests build and run Actors on the platform. The combination of max-parallel 2 with 16 pytest
89-
# workers and a global concurrency group is a compromise between stability and performance - it keeps
90-
# the platform's resource usage in check while still allowing reasonable test throughput.
91-
concurrency:
92-
group: e2e-tests
93-
cancel-in-progress: false
94-
9588
strategy:
96-
max-parallel: 2
9789
matrix:
9890
os: ["ubuntu-latest"]
9991
python-version: ["3.10", "3.14"]

.github/workflows/on_master.yaml

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ jobs:
1717
uses: ./.github/workflows/_check_docs.yaml
1818

1919
doc_release:
20-
# Skip this for non-docs commits and forks.
21-
if: "startsWith(github.event.head_commit.message, 'docs') && startsWith(github.repository, 'apify/')"
20+
# Skip this for non-"docs" commits.
21+
if: startsWith(github.event.head_commit.message, 'docs')
2222
name: Doc release
2323
needs: [doc_checks]
2424
uses: ./.github/workflows/_release_docs.yaml
@@ -32,15 +32,20 @@ jobs:
3232
uses: ./.github/workflows/_check_code.yaml
3333

3434
tests:
35-
# Skip this for "ci" and "docs" commits.
36-
if: "!startsWith(github.event.head_commit.message, 'ci') && !startsWith(github.event.head_commit.message, 'docs')"
35+
# Skip this for "docs" commits.
36+
if: "!startsWith(github.event.head_commit.message, 'docs')"
3737
name: Tests
3838
uses: ./.github/workflows/_tests.yaml
3939
secrets: inherit
4040

4141
release_prepare:
42-
# Skip this for "ci", "docs" and "test" commits and for forks.
43-
if: "!startsWith(github.event.head_commit.message, 'ci') && !startsWith(github.event.head_commit.message, 'docs') && !startsWith(github.event.head_commit.message, 'test') && startsWith(github.repository, 'apify/')"
42+
# Run this only for "feat", "fix", "perf", "refactor" and "style" commits.
43+
if: >-
44+
startsWith(github.event.head_commit.message, 'feat') ||
45+
startsWith(github.event.head_commit.message, 'fix') ||
46+
startsWith(github.event.head_commit.message, 'perf') ||
47+
startsWith(github.event.head_commit.message, 'refactor') ||
48+
startsWith(github.event.head_commit.message, 'style')
4449
name: Release prepare
4550
needs: [code_checks, tests]
4651
runs-on: ubuntu-latest

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
88
### 🐛 Bug Fixes
99

1010
- Resolve LogRecord attribute conflict in event manager logging ([#802](https://github.com/apify/apify-sdk-python/pull/802)) ([e1bdbc9](https://github.com/apify/apify-sdk-python/commit/e1bdbc9e303c24571b9511f43ec0815e7e9f4b55)) by [@vdusek](https://github.com/vdusek)
11+
- Update models.py to align with the current API behavior ([#782](https://github.com/apify/apify-sdk-python/pull/782)) ([b06355d](https://github.com/apify/apify-sdk-python/commit/b06355dbc1c8276e9930ecbde72795b6570dde33)) by [@vdusek](https://github.com/vdusek), closes [#778](https://github.com/apify/apify-sdk-python/issues/778)
1112

1213

1314
<!-- git-cliff-unreleased-end -->

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 = 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: 153 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,103 @@
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 typing import Annotated, Literal
65

76
from pydantic import BaseModel, BeforeValidator, ConfigDict, Field
87

98
from apify_shared.consts import ActorJobStatus, MetaOrigin, WebhookEventType
10-
from crawlee._utils.models import timedelta_ms
119
from crawlee._utils.urls import validate_http_url
1210

1311
from apify._utils import docs_group
1412

15-
if TYPE_CHECKING:
16-
from typing import TypeAlias
13+
PricingModel = Literal['PAY_PER_EVENT', 'PRICE_PER_DATASET_ITEM', 'FLAT_PRICE_PER_MONTH', 'FREE']
14+
"""Pricing model for an Actor."""
15+
16+
GeneralAccess = Literal['ANYONE_WITH_ID_CAN_READ', 'ANYONE_WITH_NAME_CAN_READ', 'FOLLOW_USER_SETTING', 'RESTRICTED']
17+
"""Defines the general access level for the resource."""
18+
19+
20+
class WebhookCondition(BaseModel):
21+
"""Condition for triggering a webhook."""
22+
23+
model_config = ConfigDict(populate_by_name=True, extra='allow')
24+
25+
actor_id: Annotated[str | None, Field(alias='actorId')] = None
26+
actor_task_id: Annotated[str | None, Field(alias='actorTaskId')] = None
27+
actor_run_id: Annotated[str | None, Field(alias='actorRunId')] = None
28+
29+
30+
WebhookDispatchStatus = Literal['ACTIVE', 'SUCCEEDED', 'FAILED']
31+
"""Status of a webhook dispatch."""
32+
33+
34+
class ExampleWebhookDispatch(BaseModel):
35+
"""Information about a webhook dispatch."""
36+
37+
model_config = ConfigDict(populate_by_name=True, extra='allow')
38+
39+
status: WebhookDispatchStatus
40+
finished_at: Annotated[datetime, Field(alias='finishedAt')]
41+
42+
43+
class WebhookStats(BaseModel):
44+
"""Statistics about webhook dispatches."""
45+
46+
model_config = ConfigDict(populate_by_name=True, extra='allow')
47+
48+
total_dispatches: Annotated[int, Field(alias='totalDispatches')]
1749

1850

1951
@docs_group('Actor')
2052
class Webhook(BaseModel):
21-
__model_config__ = ConfigDict(populate_by_name=True)
53+
model_config = ConfigDict(populate_by_name=True, extra='allow')
2254

2355
event_types: Annotated[
2456
list[WebhookEventType],
25-
Field(description='Event types that should trigger the webhook'),
57+
Field(alias='eventTypes', description='Event types that should trigger the webhook'),
2658
]
2759
request_url: Annotated[
2860
str,
29-
Field(description='URL that the webhook should call'),
61+
Field(alias='requestUrl', description='URL that the webhook should call'),
3062
BeforeValidator(validate_http_url),
3163
]
64+
id: Annotated[str | None, Field(alias='id')] = None
65+
created_at: Annotated[datetime | None, Field(alias='createdAt')] = None
66+
modified_at: Annotated[datetime | None, Field(alias='modifiedAt')] = None
67+
user_id: Annotated[str | None, Field(alias='userId')] = None
68+
is_ad_hoc: Annotated[bool | None, Field(alias='isAdHoc')] = None
69+
should_interpolate_strings: Annotated[bool | None, Field(alias='shouldInterpolateStrings')] = None
70+
condition: Annotated[WebhookCondition | None, Field(alias='condition')] = None
71+
ignore_ssl_errors: Annotated[bool | None, Field(alias='ignoreSslErrors')] = None
72+
do_not_retry: Annotated[bool | None, Field(alias='doNotRetry')] = None
3273
payload_template: Annotated[
3374
str | None,
34-
Field(description='Template for the payload sent by the webook'),
75+
Field(alias='payloadTemplate', description='Template for the payload sent by the webhook'),
3576
] = None
77+
headers_template: Annotated[str | None, Field(alias='headersTemplate')] = None
78+
description: Annotated[str | None, Field(alias='description')] = None
79+
last_dispatch: Annotated[ExampleWebhookDispatch | None, Field(alias='lastDispatch')] = None
80+
stats: Annotated[WebhookStats | None, Field(alias='stats')] = None
3681

3782

3883
@docs_group('Actor')
3984
class ActorRunMeta(BaseModel):
40-
__model_config__ = ConfigDict(populate_by_name=True)
85+
model_config = ConfigDict(populate_by_name=True, extra='allow')
4186

4287
origin: Annotated[MetaOrigin, Field()]
88+
client_ip: Annotated[str | None, Field(alias='clientIp')] = None
89+
user_agent: Annotated[str | None, Field(alias='userAgent')] = None
90+
schedule_id: Annotated[str | None, Field(alias='scheduleId')] = None
91+
scheduled_at: Annotated[datetime | None, Field(alias='scheduledAt')] = None
4392

4493

4594
@docs_group('Actor')
4695
class ActorRunStats(BaseModel):
47-
__model_config__ = ConfigDict(populate_by_name=True)
96+
model_config = ConfigDict(populate_by_name=True, extra='allow')
4897

4998
input_body_len: Annotated[int | None, Field(alias='inputBodyLen')] = None
99+
migration_count: Annotated[int | None, Field(alias='migrationCount')] = None
100+
reboot_count: Annotated[int | None, Field(alias='rebootCount')] = None
50101
restart_count: Annotated[int, Field(alias='restartCount')]
51102
resurrect_count: Annotated[int, Field(alias='resurrectCount')]
52103
mem_avg_bytes: Annotated[float | None, Field(alias='memAvgBytes')] = None
@@ -57,26 +108,47 @@ class ActorRunStats(BaseModel):
57108
cpu_current_usage: Annotated[float | None, Field(alias='cpuCurrentUsage')] = None
58109
net_rx_bytes: Annotated[int | None, Field(alias='netRxBytes')] = None
59110
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
111+
duration_millis: Annotated[int | None, Field(alias='durationMillis')] = None
112+
run_time_secs: Annotated[float | None, Field(alias='runTimeSecs')] = None
62113
metamorph: Annotated[int | None, Field(alias='metamorph')] = None
63114
compute_units: Annotated[float, Field(alias='computeUnits')]
64115

65116

66117
@docs_group('Actor')
67118
class ActorRunOptions(BaseModel):
68-
__model_config__ = ConfigDict(populate_by_name=True)
119+
model_config = ConfigDict(populate_by_name=True, extra='allow')
69120

70121
build: str
71-
timeout: Annotated[timedelta, Field(alias='timeoutSecs')]
122+
timeout_secs: Annotated[int, Field(alias='timeoutSecs')]
72123
memory_mbytes: Annotated[int, Field(alias='memoryMbytes')]
73124
disk_mbytes: Annotated[int, Field(alias='diskMbytes')]
74-
max_total_charge_usd: Annotated[Decimal | None, Field(alias='maxTotalChargeUsd')] = None
125+
max_items: Annotated[int | None, Field(alias='maxItems')] = None
126+
max_total_charge_usd: Annotated[float | None, Field(alias='maxTotalChargeUsd')] = None
75127

76128

77129
@docs_group('Actor')
78130
class ActorRunUsage(BaseModel):
79-
__model_config__ = ConfigDict(populate_by_name=True)
131+
model_config = ConfigDict(populate_by_name=True, extra='allow')
132+
133+
actor_compute_units: Annotated[float | None, Field(alias='ACTOR_COMPUTE_UNITS')] = None
134+
dataset_reads: Annotated[int | None, Field(alias='DATASET_READS')] = None
135+
dataset_writes: Annotated[int | None, Field(alias='DATASET_WRITES')] = None
136+
key_value_store_reads: Annotated[int | None, Field(alias='KEY_VALUE_STORE_READS')] = None
137+
key_value_store_writes: Annotated[int | None, Field(alias='KEY_VALUE_STORE_WRITES')] = None
138+
key_value_store_lists: Annotated[int | None, Field(alias='KEY_VALUE_STORE_LISTS')] = None
139+
request_queue_reads: Annotated[int | None, Field(alias='REQUEST_QUEUE_READS')] = None
140+
request_queue_writes: Annotated[int | None, Field(alias='REQUEST_QUEUE_WRITES')] = None
141+
data_transfer_internal_gbytes: Annotated[float | None, Field(alias='DATA_TRANSFER_INTERNAL_GBYTES')] = None
142+
data_transfer_external_gbytes: Annotated[float | None, Field(alias='DATA_TRANSFER_EXTERNAL_GBYTES')] = None
143+
proxy_residential_transfer_gbytes: Annotated[float | None, Field(alias='PROXY_RESIDENTIAL_TRANSFER_GBYTES')] = None
144+
proxy_serps: Annotated[int | None, Field(alias='PROXY_SERPS')] = None
145+
146+
147+
@docs_group('Actor')
148+
class ActorRunUsageUsd(BaseModel):
149+
"""Resource usage costs in USD."""
150+
151+
model_config = ConfigDict(populate_by_name=True, extra='allow')
80152

81153
actor_compute_units: Annotated[float | None, Field(alias='ACTOR_COMPUTE_UNITS')] = None
82154
dataset_reads: Annotated[float | None, Field(alias='DATASET_READS')] = None
@@ -92,9 +164,67 @@ class ActorRunUsage(BaseModel):
92164
proxy_serps: Annotated[float | None, Field(alias='PROXY_SERPS')] = None
93165

94166

167+
class Metamorph(BaseModel):
168+
"""Information about a metamorph event that occurred during the run."""
169+
170+
model_config = ConfigDict(populate_by_name=True, extra='allow')
171+
172+
created_at: Annotated[datetime, Field(alias='createdAt')]
173+
actor_id: Annotated[str, Field(alias='actorId')]
174+
build_id: Annotated[str, Field(alias='buildId')]
175+
input_key: Annotated[str | None, Field(alias='inputKey')] = None
176+
177+
178+
class CommonActorPricingInfo(BaseModel):
179+
model_config = ConfigDict(populate_by_name=True, extra='allow')
180+
181+
apify_margin_percentage: Annotated[float | None, Field(alias='apifyMarginPercentage')] = None
182+
created_at: Annotated[datetime | None, Field(alias='createdAt')] = None
183+
started_at: Annotated[datetime | None, Field(alias='startedAt')] = None
184+
notified_about_future_change_at: Annotated[datetime | None, Field(alias='notifiedAboutFutureChangeAt')] = None
185+
notified_about_change_at: Annotated[datetime | None, Field(alias='notifiedAboutChangeAt')] = None
186+
reason_for_change: Annotated[str | None, Field(alias='reasonForChange')] = None
187+
188+
189+
class FreeActorPricingInfo(CommonActorPricingInfo):
190+
pricing_model: Annotated[Literal['FREE'], Field(alias='pricingModel')]
191+
192+
193+
class FlatPricePerMonthActorPricingInfo(CommonActorPricingInfo):
194+
pricing_model: Annotated[Literal['FLAT_PRICE_PER_MONTH'], Field(alias='pricingModel')]
195+
trial_minutes: Annotated[int, Field(alias='trialMinutes')]
196+
price_per_unit_usd: Annotated[float, Field(alias='pricePerUnitUsd')]
197+
198+
199+
class PricePerDatasetItemActorPricingInfo(CommonActorPricingInfo):
200+
pricing_model: Annotated[Literal['PRICE_PER_DATASET_ITEM'], Field(alias='pricingModel')]
201+
unit_name: Annotated[str, Field(alias='unitName')]
202+
price_per_unit_usd: Annotated[float, Field(alias='pricePerUnitUsd')]
203+
204+
205+
class ActorChargeEvent(BaseModel):
206+
model_config = ConfigDict(populate_by_name=True, extra='allow')
207+
208+
event_price_usd: Annotated[float, Field(alias='eventPriceUsd')]
209+
event_title: Annotated[str, Field(alias='eventTitle')]
210+
event_description: Annotated[str | None, Field(alias='eventDescription')] = None
211+
212+
213+
class PricingPerEvent(BaseModel):
214+
model_config = ConfigDict(populate_by_name=True, extra='allow')
215+
216+
actor_charge_events: Annotated[dict[str, ActorChargeEvent] | None, Field(alias='actorChargeEvents')] = None
217+
218+
219+
class PayPerEventActorPricingInfo(CommonActorPricingInfo):
220+
pricing_model: Annotated[Literal['PAY_PER_EVENT'], Field(alias='pricingModel')]
221+
pricing_per_event: Annotated[PricingPerEvent, Field(alias='pricingPerEvent')]
222+
minimal_max_total_charge_usd: Annotated[float | None, Field(alias='minimalMaxTotalChargeUsd')] = None
223+
224+
95225
@docs_group('Actor')
96226
class ActorRun(BaseModel):
97-
__model_config__ = ConfigDict(populate_by_name=True)
227+
model_config = ConfigDict(populate_by_name=True, extra='allow')
98228

99229
id: Annotated[str, Field(alias='id')]
100230
act_id: Annotated[str, Field(alias='actId')]
@@ -110,16 +240,17 @@ class ActorRun(BaseModel):
110240
options: Annotated[ActorRunOptions, Field(alias='options')]
111241
build_id: Annotated[str, Field(alias='buildId')]
112242
exit_code: Annotated[int | None, Field(alias='exitCode')] = None
243+
general_access: Annotated[str | None, Field(alias='generalAccess')] = None
113244
default_key_value_store_id: Annotated[str, Field(alias='defaultKeyValueStoreId')]
114245
default_dataset_id: Annotated[str, Field(alias='defaultDatasetId')]
115246
default_request_queue_id: Annotated[str, Field(alias='defaultRequestQueueId')]
116247
build_number: Annotated[str | None, Field(alias='buildNumber')] = None
117-
container_url: Annotated[str, Field(alias='containerUrl')]
248+
container_url: Annotated[str | None, Field(alias='containerUrl')] = None
118249
is_container_server_ready: Annotated[bool | None, Field(alias='isContainerServerReady')] = None
119250
git_branch_name: Annotated[str | None, Field(alias='gitBranchName')] = None
120251
usage: Annotated[ActorRunUsage | None, Field(alias='usage')] = None
121252
usage_total_usd: Annotated[float | None, Field(alias='usageTotalUsd')] = None
122-
usage_usd: Annotated[ActorRunUsage | None, Field(alias='usageUsd')] = None
253+
usage_usd: Annotated[ActorRunUsageUsd | None, Field(alias='usageUsd')] = None
123254
pricing_info: Annotated[
124255
FreeActorPricingInfo
125256
| FlatPricePerMonthActorPricingInfo
@@ -132,43 +263,4 @@ class ActorRun(BaseModel):
132263
dict[str, int] | None,
133264
Field(alias='chargedEventCounts'),
134265
] = 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-
]
266+
metamorphs: Annotated[list[Metamorph] | None, Field(alias='metamorphs')] = None

0 commit comments

Comments
 (0)