feat: add models part 2

This commit is contained in:
firedotguy
2026-01-31 18:28:23 +03:00
parent a388426d8d
commit 2a9f7da9a9
9 changed files with 183 additions and 48 deletions

View File

@@ -6,7 +6,7 @@ from requests.exceptions import HTTPError
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
from itd.routes.etc import get_top_clans, get_who_to_follow, get_platform_status 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 from itd.routes.comments import get_comments, add_comment, delete_comment, like_comment, unlike_comment, add_reply_comment
from itd.routes.hashtags import get_hastags, get_posts_by_hastag from itd.routes.hashtags import get_hastags, get_posts_by_hastag
from itd.routes.notifications import get_notifications, mark_as_read, mark_all_as_read, get_unread_notifications_count from itd.routes.notifications import get_notifications, mark_as_read, mark_all_as_read, get_unread_notifications_count
from itd.routes.posts import create_post, get_posts, get_post, edit_post, delete_post, pin_post, repost, view_post, get_liked_posts from itd.routes.posts import create_post, get_posts, get_post, edit_post, delete_post, pin_post, repost, view_post, get_liked_posts
@@ -16,13 +16,14 @@ from itd.routes.files import upload_file
from itd.routes.auth import refresh_token, change_password, logout from itd.routes.auth import refresh_token, change_password, logout
from itd.routes.verification import verificate, get_verification_status from itd.routes.verification import verificate, get_verification_status
from itd.models.comment import Comment
from itd.models.clan import Clan from itd.models.clan import Clan
from itd.models.user import User, UserProfileUpdate, UserPrivacy, UserFollower, UserWhoToFollow from itd.models.user import User, UserProfileUpdate, UserPrivacy, UserFollower, UserWhoToFollow
from itd.models.pagination import Pagination from itd.models.pagination import Pagination
from itd.models.verification import Verification, VerificationStatus from itd.models.verification import Verification, VerificationStatus
from itd.request import set_cookies from itd.request import set_cookies
from itd.exceptions import NoCookie, NoAuthData, SamePassword, InvalidOldPassword, UserNotFound, InvalidProfileData, UserBanned, PendingRequestExists from itd.exceptions import NoCookie, NoAuthData, SamePassword, InvalidOldPassword, NotFound, ValidationError, UserBanned, PendingRequestExists
def refresh_on_error(func): def refresh_on_error(func):
@@ -122,7 +123,7 @@ class Client:
username (str): username или "me" username (str): username или "me"
Raises: Raises:
UserNotFound: Пользователь не найден NotFound: Пользователь не найден
UserBanned: Пользователь заблокирован UserBanned: Пользователь заблокирован
Returns: Returns:
@@ -130,7 +131,7 @@ class Client:
""" """
res = get_user(self.token, username) res = get_user(self.token, username)
if res.json().get('error', {}).get('code') == 'NOT_FOUND': if res.json().get('error', {}).get('code') == 'NOT_FOUND':
raise UserNotFound() raise NotFound('User')
if res.json().get('error', {}).get('code') == 'USER_BLOCKED': if res.json().get('error', {}).get('code') == 'USER_BLOCKED':
raise UserBanned() raise UserBanned()
res.raise_for_status() res.raise_for_status()
@@ -157,14 +158,14 @@ class Client:
banner_id (UUID | None, optional): UUID баннера. Defaults to None. banner_id (UUID | None, optional): UUID баннера. Defaults to None.
Raises: Raises:
InvalidProfileData: Неправильные данные (валидация не прошла) ValidationError: Ошибка валидации
Returns: Returns:
UserProfileUpdate: Обновленный профиль UserProfileUpdate: Обновленный профиль
""" """
res = update_profile(self.token, bio, display_name, username, banner_id) res = update_profile(self.token, bio, display_name, username, banner_id)
if res.status_code == 422 and 'found' in res.json(): if res.status_code == 422 and 'found' in res.json():
raise InvalidProfileData(*list(res.json()['found'].items())[0]) raise ValidationError(*list(res.json()['found'].items())[0])
res.raise_for_status() res.raise_for_status()
return UserProfileUpdate.model_validate(res.json()) return UserProfileUpdate.model_validate(res.json())
@@ -193,14 +194,14 @@ class Client:
username (str): username username (str): username
Raises: Raises:
UserNotFound: Пользователь не найден NotFound: Пользователь не найден
Returns: Returns:
int: Число подписчиков после подписки int: Число подписчиков после подписки
""" """
res = follow(self.token, username) res = follow(self.token, username)
if res.json().get('error', {}).get('code') == 'NOT_FOUND': if res.json().get('error', {}).get('code') == 'NOT_FOUND':
raise UserNotFound() raise NotFound('User')
res.raise_for_status() res.raise_for_status()
return res.json()['followersCount'] return res.json()['followersCount']
@@ -213,14 +214,14 @@ class Client:
username (str): username username (str): username
Raises: Raises:
UserNotFound: Пользователь не найден NotFound: Пользователь не найден
Returns: Returns:
int: Число подписчиков после отписки int: Число подписчиков после отписки
""" """
res = unfollow(self.token, username) res = unfollow(self.token, username)
if res.json().get('error', {}).get('code') == 'NOT_FOUND': if res.json().get('error', {}).get('code') == 'NOT_FOUND':
raise UserNotFound() raise NotFound('User')
res.raise_for_status() res.raise_for_status()
return res.json()['followersCount'] return res.json()['followersCount']
@@ -235,7 +236,7 @@ class Client:
page (int, optional): Страница (при дозагрузке, увеличивайте на 1). Defaults to 1. page (int, optional): Страница (при дозагрузке, увеличивайте на 1). Defaults to 1.
Raises: Raises:
UserNotFound: Пользователь не найден NotFound: Пользователь не найден
Returns: Returns:
list[UserFollower]: Список подписчиков list[UserFollower]: Список подписчиков
@@ -243,7 +244,7 @@ class Client:
""" """
res = get_followers(self.token, username, limit, page) res = get_followers(self.token, username, limit, page)
if res.json().get('error', {}).get('code') == 'NOT_FOUND': if res.json().get('error', {}).get('code') == 'NOT_FOUND':
raise UserNotFound() raise NotFound('User')
res.raise_for_status() res.raise_for_status()
return [UserFollower.model_validate(user) for user in res.json()['data']['users']], Pagination.model_validate(res.json()['data']['pagination']) return [UserFollower.model_validate(user) for user in res.json()['data']['users']], Pagination.model_validate(res.json()['data']['pagination'])
@@ -258,7 +259,7 @@ class Client:
page (int, optional): Страница (при дозагрузке, увеличивайте на 1). Defaults to 1. page (int, optional): Страница (при дозагрузке, увеличивайте на 1). Defaults to 1.
Raises: Raises:
UserNotFound: Пользователь не найден NotFound: Пользователь не найден
Returns: Returns:
list[UserFollower]: Список подписок list[UserFollower]: Список подписок
@@ -266,7 +267,7 @@ class Client:
""" """
res = get_following(self.token, username, limit, page) res = get_following(self.token, username, limit, page)
if res.json().get('error', {}).get('code') == 'NOT_FOUND': if res.json().get('error', {}).get('code') == 'NOT_FOUND':
raise UserNotFound() raise NotFound('User')
res.raise_for_status() res.raise_for_status()
return [UserFollower.model_validate(user) for user in res.json()['data']['users']], Pagination.model_validate(res.json()['data']['pagination']) return [UserFollower.model_validate(user) for user in res.json()['data']['users']], Pagination.model_validate(res.json()['data']['pagination'])
@@ -274,6 +275,17 @@ class Client:
@refresh_on_error @refresh_on_error
def verificate(self, file_url: str) -> Verification: def verificate(self, file_url: str) -> Verification:
"""Отправить запрос на верификацию
Args:
file_url (str): Ссылка на видео
Raises:
PendingRequestExists: Запрос уже отправлен
Returns:
Verification: Верификация
"""
res = verificate(self.token, file_url) res = verificate(self.token, file_url)
if res.json().get('error', {}).get('code') == 'PENDING_REQUEST_EXISTS': if res.json().get('error', {}).get('code') == 'PENDING_REQUEST_EXISTS':
raise PendingRequestExists() raise PendingRequestExists()
@@ -283,6 +295,11 @@ class Client:
@refresh_on_error @refresh_on_error
def get_verification_status(self) -> VerificationStatus: def get_verification_status(self) -> VerificationStatus:
"""Получить статус верификации
Returns:
VerificationStatus: Верификация
"""
res = get_verification_status(self.token) res = get_verification_status(self.token)
res.raise_for_status() res.raise_for_status()
@@ -291,6 +308,11 @@ class Client:
@refresh_on_error @refresh_on_error
def get_who_to_follow(self) -> list[UserWhoToFollow]: def get_who_to_follow(self) -> list[UserWhoToFollow]:
"""Получить список популярнык пользователей (кого читать)
Returns:
list[UserWhoToFollow]: Список пользователей
"""
res = get_who_to_follow(self.token) res = get_who_to_follow(self.token)
res.raise_for_status() res.raise_for_status()
@@ -298,34 +320,101 @@ class Client:
@refresh_on_error @refresh_on_error
def get_top_clans(self) -> list[Clan]: def get_top_clans(self) -> list[Clan]:
"""Получить топ кланов
Returns:
list[Clan]: Топ кланов
"""
res = get_top_clans(self.token) res = get_top_clans(self.token)
res.raise_for_status() res.raise_for_status()
return [Clan.model_validate(clan) for clan in res.json()['clans']] return [Clan.model_validate(clan) for clan in res.json()['clans']]
@refresh_on_error @refresh_on_error
def get_platform_status(self) -> dict: def get_platform_status(self) -> bool:
return get_platform_status(self.token) """Получить статус платформы
Returns:
bool: read only
"""
res = get_platform_status(self.token)
res.raise_for_status()
return res.json()['readOnly']
@refresh_on_error @refresh_on_error
def add_comment(self, post_id: str, content: str, reply_comment_id: str | None = None): def add_comment(self, post_id: UUID, content: str) -> Comment:
return add_comment(self.token, post_id, content, reply_comment_id) """Добавить комментарий
Args:
post_id (str): UUID поста
content (str): Содержание
reply_comment_id (UUID | None, optional): ID коммента для ответа. Defaults to None.
Raises:
ValidationError: Ошибка валидации
Returns:
Comment: Комментарий
"""
res = add_comment(self.token, post_id, content)
if res.status_code == 422 and 'found' in res.json():
raise ValidationError(*list(res.json()['found'].items())[0])
if res.json().get('error', {}).get('code') == 'NOT_FOUND':
raise NotFound('Post')
res.raise_for_status()
return Comment.model_validate(res.json())
@refresh_on_error @refresh_on_error
def get_comments(self, post_id: str, limit: int = 20, cursor: int = 0, sort: str = 'popular'): def add_reply_comment(self, comment_id: UUID, content: str, author_id: UUID) -> Comment:
return get_comments(self.token, post_id, limit, cursor, sort) """Добавить ответный комментарий
Args:
comment_id (str): UUID комментария
content (str): Содержание
author_id (UUID | None, optional): ID пользователя, отправившего комментарий. Defaults to None.
Raises:
ValidationError: Ошибка валидации
Returns:
Comment: Комментарий
"""
res = add_reply_comment(self.token, comment_id, content, author_id)
if res.status_code == 500 and 'Failed query' in res.text:
raise NotFound('User')
if res.status_code == 422 and 'found' in res.json():
raise ValidationError(*list(res.json()['found'].items())[0])
if res.json().get('error', {}).get('code') == 'NOT_FOUND':
raise NotFound('Comment')
res.raise_for_status()
return Comment.model_validate(res.json())
@refresh_on_error @refresh_on_error
def like_comment(self, id: str): def get_comments(self, post_id: UUID, limit: int = 20, cursor: int = 0, sort: str = 'popular') -> tuple[list[Comment], Pagination]:
res = get_comments(self.token, post_id, limit, cursor, sort)
if res.json().get('error', {}).get('code') == 'NOT_FOUND':
raise NotFound('Post')
res.raise_for_status()
data = res.json()['data']
return [Comment.model_validate(comment) for comment in data['comments']], Pagination(page=(cursor // limit) or 1, limit=limit, total=data['total'], hasMore=data['hasMore'])
@refresh_on_error
def like_comment(self, id: UUID):
return like_comment(self.token, id) return like_comment(self.token, id)
@refresh_on_error @refresh_on_error
def unlike_comment(self, id: str): def unlike_comment(self, id: UUID):
return unlike_comment(self.token, id) return unlike_comment(self.token, id)
@refresh_on_error @refresh_on_error
def delete_comment(self, id: str): def delete_comment(self, id: UUID):
return delete_comment(self.token, id) return delete_comment(self.token, id)
@@ -426,6 +515,6 @@ class Client:
def upload_file(self, name: str, data: BufferedReader): def upload_file(self, name: str, data: BufferedReader):
return upload_file(self.token, name, data) return upload_file(self.token, name, data)
def update_banner(self, name: str): def update_banner(self, name: str) -> UserProfileUpdate:
id = self.upload_file(name, cast(BufferedReader, open(name, 'rb')))['id'] id = self.upload_file(name, cast(BufferedReader, open(name, 'rb')))['id']
return self.update_profile(banner_id=id) return self.update_profile(banner_id=id)

View File

@@ -23,3 +23,7 @@ class ReportTargetReason(Enum):
ADULT = 'adult' # 18+ ADULT = 'adult' # 18+
FRAUD = 'fraud' # обман\мошенничество FRAUD = 'fraud' # обман\мошенничество
OTHER = 'other' # другое OTHER = 'other' # другое
class AttachType(Enum):
AUDIO = 'audio'
IMAGE = 'image'

View File

@@ -23,21 +23,29 @@ class InvalidOldPassword(Exception):
def __str__(self): def __str__(self):
return 'Old password is incorrect' return 'Old password is incorrect'
class UserNotFound(Exception): class NotFound(Exception):
def __init__(self, obj):
self.obj = obj
def __str__(self): def __str__(self):
return 'User not found' return f'{self.obj} not found'
class UserBanned(Exception): class UserBanned(Exception):
def __str__(self): def __str__(self):
return 'User banned' return 'User banned'
class InvalidProfileData(Exception): class ValidationError(Exception):
def __init__(self, name: str, value: str): def __init__(self, name: str, value: str):
self.name = name self.name = name
self.value = value self.value = value
def __str__(self): def __str__(self):
return f'Invalid update profile data {self.name}: "{self.value}"' return f'Failed validation on {self.name}: "{self.value}"'
class PendingRequestExists(Exception): class PendingRequestExists(Exception):
def __str__(self): def __str__(self):
return 'Pending verifiaction request already exists' return 'Pending verifiaction request already exists'
class RateLimitExceeded(Exception):
def __init__(self, retry_after: int):
self.retry_after = retry_after
def __str__(self):
return f'Rate limit exceeded - too much requests. Retry after {self.retry_after} seconds'

View File

@@ -1,17 +1,26 @@
from uuid import UUID from uuid import UUID
from datetime import datetime from datetime import datetime
from pydantic import BaseModel, Field from pydantic import BaseModel, Field, field_validator
from itd.models.user import UserPost from itd.models.user import UserPost
from itd.models.file import Attach
class TextObject(BaseModel): class TextObject(BaseModel):
id: UUID id: UUID
content: str content: str
author: UserPost author: UserPost
attachments: list[UUID] attachments: list[Attach] = []
created_at: datetime = Field(alias='createdAt') created_at: datetime = Field(alias='createdAt')
model_config = {'populate_by_name': True} model_config = {'populate_by_name': True}
@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.%fZ')

View File

@@ -1,11 +1,13 @@
from pydantic import Field from pydantic import Field
from itd.models._text import TextObject from itd.models._text import TextObject
from itd.models.user import UserPost
class CommentShort(TextObject): class Comment(TextObject):
likes_count: int = Field(0, alias='likesCount') likes_count: int = Field(0, alias='likesCount')
replies_count: int = Field(0, alias='repliesCount') replies_count: int = Field(0, alias='repliesCount')
is_liked: bool = Field(False, alias='isLiked') is_liked: bool = Field(False, alias='isLiked')
replies: list['CommentShort'] = [] replies: list['Comment'] = []
reply_to: UserPost | None = None # author of replied comment, if this comment is reply

View File

@@ -2,9 +2,25 @@ from uuid import UUID
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from itd.enums import AttachType
class File(BaseModel): class File(BaseModel):
id: UUID id: UUID
url: str url: str
filename: str filename: str
mime_type: str = Field('image/png', alias='mimeType') mime_type: str = Field('image/png', alias='mimeType')
size: int size: int
class Attach(BaseModel):
id: UUID
type: AttachType = AttachType.IMAGE
url: str
thumbnail_url: str | None = Field(None, alias='thumbnailUrl')
filename: str
mime_type: str = Field(alias='mimeType')
size: int
width: int | None = None
height: int | None = None
duration: int | None = None
order: int = 0

View File

@@ -13,7 +13,7 @@ class UserPrivacy(BaseModel):
class UserProfileUpdate(BaseModel): class UserProfileUpdate(BaseModel):
id: UUID id: UUID
username: str username: str | None = None
display_name: str = Field(alias='displayName') display_name: str = Field(alias='displayName')
bio: str | None = None bio: str | None = None
@@ -24,7 +24,7 @@ class UserProfileUpdate(BaseModel):
class UserNotification(BaseModel): class UserNotification(BaseModel):
id: UUID id: UUID
username: str username: str | None = None
display_name: str = Field(alias='displayName') display_name: str = Field(alias='displayName')
avatar: str avatar: str

View File

@@ -2,7 +2,7 @@ from _io import BufferedReader
from requests import Session from requests import Session
from itd.exceptions import InvalidToken, InvalidCookie from itd.exceptions import InvalidToken, InvalidCookie, RateLimitExceeded
s = Session() s = Session()
@@ -31,6 +31,9 @@ def fetch(token: str, method: str, url: str, params: dict = {}, files: dict[str,
else: else:
res = s.request(method.upper(), base, timeout=20, json=params, headers=headers, files=files) res = s.request(method.upper(), base, timeout=20, json=params, headers=headers, files=files)
if res.json().get('error', {}).get('code') == 'RATE_LIMIT_EXCEEDED':
raise RateLimitExceeded(res.json()['error'].get('retryAfter', 0))
print(res.text) print(res.text)
return res return res
@@ -72,6 +75,8 @@ def auth_fetch(cookies: str, method: str, url: str, params: dict = {}, token: st
# print(res.text) # print(res.text)
if res.text == 'UNAUTHORIZED': if res.text == 'UNAUTHORIZED':
raise InvalidToken() raise InvalidToken()
if res.json().get('error', {}).get('code') == 'RATE_LIMIT_EXCEEDED':
raise RateLimitExceeded(res.json()['error'].get('retryAfter', 0))
if res.json().get('error', {}).get('code') in ('SESSION_NOT_FOUND', 'REFRESH_TOKEN_MISSING'): if res.json().get('error', {}).get('code') in ('SESSION_NOT_FOUND', 'REFRESH_TOKEN_MISSING'):
raise InvalidCookie() raise InvalidCookie()

View File

@@ -1,19 +1,21 @@
from uuid import UUID
from itd.request import fetch from itd.request import fetch
def add_comment(token: str, post_id: str, content: str, reply_comment_id: str | None = None): def add_comment(token: str, post_id: UUID, content: str):
data = {'content': content} return fetch(token, 'post', f'posts/{post_id}/comments', {'content': content})
if reply_comment_id:
data['replyTo'] = str(reply_comment_id)
return fetch(token, 'post', f'posts/{post_id}/comments', data)
def get_comments(token: str, post_id: str, limit: int = 20, cursor: int = 0, sort: str = 'popular'): def add_reply_comment(token: str, comment_id: UUID, content: str, author_id: UUID):
return fetch(token, 'post', f'comments/{comment_id}/replies', {'content': content, 'replyToUserId': str(author_id)})
def get_comments(token: str, post_id: UUID, limit: int = 20, cursor: int = 0, sort: str = 'popular'):
return fetch(token, 'get', f'posts/{post_id}/comments', {'limit': limit, 'sort': sort, 'cursor': cursor}) return fetch(token, 'get', f'posts/{post_id}/comments', {'limit': limit, 'sort': sort, 'cursor': cursor})
def like_comment(token: str, comment_id: str): def like_comment(token: str, comment_id: UUID):
return fetch(token, 'post', f'comments/{comment_id}/like') return fetch(token, 'post', f'comments/{comment_id}/like')
def unlike_comment(token: str, comment_id: str): def unlike_comment(token: str, comment_id: UUID):
return fetch(token, 'delete', f'comments/{comment_id}/like') return fetch(token, 'delete', f'comments/{comment_id}/like')
def delete_comment(token: str, comment_id: str): def delete_comment(token: str, comment_id: UUID):
return fetch(token, 'delete', f'comments/{comment_id}') return fetch(token, 'delete', f'comments/{comment_id}')