jsonseo 1.0.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.
- jsonseo-1.0.0/LICENSE +21 -0
- jsonseo-1.0.0/PKG-INFO +511 -0
- jsonseo-1.0.0/README.md +483 -0
- jsonseo-1.0.0/jsonseo/__init__.py +45 -0
- jsonseo-1.0.0/jsonseo/client.py +394 -0
- jsonseo-1.0.0/jsonseo/errors.py +127 -0
- jsonseo-1.0.0/jsonseo/py.typed +0 -0
- jsonseo-1.0.0/jsonseo/transport.py +107 -0
- jsonseo-1.0.0/jsonseo/types.py +328 -0
- jsonseo-1.0.0/jsonseo.egg-info/PKG-INFO +511 -0
- jsonseo-1.0.0/jsonseo.egg-info/SOURCES.txt +17 -0
- jsonseo-1.0.0/jsonseo.egg-info/dependency_links.txt +1 -0
- jsonseo-1.0.0/jsonseo.egg-info/top_level.txt +1 -0
- jsonseo-1.0.0/pyproject.toml +40 -0
- jsonseo-1.0.0/setup.cfg +4 -0
- jsonseo-1.0.0/tests/test_client.py +231 -0
- jsonseo-1.0.0/tests/test_errors.py +116 -0
- jsonseo-1.0.0/tests/test_retry.py +212 -0
- jsonseo-1.0.0/tests/test_transport.py +258 -0
jsonseo-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JSON SEO
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
jsonseo-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jsonseo
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Официальный Python SDK для JSON SEO API: выдача Яндекса, Google и Bing, Вордстат, прогноз Директа и геолокация по IP
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://jsonseo.ru
|
|
7
|
+
Project-URL: Documentation, https://jsonseo.ru/docs
|
|
8
|
+
Project-URL: Repository, https://github.com/jsonseo/python-sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/jsonseo/python-sdk/issues
|
|
10
|
+
Keywords: jsonseo,seo,serp,yandex,google,bing,wordstat,api
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# JSON SEO Python SDK
|
|
30
|
+
|
|
31
|
+
Официальный Python-клиент [JSON SEO API](https://jsonseo.ru): выдача Яндекса, Google и Bing, картинки и видео, поисковые подсказки, Яндекс Вордстат, прогноз показов Директа и геолокация по IP.
|
|
32
|
+
|
|
33
|
+
- Работает на **Python 3.8 и выше**.
|
|
34
|
+
- Без зависимостей: только стандартная библиотека.
|
|
35
|
+
- Типы в комплекте (`py.typed`), редактор подсказывает поля ответов.
|
|
36
|
+
- Двадцать один метод сервиса.
|
|
37
|
+
- Три попытки на запрос по умолчанию: если сервис затупил, SDK сходит ещё раз сам.
|
|
38
|
+
|
|
39
|
+
## Установка
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install jsonseo
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Ключ берётся в [личном кабинете](https://jsonseo.ru).
|
|
46
|
+
|
|
47
|
+
## Быстрый старт
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from jsonseo import Client
|
|
51
|
+
|
|
52
|
+
client = Client("ВАШ_КЛЮЧ")
|
|
53
|
+
|
|
54
|
+
serp = client.yandex("купить ноутбук", region=213)
|
|
55
|
+
|
|
56
|
+
for position, result in enumerate(serp["results"], start=1):
|
|
57
|
+
print(position, result["domain"], "—", result["title"])
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Первый аргумент — основной параметр метода, остальное передаётся по имени:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
client.yandex("купить ноутбук", region=213, pages=2)
|
|
64
|
+
client.geoip("77.88.55.242")
|
|
65
|
+
client.wordstat_frequency("ремонт айфона", kind="exact")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
# Примеры запросов
|
|
71
|
+
|
|
72
|
+
## Позиции сайта в Яндексе
|
|
73
|
+
|
|
74
|
+
`break_domain` останавливает сбор на нужном домене — платить за страницы ниже найденной позиции незачем.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
serp = client.yandex(
|
|
78
|
+
"ремонт айфона",
|
|
79
|
+
region=213, # Москва
|
|
80
|
+
pages=10, # до 100 позиций
|
|
81
|
+
break_domain="example.com",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
for position, result in enumerate(serp["results"], start=1):
|
|
85
|
+
if result["domain"].endswith("example.com"):
|
|
86
|
+
print("Позиция:", position)
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
print("Собрано страниц:", serp["pages"])
|
|
90
|
+
print("Нашлось всего:", serp["found_human"])
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
В ответе:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
{
|
|
97
|
+
"pages": 3,
|
|
98
|
+
"exhausted": False,
|
|
99
|
+
"breakDomainHit": True, # остановились на нужном домене
|
|
100
|
+
"query": "ремонт айфона",
|
|
101
|
+
"rawQuery": "ремонт айфона",
|
|
102
|
+
"found": 28000000,
|
|
103
|
+
"found_human": "нашлось 28 млн результатов",
|
|
104
|
+
"lr": 213,
|
|
105
|
+
"url": "https://yandex.ru/search/?text=...",
|
|
106
|
+
"results": [
|
|
107
|
+
{
|
|
108
|
+
"url": "https://example.com/remont-iphone/",
|
|
109
|
+
"domain": "example.com",
|
|
110
|
+
"title": "Ремонт айфонов в Москве",
|
|
111
|
+
"passage": "Починим за 30 минут...",
|
|
112
|
+
"breadcrumbs": "example.com › услуги",
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Выдача Google по нужному городу
|
|
119
|
+
|
|
120
|
+
Регион задаётся числовым ID из справочника — сервис сам соберёт `uule` и подставит `gl`.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
regions = client.google_regions("Казань")
|
|
124
|
+
|
|
125
|
+
serp = client.google(
|
|
126
|
+
"заказать пиццу",
|
|
127
|
+
region=regions["regions"][0]["id"],
|
|
128
|
+
hl="ru",
|
|
129
|
+
device="desktop",
|
|
130
|
+
pages=2,
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Если Google схлопнул часть результатов как «очень похожие», причина придёт в `filter_description`, а вернуть их можно параметром `filter`:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
serp = client.google("заказать пиццу", filter=0)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Выдача Bing
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
serp = client.bing("buy a laptop", mkt="en-US", pages=2)
|
|
144
|
+
|
|
145
|
+
print(serp["mkt"], serp["lang"]) # фактический рынок и язык
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Реклама на странице выдачи
|
|
149
|
+
|
|
150
|
+
Приходит отдельным списком, органика не меняется. Стоит +0.01 ₽ за страницу, на которой реклама нашлась.
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
serp = client.yandex("пластиковые окна", region=213, ads=True)
|
|
154
|
+
|
|
155
|
+
for ad in serp.get("ads", []):
|
|
156
|
+
print(ad["block"], "#{}".format(ad["position"]), "—", ad["domain"])
|
|
157
|
+
print(" ", ad.get("title"))
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`block` — где стоял блок: `top` до органики, `bottom` после неё, `inline` между результатами. Пустой список `ads` значит «рекламу просили, но её не было», а отсутствие ключа — «не просили».
|
|
161
|
+
|
|
162
|
+
## Ответ нейросети над выдачей
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
serp = client.yandex("чем отличается osb от фанеры", ai=True)
|
|
166
|
+
|
|
167
|
+
answer = serp.get("aiAnswer")
|
|
168
|
+
|
|
169
|
+
if answer:
|
|
170
|
+
print(answer["markdown"])
|
|
171
|
+
|
|
172
|
+
for source in answer.get("sources", []):
|
|
173
|
+
print("[{}]".format(source["id"]), source["domain"])
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Стоит +0.01 ₽ и только когда ответ есть: если поисковик его не показал, запрос обойдётся в обычную цену. Доступен только с первой страницы.
|
|
177
|
+
|
|
178
|
+
## Картинки
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
images = client.yandex_images(
|
|
182
|
+
"скандинавский интерьер",
|
|
183
|
+
orientation="horizontal",
|
|
184
|
+
size="large",
|
|
185
|
+
format="jpg",
|
|
186
|
+
pages=2,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
for image in images["results"]:
|
|
190
|
+
print("{}×{}".format(image.get("width"), image.get("height")), image["url"])
|
|
191
|
+
print(" источник:", image["sourceUrl"])
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Те же параметры работают у `google_images()` и `bing_images()` — SDK переводит общий фильтр в родной параметр движка. Если у поисковика такого значения нет, придёт `ValidationError` с указанием, чем заменить.
|
|
195
|
+
|
|
196
|
+
## Видео
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
videos = client.google_video("как заменить ремень грм", duration="long", hl="ru")
|
|
200
|
+
|
|
201
|
+
for video in videos["results"]:
|
|
202
|
+
print(video["title"], "—", video.get("durationText"))
|
|
203
|
+
print(" ", video["url"], "({})".format(video.get("provider")))
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Поле `duration` приходит в секундах, но не всегда: у прямых эфиров вместо длины стоит `LIVE`. Отбор вида `duration < 600` молча выбросит такие ролики — ориентируйтесь на `durationText`, он на месте всегда.
|
|
207
|
+
|
|
208
|
+
## Поисковые подсказки
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
suggest = client.yandex_suggest("купить кв", region=213)
|
|
212
|
+
|
|
213
|
+
print(suggest["results"])
|
|
214
|
+
# ['купить квартиру в москве', 'купить квартиру в новостройке', ...]
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Есть у всех трёх поисковиков: `yandex_suggest()`, `google_suggest()`, `bing_suggest()`.
|
|
218
|
+
|
|
219
|
+
## Справочник регионов
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
regions = client.yandex_regions("Казань")
|
|
223
|
+
|
|
224
|
+
for region in regions["regions"]:
|
|
225
|
+
print(region["id"], "—", region["name"], "({})".format(region["subname"]))
|
|
226
|
+
# 43 — Казань (Республика Татарстан)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Бесплатно, но ключ нужен: по нему считается лимит запросов в минуту. У `google_regions()` в ответе дополнительно приходит готовая строка `uule`.
|
|
230
|
+
|
|
231
|
+
## Вордстат: частота запроса
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
frequency = client.wordstat_frequency(
|
|
235
|
+
"ремонт айфона",
|
|
236
|
+
kind="exact", # точная частотность: "!ремонт !айфона"
|
|
237
|
+
region=213,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
print(frequency["results"]["totalValue"]) # 27356
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Вид частотности задаётся параметром `kind`, кавычки и операторы расставит сервис — фразу передавайте как есть:
|
|
244
|
+
|
|
245
|
+
| `kind` | Что считает |
|
|
246
|
+
| --- | --- |
|
|
247
|
+
| `base` | Базовая: фраза как есть |
|
|
248
|
+
| `phrase` | Фразовая: `"фраза"` |
|
|
249
|
+
| `exact` | Точная: `"!слово !слово"` — для прогноза трафика берут её |
|
|
250
|
+
| `superexact` | Сверхточная: `"[!слово !слово]"` |
|
|
251
|
+
|
|
252
|
+
## Вордстат: расширение семантики
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
wordstat = client.wordstat("ремонт айфона", region=[213, 2])
|
|
256
|
+
|
|
257
|
+
for phrase in wordstat["results"]["popular"]:
|
|
258
|
+
print(phrase["value"], phrase["text"])
|
|
259
|
+
|
|
260
|
+
for phrase in wordstat["results"]["associations"]:
|
|
261
|
+
print(phrase["value"], phrase["text"])
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`popular` — что ищут вместе с фразой, `associations` — соседняя семантика.
|
|
265
|
+
|
|
266
|
+
## Вордстат: сезонность
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
graph = client.wordstat_graph("купить ёлку", graph_type="month")
|
|
270
|
+
|
|
271
|
+
for point in graph["results"]["graph"]:
|
|
272
|
+
print(point["text"], point["absolute"])
|
|
273
|
+
# июнь 2026 9042
|
|
274
|
+
# июль 2026 11780
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`month` и `week` отдают историю с 2018 года, `day` — последние 60 дней.
|
|
278
|
+
|
|
279
|
+
## Вордстат: география спроса
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
geo = client.wordstat_map("купить ноутбук", map_type="regions")
|
|
283
|
+
|
|
284
|
+
for row in geo["results"]["rows"]:
|
|
285
|
+
print(row["text"], row["absolute"], "индекс", row["popularity"])
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`popularity` — affinity-индекс: 100 означает средний по стране интерес, выше — повышенный. В каждой строке приходит `region_id`, его можно сразу подставить в `region` других методов.
|
|
289
|
+
|
|
290
|
+
## Прогноз показов Яндекс Директа
|
|
291
|
+
|
|
292
|
+
Рекламный кабинет не нужен. Список фраз передаётся списком — SDK склеит его сам.
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
forecast = client.direct(
|
|
296
|
+
["ремонт айфона", "замена экрана iphone", '"ремонт айфона"'],
|
|
297
|
+
region=213,
|
|
298
|
+
period="month",
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
for row in forecast["results"]:
|
|
302
|
+
print("{}: {} показов".format(row["phrase"], row["shows"]))
|
|
303
|
+
|
|
304
|
+
for place, bid in row["positions"].items():
|
|
305
|
+
print(" {}: ставка {} ₽, бюджет {} ₽, кликов {}".format(
|
|
306
|
+
place, bid["bid"], bid["budget"], bid["clicks"]
|
|
307
|
+
))
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Вид частотности задаётся операторами прямо во фразе: `ремонт айфона` — базовая, `"ремонт айфона"` — фразовая, `"!ремонт !айфона"` — точная.
|
|
311
|
+
|
|
312
|
+
Стоимость — 0.01 ₽ за пачку до 4000 символов, это около 150 обычных фраз. За один запрос принимается до 1000 фраз, на аккаунт — не больше 100 запросов в час.
|
|
313
|
+
|
|
314
|
+
## Геолокация по IP
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
location = client.geoip("77.88.55.242")
|
|
318
|
+
|
|
319
|
+
print(location["country"]["name"], location["region"]["name"])
|
|
320
|
+
print(location["latitude"], location["longitude"])
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
ID региона тот же, что у Яндекса, — его можно сразу подставить в `region` методов выдачи и Вордстата:
|
|
324
|
+
|
|
325
|
+
```python
|
|
326
|
+
serp = client.yandex("доставка пиццы", region=location["region"]["id"])
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Баланс
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
balance = client.balance()
|
|
333
|
+
|
|
334
|
+
print(balance["balance"], balance["currency"]) # 123.45 RUB
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
# Справочник методов
|
|
340
|
+
|
|
341
|
+
| Метод | Путь API | Что делает |
|
|
342
|
+
| --- | --- | --- |
|
|
343
|
+
| `yandex()` | `/yandex` | Органическая выдача Яндекса |
|
|
344
|
+
| `yandex_suggest()` | `/yandex/suggest` | Поисковые подсказки |
|
|
345
|
+
| `yandex_regions()` | `/yandex/regions` | Справочник регионов, бесплатно |
|
|
346
|
+
| `yandex_images()` | `/yandex/images` | Поиск по картинкам |
|
|
347
|
+
| `yandex_video()` | `/yandex/video` | Поиск по видео |
|
|
348
|
+
| `google()` | `/google` | Органическая выдача Google |
|
|
349
|
+
| `google_suggest()` | `/google/suggest` | Подсказки |
|
|
350
|
+
| `google_regions()` | `/google/regions` | Регионы и готовый `uule`, бесплатно |
|
|
351
|
+
| `google_images()` | `/google/images` | Поиск по картинкам |
|
|
352
|
+
| `google_video()` | `/google/video` | Поиск по видео |
|
|
353
|
+
| `bing()` | `/bing` | Органическая выдача Bing |
|
|
354
|
+
| `bing_suggest()` | `/bing/suggest` | Подсказки |
|
|
355
|
+
| `bing_images()` | `/bing/images` | Поиск по картинкам |
|
|
356
|
+
| `bing_video()` | `/bing/video` | Поиск по видео |
|
|
357
|
+
| `wordstat()` | `/wordstat` | Популярные и похожие запросы |
|
|
358
|
+
| `wordstat_frequency()` | `/wordstat/frequency` | Частота запроса одним числом |
|
|
359
|
+
| `wordstat_graph()` | `/wordstat/graph` | Динамика по месяцам, неделям, дням |
|
|
360
|
+
| `wordstat_map()` | `/wordstat/map` | География показов |
|
|
361
|
+
| `direct()` | `/direct` | Прогноз показов Яндекс Директа |
|
|
362
|
+
| `geoip()` | `/geoip` | Геолокация по IPv4, бесплатно |
|
|
363
|
+
| `balance()` | `/balance` | Остаток на счёте, бесплатно |
|
|
364
|
+
|
|
365
|
+
Полный список параметров каждого метода — в [документации](https://jsonseo.ru/docs).
|
|
366
|
+
|
|
367
|
+
Появился метод, которого ещё нет в SDK? Его можно вызвать напрямую:
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
client.call("новый/метод", {"параметр": "значение"}) # разберёт JSON
|
|
371
|
+
client.call_raw("новый/метод", {"параметр": "значение"}) # вернёт тело как есть
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
# Как SDK помогает с параметрами
|
|
375
|
+
|
|
376
|
+
**Списки передаются списками.** Фразы для Директа склеиваются переводом строки, остальные списки — запятой:
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
client.direct(["ремонт айфона", "ремонт телефона", "замена экрана"])
|
|
380
|
+
client.wordstat("ремонт", region=[213, 2], device=["desktop", "phone"])
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Флаги принимаются флагами.** `True` и `False` уезжают как `1` и `0`:
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
client.yandex("купить ноутбук", ai=True, ads=True)
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
**`None` и пустой список не отправляются.** Необязательный параметр, который вы ещё не посчитали, можно не вычищать из вызова руками.
|
|
390
|
+
|
|
391
|
+
**Родные параметры поисковиков проходят насквозь.** Вертикали принимают не только общие фильтры, но и `tbs` у Google, `isize` у Яндекса, `qft` у Bing.
|
|
392
|
+
|
|
393
|
+
# Ошибки
|
|
394
|
+
|
|
395
|
+
Всё, что бросает SDK, наследуется от `JsonSeoError`.
|
|
396
|
+
|
|
397
|
+
| Ошибка | Статус | Когда |
|
|
398
|
+
| --- | --- | --- |
|
|
399
|
+
| `ValidationError` | 422 | Параметры не приняты. `errors` — сообщения по полям, `fields` — их имена |
|
|
400
|
+
| `UnauthorizedError` | 403, 401 | Ключ не передан или недействителен |
|
|
401
|
+
| `PaymentRequiredError` | 402 | На счёте не хватает средств |
|
|
402
|
+
| `RateLimitError` | 429 | Превышен лимит частоты |
|
|
403
|
+
| `ServiceUnavailableError` | 503 | Выдачу получить не вышло. Деньги не списаны |
|
|
404
|
+
| `ApiError` | прочие | Любой другой отказ сервиса |
|
|
405
|
+
|
|
406
|
+
У всех ошибок сервиса есть `status`, `body`, разобранный `payload` и `retry_after` — срок, который назвал сервис, если он его назвал.
|
|
407
|
+
|
|
408
|
+
| Ошибка | Когда |
|
|
409
|
+
| --- | --- |
|
|
410
|
+
| `NetworkError` | До сервиса не достучались: сеть, DNS, TLS |
|
|
411
|
+
| `TimeoutError` | Ответа не дождались |
|
|
412
|
+
| `IncompleteResponseError` | Соединение оборвалось посреди тела |
|
|
413
|
+
| `ParseError` | Ответ пришёл, но не разобрался как JSON. `body` — тело как есть |
|
|
414
|
+
| `InvalidArgumentError` | SDK забраковал аргументы, запрос не отправлялся |
|
|
415
|
+
|
|
416
|
+
```python
|
|
417
|
+
from jsonseo import PaymentRequiredError, RateLimitError, ValidationError
|
|
418
|
+
|
|
419
|
+
try:
|
|
420
|
+
serp = client.yandex("купить ноутбук", pages=50)
|
|
421
|
+
except ValidationError as error:
|
|
422
|
+
for field, messages in error.errors.items():
|
|
423
|
+
print(field, ":", ", ".join(messages))
|
|
424
|
+
except PaymentRequiredError:
|
|
425
|
+
print("Баланс кончился:", client.balance()["balance"])
|
|
426
|
+
except RateLimitError as error:
|
|
427
|
+
print("Вернуться через", error.retry_after, "с")
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
`TimeoutError` наследует и встроенный `TimeoutError`, так что привычный `except TimeoutError` его тоже поймает.
|
|
431
|
+
|
|
432
|
+
# Повторы
|
|
433
|
+
|
|
434
|
+
**У каждого запроса три попытки по умолчанию: одна основная и две повторных.** Если сервис затупил и выдачу собрать не вышло (`503`), SDK сам сходит ещё дважды, и обычно этого хватает. `attempts=1` отключает повторы совсем.
|
|
435
|
+
|
|
436
|
+
`429`, `5xx` и обрывы связи повторяются автоматически — это ровно те отказы, за которые сервис денег не берёт. Отказы по ключу, балансу и параметрам не повторяются: сами они не изменятся.
|
|
437
|
+
|
|
438
|
+
Таймаут и оборвавшееся посреди тела соединение не повторяются, и это намеренно: работу на стороне сервиса обрыв у клиента не отменяет — выдача будет собрана и оплачена, а повтор стоил бы ещё раз. Если ответ не успевает прийти, поднимайте `timeout`, а не `attempts`.
|
|
439
|
+
|
|
440
|
+
Пауза между попытками удваивается и разбавляется случайной добавкой. Если сервис прислал `Retry-After`, SDK не вернётся раньше названного срока. Когда сервис просит ждать дольше `max_retry_delay`, SDK не ждёт вовсе, а отдаёт ошибку с `retry_after` — решение остаётся за вами.
|
|
441
|
+
|
|
442
|
+
# Настройки клиента
|
|
443
|
+
|
|
444
|
+
```python
|
|
445
|
+
client = Client(
|
|
446
|
+
"ВАШ_КЛЮЧ",
|
|
447
|
+
base_url="https://jsonseo.ru/api", # адрес API
|
|
448
|
+
timeout=300.0, # сколько ждать ответа на попытку, секунд
|
|
449
|
+
attempts=3, # всего попыток, вместе с первой
|
|
450
|
+
retry_delay=1.0, # стартовая пауза между попытками
|
|
451
|
+
max_retry_delay=30.0, # потолок паузы
|
|
452
|
+
auth="header", # или "query" — ключ в параметре key
|
|
453
|
+
user_agent="мой-проект/1.0",
|
|
454
|
+
transport=my_transport, # свой транспорт
|
|
455
|
+
)
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Настройки — только именованные аргументы, поэтому опечатку ловит сам Python: `Client("КЛЮЧ", timeuot=5)` не запустится.
|
|
459
|
+
|
|
460
|
+
Таймаут по умолчанию намеренно большой: многостраничный запрос выдачи собирается минутами, и обрыв на стороне клиента не отменяет запрос на стороне сервиса — деньги за него уже списаны. Считается он на **каждую попытку** отдельно, а не на весь вызов.
|
|
461
|
+
|
|
462
|
+
Ключ по умолчанию едет в заголовке `Authorization: Bearer`, а не в адресе: так он не оседает в логах прокси и серверов. `auth="query"` нужен там, где заголовки до API не доходят.
|
|
463
|
+
|
|
464
|
+
# Свой транспорт
|
|
465
|
+
|
|
466
|
+
Если HTTP в проекте уже ходит через `requests`, `httpx` или что-то своё, SDK можно отдать этот клиент — достаточно объекта с одним методом:
|
|
467
|
+
|
|
468
|
+
Транспорт обязан бросать ошибки SDK: от их класса зависит, повторит клиент запрос или нет. Чужие исключения пройдут мимо политики повторов и мимо пользовательского `except JsonSeoError`.
|
|
469
|
+
|
|
470
|
+
```python
|
|
471
|
+
import requests
|
|
472
|
+
|
|
473
|
+
from jsonseo import IncompleteResponseError, NetworkError, TimeoutError
|
|
474
|
+
from jsonseo.transport import Response
|
|
475
|
+
|
|
476
|
+
class RequestsTransport:
|
|
477
|
+
def __init__(self, session):
|
|
478
|
+
self.session = session
|
|
479
|
+
|
|
480
|
+
def send(self, method, url, headers, body, timeout):
|
|
481
|
+
try:
|
|
482
|
+
response = self.session.request(
|
|
483
|
+
method, url, headers=headers, data=body, timeout=timeout
|
|
484
|
+
)
|
|
485
|
+
except requests.Timeout as error:
|
|
486
|
+
raise TimeoutError("Ответа не дождались.") from error
|
|
487
|
+
except requests.ConnectionError as error:
|
|
488
|
+
raise NetworkError("Запрос не удался: {}.".format(error)) from error
|
|
489
|
+
|
|
490
|
+
try:
|
|
491
|
+
text = response.text
|
|
492
|
+
except requests.RequestException as error:
|
|
493
|
+
# Заголовки уже пришли: выдача собрана и оплачена, повторять нельзя.
|
|
494
|
+
raise IncompleteResponseError("Ответ пришёл не целиком.") from error
|
|
495
|
+
|
|
496
|
+
return Response(response.status_code, dict(response.headers), text)
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Тот же приём годится для тестов: подмените транспорт заглушкой, и запросы никуда не пойдут.
|
|
500
|
+
|
|
501
|
+
# Разработка
|
|
502
|
+
|
|
503
|
+
```bash
|
|
504
|
+
python -m unittest discover -s tests -t .
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Тесты идут без сети: транспорт подменяется заглушкой.
|
|
508
|
+
|
|
509
|
+
# Лицензия
|
|
510
|
+
|
|
511
|
+
MIT.
|