get_ad & get_user functions, extended user info (badges, feedback, pro), and new usage examples

This commit is contained in:
etienne-hd
2025-06-26 23:36:54 +02:00
parent 241645baae
commit 5841f55b20
20 changed files with 584 additions and 180 deletions

View File

@@ -1,3 +1,19 @@
## 1.0.4
### Added
* A lot of new user information can be retrieved (feedback, badges & professional info).
* New [examples](examples/) directory with practical usage cases.
* `get_ad` function to retrieve ad information.
* `get_user` function to retrieve user information (with pro info such as siret).
* Automatic cookies initialization during session setup to prevent HTTP 403 errors.
### Changed
* Codebase refactored: models are now split by functionality (e.g., all user dataclasses are in `user.py`).
* Proxies can now be updated after `Client` creation via `client.proxy = ...`.
* The session can also be changed dynamically via `client.session = ...`.
### Removed
* Removed the `test/` folder (migrated to `examples/`).
## 1.0.3
### Fixed
* Incorrect raw data extraction for location and owner in `Search.build` function

View File

@@ -1,5 +1,6 @@
# lbc
[![Latest version](https://img.shields.io/pypi/v/lbc?style=for-the-badge)](https://pypi.org/project/lbc)
![PyPI - Downloads](https://img.shields.io/pypi/dm/lbc?style=for-the-badge)
[![GitHub license](https://img.shields.io/github/license/etienne-hd/lbc?style=for-the-badge)](https://github.com/etienne-hd/lbc/blob/master/LICENSE)
**Unofficial client for Leboncoin API**
@@ -34,12 +35,16 @@ for ad in result.ads:
*lbc is not affiliated with, endorsed by, or in any way associated with Leboncoin or its services. Use at your own risk.*
## Installation
Required Python 3.9+
Required **Python 3.9+**
```bash
pip install lbc
```
## Usage
**Full documentation will be available soon.**
Start with the [examples](examples/) to quickly understand how to use the library in real-world scenarios.
### Client
To create client you need to use `lbc.Client` class
```python

22
examples/get_ad.py Normal file
View File

@@ -0,0 +1,22 @@
"""Get detailed information about an ad on Leboncoin using its ID."""
import lbc
def main() -> None:
# Initialize the Leboncoin API client
client = lbc.Client()
# Fetch an ad by its Leboncoin ID (replace with a real one for testing)
ad = client.get_ad("0123456789")
# Print basic information about the ad
print("Title:", ad.subject)
print("Price:", ad.price)
print("Favorites:", ad.favorites)
print("First published on:", ad.first_publication_date)
# Print information about the user who posted the ad
print("User info:", ad.user)
if __name__ == "__main__":
main()

20
examples/get_user.py Normal file
View File

@@ -0,0 +1,20 @@
"""Get detailed information about a Leboncoin user using their user ID."""
import lbc
def main() -> None:
# Initialize the Leboncoin API client
client = lbc.Client()
# Fetch a user by their Leboncoin user ID
# Replace the ID with a real one for testing
user = client.get_user("01234567-89ab-cdef-0123-456789abcdef")
# Print raw user attributes
print("User ID:", user.id)
print("Name:", user.name)
print("Pro status:", user.is_pro)
print("Ads count:", user.total_ads)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,38 @@
"""Search for ads on Leboncoin by location and filters (example: real estate in Paris)."""
import lbc
def main() -> None:
# Initialize the Leboncoin API client
client = lbc.Client()
# Define the search location: Paris with a 10 km radius
location = lbc.City(
lat=48.85994982004764,
lng=2.33801967847424,
radius=10_000, # 10 km
city="Paris"
)
# Perform a search with various filters
result = client.search(
text="maison", # Search for houses
locations=[location], # Only in Paris
page=1,
limit=35, # Max results per page
limit_alu=0, # No auto-suggestions
sort=lbc.Sort.NEWEST, # Sort by newest ads
ad_type=lbc.AdType.OFFER, # Only offers, not searches
category=lbc.Category.IMMOBILIER, # Real estate category
owner_type=lbc.OwnerType.ALL, # All types of sellers
search_in_title_only=True, # Only search in titles
square=(200, 400), # Surface between 200 and 400 m²
price=[300_000, 700_000] # Price range in euros
)
# Display summary of each ad
for ad in result.ads:
print(f"{ad.id} | {ad.url} | {ad.subject} | {ad.price}€ | Seller: {ad.user}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,43 @@
"""Search for professional ads on Leboncoin (example: real estate in Paris)."""
import lbc
def main() -> None:
# Initialize the Leboncoin API client
client = lbc.Client()
# Define search location: Paris with a 10 km radius
location = lbc.City(
lat=48.85994982004764,
lng=2.33801967847424,
radius=10_000, # 10 km
city="Paris"
)
# Perform a search with specific filters
result = client.search(
text="maison", # Search for houses
locations=[location], # Only in Paris
page=1,
limit=35, # Max results per page
limit_alu=0, # No auto-suggestions
sort=lbc.Sort.NEWEST, # Sort by newest ads
ad_type=lbc.AdType.OFFER, # Only offers, not searches
category=lbc.Category.IMMOBILIER, # Real estate category
owner_type=lbc.OwnerType.ALL, # All types of sellers
search_in_title_only=True, # Only search in titles
square=(200, 400), # Surface between 200 and 400 m²
price=[300_000, 700_000] # Price range in euros
)
# Display only professional ads with their legal/professional data
for ad in result.ads:
if ad.user.is_pro and ad.user.pro:
print(
f"Store: {ad.user.pro.online_store_name} | "
f"SIRET: {ad.user.pro.siret} | "
f"Website: {ad.user.pro.website_url or 'N/A'}"
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,21 @@
"""Search for ads on Leboncoin using a full search URL."""
import lbc
def main() -> None:
# Initialize the Leboncoin API client
client = lbc.Client()
# Perform a search using a prebuilt Leboncoin URL
result = client.search(
url="https://www.leboncoin.fr/recherche?category=10&text=maison&locations=Paris__48.86023250788424_2.339006433295173_9256_30000",
page=1,
limit=35
)
# Print basic info about each ad
for ad in result.ads:
print(f"{ad.id} | {ad.url} | {ad.subject} | {ad.price}€ | Seller: {ad.user}")
if __name__ == "__main__":
main()

View File

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

View File

@@ -1,5 +1,5 @@
from .session import Session
from .models import Proxy, Search, Category, AdType, OwnerType, Sort, Region, Department, City
from .models import Proxy, Search, Category, AdType, OwnerType, Sort, Region, Department, City, User, Ad
from .exceptions import DatadomeError, RequestError
from .utils import build_search_payload_with_args, build_search_payload_with_url
@@ -9,7 +9,7 @@ class Client(Session):
def __init__(self, proxy: Optional[Proxy] = None):
super().__init__(proxy=proxy)
def _fetch(self, method: str, url: str, payload: Optional[dict] = None, timeout: int = 30) -> dict:
def _fetch(self, method: str, url: str, payload: Optional[dict] = None, timeout: int = 30) -> Union[dict, None]:
"""
Internal method to send an HTTP request using the configured session.
@@ -95,4 +95,49 @@ class Client(Session):
)
body = self._fetch(method="POST", url="https://api.leboncoin.fr/finder/search", payload=payload)
return Search.build(raw=body)
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")
pro_data = None
if user_data.get("account_type") == "pro":
pro_data = self._fetch(method="GET", url=f"https://api.leboncoin.fr/api/onlinestores/v2/users/{user_id}?fields=all")
return User._build(user_data=user_data, pro_data=pro_data)
def get_ad(
self,
ad_id: Union[str, int]
):
"""
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}")
return Ad._build(raw=body, client=self)

View File

@@ -1,4 +1,6 @@
from .proxy import Proxy
from .search import Search
from .ad import Ad
from .user import User
from .enums import *
from .city import City

View File

@@ -1,17 +1,41 @@
from .attribute import Attribute
from .location import Location
from .owner import Owner
from .user import User
from dataclasses import dataclass
from datetime import datetime
from typing import List
from typing import List, Any, Optional
@dataclass
class Location:
country_id: str
region_id: str
region_name: str
department_id: str
department_name: str
city_label: str
city: str
zipcode: str
lat: float
lng: float
source: str
provider: str
is_shape: bool
@dataclass
class Attribute:
key: str
key_label: Optional[str]
value: str
value_label: str
values: List[str]
values_label: Optional[List[str]]
value_label_reader: Optional[str]
generic: bool
@dataclass
class Ad:
id: int
first_publication_date: datetime
expiration_date: datetime
index_date: datetime
first_publication_date: str
expiration_date: str
index_date: str
status: str
category_id: str
category_name: str
@@ -24,9 +48,78 @@ class Ad:
images: List[str]
attributes: List[Attribute]
location: Location
owner: Owner
has_phone: bool
favorites: int # Unvailaible on Ad from Search
_client: Any
_user_id: str
_user: User
@staticmethod
def _build(raw: dict, client: Any) -> "Ad":
attributes: List[Attribute] = []
for raw_attribute in raw.get("attributes", []):
attributes.append(
Attribute(
key=raw_attribute.get("key"),
key_label=raw_attribute.get("key_label"),
value=raw_attribute.get("value"),
value_label=raw_attribute.get("value_label"),
values=raw_attribute.get("values"),
values_label=raw_attribute.get("values_label"),
value_label_reader=raw_attribute.get("value_label_reader"),
generic=raw_attribute.get("generic")
)
)
raw_location: dict = raw.get("location", {})
location = Location(
country_id=raw_location.get("country_id"),
region_id=raw_location.get("region_id"),
region_name=raw_location.get("region_name"),
department_id=raw_location.get("department_id"),
department_name=raw_location.get("department_name"),
city_label=raw_location.get("city_label"),
city=raw_location.get("city"),
zipcode=raw_location.get("zipcode"),
lat=raw_location.get("lat"),
lng=raw_location.get("lng"),
source=raw_location.get("source"),
provider=raw_location.get("provider"),
is_shape=raw_location.get("is_shape")
)
raw_owner: dict = raw.get("owner", {})
return Ad(
id=raw.get("list_id"),
first_publication_date=raw.get("first_publication_date"),
expiration_date=raw.get("expiration_date"),
index_date=raw.get("index_date"),
status=raw.get("status"),
category_id=raw.get("category_id"),
category_name=raw.get("category_name"),
subject=raw.get("subject"),
body=raw.get("body"),
brand=raw.get("brand"),
ad_type=raw.get("ad_type"),
url=raw.get("url"),
price=raw.get("price_cents") / 100 if raw.get("price_cents") else None,
images=raw.get("images", {}).get("urls_large"),
attributes=attributes,
location=location,
has_phone=raw.get("has_phone"),
favorites=raw.get("counters", {}).get("favorites"),
_client=client,
_user_id=raw_owner.get("user_id"),
_user=None
)
@property
def title(self) -> str:
return self.subject
return self.subject
@property
def user(self) -> User:
if self._user is None:
self._user = self._client.get_user(user_id=self._user_id)
return self._user

View File

@@ -1,13 +0,0 @@
from dataclasses import dataclass
from typing import Optional, List
@dataclass
class Attribute:
key: str
key_label: Optional[str]
value: str
value_label: str
values: List[str]
values_label: Optional[List[str]]
value_label_reader: Optional[str]
generic: bool

View File

@@ -1,5 +1,4 @@
from enum import Enum
from typing import Union, Tuple
class OwnerType(Enum):
PRO = "pro"

View File

@@ -1,17 +0,0 @@
from dataclasses import dataclass
@dataclass
class Location:
country_id: str
region_id: str
region_name: str
department_id: str
department_name: str
city_label: str
city: str
zipcode: str
lat: float
lng: float
source: str
provider: str
is_shape: bool

View File

@@ -1,9 +0,0 @@
from dataclasses import dataclass
@dataclass
class Owner:
store_id: str
user_id: str
type: str
name: str
no_salesmen: bool

View File

@@ -1,11 +1,7 @@
from .ad import Ad
from .attribute import Attribute
from .location import Location
from .owner import Owner
from dataclasses import dataclass
from typing import List
from datetime import datetime
from typing import List, Any
@dataclass
class Search:
@@ -20,73 +16,11 @@ class Search:
ads: List[Ad]
@staticmethod
def build(raw: dict) -> "Search":
ads: List[Ad] = []
for raw_ad in raw.get("ads", []):
attributes: List[Attribute] = []
for raw_attribute in raw_ad.get("attributes", []):
attributes.append(
Attribute(
key=raw_attribute.get("key"),
key_label=raw_attribute.get("key_label"),
value=raw_attribute.get("value"),
value_label=raw_attribute.get("value_label"),
values=raw_attribute.get("values"),
values_label=raw_attribute.get("values_label"),
value_label_reader=raw_attribute.get("value_label_reader"),
generic=raw_attribute.get("generic")
)
)
raw_location: dict = raw_ad.get("location", {})
location = Location(
country_id=raw_location.get("country_id"),
region_id=raw_location.get("region_id"),
region_name=raw_location.get("region_name"),
department_id=raw_location.get("department_id"),
department_name=raw_location.get("department_name"),
city_label=raw_location.get("city_label"),
city=raw_location.get("city"),
zipcode=raw_location.get("zipcode"),
lat=raw_location.get("lat"),
lng=raw_location.get("lng"),
source=raw_location.get("source"),
provider=raw_location.get("provider"),
is_shape=raw_location.get("is_shape")
)
raw_owner: dict = raw_ad.get("owner", {})
owner = Owner(
store_id=raw_owner.get("store_id"),
user_id=raw_owner.get("user_id"),
type=raw_owner.get("type"),
name=raw_owner.get("name"),
no_salesmen=raw_owner.get("no_salesmen")
)
ads.append(
Ad(
id=raw_ad.get("list_id"),
first_publication_date=datetime.strptime(raw_ad.get("first_publication_date"), "%Y-%m-%d %H:%M:%S") if raw_ad.get("first_publication_date") else None,
expiration_date=datetime.strptime(raw_ad.get("expiration_date"), "%Y-%m-%d %H:%M:%S") if raw_ad.get("expiration_date") else None,
index_date=datetime.strptime(raw_ad.get("index_date"), "%Y-%m-%d %H:%M:%S") if raw_ad.get("index_date") else None,
status=raw_ad.get("status"),
category_id=raw_ad.get("category_id"),
category_name=raw_ad.get("category_name"),
subject=raw_ad.get("subject"),
body=raw_ad.get("body"),
brand=raw_ad.get("brand"),
ad_type=raw_ad.get("ad_type"),
url=raw_ad.get("url"),
price=raw_ad.get("price_cents") / 100 if raw_ad.get("price_cents") else None,
images=raw_ad.get("images", {}).get("urls_large"),
attributes=attributes,
location=location,
owner=owner,
has_phone=raw_ad.get("has_phone")
)
)
def _build(raw: dict, client: Any) -> "Search":
ads: List[Ad] = [
Ad._build(raw=ad, client=client)
for ad in raw.get("ads", [])
]
return Search(
total=raw.get("total"),
@@ -97,5 +31,5 @@ class Search:
total_inactive=raw.get("total_inactive"),
total_shippable=raw.get("total_shippable"),
max_pages=raw.get("max_pages"),
ads=ads
ads=ads,
)

235
src/lbc/models/user.py Normal file
View File

@@ -0,0 +1,235 @@
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class Reply:
in_minutes: int
text: str
rate_text: str
rate: int
reply_time_text: str
@dataclass
class Presence:
status: str
presence_text: str
last_activity: str
enabled: bool
@dataclass
class Badge:
type: str
name: str
@dataclass
class Feedback:
overall_score: float
cleanness: float
communication: float
conformity: float
package: float
product: float
recommendation: float
respect: float
transaction: float
user_attention: float
received_count: float
@property
def score(self) -> float:
return self.overall_score * 5 if self.overall_score else None
@dataclass
class Location:
address: str
district: str
city: str
label: str
lat: float
lng: float
zipcode: str
geo_source: str
geo_provider: str
region: str
region_label: str
department: str
department_label: str
country: str
@dataclass
class Review:
author_name: str
rating_value: int
text: str
review_time: str
@dataclass
class Rating:
rating_value: int
user_ratings_total: int
source: str
source_display: str
retrieval_time: str
url: str
reviews: List[Review]
@dataclass
class Pro:
online_store_id: int
online_store_name: str
activity_sector_id: int
activity_sector: str
category_id: int
siren: str
siret: str
store_id: int
active_since: str
location: Location
logo: str
cover: str
slogan: str
description: str
opening_hours: str
website_url: str
rating: Rating
@dataclass
class User:
id: str
name: str
registered_at: str
location: str
feedback: Feedback
profile_picture: str
reply: Reply
presence: Presence
badges: List[Badge]
total_ads: int
store_id: int
account_type: str
description: str
pro: Optional[Pro]
@staticmethod
def _build(user_data: dict, pro_data: Optional[dict]) -> "User":
raw_feedback = user_data.get("feedback", {})
feedback = Feedback(
overall_score=raw_feedback.get("overall_score"),
cleanness=raw_feedback.get("category_scores", {}).get("CLEANNESS"),
communication=raw_feedback.get("category_scores", {}).get("COMMUNICATION"),
conformity=raw_feedback.get("category_scores", {}).get("CONFORMITY"),
package=raw_feedback.get("category_scores", {}).get("PACKAGE"),
product=raw_feedback.get("category_scores", {}).get("PRODUCT"),
recommendation=raw_feedback.get("category_scores", {}).get("RECOMMENDATION"),
respect=raw_feedback.get("category_scores", {}).get("RESPECT"),
transaction=raw_feedback.get("category_scores", {}).get("TRANSACTION"),
user_attention=raw_feedback.get("category_scores", {}).get("USER_ATTENTION"),
received_count=raw_feedback.get("received_count")
)
raw_reply = user_data.get("reply", {})
reply = Reply(
in_minutes=raw_reply.get("in_minutes"),
text=raw_reply.get("text"),
rate_text=raw_reply.get("rate_text"),
rate=raw_reply.get("rate"),
reply_time_text=raw_reply.get("reply_time_text")
)
raw_presence = user_data.get("presence", {})
presence = Presence(
status=raw_presence.get("status"),
presence_text=raw_presence.get("presence_text"),
last_activity=raw_presence.get("last_activity"),
enabled=raw_presence.get("enabled")
)
badges = [
Badge(type=badge.get("type"), name=badge.get("name"))
for badge in user_data.get("badges", [])
]
pro = None
if pro_data:
raw_pro_location = pro_data.get("location", {})
pro_location = Location(
address=raw_pro_location.get("address"),
district=raw_pro_location.get("district"),
city=raw_pro_location.get("city"),
label=raw_pro_location.get("label"),
lat=raw_pro_location.get("lat"),
lng=raw_pro_location.get("lng"),
zipcode=raw_pro_location.get("zipcode"),
geo_source=raw_pro_location.get("geo_source"),
geo_provider=raw_pro_location.get("geo_provider"),
region=raw_pro_location.get("region"),
region_label=raw_pro_location.get("region_label"),
department=raw_pro_location.get("department"),
department_label=raw_pro_location.get("dpt_label"),
country=raw_pro_location.get("country")
)
raw_pro_rating = pro_data.get("rating", {})
pro_rating_reviews = [
Review(
author_name=review.get("author_name"),
rating_value=review.get("rating_value"),
text=review.get("text"),
review_time=review.get("review_time")
)
for review in raw_pro_rating.get("reviews", [])
]
pro_rating = Rating(
rating_value=raw_pro_rating.get("rating_value"),
user_ratings_total=raw_pro_rating.get("user_ratings_total"),
source=raw_pro_rating.get("source"),
source_display=raw_pro_rating.get("source_display"),
retrieval_time=raw_pro_rating.get("retrieval_time"),
url=raw_pro_rating.get("url"),
reviews=pro_rating_reviews
)
pro_owner = pro_data.get("owner", {})
pro_brand = pro_data.get("brand", {})
pro_information = pro_data.get("information", {})
pro = Pro(
online_store_id=pro_data.get("online_store_id"),
online_store_name=pro_data.get("online_store_name"),
activity_sector_id=pro_owner.get("activitySectorID"),
activity_sector=pro_owner.get("activitySector"),
category_id=pro_owner.get("categoryId"),
siren=pro_owner.get("siren"),
siret=pro_owner.get("siret"),
store_id=pro_owner.get("storeId"),
active_since=pro_owner.get("activeSince"),
location=pro_location,
logo=pro_brand.get("logo", {}).get("large"),
cover=pro_brand.get("cover", {}).get("large"),
slogan=pro_brand.get("slogan"),
description=pro_information.get("description"),
opening_hours=pro_information.get("opening_hours"),
website_url=pro_information.get("website_url"),
rating=pro_rating
)
return User(
id=user_data.get("user_id"),
name=user_data.get("name"),
registered_at=user_data.get("registered_at"),
location=user_data.get("location"),
feedback=feedback,
profile_picture=user_data.get("profile_picture", {}).get("extra_large_url"),
reply=reply,
presence=presence,
badges=badges,
total_ads=user_data.get("total_ads"),
store_id=user_data.get("store_id"),
account_type=user_data.get("account_type"),
description=user_data.get("description"),
pro=pro
)
@property
def is_pro(self):
return self.account_type == "pro"

View File

@@ -36,12 +36,31 @@ class Session:
"https": proxy.url
}
session.get("https://www.leboncoin.fr/") # Init cookies
return session
@property
def session(self):
def session(self) -> requests.Session:
return self._session
@session.setter
def session(self, value: requests.Session):
if isinstance(value, requests.Session):
self._session = value
else:
raise TypeError("Session must be an instance of the curl_cffi.requests.Session")
@property
def proxy(self):
return self._proxy
def proxy(self) -> Proxy:
return self._proxy
@proxy.setter
def proxy(self, value: Proxy):
if isinstance(value, Proxy):
self._session.proxies = {
"http": value.url,
"https": value.url
}
else:
raise TypeError("Proxy must be an instance of the Proxy class")

View File

@@ -1,33 +0,0 @@
import lbc
def main() -> None:
client = lbc.Client()
# Paris
location = lbc.City(
lat=48.85994982004764,
lng=2.33801967847424,
radius=10_000, # 10 km
city="Paris"
)
result = client.search(
text="maison",
locations=[location],
page=1,
limit=35,
limit_alu=0,
sort=lbc.Sort.NEWEST,
ad_type=lbc.AdType.OFFER,
category=lbc.Category.IMMOBILIER,
owner_type=lbc.OwnerType.ALL,
search_in_title_only=True,
square=(200, 400),
price=[300_000, 700_000]
)
for ad in result.ads:
print(ad.id, ad.url, ad.subject, ad.price)
if __name__ == "__main__":
main()

View File

@@ -1,16 +0,0 @@
import lbc
def main() -> None:
client = lbc.Client()
result = client.search(
url="https://www.leboncoin.fr/recherche?category=9&text=maison&locations=Paris__48.86023250788424_2.339006433295173_9256&square=100-200&price=500000-1000000&rooms=1-6&bedrooms=3-6&outside_access=garden,terrace&orientation=south_west&owner_type=private",
page=1,
limit=35
)
for ad in result.ads:
print(ad.id, ad.url, ad.subject, ad.price)
if __name__ == "__main__":
main()