Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "scrapebadger"
version = "0.13.1"
version = "0.14.0"
description = "Official Python SDK for ScrapeBadger - Async web scraping APIs for Twitter and more"
readme = "README.md"
license = { text = "MIT" }
Expand Down
2 changes: 1 addition & 1 deletion src/scrapebadger/_internal/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
T = TypeVar("T")

# User agent for SDK requests
SDK_VERSION = "0.13.1"
SDK_VERSION = "0.14.0"
USER_AGENT = f"scrapebadger-python/{SDK_VERSION}"


Expand Down
8 changes: 8 additions & 0 deletions src/scrapebadger/vinted/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ class VintedItemSummary(_BaseModel):
user: Summary of the item owner.
photo: Main photo.
photos: All photos attached to the item.
seller_country_code: Physical country of the seller as an upper-case
ISO-2 code (e.g. "FR"), or None. Populated only when the
``seller_country`` search filter is used.
"""

id: int
Expand All @@ -148,6 +151,7 @@ class VintedItemSummary(_BaseModel):
user: VintedUserSummary | None = None
photo: VintedPhoto | None = None
photos: list[VintedPhoto] = Field(default_factory=list)
seller_country_code: str | None = None


class VintedItemDetail(_BaseModel):
Expand Down Expand Up @@ -366,11 +370,15 @@ class SearchResponse(_BaseModel):
items: List of matching items.
pagination: Pagination metadata.
market: Market code used for the search.
seller_country: Echo of the normalized ``seller_country`` filter applied
to this search (comma-separated ISO-2 codes, e.g. "fr,be"), or None
when no filter was used.
"""

items: list[VintedItemSummary] = Field(default_factory=list)
pagination: VintedPagination | None = None
market: str = ""
seller_country: str | None = None


class ItemDetailResponse(_BaseModel):
Expand Down
20 changes: 20 additions & 0 deletions src/scrapebadger/vinted/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ async def search(
color_ids: str | None = None,
status_ids: str | None = None,
order: str | None = None,
seller_country: str | None = None,
) -> SearchResponse:
"""Search for items on Vinted.

Expand All @@ -72,6 +73,14 @@ async def search(
color_ids: Comma-separated color IDs to filter by.
status_ids: Comma-separated status IDs to filter by.
order: Sort order (e.g. "newest_first", "price_low_to_high").
seller_country: Filter results to items whose seller is physically
located in one of the given countries. A comma-separated list of
ISO-2 country codes (e.g. ``"fr"`` or ``"fr,be"``). Vinted
federates cross-border EU listings into each market domain and has
no native country filter, so ScrapeBadger applies this filter.
When set, each returned item gains a ``seller_country_code`` and
the response gains a top-level ``seller_country`` echo.
Billing: 1 base credit + 1 credit per uncached seller looked up.

Returns:
Search response with matching items and pagination metadata.
Expand All @@ -92,6 +101,16 @@ async def search(
print(f"Found {results.pagination.total_entries} items")
for item in results.items:
print(f" {item.title} - {item.price.amount} {item.price.currency_code}")

# Filter to sellers physically located in France or Belgium.
# Billing: 1 base credit + 1 credit per uncached seller looked up.
local = await client.vinted.search.search(
"vintage jacket",
seller_country="fr,be",
)
print(f"Applied seller_country filter: {local.seller_country}")
for item in local.items:
print(f" {item.title} - seller in {item.seller_country_code}")
```
"""
params: dict[str, Any] = {
Expand All @@ -105,6 +124,7 @@ async def search(
"color_ids": color_ids,
"status_ids": status_ids,
"order": order,
"seller_country": seller_country,
}
response = await self._client.get("/v1/vinted/search", params=params)
return SearchResponse.model_validate(response)
26 changes: 26 additions & 0 deletions tests/test_vinted.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,22 @@ def test_search_response_empty(self) -> None:
assert resp.items == []
assert resp.pagination is None

def test_search_response_seller_country_default(self) -> None:
resp = SearchResponse.model_validate(SEARCH_RESPONSE)
assert resp.seller_country is None
assert resp.items[0].seller_country_code is None

def test_search_response_with_seller_country(self) -> None:
resp = SearchResponse.model_validate(
{
"items": [{"id": 1, "seller_country_code": "FR"}],
"market": "fr",
"seller_country": "fr,be",
}
)
assert resp.seller_country == "fr,be"
assert resp.items[0].seller_country_code == "FR"

def test_item_detail_response(self) -> None:
resp = ItemDetailResponse.model_validate(ITEM_DETAIL_RESPONSE)
assert resp.item is not None
Expand Down Expand Up @@ -610,6 +626,15 @@ async def test_search_with_filters(
assert params["status_ids"] == "6"
assert params["order"] == "newest_first"

async def test_search_with_seller_country(
self, search_client: SearchClient, mock_base_client: MagicMock
) -> None:
mock_base_client.get.return_value = SEARCH_RESPONSE
await search_client.search("jacket", seller_country="fr,be")

params = mock_base_client.get.call_args[1]["params"]
assert params["seller_country"] == "fr,be"

async def test_search_optional_params_default_none(
self, search_client: SearchClient, mock_base_client: MagicMock
) -> None:
Expand All @@ -623,6 +648,7 @@ async def test_search_optional_params_default_none(
assert params["color_ids"] is None
assert params["status_ids"] is None
assert params["order"] is None
assert params["seller_country"] is None

async def test_search_returns_search_response(
self, search_client: SearchClient, mock_base_client: MagicMock
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading