feat: add models final part 5

This commit is contained in:
firedotguy
2026-02-07 17:23:42 +03:00
parent f33ed4f76a
commit 506e6a5d09
10 changed files with 189 additions and 57 deletions

View File

@@ -2,6 +2,7 @@ from warnings import deprecated
from uuid import UUID from uuid import UUID
from _io import BufferedReader from _io import BufferedReader
from typing import cast from typing import cast
from datetime import datetime
from requests.exceptions import ConnectionError, HTTPError from requests.exceptions import ConnectionError, HTTPError
@@ -10,7 +11,7 @@ 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 from itd.routes.comments import get_comments, add_comment, delete_comment, like_comment, unlike_comment, add_reply_comment
from itd.routes.hashtags import get_hashtags, get_posts_by_hashtag 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 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, restore_post, like_post, delete_like_post 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
from itd.routes.reports import report from itd.routes.reports import report
from itd.routes.search import search from itd.routes.search import search
from itd.routes.files import upload_file from itd.routes.files import upload_file
@@ -19,19 +20,21 @@ from itd.routes.verification import verify, get_verification_status
from itd.models.comment import Comment from itd.models.comment import Comment
from itd.models.notification import Notification from itd.models.notification import Notification
from itd.models.post import Post, NewPost, LikePostResponse from itd.models.post import Post, NewPost
from itd.models.clan import Clan from itd.models.clan import Clan
from itd.models.hashtag import Hashtag 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
from itd.models.pagination import Pagination, PostsPagintaion, LikedPostsPagintaion from itd.models.pagination import Pagination, PostsPagintaion, LikedPostsPagintaion
from itd.models.verification import Verification, VerificationStatus from itd.models.verification import Verification, VerificationStatus
from itd.models.report import NewReport
from itd.models.file import File
from itd.enums import PostsTab from itd.enums import PostsTab, ReportTargetType, ReportTargetReason
from itd.request import set_cookies from itd.request import set_cookies
from itd.exceptions import ( from itd.exceptions import (
NoCookie, NoAuthData, SamePassword, InvalidOldPassword, NotFound, ValidationError, UserBanned, NoCookie, NoAuthData, SamePassword, InvalidOldPassword, NotFound, ValidationError, UserBanned,
PendingRequestExists, Forbidden, UsernameTaken, CantFollowYourself, Unauthorized, PendingRequestExists, Forbidden, UsernameTaken, CantFollowYourself, Unauthorized,
CantRepostYourPost, AlreadyReposted CantRepostYourPost, AlreadyReposted, AlreadyReported, TooLarge
) )
@@ -797,7 +800,21 @@ class Client:
res.raise_for_status() res.raise_for_status()
@refresh_on_error @refresh_on_error
def get_liked_posts(self, username_or_id: str | UUID, limit: int = 20, cursor: int = 0) -> tuple[list[Post], LikedPostsPagintaion]: def get_liked_posts(self, username_or_id: str | UUID, limit: int = 20, cursor: datetime | None = None) -> tuple[list[Post], LikedPostsPagintaion]:
"""Получить список лайкнутых постов пользователя
Args:
username_or_id (str | UUID): UUID или username пользователя
limit (int, optional): Лимит. Defaults to 20.
cursor (datetime | None, optional): Сдвиг (next_cursor). Defaults to None.
Raises:
NotFound: Пользователь не найден
Returns:
list[Post]: Список постов
LikedPostsPagintaion: Пагинация
"""
res = get_liked_posts(self.token, username_or_id, limit, cursor) res = get_liked_posts(self.token, username_or_id, limit, cursor)
if res.json().get('error', {}).get('code') == 'NOT_FOUND': if res.json().get('error', {}).get('code') == 'NOT_FOUND':
raise NotFound('User') raise NotFound('User')
@@ -808,41 +825,114 @@ class Client:
@refresh_on_error @refresh_on_error
def report(self, id: str, type: str = 'post', reason: str = 'other', description: str = ''): def report(self, id: UUID, type: ReportTargetType = ReportTargetType.POST, reason: ReportTargetReason = ReportTargetReason.OTHER, description: str | None = None) -> NewReport:
return report(self.token, id, type, reason, description) """Отправить жалобу
@refresh_on_error Args:
def report_user(self, id: str, reason: str = 'other', description: str = ''): id (UUID): UUID цели
return report(self.token, id, 'user', reason, description) type (ReportTargetType, optional): Тип цели (пост/пользователь/комментарий). Defaults to ReportTargetType.POST.
reason (ReportTargetReason, optional): Причина. Defaults to ReportTargetReason.OTHER.
description (str | None, optional): Описание. Defaults to None.
@refresh_on_error Raises:
def report_post(self, id: str, reason: str = 'other', description: str = ''): NotFound: Цель не найдена
return report(self.token, id, 'post', reason, description) AlreadyReported: Жалоба уже отправлена
ValidationError: Ошибка валидации
@refresh_on_error Returns:
def report_comment(self, id: str, reason: str = 'other', description: str = ''): NewReport: Новая жалоба
return report(self.token, id, 'comment', reason, description) """
res = report(self.token, id, type, reason, description)
if res.json().get('error', {}).get('code') == 'VALIDATION_ERROR' and 'не найден' in res.json()['error'].get('message', ''):
raise NotFound(type.value.title())
if res.json().get('error', {}).get('code') == 'VALIDATION_ERROR' and 'Вы уже отправляли жалобу' in res.json()['error'].get('message', ''):
raise AlreadyReported(type.value.title())
if res.status_code == 422 and 'found' in res.json():
raise ValidationError(*list(res.json()['found'].items())[-1])
res.raise_for_status()
return NewReport.model_validate(res.json()['data'])
@refresh_on_error @refresh_on_error
def search(self, query: str, user_limit: int = 5, hashtag_limit: int = 5): def search(self, query: str, user_limit: int = 5, hashtag_limit: int = 5) -> tuple[list[UserWhoToFollow], list[Hashtag]]:
return search(self.token, query, user_limit, hashtag_limit) """Поиск по пользователям и хэштэгам
Args:
query (str): Запрос
user_limit (int, optional): Лимит пользователей. Defaults to 5.
hashtag_limit (int, optional): Лимит хэштэгов. Defaults to 5.
Raises:
TooLarge: Слишком длинный запрос
Returns:
list[UserWhoToFollow]: Список пользователей
list[Hashtag]: Список хэштэгов
"""
res = search(self.token, query, user_limit, hashtag_limit)
if res.status_code == 414:
raise TooLarge()
res.raise_for_status()
data = res.json()['data']
return [UserWhoToFollow.model_validate(user) for user in data['users']], [Hashtag.model_validate(hashtag) for hashtag in data['hashtags']]
@refresh_on_error @refresh_on_error
def search_user(self, query: str, limit: int = 5): def search_user(self, query: str, limit: int = 5) -> list[UserWhoToFollow]:
return search(self.token, query, limit, 0) """Поиск пользователей
Args:
query (str): Запрос
limit (int, optional): Лимит. Defaults to 5.
Returns:
list[UserWhoToFollow]: Список пользователей
"""
return self.search(query, limit, 0)[0]
@refresh_on_error @refresh_on_error
def search_hashtag(self, query: str, limit: int = 5): def search_hashtag(self, query: str, limit: int = 5) -> list[Hashtag]:
return search(self.token, query, 0, limit) """Поиск хэштэгов
Args:
query (str): Запрос
limit (int, optional): Лимит. Defaults to 5.
Returns:
list[Hashtag]: Список хэштэгов
"""
return self.search(query, 0, limit)[1]
@refresh_on_error @refresh_on_error
def upload_file(self, name: str, data: BufferedReader): def upload_file(self, name: str, data: BufferedReader) -> File:
return upload_file(self.token, name, data).json() """Загрузить файл
Args:
name (str): Имя файла
data (BufferedReader): Содержимое (open('имя', 'rb'))
Returns:
File: Файл
"""
res = upload_file(self.token, name, data)
res.raise_for_status()
return File.model_validate(res.json())
def update_banner(self, name: str) -> UserProfileUpdate: def update_banner(self, name: str) -> UserProfileUpdate:
id = self.upload_file(name, cast(BufferedReader, open(name, 'rb')))['id'] """Обновить банер (шорткат из upload_file + update_profile)
Args:
name (str): Имя файла
Returns:
UserProfileUpdate: Обновленный профиль
"""
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)
@refresh_on_error @refresh_on_error
@@ -852,28 +942,45 @@ class Client:
Args: Args:
post_id: UUID поста post_id: UUID поста
""" """
restore_post(self.token, post_id) res = restore_post(self.token, post_id)
res.raise_for_status()
@refresh_on_error @refresh_on_error
def like_post(self, post_id: UUID) -> LikePostResponse: def like_post(self, post_id: UUID) -> int:
"""Поставить лайк на пост """Лайкнуть пост
Args: Args:
post_id: UUID поста post_id (UUID): UUID поста
Raises:
NotFound: Пост не найден
Returns:
int: Количество лайков
""" """
res = like_post(self.token, post_id) res = like_post(self.token, post_id)
if res.status_code == 404: if res.status_code == 404:
raise NotFound("Post not found") raise NotFound("Post")
return LikePostResponse.model_validate(res.json())
return res.json()['likesCount']
@refresh_on_error @refresh_on_error
def delete_like_post(self, post_id: UUID) -> LikePostResponse: def unlike_post(self, post_id: UUID) -> int:
"""Убрать лайк с поста """Убрать лайк с поста
Args: Args:
post_id: UUID поста post_id (UUID): UUID поста
Raises:
NotFound: Пост не найден
Returns:
int: Количество лайков
""" """
res = delete_like_post(self.token, post_id) res = unlike_post(self.token, post_id)
if res.status_code == 404: if res.status_code == 404:
raise NotFound("Post not found") raise NotFound("Post not found")
return LikePostResponse.model_validate(res.json())
return res.json()['likesCount']

View File

@@ -11,15 +11,17 @@ class InvalidCookie(Exception):
self.code = code self.code = code
def __str__(self): def __str__(self):
if self.code == 'SESSION_NOT_FOUND': if self.code == 'SESSION_NOT_FOUND':
return f'Invalid cookie data: Session not found (incorrect refresh token)' return 'Invalid cookie data: Session not found (incorrect refresh token)'
elif self.code == 'REFRESH_TOKEN_MISSING': elif self.code == 'REFRESH_TOKEN_MISSING':
return f'Invalid cookie data: No refresh token' return 'Invalid cookie data: No refresh token'
elif self.code == 'SESSION_EXPIRED':
return 'Invalid cookie data: Session expired'
# SESSION_REVOKED # SESSION_REVOKED
return f'Invalid cookie data: Session revoked (logged out)' return 'Invalid cookie data: Session revoked (logged out)'
class InvalidToken(Exception): class InvalidToken(Exception):
def __str__(self): def __str__(self):
return f'Invalid access token' return 'Invalid access token'
class SamePassword(Exception): class SamePassword(Exception):
def __str__(self): def __str__(self):
@@ -30,7 +32,7 @@ class InvalidOldPassword(Exception):
return 'Old password is incorrect' return 'Old password is incorrect'
class NotFound(Exception): class NotFound(Exception):
def __init__(self, obj): def __init__(self, obj: str):
self.obj = obj self.obj = obj
def __str__(self): def __str__(self):
return f'{self.obj} not found' return f'{self.obj} not found'
@@ -81,3 +83,13 @@ class CantRepostYourPost(Exception):
class AlreadyReposted(Exception): class AlreadyReposted(Exception):
def __str__(self): def __str__(self):
return 'Post already reposted' return 'Post already reposted'
class AlreadyReported(Exception):
def __init__(self, obj: str) -> None:
self.obj = obj
def __str__(self):
return f'{self.obj} already reported'
class TooLarge(Exception):
def __str__(self):
return 'Search query too large'

