41 Commits

Author SHA1 Message Date
723cf98475 [+] BaseParseFactory
[+] AuthLoginFactory
[^] Endpoint
[+] _ApiMethodGenerator
2025-04-11 19:00:10 +03:00
0682bdb42b Endpoint constructor 2025-04-04 18:17:36 +03:00
63bf159679 [~] minor 2025-04-04 18:17:23 +03:00
8a944319d6 [+] Fixes for post 2025-04-04 17:57:52 +03:00
aee5c542b4 Данные о версии 0.3.0.1 2025-04-02 18:21:08 +03:00
c740242c3f Обновил ошибки в хандлере 2025-04-02 18:17:36 +03:00
2f18c5ce85 [^] Изменил версию и статус 2025-04-02 18:08:52 +03:00
9c94b68d55 [+] Profile.from_response 2025-04-02 18:07:54 +03:00
bb852d2b3a [~] Починил авторизацию 2025-04-02 17:55:28 +03:00
d9a577ee14 [~] Ошибки поменялись? 2025-04-02 17:55:16 +03:00
52ebb42aa2 [~] Промежуточный вариант 2025-04-02 17:47:27 +03:00
d726088002 [~] Вычисляю где и как кто какал 2025-04-02 17:47:08 +03:00
577e8dbf2a [+] По дефолту теперь AnixartAccountGuest 2025-04-02 17:46:53 +03:00
c68735df97 [+] profile.objects.PROFILE
[+] profile.objects.PROFILE_NICK_HISTORY
2025-04-02 17:46:28 +03:00
dd550855c0 [+] AnixartAccountGuest
[+] AnixartAccount.login
[+] auth.objects
2025-04-02 17:43:46 +03:00
733da877fa [+] secrets.txt 2025-04-02 16:58:52 +03:00
14fb575548 [~] Надо бы сделать 2025-04-02 16:23:36 +03:00
8762cca288 Обновил импорты 2025-04-02 16:04:10 +03:00
d288837bb4 [~] 2025-04-02 16:03:39 +03:00
3bd27840e1 [+] AnixartApiErrors
[+] Описание для AnixartAPIError
2025-04-02 16:03:27 +03:00
87fa813700 [>] {api/, request_handler} > api.py 2025-04-02 16:02:46 +03:00
29205efe4f [+] AnixartAccountSaved 2025-04-02 16:02:01 +03:00
68e45336ad [+] Версия ?.?.? 2025-04-02 15:35:06 +03:00
ae1f97a07c [+] 0.3.0? 2025-04-02 15:33:25 +03:00
85c7605b2c [~] Обновил год в LICENSE 2025-04-02 15:29:24 +03:00
82874814e0 [!] Глобальные изменения
[+] Блок auth
[>] __version__ > __meta__
[>] errors > exceptions
2025-04-02 15:20:46 +03:00
44e0485f99 [~] Изменил версию 2025-04-02 15:19:22 +03:00
daec2199b6 [~] Обновил команду) 2025-04-02 14:31:09 +03:00
fbb738fd1c [+] poetry 2025-04-02 14:30:51 +03:00
c4d758f65a [+] poetry.lock 2025-04-02 14:30:20 +03:00
04be5ed2a9 Немного изменил нейминг 2022-09-27 23:36:32 +03:00
87ff4463c4 Перетащил <br/> 2022-09-27 23:33:38 +03:00
403e7c8b79 Как работать с библиотекой 2022-09-27 23:33:09 +03:00
2f0e4e3942 Создал список примеров 2022-09-27 23:30:03 +03:00
1c8d56dca6 Добавил "Обратная связь" 2022-09-27 23:29:43 +03:00
326034fda9 Добавил способ установки
Изменил инфу по поводу доков
2022-09-27 23:25:04 +03:00
0cf61563be Development Status :: 3 - Alpha 2022-09-27 23:16:06 +03:00
a6f1655449 * Перетащил файлы из прошлого проекта
- Поменял нейминиги
  - Оптимизировал `request_handler.py`
  - Оптимизировал `errors.py`
  - Оптимизировал `auth.py`
  - Оптимизировал `api/api.py`
  - Добавил `api/api.pyi`
* Добавил пример: `/examples/auth.py`
* Обновил зависимости
2022-09-27 23:14:42 +03:00
c6bfda72df Добавил данные в ченчлог 2022-09-27 23:06:10 +03:00
8e9716cd06 Добавил эндпоинты 2022-09-27 20:56:59 +03:00
9f50168205 Немного изменил инфу о себе 2022-09-27 20:56:40 +03:00
26 changed files with 1114 additions and 75 deletions

3
.gitignore vendored
View File

@@ -130,3 +130,6 @@ dmypy.json
# PyCharm
.idea/
poetry.lock
secrets.txt

View File

@@ -1,4 +1,4 @@
Copyright (c) 2022 Maxim Khomutov
Copyright (c) 2025 Maxim Khomutov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without limitation in the rights to use, copy, modify, merge, publish, and/ or distribute copies of the Software in an educational or personal context, subject to the following conditions:

View File

