Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified requirements.txt
Binary file not shown.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
packages=find_packages(),
url="https://github.com/Pawikoski/vinted-api-wrapper",
python_requires=">=3.10",
install_requires=["cloudscraper", "dacite", "beautifulsoup4"],
install_requires=["cloudscraper", "dacite", "beautifulsoup4", "fake_useragent", "urllib3"],
long_description=long_description,
long_description_content_type="text/markdown",
license="MIT",
Expand Down
3 changes: 2 additions & 1 deletion vinted/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ class Endpoints(Enum):
USER_FEEDBACKS = "/user_feedbacks"
USER_ITEMS = "/users/{}/items"
USER_FEEDBACKS_SUMMARY = "/user_feedbacks/summary"
SEARCH_SUGGESTIONS = "/search_suggestions"
SEARCH_SUGGESTIONS = "/search_suggestions"
SHIPPING_DETAILS = "/items/{}/shipping_details"
6 changes: 6 additions & 0 deletions vinted/models/money.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ class Conversion:
class Price:
amount: Optional[str]
currency_code: Optional[str]


@dataclass
class Discount:
max_percent: int
delivery_type: str
16 changes: 16 additions & 0 deletions vinted/models/search.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .base import VintedResponse
from .items import Item
from .users import DetailedUser
from .money import Discount, Price
from dataclasses import dataclass
from typing import List, Optional

Expand Down Expand Up @@ -47,6 +48,15 @@ class SearchSuggestion:
suggestion_type: int


@dataclass
class ShippingDetails:
pickup_only: bool
multiple_shipping_options_available: bool
free_shipping: bool
price: Optional[Price] = None
discount: Optional[Discount] = None


@dataclass
class SearchResponse(VintedResponse):
dominant_brand: Optional[DominantBrand]
Expand All @@ -62,3 +72,9 @@ class UserSearchResponse(VintedResponse):
@dataclass
class SearchSuggestionsResponse(VintedResponse):
search_suggestions: List[SearchSuggestion]


@dataclass
class ShippingResponse(VintedResponse):
shipping_details: ShippingDetails
code: int
111 changes: 96 additions & 15 deletions vinted/vinted.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import logging
import time
import math
import random
from copy import deepcopy
from typing import List, Literal
from urllib.parse import urlencode, urlparse, urlunparse

import cloudscraper
from bs4 import BeautifulSoup
from dacite import from_dict
from fake_useragent import UserAgent

