itdpy 1.0.2__tar.gz → 1.0.4__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.
Files changed (53) hide show
  1. itdpy-1.0.4/PKG-INFO +260 -0
  2. itdpy-1.0.4/README.md +244 -0
  3. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/client.py +15 -9
  4. itdpy-1.0.4/itdpy/config.py +20 -0
  5. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/enums.py +1 -0
  6. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/request.py +14 -0
  7. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/streaming.py +31 -4
  8. itdpy-1.0.4/itdpy.egg-info/PKG-INFO +260 -0
  9. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy.egg-info/SOURCES.txt +0 -1
  10. {itdpy-1.0.2 → itdpy-1.0.4}/pyproject.toml +1 -1
  11. itdpy-1.0.2/PKG-INFO +0 -267
  12. itdpy-1.0.2/README.md +0 -251
  13. itdpy-1.0.2/itdpy/auth.py +0 -35
  14. itdpy-1.0.2/itdpy/config.py +0 -12
  15. itdpy-1.0.2/itdpy.egg-info/PKG-INFO +0 -267
  16. {itdpy-1.0.2 → itdpy-1.0.4}/LICENSE +0 -0
  17. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/__init__.py +0 -0
  18. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/api/__init__.py +0 -0
  19. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/api/_common.py +0 -0
  20. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/api/base.py +0 -0
  21. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/api/comments.py +0 -0
  22. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/api/discovery.py +0 -0
  23. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/api/files.py +0 -0
  24. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/api/notifications.py +0 -0
  25. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/api/pins.py +0 -0
  26. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/api/posts.py +0 -0
  27. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/api/search.py +0 -0
  28. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/api/users.py +0 -0
  29. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/exceptions.py +0 -0
  30. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/formatting/__init__.py +0 -0
  31. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/formatting/parser.py +0 -0
  32. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/formatting/validator.py +0 -0
  33. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/models/__init__.py +0 -0
  34. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/models/base.py +0 -0
  35. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/models/clan.py +0 -0
  36. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/models/comment.py +0 -0
  37. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/models/file.py +0 -0
  38. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/models/follow_status.py +0 -0
  39. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/models/hashtags.py +0 -0
  40. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/models/notification.py +0 -0
  41. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/models/pagination.py +0 -0
  42. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/models/pin.py +0 -0
  43. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/models/portal.py +0 -0
  44. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/models/post.py +0 -0
  45. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/models/search.py +0 -0
  46. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/models/settings_models.py +0 -0
  47. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/models/user.py +0 -0
  48. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/models/who_to_follow.py +0 -0
  49. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy/utils/suggestions.py +0 -0
  50. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy.egg-info/dependency_links.txt +0 -0
  51. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy.egg-info/requires.txt +0 -0
  52. {itdpy-1.0.2 → itdpy-1.0.4}/itdpy.egg-info/top_level.txt +0 -0
  53. {itdpy-1.0.2 → itdpy-1.0.4}/setup.cfg +0 -0
