diff --git a/CHANGELOG.md b/CHANGELOG.md index 550d29a..3965935 100644 --- a/CHANGELOG.md +++ b/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 ### Fixed * Incorrect raw data extraction for location and owner in `Search.build` function diff --git a/README.md b/README.md index 9df27d8..1fa34b3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/get_ad.py b/examples/get_ad.py new file mode 100644 index 0000000..72dabe0 --- /dev/null +++ b/examples/get_ad.py @@ -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() diff --git a/examples/get_user.py b/examples/get_user.py new file mode 100644 index 0000000..4b76c24 --- /dev/null +++ b/examples/get_user.py @@ -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() diff --git a/examples/search_with_args.py b/examples/search_with_args.py new file mode 100644 index 0000000..dbaa94a --- /dev/null +++ b/examples/search_with_args.py @@ -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() diff --git a/examples/search_with_args_pro.py b/examples/search_with_args_pro.py new file mode 100644 index 0000000..d2bc2ee --- /dev/null +++ b/examples/search_with_args_pro.py @@ -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() diff --git a/examples/search_with_url.py b/examples/search_with_url.py new file mode 100644 index 0000000..a02f7ee --- /dev/null +++ b/examples/search_with_url.py @@ -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() diff --git a/pyproject.toml b/pyproject.toml index 2d4f4a9..bdc51dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"} diff --git a/src/lbc/client.py b/src/lbc/client.py index c88676b..efc3084 100644 --- a/src/lbc/client.py +++ b/src/lbc/client.py @@ -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) \ No newline at end of file diff --git a/src/lbc/models/__init__.py b/src/lbc/models/__init__.py index 1a7c89b..f60c684 100644 --- a/src/lbc/models/__init__.py +++ b/src/lbc/models/__init__.py @@ -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 \ No newline at end of file diff --git a/src/lbc/models/ad.py b/src/lbc/models/ad.py index 66fd5a4..c296ba6 100644 --- a/src/lbc/models/ad.py +++ b/src/lbc/models/ad.py @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/src/lbc/models/attribute.py b/src/lbc/models/attribute.py deleted file mode 100644 index 56126ed..0000000 --- a/src/lbc/models/attribute.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/lbc/models/enums.py b/src/lbc/models/enums.py index 27de24e..bd199ad 100644 --- a/src/lbc/models/enums.py +++ b/src/lbc/models/enums.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Union, Tuple class OwnerType(Enum): PRO = "pro" diff --git a/src/lbc/models/location.py b/src/lbc/models/location.py deleted file mode 100644 index 257f655..0000000 --- a/src/lbc/models/location.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/lbc/models/owner.py b/src/lbc/models/owner.py deleted file mode 100644 index b537f11..0000000 --- a/src/lbc/models/owner.py +++ /dev/null @@ -1,9 +0,0 @@ -from dataclasses import dataclass - -@dataclass -class Owner: - store_id: str - user_id: str - type: str - name: str - no_salesmen: bool \ No newline at end of file diff --git a/src/lbc/models/search.py b/src/lbc/models/search.py index 7ad69dc..938cab5 100644 --- a/src/lbc/models/search.py +++ b/src/lbc/models/search.py @@ -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, ) \ No newline at end of file diff --git a/src/lbc/models/user.py b/src/lbc/models/user.py new file mode 100644 index 0000000..1db392f --- /dev/null +++ b/src/lbc/models/user.py @@ -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" \ No newline at end of file diff --git a/src/lbc/session.py b/src/lbc/session.py index cf8c17f..f68b3d0 100644 --- a/src/lbc/session.py +++ b/src/lbc/session.py @@ -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 \ No newline at end of file + 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") diff --git a/tests/test_search_with_args.py b/tests/test_search_with_args.py deleted file mode 100644 index f6505f8..0000000 --- a/tests/test_search_with_args.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/tests/test_search_with_url.py b/tests/test_search_with_url.py deleted file mode 100644 index eabead4..0000000 --- a/tests/test_search_with_url.py +++ /dev/null @@ -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() \ No newline at end of file