Skip to content

Commit a38fc94

Browse files
committed
New client commit
1 parent 6b0c66c commit a38fc94

14 files changed

Lines changed: 119 additions & 46 deletions

pyproject.toml

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

src/apify/_actor.py

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

915-
run_dict = run.model_dump(by_alias=True)
916-
return ActorRun.model_validate(run_dict)
915+
return ActorRun.from_client_actor_run(run)
917916

918917
async def abort(
919918
self,
@@ -951,8 +950,7 @@ async def abort(
951950
if run is None:
952951
raise RuntimeError(f'Failed to abort Actor run with ID "{run_id}".')
953952

954-
run_dict = run.model_dump(by_alias=True)
955-
return ActorRun.model_validate(run_dict)
953+
return ActorRun.from_client_actor_run(run)
956954

957955
async def call(
958956
self,
@@ -1040,8 +1038,7 @@ async def call(
10401038
if run is None:
10411039
raise RuntimeError(f'Failed to call Actor with ID "{actor_id}".')
10421040

1043-
run_dict = run.model_dump(by_alias=True)
1044-
return ActorRun.model_validate(run_dict)
1041+
return ActorRun.from_client_actor_run(run)
10451042

10461043
async def call_task(
10471044
self,
@@ -1116,8 +1113,7 @@ async def call_task(
11161113
if run is None:
11171114
raise RuntimeError(f'Failed to call Task with ID "{task_id}".')
11181115

1119-
run_dict = run.model_dump(by_alias=True)
1120-
return ActorRun.model_validate(run_dict)
1116+
return ActorRun.from_client_actor_run(run)
11211117

11221118
async def metamorph(
11231119
self,
@@ -1305,8 +1301,7 @@ async def set_status_message(
13051301
f'Failed to set status message for Actor run with ID "{self.configuration.actor_run_id}".'
13061302
)
13071303

1308-
run_dict = run.model_dump(by_alias=True)
1309-
return ActorRun.model_validate(run_dict)
1304+
return ActorRun.from_client_actor_run(run)
13101305

13111306
async def create_proxy_configuration(
13121307
self,

src/apify/_charging.py

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

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

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

src/apify/_models.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

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

8-
from apify_client._models import ActorJobStatus
8+
from apify_client._models import ActorJobStatus, Run
99
from crawlee._utils.models import timedelta_ms
1010
from crawlee._utils.urls import validate_http_url
1111

@@ -48,6 +48,8 @@ class WebhookStats(BaseModel):
4848

4949
total_dispatches: Annotated[int, Field(alias='totalDispatches')]
5050

51+
from apify_client._models import Run
52+
5153

5254
@docs_group('Actor')
5355
class Webhook(BaseModel):
@@ -322,3 +324,19 @@ class ActorRun(BaseModel):
322324

323325
metamorphs: Annotated[list[Metamorph] | None, Field(alias='metamorphs')] = None
324326
"""List of metamorph events that occurred during the run."""
327+
328+
@classmethod
329+
def from_client_actor_run(cls, client_actor_run: Run) -> ActorRun:
330+
"""Create an `ActorRun` from an Apify API client's `Run` model.
331+
332+
Args:
333+
client_actor_run: `Run` instance from Apify API client.
334+
335+
Returns:
336+
`ActorRun` instance with properly converted types.
337+
"""
338+
# Dump to dict first with mode='json' to serialize special types
339+
client_actor_run_dict = client_actor_run.model_dump(by_alias=True, mode='json')
340+
341+
# Validate and construct ActorRun from the serialized dict
342+
return cls.model_validate(client_actor_run_dict)

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:
@@ -331,8 +332,7 @@ async def _get_request_by_id(self, id: str) -> Request | None:
331332
if response is None:
332333
return None
333334

334-
response_dict = response.model_dump(by_alias=True)
335-
request = Request.model_validate(response_dict)
335+
request = to_crawlee_request(response)
336336

337337
# Updated local caches
338338
if id in self._requests_in_progress:
@@ -383,7 +383,7 @@ async def _init_caches(self) -> None:
383383
"""
384384
response = await self._api_client.list_requests(limit=10_000)
385385
for request_data in response.items:
386-
request = Request.model_validate(request_data.model_dump(by_alias=True))
386+
request = to_crawlee_request(request_data)
387387
request_id = request_data.id
388388

389389
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/e2e/conftest.py

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

393393
run_client = apify_client_async.run(call_result.id)
394-
actor_run = await run_client.wait_for_finish(wait_secs=600)
394+
client_actor_run = await run_client.wait_for_finish(wait_secs=600)
395395

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

398-
actor_run_dict = actor_run.model_dump(by_alias=True)
399-
return ActorRun.model_validate(actor_run_dict)
398+
return ActorRun.from_client_actor_run(client_actor_run)
400399

401400
return _run_actor

0 commit comments

Comments
 (0)