From 0aa404e1c3f1cdebfde516196982603c1bc9fa3e Mon Sep 17 00:00:00 2001 From: kasparasizi1 <132673909+kasparasizi1@users.noreply.github.com> Date: Sun, 12 Jul 2026 12:10:51 +0300 Subject: [PATCH] feat(depop): add Depop scraper client (v0.19.0) Search, product, shop, user-products and markets endpoints for the Depop second-hand fashion marketplace (/v1/depop/*). One global host localised by market. Mirrors the Redfin client module. --- pyproject.toml | 2 +- src/scrapebadger/__init__.py | 37 +++- src/scrapebadger/_internal/client.py | 2 +- src/scrapebadger/client.py | 25 +++ src/scrapebadger/depop/__init__.py | 55 ++++++ src/scrapebadger/depop/client.py | 246 +++++++++++++++++++++++++++ src/scrapebadger/depop/models.py | 128 ++++++++++++++ uv.lock | 2 +- 8 files changed, 493 insertions(+), 4 deletions(-) create mode 100644 src/scrapebadger/depop/__init__.py create mode 100644 src/scrapebadger/depop/client.py create mode 100644 src/scrapebadger/depop/models.py diff --git a/pyproject.toml b/pyproject.toml index 45f6093..1c86193 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scrapebadger" -version = "0.18.0" +version = "0.19.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/__init__.py b/src/scrapebadger/__init__.py index 3a12daf..2debeb4 100644 --- a/src/scrapebadger/__init__.py +++ b/src/scrapebadger/__init__.py @@ -82,6 +82,31 @@ async def main(): SearchResult as AmazonSearchResult, ) from scrapebadger.client import ScrapeBadger +from scrapebadger.depop.client import DepopClient +from scrapebadger.depop.models import ( + DepopCard, +) +from scrapebadger.depop.models import ( + Market as DepopMarket, +) +from scrapebadger.depop.models import ( + MarketsResponse as DepopMarketsResponse, +) +from scrapebadger.depop.models import ( + ProductDetail as DepopProductDetail, +) +from scrapebadger.depop.models import ( + SearchMeta as DepopSearchMeta, +) +from scrapebadger.depop.models import ( + SearchResponse as DepopSearchResponse, +) +from scrapebadger.depop.models import ( + ShopProfile as DepopShopProfile, +) +from scrapebadger.depop.models import ( + UserProductsResponse as DepopUserProductsResponse, +) from scrapebadger.ebay.client import EbayClient from scrapebadger.ebay.models import ( AutocompleteResponse as EbayAutocompleteResponse, @@ -810,7 +835,7 @@ async def main(): ZestimateHistoryPoint as ZillowZestimateHistoryPoint, ) -__version__ = "0.18.0" +__version__ = "0.19.0" __all__ = [ # TikTok core models @@ -837,6 +862,16 @@ async def main(): "ColorsResponse", "Deal", "DealsResponse", + # Depop + "DepopCard", + "DepopClient", + "DepopMarket", + "DepopMarketsResponse", + "DepopProductDetail", + "DepopSearchMeta", + "DepopSearchResponse", + "DepopShopProfile", + "DepopUserProductsResponse", # Web scraping "DetectResult", # eBay response envelopes / models diff --git a/src/scrapebadger/_internal/client.py b/src/scrapebadger/_internal/client.py index d8ef167..73b7b5c 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.18.0" +SDK_VERSION = "0.19.0" USER_AGENT = f"scrapebadger-python/{SDK_VERSION}" diff --git a/src/scrapebadger/client.py b/src/scrapebadger/client.py index 1d3d8be..867e60b 100644 --- a/src/scrapebadger/client.py +++ b/src/scrapebadger/client.py @@ -10,6 +10,7 @@ from scrapebadger._internal.client import BaseClient from scrapebadger._internal.config import ClientConfig from scrapebadger.amazon.client import AmazonClient +from scrapebadger.depop.client import DepopClient from scrapebadger.ebay.client import EbayClient from scrapebadger.google.client import GoogleClient from scrapebadger.immobiliare.client import ImmobiliareClient @@ -127,6 +128,7 @@ def __init__( self._google: GoogleClient | None = None self._reddit: RedditClient | None = None self._redfin: RedfinClient | None = None + self._depop: DepopClient | None = None self._amazon: AmazonClient | None = None self._ebay: EbayClient | None = None self._youtube: YoutubeClient | None = None @@ -238,6 +240,29 @@ def redfin(self) -> RedfinClient: self._redfin = RedfinClient(self._base_client) return self._redfin + @property + def depop(self) -> DepopClient: + """Access Depop Scraper API operations. + + Returns: + DepopClient providing access to all Depop endpoints (search, + product detail, shop profile, user products, markets). Depop is a + second-hand fashion marketplace: one global host (depop.com) + localised by market (us, gb [alias uk], au, ie, it, fr, de, es, nl, + nz) → country/currency. + + Example: + ```python + results = await client.depop.search("nike vintage") + detail = await client.depop.get_product("some-product-slug") + shop = await client.depop.get_user("someseller") + markets = await client.depop.list_markets() + ``` + """ + if self._depop is None: + self._depop = DepopClient(self._base_client) + return self._depop + @property def google(self) -> GoogleClient: """Access Google Scraper API operations. diff --git a/src/scrapebadger/depop/__init__.py b/src/scrapebadger/depop/__init__.py new file mode 100644 index 0000000..a96d648 --- /dev/null +++ b/src/scrapebadger/depop/__init__.py @@ -0,0 +1,55 @@ +"""Depop API module for ScrapeBadger SDK. + +This module provides an async client for scraping Depop second-hand fashion +marketplace data through the ScrapeBadger API. All methods are async and return +strongly-typed Pydantic models. + +Example: + ```python + from scrapebadger import ScrapeBadger + + async with ScrapeBadger(api_key="your-key") as client: + # Search products + results = await client.depop.search("nike vintage") + for card in results.products: + print(f"{card.slug} — {card.price}") + + # Get product detail + detail = await client.depop.get_product("some-product-slug") + print(detail.title) + + # Get a shop profile + shop = await client.depop.get_user("someseller") + print(shop.name) + ``` +""" + +from scrapebadger.depop.client import DepopClient +from scrapebadger.depop.models import ( + DepopCard, + Market, + MarketsResponse, + ProductDetail, + SearchMeta, + SearchResponse, + ShopProfile, + UserProductsResponse, +) + +__all__ = [ + # Cards + "DepopCard", + # Client + "DepopClient", + # Markets + "Market", + "MarketsResponse", + # Product detail + "ProductDetail", + # Search / user-products + "SearchMeta", + "SearchResponse", + # Shop profile + "ShopProfile", + "UserProductsResponse", +] diff --git a/src/scrapebadger/depop/client.py b/src/scrapebadger/depop/client.py new file mode 100644 index 0000000..753c5b0 --- /dev/null +++ b/src/scrapebadger/depop/client.py @@ -0,0 +1,246 @@ +"""Depop API client. + +Depop endpoints: search (product grid), get_product (full single-listing +detail, by slug), get_user (shop/seller profile), get_user_products (a +seller's product grid), and list_markets. All methods are async and return +strongly-typed Pydantic models. Single global host (depop.com) localised by +``market`` — the market code selects country and currency (us, gb [alias uk], +au, ie, it, fr, de, es, nl, nz). +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from scrapebadger.depop.models import ( + MarketsResponse, + ProductDetail, + SearchResponse, + ShopProfile, + UserProductsResponse, +) + +if TYPE_CHECKING: + from scrapebadger._internal.client import BaseClient + + +class DepopClient: + """Client for all Depop API operations. + + Depop is a second-hand fashion marketplace. There is one global host + (depop.com) localised by ``market`` (us, gb [alias uk], au, ie, it, fr, de, + es, nl, nz), which selects the country and currency the storefront renders + in. Search and user-products return card grids (24 per page, ``page``-based + pagination); get_product and get_user return full detail. + + Example: + ```python + from scrapebadger import ScrapeBadger + + async with ScrapeBadger(api_key="your-key") as client: + # Search products + results = await client.depop.search("nike vintage") + for card in results.products: + print(f"{card.slug} — {card.price}") + + # Full product detail + detail = await client.depop.get_product("some-product-slug") + print(detail.title) + + # Shop profile + their products + shop = await client.depop.get_user("someseller") + listings = await client.depop.get_user_products("someseller") + + # Supported markets + markets = await client.depop.list_markets() + ``` + + Note: + Depop data is browser-rendered (cloakbrowser), so calls take a few + seconds to complete. + + This client is not instantiated directly. Instead, access it through + the `depop` property of the main `ScrapeBadger` client. + """ + + def __init__(self, client: BaseClient) -> None: + """Initialize Depop client. + + Args: + client: The base HTTP client for making API requests. + """ + self._client = client + + async def search( + self, + query: str, + *, + market: str = "us", + per_page: int = 24, + page: int = 1, + price_min: float | None = None, + price_max: float | None = None, + brands: str | None = None, + sizes: str | None = None, + colours: str | None = None, + conditions: str | None = None, + gender: str | None = None, + sort: str | None = None, + ) -> SearchResponse: + """Search Depop for products. + + Args: + query: Search term (required). + market: Market code (us, gb [alias uk], au, ie, it, fr, de, es, nl, + nz). Defaults to "us". + per_page: Cards per page. Defaults to 24. + page: Page number (``page``-based pagination). Defaults to 1. + price_min: Minimum price filter. + price_max: Maximum price filter. + brands: Comma-separated brand filter. + sizes: Comma-separated size filter. + colours: Comma-separated colour filter. + conditions: Comma-separated condition filter. + gender: Gender filter. + sort: Sort order. + + Returns: + Search response with matching product cards and pagination. + + Raises: + AuthenticationError: If the API key is invalid. + ValidationError: If the parameters are invalid. + + Example: + ```python + results = await client.depop.search( + "nike vintage", + market="gb", + price_max=100, + sort="priceAscending", + ) + print(f"Page {results.meta.page}") + ``` + """ + params: dict[str, Any] = { + "query": query, + "market": market, + "per_page": per_page, + "page": page, + "price_min": price_min, + "price_max": price_max, + "brands": brands, + "sizes": sizes, + "colours": colours, + "conditions": conditions, + "gender": gender, + "sort": sort, + } + response = await self._client.get("/v1/depop/search", params=params) + return SearchResponse.model_validate(response) + + async def get_product(self, slug: str, *, market: str = "us") -> ProductDetail: + """Get a single Depop product's full detail by slug. + + Args: + slug: The Depop product slug. + market: Market code (us, gb [alias uk], au, ie, it, fr, de, es, nl, + nz). Defaults to "us". + + Returns: + Product detail response. + + Raises: + NotFoundError: If the product doesn't exist. + AuthenticationError: If the API key is invalid. + + Example: + ```python + detail = await client.depop.get_product("some-product-slug") + print(detail.title, detail.price) + ``` + """ + params: dict[str, Any] = {"market": market} + response = await self._client.get(f"/v1/depop/products/{slug}", params=params) + return ProductDetail.model_validate(response) + + async def get_user(self, username: str, *, market: str = "us") -> ShopProfile: + """Get a Depop seller's shop profile. + + Args: + username: The Depop seller username. + market: Market code (us, gb [alias uk], au, ie, it, fr, de, es, nl, + nz). Defaults to "us". + + Returns: + Shop profile response. + + Raises: + NotFoundError: If the user doesn't exist. + AuthenticationError: If the API key is invalid. + + Example: + ```python + shop = await client.depop.get_user("someseller") + print(shop.name, shop.follower_count) + ``` + """ + params: dict[str, Any] = {"market": market} + response = await self._client.get(f"/v1/depop/users/{username}", params=params) + return ShopProfile.model_validate(response) + + async def get_user_products( + self, + username: str, + *, + market: str = "us", + per_page: int = 24, + page: int = 1, + ) -> UserProductsResponse: + """Get a Depop seller's product grid. + + Args: + username: The Depop seller username. + market: Market code (us, gb [alias uk], au, ie, it, fr, de, es, nl, + nz). Defaults to "us". + per_page: Cards per page. Defaults to 24. + page: Page number (``page``-based pagination). Defaults to 1. + + Returns: + User products response with the seller's product cards and + pagination. + + Raises: + NotFoundError: If the user doesn't exist. + AuthenticationError: If the API key is invalid. + + Example: + ```python + listings = await client.depop.get_user_products("someseller") + for card in listings.products: + print(card.slug, card.price) + ``` + """ + params: dict[str, Any] = { + "market": market, + "per_page": per_page, + "page": page, + } + response = await self._client.get(f"/v1/depop/users/{username}/products", params=params) + return UserProductsResponse.model_validate(response) + + async def list_markets(self) -> MarketsResponse: + """Get all supported Depop coverage markets. + + Returns: + Markets response with all supported markets. + + Example: + ```python + result = await client.depop.list_markets() + for m in result.markets: + print(f"{m.code}: {m.name} ({m.currency})") + ``` + """ + response = await self._client.get("/v1/depop/markets") + return MarketsResponse.model_validate(response) diff --git a/src/scrapebadger/depop/models.py b/src/scrapebadger/depop/models.py new file mode 100644 index 0000000..92bdc20 --- /dev/null +++ b/src/scrapebadger/depop/models.py @@ -0,0 +1,128 @@ +"""Pydantic models for Depop API responses. + +These models mirror the backend ``depop_scraper`` response schema +field-for-field. All models are immutable (frozen) and ignore unknown fields +for forward compatibility. + +Depop is a single global host (depop.com) localised by ``market`` — the market +code selects the country and currency the storefront renders in. Search and +user-products endpoints return card grids; product and shop endpoints return +full detail. +""" + +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict, Field + +# ============================================================================= +# Base Configuration +# ============================================================================= + + +class _BaseModel(BaseModel): + """Base model with common configuration.""" + + model_config = ConfigDict( + frozen=True, + populate_by_name=True, + extra="ignore", + ) + + +# ============================================================================= +# Search / user-products cards +# ============================================================================= + + +class DepopCard(_BaseModel): + """One Depop product card from a search or user-products grid.""" + + slug: str + url: str + seller_username: str | None = None + brand: str | None = None + size: str | None = None + price: str | None = None + original_price: str | None = None + currency: str | None = None + is_sold: bool = False + image: str | None = None + + +class SearchMeta(_BaseModel): + """Pagination metadata for a card grid.""" + + result_count: int = 0 + page: int = 1 + has_more: bool = False + + +class SearchResponse(_BaseModel): + products: list[DepopCard] = Field(default_factory=list) + meta: SearchMeta + market: str + query: str + + +# ============================================================================= +# Product detail +# ============================================================================= + + +class ProductDetail(_BaseModel): + """Full Depop product detail.""" + + id: int | None = None + slug: str + title: str | None + description: str | None + brand: str | None + condition: str | None + price: str | None + currency: str | None + availability: str | None + seller_username: str | None + images: list[str] = Field(default_factory=list) + url: str + + +# ============================================================================= +# Shop / user profile +# ============================================================================= + + +class ShopProfile(_BaseModel): + """A Depop seller's shop profile.""" + + username: str + name: str | None + description: str | None + rating_value: str | None + rating_count: int | None + follower_count: int | None + url: str + + +class UserProductsResponse(_BaseModel): + username: str + products: list[DepopCard] = Field(default_factory=list) + meta: SearchMeta + market: str + + +# ============================================================================= +# Markets +# ============================================================================= + + +class Market(_BaseModel): + """A supported Depop market (country + currency).""" + + code: str + country_code: str + currency: str + name: str + + +class MarketsResponse(_BaseModel): + markets: list[Market] = Field(default_factory=list) diff --git a/uv.lock b/uv.lock index bd9ec59..ddb1f7e 100644 --- a/uv.lock +++ b/uv.lock @@ -752,7 +752,7 @@ wheels = [ [[package]] name = "scrapebadger" -version = "0.18.0" +version = "0.19.0" source = { editable = "." } dependencies = [ { name = "httpx" },