vinted-api-kit 1.0.0__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/PKG-INFO +4 -21
  2. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/README.md +3 -20
  3. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/pyproject.toml +4 -1
  4. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/__init__.py +3 -1
  5. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/api/base.py +4 -2
  6. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/api/catalog.py +33 -12
  7. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/api/items.py +23 -24
  8. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/client.py +16 -4
  9. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/constants.py +0 -2
  10. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/exceptions.py +4 -0
  11. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/models/config.py +5 -1
  12. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/models/item.py +29 -7
  13. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/session.py +41 -14
  14. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/utils.py +5 -12
  15. vinted_api_kit-1.0.0/.editorconfig +0 -22
  16. vinted_api_kit-1.0.0/.github/workflows/ci.yaml +0 -35
  17. vinted_api_kit-1.0.0/.github/workflows/coverage.yaml +0 -31
  18. vinted_api_kit-1.0.0/.pre-commit-config.yaml +0 -27
  19. vinted_api_kit-1.0.0/CHANGELOG.md +0 -69
  20. vinted_api_kit-1.0.0/Makefile +0 -91
  21. vinted_api_kit-1.0.0/assets/logo.png +0 -0
  22. vinted_api_kit-1.0.0/examples/base_usage.py +0 -32
  23. vinted_api_kit-1.0.0/examples/raw_data.py +0 -23
  24. vinted_api_kit-1.0.0/examples/with_proxy.py +0 -41
  25. vinted_api_kit-1.0.0/tests/__init__.py +0 -0
  26. vinted_api_kit-1.0.0/tests/api/__init__.py +0 -0
  27. vinted_api_kit-1.0.0/tests/api/test_catalog.py +0 -98
  28. vinted_api_kit-1.0.0/tests/api/test_items.py +0 -64
  29. vinted_api_kit-1.0.0/tests/conftest.py +0 -74
  30. vinted_api_kit-1.0.0/tests/models/__init__.py +0 -0
  31. vinted_api_kit-1.0.0/tests/models/test_catalog_item.py +0 -54
  32. vinted_api_kit-1.0.0/tests/models/test_config.py +0 -47
  33. vinted_api_kit-1.0.0/tests/models/test_detailed_item.py +0 -41
  34. vinted_api_kit-1.0.0/tests/storage/__init__.py +0 -0
  35. vinted_api_kit-1.0.0/tests/storage/test_json.py +0 -95
  36. vinted_api_kit-1.0.0/tests/storage/test_mozilla.py +0 -49
  37. vinted_api_kit-1.0.0/tests/storage/test_pickle.py +0 -59
  38. vinted_api_kit-1.0.0/tests/test_auth.py +0 -62
  39. vinted_api_kit-1.0.0/tests/test_client.py +0 -40
  40. vinted_api_kit-1.0.0/tests/test_session.py +0 -138
  41. vinted_api_kit-1.0.0/tests/test_utils.py +0 -44
  42. vinted_api_kit-1.0.0/tox.ini +0 -11
  43. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/.gitignore +0 -0
  44. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/LICENSE +0 -0
  45. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/api/__init__.py +0 -0
  46. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/auth.py +0 -0
  47. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/models/__init__.py +0 -0
  48. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/storage/__init__.py +0 -0
  49. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/storage/base.py +0 -0
  50. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/storage/json.py +0 -0
  51. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/storage/mozilla.py +0 -0
  52. {vinted_api_kit-1.0.0 → vinted_api_kit-1.1.0}/vinted/storage/pickle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vinted-api-kit
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Lightweight asynchronous Python client library for accessing Vinted API and scraping item data.
5
5
  Project-URL: Homepage, https://github.com/vlymar1/vinted-api-kit
6
6
  Project-URL: Documentation, https://github.com/vlymar1/vinted-api-kit
@@ -50,11 +50,10 @@ Description-Content-Type: text/markdown
50
50
  - 🚀 **Asynchronous** - Built with asyncio for high performance
51
51
  - 🌍 **Auto-locale Detection** - Automatically detects locale from URL
52
52
  - 🔍 **Item Search** - Search catalog with filters, sorting and pagination
53
- - 📦 **Item Details** - Get complete item information with rich metadata
54
53
  - 🍪 **Cookie Persistence** - Automatic session management with multiple storage formats
