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.
- lalafo_api-0.1.0/PKG-INFO +137 -0
- lalafo_api-0.1.0/README.md +123 -0
- lalafo_api-0.1.0/lalafo_api/__init__.py +22 -0
- lalafo_api-0.1.0/lalafo_api/api/__init__.py +8 -0
- lalafo_api-0.1.0/lalafo_api/api/async_lalafo_client.py +266 -0
- lalafo_api-0.1.0/lalafo_api/api/base_lalafo_client.py +452 -0
- lalafo_api-0.1.0/lalafo_api/api/clients.py +24 -0
- lalafo_api-0.1.0/lalafo_api/api/lalafo_client.py +265 -0
- lalafo_api-0.1.0/lalafo_api/enums.py +39 -0
- lalafo_api-0.1.0/lalafo_api/exceptions.py +36 -0
- lalafo_api-0.1.0/lalafo_api/models/__init__.py +63 -0
- lalafo_api-0.1.0/lalafo_api/models/ad_models.py +100 -0
- lalafo_api-0.1.0/lalafo_api/models/base_models.py +91 -0
- lalafo_api-0.1.0/lalafo_api/models/category_models.py +217 -0
- lalafo_api-0.1.0/lalafo_api/models/filter_models.py +78 -0
- lalafo_api-0.1.0/lalafo_api/models/request_models.py +89 -0
- lalafo_api-0.1.0/lalafo_api/models/response_models.py +216 -0
- lalafo_api-0.1.0/lalafo_api/utils/__init__.py +28 -0
- lalafo_api-0.1.0/lalafo_api/utils/ad_utils.py +62 -0
- lalafo_api-0.1.0/lalafo_api/utils/url_utils.py +224 -0
- lalafo_api-0.1.0/lalafo_api.egg-info/PKG-INFO +137 -0
- lalafo_api-0.1.0/lalafo_api.egg-info/SOURCES.txt +26 -0
- lalafo_api-0.1.0/lalafo_api.egg-info/dependency_links.txt +1 -0
- lalafo_api-0.1.0/lalafo_api.egg-info/requires.txt +2 -0
- lalafo_api-0.1.0/lalafo_api.egg-info/top_level.txt +1 -0
- lalafo_api-0.1.0/pyproject.toml +22 -0
- lalafo_api-0.1.0/setup.cfg +24 -0
|
@@ -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,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'])
|