vinted_scraper

Vinted Scraper - A Python package for scraping Vinted marketplace.

This package provides both synchronous and asynchronous clients for interacting with the Vinted API. Choose between typed model responses (Scraper) or raw JSON responses (Wrapper).

Classes: VintedScraper: Synchronous client with typed VintedItem responses. VintedWrapper: Synchronous client with raw JSON responses. AsyncVintedScraper: Asynchronous client with typed VintedItem responses. AsyncVintedWrapper: Asynchronous client with raw JSON responses.

Examples:
    See https://github.com/Giglium/vinted_scraper/tree/main/examples
 1"""Vinted Scraper - A Python package for scraping Vinted marketplace.
 2
 3This package provides both synchronous and asynchronous clients for interacting
 4with the Vinted API. Choose between typed model responses (Scraper) or raw JSON
 5responses (Wrapper).
 6
 7Classes:
 8    VintedScraper: Synchronous client with typed VintedItem responses.
 9    VintedWrapper: Synchronous client with raw JSON responses.
10    AsyncVintedScraper: Asynchronous client with typed VintedItem responses.
11    AsyncVintedWrapper: Asynchronous client with raw JSON responses.
12
13    Examples:
14        See https://github.com/Giglium/vinted_scraper/tree/main/examples
15"""
16
17from ._async_scraper import AsyncVintedScraper
18from ._async_wrapper import AsyncVintedWrapper
19from ._scraper import VintedScraper
20from ._wrapper import VintedWrapper
21from .models import OgField, VintedSession
22
23__all__ = [
24    "AsyncVintedWrapper",
25    "VintedWrapper",
26    "AsyncVintedScraper",
27    "VintedScraper",
28    "OgField",
29    "VintedSession",
30]
@dataclass
class AsyncVintedWrapper(vinted_scraper._base_wrapper.BaseVintedWrapper):
 39@dataclass
 40class AsyncVintedWrapper(BaseVintedWrapper):
 41    """Asynchronous Vinted API wrapper returning raw JSON responses.
 42
 43    Handles cookie management, retries, and async HTTP requests automatically.
 44    Returns raw JSON dictionaries instead of typed objects.
 45
 46    Attributes:
 47        baseurl: Vinted domain URL (e.g., "https://www.vinted.com").
 48        session: API identity (VintedSession). Auto-fetched if None or empty.
 49        user_agent: Custom user agent string. Auto-generated if None.
 50        config: httpx client configuration dict.
 51        cookie_names: List of cookie names to extract. Defaults to ["access_token_web"].
 52        locale: Optional ``Locale`` override (e.g. "en-US"). Read from the
 53            landing page when None, falling back to a guess from ``baseurl``.
 54
 55    Example:
 56        See https://github.com/Giglium/vinted_scraper/blob/main/examples/async_wrapper.py
 57    """
 58
 59    _client: httpx.AsyncClient = field(init=False, repr=False)
 60    _api_client: httpx.AsyncClient = field(init=False, repr=False)
 61
 62    @classmethod
 63    async def create(
 64        cls,
 65        baseurl: str,
 66        user_agent: Optional[str] = None,
 67        config: Optional[Dict] = None,
 68        cookie_names: Optional[List[str]] = None,
 69        session: Optional[VintedSession] = None,
 70        locale: Optional[str] = None,
 71    ):
 72        """Factory method to create an AsyncVintedWrapper instance.
 73
 74        Use this instead of direct instantiation to automatically fetch the
 75        session identity. Pass
 76        ``session`` to reuse a prefetched one and skip the network fetch.
 77
 78        Args:
 79            baseurl: Vinted domain URL (e.g., "https://www.vinted.com").
 80            user_agent: Custom user agent string. Auto-generated if None.
 81            config: httpx client configuration dict.
 82            cookie_names: List of cookie names to extract. Defaults to ["access_token_web"].
 83            session: A prefetched ``VintedSession``. Auto-fetched if None/empty.
 84            locale: Optional ``Locale`` override. When None, the locale is read
 85                from the landing page, falling back to a guess from ``baseurl``.
 86
 87        Returns:
 88            Initialized AsyncVintedWrapper instance with a resolved session.
 89        """
 90        _log.debug("Creating the async wrapper using the factory method")
 91        self = cls(
 92            baseurl,
 93            session=session,
 94            user_agent=user_agent,
 95            config=config,
 96            cookie_names=cookie_names,
 97            locale=locale,
 98        )
 99        if self._needs_session():
