lalafo-api 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lalafo_api/__init__.py +22 -0
- lalafo_api/api/__init__.py +8 -0
- lalafo_api/api/async_lalafo_client.py +266 -0
- lalafo_api/api/base_lalafo_client.py +452 -0
- lalafo_api/api/clients.py +24 -0
- lalafo_api/api/lalafo_client.py +265 -0
- lalafo_api/enums.py +39 -0
- lalafo_api/exceptions.py +36 -0
- lalafo_api/models/__init__.py +63 -0
- lalafo_api/models/ad_models.py +100 -0
- lalafo_api/models/base_models.py +91 -0
- lalafo_api/models/category_models.py +217 -0
- lalafo_api/models/filter_models.py +78 -0
- lalafo_api/models/request_models.py +89 -0
- lalafo_api/models/response_models.py +216 -0
- lalafo_api/utils/__init__.py +28 -0
- lalafo_api/utils/ad_utils.py +62 -0
- lalafo_api/utils/url_utils.py +224 -0
- lalafo_api-0.1.0.dist-info/METADATA +137 -0
- lalafo_api-0.1.0.dist-info/RECORD +22 -0
- lalafo_api-0.1.0.dist-info/WHEEL +5 -0
- lalafo_api-0.1.0.dist-info/top_level.txt +1 -0
lalafo_api/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Библиотека для работы с API Lalafo
|
|
3
|
+
"""
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
LalafoError, APIError, ValidationError,
|
|
6
|
+
RateLimitError, AuthenticationError, NotFoundError
|
|
7
|
+
)
|
|
8
|
+
from .models import *
|
|
9
|
+
from .enums import Currency, SortBy, FilterKind, AdStatus
|
|
10
|
+
from .api import *
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'LalafoError', 'APIError', 'ValidationError',
|
|
14
|
+
'RateLimitError', 'AuthenticationError', 'NotFoundError',
|
|
15
|
+
|
|
16
|
+
'ClientConfig', 'SearchRequest', 'Currency', 'SortBy', 'FilterKind', 'AdStatus',
|
|
17
|
+
'Category', 'AdCompact', 'AdDetail', 'User', 'Image', 'Filter', 'FilterValue',
|
|
18
|
+
'CategoriesResponse', 'FiltersResponse', 'ParamsFilterResponse',
|
|
19
|
+
'AdDetailsResponse', 'SearchResponse',
|
|
20
|
+
|
|
21
|
+
'LalafoClient', 'AsyncLalafoClient'
|
|
22
|
+
]
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Optional, Dict, Any, List
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from ..enums import Currency
|
|
7
|
+
from ..models import (
|
|
8
|
+
ClientConfig, SearchRequest, CategoriesResponse,
|
|
9
|
+
FiltersResponse, ParamsFilterResponse, AdDetailsResponse,
|
|
10
|
+
SearchResponse, AdCompact
|
|
11
|
+
)
|
|
12
|
+
from ..utils import parse_ad_url, parse_search_url
|
|
13
|
+
from ..exceptions import APIError
|
|
14
|
+
from .base_lalafo_client import BaseLalafoClient
|
|
15
|
+
from .clients import get_async_client
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AsyncLalafoClient(BaseLalafoClient):
|
|
19
|
+
"""Асинхронный клиент Lalafo API"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
config: Optional[ClientConfig] = None,
|
|
24
|
+
logger: Optional[logging.Logger] = None
|
|
25
|
+
):
|
|
26
|
+
super().__init__(config, logger)
|
|
27
|
+
|
|
28
|
+
async def _make_request(
|
|
29
|
+
self,
|
|
30
|
+
method: str,
|
|
31
|
+
endpoint: str,
|
|
32
|
+
params: Optional[Dict[str, Any]] = None,
|
|
33
|
+
headers: Optional[Dict[str, str]] = None,
|
|
34
|
+
retries: int = None
|
|
35
|
+
) -> Dict[str, Any]:
|
|
36
|
+
retries = retries or self.config.max_retries
|
|
37
|
+
headers = self._prepare_headers(headers)
|
|
38
|
+
|
|
39
|
+
for attempt in range(retries):
|
|
40
|
+
try:
|
|
41
|
+
self._log_request(method, endpoint, params)
|
|
42
|
+
|
|
43
|
+
async with get_async_client() as client:
|
|
44
|
+
response = await client.request(
|
|
45
|
+
method=method,
|
|
46
|
+
url=endpoint,
|
|
47
|
+
params=params,
|
|
48
|
+
headers=headers
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
self._log_response(response)
|
|
52
|
+
return self._handle_response(response)
|
|
53
|
+
|
|
54
|
+
except httpx.TimeoutException as e:
|
|
55
|
+
self.logger.warning(f"Таймаут запроса (попытка {attempt + 1}/{retries}): {e}")
|
|
56
|
+
if attempt == retries - 1:
|
|
57
|
+
raise APIError(f"Таймаут запроса после {retries} попыток", 408)
|
|
58
|
+
await asyncio.sleep(self.config.retry_delay * (attempt + 1))
|
|
59
|
+
|
|
60
|
+
except httpx.RequestError as e:
|
|
61
|
+
self.logger.error(f"Ошибка запроса: {e}")
|
|
62
|
+
if attempt == retries - 1:
|
|
63
|
+
raise APIError(f"Ошибка сети: {str(e)}", 0)
|
|
64
|
+
await asyncio.sleep(self.config.retry_delay * (attempt + 1))
|
|
65
|
+
|
|
66
|
+
async def get_categories_tree(self, with_duplicates: bool = True) -> CategoriesResponse:
|
|
67
|
+
endpoint = "/v3/categories/tree"
|
|
68
|
+
params = {"with_duplicates": 1 if with_duplicates else 0}
|
|
69
|
+
|
|
70
|
+
data = await self._make_request("GET", endpoint, params=params)
|
|
71
|
+
return CategoriesResponse.from_api_response(data)
|
|
72
|
+
|
|
73
|
+
async def get_filters(
|
|
74
|
+
self,
|
|
75
|
+
category_id: Optional[int] = None,
|
|
76
|
+
city_id: Optional[int] = None,
|
|
77
|
+
currency: Currency = Currency.KGS,
|
|
78
|
+
sort_by: str = "default",
|
|
79
|
+
with_filters: bool = True,
|
|
80
|
+
with_empty_values: bool = True,
|
|
81
|
+
with_category_counter: bool = True,
|
|
82
|
+
parameters: Optional[Dict[str, List[int]]] = None,
|
|
83
|
+
price_from: Optional[int] = None,
|
|
84
|
+
price_to: Optional[int] = None
|
|
85
|
+
) -> FiltersResponse:
|
|
86
|
+
endpoint = "/v3/ads/filters"
|
|
87
|
+
|
|
88
|
+
params = {
|
|
89
|
+
"with_filters": str(with_filters).lower(),
|
|
90
|
+
"with_empty_values": str(with_empty_values).lower(),
|
|
91
|
+
"with_category_counter": str(with_category_counter).lower(),
|
|
92
|
+
"currency": currency.value,
|
|
93
|
+
"sort_by": sort_by
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if category_id:
|
|
97
|
+
params["category_id"] = category_id
|
|
98
|
+
if city_id:
|
|
99
|
+
params["city_id"] = city_id
|
|
100
|
+
if price_from is not None:
|
|
101
|
+
params["price[from]"] = price_from
|
|
102
|
+
if price_to is not None:
|
|
103
|
+
params["price[to]"] = price_to
|
|
104
|
+
|
|
105
|
+
if parameters:
|
|
106
|
+
for param_id, values in parameters.items():
|
|
107
|
+
for i, value_id in enumerate(values):
|
|
108
|
+
params[f"parameters[{param_id}][{i}]"] = value_id
|
|
109
|
+
|
|
110
|
+
data = await self._make_request("GET", endpoint, params=params)
|
|
111
|
+
return FiltersResponse(**data)
|
|
112
|
+
|
|
113
|
+
async def get_params_filter(
|
|
114
|
+
self,
|
|
115
|
+
category_id: int,
|
|
116
|
+
city_id: Optional[int] = None,
|
|
117
|
+
radius: int = 0,
|
|
118
|
+
location_lat: Optional[float] = None,
|
|
119
|
+
location_lng: Optional[float] = None
|
|
120
|
+
) -> ParamsFilterResponse:
|
|
121
|
+
endpoint = "/v3/params/filter"
|
|
122
|
+
|
|
123
|
+
params = {
|
|
124
|
+
"category_id": category_id,
|
|
125
|
+
"radius": radius
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if city_id:
|
|
129
|
+
params["city_id"] = city_id
|
|
130
|
+
if location_lat:
|
|
131
|
+
params["location[lat]"] = location_lat
|
|
132
|
+
if location_lng:
|
|
133
|
+
params["location[lng]"] = location_lng
|
|
134
|
+
|
|
135
|
+
data = await self._make_request("GET", endpoint, params=params)
|
|
136
|
+
return ParamsFilterResponse.from_api_response(data)
|
|
137
|
+
|
|
138
|
+
async def get_ad_details(self, ad_id: int) -> AdDetailsResponse:
|
|
139
|
+
endpoint = f"/v3/ads/details/{ad_id}"
|
|
140
|
+
|
|
141
|
+
data = await self._make_request("GET", endpoint)
|
|
142
|
+
return AdDetailsResponse.from_api_response(data)
|
|
143
|
+
|
|
144
|
+
async def search_ads(self, search_request: SearchRequest) -> SearchResponse:
|
|
145
|
+
endpoint = "/v3/ads/search"
|
|
146
|
+
params = search_request.to_query_params()
|
|
147
|
+
|
|
148
|
+
data = await self._make_request("GET", endpoint, params=params)
|
|
149
|
+
return SearchResponse.from_api_response(data)
|
|
150
|
+
|
|
151
|
+
async def search_ads_all_pages(
|
|
152
|
+
self,
|
|
153
|
+
search_request: SearchRequest,
|
|
154
|
+
max_pages: int = 100,
|
|
155
|
+
delay_between_pages: float = 0.5
|
|
156
|
+
) -> List[AdCompact]:
|
|
157
|
+
all_items = []
|
|
158
|
+
current_page = search_request.page
|
|
159
|
+
|
|
160
|
+
for page_num in range(max_pages):
|
|
161
|
+
search_request.page = current_page
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
response = await self.search_ads(search_request)
|
|
165
|
+
all_items.extend(response.items)
|
|
166
|
+
|
|
167
|
+
if not response.has_next_page:
|
|
168
|
+
break
|
|
169
|
+
|
|
170
|
+
current_page += 1
|
|
171
|
+
|
|
172
|
+
if delay_between_pages > 0:
|
|
173
|
+
await asyncio.sleep(delay_between_pages)
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
return all_items
|
|
179
|
+
|
|
180
|
+
async def search_ads_simple(
|
|
181
|
+
self,
|
|
182
|
+
category_id: Optional[int] = None,
|
|
183
|
+
city_id: Optional[int] = None,
|
|
184
|
+
q: Optional[str] = None,
|
|
185
|
+
price_from: Optional[int] = None,
|
|
186
|
+
price_to: Optional[int] = None,
|
|
187
|
+
page: int = 1,
|
|
188
|
+
per_page: int = 40
|
|
189
|
+
) -> SearchResponse:
|
|
190
|
+
search_request = SearchRequest(
|
|
191
|
+
category_id=category_id,
|
|
192
|
+
city_id=city_id,
|
|
193
|
+
q=q,
|
|
194
|
+
price_from=price_from,
|
|
195
|
+
price_to=price_to,
|
|
196
|
+
page=page,
|
|
197
|
+
per_page=per_page
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return await self.search_ads(search_request)
|
|
201
|
+
|
|
202
|
+
async def get_user_ads(
|
|
203
|
+
self,
|
|
204
|
+
user_id: int,
|
|
205
|
+
page: int = 1,
|
|
206
|
+
per_page: int = 40
|
|
207
|
+
) -> SearchResponse:
|
|
208
|
+
search_request = SearchRequest(
|
|
209
|
+
user_id=user_id,
|
|
210
|
+
page=page,
|
|
211
|
+
per_page=per_page
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return await self.search_ads(search_request)
|
|
215
|
+
|
|
216
|
+
async def get_related_ads(
|
|
217
|
+
self,
|
|
218
|
+
ad_id: int,
|
|
219
|
+
category_id: int,
|
|
220
|
+
city_id: int,
|
|
221
|
+
per_page: int = 40,
|
|
222
|
+
vip_count: int = 2
|
|
223
|
+
) -> SearchResponse:
|
|
224
|
+
endpoint = f"/v3/ads/related-items/{ad_id}"
|
|
225
|
+
|
|
226
|
+
params = {
|
|
227
|
+
"category_id": category_id,
|
|
228
|
+
"city_id": city_id,
|
|
229
|
+
"per-page": per_page,
|
|
230
|
+
"vip-count": vip_count,
|
|
231
|
+
"with_feed_banner": "true"
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
data = await self._make_request("GET", endpoint, params=params)
|
|
235
|
+
return SearchResponse.from_api_response(data)
|
|
236
|
+
|
|
237
|
+
async def get_feed_elements(self, category_id: int) -> Dict[str, Any]:
|
|
238
|
+
endpoint = "/v3/feed-elements/scheme"
|
|
239
|
+
params = {"category_id": category_id}
|
|
240
|
+
|
|
241
|
+
return await self._make_request("GET", endpoint, params=params)
|
|
242
|
+
|
|
243
|
+
async def get_user_status(self, user_ids: List[int]) -> List[Dict[str, Any]]:
|
|
244
|
+
endpoint = "/v3/users/get-user-status"
|
|
245
|
+
|
|
246
|
+
params = {}
|
|
247
|
+
for i, user_id in enumerate(user_ids):
|
|
248
|
+
params[f"userIds[{i}]"] = user_id
|
|
249
|
+
|
|
250
|
+
return await self._make_request("GET", endpoint, params=params)
|
|
251
|
+
|
|
252
|
+
async def fetch_ad_from_url(self, url: str) -> AdDetailsResponse:
|
|
253
|
+
parsed = parse_ad_url(url)
|
|
254
|
+
if not parsed or 'ad_id' not in parsed:
|
|
255
|
+
raise ValueError(f"Не удалось извлечь ID объявления из URL: {url}")
|
|
256
|
+
|
|
257
|
+
return await self.get_ad_details(parsed['ad_id'])
|
|
258
|
+
|
|
259
|
+
async def get_search_request_from_url(
|
|
260
|
+
self, url: str,
|
|
261
|
+
currency: Currency = Currency.KGS
|
|
262
|
+
) -> Optional[SearchRequest]:
|
|
263
|
+
parsed = parse_search_url(url, (await self.get_filters(currency = currency)).filters[0].cities, await self.get_categories_tree())
|
|
264
|
+
if not parsed:
|
|
265
|
+
return None
|
|
266
|
+
return SearchRequest(**parsed['search_params'])
|