mirror of
https://github.com/etienne-hd/lbc.git
synced 2025-12-05 09:08:10 +01:00
get_ad & get_user functions, extended user info (badges, feedback, pro), and new usage examples
This commit is contained in:
16
CHANGELOG.md
16
CHANGELOG.md
@@ -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
|
## 1.0.3
|
||||||
### Fixed
|
### Fixed
|
||||||
* Incorrect raw data extraction for location and owner in `Search.build` function
|
* Incorrect raw data extraction for location and owner in `Search.build` function
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# lbc
|
# lbc
|
||||||
[](https://pypi.org/project/lbc)
|
[](https://pypi.org/project/lbc)
|
||||||
|

|
||||||
[](https://github.com/etienne-hd/lbc/blob/master/LICENSE)
|
[](https://github.com/etienne-hd/lbc/blob/master/LICENSE)
|
||||||
|
|
||||||
**Unofficial client for Leboncoin API**
|
**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.*
|
*lbc is not affiliated with, endorsed by, or in any way associated with Leboncoin or its services. Use at your own risk.*
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
Required Python 3.9+
|
Required **Python 3.9+**
|
||||||
```bash
|
```bash
|
||||||
pip install lbc
|
pip install lbc
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## 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
|
### Client
|
||||||
To create client you need to use `lbc.Client` class
|
To create client you need to use `lbc.Client` class
|
||||||
```python
|
```python
|
||||||
|
|||||||
22
examples/get_ad.py
Normal file
22
examples/get_ad.py
Normal 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
20
examples/get_user.py
Normal 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()
|
||||||
38
examples/search_with_args.py
Normal file
38
examples/search_with_args.py
Normal 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()
|
||||||
43
examples/search_with_args_pro.py
Normal file
43
examples/search_with_args_pro.py
Normal 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()
|
||||||
21
examples/search_with_url.py
Normal file
21
examples/search_with_url.py
Normal 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()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "lbc"
|
name = "lbc"
|
||||||
version = "1.0.3"
|
version = "1.0.4"
|
||||||
description = "Unofficial client for Leboncoin API"
|
description = "Unofficial client for Leboncoin API"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from .session import Session
|
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 .exceptions import DatadomeError, RequestError
|
||||||
from .utils import build_search_payload_with_args, build_search_payload_with_url
|
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):
|
def __init__(self, proxy: Optional[Proxy] = None):
|
||||||
super().__init__(proxy=proxy)
|
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.
|
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)
|
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)
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
from .proxy import Proxy
|
from .proxy import Proxy
|
||||||
from .search import Search
|
from .search import Search
|
||||||
|
from .ad import Ad
|
||||||
|
from .user import User
|
||||||
from .enums import *
|
from .enums import *
|
||||||
from .city import City
|
from .city import City
|
||||||
@@ -1,17 +1,41 @@
|
|||||||
from .attribute import Attribute
|
from .user import User
|
||||||
from .location import Location
|
|
||||||
from .owner import Owner
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from typing import List, Any, Optional
|
||||||
from typing import List
|
|
||||||
|
@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
|
@dataclass
|
||||||
class Ad:
|
class Ad:
|
||||||
id: int
|
id: int
|
||||||
first_publication_date: datetime
|
first_publication_date: str
|
||||||
expiration_date: datetime
|
expiration_date: str
|
||||||
index_date: datetime
|
index_date: str
|
||||||
status: str
|
status: str
|
||||||
category_id: str
|
category_id: str
|
||||||
category_name: str
|
category_name: str
|
||||||
@@ -24,9 +48,78 @@ class Ad:
|
|||||||
images: List[str]
|
images: List[str]
|
||||||
attributes: List[Attribute]
|
attributes: List[Attribute]
|
||||||
location: Location
|
location: Location
|
||||||
owner: Owner
|
|
||||||
has_phone: bool
|
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
|
@property
|
||||||
def title(self) -> str:
|
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
|
||||||
@@ -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
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Union, Tuple
|
|
||||||
|
|
||||||
class OwnerType(Enum):
|
class OwnerType(Enum):
|
||||||
PRO = "pro"
|
PRO = "pro"
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Owner:
|
|
||||||
store_id: str
|
|
||||||
user_id: str
|
|
||||||
type: str
|
|
||||||
name: str
|
|
||||||
no_salesmen: bool
|
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
from .ad import Ad
|
from .ad import Ad
|
||||||
from .attribute import Attribute
|
|
||||||
from .location import Location
|
|
||||||
from .owner import Owner
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List
|
from typing import List, Any
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Search:
|
class Search:
|
||||||
@@ -20,73 +16,11 @@ class Search:
|
|||||||
ads: List[Ad]
|
ads: List[Ad]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def build(raw: dict) -> "Search":
|
def _build(raw: dict, client: Any) -> "Search":
|
||||||
ads: List[Ad] = []
|
ads: List[Ad] = [
|
||||||
|
Ad._build(raw=ad, client=client)
|
||||||
for raw_ad in raw.get("ads", []):
|
for 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")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return Search(
|
return Search(
|
||||||
total=raw.get("total"),
|
total=raw.get("total"),
|
||||||
@@ -97,5 +31,5 @@ class Search:
|
|||||||
total_inactive=raw.get("total_inactive"),
|
total_inactive=raw.get("total_inactive"),
|
||||||
total_shippable=raw.get("total_shippable"),
|
total_shippable=raw.get("total_shippable"),
|
||||||
max_pages=raw.get("max_pages"),
|
max_pages=raw.get("max_pages"),
|
||||||
ads=ads
|
ads=ads,
|
||||||
)
|
)
|
||||||
235
src/lbc/models/user.py
Normal file
235
src/lbc/models/user.py
Normal 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"
|
||||||
@@ -36,12 +36,31 @@ class Session:
|
|||||||
"https": proxy.url
|
"https": proxy.url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session.get("https://www.leboncoin.fr/") # Init cookies
|
||||||
|
|
||||||
return session
|
return session
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def session(self):
|
def session(self) -> requests.Session:
|
||||||
return self._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
|
@property
|
||||||
def proxy(self):
|
def proxy(self) -> Proxy:
|
||||||
return 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")
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
Reference in New Issue
Block a user