itdpy-1.0.4/PKG-INFO ADDED
@@ -0,0 +1,260 @@
1
+ Metadata-Version: 2.4
2
+ Name: itdpy
3
+ Version: 1.0.4
4
+ Summary: Production-ready Python SDK for ITD API
5
+ Author: Gam5510
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Gam5510/ITDpy
8
+ Project-URL: Repository, https://github.com/Gam5510/ITDpy
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: requests>=2.28.0
13
+ Requires-Dist: pydantic>=2.0.0
14
+ Requires-Dist: sseclient-py>=1.8.0
15
+ Dynamic: license-file
16
+
17
+ # ITDpy
18
+
19
+ <p align="center">
20
+ <img src="https://i.postimg.cc/gJ9z8RDk/ITDpy-(1)-pixian-ai.png" width="700">
21
+ </p>
22
+
23
+ <p align="center">
24
+ <img src="https://img.shields.io/pypi/v/itdpy?nocache=1" alt="PyPI version">
25
+ <img src="https://static.pepy.tech/badge/itdpy?nocache=1" alt="Downloads">
26
+ <img src="https://img.shields.io/github/license/Gam5510/ITDpy" alt="License">
27
+ <a href="https://gam5510.github.io/ITDpy/">
28
+ <img src="https://img.shields.io/badge/docs-online-blue" alt="Docs">
29
+ </a>
30
+ </p>
31
+
32
+ Python SDK для социальной сети итд.com.
33
+
34
+ > Неофициальный API-клиент.
35
+ > SDK предназначен для клиентских приложений, интеграций и сервисов, работающих в рамках правил платформы.
36
+
37
+ ## Безопасность и позиция проекта
38
+
39
+ ITDpy не поддерживает спам, массовую автоматизацию, накрутку, ботов для злоупотреблений и другие сценарии, нарушающие правила платформы.
40
+
41
+ Библиотека ориентирована на:
42
+
43
+ - клиентские приложения
44
+ - внутренние сервисы
45
+ - интеграции
46
+ - тестирование API
47
+
48
+ ## User-Agent
49
+
50
+ По умолчанию библиотека использует браузерный User-Agent:
51
+
52
+ ```text
53
+ Mozilla/5.0 (Linux; Android 11; SM-G991B)
54
+ AppleWebKit/537.36 (KHTML, like Gecko)
55
+ Chrome/120.0.6099.144 Mobile Safari/537.36
56
+ ```
57
+
58
+ Это сделано для обеспечения стабильной работы, так как некоторые эндпоинты API могут некорректно обрабатывать нестандартные клиенты.
59
+
60
+ Библиотека не использует User-Agent для обхода ограничений и ориентирована на корректное использование API.
61
+
62
+ ## Установка
63
+
64
+ ```bash
65
+ pip install itdpy
66
+ ```
67
+
68
+ ### Через git
69
+
70
+ ```bash
71
+ git clone https://github.com/Gam5510/ITDpy
72
+ cd ITDpy
73
+ pip install -r requirements.txt
74
+ pip install -e .
75
+ ```
76
+
77
+ ## Документация
78
+
79
+ [![Docs](https://img.shields.io/badge/docs-online-blue)](https://gam5510.github.io/ITDpy/)
80
+
81
+ Ссылка: [https://gam5510.github.io/ITDpy/](https://gam5510.github.io/ITDpy/)
82
+
83
+ ## Быстрый старт
84
+
85
+ > ![Получение токена](https://i.ibb.co/DH1m8GL7/Assistant.png)
86
+ > Как получить токен
87
+
88
+ - Открой `итд.com` в браузере и войди в аккаунт.
89
+ - Открой DevTools (F12).
90
+ - Перейди в `Application` -> `Cookies`.
91
+ - Найди cookie `refresh_token`.
92
+ - Скопируй её значение.
93
+
94
+ ```python
95
+ from itdpy import ITDClient
96
+
97
+ client = ITDClient(refresh_token="YOUR_REFRESH_TOKEN")
98
+
99
+ me = client.users.me()
100
+ print(me.id)
101
+ print(me.username)
102
+ ```
103
+
104
+ ## Конфигурация
105
+
106
+ ```python
107
+ from itdpy import ITDClient, Config
108
+
109
+ config = Config(
110
+ timeout=30,
111
+ upload_timeout=180,
112
+ max_retries=5,
113
+ backoff_factor=2.0,
114
+ )
115
+
116
+ client = ITDClient(
117
+ refresh_token="YOUR_REFRESH_TOKEN",
118
+ config=config,
119
+ )
120
+ ```
121
+
122
+ ## Кастомный User-Agent
123
+
124
+ Если нужен полностью свой `User-Agent`, можно передать его через `Config.custom_user_agent`:
125
+
126
+ ```python
127
+ from itdpy import ITDClient, Config
128
+
129
+ config = Config(
130
+ custom_user_agent="my-app/2.0",
131
+ )
132
+
133
+ client = ITDClient(
134
+ refresh_token="YOUR_REFRESH_TOKEN",
135
+ config=config,
136
+ )
137
+ ```
138
+
139
+ Если `custom_user_agent` не задан, библиотека использует стандартный стартовый браузерный `User-Agent`.
140
+
141
+ ## User-Agent с данными пользователя
142
+
143
+ Если нужно после авторизации переключиться на `User-Agent` с данными SDK и пользователя, включи `use_user_data_in_user_agent=True`:
144
+
145
+ ```python
146
+ from itdpy import ITDClient, Config
147
+
148
+ config = Config(
149
+ service="my_app",
150
+ use_user_data_in_user_agent=True,
151
+ )
152
+
153
+ client = ITDClient(
154
+ refresh_token="YOUR_REFRESH_TOKEN",
155
+ config=config,
156
+ )
157
+ ```
158
+
159
+ По умолчанию этот режим выключен.
160
+
161
+ Шаблон можно переопределить через `Config.user_agent_template`:
162
+
163
+ ```python
164
+ from itdpy import Config
165
+
166
+ config = Config(
167
+ use_user_data_in_user_agent=True,
168
+ user_agent_template="itdpy/{sdk_version} ({parts})",
169
+ )
170
+ ```
171
+
172
+ Доступные поля шаблона:
173
+
174
+ - `{sdk_version}`
175
+ - `{parts}`
176
+ - `{user_id}`
177
+ - `{service}`
178
+
179
+ ## Примеры
180
+
181
+ ### Получить пост
182
+
183
+ ```python
184
+ post = client.posts.get("POST_ID")
185
+ print(post.id)
186
+ print(post.to_dict())
187
+ ```
188
+
189
+ ### Лента постов
190
+
191
+ ```python
192
+ posts = client.posts.list(limit=10)
193
+ print(len(posts))
194
+ print(posts[0].id)
195
+ ```
196
+
197
+ ### Создать пост
198
+
199
+ ```python
200
+ client.posts.create(
201
+ content="Привет из ITDpy",
202
+ )
203
+ ```
204
+
205
+ ### Markdown и HTML
206
+
207
+ ```python
208
+ client.posts.create(
209
+ content="**Жирный** текст",
210
+ parse_md=True,
211
+ )
212
+
213
+ client.posts.create(
214
+ content="<b>Жирный</b> текст",
215
+ parse_html=True,
216
+ )
217
+ ```
218
+
219
+ ### SSE streaming
220
+
221
+ ```python
222
+ stream = client.notifications.stream()
223
+
224
+ @stream.on("notification")
225
+ def on_notification(event):
226
+ print(event.data)
227
+
228
+ stream.run()
229
+ ```
230
+
231
+ ### keep_online
232
+
233
+ ```python
234
+ client.keep_online(
235
+ on_event=lambda event_type, data: print(event_type, data),
236
+ background=True,
237
+ )
238
+ ```
239
+
240
+ ### Обработка ошибок
241
+
242
+ ```python
243
+ from itdpy import APIError, NotFoundError, RateLimitError, ValidationError
244
+
245
+ try:
246
+ client.posts.get("invalid")
247
+ except NotFoundError:
248
+ print("Не найдено")
249
+ except ValidationError as e:
250
+ print(e.message)
251
+ except RateLimitError as e:
252
+ print(e.retry_after)
253
+ except APIError as e:
254
+ print(e.message)
255
+ ```
256
+
257
+ ## Прочее
258
+
259
+ Проект активно развивается. Если у вас есть предложения или pull request, создавайте issue в репозитории.
260
+ Мой телеграм для обратной связи: [@gam5510](https://t.me/gam5510)
itdpy-1.0.4/README.md ADDED
@@ -0,0 +1,244 @@
1
+ # ITDpy
2
+
3
+ <p align="center">
4
+ <img src="https://i.postimg.cc/gJ9z8RDk/ITDpy-(1)-pixian-ai.png" width="700">
5
+ </p>
6
+
7
+ <p align="center">
8
+ <img src="https://img.shields.io/pypi/v/itdpy?nocache=1" alt="PyPI version">
9
+ <img src="https://static.pepy.tech/badge/itdpy?nocache=1" alt="Downloads">
10
+ <img src="https://img.shields.io/github/license/Gam5510/ITDpy" alt="License">
11
+ <a href="https://gam5510.github.io/ITDpy/">
12
+ <img src="https://img.shields.io/badge/docs-online-blue" alt="Docs">
13
+ </a>
14
+ </p>
15
+
16
+ Python SDK для социальной сети итд.com.
17
+
18
+ > Неофициальный API-клиент.
19
+ > SDK предназначен для клиентских приложений, интеграций и сервисов, работающих в рамках правил платформы.
20
+
21
+ ## Безопасность и позиция проекта
22
+
23
+ ITDpy не поддерживает спам, массовую автоматизацию, накрутку, ботов для злоупотреблений и другие сценарии, нарушающие правила платформы.
24
+
25
+ Библиотека ориентирована на:
26
+
27
+ - клиентские приложения
28
+ - внутренние сервисы
29
+ - интеграции
30
+ - тестирование API
31
+
32
+ ## User-Agent
33
+
34
+ По умолчанию библиотека использует браузерный User-Agent:
35
+
36
+ ```text
37
+ Mozilla/5.0 (Linux; Android 11; SM-G991B)
38
+ AppleWebKit/537.36 (KHTML, like Gecko)
39
+ Chrome/120.0.6099.144 Mobile Safari/537.36
40
+ ```
41
+
42
+ Это сделано для обеспечения стабильной работы, так как некоторые эндпоинты API могут некорректно обрабатывать нестандартные клиенты.
43
+
44
+ Библиотека не использует User-Agent для обхода ограничений и ориентирована на корректное использование API.
45
+
46
+ ## Установка
47
+
48
+ ```bash
49
+ pip install itdpy
50
+ ```
51
+
52
+ ### Через git
53
+
54
+ ```bash
55
+ git clone https://github.com/Gam5510/ITDpy
56
+ cd ITDpy
57
+ pip install -r requirements.txt
58
+ pip install -e .
59
+ ```
60
+
61
+ ## Документация
62
+
63
+ [![Docs](https://img.shields.io/badge/docs-online-blue)](https://gam5510.github.io/ITDpy/)
64
+
65
+ Ссылка: [https://gam5510.github.io/ITDpy/](https://gam5510.github.io/ITDpy/)
66
+
67
+ ## Быстрый старт
68
+
69
+ > ![Получение токена](https://i.ibb.co/DH1m8GL7/Assistant.png)
70
+ > Как получить токен
71
+
72
+ - Открой `итд.com` в браузере и войди в аккаунт.
73
+ - Открой DevTools (F12).
74
+ - Перейди в `Application` -> `Cookies`.
75
+ - Найди cookie `refresh_token`.
76
+ - Скопируй её значение.
77
+
78
+ ```python
79
+ from itdpy import ITDClient
80
+
81
+ client = ITDClient(refresh_token="YOUR_REFRESH_TOKEN")
82
+
83
+ me = client.users.me()
84
+ print(me.id)
85
+ print(me.username)
86
+ ```
87
+
88
+ ## Конфигурация
89
+
90
+ ```python
91
+ from itdpy import ITDClient, Config
92
+
93
+ config = Config(
94
+ timeout=30,
95
+ upload_timeout=180,
96
+ max_retries=5,
97
+ backoff_factor=2.0,
98
+ )
99
+
100
+ client = ITDClient(
101
+ refresh_token="YOUR_REFRESH_TOKEN",
102
+ config=config,
103
+ )
104
+ ```
105
+
106
+ ## Кастомный User-Agent
107
+
108
+ Если нужен полностью свой `User-Agent`, можно передать его через `Config.custom_user_agent`:
109
+
110
+ ```python
111
+ from itdpy import ITDClient, Config
112
+
113
+ config = Config(
114
+ custom_user_agent="my-app/2.0",
115
+ )
116
+
117
+ client = ITDClient(
118
+ refresh_token="YOUR_REFRESH_TOKEN",
119
+ config=config,
120
+ )
121
+ ```
122
+
123
+ Если `custom_user_agent` не задан, библиотека использует стандартный стартовый браузерный `User-Agent`.
124
+
125
+ ## User-Agent с данными пользователя
126
+
127
+ Если нужно после авторизации переключиться на `User-Agent` с данными SDK и пользователя, включи `use_user_data_in_user_agent=True`:
128
+
129
+ ```python
130
+ from itdpy import ITDClient, Config
131
+
132
+ config = Config(
133
+ service="my_app",
134
+ use_user_data_in_user_agent=True,
135
+ )
136
+
137
+ client = ITDClient(
138
+ refresh_token="YOUR_REFRESH_TOKEN",
139
+ config=config,
140
+ )
141
+ ```
142
+
143
+ По умолчанию этот режим выключен.
144
+
145
+ Шаблон можно переопределить через `Config.user_agent_template`:
146
+
147
+ ```python
148
+ from itdpy import Config
149
+
150
+ config = Config(
151
+ use_user_data_in_user_agent=True,
152
+ user_agent_template="itdpy/{sdk_version} ({parts})",
153
+ )
154
+ ```
155
+
156
+ Доступные поля шаблона:
157
+
158
+ - `{sdk_version}`
159
+ - `{parts}`
160
+ - `{user_id}`
161
+ - `{service}`
162
+
163
+ ## Примеры
164
+
165
+ ### Получить пост
166
+
167
+ ```python
168
+ post = client.posts.get("POST_ID")
169
+ print(post.id)
170
+ print(post.to_dict())
171
+ ```
172
+
173
+ ### Лента постов
174
+
175
+ ```python
176
+ posts = client.posts.list(limit=10)
177
+ print(len(posts))
178
+ print(posts[0].id)
179
+ ```
180
+
181
+ ### Создать пост
182
+
183
+ ```python
184
+ client.posts.create(
185
+ content="Привет из ITDpy",
186
+ )
187
+ ```
188
+
189
+ ### Markdown и HTML
190
+
191
+ ```python
192
+ client.posts.create(
193
+ content="**Жирный** текст",
194
+ parse_md=True,
195
+ )
196
+
197
+ client.posts.create(
198
+ content="<b>Жирный</b> текст",
199
+ parse_html=True,
200
+ )
201
+ ```
202
+
203
+ ### SSE streaming
204
+
205
+ ```python
206
+ stream = client.notifications.stream()
207
+
208
+ @stream.on("notification")
209
+ def on_notification(event):
210
+ print(event.data)
211
+
212
+ stream.run()
213
+ ```
214
+
215
+ ### keep_online
216
+
217
+ ```python
218
+ client.keep_online(
219
+ on_event=lambda event_type, data: print(event_type, data),
220
+ background=True,
221
+ )
222
+ ```
223
+
224
+ ### Обработка ошибок
225
+
226
+ ```python
227
+ from itdpy import APIError, NotFoundError, RateLimitError, ValidationError
228
+
229
+ try:
230
+ client.posts.get("invalid")
231
+ except NotFoundError:
232
+ print("Не найдено")
233
+ except ValidationError as e:
234
+ print(e.message)
235
+ except RateLimitError as e:
236
+ print(e.retry_after)
237
+ except APIError as e:
238
+ print(e.message)
239
+ ```
240
+
241
+ ## Прочее
242
+
243
+ Проект активно развивается. Если у вас есть предложения или pull request, создавайте issue в репозитории.
244
+ Мой телеграм для обратной связи: [@gam5510](https://t.me/gam5510)
@@ -40,14 +40,7 @@ class ITDClient:
40
40
  name="refresh_token",
41
41
  value=self._refresh_token,
42
42
  domain="xn--d1ah4a.com",
43
- path="/api",
44
- )
45
-
46
- self._request_handler.session.headers.update(
47
- {
48
- "Origin": self.config.base_url,
49
- "Referer": f"{self.config.base_url}/",
50
- }
43
+ path="/",
51
44
  )
52
45
 
53
46
  def _authenticate(self) -> None:
@@ -73,6 +66,14 @@ class ITDClient:
73
66
  self._update_user_agent()
74
67
 
75
68
  def _update_user_agent(self) -> None:
69
+ if self.config.custom_user_agent:
70
+ self._request_handler.session.headers["User-Agent"] = self.config.custom_user_agent
71
+ return
72
+
73
+ if not self.config.use_user_data_in_user_agent:
74
+ self._request_handler.session.headers["User-Agent"] = self.config.initial_user_agent
75
+ return
76
+
76
77
  parts = [f"platform=python"]
77
78
  if self._user_id:
78
79
  parts.insert(0, f"userid={self._user_id}")
@@ -81,7 +82,12 @@ class ITDClient:
81
82
  if self.config.service:
82
83
  parts.append(f"service={self.config.service}")
83
84
 
84
- user_agent = f"itdpy/{self.config.sdk_version} ({'; '.join(parts)})"
85
+ user_agent = self.config.user_agent_template.format(
86
+ sdk_version=self.config.sdk_version,
87
+ parts="; ".join(parts),
88
+ user_id=self._user_id or "",
89
+ service=self.config.service or "",
90
+ )
85
91
  self._request_handler.session.headers["User-Agent"] = user_agent
86
92
 
87
93
  @property
@@ -0,0 +1,20 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Config:
6
+ base_url: str = "https://xn--d1ah4a.com"
7
+ timeout: int = 20
8
+ upload_timeout: int = 120
9
+ max_retries: int = 3
10
+ backoff_factor: float = 1.5
11
+ sdk_version: str = "1.0.2"
12
+ service: str | None = None
13
+ initial_user_agent: str = (
14
+ "Mozilla/5.0 (Linux; Android 11; SM-G991B)"
15
+ "AppleWebKit/537.36 (KHTML, like Gecko)"
16
+ "Chrome/120.0.6099.144 Mobile Safari/537.36"
17
+ )
18
+ custom_user_agent: str | None = None
19
+ user_agent_template: str = "itdpy/{sdk_version} ({parts})"
20
+ use_user_data_in_user_agent: bool = False
@@ -60,6 +60,7 @@ class NotificationType(str, Enum):
60
60
  REPLY = "reply"
61
61
  REPOST = "repost"
62
62
  FOLLOW = "follow"
63
+ WALL_POST = "wall_post"
63
64
 
64
65
 
65
66
  class NotificationTargetType(str, Enum):
@@ -37,8 +37,22 @@ class RequestHandler:
37
37
  adapter = HTTPAdapter(max_retries=retry_strategy)
38
38
  session.mount("http://", adapter)
39
39
  session.mount("https://", adapter)
40
+
41
+ session.headers.update(self._build_default_headers())
40
42
 
41
43
  return session
44
+
45
+ def _build_default_headers(self) -> dict[str, str]:
46
+ return {
47
+ "User-Agent": self._build_initial_user_agent(),
48
+ "Accept": "application/json",
49
+ "Content-Type": "application/json",
50
+ "Origin": self.config.base_url,
51
+ "Referer": f"{self.config.base_url}/",
52
+ }
53
+
54
+ def _build_initial_user_agent(self) -> str:
55
+ return self.config.custom_user_agent or self.config.initial_user_agent
42
56
 
43
57
  def request(
44
58
  self,
@@ -45,6 +45,7 @@ class NotificationStream:
45
45
  self._stopped = False
46
46
  self._last_event_id: str | None = None
47
47
  self._handlers: dict[str, list[_HandlerSubscription]] = {}
48
+ self._active_response: requests.Response | None = None
48
49
 
49
50
  def on(
50
51
  self,
@@ -64,17 +65,23 @@ class NotificationStream:
64
65
 
65
66
  def stop(self) -> None:
66
67
  self._stopped = True
68
+ self._close_active_response()
67
69
 
68
70
  def run(self) -> None:
69
- for _ in self:
70
- pass
71
+ try:
72
+ for _ in self:
73
+ pass
74
+ except KeyboardInterrupt:
75
+ self.stop()
71
76
 
72
77
  def __iter__(self) -> Iterator[StreamEvent]:
73
78
  backoff = 1
74
79
 
75
80
  while not self._stopped:
81
+ response: requests.Response | None = None
76
82
  try:
77
83
  response = self._connect()
84
+ self._active_response = response
78
85
  backoff = 1
79
86
 
80
87
  client = self._create_sse_client(response)
@@ -89,7 +96,9 @@ class NotificationStream:
89
96
 
90
97
  self._dispatch(parsed_event)
91
98
  yield parsed_event
92
-
99
+ except KeyboardInterrupt:
100
+ self.stop()
101
+ return
93
102
  except requests.RequestException as exc:
94
103
  error_event = StreamEvent(event="error", data={"message": str(exc)})
95
104
  self._dispatch(error_event)
@@ -98,6 +107,11 @@ class NotificationStream:
98
107
  error_event = StreamEvent(event="error", data={"message": str(exc)})
99
108
  self._dispatch(error_event)
100
109
  yield error_event
110
+ finally:
111
+ if response is not None:
112
+ response.close()
113
+ if self._active_response is response:
114
+ self._active_response = None
101
115
 
102
116
  if self._stopped:
103
117
  break
@@ -105,7 +119,11 @@ class NotificationStream:
105
119
  reconnect_event = StreamEvent(event="reconnecting", data={"delay": backoff})
106
120
  self._dispatch(reconnect_event)
107
121
  yield reconnect_event
108
- time.sleep(backoff)
122
+ try:
123
+ time.sleep(backoff)
124
+ except KeyboardInterrupt:
125
+ self.stop()
126
+ break
109
127
  backoff = min(self._next_backoff(backoff), self._max_backoff)
110
128
 
111
129
  def _connect(self) -> requests.Response:
@@ -189,3 +207,12 @@ class NotificationStream:
189
207
  if current < 5:
190
208
  return 5
191
209
  return current * 2
210
+
211
+ def _close_active_response(self) -> None:
212
+ if self._active_response is None:
213
+ return
214
+
215
+ try:
216
+ self._active_response.close()
217
+ finally:
218
+ self._active_response = None