Changed: split functionnality into different mixin

This commit is contained in:
etienne-hd
2025-12-24 13:58:39 +01:00
parent 00cf534191
commit 4e69194821
17 changed files with 138 additions and 124 deletions

View File

@@ -1,6 +1,6 @@
[project]
name = "lbc"
version = "1.0.10"
version = "1.1.0"
description = "Unofficial client for Leboncoin API"
readme = "README.md"
license = {text = "MIT"}

View File

@@ -1,2 +1,2 @@
from .client import Client
from .models import *
from .model import *

View File

@@ -1,14 +1,23 @@
from .session import Session
from .models import Proxy, Search, Category, AdType, OwnerType, Sort, Region, Department, City, User, Ad
from .exceptions import DatadomeError, RequestError, NotFoundError
from .utils import build_search_payload_with_args, build_search_payload_with_url
from typing import Optional, List, Union
from typing import Optional, Union
from curl_cffi import BrowserTypeLiteral
class Client(Session):
from .mixin import (
SessionMixin,
SearchMixin,
UserMixin,
AdMixin
)
from .model import Proxy
from .exceptions import DatadomeError, RequestError, NotFoundError
class Client(
SessionMixin,
SearchMixin,
UserMixin,
AdMixin
):
def __init__(self, proxy: Optional[Proxy] = None, impersonate: BrowserTypeLiteral = None,
request_verify: bool = True, timeout: int = 30, max_retries: int = 5):
request_verify: bool = True, timeout: float = 30.0, max_retries: int = 5):
"""
Initializes a Leboncoin Client instance with optional proxy, browser impersonation, and SSL verification settings.
@@ -65,107 +74,4 @@ class Client(Session):
elif response.status_code == 404 or response.status_code == 410:
raise NotFoundError(f"Unable to find ad or user.")
else:
raise RequestError(f"Request failed with status code {response.status_code}.")
def search(
self,
url: Optional[str] = None,
text: Optional[str] = None,
category: Category = Category.TOUTES_CATEGORIES,
sort: Sort = Sort.RELEVANCE,
locations: Optional[Union[List[Union[Region, Department, City]], Union[Region, Department, City]]] = None,
limit: int = 35,
limit_alu: int = 3,
page: int = 1,
ad_type: AdType = AdType.OFFER,
owner_type: Optional[OwnerType] = None,
shippable: Optional[bool] = None,
search_in_title_only: bool = False,
**kwargs
) -> Search:
"""
Perform a classified ads search on Leboncoin with the specified criteria.
You can either:
- Provide a full `url` from a Leboncoin search to replicate the search directly.
- Or use the individual parameters (`text`, `category`, `locations`, etc.) to construct a custom search.
Args:
url (Optional[str], optional): A full Leboncoin search URL. If provided, all other parameters will be ignored and the search will replicate the results from the URL.
text (Optional[str], optional): Search keywords. If None, returns all matching ads without filtering by keyword. Defaults to None.
category (Category, optional): Category to search in. Defaults to Category.TOUTES_CATEGORIES.
sort (Sort, optional): Sorting method for results (e.g., relevance, date, price). Defaults to Sort.RELEVANCE.
locations (Optional[Union[List[Union[Region, Department, City]], Union[Region, Department, City]]], optional): One or multiple locations (region, department, or city) to filter results. Defaults to None.
limit (int, optional): Maximum number of results to return. Defaults to 35.
limit_alu (int, optional): Number of ALU (Annonces Lu / similar ads) suggestions to include. Defaults to 3.
page (int, optional): Page number to retrieve for paginated results. Defaults to 1.
ad_type (AdType, optional): Type of ad (offer or request). Defaults to AdType.OFFER.
owner_type (Optional[OwnerType], optional): Filter by seller type (individual, professional, or all). Defaults to None.
shippable (Optional[bool], optional): If True, only includes ads that offer shipping. Defaults to None.
search_in_title_only (bool, optional): If True, search will only be performed on ad titles. Defaults to False.
**kwargs: Additional advanced filters such as price range (`price=(min, max)`), surface area (`square=(min, max)`), property type, and more.
Returns:
Search: A `Search` object containing the parsed search results.
"""
if url:
payload = build_search_payload_with_url(
url=url, limit=limit, page=page
)
else:
payload = build_search_payload_with_args(
text=text, category=category, sort=sort, locations=locations,
limit=limit, limit_alu=limit_alu, page=page, ad_type=ad_type,
owner_type=owner_type, shippable=shippable, search_in_title_only=search_in_title_only, **kwargs
)
body = self._fetch(method="POST", url="https://api.leboncoin.fr/finder/search", payload=payload, timeout=self.timeout, max_retries=self.max_retries)
return Search._build(raw=body, client=self)
def get_user(
self,
user_id: str
) -> User:
"""
Retrieve information about a user based on their user ID.
This method fetches detailed user data such as their profile, professional status,
and other relevant metadata available through the public user API.
Args:
user_id (str): The unique identifier of the user on Leboncoin. Usually found in the url (e.g 57f99bb6-0446-4b82-b05d-a44ea7bcd2cc).
Returns:
User: A `User` object containing the parsed user information.
"""
user_data = self._fetch(method="GET", url=f"https://api.leboncoin.fr/api/user-card/v2/{user_id}/infos", timeout=self.timeout, max_retries=self.max_retries)
pro_data = None
if user_data.get("account_type") == "pro":
try:
pro_data = self._fetch(method="GET", url=f"https://api.leboncoin.fr/api/onlinestores/v2/users/{user_id}?fields=all", timeout=self.timeout, max_retries=self.max_retries)
except NotFoundError:
pass # Some professional users may not have a Leboncoin page.
return User._build(user_data=user_data, pro_data=pro_data)
def get_ad(
self,
ad_id: Union[str, int]
) -> Ad:
"""
Retrieve detailed information about a classified ad using its ID.
This method fetches the full content of an ad, including its description,
pricing, location, and other relevant metadata made
available through the public Leboncoin ad API.
Args:
ad_id (Union[str, int]): The unique identifier of the ad on Leboncoin. Can be found in the ad URL.
Returns:
Ad: An `Ad` object containing the parsed ad information.
"""
body = self._fetch(method="GET", url=f"https://api.leboncoin.fr/api/adfinder/v1/classified/{ad_id}", timeout=self.timeout, max_retries=self.max_retries)
return Ad._build(raw=body, client=self)
raise RequestError(f"Request failed with status code {response.status_code}.")

View File

@@ -1,18 +1,14 @@
class LBCError(Exception):
"""Base exception for all errors raised by the LBC client."""
class InvalidValue(LBCError):
"""Raised when a provided value is invalid or improperly formatted."""
class RequestError(LBCError):
"""Raised when an HTTP request fails with a non-success status code."""
class DatadomeError(RequestError):
"""Raised when access is blocked by Datadome anti-bot protection."""
class NotFoundError(LBCError):
"""Raised when a user or ad is not found."""

View File

@@ -0,0 +1,4 @@
from .session import SessionMixin
from .search import SearchMixin
from .user import UserMixin
from .ad import AdMixin

22
src/lbc/mixin/ad.py Normal file
View File

@@ -0,0 +1,22 @@
from typing import Union
from ..model import Ad
class AdMixin:
def get_ad(self, ad_id: Union[str, int]) -> Ad:
"""
Retrieve detailed information about a classified ad using its ID.
This method fetches the full content of an ad, including its description,
pricing, location, and other relevant metadata made
available through the public Leboncoin ad API.
Args:
ad_id (Union[str, int]): The unique identifier of the ad on Leboncoin. Can be found in the ad URL.
Returns:
Ad: An `Ad` object containing the parsed ad information.
"""
body = self._fetch(method="GET", url=f"https://api.leboncoin.fr/api/adfinder/v1/classified/{ad_id}", timeout=self.timeout, max_retries=self.max_retries)
return Ad._build(raw=body, client=self)

60
src/lbc/mixin/search.py Normal file
View File

@@ -0,0 +1,60 @@
from typing import Optional, Union, List
from ..model import Category, Sort, Region, Department, City, AdType, OwnerType, Search
from ..utils import build_search_payload_with_args, build_search_payload_with_url
class SearchMixin:
def search(
self,
url: Optional[str] = None,
text: Optional[str] = None,
category: Category = Category.TOUTES_CATEGORIES,
sort: Sort = Sort.RELEVANCE,
locations: Optional[Union[List[Union[Region, Department, City]], Union[Region, Department, City]]] = None,
limit: int = 35,
limit_alu: int = 3,
page: int = 1,
ad_type: AdType = AdType.OFFER,
owner_type: Optional[OwnerType] = None,
shippable: Optional[bool] = None,
search_in_title_only: bool = False,
**kwargs
) -> Search:
"""
Perform a classified ads search on Leboncoin with the specified criteria.
You can either:
- Provide a full `url` from a Leboncoin search to replicate the search directly.
- Or use the individual parameters (`text`, `category`, `locations`, etc.) to construct a custom search.
Args:
url (Optional[str], optional): A full Leboncoin search URL. If provided, all other parameters will be ignored and the search will replicate the results from the URL.
text (Optional[str], optional): Search keywords. If None, returns all matching ads without filtering by keyword. Defaults to None.
category (Category, optional): Category to search in. Defaults to Category.TOUTES_CATEGORIES.
sort (Sort, optional): Sorting method for results (e.g., relevance, date, price). Defaults to Sort.RELEVANCE.
locations (Optional[Union[List[Union[Region, Department, City]], Union[Region, Department, City]]], optional): One or multiple locations (region, department, or city) to filter results. Defaults to None.
limit (int, optional): Maximum number of results to return. Defaults to 35.
limit_alu (int, optional): Number of ALU (Annonces Lu / similar ads) suggestions to include. Defaults to 3.
page (int, optional): Page number to retrieve for paginated results. Defaults to 1.
ad_type (AdType, optional): Type of ad (offer or request). Defaults to AdType.OFFER.
owner_type (Optional[OwnerType], optional): Filter by seller type (individual, professional, or all). Defaults to None.
shippable (Optional[bool], optional): If True, only includes ads that offer shipping. Defaults to None.
search_in_title_only (bool, optional): If True, search will only be performed on ad titles. Defaults to False.
**kwargs: Additional advanced filters such as price range (`price=(min, max)`), surface area (`square=(min, max)`), property type, and more.
Returns:
Search: A `Search` object containing the parsed search results.
"""
if url:
payload = build_search_payload_with_url(
url=url, limit=limit, page=page
)
else:
payload = build_search_payload_with_args(
text=text, category=category, sort=sort, locations=locations,
limit=limit, limit_alu=limit_alu, page=page, ad_type=ad_type,
owner_type=owner_type, shippable=shippable, search_in_title_only=search_in_title_only, **kwargs
)
body = self._fetch(method="POST", url="https://api.leboncoin.fr/finder/search", payload=payload, timeout=self.timeout, max_retries=self.max_retries)
return Search._build(raw=body, client=self)

View File

@@ -1,14 +1,15 @@
from .models import Proxy
from ..model import Proxy
from curl_cffi import requests, BrowserTypeLiteral
from typing import Optional
import random
class Session:
def __init__(self, proxy: Optional[Proxy] = None, impersonate: BrowserTypeLiteral = None, request_verify: bool = True):
class SessionMixin:
def __init__(self, proxy: Optional[Proxy] = None, impersonate: BrowserTypeLiteral = None, request_verify: bool = True, **kwargs):
self._session = self._init_session(proxy=proxy, impersonate=impersonate, request_verify=request_verify)
self._proxy = proxy
self._impersonate = impersonate
super().__init__(**kwargs)
def _init_session(self, proxy: Optional[Proxy] = None, impersonate: BrowserTypeLiteral = None, request_verify: bool = True) -> requests.Session:
"""
@@ -47,7 +48,6 @@ class Session:
'Sec-Fetch-Site': 'same-site',
}
)
if proxy:
session.proxies = {
"http": proxy.url,
@@ -55,7 +55,6 @@ class Session:
}
session.get("https://www.leboncoin.fr/", verify=request_verify) # Init cookies
return session
@property

