From 55630bc23fbeabc745f838d4b31e0b8b6f6ffe68 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Thu, 5 Feb 2026 00:20:26 +0300 Subject: [PATCH] feat: add models part 4 --- itd/client.py | 199 ++++++++++++++++++++++++++++++++++----- itd/enums.py | 4 + itd/exceptions.py | 12 ++- itd/models/pagination.py | 15 ++- itd/request.py | 2 +- itd/routes/posts.py | 17 +--- 6 files changed, 208 insertions(+), 41 deletions(-) diff --git a/itd/client.py b/itd/client.py index f740b31..45188bb 100644 --- a/itd/client.py +++ b/itd/client.py @@ -23,11 +23,16 @@ from itd.models.post import Post, NewPost 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.pagination import Pagination +from itd.models.pagination import Pagination, PostsPagintaion, LikedPostsPagintaion from itd.models.verification import Verification, VerificationStatus +from itd.enums import PostsTab from itd.request import set_cookies -from itd.exceptions import NoCookie, NoAuthData, SamePassword, InvalidOldPassword, NotFound, ValidationError, UserBanned, PendingRequestExists, Forbidden, UsernameTaken, CantFollowYourself, Unauthorized +from itd.exceptions import ( + NoCookie, NoAuthData, SamePassword, InvalidOldPassword, NotFound, ValidationError, UserBanned, + PendingRequestExists, Forbidden, UsernameTaken, CantFollowYourself, Unauthorized, + CantRepostYourPost, AlreadyReposted +) def refresh_on_error(func): @@ -302,15 +307,15 @@ class Client: def verify(self, file_url: str) -> Verification: """Отправить запрос на верификацию - Args: - file_url (str): Ссылка на видео + Args: + file_url (str): Ссылка на видео - Raises: - PendingRequestExists: Запрос уже отправлен + Raises: + PendingRequestExists: Запрос уже отправлен - Returns: - Verification: Верификация - """ + Returns: + Verification: Верификация + """ res = verify(self.token, file_url) if res.json().get('error', {}).get('code') == 'PENDING_REQUEST_EXISTS': raise PendingRequestExists() @@ -614,6 +619,20 @@ class Client: @refresh_on_error def create_post(self, content: str, wall_recipient_id: UUID | None = None, attach_ids: list[UUID] = []) -> NewPost: + """Создать пост + + Args: + content (str): Содержимое + wall_recipient_id (UUID | None, optional): UUID пользователя (чтобы создать пост ему на стене). Defaults to None. + attach_ids (list[UUID], optional): UUID вложений. Defaults to []. + + Raises: + NotFound: Пользователь не найден + ValidationError: Ошибка валидации + + Returns: + NewPost: Новый пост + """ res = create_post(self.token, content, wall_recipient_id, attach_ids) if res.json().get('error', {}).get('code') == 'NOT_FOUND': raise NotFound('Wall recipient') @@ -624,36 +643,166 @@ class Client: return NewPost.model_validate(res.json()) @refresh_on_error - def get_posts(self, username: str | None = None, limit: int = 20, cursor: int = 0, sort: str = '', tab: str = ''): - return get_posts(self.token, username, limit, cursor, sort, tab) + def get_posts(self, cursor: int = 0, tab: PostsTab = PostsTab.POPULAR) -> tuple[list[Post], PostsPagintaion]: + """Получить список постов + + Args: + cursor (int, optional): Страница. Defaults to 0. + tab (PostsTab, optional): Вкладка (популярное или подписки). Defaults to PostsTab.POPULAR. + + Returns: + list[Post]: Список постов + Pagination: Пагинация + """ + res = get_posts(self.token, cursor, tab) + res.raise_for_status() + data = res.json()['data'] + + return [Post.model_validate(post) for post in data['posts']], PostsPagintaion.model_validate(data['pagination']) @refresh_on_error - def get_post(self, id: str): - return get_post(self.token, id) + def get_post(self, id: UUID) -> Post: + """Получить пост + + Args: + id (UUID): UUID поста + + Raises: + NotFound: Пост не найден + + Returns: + Post: Пост + """ + res = get_post(self.token, id) + if res.json().get('error', {}).get('code') == 'NOT_FOUND': + raise NotFound('Post') + res.raise_for_status() + + return Post.model_validate(res.json()['data']) @refresh_on_error - def edit_post(self, id: str, content: str): - return edit_post(self.token, id, content) + def edit_post(self, id: UUID, content: str) -> str: + """Редактировать пост + + Args: + id (UUID): UUID поста + content (str): Содержимое + + Raises: + NotFound: Пост не найден + Forbidden: Нет доступа + ValidationError: Ошибка валидации + + Returns: + str: Новое содержимое + """ + res = edit_post(self.token, id, content) + + if res.json().get('error', {}).get('code') == 'NOT_FOUND': + raise NotFound('Post') + if res.json().get('error', {}).get('code') == 'FORBIDDEN': + raise Forbidden('edit post') + if res.status_code == 422 and 'found' in res.json(): + raise ValidationError(*list(res.json()['found'].items())[0]) + res.raise_for_status() + + return res.json()['content'] @refresh_on_error - def delete_post(self, id: str): - return delete_post(self.token, id) + def delete_post(self, id: UUID) -> None: + """Удалить пост + + Args: + id (UUID): UUID поста + + Raises: + NotFound: Пост не найден + Forbidden: Нет доступа + """ + res = delete_post(self.token, id) + if res.status_code == 204: + return + + if res.json().get('error', {}).get('code') == 'NOT_FOUND': + raise NotFound('Post') + if res.json().get('error', {}).get('code') == 'FORBIDDEN': + raise Forbidden('delete post') + res.raise_for_status() @refresh_on_error - def pin_post(self, id: str): - return pin_post(self.token, id) + def pin_post(self, id: UUID): + """Закрепить пост + + Args: + id (UUID): UUID поста + + Raises: + NotFound: Пост не найден + Forbidden: Нет доступа + """ + res = pin_post(self.token, id) + + if res.json().get('error', {}).get('code') == 'NOT_FOUND': + raise NotFound('Post') + if res.json().get('error', {}).get('code') == 'FORBIDDEN': + raise Forbidden('pin post') + res.raise_for_status() @refresh_on_error - def repost(self, id: str, content: str | None = None): - return repost(self.token, id, content) + def repost(self, id: UUID, content: str | None = None) -> NewPost: + """Репостнуть пост + + Args: + id (UUID): UUID поста + content (str | None, optional): Содержимое (доп. комментарий). Defaults to None. + + Raises: + NotFound: Пост не найден + AlreadyReposted: Пост уже репостнут + CantRepostYourPost: Нельзя репостить самого себя + ValidationError: Ошибка валидации + + Returns: + NewPost: Новый пост + """ + res = repost(self.token, id, content) + + if res.json().get('error', {}).get('code') == 'NOT_FOUND': + raise NotFound('Post') + if res.json().get('error', {}).get('code') == 'CONFLICT': + raise AlreadyReposted() + if res.status_code == 422 and res.json().get('message') == 'Cannot repost your own post': + raise CantRepostYourPost() + if res.status_code == 422 and 'found' in res.json(): + raise ValidationError(*list(res.json()['found'].items())[0]) + res.raise_for_status() + + return NewPost.model_validate(res.json()) @refresh_on_error - def view_post(self, id: str): - return view_post(self.token, id) + def view_post(self, id: UUID) -> None: + """Просмотреть пост + + Args: + id (UUID): UUID поста + + Raises: + NotFound: Пост не найден + """ + res = view_post(self.token, id) + if res.json().get('error', {}).get('code') == 'NOT_FOUND': + raise NotFound('Post') + res.raise_for_status() @refresh_on_error - def get_liked_posts(self, username: str, limit: int = 20, cursor: int = 0): - return get_liked_posts(self.token, username, limit, cursor) + def get_liked_posts(self, username_or_id: str | UUID, limit: int = 20, cursor: int = 0): + res = get_liked_posts(self.token, username_or_id, limit, cursor) + if res.json().get('error', {}).get('code') == 'NOT_FOUND': + raise NotFound('User') + res.raise_for_status() + data = res.json()['data'] + + return [Post.model_validate(post) for post in data['posts']], LikedPostsPagintaion.model_validate(data['pagination']) @refresh_on_error diff --git a/itd/enums.py b/itd/enums.py index c37fdf5..ff474b9 100644 --- a/itd/enums.py +++ b/itd/enums.py @@ -27,3 +27,7 @@ class ReportTargetReason(Enum): class AttachType(Enum): AUDIO = 'audio' IMAGE = 'image' + +class PostsTab(Enum): + FOLLOWING = 'following' + POPULAR = 'popular' diff --git a/itd/exceptions.py b/itd/exceptions.py index 1963ed9..abfa498 100644 --- a/itd/exceptions.py +++ b/itd/exceptions.py @@ -71,5 +71,13 @@ class CantFollowYourself(Exception): return 'Cannot follow yourself' class Unauthorized(Exception): - def __str__(self) -> str: - return 'Auth required - refresh token' \ No newline at end of file + def __str__(self): + return 'Auth required - refresh token' + +class CantRepostYourPost(Exception): + def __str__(self): + return 'Cannot repost your own post' + +class AlreadyReposted(Exception): + def __str__(self): + return 'Post already reposted' diff --git a/itd/models/pagination.py b/itd/models/pagination.py index e140b8b..6bfe1c7 100644 --- a/itd/models/pagination.py +++ b/itd/models/pagination.py @@ -1,4 +1,5 @@ from uuid import UUID +from datetime import datetime from pydantic import BaseModel, Field @@ -7,4 +8,16 @@ class Pagination(BaseModel): limit: int = 20 total: int | None = None has_more: bool = Field(True, alias='hasMore') - next_cursor: UUID | None = Field(None, alias='nextCursor') \ No newline at end of file + next_cursor: UUID | None = Field(None, alias='nextCursor') + + +class PostsPagintaion(BaseModel): + limit: int = 20 + next_cursor: int = Field(1, alias='nextCursor') + has_more: bool = Field(True, alias='hasMore') + + +class LikedPostsPagintaion(BaseModel): + limit: int = 20 + next_cursor: datetime | None = Field(None, alias='nextCursor') + has_more: bool = Field(True, alias='hasMore') diff --git a/itd/request.py b/itd/request.py index ac4a2d7..81bfad5 100644 --- a/itd/request.py +++ b/itd/request.py @@ -38,7 +38,7 @@ def fetch(token: str, method: str, url: str, params: dict = {}, files: dict[str, if res.json().get('error', {}).get('code') == 'UNAUTHORIZED': raise Unauthorized() except JSONDecodeError: - pass + pass # todo if not res.ok: print(res.text) diff --git a/itd/routes/posts.py b/itd/routes/posts.py index 97edcd2..9a5edd7 100644 --- a/itd/routes/posts.py +++ b/itd/routes/posts.py @@ -1,6 +1,7 @@ from uuid import UUID from itd.request import fetch +from itd.enums import PostsTab def create_post(token: str, content: str, wall_recipient_id: UUID | None = None, attach_ids: list[UUID] = []): data: dict = {'content': content} @@ -11,16 +12,8 @@ def create_post(token: str, content: str, wall_recipient_id: UUID | None = None, return fetch(token, 'post', 'posts', data) -def get_posts(token: str, username: str | None = None, limit: int = 20, cursor: int = 0, sort: str = '', tab: str = ''): - data: dict = {'limit': limit, 'cursor': cursor} - if username: - data['username'] = username - if sort: - data['sort'] = sort - if tab: - data['tab'] = tab - - return fetch(token, 'get', 'posts', data) +def get_posts(token: str, cursor: int = 0, tab: PostsTab = PostsTab.POPULAR): + return fetch(token, 'get', 'posts', {'cursor': cursor, 'tab': tab.value}) def get_post(token: str, id: UUID): return fetch(token, 'get', f'posts/{id}') @@ -43,8 +36,8 @@ def repost(token: str, id: UUID, content: str | None = None): def view_post(token: str, id: UUID): return fetch(token, 'post', f'posts/{id}/view') -def get_liked_posts(token: str, username: str, limit: int = 20, cursor: int = 0): - return fetch(token, 'get', f'posts/user/{username}/liked', {'limit': limit, 'cursor': cursor}) +def get_liked_posts(token: str, username_or_id: str | UUID, limit: int = 20, cursor: int = 0): + return fetch(token, 'get', f'posts/user/{username_or_id}/liked', {'limit': limit}) # todo post restore # todo post like \ No newline at end of file