Skip to content

Commit 54945ce

Browse files
committed
Strict typing for walmart/store + lowes/store plugin params
Scrape.do's async-api/plugins page now formally documents the schema for both `walmart/store` and `lowes/store`. Replace the `extra="allow"` schema-free passthrough with proper typed fields and cross-field validators. WalmartStoreParameters: - `url` (required, must contain walmart.com) - `zipcode` + `storeid` conditional pair (both or neither; setting only one raises ValidationError) - gateway-side toggles: disableretry, transparentresponse, timeout (5000-120000ms) LowesStoreParameters: - `url` (required, must contain lowes.com) - `zipcode` (required, digits-only) - `storeid` (required, digits-only) - same gateway-side toggles as Walmart Tests: - tests/unit/async_api/models/plugins/test_additional.py expanded to cover every required-field / cross-field / format-validation case. - tests/unit/async_api/models/plugins/test_discriminated_union.py walmart/lowes cases updated to use the new required-field combos. - Integration plugin sweep re-introduces `google/trends` and `lowes/store` now that the pass criterion (`assert_request_accepted`) tolerates upstream/engine transient failures. The plugin engine's HTTP 400 parameter rejection is still surfaced as a test failure. Breaking change for callers passing undocumented extras through the previous schema-free passthrough.
1 parent ea32336 commit 54945ce

7 files changed

Lines changed: 451 additions & 123 deletions

File tree

.github/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,16 @@ All notable changes to this project will be documented in this file.
88

99
## [Unreleased]
1010

11+
### Changed
12+
13+
- **`WalmartStoreParameters` / `LowesStoreParameters`** now enforce the documented schema from `Scrape.do's` async-api/plugins page instead of accepting arbitrary extras via `extra="allow"`. Walmart requires `url` (walmart.com domain) and treats `zipcode` + `storeid` as a conditional pair (both or neither). Lowes requires `url` (lowes.com domain) plus digit-only `zipcode` and `storeid`. Both pick up the gateway-side `disableretry` / `transparentresponse` / `timeout` knobs. Breaking change for callers passing undocumented extras through the previous schema-free passthrough.
14+
1115
### Internal
1216

1317
- Integration suite standardized around three test categories — content-dependent tests retry on transient Scrape.do gateway failures, shape-dependent tests assert only that the request wasn't rejected (HTTP 400), and error-routing tests are unchanged.
1418

19+
- Re-introduced `google/trends` and `lowes/store` into the live plugin sweep now that the pass criterion tolerates upstream / engine-side transient failures.
20+
1521
## [0.3.0] — 2026-05-24
1622

1723
### Added

docs/changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@
1010

1111
## `Unreleased`
1212

13+
### Changed
14+
15+
- **[`WalmartStoreParameters`][scrape_do.async_api.models.plugins.WalmartStoreParameters] / [`LowesStoreParameters`][scrape_do.async_api.models.plugins.LowesStoreParameters]** now enforce the documented schema from `Scrape.do's` async-api/plugins page instead of accepting arbitrary extras via `extra="allow"`. Walmart requires `url` (walmart.com domain) and treats `zipcode` + `storeid` as a conditional pair (both or neither). Lowes requires `url` (lowes.com domain) plus digit-only `zipcode` and `storeid`. Both pick up the gateway-side `disableretry` / `transparentresponse` / `timeout` knobs. Breaking change for callers passing undocumented extras through the previous schema-free passthrough.
16+
1317
### Internal
1418

1519
- Integration suite standardized around three test categories — content-dependent tests retry on transient Scrape.do gateway failures, shape-dependent tests assert only that the request wasn't rejected (HTTP 400), and error-routing tests are unchanged.
1620

21+
- Re-introduced `google/trends` and `lowes/store` into the live plugin sweep now that the pass criterion tolerates upstream / engine-side transient failures.
22+
1723
---
1824

1925
## `0.3.0` — 2026-05-24

src/scrape_do/async_api/models/plugins/additional.py