27
src/lbc/mixin/user.py Normal file
View File

@@ -0,0 +1,27 @@
from ..model import User
from ..exceptions import NotFoundError
class UserMixin:
def get_user(self, user_id: str) -> User:
"""
Retrieve information about a user based on their user ID.
This method fetches detailed user data such as their profile, professional status,
and other relevant metadata available through the public user API.
Args:
user_id (str): The unique identifier of the user on Leboncoin. Usually found in the url (e.g 57f99bb6-0446-4b82-b05d-a44ea7bcd2cc).
Returns:
User: A `User` object containing the parsed user information.
"""
user_data = self._fetch(method="GET", url=f"https://api.leboncoin.fr/api/user-card/v2/{user_id}/infos", timeout=self.timeout, max_retries=self.max_retries)
pro_data = None
if user_data.get("account_type") == "pro":
try:
pro_data = self._fetch(method="GET", url=f"https://api.leboncoin.fr/api/onlinestores/v2/users/{user_id}?fields=all", timeout=self.timeout, max_retries=self.max_retries)
except NotFoundError:
pass # Some professional users may not have a Leboncoin page.
return User._build(user_data=user_data, pro_data=pro_data)

View File

@@ -1,4 +1,4 @@
from .models import Category, AdType, OwnerType, Sort, Region, Department, City
from .model import Category, AdType, OwnerType, Sort, Region, Department, City
from .exceptions import InvalidValue
from typing import Optional, Union, List