55
54
  - 🔐 **Proxy Support** - Simple string-based proxy configuration
56
55
  - 📊 **Type Hints** - Full typing support with Literal types for better IDE experience
57
- - 🎯 **Dataclass Models** - Fast and efficient data models (15% performance boost) # TODO: Убрать у будущем
56
+ - 🎯 **Dataclass Models** - Fast and efficient data models (15% performance boost)
58
57
  - 🛡️ **Custom Exceptions** - Detailed error hierarchy for precise error handling
59
58
  - 💾 **Flexible Storage** - Choose between pickle, JSON, or Mozilla cookie formats
60
59
 
@@ -165,22 +164,7 @@ raw_items = await client.search_items(
165
164
 
166
165
  ### Item Details
167
166
 
168
- > Fetch detailed information about a specific item:
169
- ```python
170
- item = await client.item_details(
171
- url="https://www.vinted.com/items/1234567890"
172
- )
173
-
174
- print(f"Title: {item.title}")
175
- print(f"Brand: {item.brand_title}")
176
- print(f"Size: {item.size_title}")
177
- print(f"Price: {item.price} {item.currency}")
178
- print(f"Total: {item.total_item_price}")
179
- print(f"Description: {item.description}")
180
-
181
- # Or get raw JSON
182
- raw_item = await client.item_details(url, raw_data=True)
183
- ```
167
+ > ⚠️ **Deprecated since 1.1.0.** Vinted removed the item details API, so `item_details()` raises `VintedDeprecatedError`.
184
168
 
185
169
  ### Parameters
186
170
 
@@ -352,8 +336,7 @@ vinted-api-kit/
352
336
  │ ├── constants.py # Constants and type definitions
353
337
  │ ├── exceptions.py # Custom exceptions
354
338
  │ └── utils.py # Utility functions
355
- ├── tests/ # Test suite
356
- └── examples/ # Usage examples
339
+ └── tests/ # Test suite
357
340
  ```
358
341
 
359
342
  ### Testing
@@ -18,11 +18,10 @@
18
18
  - 🚀 **Asynchronous** - Built with asyncio for high performance
19
19
  - 🌍 **Auto-locale Detection** - Automatically detects locale from URL
20
20
  - 🔍 **Item Search** - Search catalog with filters, sorting and pagination
21
- - 📦 **Item Details** - Get complete item information with rich metadata
22
21
  - 🍪 **Cookie Persistence** - Automatic session management with multiple storage formats
23
22
  - 🔐 **Proxy Support** - Simple string-based proxy configuration
24
23
  - 📊 **Type Hints** - Full typing support with Literal types for better IDE experience
25
- - 🎯 **Dataclass Models** - Fast and efficient data models (15% performance boost) # TODO: Убрать у будущем
24
+ - 🎯 **Dataclass Models** - Fast and efficient data models (15% performance boost)
26
25
  - 🛡️ **Custom Exceptions** - Detailed error hierarchy for precise error handling
27
26
  - 💾 **Flexible Storage** - Choose between pickle, JSON, or Mozilla cookie formats
28
27
 
@@ -133,22 +132,7 @@ raw_items = await client.search_items(
133
132
 
134
133
  ### Item Details
135
134
 
136
- > Fetch detailed information about a specific item:
137
- ```python
138
- item = await client.item_details(
139
- url="https://www.vinted.com/items/1234567890"
140
- )
141
-
142
- print(f"Title: {item.title}")
143
- print(f"Brand: {item.brand_title}")
144
- print(f"Size: {item.size_title}")
145
- print(f"Price: {item.price} {item.currency}")
146
- print(f"Total: {item.total_item_price}")
147
- print(f"Description: {item.description}")
148
-
149
- # Or get raw JSON
150
- raw_item = await client.item_details(url, raw_data=True)
151
- ```
135
+ > ⚠️ **Deprecated since 1.1.0.** Vinted removed the item details API, so `item_details()` raises `VintedDeprecatedError`.
152
136
 
153
137
  ### Parameters
154
138
 
@@ -320,8 +304,7 @@ vinted-api-kit/
320
304
  │ ├── constants.py # Constants and type definitions
321
305
  │ ├── exceptions.py # Custom exceptions
322
306
  │ └── utils.py # Utility functions
323
- ├── tests/ # Test suite
324
- └── examples/ # Usage examples
307
+ └── tests/ # Test suite
325
308
  ```
326
309
 
327
310
  ### Testing
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vinted-api-kit"
7
- version = "1.0.0"
7
+ version = "1.1.0"
8
8
  description = "Lightweight asynchronous Python client library for accessing Vinted API and scraping item data."
9
9
  authors = [
10
10
  {name = "Lymar Volodymyr",email = "volodymyr.lymar1@gmail.com"},
@@ -71,6 +71,9 @@ all = ["format", "lint", "type-check", "test-cov"]
71
71
  [tool.hatch.build.targets.wheel]
72
72
  packages = ["vinted"]
73
73
 
74
+ [tool.hatch.build.targets.sdist]
75
+ include = ["vinted", "README.md", "LICENSE"]
76
+
74
77
  [tool.ruff]
75
78
  exclude = [
76
79
  ".git",
@@ -4,6 +4,7 @@ from vinted.exceptions import (
4
4
  VintedAPIError,
5
5
  VintedAuthError,
6
6
  VintedConfigError,
7
+ VintedDeprecatedError,
7
8
  VintedError,
8
9
  VintedNetworkError,
9
10
  VintedRateLimitError,
@@ -11,7 +12,7 @@ from vinted.exceptions import (
11
12
  )
12
13
  from vinted.models.item import CatalogItem, DetailedItem
13
14
 
14
- __version__ = "1.0.0"
15
+ __version__ = "1.1.0"
15
16
 
16
17
  __all__ = [
17
18
  "VintedClient",
@@ -24,6 +25,7 @@ __all__ = [
24
25
  "VintedNetworkError",
25
26
  "VintedConfigError",
26
27
  "VintedValidationError",
28
+ "VintedDeprecatedError",
27
29
  "SortOrder",
28
30
  "StorageFormat",
29
31
  ]
@@ -4,6 +4,8 @@ This module provides a small base class that stores a shared
4
4
  `HttpSession` instance used by higher-level API wrappers.
5
5
  """
6
6
 
7
+ from vinted.exceptions import VintedConfigError
8
+
7
9
  from ..session import HttpSession
8
10
 
9
11
 
@@ -22,11 +24,11 @@ class BaseAPI:
22
24
  """Return configured base URL for API requests.
23
25
 
24
26
  Raises:
25
- ValueError: If the session `base_url` is not configured.
27
+ VintedConfigError: If the session `base_url` is not configured.
26
28
 
27
29
  Returns:
28
30
  The base URL (scheme + host) derived from the session.
29
31
  """
30
32
  if not self.session.base_url:
31
- raise ValueError("Session base_url not configured")
33
+ raise VintedConfigError("Session base_url not configured")
32
34
  return self.session.base_url
@@ -8,7 +8,9 @@ and returns either raw JSON items or parsed `CatalogItem` instances.
8
8
  import logging
9
9
  import time
10
10
  from typing import Any, Union
11
- from urllib.parse import parse_qsl, urlparse
11
+ from urllib.parse import parse_qsl, urljoin, urlparse
12
+
13
+ from vinted.exceptions import VintedValidationError
12
14
 
13
15
  from ..constants import SortOrder
14
16
  from ..models import CatalogItem
@@ -46,8 +48,9 @@ class CatalogAPI(BaseAPI):
46
48
  Returns:
47
49
  List of `CatalogItem` instances or raw item dicts.
48
50
  """
51
+ self._validate_catalog_url(url)
49
52
  self.session.configure_from_url(url)
50
- api_url = f"{self.base_url}/api/v2/catalog/items"
53
+ api_url = f"{self.base_url}/web/gateway/svc-catalogue/items"
51
54
 
52
55
  params = self._build_params(url, per_page, page)
53
56
  params["time"] = timestamp or int(time.time())
@@ -66,7 +69,11 @@ class CatalogAPI(BaseAPI):
66
69
  if raw_data:
67
70
  return items
68
71
 
69
- return [CatalogItem(raw_data=item) for item in items]
72
+ catalog_items = [CatalogItem(raw_data=item) for item in items]
73
+ for catalog_item in catalog_items:
74
+ catalog_item.url = urljoin(f"{self.base_url}/", catalog_item.url)
75
+
76
+ return catalog_items
70
77
 
71
78
  def _build_params(self, url: str, per_page: int, page: int) -> dict:
72
79
  """Build API query params from a public catalog URL.
@@ -80,17 +87,19 @@ class CatalogAPI(BaseAPI):
80
87
  catalog_id = self._extract_catalog_id(parsed.path)
81
88
  catalog_ids_query = self._join_values(query_params, "catalog[]")
82
89
 
90
+ status_ids = self._extract_values(query_params, "status_ids[]") + self._extract_values(
91
+ query_params, "status[]"
92
+ )
93
+
83
94
  params = {
84
95
  "search_text": "+".join(self._extract_values(query_params, "search_text")),
85
- "catalog_ids": str(catalog_id) if catalog_id else catalog_ids_query,
86
- "color_ids": self._join_values(query_params, "color_ids[]"),
87
- "brand_ids": self._join_values(query_params, "brand_ids[]"),
88
- "size_ids": self._join_values(query_params, "size_ids[]"),
89
- "material_ids": self._join_values(query_params, "material_ids[]"),
90
- "status_ids": self._join_values(query_params, "status[]"),
91
- "country_ids": self._join_values(query_params, "country_ids[]"),
92
- "city_ids": self._join_values(query_params, "city_ids[]"),
93
- "is_for_swap": ",".join("1" for _ in self._extract_values(query_params, "disposal[]")),
96
+ "attribute_ids[catalog]": str(catalog_id) if catalog_id else catalog_ids_query,
97
+ "attribute_ids[color]": self._join_values(query_params, "color_ids[]"),
98
+ "attribute_ids[brand]": self._join_values(query_params, "brand_ids[]"),
99
+ "attribute_ids[size]": self._join_values(query_params, "size_ids[]"),
100
+ "attribute_ids[material]": self._join_values(query_params, "material_ids[]"),
101
+ "attribute_ids[status]": ",".join(status_ids),
102
+ "attribute_ids[patterns]": self._join_values(query_params, "patterns_ids[]"),
94
103
  "currency": self._join_values(query_params, "currency"),
95
104
  "price_to": self._join_values(query_params, "price_to"),
96
105
  "price_from": self._join_values(query_params, "price_from"),
@@ -101,6 +110,18 @@ class CatalogAPI(BaseAPI):
101
110
 
102
111
  return {k: v for k, v in params.items() if v}
103
112
 
113
+ @staticmethod
114
+ def _validate_catalog_url(url: str) -> None:
115
+ """Validate basic catalog URL shape without restricting the domain."""
116
+ parsed = urlparse(url)
117
+
118
+ if parsed.scheme not in ("http", "https") or not parsed.netloc:
119
+ raise VintedValidationError("Invalid catalog URL: expected an absolute HTTP(S) URL")
120
+
121
+ path = parsed.path.rstrip("/")
122
+ if path and not path.startswith("/catalog"):
123
+ raise VintedValidationError("Invalid catalog URL: expected path '/catalog'")
124
+
104
125
  @staticmethod
105
126
  def _extract_catalog_id(path: str) -> int | None:
106
127
  """Extract catalog id from path if present.
@@ -6,9 +6,11 @@ URL and returns either a parsed `DetailedItem` or raw JSON.
6
6
  """
7
7
 
8
8
  import logging
9
- from typing import Any, Union
9
+ from typing import Union
10
10
  from urllib.parse import urlparse
11
11
 
12
+ from vinted.exceptions import VintedDeprecatedError, VintedValidationError
13
+
12
14
  from ..models import DetailedItem
13
15
  from .base import BaseAPI
14
16
 
@@ -27,33 +29,18 @@ class ItemsAPI(BaseAPI):
27
29
  url: str,
28
30
  raw_data: bool = False,
29
31
  ) -> Union[DetailedItem, dict]:
30
- """Fetch item details.
32
+ """Deprecated: always raises `VintedDeprecatedError`.
33
+
34
+ Vinted removed the item details API endpoint.
31
35
 
32
36
  Args:
33
37
  url: Public Vinted item URL.
34
38
  raw_data: If True, return raw JSON dictionary instead of
35
39
  a `DetailedItem` instance.
36
-
37
- Returns:
38
- `DetailedItem` or raw dict depending on `raw_data`.
39
40
  """
40
- self.session.configure_from_url(url)
41
-
42
- product_id = self._extract_product_id(url)
43
- api_url = f"{self.base_url}/api/v2/items/{product_id}/details"
44
-
45
- logger.debug("Fetching item details: %s", api_url)
46
-
47
- response = await self.session.request(api_url)
48
- data = response.json()
49
- item_data: dict[Any, Any] = data.get("item", {})
50
-
51
- logger.debug("Item details fetched successfully")
52
-
53
- if raw_data:
54
- return item_data
55
-
56
- return DetailedItem(raw_data=item_data)
41
+ raise VintedDeprecatedError(
42
+ "item_details() no longer works: Vinted removed the item details API"
43
+ )
57
44
 
58
45
  @staticmethod
59
46
  def _extract_product_id(url: str) -> str:
@@ -62,5 +49,17 @@ class ItemsAPI(BaseAPI):
62
49
  The public path is expected to be `/items/<id>-...` or similar; the
63
50
  method extracts the numeric id component.
64
51
  """
65
- path = urlparse(url).path
66
- return path.split("/")[2].split("-")[0]
52
+ parsed = urlparse(url)
53
+ path_parts = parsed.path.strip("/").split("/")
54
+
55
+ if parsed.scheme not in ("http", "https") or not parsed.netloc:
56
+ raise VintedValidationError("Invalid item URL: expected an absolute HTTP(S) URL")
57
+
58
+ if len(path_parts) < 2 or path_parts[0] != "items":
59
+ raise VintedValidationError("Invalid item URL: expected path '/items/<id>'")
60
+
61
+ product_id = path_parts[1].split("-")[0]
62
+ if not product_id.isdigit():
63
+ raise VintedValidationError("Invalid item URL: item id must be numeric")
64
+
65
+ return product_id
@@ -12,6 +12,7 @@ from typing import Type, Union
12
12
  from .api.catalog import CatalogAPI
13
13
  from .api.items import ItemsAPI
14
14
  from .constants import SortOrder, StorageFormat
15
+ from .exceptions import VintedConfigError
15
16
  from .models.config import ClientConfig
16
17
  from .models.item import CatalogItem, DetailedItem
17
18
  from .session import HttpSession
@@ -82,6 +83,12 @@ class VintedClient:
82
83
  "mozilla": MozillaStorage,
83
84
  }
84
85
 
86
+ if config.storage_format not in storage_map:
87
+ valid_formats = ", ".join(storage_map)
88
+ raise VintedConfigError(
89
+ f"Invalid storage_format '{config.storage_format}'. Valid options: {valid_formats}"
90
+ )
91
+
85
92
  storage_class = storage_map[config.storage_format]
86
93
  return storage_class(filepath)
87
94
 
@@ -110,6 +117,12 @@ class VintedClient:
110
117
  "mozilla": ".txt",
111
118
  }
112
119
 
120
+ if config.storage_format not in extensions:
121
+ valid_formats = ", ".join(extensions)
122
+ raise VintedConfigError(
123
+ f"Invalid storage_format '{config.storage_format}'. Valid options: {valid_formats}"
124
+ )
125
+
113
126
  filename += extensions[config.storage_format]
114
127
 
115
128
  return config.cookies_dir / filename
@@ -150,14 +163,13 @@ class VintedClient:
150
163
  url: str,
151
164
  raw_data: bool = False,
152
165
  ) -> Union[DetailedItem, dict]:
153
- """Fetch item details by URL.
166
+ """Deprecated: always raises `VintedDeprecatedError`.
167
+
168
+ Vinted removed the item details API endpoint.
154
169
 
155
170
  Args:
156
171
  url: Item page URL or API endpoint.
157
172
  raw_data: When True returns raw dict instead of `DetailedItem`.
158
-
159
- Returns:
160
- `DetailedItem` or raw dict when `raw_data` is True.
161
173
  """
162
174
  return await self._items.get_details(url=url, raw_data=raw_data)
163
175
 
@@ -57,8 +57,6 @@ LOCALE_TO_ACCEPT_LANGUAGE = {
57
57
  "com": "en-US",
58
58
  }
59
59
 
60
- VINTED_BASE_URL_TEMPLATE = "https://www.vinted.{locale}"
61
-
62
60
  SortOrder = Literal["newest_first", "relevance", "price_high_to_low", "price_low_to_high"]
63
61
 
64
62
  StorageFormat = Literal["json", "pickle", "mozilla"]
@@ -59,3 +59,7 @@ class VintedConfigError(VintedError):
59
59
 
60
60
  class VintedValidationError(VintedError):
61
61
  """Raised when input validation fails."""
62
+
63
+
64
+ class VintedDeprecatedError(VintedError):
65
+ """Raised when a deprecated feature no longer works with the Vinted API."""
@@ -8,6 +8,7 @@ from dataclasses import dataclass, field
8
8
  from pathlib import Path
9
9
 
10
10
  from ..constants import StorageFormat
11
+ from ..exceptions import VintedConfigError
11
12
 
12
13
 
13
14
  @dataclass
@@ -35,4 +36,7 @@ class ClientConfig:
35
36
  if isinstance(self.cookies_dir, str):
36
37
  self.cookies_dir = Path(self.cookies_dir)
37
38
 
38
- self.cookies_dir.mkdir(parents=True, exist_ok=True)
39
+ try:
40
+ self.cookies_dir.mkdir(parents=True, exist_ok=True)
41
+ except OSError as e:
42
+ raise VintedConfigError(f"Invalid cookies_dir '{self.cookies_dir}': {e}") from e
@@ -8,6 +8,8 @@ from dataclasses import dataclass, field
8
8
  from datetime import datetime, timezone
9
9
  from typing import Any
10
10
 
11
+ from ..exceptions import VintedDeprecatedError
12
+
11
13
 
12
14
  @dataclass
13
15
  class CatalogItem:
@@ -15,7 +17,12 @@ class CatalogItem:
15
17
 
16
18
  The class stores the original `raw_data` (hidden from the repr)
17
19
  and extracts commonly-used fields like `id`, `title`, `price` and
18
- `photo` for convenience.
20
+ `photo` for convenience. Brand, size and condition come from
21
+ `item_box` in the language of the Vinted domain.
22
+
23
+ `created_at_ts` and `raw_timestamp` are deprecated: the catalog API
24
+ no longer returns the photo timestamp, so they always hold epoch
25
+ and 0. They will be removed in the next major release.
19
26
  """
20
27
 
21
28
  raw_data: dict[str, Any] = field(repr=False)
@@ -23,8 +30,10 @@ class CatalogItem:
23
30
  title: str = field(init=False)
24
31
  brand_title: str = field(init=False)
25
32
  size_title: str = field(init=False)
33
+ condition: str = field(init=False)
26
34
  currency: str = field(init=False)
27
35
  price: float = field(init=False)
36
+ total_item_price: float = field(init=False)
28
37
  photo: str = field(init=False)
29
38
  url: str = field(init=False)
30
39
  created_at_ts: datetime = field(init=False)
@@ -33,12 +42,19 @@ class CatalogItem:
33
42
  def __post_init__(self):
34
43
  self.id = self.raw_data.get("id", 0)
35
44
  self.title = self.raw_data.get("title", "")
36
- self.brand_title = self.raw_data.get("brand_title", "")
37
- self.size_title = self.raw_data.get("size_title", "")
45
+
46
+ item_box = self.raw_data.get("item_box") or {}
47
+ self.brand_title = item_box.get("first_line", "")
48
+ # second_line is "<size> · <condition>", the size part is optional
49
+ *sizes, self.condition = item_box.get("second_line", "").split(" · ")
50
+ self.size_title = sizes[0] if sizes else ""
38
51
 
39
52
  price = self.raw_data.get("price") or {}
40
53
  self.currency = price.get("currency_code", "")
41
- self.price = price.get("amount", 0.0)
54
+ self.price = float(price.get("amount") or 0.0)
55
+
56
+ total_item_price = self.raw_data.get("total_item_price") or {}
57
+ self.total_item_price = float(total_item_price.get("amount") or 0.0)
42
58
 
43
59
  photo = self.raw_data.get("photo") or {}
44
60
  self.photo = photo.get("url", "")
@@ -69,13 +85,19 @@ class CatalogItem:
69
85
  return hash(self.id)
70
86
 
71
87
  def is_new_item(self, minutes: int = 1) -> bool:
72
- """Return True when the item was created within `minutes`.
88
+ """Deprecated: always raises `VintedDeprecatedError`.
89
+
90
+ The check compared `created_at_ts` with the current time, but the
91
+ catalog API no longer returns the photo timestamp. The method will
92
+ be removed in the next major release.
73
93
 
74
94
  Args:
75
95
  minutes: Age threshold in minutes (default: 1).
76
96
  """
77
- delta = datetime.now(timezone.utc) - self.created_at_ts
78
- return delta.total_seconds() < minutes * 60
97
+ raise VintedDeprecatedError(
98
+ "CatalogItem.is_new_item() no longer works: the Vinted catalog API "
99
+ "does not return item timestamps anymore"
100
+ )
79
101
 
80
102
 
81
103
  @dataclass
@@ -12,16 +12,23 @@ from curl_cffi import AsyncSession
12
12
  from curl_cffi.requests import Response
13
13
  from curl_cffi.requests.exceptions import HTTPError as CurlHTTPError
14
14
 
15
- from vinted.exceptions import VintedAPIError, VintedAuthError, VintedNetworkError
15
+ from vinted.exceptions import (
16
+ VintedAPIError,
17
+ VintedAuthError,
18
+ VintedConfigError,
19
+ VintedNetworkError,
20
+ VintedRateLimitError,
21
+ )
16
22
 
17
23
  from .auth import AuthManager
18
24
  from .constants import (
19
25
  DEFAULT_HEADERS,
20
26
  HTTP_STATUS_FORBIDDEN,
27
+ HTTP_STATUS_RATE_LIMIT,
21
28
  HTTP_STATUS_UNAUTHORIZED,
22
29
  )
23
30
  from .storage import CookieStorage
24
- from .utils import format_proxy_for_log, get_accept_language
31
+ from .utils import format_proxy_for_log, get_accept_language, get_locale_code
25
32
 
26
33
  logger = logging.getLogger(__name__)
27
34
 
@@ -77,23 +84,30 @@ class HttpSession:
77
84
  def configure_from_url(self, url: str) -> None:
78
85
  parsed = urlparse(url)
79
86
  self.base_url = f"https://{parsed.netloc}"
87
+ self.locale = None
88
+
89
+ host = parsed.netloc.lower()
90
+ if host.startswith("www."):
91
+ host = host[4:]
80
92
 
81
- domain_parts = parsed.netloc.split(".")
82
- if len(domain_parts) > 1:
83
- self.locale = domain_parts[-1]
93
+ if host.startswith("vinted."):
94
+ self.locale = host[len("vinted.") :]
84
95
  accept_language = get_accept_language(self.locale)
85
- self.session.headers.update({"Accept-Language": accept_language})
96
+ # `locale` selects the marketplace whose items the catalog returns
97
+ self.session.headers.update(
98
+ {"Accept-Language": accept_language, "locale": get_locale_code(self.locale)}
99
+ )
86
100
 
87
101
  self.session.headers.update({"Referer": self.base_url})
88
102
  logger.debug("Configured: base_url=%s, locale=%s", self.base_url, self.locale)
89
103
 
90
104
  async def refresh_cookies(self) -> None:
91
105
  if not self.base_url:
92
- raise VintedAPIError("base_url not configured")
106
+ raise VintedConfigError("base_url not configured")
93
107
 
94
108
  logger.debug("Refreshing session cookies...")
95
109
 
96
- self._clear_cookies()
110
+ self._clear_session_cookies()
97
111
 
98
112
  self.session.headers.update({"Referer": ""})
99
113
 
@@ -108,15 +122,16 @@ class HttpSession:
108
122
  logger.debug("Fresh cookies received: %d cookies", len(self.session.cookies))
109
123
 
110
124
  if self.storage:
111
- self.storage.save(self.session.cookies.jar)
125
+ try:
126
+ self.storage.save(self.session.cookies.jar)
127
+ except Exception as e:
128
+ raise VintedConfigError("Failed to save cookies") from e
112
129
 
113
130
  logger.info("Session cookies refreshed successfully")
114
131
 
115
- def _clear_cookies(self) -> None:
132
+ def _clear_session_cookies(self) -> None:
116
133
  self.session.cookies.clear()
117
- if self.storage:
118
- self.storage.clear()
119
- logger.debug("Cookies cleared")
134
+ logger.debug("Session cookies cleared")
120
135
 
121
136
  def _load_cookies(self) -> bool:
122
137
  if not self.storage:
@@ -137,7 +152,6 @@ class HttpSession:
137
152
  logger.info("Access token expired, refreshing cookies")
138
153
  await self.refresh_cookies()
139
154
  else:
140
- # Если куки не были загружены, то рефрешим
141
155
  logger.debug("No saved cookies, refreshing...")
142
156
  await self.refresh_cookies()
143
157
 
@@ -173,6 +187,19 @@ class HttpSession:
173
187
  )
174
188
  except Exception as e:
175
189
  raise VintedNetworkError("Network error on retry", e)
190
+ if response.status_code == HTTP_STATUS_RATE_LIMIT:
191
+ raise VintedRateLimitError(
192
+ f"HTTP {response.status_code}: {response.reason}",
193
+ status_code=response.status_code,
194
+ response=response,
195
+ )
196
+
197
+ if response.status_code in (HTTP_STATUS_UNAUTHORIZED, HTTP_STATUS_FORBIDDEN):
198
+ raise VintedAuthError(
199
+ f"Authentication failed after retry: HTTP {response.status_code}: {response.reason}",
200
+ status_code=response.status_code,
201
+ response=response,
202
+ )
176
203
 
177
204
  if response.status_code >= 400:
178
205
  raise VintedAPIError(
@@ -4,7 +4,7 @@ Small helpers used across the library for formatting proxy strings and
4
4
  resolving Accept-Language headers.
5
5
  """
6
6
 
7
- from .constants import LOCALE_TO_ACCEPT_LANGUAGE, VALID_LOCALES
7
+ from .constants import LOCALE_TO_ACCEPT_LANGUAGE
8
8
 
9
9
 
10
10
  def format_proxy_for_log(proxy: str | None) -> str:
@@ -28,16 +28,9 @@ def format_proxy_for_log(proxy: str | None) -> str:
28
28
  return proxy
29
29
 
30
30
 
31
- def validate_locale(locale: str) -> None:
32
- """Validate that `locale` is supported.
33
-
34
- Raises `ValueError` when the provided `locale` is not in
35
- `VALID_LOCALES`.
36
- """
37
- if locale not in VALID_LOCALES:
38
- raise ValueError(
39
- "Invalid locale '%s'. Valid options: %s" % (locale, ", ".join(VALID_LOCALES))
40
- )
31
+ def get_locale_code(locale: str) -> str:
32
+ """Return the full locale code for `locale` (e.g. "sk" -> "sk-SK")."""
33
+ return LOCALE_TO_ACCEPT_LANGUAGE.get(locale, "en-US")
41
34
 
42
35
 
43
36
  def get_accept_language(locale: str) -> str:
@@ -46,6 +39,6 @@ def get_accept_language(locale: str) -> str:
46
39
  The returned value contains the full locale code and a fallback
47
40
  language prefix with a lower q-value.
48
41
  """
49
- locale_code = LOCALE_TO_ACCEPT_LANGUAGE.get(locale, "en-US")
42
+ locale_code = get_locale_code(locale)
50
43
  lang_prefix = locale_code.split("-")[0]
51
44
  return "%s,%s;q=0.9" % (locale_code, lang_prefix)
@@ -1,22 +0,0 @@
1
- root = true
2
-
3
- [**]
4
- charset = utf-8
5
- end_of_line = lf
6
- indent_size = 4
7
- indent_style = space
8
- insert_final_newline = true
9
- trim_trailing_whitespace = true
10
- max_line_length = 99
11
-
12
- [**.{yml,yaml,json,conf}]
13
- indent_size = 2
14
-
15
- [**.{md,txt,rst}]
16
- trim_trailing_whitespace = false
17
-
18
- [**.rst]
19
- indent_size = 2
20
-
21
- [Makefile]
22
- indent_style = tab