Skip to content

Commit 63eb771

Browse files
authored
fix: coerce null stats in Apify request queue metadata (#974)
Opening a request queue crashed when the Apify API omitted `stats` (sending an explicit `null`): `model_dump(by_alias=True)` preserved `stats: None`, and the non-optional `stats` field on `ApifyRequestQueueMetadata` rejected it. The `default_factory` only fires when the key is absent, not when it is present-but-null. This adds a `BeforeValidator` that coerces `None` to a default `RequestQueueStats`, mirroring the defensive `metadata.stats or RequestQueueStats()` already used in `get_metadata()`, so every validation path is protected.
1 parent 2cfd8a5 commit 63eb771

2 files changed

Lines changed: 39 additions & 4 deletions

File tree

src/apify/storage_clients/_apify/_models.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from datetime import datetime, timedelta
44
from typing import TYPE_CHECKING, Annotated
55

6-
from pydantic import AliasChoices, BaseModel, ConfigDict, Field
6+
from pydantic import AliasChoices, BaseModel, BeforeValidator, ConfigDict, Field
77
from pydantic.alias_generators import to_camel
88

99
from apify_client._models import RequestQueueStats
@@ -105,5 +105,13 @@ class CachedRequest(BaseModel):
105105
class ApifyRequestQueueMetadata(RequestQueueMetadata):
106106
model_config = ConfigDict(alias_generator=to_camel)
107107

108-
stats: Annotated[RequestQueueStats, Field(default_factory=RequestQueueStats)]
109-
"""Additional statistics about the request queue."""
108+
stats: Annotated[
109+
RequestQueueStats,
110+
BeforeValidator(lambda value: RequestQueueStats() if value is None else value),
111+
Field(default_factory=RequestQueueStats),
112+
]
113+
"""Additional statistics about the request queue.
114+
115+
The API may omit the stats (sending an explicit `null`), so a `None` value is coerced to a default
116+
`RequestQueueStats` rather than failing validation.
117+
"""

tests/unit/storage_clients/test_apify_request_queue_client.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55

66
import pytest
77

8-
from apify_client._models import RequestQueueHead
8+
from apify_client._models import RequestQueueHead, RequestQueueStats
99
from crawlee.storage_clients.models import RequestQueueMetadata
1010

11+
from apify.storage_clients._apify._models import ApifyRequestQueueMetadata
1112
from apify.storage_clients._apify._request_queue_single_client import ApifyRequestQueueSingleClient
1213
from apify.storage_clients._apify._utils import unique_key_to_request_id
1314

@@ -46,6 +47,32 @@ def test_unique_key_to_request_id_consistency() -> None:
4647
assert request_id_1 == request_id_2, 'The same unique key should generate consistent request IDs.'
4748

4849

50+
@pytest.mark.parametrize(
51+
('stats', 'expected_read_count'),
52+
[(None, None), ({'readCount': 5}, 5)],
53+
ids=['none_coerced_to_default', 'populated_passed_through'],
54+
)
55+
def test_metadata_stats_validation(stats: dict | None, expected_read_count: int | None) -> None:
56+
"""A `stats: None` payload (as `open()` produces via `model_dump`) defaults; a populated one passes through."""
57+
now = datetime.now(tz=UTC)
58+
metadata = ApifyRequestQueueMetadata.model_validate(
59+
{
60+
'id': 'test-rq-id',
61+
'name': None,
62+
'accessedAt': now,
63+
'createdAt': now,
64+
'modifiedAt': now,
65+
'hadMultipleClients': False,
66+
'handledRequestCount': 0,
67+
'pendingRequestCount': 0,
68+
'totalRequestCount': 0,
69+
'stats': stats,
70+
}
71+
)
72+
assert isinstance(metadata.stats, RequestQueueStats)
73+
assert metadata.stats.read_count == expected_read_count
74+
75+
4976
@pytest.mark.parametrize(
5077
('unique_key', 'expected_request_id'),
5178
[

0 commit comments

Comments
 (0)