diff --git a/' b/'
new file mode 100644
index 0000000..e69de29
diff --git a/.gitignore b/.gitignore
index 060af60..e4d0eb1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,5 @@ venv/
__pycache__/
dist
itd_sdk.egg-info
-nowkie.gif
\ No newline at end of file
+nowkie.gif
+g.gif
\ No newline at end of file
diff --git a/README.md b/README.md
index 309d491..4a29b31 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,9 @@
# itd-sdk
Клиент ITD для python
+> [!CAUTION]
+> ~~Мой основной аккаунт itd_sdk был забанен. Новый акк - itdsdk. #димонверниаккаунты~~
+> ~~Проект больше не будет обнолвяться! яPR буду мержить~~
+> ладно, буду мейнтйнить потихоньку...
## Установка
@@ -18,10 +22,25 @@ c = ITDClient('TOKEN', 'refresh_token=...; __ddg1_=...; __ddgid_=...; is_auth=1;
print(c.get_me())
```
-
+
+
+### Получение cookies
+
+Для получения access_token требуются cookies с `refresh_token`. Как их получить:
+
+1. Откройте [итд.com](https://xn--d1ah4a.com) в браузере
+2. Откройте DevTools (F12)
+3. Перейдите на вкладку **Network**
+4. Обновите страницу
+5. Найдите запрос к `/auth/refresh`
+6. Скопируйте значение **Cookie** из Request Headers
+> Пример: `refresh_token=123123A67BCdEfGG; is_auth=1`
+> В cookies также могут присутствовать значения типа `__ddgX__` (DDoS-Guard cookies) или `_ym_XXXX` (`X` - любое число или буква). Они необязательные и их наличие не влияет на результат
+
+
---
### Скрипт на обновление имени
@@ -46,10 +65,9 @@ while True:
```python
from itd import ITDClient
-c = ITDClient(None, '...')
+c = ITDClient(None, 'Ваши cookies')
-id = c.upload_file('любое-имя.png', open('реальное-имя-файла.png', 'rb'))['id']
-c.update_profile(banner_id=id)
+c.update_banner('имя-файла.png')
print('баннер обновлен')
```
@@ -64,7 +82,27 @@ c.create_post('тест1') # создание постов
# итд
```
-### SSE - прослушивание уведомлений в реальном времени
+### Стилизация постов ("спаны")
+С обновления 1.3.0 добавлена функция "спанов". Для парсинга пока поддерживается только html, но в будущем будет добавлен markdown.
+```python
+from itd import ITDClient
+from itd.utils import parse_html
+
+с = ITDClient(cookies='refresh_token=123')
+
+print(с.create_post(*parse_html('значит, я это щас отправил со своего клиента, воот. И еще тут спаны написаны через html, по типу < i > 11')))
+```
+Поддерживаемые теги:
+ - ``: жирный
+ - ``: курсивный
+ - ``: зачеркнутый
+ - ``: подчеркнутый
+ - ``: код
+ - ``: спойлер
+ - ``: ссылка
+ - ``: цитата
+
+
### Кастомные запросы
@@ -103,17 +141,10 @@ fetch(c.token, 'метод', 'эндпоинт', {'данные': 'данные'
> [!NOTE]
> `xn--d1ah4a.com` - punycode от "итд.com"
-## Планы
-
- - Форматированные сообщения об ошибках
- - Логирование (через logging)
- - Добавление ООП (отдеьные классы по типу User или Post вместо обычного JSON)
- - Голосовые сообщения
-
## Прочее
-Лицезия: [MIT](./LICENSE)
+Лицезия: [MIT](./LICENSE)
Идея (и часть эндпоинтов): https://github.com/FriceKa/ITD-SDK-js
- По сути этот проект является реворком, просто на другом языке
-Автор: [itd_sdk](https://xn--d1ah4a.com/itd_sdk) (в итд) [@desicars](https://t.me/desicars) (в тг)
+Автор: ~~[itd_sdk](https://xn--d1ah4a.com/itd_sdk) забанили~~ [itdsdk](https://xn--d1ah4a.com/itdsdk) (в итд) [@desicars](https://t.me/desicars) (в тг)
diff --git a/examples/README.md b/examples/README.md
index 870e87e..055735c 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -2,62 +2,6 @@
Эта папка содержит примеры использования ITD SDK для различных сценариев.
-## Структура
-
-```
-examples/
-├── README.md # Этот файл
-└── stream/ # Примеры работы с SSE потоком уведомлений
- ├── basic_stream.py
- ├── stop_stream.py
- ├── filter_notifications.py
- └── notification_logger.py
-```
-
-## Подготовка
-
-Перед запуском примеров установите зависимости:
-
-```bash
-pip install -r ../requirements.txt
-```
-
-## Получение cookies
-
-Все примеры требуют cookies с `refresh_token`. Как их получить:
-
-1. Откройте [итд.com](https://xn--d1ah4a.com) в браузере
-2. Откройте DevTools (F12)
-3. Перейдите на вкладку **Network**
-4. Найдите запрос к `/auth/refresh`
-5. Скопируйте значение **Cookie** из Request Headers
-6. Формат: `refresh_token=...; __ddg1_=...; is_auth=1`
-
-См. `cookie-screen.png` в корне проекта для примера.
-
----
-
-## Stream - Прослушивание уведомлений
-
-Примеры работы с SSE потоком уведомлений в реальном времени.
-
-📁 **Папка:** `stream/`
-📖 **Документация:** [stream/README.md](stream/README.md)
-
-**Примеры:**
-- `basic_stream.py` - Базовое прослушивание всех уведомлений
-- `stop_stream.py` - Программная остановка потока
-- `filter_notifications.py` - Фильтрация по типу уведомлений
-- `notification_logger.py` - Логирование в JSON файл
-
-**Быстрый старт:**
-```bash
-cd stream
-python basic_stream.py
-```
-
----
-
## Дополнительная информация
- [Основной README](../README.md) - Документация по всему SDK
diff --git a/examples/kilyabin/README.md b/examples/kilyabin/README.md
new file mode 100644
index 0000000..7a5dbff
--- /dev/null
+++ b/examples/kilyabin/README.md
@@ -0,0 +1,7 @@
+# Коллекция скриптов от [@kilyabin](https://github.com/kilyabin)
+
+Пока всего два скрипта, однако будет дополняться
+
+Работают через аргументы командной строки (например, `--file` или `--text`)
+
+Есть помощь при аргументе `-h`, потому - разберетесь
diff --git a/examples/kilyabin/itd-change-banner.py b/examples/kilyabin/itd-change-banner.py
new file mode 100644
index 0000000..36b86bf
--- /dev/null
+++ b/examples/kilyabin/itd-change-banner.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+from argparse import ArgumentParser
+from os import getenv
+from os.path import isfile
+import sys
+
+from itd import ITDClient
+
+def main():
+ parser = ArgumentParser(
+ description='Upload image and set it as profile banner'
+ )
+
+ parser.add_argument(
+ '--token',
+ default=getenv('ITD_TOKEN'),
+ help='API token (or ITD_TOKEN env var)'
+ )
+
+ parser.add_argument(
+ '--file',
+ required=True,
+ help='Path to image file'
+ )
+
+ args = parser.parse_args()
+
+ if not args.token:
+ print('❌ Токен не задан (--token или ITD_TOKEN)', file=sys.stderr)
+ quit()
+
+ file_path = args.file
+
+ if not isfile(file_path):
+ print(f'❌ Файл не найден: {file_path}', file=sys.stderr)
+ quit()
+
+ try:
+ client = ITDClient(None, args.token)
+ data, _ = client.update_banner_new(file_path)
+
+ print('✅ Баннер обновлён!')
+ print('📄 Информация о файле:')
+ print(f' id: {data.id}')
+ print(f' filename: {data.filename}')
+ print(f' mime_type: {data.mime_type}')
+ print(f' size: {data.size} bytes')
+ print(f' url: {data.url}')
+
+ except Exception as e:
+ print('❌ Произошла ошибка:', e, file=sys.stderr)
+ quit()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/examples/kilyabin/itd-create-post.py b/examples/kilyabin/itd-create-post.py
new file mode 100644
index 0000000..c8c9739
--- /dev/null
+++ b/examples/kilyabin/itd-create-post.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+from uuid import UUID
+from argparse import ArgumentParser
+from os import getenv
+from os.path import isfile, basename
+
+from itd import ITDClient
+
+def main():
+ parser = ArgumentParser(
+ description='Create a post on ITD via CLI'
+ )
+
+ parser.add_argument(
+ '--token',
+ default=getenv('ITD_TOKEN'),
+ help='Refresh token (or set ITD_TOKEN environment variable)'
+ )
+
+ parser.add_argument(
+ '--text',
+ required=True,
+ help='Text content of the post'
+ )
+
+ parser.add_argument(
+ '--file',
+ help='Optional file to attach to the post'
+ )
+
+ parser.add_argument(
+ '--filename',
+ help='Filename on server (if --file is used, default: local filename)'
+ )
+
+ args = parser.parse_args()
+
+ if not args.token:
+ print('❌ Token not provided (--token or ITD_TOKEN)')
+ quit()
+
+ try:
+ client = ITDClient(None, args.token)
+
+ file_id = None
+ if args.file:
+ if not isfile(args.file):
+ print(f'❌ File not found: {args.file}')
+ quit()
+
+ server_name = args.filename or basename(args.file)
+ with open(args.file, 'rb') as f:
+ response = client.upload_file(server_name, f)
+
+ file_id = str(getattr(response, 'id', None))
+ if not file_id:
+ print('❌ Failed to get file ID')
+ quit()
+ print(f'✅ File uploaded: {response.filename} (id={file_id})')
+
+ # Создаём пост с правильным аргументом 'content'
+ if file_id:
+ post_resp = client.create_post(content=args.text, attach_ids=[UUID(file_id)])
+ else:
+ post_resp = client.create_post(content=args.text)
+
+ print('✅ Post created successfully!')
+ print(f' id: {post_resp.id}')
+ print(f' text: {args.text}')
+ if file_id:
+ print(f' attached file id: {file_id}')
+
+ except Exception as e:
+ print('❌ Error:', e)
+ quit()
+
+if __name__ == '__main__':
+ main()
diff --git a/examples/stream/basic_stream.py b/examples/stream/basic_stream.py
index 6928ea1..cbffb0d 100644
--- a/examples/stream/basic_stream.py
+++ b/examples/stream/basic_stream.py
@@ -5,20 +5,20 @@ import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
-from itd import ITDClient, StreamConnect, StreamNotification
+from itd import ITDClient, StreamConnect
def main():
cookies = 'YOUR_COOKIES_HERE'
-
+
if cookies == 'YOUR_COOKIES_HERE':
print('! Укажите cookies в переменной cookies')
print(' См. examples/README.md для инструкций')
return
-
+
client = ITDClient(cookies=cookies)
-
+
print('-- Подключение к SSE...')
-
+
try:
for event in client.stream_notifications():
if isinstance(event, StreamConnect):
@@ -29,7 +29,7 @@ def main():
if event.preview:
preview = event.preview[:50] + '...' if len(event.preview) > 50 else event.preview
print(f' {preview}')
-
+
except KeyboardInterrupt:
print(f'\n! Отключение...')
diff --git a/examples/stream/filter_notifications.py b/examples/stream/filter_notifications.py
index f26063e..c6b12c6 100644
--- a/examples/stream/filter_notifications.py
+++ b/examples/stream/filter_notifications.py
@@ -5,48 +5,48 @@ import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
-from itd import ITDClient, StreamConnect, StreamNotification
+from itd import ITDClient, StreamConnect
from itd.enums import NotificationType
def main():
cookies = 'YOUR_COOKIES_HERE'
+
if cookies == 'YOUR_COOKIES_HERE':
print('! Укажите cookies в переменной cookies')
print(' См. examples/README.md для инструкций')
return
-
+
client = ITDClient(cookies=cookies)
-
- # Настройка: какие типы уведомлений показывать
+
SHOW_TYPES = {
NotificationType.LIKE,
NotificationType.FOLLOW,
- NotificationType.COMMENT,
+ NotificationType.COMMENT
}
-
+
print('-- Подключение к SSE...')
print(f'-- Фильтр: {", ".join(t.value for t in SHOW_TYPES)}\n')
-
+
try:
for event in client.stream_notifications():
if isinstance(event, StreamConnect):
print(f'✅ Подключено! User ID: {event.user_id}\n')
continue
-
+
if event.type not in SHOW_TYPES:
continue
-
+
# Обработка разных типов
if event.type == NotificationType.LIKE:
print(f'❤️ {event.actor.display_name} лайкнул ваш пост')
-
+
elif event.type == NotificationType.FOLLOW:
print(f'👤 {event.actor.display_name} подписался на вас')
-
+
elif event.type == NotificationType.COMMENT:
print(f'💬 {event.actor.display_name}: {event.preview}')
-
+
except KeyboardInterrupt:
print(f'\n! Отключение...')
diff --git a/examples/stream/notification_logger.py b/examples/stream/notification_logger.py
index fb758b0..9e0bd0b 100644
--- a/examples/stream/notification_logger.py
+++ b/examples/stream/notification_logger.py
@@ -5,29 +5,29 @@ import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
-from itd import ITDClient, StreamConnect, StreamNotification
+from itd import ITDClient, StreamConnect
from datetime import datetime
import json
def main():
cookies = 'YOUR_COOKIES_HERE'
-
+
if cookies == 'YOUR_COOKIES_HERE':
print('! Укажите cookies в переменной cookies')
print(' См. examples/README.md для инструкций')
return
-
+
client = ITDClient(cookies=cookies)
log_file = f'notifications_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log'
-
+
print(f'-- Подключение к SSE...')
print(f'-- Логирование в: {log_file}\n')
-
+
try:
with open(log_file, 'w', encoding='utf-8') as f:
for event in client.stream_notifications():
timestamp = datetime.now().isoformat()
-
+
if isinstance(event, StreamConnect):
log_entry = {
'timestamp': timestamp,
@@ -49,10 +49,10 @@ def main():
'target_id': str(event.target_id) if event.target_id else None
}
print(f'* {event.type.value}: {event.actor.username}')
-
+
f.write(json.dumps(log_entry, ensure_ascii=False) + '\n')
f.flush()
-
+
except KeyboardInterrupt:
print(f'\n! Отключение... Лог сохранен в {log_file}')
diff --git a/examples/stream/stop_stream.py b/examples/stream/stop_stream.py
index ef46cf2..91c29b9 100644
--- a/examples/stream/stop_stream.py
+++ b/examples/stream/stop_stream.py
@@ -6,19 +6,17 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
import threading
-import time
-from itd import ITDClient, StreamConnect, StreamNotification
+from itd import ITDClient, StreamConnect
def main():
- cookies = 'YOUR_COOKIES_HERE'
-
+ cookies = 'YOUR_COOKIES_HERE'
+
if cookies == 'YOUR_COOKIES_HERE':
print('! Укажите cookies в переменной cookies')
return
-
+
client = ITDClient(cookies=cookies)
-
- # Функция для прослушивания в отдельном потоке
+
def listen():
print('! Начинаем прослушивание...')
try:
@@ -29,17 +27,16 @@ def main():
print(f'🔔 {event.type.value}: {event.actor.username}')
except Exception as e:
print(f'! Ошибка: {e}')
-
- # В отдельном потоке
+
thread = threading.Thread(target=listen, daemon=True)
thread.start()
-
+
print('Прослушивание запущено. Нажмите Enter для остановки...')
input()
-
+
print('!! Останавливаем прослушивание...')
client.stop_stream()
-
+
thread.join(timeout=5)
print('! Остановлено')
diff --git a/itd/client.py b/itd/client.py
index 7d36255..10dd4d2 100644
--- a/itd/client.py
+++ b/itd/client.py
@@ -1,20 +1,19 @@
-# from warnings import deprecated
from uuid import UUID
from _io import BufferedReader
from typing import cast, Iterator
from datetime import datetime
-import json
-import time
+from json import JSONDecodeError, loads
+from time import sleep
from requests.exceptions import ConnectionError, HTTPError
from sseclient import SSEClient
-from itd.routes.users import get_user, update_profile, follow, unfollow, get_followers, get_following, update_privacy
+from itd.routes.users import get_user, update_profile, follow, unfollow, get_followers, get_following, update_privacy, update_privacy_new
from itd.routes.etc import get_top_clans, get_who_to_follow, get_platform_status
from itd.routes.comments import get_comments, add_comment, delete_comment, like_comment, unlike_comment, add_reply_comment, get_replies
from itd.routes.hashtags import get_hashtags, get_posts_by_hashtag
from itd.routes.notifications import get_notifications, mark_as_read, mark_all_as_read, get_unread_notifications_count, stream_notifications
-from itd.routes.posts import create_post, get_posts, get_post, edit_post, delete_post, pin_post, repost, view_post, get_liked_posts, restore_post, like_post, unlike_post, get_user_posts
+from itd.routes.posts import create_post, get_posts, get_post, edit_post, delete_post, pin_post, repost, view_post, get_liked_posts, restore_post, like_post, unlike_post, get_user_posts, vote
from itd.routes.reports import report
from itd.routes.search import search
from itd.routes.files import upload_file, get_file, delete_file
@@ -24,10 +23,10 @@ from itd.routes.pins import get_pins, remove_pin, set_pin
from itd.models.comment import Comment
from itd.models.notification import Notification
-from itd.models.post import Post, NewPost
+from itd.models.post import Post, NewPost, PollData, Poll, Span
from itd.models.clan import Clan
from itd.models.hashtag import Hashtag
-from itd.models.user import User, UserProfileUpdate, UserPrivacy, UserFollower, UserWhoToFollow
+from itd.models.user import User, UserProfileUpdate, UserPrivacy, UserFollower, UserWhoToFollow, UserPrivacyData
from itd.models.pagination import Pagination, PostsPagintaion, LikedPostsPagintaion
from itd.models.verification import Verification, VerificationStatus
from itd.models.report import NewReport
@@ -41,7 +40,7 @@ from itd.exceptions import (
NoCookie, NoAuthData, SamePassword, InvalidOldPassword, NotFound, ValidationError, UserBanned,
PendingRequestExists, Forbidden, UsernameTaken, CantFollowYourself, Unauthorized,
CantRepostYourPost, AlreadyReposted, AlreadyReported, TooLarge, PinNotOwned, NoContent,
- AlreadyFollowing, NotFoundOrForbidden
+ AlreadyFollowing, NotFoundOrForbidden, OptionsNotBelong, NotMultipleChoice, EmptyOptions
)
@@ -195,7 +194,7 @@ class Client:
@refresh_on_error
def update_privacy(self, wall_closed: bool = False, private: bool = False) -> UserPrivacy:
- """Обновить настройки приватности
+ """(УСТАРЕЛО! Используйте update_privacy_new) настройки приватности
Args:
wall_closed (bool, optional): Закрыть стену. Defaults to False.
@@ -209,6 +208,21 @@ class Client:
return UserPrivacy.model_validate(res.json())
+ @refresh_on_error
+ def update_privacy_new(self, privacy: UserPrivacyData) -> UserPrivacy:
+ """Обновить настройки приватности
+
+ Args:
+ privacy (UserPrivacyData): Данные приватности
+
+ Returns:
+ UserPrivacy: Обновленные данные приватности
+ """
+ res = update_privacy_new(self.token, privacy)
+ res.raise_for_status()
+
+ return UserPrivacy.model_validate(res.json())
+
@refresh_on_error
def follow(self, username: str) -> int:
"""Подписаться на пользователя
@@ -455,13 +469,13 @@ class Client:
@refresh_on_error
def get_replies(self, comment_id: UUID, limit: int = 50, page: int = 1, sort: str = 'oldest') -> tuple[list[Comment], Pagination]:
- """Получить список комментариев
+ """Получить список ответов на комментарий
Args:
comment_id (UUID): UUID поста
limit (int, optional): Лимит. Defaults to 50.
page (int, optional): Курсор (сколько пропустить). Defaults to 1.
- sort (str, optional): Сортировка. Defaults to 'oldesr'.
+ sort (str, optional): Сортировка. Defaults to 'oldest'.
Raises:
NotFound: Пост не найден
@@ -616,7 +630,6 @@ class Client:
res = mark_all_as_read(self.token)
res.raise_for_status()
-
@refresh_on_error
def get_unread_notifications_count(self) -> int:
"""Получить количество непрочитанных уведомлений
@@ -631,13 +644,15 @@ class Client:
@refresh_on_error
- def create_post(self, content: str, wall_recipient_id: UUID | None = None, attach_ids: list[UUID] = []) -> NewPost:
+ def create_post(self, content: str | None = None, spans: list[Span] = [], wall_recipient_id: UUID | None = None, attachment_ids: list[UUID] = [], poll: PollData | None = None) -> NewPost:
"""Создать пост
Args:
- content (str): Содержимое
+ content (str | None, optional): Содержимое. Defaults to None.
+ spans (lsit[Span], optional): Стилизация содержимого. Defaults to [].
wall_recipient_id (UUID | None, optional): UUID пользователя (чтобы создать пост ему на стене). Defaults to None.
- attach_ids (list[UUID], optional): UUID вложений. Defaults to [].
+ attachment_ids (list[UUID], optional): UUID вложений. Defaults to [].
+ poll (PollData | None, optional): Опрос. Defaults to None.
Raises:
NotFound: Пользователь не найден
@@ -646,7 +661,8 @@ class Client:
Returns:
NewPost: Новый пост
"""
- res = create_post(self.token, content, wall_recipient_id, attach_ids)
+ res = create_post(self.token, content, [span.model_dump(mode="json") for span in spans], wall_recipient_id, attachment_ids, poll.poll if poll else None)
+
if res.json().get('error', {}).get('code') == 'NOT_FOUND':
raise NotFound('Wall recipient')
if res.status_code == 422 and 'found' in res.json():
@@ -655,6 +671,41 @@ class Client:
return NewPost.model_validate(res.json())
+ @refresh_on_error
+ def vote(self, id: UUID, option_ids: list[UUID]) -> Poll:
+ """Проголосовать в опросе
+
+ Args:
+ id (UUID): UUID поста
+ option_ids (list[UUID]): Список UUID вариантов
+
+ Raises:
+ EmptyOptions: Пустые варианты
+ NotFound: Пост не найден или в посте нет опроса
+ NotFound: _description_
+ OptionsNotBelong: Неверные варианты (варинты не пренадлежат опросу)
+ NotMultipleChoice: Можно выбрать только 1 вариант (для опросов, где не разрешены несколько ответов)
+
+ Returns:
+ Poll: Опрос
+ """
+ if not option_ids:
+ raise EmptyOptions()
+
+ res = vote(self.token, id, option_ids)
+
+ if res.json().get('error', {}).get('code') == 'NOT_FOUND' and res.json().get('error', {}).get('message') == 'Опрос не найден':
+ raise NotFound('Poll')
+ if res.json().get('error', {}).get('code') == 'NOT_FOUND':
+ raise NotFound('Post')
+ if res.json().get('error', {}).get('code') == 'VALIDATION_ERROR' and res.json().get('error', {}).get('message') == 'Один или несколько вариантов не принадлежат этому опросу':
+ raise OptionsNotBelong()
+ if res.json().get('error', {}).get('code') == 'VALIDATION_ERROR' and res.json().get('error', {}).get('message') == 'В этом опросе можно выбрать только один вариант':
+ raise NotMultipleChoice()
+ res.raise_for_status()
+
+ return Poll.model_validate(res.json()['data'])
+
@refresh_on_error
def get_posts(self, cursor: int = 0, tab: PostsTab = PostsTab.POPULAR) -> tuple[list[Post], PostsPagintaion]:
"""Получить список постов
@@ -994,8 +1045,9 @@ class Client:
return File.model_validate(res.json())
+ # @deprecated # Этот декоратор появился в 3.13, а наша библиотека поддерживает с 3.9
def update_banner(self, name: str) -> UserProfileUpdate:
- """Обновить банер (шорткат из upload_file + update_profile)
+ """[DEPRECATED] Обновить банер (шорткат из upload_file + update_profile)
Args:
name (str): Имя файла
@@ -1006,6 +1058,19 @@ class Client:
id = self.upload_file(name, cast(BufferedReader, open(name, 'rb'))).id
return self.update_profile(banner_id=id)
+ def update_banner_new(self, name: str) -> tuple[File, UserProfileUpdate]:
+ """Обновить банер (шорткат из upload_file + update_profile)
+
+ Args:
+ name (str): Имя файла
+
+ Returns:
+ File: Загруженный файл
+ UserProfileUpdate: Обновленный профиль
+ """
+ file = self.upload_file(name, cast(BufferedReader, open(name, 'rb')))
+ return file, self.update_profile(banner_id=file.id)
+
@refresh_on_error
def restore_post(self, post_id: UUID) -> None:
"""Восстановить удалённый пост
@@ -1088,62 +1153,63 @@ class Client:
return res.json()['pin']
+
@refresh_on_error
def stream_notifications(self) -> Iterator[StreamConnect | StreamNotification]:
"""Слушать SSE поток уведомлений
-
+
Yields:
StreamConnect | StreamNotification: События подключения или уведомления
-
+
Example:
```python
from itd import ITDClient
-
+
client = ITDClient(cookies='refresh_token=...')
-
+
# Запуск прослушивания
for event in client.stream_notifications():
if isinstance(event, StreamConnect):
print(f'Подключено: {event.user_id}')
else:
print(f'Уведомление: {event.type} от {event.actor.username}')
-
+
# Остановка из другого потока или обработчика
# client.stop_stream()
```
"""
self._stream_active = True
-
+
while self._stream_active:
try:
response = stream_notifications(self.token)
response.raise_for_status()
-
+
client = SSEClient(response)
-
+
for event in client.events():
if not self._stream_active:
response.close()
return
-
+
try:
if not event.data or event.data.strip() == '':
continue
-
- data = json.loads(event.data)
-
+
+ data = loads(event.data)
+
if 'userId' in data and 'timestamp' in data and 'type' not in data:
yield StreamConnect.model_validate(data)
else:
yield StreamNotification.model_validate(data)
-
- except json.JSONDecodeError:
+
+ except JSONDecodeError:
print(f'Не удалось распарсить сообщение: {event.data}')
continue
except Exception as e:
print(f'Ошибка обработки события: {e}')
continue
-
+
except Unauthorized:
if self.cookies and self._stream_active:
print('Токен истек, обновляем...')
@@ -1155,27 +1221,27 @@ class Client:
if not self._stream_active:
return
print(f'Ошибка соединения: {e}, переподключение через 5 секунд...')
- time.sleep(5)
+ sleep(5)
continue
-
+
def stop_stream(self):
"""Остановить прослушивание SSE потока
-
+
Example:
```python
import threading
from itd import ITDClient
-
+
client = ITDClient(cookies='refresh_token=...')
-
+
# Запуск в отдельном потоке
def listen():
for event in client.stream_notifications():
print(event)
-
+
thread = threading.Thread(target=listen)
thread.start()
-
+
# Остановка через 10 секунд
import time
time.sleep(10)
@@ -1183,4 +1249,5 @@ class Client:
thread.join()
```
"""
+ print('stop event')
self._stream_active = False
\ No newline at end of file
diff --git a/itd/enums.py b/itd/enums.py
index a4255aa..f3cc0c4 100644
--- a/itd/enums.py
+++ b/itd/enums.py
@@ -33,3 +33,21 @@ class AttachType(Enum):
class PostsTab(Enum):
FOLLOWING = 'following'
POPULAR = 'popular'
+
+class AccessType(Enum):
+ """Типы разрешений для видимости лайков и записей на стене"""
+ NOBODY = 'nobody' # никто
+ MUTUAL = 'mutual' # взаимные
+ FOLLOWERS = 'followers' # подписчики
+ EVERYONE = 'everyone' # все
+
+class SpanType(Enum):
+ MONOSPACE = 'monospace' # моноширный (код)
+ STRIKE = 'strike' # зачеркнутый
+ BOLD = 'bold' # жирный
+ ITALIC = 'italic' # курсив
+ SPOILER = 'spoiler' # спойлер
+ UNDERLINE = 'underline' # подчеркнутый
+ HASHTAG = 'hashtag' # хэштэг ? (появляется только при получении постов, при создании нету)
+ LINK = 'link' # ссылка
+ QUOTE = 'quote' # цитата
diff --git a/itd/exceptions.py b/itd/exceptions.py
index e8a8b51..22767f2 100644
--- a/itd/exceptions.py
+++ b/itd/exceptions.py
@@ -49,11 +49,11 @@ class UserBanned(Exception):
return 'User banned'
class ValidationError(Exception):
- def __init__(self, name: str, value: str):
- self.name = name
- self.value = value
+ # def __init__(self, name: str, value: str):
+ # self.name = name
+ # self.value = value
def __str__(self):
- return f'Failed validation on {self.name}: "{self.value}"'
+ return 'Failed validation'# on {self.name}: "{self.value}"'
class PendingRequestExists(Exception):
def __str__(self):
@@ -113,4 +113,24 @@ class NoContent(Exception):
class AlreadyFollowing(Exception):
def __str__(self) -> str:
- return 'Already following user'
\ No newline at end of file
+ return 'Already following user'
+
+class AccountBanned(Exception):
+ def __str__(self) -> str:
+ return 'Account has been deactivated'
+
+class OptionsNotBelong(Exception):
+ def __str__(self) -> str:
+ return 'One or more options do not belong to poll'
+
+class NotMultipleChoice(Exception):
+ def __str__(self) -> str:
+ return 'Only one option can be choosen in this poll'
+
+class EmptyOptions(Exception):
+ def __str__(self) -> str:
+ return 'Options cannot be empty (pre-validation)'
+
+class ProfileRequired(Exception):
+ def __str__(self) -> str:
+ return 'No profile. Please create your profile first'
\ No newline at end of file
diff --git a/itd/models/event.py b/itd/models/event.py
index 775bd8c..fa4ef2d 100644
--- a/itd/models/event.py
+++ b/itd/models/event.py
@@ -1,11 +1,8 @@
from uuid import UUID
-from datetime import datetime
-from typing import Literal
from pydantic import BaseModel, Field
-from itd.enums import NotificationType, NotificationTargetType
-from itd.models.user import UserNotification
+from itd.models.notification import Notification
class StreamConnect(BaseModel):
@@ -14,20 +11,7 @@ class StreamConnect(BaseModel):
timestamp: int
-class StreamNotification(BaseModel):
+class StreamNotification(Notification):
"""Уведомление из SSE потока"""
- id: UUID
- type: NotificationType
-
- target_type: NotificationTargetType | None = Field(None, alias='targetType')
- target_id: UUID | None = Field(None, alias='targetId')
-
- preview: str | None = None
- read_at: datetime | None = Field(None, alias='readAt')
- created_at: datetime = Field(alias='createdAt')
-
user_id: UUID = Field(alias='userId')
- actor: UserNotification
-
- read: bool = False
sound: bool = True
diff --git a/itd/models/post.py b/itd/models/post.py
index 429de17..1d68deb 100644
--- a/itd/models/post.py
+++ b/itd/models/post.py
@@ -1,46 +1,104 @@
from uuid import UUID
+from datetime import datetime
-from pydantic import Field, BaseModel
+from pydantic import Field, BaseModel, field_validator
from itd.models.user import UserPost, UserNewPost
from itd.models._text import TextObject
from itd.models.file import PostAttach
from itd.models.comment import Comment
+from itd.enums import SpanType
-class _PostShort(TextObject):
+class NewPollOption(BaseModel):
+ text: str
+
+
+class PollOption(NewPollOption):
+ id: UUID
+ position: int = 0
+ votes: int = Field(0, alias='votesCount')
+
+
+class _Poll(BaseModel):
+ multiple: bool = Field(False, alias='multipleChoice')
+ question: str
+
+
+class NewPoll(_Poll):
+ options: list[NewPollOption]
+ model_config = {'serialize_by_alias': True}
+
+
+class PollData:
+ def __init__(self, question: str, options: list[str], multiple: bool = False):
+ self.poll = NewPoll(question=question, options=[NewPollOption(text=option) for option in options], multipleChoice=multiple)
+
+
+class Poll(_Poll):
+ id: UUID
+ post_id: UUID = Field(alias='postId')
+
+ options: list[PollOption]
+ votes: int = Field(0, alias='totalVotes')
+ is_voted: bool = Field(False, alias='hasVoted')
+ voted_option_ids: list[UUID] = Field([], alias='votedOptionIds')
+
+ created_at: datetime = Field(alias='createdAt')
+
+ @field_validator('created_at', mode='plain')
+ @classmethod
+ def validate_created_at(cls, v: str):
+ try:
+ return datetime.strptime(v + '00', '%Y-%m-%d %H:%M:%S.%f%z')
+ except ValueError:
+ return datetime.strptime(v, '%Y-%m-%dT%H:%M:%S.%f')
+
+
+class Span(BaseModel):
+ length: int
+ offset: int
+ type: SpanType
+ url: str | None = None
+
+
+class _PostCounts(TextObject):
likes_count: int = Field(0, alias='likesCount')
comments_count: int = Field(0, alias='commentsCount')
reposts_count: int = Field(0, alias='repostsCount')
views_count: int = Field(0, alias='viewsCount')
+ spans: list[Span] = []
-class PostShort(_PostShort):
+
+class _PostAuthor(_PostCounts):
author: UserPost
-class OriginalPost(PostShort):
+class OriginalPost(_PostAuthor):
is_deleted: bool = Field(False, alias='isDeleted')
-class _Post(_PostShort):
+class _Post(_PostCounts):
is_liked: bool = Field(False, alias='isLiked')
is_reposted: bool = Field(False, alias='isReposted')
is_viewed: bool = Field(False, alias='isViewed')
is_owner: bool = Field(False, alias='isOwner')
+ is_pinned: bool = Field(False, alias='isPinned') # only for user wall
attachments: list[PostAttach] = []
comments: list[Comment] = []
- original_post: OriginalPost | None = None
+ original_post: OriginalPost | None = None # for reposts
wall_recipient_id: UUID | None = Field(None, alias='wallRecipientId')
wall_recipient: UserPost | None = Field(None, alias='wallRecipient')
-class Post(_Post, PostShort):
- pass
+class Post(_Post, _PostAuthor):
+ poll: Poll | None = None
class NewPost(_Post):
author: UserNewPost
+ poll: NewPoll | None = None
diff --git a/itd/models/user.py b/itd/models/user.py
index 5a90f02..868c0e4 100644
--- a/itd/models/user.py
+++ b/itd/models/user.py
@@ -4,13 +4,41 @@ from datetime import datetime
from pydantic import BaseModel, Field
from itd.models.pin import ShortPin
+from itd.enums import AccessType
-class UserPrivacy(BaseModel):
+class _UserPrivacy(BaseModel):
private: bool | None = Field(None, alias='isPrivate') # none for not me
- wall_closed: bool = Field(False, alias='wallClosed')
+ wall_closed: bool | None = Field(None, alias='wallClosed', deprecated=True)
+ wall_access: AccessType = Field(AccessType.EVERYONE, alias='wallAccess')
+ likes_visibility: AccessType = Field(AccessType.EVERYONE, alias='likesVisibility')
- model_config = {'populate_by_name': True}
+ model_config = {'serialize_by_alias': True}
+
+
+class UserPrivacy(_UserPrivacy):
+ show_last_seen: bool = Field(True, alias='showLastSeen')
+
+
+class UserPrivacyData:
+ def __init__(self, private: bool | None = None, wall_access: AccessType | None = None, likes_visibility: AccessType | None = None, show_last_seen: bool | None = None) -> None:
+ self.private = private
+ self.wall_access = wall_access
+ self.likes_visibility = likes_visibility
+ self.show_last_seen = show_last_seen
+
+ def to_dict(self):
+ data = {}
+ if self.private is not None:
+ data['isPrivate'] = self.private
+ if self.wall_access is not None:
+ data['wallAccess'] = self.wall_access.value
+ if self.likes_visibility is not None:
+ data['likesVisibility'] = self.likes_visibility.value
+ if self.show_last_seen is not None:
+ data['showLastSeen'] = self.show_last_seen
+
+ return data
class UserProfileUpdate(BaseModel):
@@ -51,7 +79,7 @@ class UserSearch(UserFollower, UserWhoToFollow):
pass
-class User(UserSearch, UserPrivacy):
+class User(UserSearch, _UserPrivacy):
banner: str | None = None
bio: str | None = None
pinned_post_id: UUID | None = Field(None, alias='pinnedPostId')
@@ -62,3 +90,5 @@ class User(UserSearch, UserPrivacy):
is_followed: bool | None = Field(None, alias='isFollowedBy') # none for me
created_at: datetime = Field(alias='createdAt')
+ last_seen_at: datetime | None = Field(None, alias='lastSeen')
+ online: bool = False
diff --git a/itd/request.py b/itd/request.py
index c634258..a5acaa0 100644
--- a/itd/request.py
+++ b/itd/request.py
@@ -3,7 +3,7 @@ from _io import BufferedReader
from requests import Session
from requests.exceptions import JSONDecodeError
-from itd.exceptions import InvalidToken, InvalidCookie, RateLimitExceeded, Unauthorized
+from itd.exceptions import InvalidToken, InvalidCookie, RateLimitExceeded, Unauthorized, AccountBanned, ProfileRequired
s = Session()
@@ -39,6 +39,10 @@ def fetch(token: str, method: str, url: str, params: dict = {}, files: dict[str,
raise RateLimitExceeded(res.json()['error'].get('retryAfter', 0))
if res.json().get('error', {}).get('code') == 'UNAUTHORIZED':
raise Unauthorized()
+ if res.json().get('error', {}).get('code') in ('ACCOUNT_BANNED', 'USER_BLOCKED'):
+ raise AccountBanned()
+ if res.json().get('error', {}).get('code') == 'PROFILE_REQUIRED':
+ raise ProfileRequired()
except (JSONDecodeError, AttributeError):
pass # todo
diff --git a/itd/routes/hashtags.py b/itd/routes/hashtags.py
index 83f3a93..057dc7a 100644
--- a/itd/routes/hashtags.py
+++ b/itd/routes/hashtags.py
@@ -1,4 +1,3 @@
-from warnings import deprecated
from uuid import UUID
from itd.request import fetch
diff --git a/itd/routes/notifications.py b/itd/routes/notifications.py
index a13111d..aa91325 100644
--- a/itd/routes/notifications.py
+++ b/itd/routes/notifications.py
@@ -16,7 +16,7 @@ def get_unread_notifications_count(token: str):
def stream_notifications(token: str):
"""Получить SSE поток уведомлений
-
+
Returns:
Response: Streaming response для SSE
"""
diff --git a/itd/routes/posts.py b/itd/routes/posts.py
index 30eb802..6bacc1b 100644
--- a/itd/routes/posts.py
+++ b/itd/routes/posts.py
@@ -3,13 +3,18 @@ from uuid import UUID
from itd.request import fetch
from itd.enums import PostsTab
+from itd.models.post import NewPoll
-def create_post(token: str, content: str, wall_recipient_id: UUID | None = None, attachment_ids: list[UUID] = []):
- data: dict = {'content': content}
+def create_post(token: str, content: str | None = None, spans: list[dict] = [], wall_recipient_id: UUID | None = None, attachment_ids: list[UUID] = [], poll: NewPoll | None = None):
+ data: dict = {'content': content or ''}
+ if spans:
+ data['spans'] = spans
if wall_recipient_id:
data['wallRecipientId'] = str(wall_recipient_id)
if attachment_ids:
data['attachmentIds'] = list(map(str, attachment_ids))
+ if poll:
+ data['poll'] = poll.model_dump()
return fetch(token, 'post', 'posts', data)
@@ -43,11 +48,14 @@ def get_liked_posts(token: str, username_or_id: str | UUID, limit: int = 20, cur
def get_user_posts(token: str, username_or_id: str | UUID, limit: int = 20, cursor: datetime | None = None):
return fetch(token, 'get', f'posts/user/{username_or_id}', {'limit': limit, 'cursor': cursor})
-def restore_post(token: str, post_id: UUID):
- return fetch(token, "post", f"posts/{post_id}/restore",)
+def restore_post(token: str, id: UUID):
+ return fetch(token, "post", f"posts/{id}/restore",)
-def like_post(token: str, post_id: UUID):
- return fetch(token, "post", f"posts/{post_id}/like")
+def like_post(token: str, id: UUID):
+ return fetch(token, "post", f"posts/{id}/like")
-def unlike_post(token: str, post_id: UUID):
- return fetch(token, "delete", f"posts/{post_id}/like")
+def unlike_post(token: str, id: UUID):
+ return fetch(token, "delete", f"posts/{id}/like")
+
+def vote(token: str, id: UUID, options: list[UUID]):
+ return fetch(token, 'post', f'posts/{id}/poll/vote', {'optionIds': [str(option) for option in options]})
diff --git a/itd/routes/users.py b/itd/routes/users.py
index ddbbfa4..4f81a96 100644
--- a/itd/routes/users.py
+++ b/itd/routes/users.py
@@ -1,6 +1,7 @@
from uuid import UUID
from itd.request import fetch
+from itd.models.user import UserPrivacyData
def get_user(token: str, username: str):
@@ -26,6 +27,9 @@ def update_privacy(token: str, wall_closed: bool = False, private: bool = False)
data['isPrivate'] = private
return fetch(token, 'put', 'users/me/privacy', data)
+def update_privacy_new(token: str, privacy: UserPrivacyData):
+ return fetch(token, 'put', 'users/me/privacy', privacy.to_dict())
+
def follow(token: str, username: str):
return fetch(token, 'post', f'users/{username}/follow')
diff --git a/itd/routes/verification.py b/itd/routes/verification.py
index ec643d5..df00509 100644
--- a/itd/routes/verification.py
+++ b/itd/routes/verification.py
@@ -1,14 +1,8 @@
-from warnings import deprecated
from itd.request import fetch
def verify(token: str, file_url: str):
# {"success":true,"request":{"id":"fc54e54f-8586-4d8c-809e-df93161f99da","userId":"9096a85b-c319-483e-8940-6921be427ad0","videoUrl":"https://943701f000610900cbe86b72234e451d.bckt.ru/videos/354f28a6-9ac7-48a6-879a-a454062b1d6b.mp4","status":"pending","rejectionReason":null,"reviewedBy":null,"reviewedAt":null,"createdAt":"2026-01-30T12:58:14.228Z","updatedAt":"2026-01-30T12:58:14.228Z"}}
return fetch(token, 'post', 'verification/submit', {'videoUrl': file_url})
-@deprecated("verificate устарела используйте verify")
-def verificate(token: str, file_url: str):
- return verify(token, file_url)
-
-
def get_verification_status(token: str):
return fetch(token, 'get', 'verification/status')
\ No newline at end of file
diff --git a/itd/utils.py b/itd/utils.py
new file mode 100644
index 0000000..3f682b1
--- /dev/null
+++ b/itd/utils.py
@@ -0,0 +1,195 @@
+# новая версия от чат гпт. у меня самого не получилось сделать
+import re
+from itd.models.post import Span
+from itd.enums import SpanType
+
+
+class Tag:
+ def __init__(self, open: str, close: str, type: SpanType):
+ self.open = open
+ self.close = close
+ self.type = type
+
+
+def _parse_spans(text: str, tags: list[Tag]) -> tuple[str, list[Span]]:
+ spans: list[Span] = []
+ stack: list[tuple[int, SpanType, int, int, str | None]] = []
+ clean_chars: list[str] = []
+ i = 0
+
+ while i < len(text):
+ # Проверка на экранирование
+ escaped = text[i] == '\\'
+
+ # Сначала проверяем закрывающие теги (с проверкой на экранирование)
+ closed = False
+ for idx, tag in enumerate(tags):
+ if text.startswith(tag.close, i) and stack and stack[-1][0] == idx:
+ if escaped:
+ # Экранированный закрывающий тег — выводим как текст (без слэша)
+ clean_chars.append(tag.close)
+ i += len(tag.close)
+ closed = True
+ break
+ _, span_type, offset, _, url = stack.pop()
+ spans.append(Span(length=len(clean_chars) - offset, offset=offset, type=span_type, url=url))
+ i += len(tag.close)
+ closed = True
+ break
+ if closed:
+ continue
+
+ # Затем проверяем открывающие теги
+ opened = False
+ for idx, tag in enumerate(tags):
+ if tag.type == SpanType.LINK:
+ if escaped:
+ match = re.match(tag.open, text[i+1:])
+ if match:
+ # Экранированный открывающий тег — выводим как текст (без слэша)
+ clean_chars.append(match.group(0))
+ i += 1 + match.end()
+ opened = True
+ break
+ else:
+ match = re.match(tag.open, text[i:])
+ if match:
+ url = match.group(1) if match.groups() else None
+ stack.append((idx, tag.type, len(clean_chars), i, url))
+ i += match.end()
+ opened = True
+ break
+ elif text.startswith(tag.open, i):
+ if escaped:
+ # Экранированный обычный тег — пропускаем, будет обработан в блоке is_escape
+ break
+ stack.append((idx, tag.type, len(clean_chars), i, None))
+ i += len(tag.open)
+ opened = True
+ break
+ if opened:
+ continue
+
+ # Если это слэш, проверяем, не экранирует ли он следующий тег
+ if escaped:
+ # Проверяем, следует ли за слэшем тег
+ is_escape = False
+ for tag in tags:
+ if tag.type == SpanType.LINK:
+ if re.match(tag.open, text[i+1:]):
+ is_escape = True
+ break
+ elif text.startswith(tag.open, i+1):
+ is_escape = True
+ break
+ # Проверяем закрывающие теги
+ if not is_escape:
+ for tag in tags:
+ if text.startswith(tag.close, i+1):
+ is_escape = True
+ break
+
+ if is_escape:
+ # Пропускаем слэш и выводим следующий тег как текст
+ i += 1
+ # Находим и выводим экранированный тег
+ skip = False
+ for tag in tags:
+ if tag.type == SpanType.LINK:
+ match = re.match(tag.open, text[i:])
+ if match:
+ clean_chars.append(match.group(0))
+ i += match.end()
+ skip = True
+ break
+ elif text.startswith(tag.open, i):
+ clean_chars.append(tag.open)
+ i += len(tag.open)
+ skip = True
+ break
+ if not skip:
+ # Проверяем закрывающие теги
+ for tag in tags:
+ if text.startswith(tag.close, i):
+ clean_chars.append(tag.close)
+ i += len(tag.close)
+ skip = True
+ break
+ continue
+
+ clean_chars.append(text[i])
+ i += 1
+
+ if stack:
+ _, last_type, _, raw_pos, _ = stack[-1]
+ raise ValueError(f'No closing tag for {last_type.value} at pos {raw_pos}')
+
+ spans.sort(key=lambda span: span.offset)
+ return ''.join(clean_chars), spans
+
+
+def parse_html(text: str) -> tuple[str, list[Span]]:
+ return _parse_spans(
+ text,
+ [
+ Tag('', '', SpanType.BOLD),
+ Tag('', '', SpanType.ITALIC),
+ Tag('', '', SpanType.STRIKE),
+ Tag('', '', SpanType.UNDERLINE),
+ Tag('', '', SpanType.MONOSPACE),
+ Tag('', '', SpanType.SPOILER),
+ Tag(r'', '', SpanType.LINK),
+ Tag(r'', '
', SpanType.QUOTE),
+ ],
+ )
+
+
+# версия от человека (не работает с вложенными тэгами)
+# from re import finditer, Match
+
+# from itd.models.post import Span
+# from itd.enums import SpanType
+
+
+# class Tag:
+# def __init__(self, open: str, close: str, type: SpanType):
+# self.open = open
+# self.close = close
+# self.type = type
+
+# def raise_error(self, pos: int):
+# raise ValueError(f'No closing tag for {self.type.value} at pos {pos - len(self.open)}')
+
+# def to_span(self, start: int, end: int) -> Span:
+# return Span(length=end - (start - len(self.open)), offset=start - len(self.open), type=self.type)
+
+# def get_pos(self, match: Match[str], text: str, offset: int) -> tuple[int, int, str]:
+# start = match.end() - offset
+# text = text[:match.start() - offset] + text[start:]
+# end = text.find(self.close, start)
+# if end == -1:
+# self.raise_error(start)
+
+# return start - len(self.open), end, text[:end] + text[end + len(self.close):]
+
+
+# def parse_html(text: str) -> tuple[str, list[Span]]:
+# spans = []
+
+# for tag in [
+# Tag('', '', SpanType.BOLD),
+# Tag('', '', SpanType.ITALIC),
+# Tag('', '', SpanType.STRIKE),
+# Tag('', '', SpanType.UNDERLINE),
+# Tag('', '', SpanType.MONOSPACE),
+# Tag('', '', SpanType.SPOILER),
+# ]:
+
+# offset = 0
+# full_text = text
+# for match in finditer(tag.open, full_text):
+# start, end, text = tag.get_pos(match, text, offset)
+# spans.append(tag.to_span(start, end))
+# offset += len(tag.open) + len(tag.close)
+
+# return text, spans
diff --git a/pyproject.toml b/pyproject.toml
index d5390c8..1d718a8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "itd-sdk"
-version = "1.1.0"
+version = "1.2.0"
description = "ITD client for python"
readme = "README.md"
authors = [
@@ -12,6 +12,6 @@ authors = [
]
license = "MIT"
dependencies = [
- "requests", "pydantic"
+ "requests", "pydantic", "sseclient-py"
]
requires-python = ">=3.9"
diff --git a/setup.py b/setup.py
index 8f007b7..b04f3fe 100644
--- a/setup.py
+++ b/setup.py
@@ -2,10 +2,10 @@ from setuptools import setup, find_packages
setup(
name='itd-sdk',
- version='1.1.0',
+ version='1.2.0',
packages=find_packages(),
install_requires=[
- 'requests', 'pydantic'
+ 'requests', 'pydantic', 'sseclient-py'
],
python_requires=">=3.9"
)