From 461424c7afeb2737d40baa916a4ffcdeb400ae35 Mon Sep 17 00:00:00 2001 From: kasparasizi1 <132673909+kasparasizi1@users.noreply.github.com> Date: Sat, 13 Jun 2026 17:03:54 +0300 Subject: [PATCH] feat(vinted): add seller_country filter to search (0.14.0) Adds an optional seller_country kwarg (CSV ISO-2) to vinted.search that filters results to sellers physically located in the given countries (Vinted federates cross-border EU listings; no native country filter). Adds seller_country_code to the item model and seller_country to the search response. Adds 1 credit per uncached seller looked up. Bumps version 0.13.1 -> 0.14.0 (pyproject + SDK_VERSION User-Agent). __init__.__version__ intentionally not touched here to avoid conflict with concurrent in-flight Shopee work in the same file. --- pyproject.toml | 2 +- src/scrapebadger/_internal/client.py | 2 +- src/scrapebadger/vinted/models.py | 8 ++++++++ src/scrapebadger/vinted/search.py | 20 ++++++++++++++++++++ tests/test_vinted.py | 26 ++++++++++++++++++++++++++ uv.lock | 2 +- 6 files changed, 57 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f422258..eb3cf93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" } diff --git a/src/scrapebadger/_internal/client.py b/src/scrapebadger/_internal/client.py index f6f2131..73bd223 100644 --- a/src/scrapebadger/_internal/client.py +++ b/src/scrapebadger/_internal/client.py @@ -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}" diff --git a/src/scrapebadger/vinted/models.py b/src/scrapebadger/vinted/models.py index beb6e8f..08ef2f2 100644 --- a/src/scrapebadger/vinted/models.py +++ b/src/scrapebadger/vinted/models.py @@ -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 @@ -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): @@ -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): diff --git a/src/scrapebadger/vinted/search.py b/src/scrapebadger/vinted/search.py index 3f2595d..2a38382 100644 --- a/src/scrapebadger/vinted/search.py +++ b/src/scrapebadger/vinted/search.py @@ -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. @@ -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. @@ -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] = { @@ -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) diff --git a/tests/test_vinted.py b/tests/test_vinted.py index ba18565..c1ac29e 100644 --- a/tests/test_vinted.py +++ b/tests/test_vinted.py @@ -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 @@ -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: @@ -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 diff --git a/uv.lock b/uv.lock index 7aee908..b38c922 100644 --- a/uv.lock +++ b/uv.lock @@ -752,7 +752,7 @@ wheels = [ [[package]] name = "scrapebadger" -version = "0.13.0" +version = "0.14.0" source = { editable = "." } dependencies = [ { name = "httpx" },