100            await self.refresh_session()
101        return self
102
103    def __post_init__(self) -> None:
104        """Initialize AsyncVintedWrapper after dataclass initialization.
105
106        Validates the base URL, sets up user agent, and initializes the two
107        httpx async clients (site host and ``api.`` host).
108
109        Raises:
110            RuntimeError: If the base URL is invalid.
111
112        Note:
113            Use the create() factory method instead of direct instantiation to
114            automatically fetch the session cookie.
115        """
116        site_config, api_config = self._validate_and_init()
117        self._client = httpx.AsyncClient(**site_config)
118        self._api_client = httpx.AsyncClient(**api_config)
119
120    async def refresh_session(self, retries: int = DEFAULT_RETRIES) -> VintedSession:
121        """Manually refresh the session cookie and API tokens.
122
123        Args:
124            retries: Number of retry attempts (default: 3).
125
126        Returns:
127            The ``VintedSession`` returned by :meth:`fetch_session`.
128
129        Raises:
130            RuntimeError: If cookies cannot be fetched after all retries.
131        """
132        log_refresh_cookie(_log)
133        session = await AsyncVintedWrapper.fetch_session(
134            self._client,
135            self._get_cookie_headers(),
136            self.cookie_names,
137            retries,
138        )
139        self.session = session
140        return session
141
142    @staticmethod
143    async def fetch_session(
144        client: httpx.AsyncClient,
145        headers: Dict,
146        cookie_names: List[str],
147        retries: int = DEFAULT_RETRIES,
148    ) -> VintedSession:
149        """Fetch the full session identity from Vinted's landing page (async).
150
151        Args:
152            client: httpx.AsyncClient instance.
153            headers: HTTP headers dictionary.
154            cookie_names: List of cookie names to extract.
155            retries: Number of retry attempts (default: 3).
156
157        Returns:
158            A ``VintedSession`` with the cookies, CSRF token and anonymous id.
159
160        Raises:
161            RuntimeError: If cookies cannot be fetched after all retries.
162        """
163        response = None
164
165        for i in range(retries):
166            log_interaction(_log, i, retries)
167            html, response = await AsyncVintedWrapper._stream_until(
168                client, "/", headers, CSRF_MARKER
169            )
170
171            if response.status_code == HTTP_OK:
172                session = build_session(response, html, cookie_names)
173                if session.is_usable():
174                    return session
175
176            sleep_time = handle_session_failure(response, i, retries)
177            if i < retries - 1:
178                await asyncio.sleep(sleep_time)
179
180        raise_session_error(client.base_url, response)
181
182    @staticmethod
183    async def _stream_until(
184        client: httpx.AsyncClient, url: str, headers: Dict, stop_marker: str
185    ) -> Tuple[str, httpx.Response]:
186        """Stream ``url``, reading only up to ``stop_marker``.
187
188        Shared by :meth:`item` (stops at ``</head>``) and the session fetch
189        (stops at the CSRF marker) so that neither downloads the full page body.
190        The chunk-boundary bookkeeping lives in :class:`ChunkAccumulator`.
191
192        Args:
193            client: httpx.AsyncClient instance.
194            url: The URL or endpoint path to request.
195            headers: Request headers for the GET.
196            stop_marker: The text marker that ends the read.
197
198        Returns:
199            Tuple of ``(html, response)``. ``html`` is empty for non-200
200            responses.
201        """
202        acc = ChunkAccumulator(stop_marker)
203        async with client.stream("GET", url, headers=headers) as response:
204            if response.status_code == HTTP_OK:
205                async for chunk in response.aiter_text(chunk_size=STREAM_CHUNK_SIZE):
206                    if acc.add(chunk):
207                        break
208            else:
209                await response.aread()
210        return acc.text, response
211
212    async def search(self, params: Optional[Dict] = None) -> Dict[str, Any]:
213        """Search for items on Vinted asynchronously.
214
215        Args:
216            params: Query parameters. Common parameters:
217                - search_text: Search query
218                - page: Page number
219                - per_page: Items per page
220                - price_from: Minimum price
221                - price_to: Maximum price
222                - order: Sort order
223                - attribute_ids[catalog]: Category IDs (comma-separated;
224                - attribute_ids[status]: Condition IDs (comma-separated;
225                - attribute_ids[brand]: Brand IDs (comma-separated;
226                - attribute_ids[size]: Size IDs (comma-separated;
227
228        Returns:
229            Dictionary containing JSON response with search results.
230        """
231        log_search(_log, params)
232        return await self.curl(
233            self._search_endpoint(), params=params, api_endpoint=True
234        )
235
236    async def item(
237        self, item_id: str, fields: Optional[List[str]] = None
238    ) -> Dict[str, Any]:
239        """Read item metadata from the public item page (HTML), asynchronously.
240
241        The JSON item endpoint (``/api/v2/items/{id}/details``) is blocked by the
242        anti-bot protection and returns ``403`` (see
243        https://github.com/Giglium/vinted_scraper/issues/59), so the item data is
244        read from the public item page instead. Uses HTTP streaming to download
245        only the ``<head>`` section, extracting OpenGraph meta tags without
246        fetching the full page body.
247
248        Args:
249            item_id: The unique identifier of the item.
250            fields: List of ``OgField`` values to extract. Defaults to all
251                fields (``[OgField.TITLE, OgField.DESCRIPTION, OgField.URL,
252                OgField.IMAGE]``).
253
254        Returns:
255            A dict always containing ``id``, plus keys ``title``,
256            ``description``, ``url``, and ``image`` (each present only if
257            found and requested).
258
259        Raises:
260            RuntimeError: If the item page cannot be fetched (non-200 status).
261        """
262        log_item(_log, item_id, fields)
263        endpoint = self._item_endpoint(item_id)
264        headers = self._build_page_headers()
265
266        head_html, response = await AsyncVintedWrapper._stream_until(
267            self._client, endpoint, headers, HEAD_END_TAG
268        )
269        status_code = response.status_code
270
271        log_curl_response(_log, endpoint, status_code, response.headers, head_html)
272
273        if status_code == HTTP_OK:
274            return parse_item_page(item_id, head_html, fields)
275
276        self._raise_curl_error(endpoint, status_code)
277
278    async def curl(
279        self,
280        endpoint: str,
281        params: Optional[Dict] = None,
282        *,
283        api_endpoint: bool = False,
284        _retries: int = 0,
285    ) -> Dict[str, Any]:
286        """Send an async HTTP GET request to a relative Vinted API endpoint.
287
288        The ``endpoint`` is always relative and is concatenated with the chosen
289        client's ``base_url``: the ``api.`` host when ``api_endpoint`` is True,
290        otherwise the site (``www.``) host. Automatically handles headers,
291        cookies, retries, and error responses.
292
293        Args:
294            endpoint: Relative API endpoint path (e.g., "/api/v2/users/name").
295            params: Optional query parameters.
296            api_endpoint: Route the request to the ``api.`` host instead of the
297                site host.
298
299        Returns:
300            Dictionary containing the parsed JSON response.
301
302        Raises:
303            RuntimeError: If response status is not 200 or JSON parsing fails.
304        """
305        client = self._api_client if api_endpoint else self._client
306        headers = self._build_curl_headers()
307        log_curl_request(_log, str(client.base_url), endpoint, headers, params)
308
309        response = await client.get(endpoint, headers=headers, params=params)
310
311        log_curl_response(
312            _log, endpoint, response.status_code, response.headers, response.text
313        )
314
315        if response.status_code == HTTP_OK:
316            return self._handle_curl_response(response, endpoint)
317
318        if response.status_code == HTTP_UNAUTHORIZED and _retries < DEFAULT_RETRIES:
319            log_cookie_retry(_log, response.status_code)
320            await self.refresh_session()
321            return await self.curl(
322                endpoint, params, api_endpoint=api_endpoint, _retries=_retries + 1
323            )
324
325        self._raise_curl_error(endpoint, response.status_code)
326
327    async def __aenter__(self) -> "AsyncVintedWrapper":  # pragma: no cover
328        """Enter async context manager.
329
330        Returns:
331            Self for use in async with statement.
332        """
333        return self
334
335    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:  # pragma: no cover
336        """Exit async context manager and close both HTTP clients.
337
338        Args:
339            exc_type: Exception type (unused).
340            exc_val: Exception value (unused).
341            exc_tb: Exception traceback (unused).
342        """
343        await self._client.aclose()
344        await self._api_client.aclose()
345
346    def __del__(self) -> None:  # pragma: no cover
347        """Best-effort cleanup of the HTTP clients on garbage collection.
348
349        Prefer using the async context manager (``async with`` statement)
350        for deterministic resource cleanup.
351
352        Note: httpx.AsyncClient exposes ``aclose()`` (async) but not
353        ``close()`` (sync). Since ``__del__`` cannot await, we attempt a
354        synchronous close via the underlying transport if available.
355        """
356        for attr in ("_client", "_api_client"):
357            client = getattr(self, attr, None)
358            if client is None or client.is_closed:
359                continue
360            try:
361                # httpx >=0.28 removed the sync close() helper on AsyncClient
362                client.close()  # type: ignore[attr-defined]
363            except AttributeError:
364                # Fallback: close the underlying transport directly
365                transport = getattr(client, "_transport", None)
366                if transport is not None and hasattr(transport, "close"):
367                    transport.close()

Asynchronous Vinted API wrapper returning raw JSON responses.

Handles cookie management, retries, and async HTTP requests automatically. Returns raw JSON dictionaries instead of typed objects.

Attributes: baseurl: Vinted domain URL (e.g., "https://www.vinted.com"). session: API identity (VintedSession). Auto-fetched if None or empty. user_agent: Custom user agent string. Auto-generated if None. config: httpx client configuration dict. cookie_names: List of cookie names to extract. Defaults to ["access_token_web"]. locale: Optional Locale override (e.g. "en-US"). Read from the landing page when None, falling back to a guess from baseurl.

Example: See https://github.com/Giglium/vinted_scraper/blob/main/examples/async_wrapper.py

AsyncVintedWrapper( baseurl: str, session: VintedSession | None = None, user_agent: str | None = None, config: Dict | None = None, cookie_names: List[str] | None = None, locale: str | None = None)
@classmethod
async def create( cls, baseurl: str, user_agent: str | None = None, config: Dict | None = None, cookie_names: List[str] | None = None, session: VintedSession | None = None, locale: str | None = None):
 62    @classmethod
 63    async def create(
 64        cls,
 65        baseurl: str,
 66        user_agent: Optional[str] = None,
 67        config: Optional[Dict] = None,
 68        cookie_names: Optional[List[str]] = None,
 69        session: Optional[VintedSession] = None,
 70        locale: Optional[str] = None,
 71    ):
 72        """Factory method to create an AsyncVintedWrapper instance.
 73
 74        Use this instead of direct instantiation to automatically fetch the
 75        session identity. Pass
 76        ``session`` to reuse a prefetched one and skip the network fetch.
 77
 78        Args:
 79            baseurl: Vinted domain URL (e.g., "https://www.vinted.com").
 80            user_agent: Custom user agent string. Auto-generated if None.
 81            config: httpx client configuration dict.
 82            cookie_names: List of cookie names to extract. Defaults to ["access_token_web"].
 83            session: A prefetched ``VintedSession``. Auto-fetched if None/empty.
 84            locale: Optional ``Locale`` override. When None, the locale is read
 85                from the landing page, falling back to a guess from ``baseurl``.
 86
 87        Returns:
 88            Initialized AsyncVintedWrapper instance with a resolved session.
 89        """
 90        _log.debug("Creating the async wrapper using the factory method")
 91        self = cls(
 92            baseurl,
 93            session=session,
 94            user_agent=user_agent,
 95            config=config,
 96            cookie_names=cookie_names,
 97            locale=locale,
 98        )
 99        if self._needs_session():
100            await self.refresh_session()
101        return self

Factory method to create an AsyncVintedWrapper instance.

Use this instead of direct instantiation to automatically fetch the session identity. Pass session to reuse a prefetched one and skip the network fetch.

Args: baseurl: Vinted domain URL (e.g., "https://www.vinted.com"). user_agent: Custom user agent string. Auto-generated if None. config: httpx client configuration dict. cookie_names: List of cookie names to extract. Defaults to ["access_token_web"]. session: A prefetched VintedSession. Auto-fetched if None/empty. locale: Optional Locale override. When None, the locale is read from the landing page, falling back to a guess from baseurl.

Returns: Initialized AsyncVintedWrapper instance with a resolved session.

async def refresh_session(self, retries: int = 3) -> VintedSession:
120    async def refresh_session(self, retries: int = DEFAULT_RETRIES) -> VintedSession:
121        """Manually refresh the session cookie and API tokens.
122
123        Args:
124            retries: Number of retry attempts (default: 3).
125
126        Returns:
127            The ``VintedSession`` returned by :meth:`fetch_session`.
128
129        Raises:
130            RuntimeError: If cookies cannot be fetched after all retries.
131        """
132        log_refresh_cookie(_log)
133        session = await AsyncVintedWrapper.fetch_session(
134            self._client,
135            self._get_cookie_headers(),
136            self.cookie_names,
137            retries,
138        )
139        self.session = session
140        return session

Manually refresh the session cookie and API tokens.

