mirror of
https://github.com/etienne-hd/lbc.git
synced 2026-04-29 18:25:37 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba7e55a544 | ||
|
|
fa576ab47c | ||
|
|
89e507dcc2 | ||
|
|
a73feccc2a | ||
|
|
069646c474 | ||
|
|
b0919f6320 | ||
|
|
8e3b8464db | ||
|
|
be2ceec208 | ||
|
|
0ba4cf44dd | ||
|
|
4623b0627a | ||
|
|
cbdf3d7821 | ||
|
|
ea459409c0 | ||
|
|
d45cc9c06d | ||
|
|
8d90173aa3 | ||
|
|
2e7bdd1b4b | ||
|
|
a44472e757 | ||
|
|
6669950f82 | ||
|
|
bbbdd1652f |
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.10
|
||||
53
CHANGELOG.md
53
CHANGELOG.md
@@ -1,43 +1,84 @@
|
||||
## 1.1.1
|
||||
## 1.1.3
|
||||
|
||||
### Changed
|
||||
|
||||
* Updated the minimum Python version from 3.9 to 3.10
|
||||
* Updated `curl_cffi`
|
||||
* Switched the build backend to `uv`
|
||||
* Formatted the codebase with `ruff`
|
||||
* Replaced `typing` imports with Python 3.10 built-in generics and union syntax (`list`, `dict`, `|`, etc.)
|
||||
* Hid internal `ad` fields from `repr`
|
||||
|
||||
## 1.1.2
|
||||
|
||||
### Changed
|
||||
|
||||
* Proxy handling, you can now remove proxy using `client.proxy = None`
|
||||
* `client._session` is now public -> `client.session`
|
||||
|
||||
### Added
|
||||
|
||||
* Proxy example at [examples/proxy.py](examples/proxy.py)
|
||||
|
||||
## 1.1.1
|
||||
|
||||
### Added
|
||||
|
||||
* Optional `scheme` attribute on the Proxy dataclass, allowing custom proxy URL schemes (#7)
|
||||
|
||||
## 1.1.0
|
||||
|
||||
### Changed
|
||||
|
||||
* Project structure reorganized: features such as **search**, **user**, and **ad** are now separated using mixins.
|
||||
|
||||
### Added
|
||||
|
||||
* Realistic dynamic mobile User-Agent generation.
|
||||
|
||||
## 1.0.10
|
||||
|
||||
### Fix
|
||||
|
||||
* KeyError when using shippable=True (#5)
|
||||
|
||||
## 1.0.9
|
||||
|
||||
### Fix
|
||||
|
||||
* Fixed SSL verification issue during Leboncoin cookie initialization.
|
||||
|
||||
## 1.0.8
|
||||
|
||||
### Added
|
||||
|
||||
* `max_retries` and `timeout` parameters to `Client`.
|
||||
* `NotFoundError` exception raised when an ad or user is not found.
|
||||
|
||||
## 1.0.7
|
||||
|
||||
### Added
|
||||
|
||||
* Automatic rotation of browser impersonation when `impersonate` argument in `Client` is set to None.
|
||||
* Ability to choose which browser to impersonate via the `impersonate` argument in `Client`.
|
||||
* Option to disable SSL verification for requests by setting `request_verify` to `False` in `Client`.
|
||||
|
||||
## 1.0.6
|
||||
|
||||
### Fixed
|
||||
|
||||
* "Unknown location type" error when searching with a URL containing a zipcode.
|
||||
|
||||
## 1.0.5
|
||||
|
||||
### Fixed
|
||||
|
||||
* 404 error when fetching a pro user who doesn't have a public page
|
||||
|
||||
## 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.
|
||||
@@ -45,28 +86,38 @@
|
||||
* 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
|
||||
|
||||
## 1.0.2
|
||||
|
||||
### Added
|
||||
|
||||
* Support for full Leboncoin URL in `client.search(url=...)`
|
||||
* New `shippable` argument in the `search` function
|
||||
|
||||
### Fixed
|
||||
|
||||
* Incorrect enum key assignment in the `build_search_payload_with_args` function
|
||||
|
||||
## 1.0.1
|
||||
|
||||
### Added
|
||||
|
||||
* Realistic `Sec-Fetch-*` headers to prevent 403 errors
|
||||
|
||||
## 1.0.0
|
||||
|
||||
* Initial release
|
||||
|
||||
31
README.md
31
README.md
@@ -35,13 +35,12 @@ 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.10+**
|
||||
```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.
|
||||
|
||||
@@ -56,13 +55,29 @@ client = lbc.Client()
|
||||
#### Proxy
|
||||
You can also configure the client to use a proxy by providing a `Proxy` object:
|
||||
```python
|
||||
proxy = lbc.Proxy(
|
||||
host=...,
|
||||
port=...,
|
||||
username=...,
|
||||
password=...
|
||||
# Setup proxy1
|
||||
proxy1 = lbc.Proxy(
|
||||
host="127.0.0.1",
|
||||
port=12345,
|
||||
username="username",
|
||||
password="password",
|
||||
scheme="http"
|
||||
)
|
||||
client = lbc.Client(proxy=proxy)
|
||||
|
||||
# Initialize client with proxy1
|
||||
client = lbc.Client(proxy=proxy1)
|
||||
|
||||
# Setup proxy2
|
||||
proxy2 = lbc.Proxy(
|
||||
host="127.0.0.1",
|
||||
port=23456,
|
||||
)
|
||||
|
||||
# Change client proxy to proxy2
|
||||
client.proxy = proxy2
|
||||
|
||||
# Remove proxy
|
||||
client.proxy = None
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import lbc
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Initialize the Leboncoin API client
|
||||
client = lbc.Client()
|
||||
@@ -18,5 +19,6 @@ def main() -> None:
|
||||
# Print information about the user who posted the ad
|
||||
print("User info:", ad.user)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import lbc
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Initialize the Leboncoin API client
|
||||
client = lbc.Client()
|
||||
@@ -16,5 +17,6 @@ def main() -> None:
|
||||
print("Pro status:", user.is_pro)
|
||||
print("Ads count:", user.total_ads)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
31
examples/proxy.py
Normal file
31
examples/proxy.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import lbc
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Setup proxy1
|
||||
proxy1 = lbc.Proxy(
|
||||
host="127.0.0.1",
|
||||
port=12345,
|
||||
username="username",
|
||||
password="password",
|
||||
scheme="http",
|
||||
)
|
||||
|
||||
# Initialize client with proxy1
|
||||
client = lbc.Client(proxy=proxy1)
|
||||
|
||||
# Setup proxy2
|
||||
proxy2 = lbc.Proxy(
|
||||
host="127.0.0.1",
|
||||
port=23456,
|
||||
)
|
||||
|
||||
# Change client proxy to proxy2
|
||||
client.proxy = proxy2
|
||||
|
||||
# Remove proxy
|
||||
client.proxy = None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import lbc
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Initialize the Leboncoin API client
|
||||
client = lbc.Client()
|
||||
@@ -11,28 +12,29 @@ def main() -> None:
|
||||
lat=48.85994982004764,
|
||||
lng=2.33801967847424,
|
||||
radius=10_000, # 10 km
|
||||
city="Paris"
|
||||
city="Paris",
|
||||
)
|
||||
|
||||
# Perform a search with various filters
|
||||
result = client.search(
|
||||
text="maison", # Search for houses
|
||||
locations=[location], # Only in Paris
|
||||
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
|
||||
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()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import lbc
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Initialize the Leboncoin API client
|
||||
client = lbc.Client()
|
||||
@@ -11,23 +12,23 @@ def main() -> None:
|
||||
lat=48.85994982004764,
|
||||
lng=2.33801967847424,
|
||||
radius=10_000, # 10 km
|
||||
city="Paris"
|
||||
city="Paris",
|
||||
)
|
||||
|
||||
# Perform a search with specific filters
|
||||
result = client.search(
|
||||
text="maison", # Search for houses
|
||||
locations=[location], # Only in Paris
|
||||
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
|
||||
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
|
||||
@@ -39,5 +40,6 @@ def main() -> None:
|
||||
f"Website: {ad.user.pro.website_url or 'N/A'}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import lbc
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Initialize the Leboncoin API client
|
||||
client = lbc.Client()
|
||||
@@ -10,12 +11,13 @@ def main() -> None:
|
||||
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
|
||||
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,20 +1,14 @@
|
||||
[project]
|
||||
name = "lbc"
|
||||
version = "1.1.1"
|
||||
version = "1.1.3"
|
||||
description = "Unofficial client for Leboncoin API"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
requires-python = ">=3.9"
|
||||
|
||||
authors = [
|
||||
{name = "Etienne HODE", email = "hode.etienne@gmail.com"}
|
||||
{ name = "etienne-hd", email = "hode.etienne@gmail.com" }
|
||||
]
|
||||
maintainers = [
|
||||
{name = "Etienne HODE", email = "hode.etienne@gmail.com"}
|
||||
]
|
||||
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"curl_cffi==0.11.3"
|
||||
"curl-cffi>=0.15.0",
|
||||
]
|
||||
|
||||
keywords = ["lbc", "leboncoin", "wrapper", "api"]
|
||||
@@ -25,5 +19,5 @@ Repository = "https://github.com/etienne-hd/lbc"
|
||||
Changelog = "https://github.com/etienne-hd/lbc/blob/main/CHANGELOG.md"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
requires = ["uv_build>=0.11.7,<0.12.0"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
curl_cffi==0.11.3
|
||||
@@ -1,2 +1,29 @@
|
||||
from .client import Client
|
||||
from .model import *
|
||||
from .model import (
|
||||
Proxy,
|
||||
Search,
|
||||
Ad,
|
||||
User,
|
||||
OwnerType,
|
||||
AdType,
|
||||
Sort,
|
||||
Department,
|
||||
Region,
|
||||
Category,
|
||||
City,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Client",
|
||||
"Proxy",
|
||||
"Search",
|
||||
"Ad",
|
||||
"User",
|
||||
"OwnerType",
|
||||
"AdType",
|
||||
"Sort",
|
||||
"Department",
|
||||
"Region",
|
||||
"Category",
|
||||
"City",
|
||||
]
|
||||
|
||||
@@ -1,49 +1,54 @@
|
||||
from typing import Optional, Union
|
||||
from curl_cffi import BrowserTypeLiteral
|
||||
import curl_cffi
|
||||
|
||||
from .mixin import (
|
||||
SessionMixin,
|
||||
SearchMixin,
|
||||
UserMixin,
|
||||
AdMixin
|
||||
)
|
||||
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: float = 30.0, max_retries: int = 5):
|
||||
|
||||
class Client(SessionMixin, SearchMixin, UserMixin, AdMixin):
|
||||
def __init__(
|
||||
self,
|
||||
proxy: Proxy | None = None,
|
||||
impersonate: BrowserTypeLiteral = None,
|
||||
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.
|
||||
|
||||
If no `impersonate` value is provided, a random browser type will be selected among common options.
|
||||
|
||||
|
||||
Args:
|
||||
proxy (Optional[Proxy], optional): Proxy configuration to use for the client. If provided, it will be applied to all requests. Defaults to None.
|
||||
proxy (Proxy | None, optional): Proxy configuration to use for the client. If provided, it will be applied to all requests. Defaults to None.
|
||||
impersonate (BrowserTypeLiteral, optional): Browser type to impersonate for requests (e.g., "firefox", "chrome", "edge", "safari", "safari_ios", "chrome_android"). If None, a random browser type will be chosen.
|
||||
request_verify (bool, optional): Whether to verify SSL certificates when sending requests. Set to False to disable SSL verification (not recommended for production). Defaults to True.
|
||||
timeout (int, optional): Maximum time in seconds to wait for a request before timing out. Defaults to 30.
|
||||
timeout (float, optional): Maximum time in seconds to wait for a request before timing out. Defaults to 30.
|
||||
max_retries (int, optional): Maximum number of times to retry a request in case of failure (403 error). Defaults to 5.
|
||||
"""
|
||||
super().__init__(proxy=proxy, impersonate=impersonate, request_verify=request_verify)
|
||||
|
||||
super().__init__(
|
||||
proxy=proxy, impersonate=impersonate, request_verify=request_verify
|
||||
)
|
||||
|
||||
self.request_verify = request_verify
|
||||
self.timeout = timeout
|
||||
self.max_retries = max_retries
|
||||
|
||||
def _fetch(self, method: str, url: str, payload: Optional[dict] = None, timeout: int = 30, max_retries: int = 5) -> Union[dict, None]:
|
||||
def _fetch(
|
||||
self,
|
||||
method: str,
|
||||
url: str,
|
||||
payload: dict | None = None,
|
||||
max_retries: int = -1,
|
||||
) -> dict:
|
||||
"""
|
||||
Internal method to send an HTTP request using the configured session.
|
||||
|
||||
Args:
|
||||
method (str): HTTP method to use (e.g., "GET", "POST").
|
||||
url (str): Full URL of the API endpoint.
|
||||
payload (Optional[dict], optional): JSON payload to send with the request. Used for POST/PUT methods. Defaults to None.
|
||||
payload (dict | None, optional): JSON payload to send with the request. Used for POST/PUT methods. Defaults to None.
|
||||
timeout (int, optional): Timeout for the request, in seconds. Defaults to 30.
|
||||
max_retries (int, optional): Number of times to retry the request in case of failure. Defaults to 5.
|
||||
|
||||
@@ -54,24 +59,39 @@ class Client(
|
||||
Returns:
|
||||
dict: Parsed JSON response from the server.
|
||||
"""
|
||||
response = self.session.request(
|
||||
if max_retries == -1:
|
||||
max_retries = self.max_retries
|
||||
|
||||
response: curl_cffi.Response = self.session.request(
|
||||
method=method,
|
||||
url=url,
|
||||
url=url,
|
||||
json=payload,
|
||||
timeout=timeout,
|
||||
verify=self.request_verify,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
if response.ok:
|
||||
return response.json()
|
||||
elif response.status_code == 403:
|
||||
if max_retries > 0:
|
||||
self.session = self._init_session(proxy=self._proxy, impersonate=self._impersonate, request_verify=self.request_verify) # Re-init session
|
||||
return self._fetch(method=method, url=url, payload=payload, timeout=timeout, max_retries=max_retries - 1)
|
||||
self.session = self._init_session(
|
||||
proxy=self._proxy,
|
||||
impersonate=self._impersonate,
|
||||
request_verify=self.request_verify,
|
||||
) # Re-init session
|
||||
return self._fetch(
|
||||
method=method, url=url, payload=payload, max_retries=max_retries - 1
|
||||
)
|
||||
if self.proxy:
|
||||
raise DatadomeError(f"Access blocked by Datadome: your proxy appears to have a poor reputation, try to change it.")
|
||||
raise DatadomeError(
|
||||
"Access blocked by Datadome: your proxy appears to have a poor reputation, try to change it."
|
||||
)
|
||||
else:
|
||||
raise DatadomeError(f"Access blocked by Datadome: your activity was flagged as suspicious. Please avoid sending excessive requests.")
|
||||
raise DatadomeError(
|
||||
"Access blocked by Datadome: your activity was flagged as suspicious. Please avoid sending excessive requests."
|
||||
)
|
||||
elif response.status_code == 404 or response.status_code == 410:
|
||||
raise NotFoundError(f"Unable to find ad or user.")
|
||||
raise NotFoundError("Unable to find ad or user.")
|
||||
else:
|
||||
raise RequestError(f"Request failed with status code {response.status_code}.")
|
||||
raise RequestError(
|
||||
f"Request failed with status code {response.status_code}."
|
||||
)
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
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."""
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from .session import SessionMixin
|
||||
from .search import SearchMixin
|
||||
from .user import UserMixin
|
||||
from .ad import AdMixin
|
||||
from .ad import AdMixin
|
||||
|
||||
__all__ = ["SessionMixin", "SearchMixin", "UserMixin", "AdMixin"]
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from typing import Union
|
||||
|
||||
from ..model import Ad
|
||||
|
||||
|
||||
class AdMixin:
|
||||
def get_ad(self, ad_id: Union[str, int]) -> Ad:
|
||||
def get_ad(self, ad_id: str | int) -> Ad:
|
||||
"""
|
||||
Retrieve detailed information about a classified ad using its ID.
|
||||
|
||||
@@ -17,5 +16,8 @@ class AdMixin:
|
||||
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)
|
||||
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,24 +1,27 @@
|
||||
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,
|
||||
url: str | None = None,
|
||||
text: str | None = 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,
|
||||
locations: list[Region, Department, City]
|
||||
| Region
|
||||
| Department
|
||||
| City
|
||||
| None = 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,
|
||||
owner_type: OwnerType | None = None,
|
||||
shippable: bool | None = None,
|
||||
search_in_title_only: bool = False,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> Search:
|
||||
"""
|
||||
Perform a classified ads search on Leboncoin with the specified criteria.
|
||||
@@ -28,7 +31,7 @@ class SearchMixin:
|
||||
- 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.
|
||||
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.
|
||||
@@ -46,15 +49,24 @@ class SearchMixin:
|
||||
Search: A `Search` object containing the parsed search results.
|
||||
"""
|
||||
if url:
|
||||
payload = build_search_payload_with_url(
|
||||
url=url, limit=limit, page=page
|
||||
)
|
||||
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
|
||||
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)
|
||||
body = self._fetch(
|
||||
method="POST", url="https://api.leboncoin.fr/finder/search", payload=payload
|
||||
)
|
||||
return Search._build(raw=body, client=self)
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
from curl_cffi import requests, BrowserTypeLiteral
|
||||
from typing import Optional
|
||||
import random
|
||||
import uuid
|
||||
|
||||
from ..model import Proxy
|
||||
|
||||
|
||||
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)
|
||||
def __init__(
|
||||
self,
|
||||
proxy: Proxy | None = 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)
|
||||
@@ -18,22 +26,90 @@ class SessionMixin:
|
||||
# LBC;<OS>;<OS_VERSION>;<MODEL>;phone;<DEVICE_ID>;wifi;<APP_VERSION>
|
||||
os = random.choice(["iOS", "Android"])
|
||||
if os == "iOS":
|
||||
os_version = random.choice(["18.0", "18.1", "18.2", "18.3", "18.4", "18.5", "18.6", "18.7", "18.7.3",
|
||||
"26.0", "26.1", "26.2"])
|
||||
os_version = random.choice(
|
||||
[
|
||||
"18.0",
|
||||
"18.1",
|
||||
"18.2",
|
||||
"18.3",
|
||||
"18.4",
|
||||
"18.5",
|
||||
"18.6",
|
||||
"18.7",
|
||||
"18.7.3",
|
||||
"26.0",
|
||||
"26.1",
|
||||
"26.2",
|
||||
]
|
||||
)
|
||||
model = "iPhone"
|
||||
device_id = str(uuid.uuid4())
|
||||
app_version = random.choice(["101.45.0", "101.44.0", "101.43.1", "101.43.0", "101.42.1", "101.42.0", "101.41.0", "101.40.0", "101.39.0", "101.38.0"])
|
||||
app_version = random.choice(
|
||||
[
|
||||
"101.45.0",
|
||||
"101.44.0",
|
||||
"101.43.1",
|
||||
"101.43.0",
|
||||
"101.42.1",
|
||||
"101.42.0",
|
||||
"101.41.0",
|
||||
"101.40.0",
|
||||
"101.39.0",
|
||||
"101.38.0",
|
||||
]
|
||||
)
|
||||
else:
|
||||
os_version = random.choice(["11", "12", "13", "14", "15"])
|
||||
model = random.choice(["SM-G991B", "SM-G996B", "SM-G998B", "SM-S911B", "SM-S916B", "SM-S918B", "SM-A505F", "SM-A546B", "SM-A137F", "SM-M336B",
|
||||
"Pixel 5", "Pixel 6", "Pixel 6a", "Pixel 7", "Pixel 7 Pro", "Pixel 8", "Pixel 8 Pro",
|
||||
"Mi 10", "Mi 11", "Mi 11 Lite", "Redmi Note 10", "Redmi Note 11", "Redmi Note 12", "POCO F3", "POCO F4", "POCO X3 Pro",
|
||||
"ONEPLUS A6003", "ONEPLUS A6013", "ONEPLUS A5000", "ONEPLUS A5010", "OnePlus 8", "OnePlus 9", "OnePlus 10 Pro", "OnePlus Nord"])
|
||||
model = random.choice(
|
||||
[
|
||||
"SM-G991B",
|
||||
"SM-G996B",
|
||||
"SM-G998B",
|
||||
"SM-S911B",
|
||||
"SM-S916B",
|
||||
"SM-S918B",
|
||||
"SM-A505F",
|
||||
"SM-A546B",
|
||||
"SM-A137F",
|
||||
"SM-M336B",
|
||||
"Pixel 5",
|
||||
"Pixel 6",
|
||||
"Pixel 6a",
|
||||
"Pixel 7",
|
||||
"Pixel 7 Pro",
|
||||
"Pixel 8",
|
||||
"Pixel 8 Pro",
|
||||
"Mi 10",
|
||||
"Mi 11",
|
||||
"Mi 11 Lite",
|
||||
"Redmi Note 10",
|
||||
"Redmi Note 11",
|
||||
"Redmi Note 12",
|
||||
"POCO F3",
|
||||
"POCO F4",
|
||||
"POCO X3 Pro",
|
||||
"ONEPLUS A6003",
|
||||
"ONEPLUS A6013",
|
||||
"ONEPLUS A5000",
|
||||
"ONEPLUS A5010",
|
||||
"OnePlus 8",
|
||||
"OnePlus 9",
|
||||
"OnePlus 10 Pro",
|
||||
"OnePlus Nord",
|
||||
]
|
||||
)
|
||||
device_id = uuid.uuid4().hex[:16]
|
||||
app_version = random.choice(["100.85.2", "100.84.1", "100.83.1", "100.82.0", "100.81.1"])
|
||||
app_version = random.choice(
|
||||
["100.85.2", "100.84.1", "100.83.1", "100.82.0", "100.81.1"]
|
||||
)
|
||||
return f"LBC;{os};{os_version};{model};phone;{device_id};wifi;{app_version}"
|
||||
|
||||
def _init_session(self, proxy: Optional[Proxy] = None, impersonate: BrowserTypeLiteral = None, request_verify: bool = True) -> requests.Session:
|
||||
def _init_session(
|
||||
self,
|
||||
proxy: Proxy | None = None,
|
||||
impersonate: BrowserTypeLiteral = None,
|
||||
request_verify: bool = True,
|
||||
) -> requests.Session:
|
||||
"""
|
||||
Initializes an HTTP session with optional proxy configuration and browser impersonation.
|
||||
|
||||
@@ -41,64 +117,44 @@ class SessionMixin:
|
||||
|
||||
Args:
|
||||
proxy (Optional[Proxy], optional): Proxy configuration to use for the session. If provided, it will be applied to both HTTP and HTTPS traffic. Defaults to None.
|
||||
impersonate (BrowserTypeLiteral, optional): Browser type to impersonate for requests (e.g., "firefox", "chrome", "edge", "safari", "safari_ios", "chrome_android"). If None, a random browser type will be chosen.
|
||||
impersonate (BrowserTypeLiteral, optional): Browser type to impersonate for requests (e.g., "firefox", "chrome", "edge", "safari", "safari_ios", "chrome_android"). If None, a random browser type will be chosen.
|
||||
request_verify (bool, optional): Whether to verify SSL certificates for HTTPS requests. Defaults to True.
|
||||
|
||||
Returns:
|
||||
requests.Session: A configured session instance ready to send requests.
|
||||
"""
|
||||
if impersonate == None: # Pick a random browser client
|
||||
if impersonate is None: # Pick a random browser client
|
||||
impersonate: BrowserTypeLiteral = random.choice(
|
||||
[
|
||||
"safari",
|
||||
"safari_ios",
|
||||
"chrome_android",
|
||||
"firefox"
|
||||
]
|
||||
["safari", "safari_ios", "chrome_android", "firefox"]
|
||||
)
|
||||
|
||||
session = requests.Session(
|
||||
impersonate=impersonate,
|
||||
)
|
||||
session = requests.Session(impersonate=impersonate)
|
||||
|
||||
session.headers.update(
|
||||
{
|
||||
'User-Agent': self._generate_user_agent(),
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Sec-Fetch-Site': 'same-site',
|
||||
"User-Agent": self._generate_user_agent(),
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "same-site",
|
||||
}
|
||||
)
|
||||
if proxy:
|
||||
session.proxies = {
|
||||
"http": proxy.url,
|
||||
"https": proxy.url
|
||||
}
|
||||
session.proxies = {"http": proxy.url, "https": proxy.url}
|
||||
|
||||
session.get("https://www.leboncoin.fr/", verify=request_verify) # Init cookies
|
||||
session.get("https://www.leboncoin.fr/", verify=request_verify) # Init cookies
|
||||
return session
|
||||
|
||||
@property
|
||||
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) -> Proxy:
|
||||
return self._proxy
|
||||
|
||||
|
||||
@proxy.setter
|
||||
def proxy(self, value: Proxy):
|
||||
if isinstance(value, Proxy):
|
||||
self._session.proxies = {
|
||||
"http": value.url,
|
||||
"https": value.url
|
||||
}
|
||||
if value:
|
||||
if isinstance(value, Proxy):
|
||||
self.session.proxies = {"http": value.url, "https": value.url}
|
||||
else:
|
||||
raise TypeError("Proxy must be an instance of the lbc.Proxy")
|
||||
else:
|
||||
raise TypeError("Proxy must be an instance of the Proxy class")
|
||||
self.session.proxies = {}
|
||||
self._proxy = value
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from ..model import User
|
||||
from ..exceptions import NotFoundError
|
||||
|
||||
|
||||
class UserMixin:
|
||||
def get_user(self, user_id: str) -> User:
|
||||
"""
|
||||
@@ -15,12 +16,18 @@ class UserMixin:
|
||||
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)
|
||||
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":
|
||||
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)
|
||||
pro_data = self._fetch(
|
||||
method="GET",
|
||||
url=f"https://api.leboncoin.fr/api/onlinestores/v2/users/{user_id}?fields=all",
|
||||
)
|
||||
except NotFoundError:
|
||||
pass # Some professional users may not have a Leboncoin page.
|
||||
return User._build(user_data=user_data, pro_data=pro_data)
|
||||
pass # Some professional users may not have a Leboncoin page.
|
||||
return User._build(user_data=user_data, pro_data=pro_data)
|
||||
|
||||
@@ -2,5 +2,19 @@ from .proxy import Proxy
|
||||
from .search import Search
|
||||
from .ad import Ad
|
||||
from .user import User
|
||||
from .enums import *
|
||||
from .city import City
|
||||
from .enums import OwnerType, AdType, Sort, Department, Region, Category
|
||||
from .city import City
|
||||
|
||||
__all__ = [
|
||||
"Proxy",
|
||||
"Search",
|
||||
"Ad",
|
||||
"User",
|
||||
"OwnerType",
|
||||
"AdType",
|
||||
"Sort",
|
||||
"Department",
|
||||
"Region",
|
||||
"Category",
|
||||
"City",
|
||||
]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Any, Optional
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from .user import User
|
||||
|
||||
|
||||
@dataclass
|
||||
class Location:
|
||||
country_id: str
|
||||
@@ -19,17 +20,19 @@ class Location:
|
||||
provider: str
|
||||
is_shape: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class Attribute:
|
||||
key: str
|
||||
key_label: Optional[str]
|
||||
key_label: str | None
|
||||
value: str
|
||||
value_label: str
|
||||
values: List[str]
|
||||
values_label: Optional[List[str]]
|
||||
value_label_reader: Optional[str]
|
||||
values: list[str]
|
||||
values_label: list[str] | None
|
||||
value_label_reader: str | None
|
||||
generic: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class Ad:
|
||||
id: int
|
||||
@@ -45,19 +48,19 @@ class Ad:
|
||||
ad_type: str
|
||||
url: str
|
||||
price: float
|
||||
images: List[str]
|
||||
attributes: List[Attribute]
|
||||
images: list[str]
|
||||
attributes: list[Attribute]
|
||||
location: Location
|
||||
has_phone: bool
|
||||
favorites: int # Unvailaible on Ad from Search
|
||||
favorites: int # Unavailable on Ad from Search
|
||||
|
||||
_client: Any
|
||||
_user_id: str
|
||||
_user: User
|
||||
_client: Any = field(repr=False)
|
||||
_user_id: str = field(repr=False)
|
||||
_user: User = field(repr=False)
|
||||
|
||||
@staticmethod
|
||||
def _build(raw: dict, client: Any) -> "Ad":
|
||||
attributes: List[Attribute] = []
|
||||
attributes: list[Attribute] = []
|
||||
for raw_attribute in raw.get("attributes", []):
|
||||
attributes.append(
|
||||
Attribute(
|
||||
@@ -68,10 +71,10 @@ class Ad:
|
||||
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")
|
||||
generic=raw_attribute.get("generic"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
raw_location: dict = raw.get("location", {})
|
||||
location = Location(
|
||||
country_id=raw_location.get("country_id"),
|
||||
@@ -86,9 +89,9 @@ class Ad:
|
||||
lng=raw_location.get("lng"),
|
||||
source=raw_location.get("source"),
|
||||
provider=raw_location.get("provider"),
|
||||
is_shape=raw_location.get("is_shape")
|
||||
is_shape=raw_location.get("is_shape"),
|
||||
)
|
||||
|
||||
|
||||
raw_owner: dict = raw.get("owner", {})
|
||||
return Ad(
|
||||
id=raw.get("list_id"),
|
||||
@@ -111,15 +114,15 @@ class Ad:
|
||||
favorites=raw.get("counters", {}).get("favorites"),
|
||||
_client=client,
|
||||
_user_id=raw_owner.get("user_id"),
|
||||
_user=None
|
||||
_user=None,
|
||||
)
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
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
|
||||
return self._user
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class City:
|
||||
lat: float
|
||||
lng: float
|
||||
radius: int = 10_000
|
||||
city: Optional[str] = None
|
||||
city: str | None = None
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class OwnerType(Enum):
|
||||
PRO = "pro"
|
||||
PRIVATE = "private"
|
||||
ALL = "all"
|
||||
|
||||
|
||||
class AdType(Enum):
|
||||
OFFER = "offer"
|
||||
DEMAND = "demand"
|
||||
|
||||
|
||||
class Sort(Enum):
|
||||
RELEVANCE = ("relevance", None)
|
||||
NEWEST = ("time", "desc")
|
||||
@@ -16,6 +19,7 @@ class Sort(Enum):
|
||||
EXPENSIVE = ("price", "asc")
|
||||
CHEAPEST = ("price", "desc")
|
||||
|
||||
|
||||
class Department(Enum):
|
||||
BAS_RHIN = ("1", "ALSACE", "67", "BAS_RHIN")
|
||||
HAUT_RHIN = ("1", "ALSACE", "68", "HAUT_RHIN")
|
||||
@@ -97,7 +101,12 @@ class Department(Enum):
|
||||
CHARENTE_MARITIME = ("20", "POITOU_CHARENTES", "17", "CHARENTE_MARITIME")
|
||||
DEUX_SEVRES = ("20", "POITOU_CHARENTES", "79", "DEUX_SEVRES")
|
||||
VIENNE = ("20", "POITOU_CHARENTES", "86", "VIENNE")
|
||||
ALPES_DE_HAUTE_PROVENCE = ("21", "PROVENCE_ALPES_COTE_DAZUR", "4", "ALPES_DE_HAUTE_PROVENCE")
|
||||
ALPES_DE_HAUTE_PROVENCE = (
|
||||
"21",
|
||||
"PROVENCE_ALPES_COTE_DAZUR",
|
||||
"4",
|
||||
"ALPES_DE_HAUTE_PROVENCE",
|
||||
)
|
||||
HAUTES_ALPES = ("21", "PROVENCE_ALPES_COTE_DAZUR", "5", "HAUTES_ALPES")
|
||||
ALPES_MARITIMES = ("21", "PROVENCE_ALPES_COTE_DAZUR", "6", "ALPES_MARITIMES")
|
||||
BOUCHES_DU_RHONE = ("21", "PROVENCE_ALPES_COTE_DAZUR", "13", "BOUCHES_DU_RHONE")
|
||||
@@ -112,6 +121,7 @@ class Department(Enum):
|
||||
SAVOIE = ("22", "RHONE_ALPES", "73", "SAVOIE")
|
||||
HAUTE_SAVOIE = ("22", "RHONE_ALPES", "74", "HAUTE_SAVOIE")
|
||||
|
||||
|
||||
class Region(Enum):
|
||||
ALSACE = ("1", "ALSACE")
|
||||
AQUITAINE = ("2", "AQUITAINE")
|
||||
@@ -148,6 +158,7 @@ class Region(Enum):
|
||||
RHONE_ALPES = ("22", "RHONE_ALPES")
|
||||
REUNION = ("26", "REUNION")
|
||||
|
||||
|
||||
class Category(Enum):
|
||||
TOUTES_CATEGORIES = "0"
|
||||
EMPLOI = "71"
|
||||
@@ -263,4 +274,4 @@ class Category(Enum):
|
||||
SERVICES_AUTRES_SERVICES = "34"
|
||||
DONS = "1000"
|
||||
DIVERS = "37"
|
||||
DIVERS_AUTRES = "38"
|
||||
DIVERS_AUTRES = "38"
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Union, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class Proxy:
|
||||
host: str
|
||||
port: Union[str, int]
|
||||
username: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
scheme: Optional[str] = "http"
|
||||
port: str | int
|
||||
username: str | None = None
|
||||
password: str | None = None
|
||||
scheme: str = "http"
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
if self.username and self.password:
|
||||
return f"{self.scheme}://{self.username}:{self.password}@{self.host}:{self.port}"
|
||||
else:
|
||||
return f"{self.scheme}://{self.host}:{self.port}"
|
||||
return f"{self.scheme}://{self.host}:{self.port}"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Any
|
||||
from typing import Any
|
||||
|
||||
from .ad import Ad
|
||||
|
||||
|
||||
@dataclass
|
||||
class Search:
|
||||
total: int
|
||||
@@ -13,14 +14,11 @@ class Search:
|
||||
total_inactive: int
|
||||
total_shippable: int
|
||||
max_pages: int
|
||||
ads: List[Ad]
|
||||
ads: list[Ad]
|
||||
|
||||
@staticmethod
|
||||
def _build(raw: dict, client: Any) -> "Search":
|
||||
ads: List[Ad] = [
|
||||
Ad._build(raw=ad, client=client)
|
||||
for ad in raw.get("ads", [])
|
||||
]
|
||||
ads: list[Ad] = [Ad._build(raw=ad, client=client) for ad in raw.get("ads", [])]
|
||||
|
||||
return Search(
|
||||
total=raw.get("total"),
|
||||
@@ -32,4 +30,4 @@ class Search:
|
||||
total_shippable=raw.get("total_shippable"),
|
||||
max_pages=raw.get("max_pages"),
|
||||
ads=ads,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class Reply:
|
||||
@@ -9,6 +9,7 @@ class Reply:
|
||||
rate: int
|
||||
reply_time_text: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Presence:
|
||||
status: str
|
||||
@@ -16,11 +17,13 @@ class Presence:
|
||||
last_activity: str
|
||||
enabled: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class Badge:
|
||||
type: str
|
||||
name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Feedback:
|
||||
overall_score: float
|
||||
@@ -39,6 +42,7 @@ class Feedback:
|
||||
def score(self) -> float:
|
||||
return self.overall_score * 5 if self.overall_score else None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Location:
|
||||
address: str
|
||||
@@ -56,6 +60,7 @@ class Location:
|
||||
department_label: str
|
||||
country: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Review:
|
||||
author_name: str
|
||||
@@ -63,6 +68,7 @@ class Review:
|
||||
text: str
|
||||
review_time: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Rating:
|
||||
rating_value: int
|
||||
@@ -71,7 +77,8 @@ class Rating:
|
||||
source_display: str
|
||||
retrieval_time: str
|
||||
url: str
|
||||
reviews: List[Review]
|
||||
reviews: list[Review]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Pro:
|
||||
@@ -93,6 +100,7 @@ class Pro:
|
||||
website_url: str
|
||||
rating: Rating
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
id: str
|
||||
@@ -103,15 +111,15 @@ class User:
|
||||
profile_picture: str
|
||||
reply: Reply
|
||||
presence: Presence
|
||||
badges: List[Badge]
|
||||
badges: list[Badge]
|
||||
total_ads: int
|
||||
store_id: int
|
||||
account_type: str
|
||||
description: str
|
||||
pro: Optional[Pro]
|
||||
pro: Pro | None
|
||||
|
||||
@staticmethod
|
||||
def _build(user_data: dict, pro_data: Optional[dict]) -> "User":
|
||||
def _build(user_data: dict, pro_data: dict | None) -> "User":
|
||||
raw_feedback = user_data.get("feedback", {})
|
||||
feedback = Feedback(
|
||||
overall_score=raw_feedback.get("overall_score"),
|
||||
@@ -120,11 +128,15 @@ class User:
|
||||
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"),
|
||||
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")
|
||||
user_attention=raw_feedback.get("category_scores", {}).get(
|
||||
"USER_ATTENTION"
|
||||
),
|
||||
received_count=raw_feedback.get("received_count"),
|
||||
)
|
||||
|
||||
raw_reply = user_data.get("reply", {})
|
||||
@@ -133,7 +145,7 @@ class User:
|
||||
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")
|
||||
reply_time_text=raw_reply.get("reply_time_text"),
|
||||
)
|
||||
|
||||
raw_presence = user_data.get("presence", {})
|
||||
@@ -141,7 +153,7 @@ class User:
|
||||
status=raw_presence.get("status"),
|
||||
presence_text=raw_presence.get("presence_text"),
|
||||
last_activity=raw_presence.get("last_activity"),
|
||||
enabled=raw_presence.get("enabled")
|
||||
enabled=raw_presence.get("enabled"),
|
||||
)
|
||||
|
||||
badges = [
|
||||
@@ -166,7 +178,7 @@ class User:
|
||||
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")
|
||||
country=raw_pro_location.get("country"),
|
||||
)
|
||||
|
||||
raw_pro_rating = pro_data.get("rating", {})
|
||||
@@ -175,7 +187,7 @@ class User:
|
||||
author_name=review.get("author_name"),
|
||||
rating_value=review.get("rating_value"),
|
||||
text=review.get("text"),
|
||||
review_time=review.get("review_time")
|
||||
review_time=review.get("review_time"),
|
||||
)
|
||||
for review in raw_pro_rating.get("reviews", [])
|
||||
]
|
||||
@@ -187,12 +199,12 @@ class User:
|
||||
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
|
||||
reviews=pro_rating_reviews,
|
||||
)
|
||||
|
||||
|
||||
pro_owner = pro_data.get("owner", {})
|
||||
pro_brand = pro_data.get("brand", {})
|
||||
pro_information = pro_data.get("information", {})
|
||||
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"),
|
||||
@@ -210,7 +222,7 @@ class User:
|
||||
description=pro_information.get("description"),
|
||||
opening_hours=pro_information.get("opening_hours"),
|
||||
website_url=pro_information.get("website_url"),
|
||||
rating=pro_rating
|
||||
rating=pro_rating,
|
||||
)
|
||||
|
||||
return User(
|
||||
@@ -227,9 +239,9 @@ class User:
|
||||
store_id=user_data.get("store_id"),
|
||||
account_type=user_data.get("account_type"),
|
||||
description=user_data.get("description"),
|
||||
pro=pro
|
||||
pro=pro,
|
||||
)
|
||||
|
||||
|
||||
@property
|
||||
def is_pro(self):
|
||||
return self.account_type == "pro"
|
||||
return self.account_type == "pro"
|
||||
|
||||
0
src/lbc/py.typed
Normal file
0
src/lbc/py.typed
Normal file
191
src/lbc/utils.py
191
src/lbc/utils.py
@@ -1,25 +1,18 @@
|
||||
from typing import Optional, Union, List
|
||||
|
||||
from .model import Category, AdType, OwnerType, Sort, Region, Department, City
|
||||
from .exceptions import InvalidValue
|
||||
|
||||
|
||||
def build_search_payload_with_url(
|
||||
url: str,
|
||||
limit: int = 35,
|
||||
limit_alu: int = 3,
|
||||
page: int = 1
|
||||
url: str, limit: int = 35, limit_alu: int = 3, page: int = 1
|
||||
):
|
||||
def build_area(area_values: list[str]) -> dict:
|
||||
area = {
|
||||
"lat": float(area_values[0]),
|
||||
"lng": float(area_values[1])
|
||||
}
|
||||
area = {"lat": float(area_values[0]), "lng": float(area_values[1])}
|
||||
if len(area_values) >= 3:
|
||||
area["default_radius"] = int(area_values[2])
|
||||
if len(area_values) >= 4:
|
||||
area["radius"] = int(area_values[3])
|
||||
return area
|
||||
|
||||
|
||||
payload = {
|
||||
"filters": {},
|
||||
"limit": limit,
|
||||
@@ -27,71 +20,76 @@ def build_search_payload_with_url(
|
||||
"offset": limit * (page - 1),
|
||||
"disable_total": True,
|
||||
"extend": True,
|
||||
"listing_source": "direct-search" if page == 1 else "pagination"
|
||||
}
|
||||
"listing_source": "direct-search" if page == 1 else "pagination",
|
||||
}
|
||||
|
||||
args: List[str] = url.split("?")[1].split("&")
|
||||
args: list[str] = url.split("?")[1].split("&")
|
||||
for arg in args:
|
||||
key, value = arg.split("=") # e.g: real_estate_type 3,4 / square 300-400 / category 9
|
||||
key, value = arg.split(
|
||||
"="
|
||||
) # e.g: real_estate_type 3,4 / square 300-400 / category 9
|
||||
|
||||
match key:
|
||||
case "text":
|
||||
payload["filters"]["keywords"] = {
|
||||
"text": value
|
||||
}
|
||||
payload["filters"]["keywords"] = {"text": value}
|
||||
|
||||
case "category":
|
||||
payload["filters"]["category"] = {
|
||||
"id": value
|
||||
}
|
||||
payload["filters"]["category"] = {"id": value}
|
||||
|
||||
case "locations":
|
||||
payload["filters"]["location"] = {
|
||||
"locations": []
|
||||
}
|
||||
payload["filters"]["location"] = {"locations": []}
|
||||
|
||||
locations = value.split(",")
|
||||
for location in locations:
|
||||
location_parts = location.split("__") # City ['Paris', '48.86023250788424_2.339006433295173_9256'], Department ['d_69'], Region ['r_18'] or Place ['p_give a star if you like it!', '0.1234567891234_-0.1234567891234567_5000_5500']
|
||||
location_parts = location.split(
|
||||
"__"
|
||||
) # City ['Paris', '48.86023250788424_2.339006433295173_9256'], Department ['d_69'], Region ['r_18'] or Place ['p_give a star if you like it!', '0.1234567891234_-0.1234567891234567_5000_5500']
|
||||
|
||||
prefix_parts = location_parts[0].split("_")
|
||||
if len(prefix_parts[0]) == 1: # Department ['d', '1'], Region ['r', '1'], or Place ['p', 'give a star if you like it!']
|
||||
location_id = prefix_parts[1] # Department '1', Region '1' or Place 'give a star if you like it!'
|
||||
if (
|
||||
len(prefix_parts[0]) == 1
|
||||
): # Department ['d', '1'], Region ['r', '1'], or Place ['p', 'give a star if you like it!']
|
||||
location_id = prefix_parts[
|
||||
1
|
||||
] # Department '1', Region '1' or Place 'give a star if you like it!'
|
||||
match prefix_parts[0]:
|
||||
case "d": # Department
|
||||
case "d": # Department
|
||||
payload["filters"]["location"]["locations"].append(
|
||||
{
|
||||
"locationType": "department",
|
||||
"department_id": location_id
|
||||
"department_id": location_id,
|
||||
}
|
||||
)
|
||||
case "r": # Region
|
||||
case "r": # Region
|
||||
payload["filters"]["location"]["locations"].append(
|
||||
{
|
||||
"locationType": "region",
|
||||
"region_id": location_id
|
||||
}
|
||||
)
|
||||
case "p": # Place
|
||||
area_values = location_parts[1].split("_") # lat, lng, default_radius, radius
|
||||
{"locationType": "region", "region_id": location_id}
|
||||
)
|
||||
case "p": # Place
|
||||
area_values = location_parts[1].split(
|
||||
"_"
|
||||
) # lat, lng, default_radius, radius
|
||||
payload["filters"]["location"]["locations"].append(
|
||||
{
|
||||
"locationType": "place",
|
||||
"place": location_id,
|
||||
"label": location_id,
|
||||
"area": build_area(area_values)
|
||||
"area": build_area(area_values),
|
||||
}
|
||||
)
|
||||
)
|
||||
case _:
|
||||
raise InvalidValue(f"Unknown location type: {prefix_parts[0]}")
|
||||
|
||||
else: # City
|
||||
area_values = location_parts[1].split("_") # lat, lng, default_radius, radius
|
||||
raise InvalidValue(
|
||||
f"Unknown location type: {prefix_parts[0]}"
|
||||
)
|
||||
|
||||
else: # City
|
||||
area_values = location_parts[1].split(
|
||||
"_"
|
||||
) # lat, lng, default_radius, radius
|
||||
payload["filters"]["location"]["locations"].append(
|
||||
{
|
||||
"locationType": "city",
|
||||
#"city": location_parts[0],
|
||||
"area": build_area(area_values)
|
||||
# "city": location_parts[0],
|
||||
"area": build_area(area_values),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -108,12 +106,12 @@ def build_search_payload_with_url(
|
||||
if value == "1":
|
||||
payload["filters"]["location"]["shippable"] = True
|
||||
|
||||
case _:
|
||||
if value in ["page"]: # Pass
|
||||
case _:
|
||||
if value in ["page"]: # Pass
|
||||
continue
|
||||
|
||||
|
||||
# Range or Enum
|
||||
elif len(value.split("-")) == 2: # Range
|
||||
elif len(value.split("-")) == 2: # Range
|
||||
range_values = value.split("-", 1)
|
||||
if len(range_values) == 2:
|
||||
min_val, max_val = range_values
|
||||
@@ -143,7 +141,7 @@ def build_search_payload_with_url(
|
||||
if ranges:
|
||||
payload["filters"]["ranges"][key] = ranges
|
||||
|
||||
else: # Enum
|
||||
else: # Enum
|
||||
if not payload["filters"].get("enums"):
|
||||
payload["filters"]["enums"] = {}
|
||||
|
||||
@@ -151,48 +149,43 @@ def build_search_payload_with_url(
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def build_search_payload_with_args(
|
||||
text: Optional[str] = None,
|
||||
text: str | None = 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,
|
||||
locations: list[Region | Department | City]
|
||||
| Region
|
||||
| Department
|
||||
| City
|
||||
| None = None,
|
||||
limit: int = 35,
|
||||
limit_alu: int = 3,
|
||||
page: int = 1,
|
||||
ad_type: AdType = AdType.OFFER,
|
||||
owner_type: Optional[OwnerType] = None,
|
||||
shippable: Optional[bool] = False,
|
||||
owner_type: OwnerType | None = None,
|
||||
shippable: bool | None = False,
|
||||
search_in_title_only: bool = False,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> dict:
|
||||
payload = {
|
||||
"filters": {
|
||||
"category": {
|
||||
"id": category.value
|
||||
},
|
||||
"enums": {
|
||||
"ad_type": [
|
||||
ad_type.value
|
||||
]
|
||||
},
|
||||
"keywords": {
|
||||
"text": text
|
||||
},
|
||||
"location": {}
|
||||
"category": {"id": category.value},
|
||||
"enums": {"ad_type": [ad_type.value]},
|
||||
"keywords": {"text": text},
|
||||
"location": {},
|
||||
},
|
||||
"limit": limit,
|
||||
"limit_alu": limit_alu,
|
||||
"offset": limit * (page - 1),
|
||||
"disable_total": True,
|
||||
"extend": True,
|
||||
"listing_source": "direct-search" if page == 1 else "pagination"
|
||||
}
|
||||
"listing_source": "direct-search" if page == 1 else "pagination",
|
||||
}
|
||||
|
||||
# Text
|
||||
if text:
|
||||
payload["filters"]["keywords"] = {
|
||||
"text": text
|
||||
}
|
||||
payload["filters"]["keywords"] = {"text": text}
|
||||
|
||||
# Owner Type
|
||||
if owner_type:
|
||||
@@ -203,30 +196,25 @@ def build_search_payload_with_args(
|
||||
payload["sort_by"] = sort_by
|
||||
if sort_order:
|
||||
payload["sort_order"] = sort_order
|
||||
|
||||
|
||||
# Location
|
||||
if locations and not isinstance(locations, list):
|
||||
locations = [locations]
|
||||
|
||||
|
||||
if locations:
|
||||
payload["filters"]["location"] = {
|
||||
"locations": []
|
||||
}
|
||||
payload["filters"]["location"] = {"locations": []}
|
||||
for location in locations:
|
||||
match location:
|
||||
case Region():
|
||||
payload["filters"]["location"]["locations"].append(
|
||||
{
|
||||
"locationType": "region",
|
||||
"region_id": location.value[0]
|
||||
}
|
||||
{"locationType": "region", "region_id": location.value[0]}
|
||||
)
|
||||
case Department():
|
||||
payload["filters"]["location"]["locations"].append(
|
||||
{
|
||||
"locationType": "department",
|
||||
"region_id": location.value[0],
|
||||
"department_id": location.value[2]
|
||||
"department_id": location.value[2],
|
||||
}
|
||||
)
|
||||
case City():
|
||||
@@ -235,15 +223,19 @@ def build_search_payload_with_args(
|
||||
"area": {
|
||||
"lat": location.lat,
|
||||
"lng": location.lng,
|
||||
"radius": location.radius
|
||||
"radius": location.radius,
|
||||
},
|
||||
"city": location.city,
|
||||
"label": f"{location.city} (toute la ville)" if location.city else None,
|
||||
"locationType": "city"
|
||||
"label": f"{location.city} (toute la ville)"
|
||||
if location.city
|
||||
else None,
|
||||
"locationType": "city",
|
||||
}
|
||||
)
|
||||
case _:
|
||||
raise InvalidValue("The provided location is invalid. It must be an instance of Region, Department, or City.")
|
||||
raise InvalidValue(
|
||||
"The provided location is invalid. It must be an instance of Region, Department, or City."
|
||||
)
|
||||
|
||||
# Search in title only
|
||||
if text:
|
||||
@@ -256,23 +248,24 @@ def build_search_payload_with_args(
|
||||
if kwargs:
|
||||
for key, value in kwargs.items():
|
||||
if not isinstance(value, (list, tuple)):
|
||||
raise InvalidValue(f"The value of '{key}' must be a list or a tuple.")
|
||||
raise InvalidValue(f"The value of '{key}' must be a list or a tuple.")
|
||||
# Range
|
||||
if all(isinstance(x, int) for x in value):
|
||||
if len(value) <= 1:
|
||||
raise InvalidValue(f"The value of '{key}' must be a list or tuple with at least two elements.")
|
||||
raise InvalidValue(
|
||||
f"The value of '{key}' must be a list or tuple with at least two elements."
|
||||
)
|
||||
|
||||
if not "ranges" in payload["filters"]:
|
||||
if "ranges" not in payload["filters"]:
|
||||
payload["filters"]["ranges"] = {}
|
||||
|
||||
payload["filters"]["ranges"][key] = {
|
||||
"min": value[0],
|
||||
"max": value[1]
|
||||
}
|
||||
payload["filters"]["ranges"][key] = {"min": value[0], "max": value[1]}
|
||||
# Enum
|
||||
elif all(isinstance(x, str) for x in value):
|
||||
payload["filters"]["enums"][key] = value
|
||||
else:
|
||||
raise InvalidValue(f"The value of '{key}' must be a list or tuple containing only integers or only strings.")
|
||||
|
||||
return payload
|
||||
raise InvalidValue(
|
||||
f"The value of '{key}' must be a list or tuple containing only integers or only strings."
|
||||
)
|
||||
|
||||
return payload
|
||||
|
||||
@@ -1,24 +1,47 @@
|
||||
import lbc
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def transform_str(string: str) -> str:
|
||||
return string.strip().replace(" ", "_").replace("-", "_").replace("&", "et").upper().replace("É", "E").replace("È", "E").replace("Ê", "E").replace("Ë", "E").replace("À", "A").replace("Á", "A").replace("Ô", "O").replace(",", "").replace("___", "_").replace("'", "")
|
||||
return (
|
||||
string.strip()
|
||||
.replace(" ", "_")
|
||||
.replace("-", "_")
|
||||
.replace("&", "et")
|
||||
.upper()
|
||||
.replace("É", "E")
|
||||
.replace("È", "E")
|
||||
.replace("Ê", "E")
|
||||
.replace("Ë", "E")
|
||||
.replace("À", "A")
|
||||
.replace("Á", "A")
|
||||
.replace("Ô", "O")
|
||||
.replace(",", "")
|
||||
.replace("___", "_")
|
||||
.replace("'", "")
|
||||
)
|
||||
|
||||
def print_category(category_data: dict, category_name: Optional[str] = None) -> None:
|
||||
|
||||
def print_category(category_data: dict, category_name: str | None = None) -> None:
|
||||
label: str = category_data["label"]
|
||||
category_name: str = transform_str(category_name) if category_name else None
|
||||
label = transform_str(label)
|
||||
print(f'{f"{category_name}_" if category_name else ""}{label} = "{category_data['catId']}"')
|
||||
print(
|
||||
f'{f"{category_name}_" if category_name else ""}{label} = "{category_data["catId"]}"'
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
client = lbc.Client(impersonate="chrome_android")
|
||||
body = client._fetch(method="GET", url="https://api.leboncoin.fr/api/frontend/v1/data/v7/fdata")
|
||||
|
||||
body = client._fetch(
|
||||
method="GET", url="https://api.leboncoin.fr/api/frontend/v1/data/v7/fdata"
|
||||
)
|
||||
|
||||
for category in body["categories"]:
|
||||
print_category(category)
|
||||
if category.get("subcategories", None):
|
||||
for sub_category in category["subcategories"]:
|
||||
print_category(sub_category, category_name=category["label"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -1,21 +1,45 @@
|
||||
import lbc
|
||||
|
||||
|
||||
def transform_str(string: str) -> str:
|
||||
return string.strip().replace(" ", "_").replace("-", "_").replace("&", "et").upper().replace("É", "E").replace("È", "E").replace("Ê", "E").replace("Ë", "E").replace("À", "A").replace("Á", "A").replace("Ô", "O").replace(",", "").replace("___", "_").replace("'", "")
|
||||
return (
|
||||
string.strip()
|
||||
.replace(" ", "_")
|
||||
.replace("-", "_")
|
||||
.replace("&", "et")
|
||||
.upper()
|
||||
.replace("É", "E")
|
||||
.replace("È", "E")
|
||||
.replace("Ê", "E")
|
||||
.replace("Ë", "E")
|
||||
.replace("À", "A")
|
||||
.replace("Á", "A")
|
||||
.replace("Ô", "O")
|
||||
.replace(",", "")
|
||||
.replace("___", "_")
|
||||
.replace("'", "")
|
||||
)
|
||||
|
||||
|
||||
def print_department(department_data: dict, region: dict) -> None:
|
||||
name: str = department_data["name"]
|
||||
name = transform_str(name)
|
||||
print(f'{name} = ("{region['rId']}", "{transform_str(region['rName'])}", "{department_data["dId"]}", "{name}")')
|
||||
print(
|
||||
f'{name} = ("{region["rId"]}", "{transform_str(region["rName"])}", "{department_data["dId"]}", "{name}")'
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
client = lbc.Client(impersonate="chrome_android")
|
||||
body = client._fetch(method="GET", url="https://api.leboncoin.fr/api/frontend/v1/data/v7/fdata")
|
||||
body = client._fetch(
|
||||
method="GET", url="https://api.leboncoin.fr/api/frontend/v1/data/v7/fdata"
|
||||
)
|
||||
|
||||
for region in body["regions"]:
|
||||
if region.get("departments", None):
|
||||
for department in region["departments"]:
|
||||
print_department(department, region=region)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -1,19 +1,41 @@
|
||||
import lbc
|
||||
|
||||
|
||||
def transform_str(string: str) -> str:
|
||||
return string.strip().replace(" ", "_").replace("-", "_").replace("&", "et").upper().replace("É", "E").replace("È", "E").replace("Ê", "E").replace("Ë", "E").replace("À", "A").replace("Á", "A").replace("Ô", "O").replace(",", "").replace("___", "_").replace("'", "")
|
||||
return (
|
||||
string.strip()
|
||||
.replace(" ", "_")
|
||||
.replace("-", "_")
|
||||
.replace("&", "et")
|
||||
.upper()
|
||||
.replace("É", "E")
|
||||
.replace("È", "E")
|
||||
.replace("Ê", "E")
|
||||
.replace("Ë", "E")
|
||||
.replace("À", "A")
|
||||
.replace("Á", "A")
|
||||
.replace("Ô", "O")
|
||||
.replace(",", "")
|
||||
.replace("___", "_")
|
||||
.replace("'", "")
|
||||
)
|
||||
|
||||
|
||||
def print_region(region_data: dict) -> None:
|
||||
name: str = region_data["rName"]
|
||||
name = transform_str(name)
|
||||
print(f'{name} = ("{region_data['rId']}", "{name}")')
|
||||
print(f'{name} = ("{region_data["rId"]}", "{name}")')
|
||||
|
||||
|
||||
def main() -> None:
|
||||
client = lbc.Client(impersonate="chrome_android")
|
||||
body = client._fetch(method="GET", url="https://api.leboncoin.fr/api/frontend/v1/data/v7/fdata")
|
||||
|
||||
body = client._fetch(
|
||||
method="GET", url="https://api.leboncoin.fr/api/frontend/v1/data/v7/fdata"
|
||||
)
|
||||
|
||||
for region in body["regions"]:
|
||||
print_region(region)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
190
uv.lock
generated
Normal file
190
uv.lock
generated
Normal file
@@ -0,0 +1,190 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.4.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curl-cffi"
|
||||
version = "0.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "cffi" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/48/5b/89fcfebd3e5e85134147ac99e9f2b2271165fd4d71984fc65da5f17819b7/curl_cffi-0.15.0.tar.gz", hash = "sha256:ea0c67652bf6893d34ee0f82c944f37e488f6147e9421bef1771cc6545b02ded", size = 196437, upload-time = "2026-04-03T11:12:31.525Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/42/54ddd442c795f30ce5dd4e49f87ce77505958d3777cd96a91567a3975d2a/curl_cffi-0.15.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:bda66404010e9ed743b1b83c20c86f24fe21a9a6873e17479d6e67e29d8ded28", size = 2795267, upload-time = "2026-04-03T11:11:46.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/2d/3915e238579b3c5a92cead5c79130c3b8d20caaba7616cc4d894650e1d6b/curl_cffi-0.15.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a25620d9bf989c9c029a7d1642999c4c265abb0bad811deb2f77b0b5b2b12e5b", size = 2573544, upload-time = "2026-04-03T11:11:47.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/b3/9d2f1057749a1b07ba1989db3c1503ce8bed998310bae9aea2c43aa64f20/curl_cffi-0.15.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:582e570aa2586b96ed47cf4a17586b9a3c462cbe43f780487c3dc245c6ef1527", size = 10515369, upload-time = "2026-04-03T11:11:50.126Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/1d/6d10dded5ce3fd8157e558ebd97d09e551b77a62cdc1c31e93d0a633cee5/curl_cffi-0.15.0-cp310-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:838e48212447d9c81364b04707a5c861daf08f8320f9ecb3406a8919d1d5c3b3", size = 10160045, upload-time = "2026-04-03T11:11:52.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/12/c70b835487ace3b9ba1502631912e3440082b8ae3a162f60b59cb0b6444d/curl_cffi-0.15.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b6c847d86283b07ae69bb72c82eb8a59242277142aa35b89850f89e792a02fc", size = 11090433, upload-time = "2026-04-03T11:11:55.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/0d/78edcc4f71934225db99df68197a107386d59080742fc7bf6bb4d007924f/curl_cffi-0.15.0-cp310-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e5e69eee735f659287e2c84444319d68a1fa68dd37abf228943a4074864283a", size = 10479178, upload-time = "2026-04-03T11:11:57.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/84/1e101c1acb1ea2f0b4992f5c3024f596d8e21db0d53540b9d583f673c4e7/curl_cffi-0.15.0-cp310-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa1323950224db24f4c510d010b3affa02196ca853fb424191fa917a513d3f4b", size = 10317051, upload-time = "2026-04-03T11:12:00.295Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/42/8ef236b22a6c23d096c85a1dc507efe37bfdfc7a2f8a4b34efb590197369/curl_cffi-0.15.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:41f80170ba844009273b2660da1964ec31e99e5719d16b3422ada87177e32e13", size = 11299660, upload-time = "2026-04-03T11:12:02.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/01/56aeb055d962da87a1be0d74c6c644e251c7e88129b5471dc44ac724e678/curl_cffi-0.15.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1977e1e12cfb5c11352cbb74acef1bed24eb7d226dab61ca57c168c21acd4d61", size = 11945049, upload-time = "2026-04-03T11:12:05.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/8c/2abf99a38d6340d66cf0557e0c750ef3f8883dfc5d450087e01c85861343/curl_cffi-0.15.0-cp310-abi3-win_amd64.whl", hash = "sha256:5a0c1896a0d5a5ac1eb89cd24b008d2b718dd1df6fd2f75451b59ca66e49e572", size = 1661649, upload-time = "2026-04-03T11:12:07.948Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/39/dfd54f2240d3a9b96d77bacc62b97813b35e2aa8ecf5cd5013c683f1ba96/curl_cffi-0.15.0-cp310-abi3-win_arm64.whl", hash = "sha256:a6d57f8389273a3a1f94370473c74897467bcc36af0a17336989780c507fa43d", size = 1410741, upload-time = "2026-04-03T11:12:10.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/6a/c24df8a4fc22fa84070dcd94abeba43c15e08cc09e35869565c0bad196fd/curl_cffi-0.15.0-cp313-abi3-android_24_arm64_v8a.whl", hash = "sha256:4682dc38d4336e0eb0b185374db90a760efde63cbea994b4e63f3521d44c4c92", size = 7190427, upload-time = "2026-04-03T11:12:12.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/56/132225cb3491d07cc6adcce5fe395e059bde87c68cff1ef87a31c88c7819/curl_cffi-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:967ad7355bd8e9586f8c2d02eaa99953747549e7ea4a9b25cd53353e6b67fe6d", size = 2795723, upload-time = "2026-04-03T11:12:13.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/8f/f4f83cd303bef7e8f1749512e5dd157e7e5d08b0a36c8211f9640a2757bf/curl_cffi-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7e63539d0d839d0a8c5eacf86229bc68c57803547f35e0db7ee0986328b478c3", size = 2573739, upload-time = "2026-04-03T11:12:15.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/5c/643d65c7fc9acd742876aa55c2d7823c438cb7665810acd2e66c9976c4d9/curl_cffi-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08c799b89740b9bc49c09fbc3d5907f13ac1f845ca52620507ef9466d4639dd5", size = 10521046, upload-time = "2026-04-03T11:12:17.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/0b/9b8037113c93f4c5323096163471fa7c35c7676c3f608eeaf1287cd99d58/curl_cffi-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b7a92767a888ee90147e18964b396d8435ff42737030d6fb00824ffd6094805", size = 11096115, upload-time = "2026-04-03T11:12:19.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/96/fff2fcbd924ef4042e0d67379f751a8a4e3186a91e75e35a4cf218b306ee/curl_cffi-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:829cc357061ecb99cc2d406301f609a039e05665322f5c025ec67c38b0dc49ce", size = 11305346, upload-time = "2026-04-03T11:12:22.151Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/1b/304b253a45ab28691c8c5e8cca1e6cbb9cf8e46dfceae4648dd536f75e73/curl_cffi-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:408d6f14e346841cd889c2e0962832bb235ba3b6749ebf609f347f747da5e60f", size = 11949834, upload-time = "2026-04-03T11:12:24.986Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/ff/4723d92f08259c707a974aba27a08d0a822b9555e35ca581bf18d055a364/curl_cffi-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b624c7ce087bfda967a013ed0a64702a525444e5b6e97d23534d567ccc6525aa", size = 1702771, upload-time = "2026-04-03T11:12:28.201Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/8c/36bbe06d66fa2b765e4a07199f643a59a9cd1a754207a96335402a9520f4/curl_cffi-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0b6c0543b993996670e9e4b78e305a2d60809d5681903ffb5568e21a387434d3", size = 1466312, upload-time = "2026-04-03T11:12:30.054Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lbc"
|
||||
version = "1.1.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "curl-cffi" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "curl-cffi", specifier = ">=0.15.0" }]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "15.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user