@@ -4,16 +4,25 @@
Враппер для использования Anixart API.\
Библиотека создана только для ознакомления c API.
**Автор презирает и не поддерживает создание авторегов / ботов для накрутки / спам ботов.**
**Автор не поддерживает и презирает создание авторегов / ботов для накрутки / спам ботов.**
### Вся документация в папке [docs](./docs)
### Установка
```shell
pip install anixart
```
### Как работать с библиотекой
#### Вся документация в папке [docs](https://github.com/SantaSpeen/anixart/tree/master/docs)
_(Пока что не дошли руки написать доки, гляньте в [examples](https://github.com/SantaSpeen/anixart/tree/master/examples) как работать с библиотекой)_
## Обратная связь
Если у вас возникли вопросы, или вы хотите каким-либо образом помочь автору вы можете написать ему в телеграм: [@SantaSpeen](https://t.me/SantaSpeen)
<br/>
## License
```text
Copyright (c) 2022 Maxim Khomutov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without limitation in the rights to use, copy, modify, merge, publish, and/ or distribute copies of the Software in an educational or personal context, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Permission is granted to sell and/ or distribute copies of the Software in a commercial context, subject to the following conditions:
- Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
Проект распространяется под лицензией `FPA License`. Подробнее в [LICENSE](LICENSE)

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
from .__meta__ import *
from .api import AnixartAPI
from . import auth
from . import enums
from . import exceptions

View File

@@ -3,9 +3,9 @@
__title__ = 'anixart'
__description__ = 'Wrapper for using the Anixart API.'
__url__ = 'https://github.com/SantaSpeen/anixart'
__version__ = '0.0.1'
__build__ = 1
__author__ = 'Maxim Khomutov'
__version__ = '0.2.1'
__build__ = 3
__author__ = 'SantaSpeen'
__author_email__ = 'SantaSpeen@gmail.com'
__license__ = "FPA"
__copyright__ = 'Copyright 2022 Maxim Khomutov'
__copyright__ = 'Copyright 2022 © SantaSpeen'

153
anixart/api.py Normal file
View File

@@ -0,0 +1,153 @@
# -*- coding: utf-8 -*-
import requests
from .__meta__ import __version__, __build__
from .auth import AnixartAccount, AnixartAccountGuest
from .endpoints_map import endpoints_map
from .exceptions import AnixartAPIRequestError, AnixartAPIError
from .exceptions import AnixartInitError
debug = True
class _ApiMethodGenerator:
def __init__(self, start_path, _callback: callable):
self._path = start_path
self._callback = _callback
def __setattr__(self, __name, __value):
raise TypeError(f"cannot set '{__name}' attribute of immutable type 'ApiMethodGenerator'")
def __getattr__(self, item):
self._path += f".{item}"
return self
def __call__(self, *args, **kwargs):
if debug:
print(f"[D] __call__ -> {self._path} args={args} kwargs={kwargs}")
return self._callback(self._path, *args, **kwargs)
def __str__(self):
return f"ApiMethodGenerator(path={self._path!r})"
def __repr__(self):
return f"<{self}>"
class AnixartAPI:
API_URL = "https://api.anixart.tv"
def __init__(self, account: AnixartAccount = None):
if account is None:
account = AnixartAccountGuest()
self.use_account(account)
def use_account(self, account: AnixartAccount):
if not isinstance(account, AnixartAccount):
raise AnixartInitError(f'Use class "AnixartAccount" for user. But {type(account)} given.')
self.__account = account
self.__account._set_api(self)
self.__account.login()
self.__token = account.token
self._session = account.session
self._session.headers = {
'User-Agent': f'AnixartPyAPI/{__version__}-{__build__} (Linux; Android 15; AnixartPyAPI Build/{__build__})'
}
@property
def account(self):
return self.__account
def __parse_response(self, res: requests.Response):
if debug:
print(f"[D] -> {res.request.method} body='{res.request.body!s}' url='{res.url!s}'")
print(f"[D] <- {res.status_code}, {len(res.text)=}")
if res.status_code != 200:
e = AnixartAPIRequestError("Bad Request: Invalid request parameters.")
e.message = (
f"Bad Request: Invalid request parameters.\n"
f"Request: {res.request.method} {res.url}\n"
f"Status code: {res.status_code}\n"
f"Response: {res.text}\n"
f"Client headers: {self._session.headers}\n"
f"Client payload: {res.request.body}"
)
e.code = 400
raise e
if not res.text:
raise AnixartAPIError("AnixartAPI send unknown error: Empty response. Is provided data correct?")
try:
response = res.json()
except ValueError as e:
raise AnixartAPIError("Failed to parse JSON response")
if debug:
print(response)
if response['code'] != 0:
e = AnixartAPIError(f"AnixartAPI send unknown error, code: {response['code']}")
e.code = response['code']
raise e
return response
def _post(self, method: str, _json: bool = False, **kwargs):
url = self.API_URL + method
kwargs["token"] = kwargs.get("token", self.__token)
if kwargs["token"]:
url += f"?token={self.__token}"
req_settings = {"url": url}
if _json:
self._session.headers["Content-Type"] = "application/json; charset=UTF-8"
req_settings.update({"json": kwargs})
else:
req_settings.update({"data": kwargs})
res = self._session.post(**req_settings)
return self.__parse_response(res)
def _get(self, method: str, **kwargs):
if self.__token:
kwargs["token"] = kwargs.get("token", self.__token)
res = self._session.get(self.API_URL + method, params=kwargs)
return self.__parse_response(res)
def _execute(self, http_method, endpoint, **kwargs):
http_method = http_method.upper()
if http_method == "GET":
return self._get(endpoint, **kwargs)
elif http_method == "POST":
return self._post(endpoint, **kwargs)
else:
raise AnixartAPIRequestError("Allow only GET and POST requests.")
def __execute_endpoint_from_map(self, key, *args, **kwargs):
endpoint = endpoints_map.get(key)
if endpoint is None:
raise AnixartAPIError("Invalid endpoint.")
return self._execute_endpoint(endpoint, *args, **kwargs)
def _execute_endpoint(self, endpoint, *args, **kwargs):
req_settings, headers = endpoint.build_request(*args, **kwargs)
self._session.headers.update(headers)
if endpoint.is_post:
res = self._session.post(**req_settings)
else:
res = self._session.get(**req_settings)
return self.__parse_response(res)
def get(self, endpoint, *args, **kwargs):
return self._execute("GET", endpoint.format(*args), **kwargs)
def post(self, endpoint, *args, **kwargs):
return self._execute("POST", endpoint.format(*args), **kwargs)
def __getattr__(self, item):
return _ApiMethodGenerator(item, self.__execute_endpoint_from_map)
def __str__(self):
return f'AnixartAPI(account={self.__account!r})'
def __repr__(self):
return f"<{self}>"

1
anixart/auth/__init__.py Normal file
View File

@@ -0,0 +1 @@
from .accounts import AnixartAccount, AnixartAccountSaved, AnixartAccountGuest

125
anixart/auth/account.py Normal file
View File

@@ -0,0 +1,125 @@
import json
from pathlib import Path
import requests
from anixart import endpoints
from anixart.exceptions import AnixartInitError
class AnixartAccount:
guest = False
def __init__(self, username: str, password: str):
self._username = username
self._password = password
if not isinstance(username, str) or not isinstance(password, str):
raise AnixartInitError("Auth data must be strings.")
self._id = None
self._token = None
self._session = requests.Session()
self._api = None
def _set_api(self, api):
self._api = api
@property
def id(self):
return self._id
@property
def username(self):
return self._username
@property
def session(self):
return self._session
@property
def token(self):
return self._token
def to_file(self, filename: str | Path) -> "AnixartAccountSaved":
"""Save the account information to a file."""
acc = AnixartAccountSaved.from_account(filename, self)
acc.save()
return acc
@classmethod
def from_file(cls, filename: str | Path) -> "AnixartAccountSaved":
"""Load the account information from a file."""
acc = AnixartAccountSaved(filename)
acc.load()
return acc
def login(self):
"""Login to Anixart and return the token."""
res = self._api.post(endpoints.SING_IN, login=self.username, password=self._password)
uid = res["profile"]["id"]
token = res["profileToken"]["token"]
self._id = uid
self._token = token
def __str__(self):
return f'AnixartAccount(login={self._username!r}, password={"*" * len(self._password)!r})'
def __repr__(self):
return f"<{self}>"
class AnixartAccountSaved(AnixartAccount):
def __init__(self, account_file: str | Path = "anixart_account.json"):
super().__init__("", "")
self._file = Path(account_file)
def save(self):
"""Save the account information to a file."""
data = {
"id": self._id,
"token": self._token
}
with open(self._file, 'w') as f:
json.dump(data, f, indent=4)
def load(self):
"""Load the account information from a file."""
if not self._file.exists():
raise AnixartInitError(f"Account file {self._file} does not exist.")
with open(self._file, 'r') as f:
data = json.load(f)
self._id = data.get("id")
self._username = data.get("username")
self._token = data.get("token")
if not self._id or not self._token:
raise AnixartInitError("id and token must be provided in the account file.")
@classmethod
def from_account(cls, account_file: str | Path, account: AnixartAccount):
c = cls(account_file)
c._username = account.username
c._password = account._password
c._token = account.token
return c
def login(self):
"""Login to Anixart using the saved credentials."""
# Проверяем токен, если просрочен, то логинимся
# Если токен валиден, то просто дальше работаем
# TODO: Implement login logic here
pass
def __str__(self):
return f'AnixartAccountSaved(account_file={self._file!r}")'
class AnixartAccountGuest(AnixartAccount):
guest = True
def __init__(self):
super().__init__("", "")
self._token = ""
def login(self): ...
def __str__(self):
return f'AnixartAccountGuest(token={self._token!r}")'

23
anixart/auth/endpoints.py Normal file
View File

@@ -0,0 +1,23 @@
from anixart.utils import endpoint
from .factories import AuthLoginFactory
# ----------- # AUTH # ----------- #
#
# POST
# SING_UP = None
# SING_IN = "/auth/signIn"
#
# !! Not Checked !!
# POST
# AUTH_SING_IN_WITH_GOOGLE = "/auth/google" # {googleIdToken} or {login, email, googleIdToken}
# AUTH_SING_IN_WITH_VK = "/auth/vk" # {vkAccessToken}
class AnixartAuthEndpoints:
"""Anixart API authentication endpoints."""
singup = None # Удалено дабы исключить автореги (по просьбе аниксарта)
login = endpoint("/auth/signIn", "POST", AuthLoginFactory, {}, {"login": str, "password": str})
# login_google = None # endpoint("/auth/google", "POST", {}, {"googleIdToken": str})
# login_vk = None # endpoint("/auth/vk", "POST", {}, {"vkAccessToken": str})

41
anixart/auth/factories.py Normal file
View File

@@ -0,0 +1,41 @@
from dataclasses import dataclass
from anixart.exceptions import AnixartAuthError
from anixart.utils import BaseParseFactory
@dataclass
class _AuthLoginFactoryProfileToken:
# { "id": 1, "token": "hash?" }
id: int # token id?
token: str # token str
@classmethod
def from_dict(cls, data: dict):
return cls(
id=data.get("id", -1),
token=data.get("token")
)
@dataclass
class AuthLoginFactory(BaseParseFactory):
# { "code": 0, "profile": {...}, "profileToken": {_AuthLoginFactoryProfileToken} }
profile: dict # TODO - create a ProfileFactory
profileToken: _AuthLoginFactoryProfileToken
_errors = {
2: "Incorrect login",
3: "Incorrect password",
}
_exception = AnixartAuthError
@classmethod
def from_dict(cls, data: dict):
return cls(
code=data["code"],
profile=data.get("profile", {}),
profileToken=_AuthLoginFactoryProfileToken.from_dict(data.get("profileToken", {})),
_errors=cls._errors,
_exception=cls._exception
)

9
anixart/endpoints_map.py Normal file
View File

@@ -0,0 +1,9 @@
from .auth.factories import AuthLoginFactory
endpoints_map = {
# Auth
"auth.login": AuthLoginFactory
# Profile
}

21
anixart/enums.py Normal file
View File

@@ -0,0 +1,21 @@
from enum import IntEnum
class AnixartComment(IntEnum):
DISLIKE = 1
LIKE = 2
class AnixartProfileVotedSort(IntEnum):
LAST_FIRST = 1
OLD_FIRST = 2
STAR_5 = 3
STAR_4 = 4
STAR_3 = 5
STAR_2 = 6
STAR_1 = 7
class AnixartLists(IntEnum):
WATCHING = 1
IN_PLANS = 2
WATCHED = 3
POSTPONED = 4
DROPPED = 5

17
anixart/exceptions.py Normal file
View File

@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
class AnixartBaseError(Exception): ...
# Init errors
class AnixartInitError(AnixartBaseError, TypeError): ...
# API errors
class AnixartAPIError(AnixartBaseError):
message = "unknown error"
code = 0
class AnixartAuthError(AnixartAPIError): ...
class AnixartAPIRequestError(AnixartAPIError): ...

View File

@@ -0,0 +1 @@
from .factories import Profile, ProfileFull, ProfileVote, ProfileRoles, ProfileHistory, ProfileFriendsPreview

View File

@@ -0,0 +1,80 @@
from anixart.utils import endpoint
# # ----------- # PROFILE # ----------- #
# # TODO PROFILE: SETTINGS, SETTINGS_RELEASE, SETTINGS_RELEASE_FIRST,
# # SETTINGS_COMMENTS, SETTINGS_COLLECTION, EDIT_AVATAR, SETTINGS_RELEASE_LIST,
# # SETTINGS_RELEASE_TYPE
#
# # GET
# PROFILE = "/profile/{}" # + profile id (Токен нужен только что бы был is_my_profile)
# PROFILE_NICK_HISTORY = "/profile/login/history/all/{}/{}" # profile id / page (Токен не нужен)
#
# PROFILE_BLACKLIST = "/profile/blocklist/all/{}" # page
# PROFILE_BLACKLIST_ADD = "/profile/blocklist/add/{}" # profile id
# PROFILE_BLACKLIST_REMOVE = "/profile/blocklist/remove/{}" # profile id
#
# FRIENDS = "/profile/friend/all/{}/{}" # profile id / page
# FRIENDS_RQ_IN = "/profile/friend/requests/in/{}" # page
# FRIENDS_RQ_OUT = "/profile/friend/requests/out/{}" # page
# FRIENDS_RQ_IN_LAST = "/profile/friend/requests/in/last"
# FRIENDS_RQ_OUT_LAST = "/profile/friend/requests/out/last"
# FRIENDS_SEND = "/profile/friend/request/send/{}" # profile id
# FRIENDS_REMOVE = "/profile/friend/request/remove/{}" # profile id
#
# VOTE_VOTED = "/profile/vote/release/voted/{}/{}" # profile id / page
# # Да, ребята из аниксарта не знают английский; ↓
# # noinspection SpellCheckingInspection
# VOTE_UNVENTED = "/profile/vote/release/unvoted/{}" # page
#
# LISTS = "/profile/list/all/{}/{}/{}" # profile id / list id / page
#
# SETTINGS_NOTIFICATION = "/profile/preference/notification/my"
# SETTINGS_NOTIFICATION_RELEASE = "/profile/preference/notification/episode/edit"
# SETTINGS_NOTIFICATION_RELEASE_FIRST = "/profile/preference/notification/episode/first/edit"
# SETTINGS_NOTIFICATION_COMMENTS = "/profile/preference/notification/comment/edit"
# SETTINGS_NOTIFICATION_COLLECTION = "/profile/preference/notification/my/collection/comment/edit"
#
# CHANGE_PASSWORD = "/profile/preference/password/change"
#
# # POST
# EDIT_STATUS = "/profile/preference/status/edit"
# EDIT_SOCIAL = "/profile/preference/social/edit"
# EDIT_AVATAR = "/profile/preference/avatar/edit"
#
# # {"profileStatusNotificationPreferences":[0 - favorite, + all in AnixList]}
# SETTINGS_NOTIFICATION_RELEASE_LIST = "/profile/preference/notification/status/edit"
# # {"profileTypeNotificationPreferences":[type ids]}
# SETTINGS_NOTIFICATION_RELEASE_TYPE = "/profile/preference/notification/type/edit"
#
# # Not Checked
# # GET
# PROFILE_SOCIAL = "/profile/social/{}" # profile id
#
# FRIENDS_RECOMMENDATION = "/profile/friend/recommendations"
# FRIENDS_RQ_HIDE = "profile/friend/request/hide/{}" # profile id
#
# SETTINGS_PROFILE = "/profile/preference/my"
# SETTINGS_PROFILE_CHANGE_EMAIL = "/profile/preference/email/change" # {current_password, current, new}
# SETTINGS_PROFILE_CHANGE_EMAIL_CONFIRM = "/profile/preference/email/change/confirm" # {current}
#
# # /profile/preference/social
# SETTINGS_PROFILE_STATUS_DELETE = "/profile/preference/status/delete"
#
# # POST
# PROFILE_PROCESS = "/profile/process/{}" # profile id
#
# SETTINGS_PROFILE_CHANGE_LOGIN = "/profile/preference/email/login/confirm" # {login}
# SETTINGS_PROFILE_CHANGE_LOGIN_INFO = "/profile/preference/email/login/info" # {login}
#
# SETTINGS_PROFILE_BIND_GOOGLE = "/profile/preference/google/bind" # {idToken, }
# SETTINGS_PROFILE_UNBIND_GOOGLE = "/profile/preference/google/unbind"
# SETTINGS_PROFILE_BIND_VK = "/profile/preference/google/bind" # {accessToken, }
# SETTINGS_PROFILE_UNBIND_VK = "/profile/preference/google/unbind"
#
# SETTINGS_PROFILE_PRIVACY_COUNTS = "/profile/preference/privacy/counts/edit"
# SETTINGS_PROFILE_PRIVACY_FRIENDS_REQUESTS = "/profile/preference/privacy/friendRequests/edit"
# SETTINGS_PROFILE_PRIVACY_SOCIAL = "/profile/preference/privacy/social/edit"
# SETTINGS_PROFILE_PRIVACY_STATS = "/profile/preference/privacy/stats/edit"
class AnixartProfileEndpoints:
profile = endpoint("/profile/{id}", "GET", None, {"id": int}, {})

View File

@@ -0,0 +1,187 @@
from dataclasses import dataclass
@dataclass
class ProfileHistory:
# TODO: Надо ещё изучить релизы
pass
@dataclass
class ProfileVote:
# TODO: Надо ещё изучить релизы
pass
@dataclass
class ProfileRoles:
# TODO: Надо ещё изучить роли (У меня их нет(()
pass
@dataclass
class Profile:
id: int
login: str
avatar: str
@dataclass
class ProfileFriendsPreview(Profile):
friend_count: int
friend_status: int
is_sponsor: bool
is_online: bool
is_verified: bool
is_social: bool
badge_id: None
badge_name: None
badge_type: None
badge_url: None
@dataclass
class ProfileFull(Profile):
status: str
rating_score: int
history: list[ProfileHistory]
votes: list[ProfileVote]
roles: list[ProfileRoles]
friends_preview: list[ProfileFriendsPreview]
collections_preview: list # TODO: Коллекции изучить
release_comments_preview: list # Тут вообще не понял
comments_preview: list # Тут типа превью к коментам
release_videos_preview: list # Тут вообще не понял
watch_dynamics: list # Тут вообще не понял
last_activity_time: int
register_date: int
vk_page: str
tg_page: str
inst_page: str
tt_page: str
discord_page: str
ban_expires: int
ban_reason: str
privilege_level: int
watching_count: int
plan_count: int
completed_count: int
hold_on_count: int
dropped_count: int
favorite_count: int
comment_count: int
collection_count: int
video_count: int
friend_count: int
subscription_count: int
watched_episode_count: int
watched_time: int
sponsorshipExpires: int
is_private: bool
is_sponsor: bool
is_banned: bool
is_perm_banned: bool
is_bookmarks_transferred: bool
is_sponsor_transferred: bool
is_vk_bound: bool
is_google_bound: bool
is_release_type_notifications_enabled: bool
is_episode_notifications_enabled: bool
is_first_episode_notification_enabled: bool
is_related_release_notifications_enabled: bool
is_report_process_notifications_enabled: bool
is_comment_notifications_enabled: bool
is_my_collection_comment_notifications_enabled: bool
is_my_article_comment_notifications_enabled: bool
is_verified: bool
is_blocked: bool
is_me_blocked: bool
is_stats_hidden: bool
is_counts_hidden: bool
is_social_hidden: bool
is_friend_requests_disallowed: bool
is_online: bool
badge: None
friend_status: None
is_my_profile: bool = False
@classmethod
def from_response(cls, response: dict) -> "Profile":
profile = {"is_my_profile": response['is_my_profile'], **response['profile']}
return cls(**profile)
class _login:
pos_id: int
id: int
newLogin: str
timestamp: int
class ProfileLoginsHistory:
content: list[_login]
total_count: int
# total_page_count: int
# current_page: int
# From singup
# "profile": {
# "id": 123,
# "login": "loginstr",
# "avatar": "*.jpg",
# "status": "123",
# "badge": null,
# "history": [],
# "votes": [],
# "roles": [],
# "last_activity_time": 100046000,
# "register_date": 100046000,
# "vk_page": "",
# "tg_page": "",
# "inst_page": "",
# "tt_page": "",
# "discord_page": "",
# "ban_expires": 0,
# "ban_reason": null,
# "privilege_level": 0,
# "watching_count": 1,
# "plan_count": 1,
# "completed_count": 1,
# "hold_on_count": 1,
# "dropped_count": 1,
# "favorite_count": 1,
# "comment_count": 1,
# "collection_count": 0,
# "video_count": 0,
# "friend_count": 1,
# "subscription_count": 0,
# "watched_episode_count": 1,
# "watched_time": 1,
# "is_private": false,
# "is_sponsor": false,
# "is_banned": false,
# "is_perm_banned": false,
# "is_bookmarks_transferred": false,
# "is_sponsor_transferred": false,
# "is_vk_bound": true,
# "is_google_bound": true,
# "is_release_type_notifications_enabled": false,
# "is_episode_notifications_enabled": true,
# "is_first_episode_notification_enabled": true,
# "is_related_release_notifications_enabled": true,
# "is_report_process_notifications_enabled": true,
# "is_comment_notifications_enabled": true,
# "is_my_collection_comment_notifications_enabled": true,
# "is_my_article_comment_notifications_enabled": false,
# "is_verified": false,
# "friends_preview": [],
# "collections_preview": [],
# "release_comments_preview": [],
# "comments_preview": [],
# "release_videos_preview": [],
# "watch_dynamics": [],
# "friend_status": null,
# "rating_score": 0,
# "is_blocked": false,
# "is_me_blocked": false,
# "is_stats_hidden": false,
# "is_counts_hidden": false,
# "is_social_hidden": false,
# "is_friend_requests_disallowed": false,
# "is_online": false,
# "sponsorshipExpires": 0
# }

303
anixart/utils.py Normal file
View File

@@ -0,0 +1,303 @@
# -*- coding: utf-8 -*-
from dataclasses import dataclass, field
from typing import Literal, Any
from anixart.exceptions import AnixartAPIError
@dataclass
class BaseParseFactory:
code: int
_errors: dict[int, str]
_exception: type[AnixartAPIError]
def raise_if_error(self):
if self.code != 0:
error = self._errors.get(self.code)
if error:
raise error
else:
raise ValueError(f"Unknown error code: {self.code}")
@classmethod
def from_dict(cls, data: dict) -> "BaseParseFactory":
return cls(
code=data.get("code", 0),
_errors={},
_exception=AnixartAPIError,
)
@dataclass
class Endpoint:
path: str
method: Literal["GET", "POST"]
parse_factory: type[BaseParseFactory]
required_args: dict[str, type] = field(default_factory=dict)
required_kwargs: dict[str, type] = field(default_factory=dict)
_json = False
_API_ENDPOINT = "https://api.anixart.tv/"
def __post_init__(self):
if self.method not in ["GET", "POST"]:
raise ValueError("Method must be either GET or POST.")
if not isinstance(self.required_kwargs, dict):
raise ValueError("Required arguments must be a dictionary.")
if not all(isinstance(v, type) for v in self.required_kwargs.values()):
raise ValueError("All values in required arguments must be types.")
@property
def is_get(self) -> bool:
return self.method == "GET"
@property
def is_post(self) -> bool:
return self.method == "POST"
@property
def is_json(self) -> bool:
return self._json
def _post(self, method: str, **kwargs):
headers = {}
url = self._API_ENDPOINT + method
if token:=kwargs.get("token"):
kwargs["token"] = token
url += f"?token={token}"
req_settings = {"url": url}
if self._json:
headers["Content-Type"] = "application/json; charset=UTF-8"
req_settings.update({"json": kwargs})
else:
req_settings.update({"data": kwargs})
return req_settings, headers
def _get(self, method: str, **kwargs):
headers = {}
url = self._API_ENDPOINT + method
if token:=kwargs.get("token"):
kwargs["token"] = token
req_settings = {"url": url, "params": kwargs}
return req_settings, headers
def _check_arguments(self, **kwargs):
"""Check if all required arguments are present."""
missing_args = [] # (arg, reason)
for arg, arg_type in self.required_args.items():
if arg not in kwargs:
missing_args.append((arg, "arg missing"))
elif not isinstance(kwargs[arg], arg_type):
missing_args.append((arg, f"arg invalid type: {type(kwargs[arg])}"))
for arg, arg_type in self.required_kwargs.items():
if arg not in kwargs:
missing_args.append((arg, "kwarg missing"))
elif not isinstance(kwargs[arg], arg_type):
missing_args.append((arg, f"kwarg invalid type: {type(kwargs[arg])}"))
if missing_args:
pretty_args = ", ".join(f"{arg} ({reason})" for arg, reason in missing_args)
raise ValueError(f"Missing or invalid arguments: {pretty_args}")
def build_request(self, **kwargs) -> None | tuple[dict[str, dict[str, Any] | str], dict[Any, Any]] | tuple[
dict[str, dict[str, Any] | str], dict[str, str]]:
"""
Build the request for the endpoint.
:param kwargs: Arguments to be passed to the endpoint.
:return: A tuple containing the HTTP method, headers, and request settings.
"""
self._check_arguments(**kwargs)
args = {arg: kwargs[arg] for arg in self.required_args}
if self.method == "POST":
return self._post(self.path.format(**args), **kwargs)
if self.method == "GET":
return self._get(self.path.format(**args), **kwargs)
def endpoint(path: str, method: Literal["GET", "POST"], parse_factory: type[BaseParseFactory], required_args: dict[str, type], required_kwargs: dict[str, type]) -> Endpoint:
return Endpoint(path, method, parse_factory, required_args, required_kwargs)
# ----------- # COLLECTION # ----------- #
# GET
COLLECTION = "/collection/{}" # collection id
COLLECTION_RELEASES = "/collection/{}/releases/{}" # collection id / page
COLLECTION_LIST = "/collection/all/{}" # page
COLLECTION_COMMENTS = "/collection/comment/all/{}/{}" # collection id / page
COLLECTION_COMMENTS_VOTE = "/collection/comment/vote/{}/{}" # collection comment id / mark (1, 2)
COLLECTION_COMMENTS_VOTES = "/collection/comment/votes/{}/{}" # collection comment id / page
COLLECTION_COMMENTS_DELETE = "/collection/comment/delete/{}" # collection comment id
COLLECTION_FAVORITE = "/collectionFavorite/all/{}" # page
COLLECTION_FAVORITE_ADD = "/collectionFavorite/add/{}" # collection id
COLLECTION_FAVORITE_DELETE = "/collectionFavorite/delete/{}" # collection id
# POST
COLLECTION_COMMENTS_ADD = "/collection/comment/add/{}" # collection id
COLLECTION_COMMENTS_EDIT = "/collection/comment/edit/{}" # collection comment id
COLLECTION_COMMENTS_REPLIES = "/collection/comment/replies/{}/{}" # collection comment id / page
# Not Checked
# GET
COLLECTION_PROFILE = "/collection/all/profile/{}/{}" # p_id/page
COLLECTION_RELEASE_IN = "/collection/all/release/{}/{}" # r_id/page
COLLECTION_MY = "/collectionMy/{id}/releases"
COLLECTION_MY_DELETE = "/collectionMy/delete/{}" # collectionId
COLLECTION_MY_ADD_RELEASE = "/collectionMy/release/add/{}" # collectionId
# POST
COLLECTION_COMMENTS_PROCESS = "/collection/comment/process/{}" # commentId
COLLECTION_COMMENTS_REPORT = "/collection/comment/report/{}" # commentId
COLLECTION_REPORT = "/collection/report/{}" # collectionId
COLLECTION_MY_CREATE = "/collectionMy/create"
COLLECTION_MY_EDIT = "/collectionMy/edit/{}" # collectionId
COLLECTION_MY_EDIT_IMAGE = "/collectionMy/editImage/{}" # collectionId
# ----------- # RELEASE # ----------- #
# GET
RELEASE = "/release/{}" # release id
RELEASE_VOTE_ADD = "/release/vote/add/{}/{}" # release id / mark 1-5
RELEASE_VOTE_DELETE = "/release/vote/delete/{}" # release id
RELEASE_RANDOM = "/release/random"
RELEASE_COMMENTS = "/release/comment/all/{}/{}" # release id / page
RELEASE_COMMENTS_VOTE = "/release/comment/vote/{}/{}" # release comment id / mark (1, 2)
RELEASE_COMMENTS_VOTES = "/release/comment/votes/{}/{}" # release comment id / page
RELEASE_COMMENTS_REPLIES = "/release/comment/replies/{}/{}" # release comment id / page
RELEASE_COMMENTS_DELETE = "/release/comment/delete/{}" # release comment id
# POST
RELEASE_COMMENTS_ADD = "/release/comment/add/{}" # release id
RELEASE_COMMENTS_EDIT = "/release/comment/edit/{}" # release comment id
# Not Checked
# GET
RELEASE_COMMENTS_REPORT = "/release/comment/report/{}" # commentId
RELEASE_COMMENTS_PROCESS = "/release/comment/process/{}" # commentId
RELEASE_PROFILE_COMMENTS = "/release/comment/all/profile/{}/{}" # p_id/page
RELEASE_STREAMING_PLATFORM = "/release/streaming/platform/{}/" # releaseId
# POST
RELEASE_REPORT = "/release/report/{}" # r_id
# ----------- # OTHER # ----------- #
# TODO OTHER: EXPORT_BOOKMARKS, IMPORT_BOOKMARKS, CAN_IMPORT_BOOKMARKS
# GET
TYPE = "/type/all"
TYPE_RELEASE = "/type/{}" # r_id
TOGGLES = "/config/toggles?version_code={}&is_beta={}" # version_code: int, is_beta: bool
SCHEDULE = "/schedule"
# POST
# {"bookmarksExportProfileLists":[0 - favorite, + all in AnixList]}
EXPORT_BOOKMARKS = "/export/bookmarks"
# {"completed":[],"dropped":[],"holdOn":[],"plans":[],"watching":[],"selected_importer_name":"Shikimori"}
IMPORT_BOOKMARKS = "/import/bookmarks"
CAN_IMPORT_BOOKMARKS = "/import/status" # code: 0 - Yes, code: 2 - no
# Not Checked
# GET
# page {token, genres: [], studio, category, status, year, episodes, sort,
# country, season, duration, ratings: [], extended_mode: bool}
FILTER = "/filter/{}"
RELATED = "related/{}/{}" # relatedId/page
# POST
VIDEO_PARSE = "/video/parse"
# ----------- # SEARCH # ----------- #
# TODO SEARCH: *
# { "query": text, "searchBy": 0}
# POST
SEARCH_COLLECTION = "/search/collections/{}" # page
SEARCH_RELEASE = "/search/releases/{}" # page
SEARCH_FAVORITE = "/search/favorites/{}" # page
SEARCH_COLLECTION_FAVORITE = "/search/favoriteCollections/{}" # page
SEARCH_LIST = "/search/profile/list/{}/{}" # list id / page
SEARCH_PROFILE = "/search/profiles/{}" # page
SEARCH_HISTORY = "/search/history/{}" # page
#
SEARCH_COLLECTION_PROFILE = "/search/profileCollections/{}/{}" # p_id/page
# ----------- # NOTIFICATIONS # ----------- #
# TODO NOTIFICATIONS: *
# GET
NOTIFICATION_READ = "/notification/read"
NOTIFICATION_COUNT = "/notification/count"
NOTIFICATION_COLLECTION_COMMENTS = "/notification/collectionComments/{}" # page
NOTIFICATION_MY_COLLECTION_COMMENTS = "/notification/my/collection/comments/{}" # page
NOTIFICATION_RELEASE_COMMENTS = "/notification/releaseComments/{}" # page
NOTIFICATION_EPISODES = "/notification/episodes/{}" # page
NOTIFICATION_FRIEND = "/notification/friends/{}" # page
NOTIFICATION_COLLECTION_COMMENTS_DELETE = "/notification/collectionComment/delete/{}" # n_id
NOTIFICATION_MY_COLLECTION_COMMENTS_DELETE = "/notification/my/collection/comment/delete/{}" # page
NOTIFICATION_RELEASE_COMMENTS_DELETE = "/notification/releaseComment/delete/{}" # page
NOTIFICATION_EPISODES_DELETE = "/notification/episode/delete/{}" # page
NOTIFICATION_FRIEND_DELETE = "/notification/friends/delete/{}" # page
# ----------- # DISCOVER # ----------- #
# TODO DISCOVER: *
# Not Checked
# POST
DISCOVER_COMMENTS = "/discover/comments"
DISCOVER_DISCUSSING = "/discover/discussing" # {token}
DISCOVER_INTERESTING = "/discover/interesting"
DISCOVER_RECOMMENDATION = "/discover/recommendations/{}" # page
DISCOVER_WATCHING = "/discover/watching/{}" # page
# ----------- # EpisodeApi.kt # ----------- #
# TODO EpisodeApi.kt: *
# Здесь методы из com.swiftsoft.anixartd.network.api.EpisodeApi.kt
# Если надо, переделай так как тебе надо
# Not Checked
# GET
# @GET("episode/target/{releaseId}/{sourceId}/{position}")
# @GET("episode/{releaseId}/{typeId}/{sourceId}")
# @GET("episode/{releaseId}")
# @GET("episode/updates/{releaseId}/{page}")
# POST
# @POST("episode/report/{releaseId}/{sourceId}/{position}")
# @POST("episode/unwatch/{releaseId}/{sourceId}/{position}")
# @POST("episode/unwatch/{releaseId}/{sourceId}")
# @POST("episode/watch/{releaseId}/{sourceId}/{position}")
# @POST("episode/watch/{releaseId}/{sourceId}")
# ----------- # FAVORITE # ----------- #
# TODO FAVORITE: *
# Not Checked
# GET
FAVORITE = "/favorite/all/{}" # r_id
FAVORITE_ADD = "/favorite/add/{}" # r_id
FAVORITE_DELETE = "/favorite/delete/{}" # r_id {sort}
# ----------- # HISTORY # ----------- #
# TODO HISTORY: *
# GET
HISTORY = "/history/{}" # page
# Not Checked
# GET
HISTORY_ADD = "/history/add/{}/{}/{}" # r_id/s_id/position
HISTORY_DELETE = "/history/delete/{}" # r_id

View File

@@ -2,8 +2,73 @@
## Anixart API Wrapper
### 02.04.2025
#### Version: 0.3.0.1
##### Changes:
* Глобальная переборка всего проекта по `SDK-like` принципу
* Переработан блок `API`:
* `AnixartAPI` - Теперь является основным классом для работы с API.
* Добавлен блок `auth`:
* Работа с аккаунтом:
* `AnixartAccount` - Основной класс взаимодействия с аккаунтом (логин, пароль)
* `AnixartAccountSaved` - Работа с сохранённым аккаунтом
* `AnixartAccountGuest` - Авторизация без логина и пароля (Как гость)
* Добавлены ошибки
* `INCORRECT_LOGIN` - Неверный логин
* `INCORRECT_PASSWORD` - Неверный пароль
* Добавлен блок `profile`:
* Добавлены объекты для работы с `/profile/`:
* `Profile` - ДатаКласс для работы с профилем (`endpoints.PROFILE`) __не закончены подклассы__
* `ProfileLoginsHistory` - ДатаКласс для работы с историей логинов (`endpoints.PROFILE_NICK_HISTORY`) __не закончен__
* Добавлены ошибки
* `PROFILE_NOT_FOUND` - Профиль не найден (или невалидный)
### 28.09.2022
#### Version: 0.2.1
##### Changes:
* Перетащил файлы из прошлого проекта
- Поменял нейминиги
- Оптимизировал `request_handler.py`
- Оптимизировал `errors.py`
- Оптимизировал `auth.py`
- Оптимизировал `api/api.py`
- Добавил `api/api.pyi`
* Добавил пример: `/examples/auth.py`
* Обновил зависимости
##### TODOs:
* Метод `AnixartAPI.execute()` пересобрать и сделать нормальный хандлер запроса.
_Из прошлых версий_
* Проверить эндпоинты через чарлес
- Задокументировать методы
- Выявить и удалить не рабочие
- Выявить и удалить не используемые
### 27.09.2022
#### Version: 0.0.1, Build: 1
#### Version: 0.1.0
##### Changes:
* Изменил информацию в `__version__.py`
* Добавил эндпоинты из прошлого проекта
##### TODOs:
* Проверить эндпоинты через чарлес
- Задокументировать методы
- Выявить и удалить не рабочие
- Выявить и удалить не используемые
### 27.09.2022
#### Version: 0.0.1
##### Changes:

View File

@@ -17,4 +17,4 @@
* 2
* ...
3. [CHANGELOG](./CHANGELOG.md)
4. [LICENSE](./License.md)
4. [LICENSE](./LICENSE.md)

View File

@@ -0,0 +1,18 @@
from anixart import AnixartAPI, AnixartAccount
from anixart import endpoints
from anixart.exceptions import AnixartAPIRequestError
from anixart.profile import Profile
anix = AnixartAPI() # По умолчанию используется гость
# acc = AnixartAccount("SantaSpeen", "I_H@ve_Very_Secret_P@ssw0rd!")
# # id у аккаунта появляется только после
# anix.use_account(acc)
if __name__ == '__main__':
try:
raw = anix.get(endpoints.PROFILE, 1)
print(Profile.from_response(raw))
except AnixartAPIRequestError as e:
print(e.message)
print(e.code)

3
examples/readme.md Normal file
View File

@@ -0,0 +1,3 @@
## Директория с примерами
* Пример авторизации и вывода информации о себе: [auth.py](./auth.py)

3
poetry.toml Normal file
View File

@@ -0,0 +1,3 @@
[virtualenvs]
create = true
in-project = true

25
pyproject.toml Normal file
View File

@@ -0,0 +1,25 @@
[project]
name = "anixart"
version = "0.3.0.1"
description = "Wrapper for using the Anixart API."
authors = [
{name = "SantaSpeen",email = "santaspeen@gmail.com"}
]
license = {text = "FPA License"}
readme = "README.md"
requires-python = ">=3.10,<4.0"
dependencies = ["requests (>=2.32.3,<3.0.0)"]
dynamic = [ "classifiers" ]
[project.urls]
repository = "https://github.com/SantaSpeen/anixart"
[tool.poetry]
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"Topic :: Software Development :: Libraries :: Python Modules"
]
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

View File

View File

@@ -1,58 +0,0 @@
# -*- coding: utf-8 -*-
import os
import sys
from setuptools import setup
here = os.path.abspath(os.path.dirname(__file__))
packages = ['anixart']
requires = ['requests']
# 'setup.py publish' shortcut.
if sys.argv[-1] == 'publish':
os.system('py -m build')
os.system('py -m twine upload --repository testpypi dist/*')
os.system('py -m twine upload --repository pypi dist/*')
sys.exit()
about = {}
with open(os.path.join(here, 'anixart', '__version__.py'), 'r', encoding='utf-8') as f:
exec(f.read(), about)
with open('README.md', 'r', encoding='utf-8') as f:
readme = f.read()
setup(
name=about['__title__'],
version=about['__version__'],
description=about['__description__'],
long_description=readme,
long_description_content_type='text/markdown',
author=about['__author__'],
author_email=about['__author_email__'],
url=about['__url__'],
packages=packages,
package_data={'': ['LICENSE']},
package_dir={'anixart': 'anixart'},
include_package_data=True,
install_requires=requires,
license=about['__license__'],
classifiers=[
"Development Status :: 1 - Planning",
"Intended Audience :: Developers",
"Natural Language :: Russian",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"License :: Other/Proprietary License",
"Operating System :: OS Independent",
],
project_urls={
'Documentation': 'https://anixart.readthedocs.io/',
'Source': 'https://github.com/SantaSpeen/anixart',
},
python_requires=">=3.7",
)