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 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,8 @@
1
+ from .lalafo_client import LalafoClient
2
+ from .async_lalafo_client import AsyncLalafoClient
3
+
4
+
5
+ __all__ = [
6
+ 'LalafoClient',
7
+ 'AsyncLalafoClient'
8
+ ]
@@ -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'])