View File

@@ -13,7 +13,7 @@ class Pagination(BaseModel):
class PostsPagintaion(BaseModel): class PostsPagintaion(BaseModel):
limit: int = 20 limit: int = 20
next_cursor: int = Field(1, alias='nextCursor') next_cursor: int | None = Field(1, alias='nextCursor')
has_more: bool = Field(True, alias='hasMore') has_more: bool = Field(True, alias='hasMore')

6
itd/models/pin.py Normal file
View File

@@ -0,0 +1,6 @@
from pydantic import BaseModel
class Pin(BaseModel):
slug: str
name: str
description: str

View File

@@ -44,8 +44,3 @@ class Post(_Post, PostShort):
class NewPost(_Post): class NewPost(_Post):
author: UserNewPost author: UserNewPost
class LikePostResponse(BaseModel):
liked: bool
likes_count: int = Field(alias="likesCount")

View File

@@ -5,12 +5,15 @@ from pydantic import BaseModel, Field
from itd.enums import ReportTargetType, ReportTargetReason from itd.enums import ReportTargetType, ReportTargetReason
class Report(BaseModel):
class NewReport(BaseModel):
id: UUID id: UUID
created_at: datetime = Field(alias='createdAt')
class Report(NewReport):
reason: ReportTargetReason reason: ReportTargetReason
description: str | None = None description: str | None = None
target_type: ReportTargetType = Field(alias='targetType') target_type: ReportTargetType = Field(alias='targetType')
target_id: UUID target_id: UUID
created_at: datetime = Field(alias='createdAt')