Lines changed: 206 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -20,95 +20,255 @@
2020
2121
- The parameter models live alongside their adapters here
2222
23-
warning: Sparse Documentation
24-
- [`Scrape.do's Async API Plugins`](https://scrape.do/documentation/
25-
async-api/plugins/) page enumerates the `walmart/store` and `lowes/store`
26-
plugin keys, but does NOT formally specify their full schema
27-
28-
- These plugins return raw HTML rather than structured JSON
29-
30-
- The models require `geocode` and accept arbitrary additional `key /
31-
value` pairs via `extra="allow"` so that callers can construct entries
32-
that match whatever shape `Scrape.do` is currently expecting
33-
without the SDK blocking on a stale schema
23+
tip: Output Format
24+
Both plugins return raw HTML rather than structured JSON
3425
26+
tip: Official Documentation
27+
[`Scrape.do's Async API Plugins`](https://scrape.do/documentation/
28+
async-api/plugins/) page formally documents the full parameter
29+
schema for `walmart/store` and `lowes/store`
3530
"""
3631

3732
from __future__ import annotations
3833

39-
from typing import List, Literal
34+
from typing import List, Literal, Optional
4035

41-
from pydantic import BaseModel, ConfigDict, Field
36+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
37+
from typing_extensions import Self
4238

4339

4440
class WalmartStoreParameters(BaseModel):
4541
"""Parameters accepted by the `walmart/store` plugin
4642
47-
warning: Schema-Free
48-
- Only `geocode` is documented as required
43+
warning: Conditional Pair
44+
- `zipcode` and `storeid` are a conditional pair, both must be
45+
set or both must be omitted
4946
50-
- Construct an instance with whatever additional keys the
51-
documentation currently lists (extras pass through unchanged
52-
thanks to `extra="allow"`)
47+
- When both are omitted, `Scrape.do` selects a random Walmart
48+
store for the request
5349
54-
- The plugin returns `raw HTML` rather than structured JSON
50+
- Setting only one raises `ValueError`
5551
5652
Attributes:
57-
geocode (str): ISO 3166-1 alpha-2 country code selecting the
58-
target Walmart marketplace
53+
url (str): Walmart product / category URL. Must point to the
54+
`walmart.com` domain
55+
zipcode (Optional[str]): U.S. zipcode (digits-only). Pairs
56+
with `storeid`
57+
storeid (Optional[int]): Walmart store ID. Pairs with `zipcode`
58+
disableretry (Optional[bool]): Skip `Scrape.do's` internal
59+
retry loop on transient failures
60+
transparentresponse (Optional[bool]): Return the target's
61+
actual status code instead of wrapping non-`2xx` in a `502`
62+
timeout (Optional[int]): Per-target timeout in milliseconds
63+
(`5000`-`120000`)
5964
6065
Example:
6166
```python
6267
from scrape_do.async_api.models.plugins import WalmartStoreParameters
6368
64-
param = WalmartStoreParameters(geocode="us", some_extra="value")
69+
param = WalmartStoreParameters(
70+
url="https://www.walmart.com/ip/123456",
71+
zipcode="72712",
72+
storeid=100,
73+
)
6574
```
6675
"""
6776

6877
model_config = ConfigDict(
6978
populate_by_name=True,
70-
extra="allow"
79+
extra="ignore",
7180
)
7281

73-
geocode: str = Field(
82+
url: str = Field(
7483
...,
75-
alias="geocode"
84+
alias="url",
85+
)
86+
zipcode: Optional[str] = Field(
87+
default=None,
88+
alias="zipcode",
89+
)
90+
storeid: Optional[int] = Field(
91+
default=None,
92+
alias="storeid",
93+
)
94+
disableretry: Optional[bool] = Field(
95+
default=None,
96+
alias="disableretry",
7697
)
98+
transparentresponse: Optional[bool] = Field(
99+
default=None,
100+
alias="transparentresponse",
101+
)
102+
timeout: Optional[int] = Field(
103+
default=None,
104+
alias="timeout",
105+
ge=5000,
106+
le=120000,
107+
)
108+
109+
@field_validator("url")
110+
@classmethod
111+
def _validate_walmart_domain(cls, value: str) -> str:
112+
"""Enforces that the URL is a Walmart domain
113+
114+
Args:
115+
value (str): The URL to validate
116+
117+
Returns:
118+
The validated URL unchanged
119+
120+
Raises:
121+
ValueError: If the URL doesn't contain `walmart.com`
122+
"""
123+
if "walmart.com" not in value:
124+
raise ValueError(
125+
f"`walmart/store` `url` must point to walmart.com,"
126+
f" got {value!r}"
127+
)
128+
return value
129+
130+
@model_validator(mode="after")
131+
def _validate_zipcode_storeid_pair(self) -> Self:
132+
"""Enforces the documented conditional-pair rule on `zipcode`
133+
and `storeid`
134+
135+
Returns:
136+
The validated instance from which the method was called
137+
138+
Raises:
139+
ValueError: If only one of `zipcode` / `storeid` is set
140+
"""
141+
has_zip = self.zipcode is not None
142+
has_storeid = self.storeid is not None
143+
if has_zip != has_storeid:
144+
raise ValueError(
145+
"`walmart/store` `zipcode` and `storeid` are a"
146+
" conditional pair, set both or omit both"
147+
)
148+
return self
77149

78150

79151
class LowesStoreParameters(BaseModel):
80152
"""Parameters accepted by the `lowes/store` plugin
81153
82-
warning: Schema-Free
83-
- Only `geocode` is documented as required
84-
85-
- Construct an instance with whatever additional keys the
86-
documentation currently lists (extras pass through unchanged
87-
thanks to `extra="allow"`)
88-
89-
- The plugin returns `raw HTML` rather than structured JSON
90-
91154
Attributes:
92-
geocode (str): ISO 3166-1 alpha-2 country code selecting the
93-
target Lowes marketplace
155+
url (str): Lowes product / store URL. Must point to the
156+
`lowes.com` domain
157+
zipcode (str): U.S. zipcode (digits-only)
158+
storeid (str): Lowes store ID (digits-only)
159+
disableretry (Optional[bool]): Skip `Scrape.do's` internal
160+
retry loop on transient failures
161+
transparentresponse (Optional[bool]): Return the target's
162+
actual status code instead of wrapping non-`2xx` in a `502`
163+
timeout (Optional[int]): Per-target timeout in milliseconds
164+
(`5000`-`120000`)
94165
95166
Example:
96167
```python
97168
from scrape_do.async_api.models.plugins import LowesStoreParameters
98169
99-
param = LowesStoreParameters(geocode="us", some_extra="value")
170+
param = LowesStoreParameters(
171+
url="https://www.lowes.com/pd/item/123",
172+
zipcode="28202",
173+
storeid="0595",
174+
)
100175
```
101176
"""
102177

103178
model_config = ConfigDict(
104179
populate_by_name=True,
105-
extra="allow"
180+
extra="ignore",
106181
)
107182

108-
geocode: str = Field(
183+
url: str = Field(
184+
...,
185+
alias="url",
186+
)
187+
zipcode: str = Field(
188+
...,
189+
alias="zipcode",
190+
)
191+
storeid: str = Field(
109192
...,
110-
alias="geocode"
193+
alias="storeid",
111194
)
195+
disableretry: Optional[bool] = Field(
196+
default=None,
197+
alias="disableretry",
198+
)
199+
transparentresponse: Optional[bool] = Field(
200+
default=None,
201+
alias="transparentresponse",
202+
)
203+
timeout: Optional[int] = Field(
204+
default=None,
205+
alias="timeout",
206+
ge=5000,
207+
le=120000,
208+
)
209+
210+
@field_validator("url")
211+
@classmethod
212+
def _validate_lowes_domain(cls, value: str) -> str:
213+
"""Enforces that the URL is a Lowes domain
214+
215+
Args:
216+
value (str): The URL to validate
217+
218+
Returns:
219+
The validated URL unchanged
220+
221+
Raises:
222+
ValueError: If the URL doesn't contain `lowes.com`
223+
"""
224+
if "lowes.com" not in value:
225+
raise ValueError(
226+
f"`lowes/store` `url` must point to lowes.com,"
227+
f" got {value!r}"
228+
)
229+
return value
230+
231+
@field_validator("zipcode")
232+
@classmethod
233+
def _validate_zipcode_digits(cls, value: str) -> str:
234+
"""Enforces digit-only `zipcode`
235+
236+
Args:
237+
value (str): The zipcode to validate
238+
239+
Returns:
240+
The validated zipcode unchanged
241+
242+
Raises:
243+
ValueError: If `zipcode` contains non-digit characters
244+
"""
245+
if not value.isdigit():
246+
raise ValueError(
247+
f"`lowes/store` `zipcode` must be digits only,"
248+
f" got {value!r}"
249+
)
250+
return value
251+
252+
@field_validator("storeid")
253+
@classmethod
254+
def _validate_storeid_digits(cls, value: str) -> str:
255+
"""Enforces digit-only `storeid`
256+
257+
Args:
258+
value (str): The storeid to validate
259+
260+
Returns:
261+
The validated storeid unchanged
262+
263+
Raises:
264+
ValueError: If `storeid` contains non-digit characters
265+
"""
266+
if not value.isdigit():
267+
raise ValueError(
268+
f"`lowes/store` `storeid` must be digits only,"
269+
f" got {value!r}"
270+
)
271+
return value
112272

113273

114274
class WalmartStoreAsyncPlugin(BaseModel):
@@ -122,18 +282,18 @@ class WalmartStoreAsyncPlugin(BaseModel):
122282

123283
model_config = ConfigDict(
124284
populate_by_name=True,
125-
extra="ignore"
285+
extra="ignore",
126286
)
127287

128288
key: Literal["walmart/store"] = Field(
129289
default="walmart/store",
130-
alias="Key"
290+
alias="Key",
131291
)
132292
params: List[WalmartStoreParameters] = Field(
133293
...,
134294
alias="Params",
135295
min_length=1,
136-
max_length=1000
296+
max_length=1000,
137297
)
138298

139299

@@ -148,16 +308,16 @@ class LowesStoreAsyncPlugin(BaseModel):
148308

149309
model_config = ConfigDict(
150310
populate_by_name=True,
151-
extra="ignore"
311+
extra="ignore",
152312
)
153313

154314
key: Literal["lowes/store"] = Field(
155315
default="lowes/store",
156-
alias="Key"
316+
alias="Key",
157317
)
158318
params: List[LowesStoreParameters] = Field(
159319
...,
160320
alias="Params",
161321
min_length=1,
162-
max_length=1000
322+
max_length=1000,
163323
)

0 commit comments

Comments
 (0)