lalafo-api 0.1.0__tar.gz

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.
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: lalafo_api
3
+ Version: 0.1.0
4
+ Summary: Библиотека для работы с API Lalafo
5
+ Author: Umar
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://github.com/Umar151515/lalafo-api
8
+ Project-URL: Repository, https://github.com/Umar151515/lalafo-api
9
+ Platform: any
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: httpx<1.0.0
13
+ Requires-Dist: pydantic>=2.0
14
+
15
+ # Документация Lalafo API
16
+
17
+ Этот код был написан в крайне сжатые сроки (буквально за 2 дня). Изначально стоял выбор: потратить месяц на попытку написать «идеально» или быстро «навайбкодить» рабочее решение. Был выбран второй вариант, однако в дальнейшем была проведена работа над ошибками, чтобы исправить структуру и привести библиотеку к нормальной архитектуре.
18
+
19
+ Библиотека предоставляет удобный интерфейс для взаимодействия с API Lalafo, поддерживая как синхронный, так и асинхронный режимы работы.
20
+
21
+ Установка: pip install lalafo-api
22
+
23
+ ## Основные возможности
24
+ * Синхронный (`LalafoClient`) и асинхронный (`AsyncLalafoClient`) клиенты.
25
+ * Полное покрытие моделей данных (объявления, категории, фильтры, пользователи).
26
+ * Удобный поиск с поддержкой пагинации и фильтров.
27
+ * Инструменты для парсинга данных напрямую из URL сайта Lalafo.
28
+ * Автоматическая обработка ошибок API.
29
+
30
+ ## Быстрый старт
31
+
32
+ ### Синхронный клиент
33
+ ```python
34
+ from lalafo import LalafoClient, SearchRequest
35
+
36
+ client = LalafoClient()
37
+
38
+ # Поиск объявлений
39
+ request = SearchRequest(q="iPhone 15", price_to=100000)
40
+ results = client.search_ads(request)
41
+
42
+ for item in results.items:
43
+ print(f"{item.title} - {item.price} {item.currency}")
44
+ ```
45
+
46
+ ### Асинхронный клиент
47
+ ```python
48
+ import asyncio
49
+ from lalafo import AsyncLalafoClient
50
+
51
+ async def main():
52
+ client = AsyncLalafoClient()
53
+ ad = await client.get_ad_details(ad_id=12345678)
54
+ print(ad.ad.title)
55
+ print(ad.ad.description)
56
+
57
+ asyncio.run(main())
58
+ ```
59
+
60
+ ## Архитектура библиотеки
61
+
62
+ ### 1. Клиенты (api/)
63
+ Библиотека использует два основных класса:
64
+ * **LalafoClient**: использует `httpx` для блокирующих запросов. Подходит для простых скриптов.
65
+ * **AsyncLalafoClient**: использует асинхронный `httpx`. Рекомендуется для ботов и веб-приложений.
66
+
67
+ Оба клиента поддерживают автоматические повторы запросов (retries) при таймаутах, которые настраиваются через `ClientConfig`.
68
+
69
+ ### 2. Конфигурация (ClientConfig)
70
+ Вы можете настроить заголовки, версию приложения и параметры сети:
71
+ ```python
72
+ from lalafo import ClientConfig, LalafoClient
73
+
74
+ config = ClientConfig(
75
+ timeout=10.0,
76
+ max_retries=5,
77
+ language="ru_RU",
78
+ user_agent="Custom Agent 1.0"
79
+ )
80
+ client = LalafoClient(config=config)
81
+ ```
82
+
83
+ ### 3. Поиск и фильтрация (SearchRequest)
84
+ Класс `SearchRequest` инкапсулирует все параметры поиска:
85
+ * `q`: текстовый запрос.
86
+ * `category_id`: ID категории.
87
+ * `city_id`: ID города.
88
+ * `price_from` / `price_to`: диапазон цен.
89
+ * `parameters`: словарь дополнительных параметров.
90
+
91
+ ### 4. Модели данных (models/)
92
+ Все ответы API преобразуются в Pydantic-модели. Это дает подсказки в IDE и гарантирует корректность данных.
93
+ * **AdCompact**: краткая информация в результатах поиска.
94
+ * **AdDetail**: полная информация об объявлении (включая все параметры и статус).
95
+ * **Category**: модель дерева категорий.
96
+ * **SearchResponse**: содержит список элементов, метаданные пагинации и ссылки на следующую/предыдущую страницы.
97
+
98
+ ### 5. Обработка исключений (exceptions.py)
99
+ Библиотека генерирует специфические исключения:
100
+ * `APIError`: базовая ошибка сервера.
101
+ * `RateLimitError`: (429) превышен лимит запросов.
102
+ * `AuthenticationError`: (401) ошибка доступа.
103
+ * `NotFoundError`: (404) ресурс не найден.
104
+ * `ValidationError`: ошибка во входных данных.
105
+
106
+ ## Утилиты (utils/)
107
+
108
+ Библиотека умеет превращать обычные ссылки с сайта Lalafo в объекты, понятные коду:
109
+
110
+ * **fetch_ad_from_url(url)**: принимает ссылку на объявление и возвращает его детали.
111
+ * **get_search_request_from_url(url)**: анализирует ссылку поиска на сайте и создает готовый объект `SearchRequest` с примененными фильтрами (работает криво).
112
+
113
+ Пример:
114
+ ```python
115
+ # Получаем объект запроса из ссылки поиска
116
+ request = client.get_search_request_from_url("https://lalafo.kg/bishkek/mobilnye-telefony-i-aksessuary/mobilnye-telefony?price[to]=2000")
117
+ # Выполняем поиск по этому запросу
118
+ results = client.search_ads(request)
119
+ ```
120
+
121
+ ## Работа с категориями
122
+ Вы можете получить полное дерево категорий и перемещаться по нему:
123
+ ```python
124
+ tree = client.get_categories_tree()
125
+
126
+ # Поиск категории по ID
127
+ category = tree.find_by_id(123)
128
+
129
+ # Получение плоского списка всех подкатегорий
130
+ flat_list = tree.flatten()
131
+ ```
132
+
133
+ ## Информация о пагинации
134
+ `SearchResponse` предоставляет удобный доступ к навигации:
135
+ * `response.has_next_page`: есть ли еще результаты.
136
+ * `response.meta.totalCount`: общее количество найденных объявлений.
137
+ * Метод `search_ads_all_pages` в клиенте позволяет автоматически собрать объявления с нескольких страниц подряд.
@@ -0,0 +1,123 @@
1
+ # Документация Lalafo API
2
+
3
+ Этот код был написан в крайне сжатые сроки (буквально за 2 дня). Изначально стоял выбор: потратить месяц на попытку написать «идеально» или быстро «навайбкодить» рабочее решение. Был выбран второй вариант, однако в дальнейшем была проведена работа над ошибками, чтобы исправить структуру и привести библиотеку к нормальной архитектуре.
4
+
5
+ Библиотека предоставляет удобный интерфейс для взаимодействия с API Lalafo, поддерживая как синхронный, так и асинхронный режимы работы.
6
+
7
+ Установка: pip install lalafo-api
8
+
9
+ ## Основные возможности
10
+ * Синхронный (`LalafoClient`) и асинхронный (`AsyncLalafoClient`) клиенты.
11
+ * Полное покрытие моделей данных (объявления, категории, фильтры, пользователи).
12
+ * Удобный поиск с поддержкой пагинации и фильтров.
13
+ * Инструменты для парсинга данных напрямую из URL сайта Lalafo.
14
+ * Автоматическая обработка ошибок API.
15
+
16
+ ## Быстрый старт
17
+
18
+ ### Синхронный клиент
19
+ ```python
20
+ from lalafo import LalafoClient, SearchRequest
21
+
22
+ client = LalafoClient()
23
+
24
+ # Поиск объявлений
25
+ request = SearchRequest(q="iPhone 15", price_to=100000)
26
+ results = client.search_ads(request)
27
+
28
+ for item in results.items:
29
+ print(f"{item.title} - {item.price} {item.currency}")
30
+ ```
31
+
32
+ ### Асинхронный клиент
33
+ ```python
34
+ import asyncio
35
+ from lalafo import AsyncLalafoClient
36
+
37
+ async def main():
38
+ client = AsyncLalafoClient()
39
+ ad = await client.get_ad_details(ad_id=12345678)
40
+ print(ad.ad.title)
41
+ print(ad.ad.description)
42
+
43
+ asyncio.run(main())
44
+ ```
45
+
46
+ ## Архитектура библиотеки
47
+
48
+ ### 1. Клиенты (api/)
49
+ Библиотека использует два основных класса:
50
+ * **LalafoClient**: использует `httpx` для блокирующих запросов. Подходит для простых скриптов.
51
+ * **AsyncLalafoClient**: использует асинхронный `httpx`. Рекомендуется для ботов и веб-приложений.
52
+
53
+ Оба клиента поддерживают автоматические повторы запросов (retries) при таймаутах, которые настраиваются через `ClientConfig`.
54
+
55
+ ### 2. Конфигурация (ClientConfig)
56
+ Вы можете настроить заголовки, версию приложения и параметры сети:
57
+ ```python
58
+ from lalafo import ClientConfig, LalafoClient
59
+
60
+ config = ClientConfig(
61
+ timeout=10.0,
62
+ max_retries=5,
63
+ language="ru_RU",
64
+ user_agent="Custom Agent 1.0"
65
+ )
66
+ client = LalafoClient(config=config)
67
+ ```
68
+
69
+ ### 3. Поиск и фильтрация (SearchRequest)
70
+ Класс `SearchRequest` инкапсулирует все параметры поиска:
71
+ * `q`: текстовый запрос.
72
+ * `category_id`: ID категории.
73
+ * `city_id`: ID города.
74
+ * `price_from` / `price_to`: диапазон цен.
75
+ * `parameters`: словарь дополнительных параметров.
76
+
77
+ ### 4. Модели данных (models/)
78
+ Все ответы API преобразуются в Pydantic-модели. Это дает подсказки в IDE и гарантирует корректность данных.
79
+ * **AdCompact**: краткая информация в результатах поиска.
80
+ * **AdDetail**: полная информация об объявлении (включая все параметры и статус).
81
+ * **Category**: модель дерева категорий.
82
+ * **SearchResponse**: содержит список элементов, метаданные пагинации и ссылки на следующую/предыдущую страницы.
83
+
84
+ ### 5. Обработка исключений (exceptions.py)
85
+ Библиотека генерирует специфические исключения:
86
+ * `APIError`: базовая ошибка сервера.
87
+ * `RateLimitError`: (429) превышен лимит запросов.
88
+ * `AuthenticationError`: (401) ошибка доступа.
89
+ * `NotFoundError`: (404) ресурс не найден.
90
+ * `ValidationError`: ошибка во входных данных.
91
+
92
+ ## Утилиты (utils/)
93
+
94
+ Библиотека умеет превращать обычные ссылки с сайта Lalafo в объекты, понятные коду:
95
+
96
+ * **fetch_ad_from_url(url)**: принимает ссылку на объявление и возвращает его детали.
97
+ * **get_search_request_from_url(url)**: анализирует ссылку поиска на сайте и создает готовый объект `SearchRequest` с примененными фильтрами (работает криво).
98
+
99
+ Пример:
100
+ ```python
101
+ # Получаем объект запроса из ссылки поиска
102
+ request = client.get_search_request_from_url("https://lalafo.kg/bishkek/mobilnye-telefony-i-aksessuary/mobilnye-telefony?price[to]=2000")
103
+ # Выполняем поиск по этому запросу
104
+ results = client.search_ads(request)
105
+ ```
106
+
107
+ ## Работа с категориями
108
+ Вы можете получить полное дерево категорий и перемещаться по нему:
109
+ ```python
110
+ tree = client.get_categories_tree()
111
+
112
+ # Поиск категории по ID
113
+ category = tree.find_by_id(123)
114
+
115
+ # Получение плоского списка всех подкатегорий
116
+ flat_list = tree.flatten()
117
+ ```
118
+
119
+ ## Информация о пагинации
120
+ `SearchResponse` предоставляет удобный доступ к навигации:
121
+ * `response.has_next_page`: есть ли еще результаты.
122
+ * `response.meta.totalCount`: общее количество найденных объявлений.
123
+ * Метод `search_ads_all_pages` в клиенте позволяет автоматически собрать объявления с нескольких страниц подряд.
@@ -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'])