View File

@@ -3,6 +3,8 @@ from datetime import datetime
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from itd.models.pin import Pin
class UserPrivacy(BaseModel): class UserPrivacy(BaseModel):
private: bool | None = Field(None, alias='isPrivate') # none for not me private: bool | None = Field(None, alias='isPrivate') # none for not me
@@ -24,6 +26,7 @@ class UserNewPost(BaseModel):
username: str | None = None username: str | None = None
display_name: str = Field(alias='displayName') display_name: str = Field(alias='displayName')
avatar: str avatar: str
pin: Pin | None = None
verified: bool = False verified: bool = False

View File

@@ -85,11 +85,11 @@ def auth_fetch(cookies: str, method: str, url: str, params: dict = {}, token: st
try: try:
if res.json().get('error', {}).get('code') == 'RATE_LIMIT_EXCEEDED': if res.json().get('error', {}).get('code') == 'RATE_LIMIT_EXCEEDED':
raise RateLimitExceeded(res.json()['error'].get('retryAfter', 0)) raise RateLimitExceeded(res.json()['error'].get('retryAfter', 0))
if res.json().get('error', {}).get('code') in ('SESSION_NOT_FOUND', 'REFRESH_TOKEN_MISSING', 'SESSION_REVOKED'): if res.json().get('error', {}).get('code') in ('SESSION_NOT_FOUND', 'REFRESH_TOKEN_MISSING', 'SESSION_REVOKED', 'SESSION_EXPIRED'):
raise InvalidCookie(res.json()['error']['code']) raise InvalidCookie(res.json()['error']['code'])
if res.json().get('error', {}).get('code') == 'UNAUTHORIZED': if res.json().get('error', {}).get('code') == 'UNAUTHORIZED':
raise Unauthorized() raise Unauthorized()
except JSONDecodeError: except JSONDecodeError:
pass print('fail to parse json')
return res return res

View File

@@ -1,3 +1,4 @@
from datetime import datetime
from uuid import UUID from uuid import UUID
from itd.request import fetch from itd.request import fetch
@@ -36,8 +37,8 @@ def repost(token: str, id: UUID, content: str | None = None):
def view_post(token: str, id: UUID): def view_post(token: str, id: UUID):
return fetch(token, 'post', f'posts/{id}/view') return fetch(token, 'post', f'posts/{id}/view')
def get_liked_posts(token: str, username_or_id: str | UUID, limit: int = 20, cursor: int = 0): def get_liked_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}/liked', {'limit': limit}) return fetch(token, 'get', f'posts/user/{username_or_id}/liked', {'limit': limit, 'cursor': cursor})
def restore_post(token: str, post_id: UUID): def restore_post(token: str, post_id: UUID):
return fetch(token, "post", f"posts/{post_id}/restore",) return fetch(token, "post", f"posts/{post_id}/restore",)
@@ -45,5 +46,5 @@ def restore_post(token: str, post_id: UUID):
def like_post(token: str, post_id: UUID): def like_post(token: str, post_id: UUID):
return fetch(token, "post", f"posts/{post_id}/like") return fetch(token, "post", f"posts/{post_id}/like")
def delete_like_post(token: str, post_id: UUID): def unlike_post(token: str, post_id: UUID):
return fetch(token, "delete", f"posts/{post_id}/like") return fetch(token, "delete", f"posts/{post_id}/like")

View File

@@ -1,4 +1,9 @@
from itd.request import fetch from uuid import UUID
def report(token: str, id: str, type: str = 'post', reason: str = 'other', description: str = ''): from itd.request import fetch
return fetch(token, 'post', 'reports', {'targetId': id, 'targetType': type, 'reason': reason, 'description': description}) from itd.enums import ReportTargetReason, ReportTargetType
def report(token: str, id: UUID, type: ReportTargetType = ReportTargetType.POST, reason: ReportTargetReason = ReportTargetReason.OTHER, description: str | None = None):
if description is None:
description = ''
return fetch(token, 'post', 'reports', {'targetId': str(id), 'targetType': type.value, 'reason': reason.value, 'description': description})