from .endpoints import Endpoints
from .exceptions import RateLimitExceededException
from .models.base import VintedResponse
from .models.filters import Catalog, FiltersResponse, InitializersResponse
from .models.items import ItemsResponse, UserItemsResponse
from .models.other import Domain, Language, SortOption
from .models.search import SearchResponse, SearchSuggestionsResponse, UserSearchResponse
from .models.search import SearchResponse, SearchSuggestionsResponse, UserSearchResponse, ShippingResponse
from .models.users import (
UserFeedbacksResponse,
UserFeedbacksSummaryResponse,
Expand All @@ -34,6 +37,10 @@
# Set default level to INFO, but users can override with logger.setLevel(logging.DEBUG)
logger.setLevel(logging.INFO)

ua = UserAgent(platforms=['windows', 'macos'], browsers=['chrome', 'firefox', 'safari'])
# Use a consistent user agent throughout the session to avoid detection
# Choose a recent Chrome user agent for better compatibility


class Vinted:
def __init__(
Expand All @@ -59,19 +66,34 @@ def __init__(

self.base_url = f"https://www.vinted.{domain}"
self.api_url = f"{self.base_url}/api/v2"
logger.debug(f"Base URL: {self.base_url}, API URL: {self.api_url}")
self.host = f"www.vinted.{domain}"
self.user_agent = ua.random

logger.debug(f"Base URL: {self.base_url}, API URL: {self.api_url}")

self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"Host": f"www.vinted.{domain}",
"Accept": "application/json, text/plain, */*",
# Basic request headers
"User-Agent": self.user_agent,
"Host": self.host,

# Accept headers
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": f"{language},*;q=0.5",

# Connection and transfer headers
"Connection": "keep-alive",
"X-Requested-With": "XMLHttpRequest",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"TE": "Trailers",

# Security and privacy headers
"DNT": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "cross-site",
"Upgrade-Insecure-Requests": "1",

# Priority header
"Priority": "u=0, i",
}

logger.debug(f"Headers configured: {self.headers}")
Expand Down Expand Up @@ -113,8 +135,18 @@ def update_proxy(self, proxy: str) -> None:

def fetch_cookies(self):
logger.debug(f"Fetching cookies from: {self.base_url}")
response = self.scraper.get(
self.base_url, headers=self.headers, proxies=self.proxy

headers = {
"User-Agent": self.user_agent,
"Host": self.host,
"Accept-Encoding": "gzip, deflate, br, zstd",
"Connection": "keep-alive",
"TE": "trailers",
"DNT": "1"
}

response = self.scraper.head(
f"{self.base_url}/how_it_works", headers=headers, proxies=self.proxy, allow_redirects=True
)
logger.info(
f"Cookies fetched successfully, status code: {response.status_code}"
Expand Down Expand Up @@ -173,12 +205,17 @@ def _call(self, method: Literal["get"], *args, **kwargs):
if "recursive" in kwargs:
del kwargs["recursive"]

# Handle custom headers
headers = kwargs.pop("headers", self.headers)
if headers != self.headers:
logger.debug(f"Using custom headers: {headers}")

logger.info(
f"Executing {method.upper()} request to: {kwargs.get('url', 'unknown URL')}"
)
response = self.scraper.request(
method=method,
headers=self.headers,
headers=headers,
cookies=self.cookies,
proxies=self.proxy,
*args,
Expand Down Expand Up @@ -280,7 +317,7 @@ def search(
params = {
"page": page,
"per_page": per_page,
"time": time.time(),
"time": math.floor(time.time() - random.random() * 60 * 3),
"search_text": query,
"price_from": price_from,
"price_to": price_to,
Expand All @@ -300,7 +337,14 @@ def search(
params.update(parse_url_to_params(url))

logger.debug(f"Final search parameters: {params}")
result = self._get(Endpoints.CATALOG_ITEMS, SearchResponse, params=params)

# Add Referer header only if url parameter is provided
headers = self.headers.copy()
if url:
headers["Referer"] = url
logger.debug(f"Added Referer header: {headers['Referer']}")

result = self._get(Endpoints.CATALOG_ITEMS, SearchResponse, params=params, headers=headers)
logger.info("Search completed successfully")
return result

Expand Down Expand Up @@ -435,7 +479,41 @@ def catalogs_list(self) -> List[Catalog]:
f"Catalogs list retrieved successfully, found {len(data.dtos.catalogs)} catalogs"
)
return data.dtos.catalogs


def fetch_shipping_details(self, item_id: int) -> ShippingResponse:
"""
Fetches shipping details for a specific item.

Args:
item_id: The ID of the item to fetch shipping details for

Returns:
ShippingResponse: Contains shipping details including pickup options,
multiple shipping options availability, free shipping status,
pricing information, and potential discounts

Raises:
RateLimitExceededException: If rate limit is exceeded
requests.exceptions.HTTPError: If the request fails

Example:
shipping = vinted.fetch_shipping_details(123456)
if shipping.shipping_details.free_shipping:
print("Free shipping available!")
print(f"Shipping price: {shipping.shipping_details.price.amount} {shipping.shipping_details.price.currency_code}")
"""
logger.info(f"Fetching shipping details for item_id: {item_id}")
logger.debug(f"Requesting shipping details from endpoint: {Endpoints.SHIPPING_DETAILS.value}")

try:
result = self._get(Endpoints.SHIPPING_DETAILS, ShippingResponse, item_id)
logger.info(f"Shipping details retrieved successfully for item_id: {item_id}")
logger.debug(f"Response code: {result.code}, Free shipping: {result.shipping_details.free_shipping}")
return result
except Exception as e:
logger.error(f"Failed to fetch shipping details for item_id {item_id}: {e}")
raise

def fetch_offer_description(self, url: str) -> str:
"""
Fetches the offer description from a given Vinted item URL.
Expand Down Expand Up @@ -468,3 +546,6 @@ def fetch_offer_description(self, url: str) -> str:
except Exception as e:
logger.error(f"An error occurred while fetching the description: {e}")
return None