Args: retries: Number of retry attempts (default: 3).

Returns: The VintedSession returned by fetch_session().

Raises: RuntimeError: If cookies cannot be fetched after all retries.

@staticmethod
async def fetch_session( client: httpx.AsyncClient, headers: Dict, cookie_names: List[str], retries: int = 3) -> VintedSession:
142    @staticmethod
143    async def fetch_session(
144        client: httpx.AsyncClient,
145        headers: Dict,
146        cookie_names: List[str],
147        retries: int = DEFAULT_RETRIES,
148    ) -> VintedSession:
149        """Fetch the full session identity from Vinted's landing page (async).
150
151        Args:
152            client: httpx.AsyncClient instance.
153            headers: HTTP headers dictionary.
154            cookie_names: List of cookie names to extract.
155            retries: Number of retry attempts (default: 3).
156
157        Returns:
158            A ``VintedSession`` with the cookies, CSRF token and anonymous id.
159
160        Raises:
161            RuntimeError: If cookies cannot be fetched after all retries.
162        """
163        response = None
164
165        for i in range(retries):
166            log_interaction(_log, i, retries)
167            html, response = await AsyncVintedWrapper._stream_until(
168                client, "/", headers, CSRF_MARKER
169            )
170
171            if response.status_code == HTTP_OK:
172                session = build_session(response, html, cookie_names)
173                if session.is_usable():
174                    return session
175
176            sleep_time = handle_session_failure(response, i, retries)
177            if i < retries - 1:
178                await asyncio.sleep(sleep_time)
179
180        raise_session_error(client.base_url, response)

Fetch the full session identity from Vinted's landing page (async).

Args: client: httpx.AsyncClient instance. headers: HTTP headers dictionary. cookie_names: List of cookie names to extract. retries: Number of retry attempts (default: 3).

Returns: A VintedSession with the cookies, CSRF token and anonymous id.

Raises: RuntimeError: If cookies cannot be fetched after all retries.

async def search(self, params: Dict | None = None) -> Dict[str, Any]:
212    async def search(self, params: Optional[Dict] = None) -> Dict[str, Any]:
213        """Search for items on Vinted asynchronously.
214
215        Args:
216            params: Query parameters. Common parameters:
217                - search_text: Search query
218                - page: Page number
219                - per_page: Items per page
220                - price_from: Minimum price
221                - price_to: Maximum price
222                - order: Sort order
223                - attribute_ids[catalog]: Category IDs (comma-separated;
224                - attribute_ids[status]: Condition IDs (comma-separated;
225                - attribute_ids[brand]: Brand IDs (comma-separated;
226                - attribute_ids[size]: Size IDs (comma-separated;
227
228        Returns:
229            Dictionary containing JSON response with search results.
230        """
231        log_search(_log, params)
232        return await self.curl(
233            self._search_endpoint(), params=params, api_endpoint=True
234        )

Search for items on Vinted asynchronously.

