Skip to content

Commit 72eaa7d

Browse files
committed
New client commit
1 parent 6bff2a4 commit 72eaa7d

File tree

15 files changed

+124
-57
lines changed

15 files changed

+124
-57
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ dependencies = [
4343
"impit>=0.8.0",
4444
"lazy-object-proxy>=1.11.0",
4545
"more_itertools>=10.2.0",
46+
"pydantic[email]>=2.11.0",
4647
"typing-extensions>=4.1.0",
4748
"websockets>=14.0",
4849
"yarl>=1.18.0",

src/apify/_actor.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -899,8 +899,7 @@ async def start(
899899
if run is None:
900900
raise RuntimeError(f'Failed to start Actor with ID "{actor_id}".')
901901

902-
run_dict = run.model_dump(by_alias=True)
903-
return ActorRun.model_validate(run_dict)
902+
return ActorRun.from_client_actor_run(run)
904903

905904
async def abort(
906905
self,
@@ -938,8 +937,7 @@ async def abort(
938937
if run is None:
939938
raise RuntimeError(f'Failed to abort Actor run with ID "{run_id}".')
940939

941-
run_dict = run.model_dump(by_alias=True)
942-
return ActorRun.model_validate(run_dict)
940+
return ActorRun.from_client_actor_run(run)
943941

944942
async def call(
945943
self,
@@ -1027,8 +1025,7 @@ async def call(
10271025
if run is None:
10281026
raise RuntimeError(f'Failed to call Actor with ID "{actor_id}".')
10291027

1030-
run_dict = run.model_dump(by_alias=True)
1031-
return ActorRun.model_validate(run_dict)
1028+
return ActorRun.from_client_actor_run(run)
10321029

10331030
async def call_task(
10341031
self,
@@ -1103,8 +1100,7 @@ async def call_task(
11031100
if run is None:
11041101
raise RuntimeError(f'Failed to call Task with ID "{task_id}".')
11051102

1106-
run_dict = run.model_dump(by_alias=True)
1107-
return ActorRun.model_validate(run_dict)
1103+
return ActorRun.from_client_actor_run(run)
11081104

11091105
async def metamorph(
11101106
self,
@@ -1292,8 +1288,7 @@ async def set_status_message(
12921288
f'Failed to set status message for Actor run with ID "{self.configuration.actor_run_id}".'
12931289
)
12941290

1295-
run_dict = run.model_dump(by_alias=True)
1296-
return ActorRun.model_validate(run_dict)
1291+
return ActorRun.from_client_actor_run(run)
12971292

12981293
async def create_proxy_configuration(
12991294
self,

src/apify/_charging.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,8 +356,7 @@ async def _fetch_pricing_info(self) -> _FetchedPricingInfoDict:
356356
if run is None:
357357
raise RuntimeError('Actor run not found')
358358

359-
run_dict = run.model_dump(by_alias=True)
360-
actor_run = run_validator.validate_python(run_dict)
359+
actor_run = ActorRun.from_client_actor_run(run)
361360

362361
if actor_run is None:
363362
raise RuntimeError('Actor run not found')

src/apify/_models.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
if TYPE_CHECKING:
1616
from typing import TypeAlias
1717

18+
from apify_client._models import Run
19+
1820

1921
@docs_group('Actor')
2022
class Webhook(BaseModel):
@@ -186,6 +188,22 @@ class ActorRun(BaseModel):
186188
] = None
187189
"""Count of charged events for pay-per-event pricing model."""
188190

191+
@classmethod
192+
def from_client_actor_run(cls, client_actor_run: Run) -> ActorRun:
193+
"""Create an `ActorRun` from an Apify API client's `Run` model.
194+
195+
Args:
196+
client_actor_run: `Run` instance from Apify API client.
197+
198+
Returns:
199+
`ActorRun` instance with properly converted types.
200+
"""
201+
# Dump to dict first with mode='json' to serialize special types
202+
client_actor_run_dict = client_actor_run.model_dump(by_alias=True, mode='json')
203+
204+
# Validate and construct ActorRun from the serialized dict
205+
return cls.model_validate(client_actor_run_dict)
206+
189207

190208
class FreeActorPricingInfo(BaseModel):
191209
pricing_model: Annotated[Literal['FREE'], Field(alias='pricingModel')]

src/apify/storage_clients/_apify/_models.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from datetime import datetime, timedelta
4-
from typing import Annotated
4+
from typing import TYPE_CHECKING, Annotated
55

66
from pydantic import BaseModel, ConfigDict, Field
77

@@ -10,6 +10,9 @@
1010
from apify import Request
1111
from apify._utils import docs_group
1212

13+
if TYPE_CHECKING:
14+
from apify_client._models import LockedRequestQueueHead
15+
1316

1417
@docs_group('Storage data')
1518
class ApifyKeyValueStoreMetadata(KeyValueStoreMetadata):
@@ -59,6 +62,22 @@ class RequestQueueHead(BaseModel):
5962
items: Annotated[list[Request], Field(alias='items', default_factory=list[Request])]
6063
"""The list of request objects retrieved from the beginning of the queue."""
6164

65+
@classmethod
66+
def from_client_locked_head(cls, client_locked_head: LockedRequestQueueHead) -> RequestQueueHead:
67+
"""Create a `RequestQueueHead` from an Apify API client's `LockedRequestQueueHead` model.
68+
69+
Args:
70+
client_locked_head: `LockedRequestQueueHead` instance from Apify API client.
71+
72+
Returns:
73+
`RequestQueueHead` instance with properly converted types.
74+
"""
75+
# Dump to dict with mode='json' to serialize special types like AnyUrl
76+
head_dict = client_locked_head.model_dump(by_alias=True, mode='json')
77+
78+
# Validate and construct RequestQueueHead from the serialized dict
79+
return cls.model_validate(head_dict)
80+
6281

6382
class KeyValueStoreKeyInfo(BaseModel):
6483
"""Model for a key-value store key info.

src/apify/storage_clients/_apify/_request_queue_client.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@
1616
from collections.abc import Sequence
1717

1818
from apify_client._resource_clients import RequestQueueClientAsync
19-
from crawlee import Request
2019
from crawlee.storage_clients.models import AddRequestsResponse, ProcessedRequest, RequestQueueMetadata
2120

22-
from apify import Configuration
21+
from apify import Configuration, Request
2322

2423
logger = getLogger(__name__)
2524

src/apify/storage_clients/_apify/_request_queue_shared_client.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111
from crawlee.storage_clients.models import AddRequestsResponse, ProcessedRequest, RequestQueueMetadata
1212

1313
from ._models import ApifyRequestQueueMetadata, CachedRequest, RequestQueueHead
14-
from ._utils import unique_key_to_request_id
15-
from apify import Request
14+
from ._utils import to_crawlee_request, unique_key_to_request_id
1615

1716
if TYPE_CHECKING:
1817
from collections.abc import Callable, Coroutine, Sequence
1918

2019
from apify_client._resource_clients import RequestQueueClientAsync
2120

21+
from apify import Request
22+
2223
logger = getLogger(__name__)
2324

2425

@@ -311,8 +312,7 @@ async def _get_request_by_id(self, request_id: str) -> Request | None:
311312
if response is None:
312313
return None
313314

314-
response_dict = response.model_dump(by_alias=True)
315-
return Request.model_validate(response_dict)
315+
return to_crawlee_request(response)
316316

317317
async def _ensure_head_is_non_empty(self) -> None:
318318
"""Ensure that the queue head has requests if they are available in the queue."""
@@ -442,7 +442,7 @@ async def _list_head(
442442
self.metadata.had_multiple_clients = locked_queue_head.had_multiple_clients
443443

444444
for request_data in locked_queue_head.items:
445-
request = Request.model_validate(request_data.model_dump(by_alias=True))
445+
request = to_crawlee_request(request_data)
446446
request_id = request_data.id
447447

448448
# Skip requests without ID or unique key
@@ -473,8 +473,7 @@ async def _list_head(
473473
# After adding new requests to the forefront, any existing leftover locked request is kept in the end.
474474
self._queue_head.append(leftover_id)
475475

476-
list_and_lost_dict = locked_queue_head.model_dump(by_alias=True)
477-
return RequestQueueHead.model_validate(list_and_lost_dict)
476+
return RequestQueueHead.from_client_locked_head(locked_queue_head)
478477

479478
def _cache_request(
480479
self,

src/apify/storage_clients/_apify/_request_queue_single_client.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99

1010
from crawlee.storage_clients.models import AddRequestsResponse, ProcessedRequest, RequestQueueMetadata
1111

12-
from ._utils import unique_key_to_request_id
13-
from apify import Request
12+
from ._utils import to_crawlee_request, unique_key_to_request_id
1413

1514
if TYPE_CHECKING:
1615
from collections.abc import Sequence
1716

1817
from apify_client._resource_clients import RequestQueueClientAsync
1918

19+
from apify import Request
20+
2021
logger = getLogger(__name__)
2122

2223

@@ -294,7 +295,7 @@ async def _list_head(self) -> None:
294295

295296
# Update the cached data
296297
for request_data in response.items:
297-
request = Request.model_validate(request_data.model_dump(by_alias=True))
298+
request = to_crawlee_request(request_data)
298299
request_id = request_data.id
299300

300301
if request_id in self._requests_in_progress:
@@ -326,8 +327,7 @@ async def _get_request_by_id(self, id: str) -> Request | None:
326327
if response is None:
327328
return None
328329

329-
response_dict = response.model_dump(by_alias=True)
330-
request = Request.model_validate(response_dict)
330+
request = to_crawlee_request(response)
331331

332332
# Updated local caches
333333
if id in self._requests_in_progress:
@@ -378,7 +378,7 @@ async def _init_caches(self) -> None:
378378
"""
379379
response = await self._api_client.list_requests(limit=10_000)
380380
for request_data in response.items:
381-
request = Request.model_validate(request_data.model_dump(by_alias=True))
381+
request = to_crawlee_request(request_data)
382382
request_id = request_data.id
383383

384384
if request.was_already_handled:

src/apify/storage_clients/_apify/_utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77

88
from crawlee._utils.crypto import compute_short_hash
99

10+
from apify import Request
11+
1012
if TYPE_CHECKING:
13+
from apify_client._models import HeadRequest, LockedHeadRequest
14+
from apify_client._models import Request as ClientRequest
15+
1116
from apify import Configuration
1217

1318

@@ -39,3 +44,19 @@ def hash_api_base_url_and_token(configuration: Configuration) -> str:
3944
if configuration.api_public_base_url is None or configuration.token is None:
4045
raise ValueError("'Configuration.api_public_base_url' and 'Configuration.token' must be set.")
4146
return compute_short_hash(f'{configuration.api_public_base_url}{configuration.token}'.encode())
47+
48+
49+
def to_crawlee_request(client_request: ClientRequest | HeadRequest | LockedHeadRequest) -> Request:
50+
"""Convert an Apify API client's `Request` model to a Crawlee's `Request` model.
51+
52+
Args:
53+
client_request: Request instances from Apify API client.
54+
55+
Returns:
56+
`Request` instance from Crawlee with properly converted types.
57+
"""
58+
# Dump to dict with mode='json' to serialize special types like AnyUrl
59+
request_dict = client_request.model_dump(by_alias=True, mode='json')
60+
61+
# Validate and construct Crawlee Request from the serialized dict
62+
return Request.model_validate(request_dict)

tests/integration/actor/conftest.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -316,11 +316,10 @@ async def _run_actor(
316316
assert call_result is not None, 'Failed to start Actor run: missing run ID in the response.'
317317

318318
run_client = apify_client_async.run(call_result.id)
319-
actor_run = await run_client.wait_for_finish(wait_secs=600)
319+
client_actor_run = await run_client.wait_for_finish(wait_secs=600)
320320

321-
assert actor_run is not None, 'Actor run did not finish successfully within the expected time.'
321+
assert client_actor_run is not None, 'Actor run did not finish successfully within the expected time.'
322322

323-
actor_run_dict = actor_run.model_dump(by_alias=True)
324-
return ActorRun.model_validate(actor_run_dict)
323+
return ActorRun.from_client_actor_run(client_actor_run)
325324

326325
return _run_actor

0 commit comments

Comments
 (0)