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]
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
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.
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.
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.
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.
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).
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.
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
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.
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.
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.
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).
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.
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
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.
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).
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).
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.
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
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.
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).
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).
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.
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).
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")
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.
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.