Args: params: Query parameters. Common parameters: - search_text: Search query - page: Page number - per_page: Items per page - price_from: Minimum price - price_to: Maximum price - order: Sort order - attribute_ids[catalog]: Category IDs (comma-separated; - attribute_ids[status]: Condition IDs (comma-separated; - attribute_ids[brand]: Brand IDs (comma-separated; - attribute_ids[size]: Size IDs (comma-separated;

Returns: Dictionary containing JSON response with search results.

async def item(self, item_id: str, fields: List[str] | None = None) -> Dict[str, Any]:
236    async def item(
237        self, item_id: str, fields: Optional[List[str]] = None
238    ) -> Dict[str, Any]:
239        """Read item metadata from the public item page (HTML), asynchronously.
240
241        The JSON item endpoint (``/api/v2/items/{id}/details``) is blocked by the
242        anti-bot protection and returns ``403`` (see
243        https://github.com/Giglium/vinted_scraper/issues/59), so the item data is
244        read from the public item page instead. Uses HTTP streaming to download
245        only the ``<head>`` section, extracting OpenGraph meta tags without
246        fetching the full page body.
247
248        Args:
249            item_id: The unique identifier of the item.
250            fields: List of ``OgField`` values to extract. Defaults to all
251                fields (``[OgField.TITLE, OgField.DESCRIPTION, OgField.URL,
252                OgField.IMAGE]``).
253
254        Returns:
255            A dict always containing ``id``, plus keys ``title``,
256            ``description``, ``url``, and ``image`` (each present only if
257            found and requested).
258
259        Raises:
260            RuntimeError: If the item page cannot be fetched (non-200 status).
261        """
262        log_item(_log, item_id, fields)
263        endpoint = self._item_endpoint(item_id)
264        headers = self._build_page_headers()
265
266        head_html, response = await AsyncVintedWrapper._stream_until(
267            self._client, endpoint, headers, HEAD_END_TAG
268        )
269        status_code = response.status_code
270
271        log_curl_response(_log, endpoint, status_code, response.headers, head_html)
272
273        if status_code == HTTP_OK:
274            return parse_item_page(item_id, head_html, fields)
275
276        self._raise_curl_error(endpoint, status_code)

Read item metadata from the public item page (HTML), asynchronously.

The JSON item endpoint (/api/v2/items/{id}/details) is blocked by the anti-bot protection and returns 403 (see https://github.com/Giglium/vinted_scraper/issues/59), so the item data is read from the public item page instead. Uses HTTP streaming to download only the <head> section, extracting OpenGraph meta tags without fetching the full page body.

Args: item_id: The unique identifier of the item. fields: List of OgField values to extract. Defaults to all fields ([OgField.TITLE, OgField.DESCRIPTION, OgField.URL, OgField.IMAGE]).

Returns: A dict always containing id, plus keys title, description, url, and image (each present only if found and requested).

Raises: RuntimeError: If the item page cannot be fetched (non-200 status).

async def curl( self, endpoint: str, params: Dict | None = None, *, api_endpoint: bool = False, _retries: int = 0) -> Dict[str, Any]:
278    async def curl(
279        self,
280        endpoint: str,
281        params: Optional[Dict] = None,
282        *,
283        api_endpoint: bool = False,
284        _retries: int = 0,
285    ) -> Dict[str, Any]:
286        """Send an async HTTP GET request to a relative Vinted API endpoint.
287
288        The ``endpoint`` is always relative and is concatenated with the chosen
289        client's ``base_url``: the ``api.`` host when ``api_endpoint`` is True,
290        otherwise the site (``www.``) host. Automatically handles headers,
291        cookies, retries, and error responses.
292
293        Args:
294            endpoint: Relative API endpoint path (e.g., "/api/v2/users/name").
295            params: Optional query parameters.
296            api_endpoint: Route the request to the ``api.`` host instead of the
297                site host.
298
299        Returns:
300            Dictionary containing the parsed JSON response.
301
302        Raises:
303            RuntimeError: If response status is not 200 or JSON parsing fails.
304        """
305        client = self._api_client if api_endpoint else self._client
306        headers = self._build_curl_headers()
307        log_curl_request(_log, str(client.base_url), endpoint, headers, params)
308
309        response = await client.get(endpoint, headers=headers, params=params)
310
311        log_curl_response(
312            _log, endpoint, response.status_code, response.headers, response.text
313        )
314
315        if response.status_code == HTTP_OK:
316            return self._handle_curl_response(response, endpoint)
317
318        if response.status_code == HTTP_UNAUTHORIZED and _retries < DEFAULT_RETRIES:
319            log_cookie_retry(_log, response.status_code)
320            await self.refresh_session()
321            return await self.curl(
322                endpoint, params, api_endpoint=api_endpoint, _retries=_retries + 1
323            )
324
325        self._raise_curl_error(endpoint, response.status_code)

Send an async HTTP GET request to a relative Vinted API endpoint.

The endpoint is always relative and is concatenated with the chosen client's base_url: the api. host when api_endpoint is True, otherwise the site (www.) host. Automatically handles headers, cookies, retries, and error responses.

Args: endpoint: Relative API endpoint path (e.g., "/api/v2/users/name"). params: Optional query parameters. api_endpoint: Route the request to the api. host instead of the site host.

Returns: Dictionary containing the parsed JSON response.

Raises: RuntimeError: If response status is not 200 or JSON parsing fails.

@dataclass
class VintedWrapper(vinted_scraper._base_wrapper.BaseVintedWrapper):
 39@dataclass
 40class VintedWrapper(BaseVintedWrapper):
 41    """Synchronous Vinted API wrapper returning raw JSON responses.
 42
 43    Handles cookie management, retries, and HTTP requests automatically.
 44    Returns raw JSON dictionaries instead of typed objects.
 45
 46    Attributes:
 47        baseurl: Vinted domain URL (e.g., "https://www.vinted.com").
 48        session: API identity (VintedSession). Auto-fetched if None or empty.
 49        user_agent: Custom user agent string. Auto-generated if None.
 50        config: httpx client configuration dict.
 51        cookie_names: List of cookie names to extract. Defaults to ["access_token_web"].
 52        locale: Optional ``Locale`` override (e.g. "en-US"). Read from the
 53            landing page when None, falling back to a guess from ``baseurl``.
 54
 55    Example:
 56        See https://github.com/Giglium/vinted_scraper/blob/main/examples/wrapper.py
 57    """
 58
 59    _client: httpx.Client = field(init=False, repr=False)
 60    _api_client: httpx.Client = field(init=False, repr=False)
 61
 62    def __post_init__(self) -> None:
 63        """Initialize VintedWrapper after dataclass initialization.
 64
 65        Validates the base URL, sets up user agent, initializes the two httpx
 66        clients (site host and ``api.`` host), and fetches the session identity
 67        (cookie + tokens) when none of its parts were provided.
 68
 69        Raises:
 70            RuntimeError: If the base URL is invalid.
 71        """
 72        site_config, api_config = self._validate_and_init()
 73        self._client = httpx.Client(**site_config)
 74        self._api_client = httpx.Client(**api_config)
 75        if self._needs_session():
 76            self.refresh_session()
 77
 78    def refresh_session(self, retries: int = DEFAULT_RETRIES) -> VintedSession:
 79        """Manually refresh the session cookie and API tokens.
 80
 81        Args:
 82            retries: Number of retry attempts (default: 3).
 83
 84        Returns:
 85            The ``VintedSession`` returned by :meth:`fetch_session`.
 86
 87        Raises:
 88            RuntimeError: If cookies cannot be fetched after all retries.
 89        """
 90        log_refresh_cookie(_log)
 91        session = VintedWrapper.fetch_session(
 92            self._client,
 93            self._get_cookie_headers(),
 94            self.cookie_names,
 95            retries,
 96        )
 97        self.session = session
 98        return session
 99
100    @staticmethod
101    def fetch_session(
102        client: httpx.Client,
103        headers: Dict,
104        cookie_names: List[str],
105        retries: int = DEFAULT_RETRIES,
106    ) -> VintedSession:
107        """Fetch the full session identity from Vinted's landing page.
108
109        Args:
110            client: httpx.Client instance.
111            headers: HTTP headers dictionary.
112            cookie_names: List of cookie names to extract.
113            retries: Number of retry attempts (default: 3).
114
115        Returns:
116            A ``VintedSession`` with the cookies, CSRF token and anonymous id.
117
118        Raises:
119            RuntimeError: If cookies cannot be fetched after all retries.
120        """
121        response = None
122
123        for i in range(retries):
124            log_interaction(_log, i, retries)
125            html, response = VintedWrapper._stream_until(
126                client, "/", headers, CSRF_MARKER
127            )
128
129            if response.status_code == HTTP_OK:
130                session = build_session(response, html, cookie_names)
131                if session.is_usable():
132                    return session
133
134            sleep_time = handle_session_failure(response, i, retries)
135            if i < retries - 1:
136                time.sleep(sleep_time)
137
138        raise_session_error(client.base_url, response)
139
140    @staticmethod
141    def _stream_until(
142        client: httpx.Client, url: str, headers: Dict, stop_marker: str
143    ) -> Tuple[str, httpx.Response]:
144        """Stream ``url``, reading only up to ``stop_marker``.
145
146        Shared by :meth:`item` (stops at ``</head>``) and the session fetch
147        (stops at the CSRF marker) so that neither downloads the full page body.
148        The chunk-boundary bookkeeping lives in :class:`ChunkAccumulator`.
149
150        Args:
151            client: httpx.Client instance.
152            url: The URL or endpoint path to request.
153            headers: Request headers for the GET.
154            stop_marker: The text marker that ends the read.
155
156        Returns:
157            Tuple of ``(html, response)``. ``html`` is empty for non-200
158            responses.
159        """
160        acc = ChunkAccumulator(stop_marker)
161        with client.stream("GET", url, headers=headers) as response:
162            if response.status_code == HTTP_OK:
163                for chunk in response.iter_text(chunk_size=STREAM_CHUNK_SIZE):
164                    if acc.add(chunk):
165                        break
166            else:
167                response.read()
168        return acc.text, response
169
170    def search(self, params: Optional[Dict] = None) -> Dict[str, Any]:
171        """Search for items on Vinted.
172
173        Args:
174            params: Query parameters. Common parameters:
175                - search_text (str): Search query
176                - page (int): Page number
177                - per_page (int): Items per page
178                - price_from (float): Minimum price
179                - price_to (float): Maximum price
180                - order (str): Sort order
181                - attribute_ids[catalog] (str): Category IDs (comma-separated;
182                - attribute_ids[status] (str): Condition IDs (comma-separated;
183                - attribute_ids[brand] (str): Brand IDs (comma-separated;
184                - attribute_ids[size] (str): Size IDs (comma-separated;
185
186        Returns:
187            Dictionary containing JSON response with search results.
188        """
189        log_search(_log, params)
190        return self.curl(self._search_endpoint(), params=params, api_endpoint=True)
191
192    def item(self, item_id: str, fields: Optional[List[str]] = None) -> Dict[str, Any]:
193        """Read item metadata from the public item page (HTML).
194
195        The JSON item endpoint (``/api/v2/items/{id}/details``) is blocked by the
196        anti-bot protection and returns ``403`` (see
197        https://github.com/Giglium/vinted_scraper/issues/59), so the item data is
198        read from the public item page instead. Uses HTTP streaming to download
199        only the ``<head>`` section, extracting OpenGraph meta tags without
200        fetching the full page body.
201
202        Args:
203            item_id: The unique identifier of the item.
204            fields: List of ``OgField`` values to extract. Defaults to all
205                fields (``[OgField.TITLE, OgField.DESCRIPTION, OgField.URL,
206                OgField.IMAGE]``).
207
208        Returns:
209            A dict always containing ``id``, plus keys ``title``,
210            ``description``, ``url``, and ``image`` (each present only if
211            found and requested).
212
213        Raises:
214            RuntimeError: If the item page cannot be fetched (non-200 status).
215        """
216        log_item(_log, item_id, fields)
217        endpoint = self._item_endpoint(item_id)
218        headers = self._build_page_headers()
219
220        head_html, response = VintedWrapper._stream_until(
221            self._client, endpoint, headers, HEAD_END_TAG
222        )
223        status_code = response.status_code
224
225        log_curl_response(_log, endpoint, status_code, response.headers, head_html)
226
227        if status_code == HTTP_OK:
228            return parse_item_page(item_id, head_html, fields)
229
230        self._raise_curl_error(endpoint, status_code)
231
232    def curl(
233        self,
234        endpoint: str,
235        params: Optional[Dict] = None,
236        *,
237        api_endpoint: bool = False,
238        _retries: int = 0,
239    ) -> Dict[str, Any]:
240        """Send a custom HTTP GET request to a relative Vinted API endpoint.
241
242        The ``endpoint`` is always relative and is concatenated with the chosen
243        client's ``base_url``: the ``api.`` host when ``api_endpoint`` is True,
244        otherwise the site (``www.``) host. Automatically handles headers,
245        cookies, retries, and error responses.
246
247        Args:
248            endpoint: Relative API endpoint path (e.g., "/api/v2/users/name").
249            params: Optional query parameters.
250            api_endpoint: Route the request to the ``api.`` host instead of the
251                site host.
252
253        Returns:
254            Dictionary containing the parsed JSON response.
255
256        Raises:
257            RuntimeError: If response status is not 200 or JSON parsing fails.
258        """
259        client = self._api_client if api_endpoint else self._client
260        headers = self._build_curl_headers()
261        log_curl_request(_log, str(client.base_url), endpoint, headers, params)
262
263        response = client.get(endpoint, headers=headers, params=params)
264
265        log_curl_response(
266            _log, endpoint, response.status_code, response.headers, response.text
267        )
268
269        if response.status_code == HTTP_OK:
270            return self._handle_curl_response(response, endpoint)
271
272        if response.status_code == HTTP_UNAUTHORIZED and _retries < DEFAULT_RETRIES:
273            log_cookie_retry(_log, response.status_code)
274            self.refresh_session()
275            return self.curl(
276                endpoint, params, api_endpoint=api_endpoint, _retries=_retries + 1
277            )
278
279        self._raise_curl_error(endpoint, response.status_code)
280
281    def __enter__(self) -> "VintedWrapper":
282        """Enter context manager.
283
284        Returns:
285            Self for use in with statement.
286        """
287        return self
288
289    def __exit__(self, exc_type, exc_val, exc_tb) -> None:  # pragma: no cover
290        """Exit context manager and close both HTTP clients.
291
292        Args:
293            exc_type: Exception type (unused).
294            exc_val: Exception value (unused).
295            exc_tb: Exception traceback (unused).
296        """
297        self._client.close()
298        self._api_client.close()
299
300    def __del__(self) -> None:  # pragma: no cover
301        """Best-effort cleanup of the HTTP clients on garbage collection.
302
303        Prefer using the context manager (``with`` statement) for
304        deterministic resource cleanup.
305        """
306        for attr in ("_client", "_api_client"):
307            client = getattr(self, attr, None)
308            if client is not None and not client.is_closed:
309                client.close()

Synchronous Vinted API wrapper returning raw JSON responses.

Handles cookie management, retries, and HTTP requests automatically. Returns raw JSON dictionaries instead of typed objects.

Attributes: baseurl: Vinted domain URL (e.g., "https://www.vinted.com"). session: API identity (VintedSession). Auto-fetched if None or empty. user_agent: Custom user agent string. Auto-generated if None. config: httpx client configuration dict. cookie_names: List of cookie names to extract. Defaults to ["access_token_web"]. locale: Optional Locale override (e.g. "en-US"). Read from the landing page when None, falling back to a guess from baseurl.

Example: See https://github.com/Giglium/vinted_scraper/blob/main/examples/wrapper.py

VintedWrapper( baseurl: str, session: VintedSession | None = None, user_agent: str | None = None, config: Dict | None = None, cookie_names: List[str] | None = None, locale: str | None = None)
def refresh_session(self, retries: int = 3) -> VintedSession:
78    def refresh_session(self, retries: int = DEFAULT_RETRIES) -> VintedSession:
79        """Manually refresh the session cookie and API tokens.
80
81        Args:
82            retries: Number of retry attempts (default: 3).
83
84        Returns:
85            The ``VintedSession`` returned by :meth:`fetch_session`.
86
87        Raises:
88            RuntimeError: If cookies cannot be fetched after all retries.
89        """
90        log_refresh_cookie(_log)
91        session = VintedWrapper.fetch_session(
92            self._client,
93            self._get_cookie_headers(),
94            self.cookie_names,
95            retries,
96        )
97        self.session = session
98        return session

Manually refresh the session cookie and API tokens.

Args: retries: Number of retry attempts (default: 3).

Returns: The VintedSession returned by fetch_session().

Raises: RuntimeError: If cookies cannot be fetched after all retries.

@staticmethod
def fetch_session( client: httpx.Client, headers: Dict, cookie_names: List[str], retries: int = 3) -> VintedSession:
100    @staticmethod
101    def fetch_session(
102        client: httpx.Client,
103        headers: Dict,
104        cookie_names: List[str],
105        retries: int = DEFAULT_RETRIES,
106    ) -> VintedSession:
107        """Fetch the full session identity from Vinted's landing page.
108
109        Args:
110            client: httpx.Client instance.
111            headers: HTTP headers dictionary.
112            cookie_names: List of cookie names to extract.
113            retries: Number of retry attempts (default: 3).
114
115        Returns:
116            A ``VintedSession`` with the cookies, CSRF token and anonymous id.
117
118        Raises:
119            RuntimeError: If cookies cannot be fetched after all retries.
120        """
121        response = None
122
123        for i in range(retries):
124            log_interaction(_log, i, retries)
125            html, response = VintedWrapper._stream_until(
126                client, "/", headers, CSRF_MARKER
127            )
128
129            if response.status_code == HTTP_OK:
130                session = build_session(response, html, cookie_names)
131                if session.is_usable():
132                    return session
133
134            sleep_time = handle_session_failure(response, i, retries)
135            if i < retries - 1:
136                time.sleep(sleep_time)
137
138        raise_session_error(client.base_url, response)

Fetch the full session identity from Vinted's landing page.

Args: client: httpx.Client instance. headers: HTTP headers dictionary. cookie_names: List of cookie names to extract. retries: Number of retry attempts (default: 3).

Returns: A VintedSession with the cookies, CSRF token and anonymous id.

Raises: RuntimeError: If cookies cannot be fetched after all retries.

def search(self, params: Dict | None = None) -> Dict[str, Any]:
170    def search(self, params: Optional[Dict] = None) -> Dict[str, Any]:
171        """Search for items on Vinted.
172
173        Args:
174            params: Query parameters. Common parameters:
175                - search_text (str): Search query
176                - page (int): Page number
177                - per_page (int): Items per page
178                - price_from (float): Minimum price
179                - price_to (float): Maximum price
180                - order (str): Sort order
181                - attribute_ids[catalog] (str): Category IDs (comma-separated;
182                - attribute_ids[status] (str): Condition IDs (comma-separated;
183                - attribute_ids[brand] (str): Brand IDs (comma-separated;
184                - attribute_ids[size] (str): Size IDs (comma-separated;
185
186        Returns:
187            Dictionary containing JSON response with search results.
188        """
189        log_search(_log, params)
190        return self.curl(self._search_endpoint(), params=params, api_endpoint=True)

Search for items on Vinted.

Args: params: Query parameters. Common parameters: - search_text (str): Search query - page (int): Page number - per_page (int): Items per page - price_from (float): Minimum price - price_to (float): Maximum price - order (str): Sort order - attribute_ids[catalog] (str): Category IDs (comma-separated; - attribute_ids[status] (str): Condition IDs (comma-separated; - attribute_ids[brand] (str): Brand IDs (comma-separated; - attribute_ids[size] (str): Size IDs (comma-separated;

Returns: Dictionary containing JSON response with search results.

def item(self, item_id: str, fields: List[str] | None = None) -> Dict[str, Any]:
192    def item(self, item_id: str, fields: Optional[List[str]] = None) -> Dict[str, Any]:
193        """Read item metadata from the public item page (HTML).
194
195        The JSON item endpoint (``/api/v2/items/{id}/details``) is blocked by the
196        anti-bot protection and returns ``403`` (see
197        https://github.com/Giglium/vinted_scraper/issues/59), so the item data is
198        read from the public item page instead. Uses HTTP streaming to download
199        only the ``<head>`` section, extracting OpenGraph meta tags without
200        fetching the full page body.
201
202        Args:
203            item_id: The unique identifier of the item.
204            fields: List of ``OgField`` values to extract. Defaults to all
205                fields (``[OgField.TITLE, OgField.DESCRIPTION, OgField.URL,
206                OgField.IMAGE]``).
207
208        Returns:
209            A dict always containing ``id``, plus keys ``title``,
210            ``description``, ``url``, and ``image`` (each present only if
211            found and requested).
212
213        Raises:
214            RuntimeError: If the item page cannot be fetched (non-200 status).
215        """
216        log_item(_log, item_id, fields)
217        endpoint = self._item_endpoint(item_id)
218        headers = self._build_page_headers()
219
220        head_html, response = VintedWrapper._stream_until(
221            self._client, endpoint, headers, HEAD_END_TAG
222        )
223        status_code = response.status_code
224
225        log_curl_response(_log, endpoint, status_code, response.headers, head_html)
226
227        if status_code == HTTP_OK:
228            return parse_item_page(item_id, head_html, fields)
229
230        self._raise_curl_error(endpoint, status_code)

Read item metadata from the public item page (HTML).

The JSON item endpoint (/api/v2/items/{id}/details) is blocked by the anti-bot protection and returns 403 (see https://github.com/Giglium/vinted_scraper/issues/59), so the item data is read from the public item page instead. Uses HTTP streaming to download only the <head> section, extracting OpenGraph meta tags without fetching the full page body.

Args: item_id: The unique identifier of the item. fields: List of OgField values to extract. Defaults to all fields ([OgField.TITLE, OgField.DESCRIPTION, OgField.URL, OgField.IMAGE]).

Returns: A dict always containing id, plus keys title, description, url, and image (each present only if found and requested).

Raises: RuntimeError: If the item page cannot be fetched (non-200 status).

def curl( self, endpoint: str, params: Dict | None = None, *, api_endpoint: bool = False, _retries: int = 0) -> Dict[str, Any]:
232    def curl(
233        self,
234        endpoint: str,
235        params: Optional[Dict] = None,
236        *,
237        api_endpoint: bool = False,
238        _retries: int = 0,
239    ) -> Dict[str, Any]:
240        """Send a custom HTTP GET request to a relative Vinted API endpoint.
241
242        The ``endpoint`` is always relative and is concatenated with the chosen
243        client's ``base_url``: the ``api.`` host when ``api_endpoint`` is True,
244        otherwise the site (``www.``) host. Automatically handles headers,
245        cookies, retries, and error responses.
246
247        Args:
248            endpoint: Relative API endpoint path (e.g., "/api/v2/users/name").
249            params: Optional query parameters.
250            api_endpoint: Route the request to the ``api.`` host instead of the
251                site host.
252
253        Returns:
254            Dictionary containing the parsed JSON response.
255
256        Raises:
257            RuntimeError: If response status is not 200 or JSON parsing fails.
258        """
259        client = self._api_client if api_endpoint else self._client
260        headers = self._build_curl_headers()
261        log_curl_request(_log, str(client.base_url), endpoint, headers, params)
262
263        response = client.get(endpoint, headers=headers, params=params)
264
265        log_curl_response(
266            _log, endpoint, response.status_code, response.headers, response.text
267        )
268
269        if response.status_code == HTTP_OK:
270            return self._handle_curl_response(response, endpoint)
271
272        if response.status_code == HTTP_UNAUTHORIZED and _retries < DEFAULT_RETRIES:
273            log_cookie_retry(_log, response.status_code)
274            self.refresh_session()
275            return self.curl(
276                endpoint, params, api_endpoint=api_endpoint, _retries=_retries + 1
277            )
278
279        self._raise_curl_error(endpoint, response.status_code)

Send a custom HTTP GET request to a relative Vinted API endpoint.

The endpoint is always relative and is concatenated with the chosen client's base_url: the api. host when api_endpoint is True, otherwise the site (www.) host. Automatically handles headers, cookies, retries, and error responses.

Args: endpoint: Relative API endpoint path (e.g., "/api/v2/users/name"). params: Optional query parameters. api_endpoint: Route the request to the api. host instead of the site host.

Returns: Dictionary containing the parsed JSON response.

Raises: RuntimeError: If response status is not 200 or JSON parsing fails.

@dataclass
class AsyncVintedScraper(vinted_scraper.AsyncVintedWrapper):
 14@dataclass
 15class AsyncVintedScraper(AsyncVintedWrapper):
 16    """Asynchronous Vinted scraper with typed model support.
 17
 18    Returns structured VintedItem objects instead of raw JSON dictionaries.
 19    Inherits all functionality from AsyncVintedWrapper.
 20
 21    Example:
 22        See https://github.com/Giglium/vinted_scraper/blob/main/examples/async_scraper.py
 23    """
 24
 25    async def search(self, params: Optional[Dict] = None) -> List[VintedItem]:
 26        """Search for items on Vinted asynchronously.
 27
 28        Args:
 29            params: Query parameters for the search. Common parameters:
 30                - search_text: Search query
 31                - page: Page number
 32                - per_page: Items per page
 33                - price_from: Minimum price
 34                - price_to: Maximum price
 35                - order: Sort order
 36                - attribute_ids[catalog]: Category IDs (comma-separated;
 37                - attribute_ids[status]: Condition IDs (comma-separated;
 38                - attribute_ids[brand]: Brand IDs (comma-separated;
 39                - attribute_ids[size]: Size IDs (comma-separated;
 40
 41        Returns:
 42            List of VintedItem objects representing search results.
 43        """
 44        response = await super().search(params)
 45        return [VintedItem(json_data=item) for item in response["items"]]
 46
 47    async def item(
 48        self, item_id: str, fields: Optional[List[str]] = None
 49    ) -> VintedItem:
 50        """Read item metadata from the public item page (HTML), asynchronously.
 51
 52        The JSON item endpoint is blocked by the anti-bot protection and returns
 53        ``403`` (see https://github.com/Giglium/vinted_scraper/issues/59), so the
 54        data is read from the public item page instead. Only the fields exposed
 55        by the page's OpenGraph tags are populated (``title``, ``description``,
 56        ``url``, ``image``).
 57
 58        Args:
 59            item_id: The unique identifier of the item.
 60            fields: List of ``OgField`` values to extract. Defaults to all
 61                fields (``[OgField.TITLE, OgField.DESCRIPTION, OgField.URL,
 62                OgField.IMAGE]``).
 63
 64        Returns:
 65            A VintedItem built from the page metadata. Always contains ``id``.
 66
 67        Raises:
 68            RuntimeError: If the item page cannot be fetched (non-200 status).
 69        """
 70        data = await super().item(item_id, fields)
 71        return VintedItem(json_data=data)
 72
 73    async def enrich(self, item: VintedItem) -> VintedItem:
 74        """Enrich an existing VintedItem with description from the item page.
 75
 76        Fetches the ``og:description`` from the public item page and populates
 77        the item's ``description`` attribute. Useful for enriching items
 78        obtained from search results (which lack a full description).
 79
 80        Args:
 81            item: A VintedItem to enrich (must have a valid ``id``).
 82
 83        Returns:
 84            The same VintedItem instance with ``description`` populated.
 85
 86        Raises:
 87            RuntimeError: If the item page cannot be fetched (non-200 status).
 88        """
 89        data = await super().item(str(item.id), [OgField.DESCRIPTION])
 90        if OgField.DESCRIPTION in data:
 91            item.description = data[OgField.DESCRIPTION]
 92        return item
 93
 94    async def curl(
 95        self,
 96        endpoint: str,
 97        params: Optional[Dict] = None,
 98        *,
 99        api_endpoint: bool = False,
100        _retries: int = 0,
101    ) -> VintedJsonModel:
102        """Send an async HTTP GET request to a relative Vinted API endpoint.
103
104        Args:
105            endpoint: The relative API endpoint path (e.g., "/api/v2/users/x").
106            params: Optional query parameters.
107            api_endpoint: Route the request to the ``api.`` host instead of the
108                site host.
109
110        Returns:
111            VintedJsonModel containing the JSON response.
112
113        Raises:
114            RuntimeError: If the request fails or returns a non-200 status code.
115        """
116        response = await super().curl(
117            endpoint, params, api_endpoint=api_endpoint, _retries=_retries
118        )
119        return VintedJsonModel(json_data=response)

Asynchronous Vinted scraper with typed model support.

Returns structured VintedItem objects instead of raw JSON dictionaries. Inherits all functionality from AsyncVintedWrapper.

Example: See https://github.com/Giglium/vinted_scraper/blob/main/examples/async_scraper.py

AsyncVintedScraper( baseurl: str, session: VintedSession | None = None, user_agent: str | None = None, config: Dict | None = None, cookie_names: List[str] | None = None, locale: str | None = None)
async def search( self, params: Dict | None = None) -> List[vinted_scraper.models._item.VintedItem]:
25    async def search(self, params: Optional[Dict] = None) -> List[VintedItem]:
26        """Search for items on Vinted asynchronously.
27
28        Args:
29            params: Query parameters for the search. Common parameters:
30                - search_text: Search query
31                - page: Page number
32                - per_page: Items per page
33                - price_from: Minimum price
34                - price_to: Maximum price
35                - order: Sort order
36                - attribute_ids[catalog]: Category IDs (comma-separated;
37                - attribute_ids[status]: Condition IDs (comma-separated;
38                - attribute_ids[brand]: Brand IDs (comma-separated;
39                - attribute_ids[size]: Size IDs (comma-separated;
40
41        Returns:
42            List of VintedItem objects representing search results.
43        """
44        response = await super().search(params)
45        return [VintedItem(json_data=item) for item in response["items"]]

Search for items on Vinted asynchronously.

Args: params: Query parameters for the search. Common parameters: - search_text: Search query - page: Page number - per_page: Items per page - price_from: Minimum price - price_to: Maximum price - order: Sort order - attribute_ids[catalog]: Category IDs (comma-separated; - attribute_ids[status]: Condition IDs (comma-separated; - attribute_ids[brand]: Brand IDs (comma-separated; - attribute_ids[size]: Size IDs (comma-separated;

Returns: List of VintedItem objects representing search results.

async def item( self, item_id: str, fields: List[str] | None = None) -> vinted_scraper.models._item.VintedItem:
47    async def item(
48        self, item_id: str, fields: Optional[List[str]] = None
49    ) -> VintedItem:
50        """Read item metadata from the public item page (HTML), asynchronously.
51
52        The JSON item endpoint is blocked by the anti-bot protection and returns
53        ``403`` (see https://github.com/Giglium/vinted_scraper/issues/59), so the
54        data is read from the public item page instead. Only the fields exposed
55        by the page's OpenGraph tags are populated (``title``, ``description``,
56        ``url``, ``image``).
57
58        Args:
59            item_id: The unique identifier of the item.
60            fields: List of ``OgField`` values to extract. Defaults to all
61                fields (``[OgField.TITLE, OgField.DESCRIPTION, OgField.URL,
62                OgField.IMAGE]``).
63
64        Returns:
65            A VintedItem built from the page metadata. Always contains ``id``.
66
67        Raises:
68            RuntimeError: If the item page cannot be fetched (non-200 status).
69        """
70        data = await super().item(item_id, fields)
71        return VintedItem(json_data=data)

Read item metadata from the public item page (HTML), asynchronously.

The JSON item endpoint is blocked by the anti-bot protection and returns 403 (see https://github.com/Giglium/vinted_scraper/issues/59), so the data is read from the public item page instead. Only the fields exposed by the page's OpenGraph tags are populated (title, description, url, image).

Args: item_id: The unique identifier of the item. fields: List of OgField values to extract. Defaults to all fields ([OgField.TITLE, OgField.DESCRIPTION, OgField.URL, OgField.IMAGE]).

Returns: A VintedItem built from the page metadata. Always contains id.

Raises: RuntimeError: If the item page cannot be fetched (non-200 status).

async def enrich( self, item: vinted_scraper.models._item.VintedItem) -> vinted_scraper.models._item.VintedItem:
73    async def enrich(self, item: VintedItem) -> VintedItem:
74        """Enrich an existing VintedItem with description from the item page.
75
76        Fetches the ``og:description`` from the public item page and populates
77        the item's ``description`` attribute. Useful for enriching items
78        obtained from search results (which lack a full description).
79
80        Args:
81            item: A VintedItem to enrich (must have a valid ``id``).
82
83        Returns:
84            The same VintedItem instance with ``description`` populated.
85
86        Raises:
87            RuntimeError: If the item page cannot be fetched (non-200 status).
88        """
89        data = await super().item(str(item.id), [OgField.DESCRIPTION])
90        if OgField.DESCRIPTION in data:
91            item.description = data[OgField.DESCRIPTION]
92        return item

Enrich an existing VintedItem with description from the item page.

Fetches the og:description from the public item page and populates the item's description attribute. Useful for enriching items obtained from search results (which lack a full description).

Args: item: A VintedItem to enrich (must have a valid id).

Returns: The same VintedItem instance with description populated.

Raises: RuntimeError: If the item page cannot be fetched (non-200 status).

async def curl( self, endpoint: str, params: Dict | None = None, *, api_endpoint: bool = False, _retries: int = 0) -> vinted_scraper.models._json_model.VintedJsonModel:
 94    async def curl(
 95        self,
 96        endpoint: str,
 97        params: Optional[Dict] = None,
 98        *,
 99        api_endpoint: bool = False,
100        _retries: int = 0,
101    ) -> VintedJsonModel:
102        """Send an async HTTP GET request to a relative Vinted API endpoint.
103
104        Args:
105            endpoint: The relative API endpoint path (e.g., "/api/v2/users/x").
106            params: Optional query parameters.
107            api_endpoint: Route the request to the ``api.`` host instead of the
108                site host.
109
110        Returns:
111            VintedJsonModel containing the JSON response.
112
113        Raises:
114            RuntimeError: If the request fails or returns a non-200 status code.
115        """
116        response = await super().curl(
117            endpoint, params, api_endpoint=api_endpoint, _retries=_retries
118        )
119        return VintedJsonModel(json_data=response)

Send an async HTTP GET request to a relative Vinted API endpoint.

Args: endpoint: The relative API endpoint path (e.g., "/api/v2/users/x"). params: Optional query parameters. api_endpoint: Route the request to the api. host instead of the site host.

Returns: VintedJsonModel containing the JSON response.

Raises: RuntimeError: If the request fails or returns a non-200 status code.

@dataclass
class VintedScraper(vinted_scraper.VintedWrapper):
 14@dataclass
 15class VintedScraper(VintedWrapper):
 16    """Synchronous Vinted scraper with typed model support.
 17
 18    Returns structured VintedItem objects instead of raw JSON dictionaries.
 19    Inherits all functionality from VintedWrapper.
 20
 21    Example:
 22       See https://github.com/Giglium/vinted_scraper/blob/main/examples/scraper.py
 23    """
 24
 25    def search(self, params: Optional[Dict] = None) -> List[VintedItem]:  # type: ignore
 26        """Search for items on Vinted.
 27
 28        Args:
 29            params: Query parameters for the search. Common parameters:
 30                - search_text: Search query
 31                - page: Page number
 32                - per_page: Items per page
 33                - price_from: Minimum price
 34                - price_to: Maximum price
 35                - order: Sort order
 36                - attribute_ids[catalog]: Category IDs (comma-separated;
 37                - attribute_ids[status]: Condition IDs (comma-separated;
 38                - attribute_ids[brand]: Brand IDs (comma-separated;
 39                - attribute_ids[size]: Size IDs (comma-separated;
 40
 41        Returns:
 42            List of VintedItem objects representing search results.
 43        """
 44        return [VintedItem(json_data=item) for item in super().search(params)["items"]]
 45
 46    def item(
 47        self, item_id: str, fields: Optional[List[str]] = None
 48    ) -> VintedItem:  # type: ignore
 49        """Read item metadata from the public item page (HTML).
 50
 51        The JSON item endpoint is blocked by the anti-bot protection and returns
 52        ``403`` (see https://github.com/Giglium/vinted_scraper/issues/59), so the
 53        data is read from the public item page instead. Only the fields exposed
 54        by the page's OpenGraph tags are populated (``title``, ``description``,
 55        ``url``, ``image``).
 56
 57        Args:
 58            item_id: The unique identifier of the item.
 59            fields: List of ``OgField`` values to extract. Defaults to all
 60                fields (``[OgField.TITLE, OgField.DESCRIPTION, OgField.URL,
 61                OgField.IMAGE]``).
 62
 63        Returns:
 64            A VintedItem built from the page metadata. Always contains ``id``.
 65
 66        Raises:
 67            RuntimeError: If the item page cannot be fetched (non-200 status).
 68        """
 69        data = super().item(item_id, fields)
 70        return VintedItem(json_data=data)
 71
 72    def enrich(self, item: VintedItem) -> VintedItem:
 73        """Enrich an existing VintedItem with description from the item page.
 74
 75        Fetches the ``og:description`` from the public item page and populates
 76        the item's ``description`` attribute. Useful for enriching items
 77        obtained from search results (which lack a full description).
 78
 79        Args:
 80            item: A VintedItem to enrich (must have a valid ``id``).
 81
 82        Returns:
 83            The same VintedItem instance with ``description`` populated.
 84
 85        Raises:
 86            RuntimeError: If the item page cannot be fetched (non-200 status).
 87        """
 88        data = super().item(str(item.id), [OgField.DESCRIPTION])
 89        if OgField.DESCRIPTION in data:
 90            item.description = data[OgField.DESCRIPTION]
 91        return item
 92
 93    def curl(
 94        self,
 95        endpoint: str,
 96        params: Optional[Dict] = None,
 97        *,
 98        api_endpoint: bool = False,
 99        _retries: int = 0,
100    ) -> VintedJsonModel:  # type: ignore
101        """Send a custom HTTP GET request to a relative Vinted API endpoint.
102
103        Args:
104            endpoint: The relative API endpoint path (e.g., "/api/v2/users/x").
105            params: Optional query parameters.
106            api_endpoint: Route the request to the ``api.`` host instead of the
107                site host.
108
109        Returns:
110            VintedJsonModel containing the JSON response.
111
112        Raises:
113            RuntimeError: If the request fails or returns a non-200 status code.
114        """
115        response = super().curl(
116            endpoint, params, api_endpoint=api_endpoint, _retries=_retries
117        )
118        return VintedJsonModel(json_data=response)

Synchronous Vinted scraper with typed model support.

Returns structured VintedItem objects instead of raw JSON dictionaries. Inherits all functionality from VintedWrapper.

Example: See https://github.com/Giglium/vinted_scraper/blob/main/examples/scraper.py

VintedScraper( baseurl: str, session: VintedSession | None = None, user_agent: str | None = None, config: Dict | None = None, cookie_names: List[str] | None = None, locale: str | None = None)
def search( self, params: Dict | None = None) -> List[vinted_scraper.models._item.VintedItem]:
25    def search(self, params: Optional[Dict] = None) -> List[VintedItem]:  # type: ignore
26        """Search for items on Vinted.
27
28        Args:
29            params: Query parameters for the search. Common parameters:
30                - search_text: Search query
31                - page: Page number
32                - per_page: Items per page
33                - price_from: Minimum price
34                - price_to: Maximum price
35                - order: Sort order
36                - attribute_ids[catalog]: Category IDs (comma-separated;
37                - attribute_ids[status]: Condition IDs (comma-separated;
38                - attribute_ids[brand]: Brand IDs (comma-separated;
39                - attribute_ids[size]: Size IDs (comma-separated;
40
41        Returns:
42            List of VintedItem objects representing search results.
43        """
44        return [VintedItem(json_data=item) for item in super().search(params)["items"]]

Search for items on Vinted.

Args: params: Query parameters for the search. Common parameters: - search_text: Search query - page: Page number - per_page: Items per page - price_from: Minimum price - price_to: Maximum price - order: Sort order - attribute_ids[catalog]: Category IDs (comma-separated; - attribute_ids[status]: Condition IDs (comma-separated; - attribute_ids[brand]: Brand IDs (comma-separated; - attribute_ids[size]: Size IDs (comma-separated;

Returns: List of VintedItem objects representing search results.

def item( self, item_id: str, fields: List[str] | None = None) -> vinted_scraper.models._item.VintedItem:
46    def item(
47        self, item_id: str, fields: Optional[List[str]] = None
48    ) -> VintedItem:  # type: ignore
49        """Read item metadata from the public item page (HTML).
50
51        The JSON item endpoint is blocked by the anti-bot protection and returns
52        ``403`` (see https://github.com/Giglium/vinted_scraper/issues/59), so the
53        data is read from the public item page instead. Only the fields exposed
54        by the page's OpenGraph tags are populated (``title``, ``description``,
55        ``url``, ``image``).
56
57        Args:
58            item_id: The unique identifier of the item.
59            fields: List of ``OgField`` values to extract. Defaults to all
60                fields (``[OgField.TITLE, OgField.DESCRIPTION, OgField.URL,
61                OgField.IMAGE]``).
62
63        Returns:
64            A VintedItem built from the page metadata. Always contains ``id``.
65
66        Raises:
67            RuntimeError: If the item page cannot be fetched (non-200 status).
68        """
69        data = super().item(item_id, fields)
70        return VintedItem(json_data=data)

Read item metadata from the public item page (HTML).

The JSON item endpoint is blocked by the anti-bot protection and returns 403 (see https://github.com/Giglium/vinted_scraper/issues/59), so the data is read from the public item page instead. Only the fields exposed by the page's OpenGraph tags are populated (title, description, url, image).

Args: item_id: The unique identifier of the item. fields: List of OgField values to extract. Defaults to all fields ([OgField.TITLE, OgField.DESCRIPTION, OgField.URL, OgField.IMAGE]).

Returns: A VintedItem built from the page metadata. Always contains id.

Raises: RuntimeError: If the item page cannot be fetched (non-200 status).

def enrich( self, item: vinted_scraper.models._item.VintedItem) -> vinted_scraper.models._item.VintedItem:
72    def enrich(self, item: VintedItem) -> VintedItem:
73        """Enrich an existing VintedItem with description from the item page.
74
75        Fetches the ``og:description`` from the public item page and populates
76        the item's ``description`` attribute. Useful for enriching items
77        obtained from search results (which lack a full description).
78
79        Args:
80            item: A VintedItem to enrich (must have a valid ``id``).
81
82        Returns:
83            The same VintedItem instance with ``description`` populated.
84
85        Raises:
86            RuntimeError: If the item page cannot be fetched (non-200 status).
87        """
88        data = super().item(str(item.id), [OgField.DESCRIPTION])
89        if OgField.DESCRIPTION in data:
90            item.description = data[OgField.DESCRIPTION]
91        return item

Enrich an existing VintedItem with description from the item page.

Fetches the og:description from the public item page and populates the item's description attribute. Useful for enriching items obtained from search results (which lack a full description).

Args: item: A VintedItem to enrich (must have a valid id).

Returns: The same VintedItem instance with description populated.

Raises: RuntimeError: If the item page cannot be fetched (non-200 status).

def curl( self, endpoint: str, params: Dict | None = None, *, api_endpoint: bool = False, _retries: int = 0) -> vinted_scraper.models._json_model.VintedJsonModel:
 93    def curl(
 94        self,
 95        endpoint: str,
 96        params: Optional[Dict] = None,
 97        *,
 98        api_endpoint: bool = False,
 99        _retries: int = 0,
100    ) -> VintedJsonModel:  # type: ignore
101        """Send a custom HTTP GET request to a relative Vinted API endpoint.
102
103        Args:
104            endpoint: The relative API endpoint path (e.g., "/api/v2/users/x").
105            params: Optional query parameters.
106            api_endpoint: Route the request to the ``api.`` host instead of the
107                site host.
108
109        Returns:
110            VintedJsonModel containing the JSON response.
111
112        Raises:
113            RuntimeError: If the request fails or returns a non-200 status code.
114        """
115        response = super().curl(
116            endpoint, params, api_endpoint=api_endpoint, _retries=_retries
117        )
118        return VintedJsonModel(json_data=response)

Send a custom HTTP GET request to a relative Vinted API endpoint.

Args: endpoint: The relative API endpoint path (e.g., "/api/v2/users/x"). params: Optional query parameters. api_endpoint: Route the request to the api. host instead of the site host.

Returns: VintedJsonModel containing the JSON response.

Raises: RuntimeError: If the request fails or returns a non-200 status code.

class OgField(builtins.str, enum.Enum):
 7class OgField(str, Enum):
 8    """Available OpenGraph fields that can be extracted from an item page.
 9
10    Members:
11        TITLE: The item title (derived from og:description).
12        DESCRIPTION: The full item description (og:description).
13        URL: The canonical item URL (og:url).
14        IMAGE: The item image URL (og:image).
15    """
16
17    TITLE = "title"
18    DESCRIPTION = "description"
19    URL = "url"
20    IMAGE = "image"

Available OpenGraph fields that can be extracted from an item page.

Members: TITLE: The item title (derived from og:description). DESCRIPTION: The full item description (og:description). URL: The canonical item URL (og:url). IMAGE: The item image URL (og:image).

TITLE = <OgField.TITLE: 'title'>
DESCRIPTION = <OgField.DESCRIPTION: 'description'>
URL = <OgField.URL: 'url'>
IMAGE = <OgField.IMAGE: 'image'>
@dataclass
class VintedSession:
 8@dataclass
 9class VintedSession:
10    """The identity needed to authenticate Vinted API calls.
11
12    Bundles the values scraped from (or supplied for) the landing page: the
13    session ``cookies``, the ``anon_id`` returned as a response header, and the
14    ``csrf_token``. They are created, stored, and sent together, so they travel
15    as a single object.
16
17    The ``csrf_token`` is a supported field (it is sent when set and can be
18    supplied by a caller), but it is **not** auto-fetched from the landing page:
19    the API currently accepts calls without it. See
20    ``utils/_session.py::build_session`` for how to re-enable that.
21
22    Attributes:
23        cookies: Session cookies keyed by name.
24        csrf_token: CSRF token for the API, if available. Not auto-fetched.
25        anon_id: Anonymous id for the API, if available.
26        locale: Market locale tag (e.g. ``"cs-CZ"``)
27    """
28
29    cookies: Dict[str, str] = field(default_factory=dict)
30    csrf_token: Optional[str] = None
31    anon_id: Optional[str] = None
32    locale: Optional[str] = None
33
34    def is_empty(self) -> bool:
35        """Return whether no identity part is set.
36
37        Returns:
38            ``True`` when there are no cookies and neither token is set.
39        """
40        return not self.cookies and self.csrf_token is None and self.anon_id is None
41
42    def is_usable(self) -> bool:
43        """Return whether the session carries enough to authenticate a call.
44
45        A session is usable once the landing page has handed over the parts the
46        API actually checks: the session cookies and the anonymous id. The CSRF
47        token is sent when present but is not required to make the call (and is
48        not auto-fetched), so it is not part of this check.
49
50        Returns:
51            ``True`` when both cookies and an anonymous id are set.
52        """
53        return bool(self.cookies) and self.anon_id is not None

The identity needed to authenticate Vinted API calls.

Bundles the values scraped from (or supplied for) the landing page: the session cookies, the anon_id returned as a response header, and the csrf_token. They are created, stored, and sent together, so they travel as a single object.

The csrf_token is a supported field (it is sent when set and can be supplied by a caller), but it is not auto-fetched from the landing page: the API currently accepts calls without it. See utils/_session.py::build_session for how to re-enable that.

Attributes: cookies: Session cookies keyed by name. csrf_token: CSRF token for the API, if available. Not auto-fetched. anon_id: Anonymous id for the API, if available. locale: Market locale tag (e.g. "cs-CZ")

VintedSession( cookies: Dict[str, str] = <factory>, csrf_token: str | None = None, anon_id: str | None = None, locale: str | None = None)
cookies: Dict[str, str]
csrf_token: str | None = None
anon_id: str | None = None
locale: str | None = None
def is_empty(self) -> bool:
34    def is_empty(self) -> bool:
35        """Return whether no identity part is set.
36
37        Returns:
38            ``True`` when there are no cookies and neither token is set.
39        """
40        return not self.cookies and self.csrf_token is None and self.anon_id is None

Return whether no identity part is set.

Returns: True when there are no cookies and neither token is set.

def is_usable(self) -> bool:
42    def is_usable(self) -> bool:
43        """Return whether the session carries enough to authenticate a call.
44
45        A session is usable once the landing page has handed over the parts the
46        API actually checks: the session cookies and the anonymous id. The CSRF
47        token is sent when present but is not required to make the call (and is
48        not auto-fetched), so it is not part of this check.
49
50        Returns:
51            ``True`` when both cookies and an anonymous id are set.
52        """
53        return bool(self.cookies) and self.anon_id is not None

Return whether the session carries enough to authenticate a call.

A session is usable once the landing page has handed over the parts the API actually checks: the session cookies and the anonymous id. The CSRF token is sent when present but is not required to make the call (and is not auto-fetched), so it is not part of this check.

Returns: True when both cookies and an anonymous id are set.