24 Commits

Author SHA1 Message Date
SantaSpeen 723cf98475 [+] BaseParseFactory
[+] AuthLoginFactory
[^] Endpoint
[+] _ApiMethodGenerator
2025-04-11 19:00:10 +03:00
SantaSpeen 0682bdb42b Endpoint constructor 2025-04-04 18:17:36 +03:00
SantaSpeen 63bf159679 [~] minor 2025-04-04 18:17:23 +03:00
SantaSpeen 8a944319d6 [+] Fixes for post 2025-04-04 17:57:52 +03:00
SantaSpeen aee5c542b4 Данные о версии 0.3.0.1 2025-04-02 18:21:08 +03:00
SantaSpeen c740242c3f Обновил ошибки в хандлере 2025-04-02 18:17:36 +03:00
SantaSpeen 2f18c5ce85 [^] Изменил версию и статус 2025-04-02 18:08:52 +03:00
SantaSpeen 9c94b68d55 [+] Profile.from_response 2025-04-02 18:07:54 +03:00
SantaSpeen bb852d2b3a [~] Починил авторизацию 2025-04-02 17:55:28 +03:00
SantaSpeen d9a577ee14 [~] Ошибки поменялись? 2025-04-02 17:55:16 +03:00
SantaSpeen 52ebb42aa2 [~] Промежуточный вариант 2025-04-02 17:47:27 +03:00
SantaSpeen d726088002 [~] Вычисляю где и как кто какал 2025-04-02 17:47:08 +03:00
SantaSpeen 577e8dbf2a [+] По дефолту теперь AnixartAccountGuest 2025-04-02 17:46:53 +03:00
SantaSpeen c68735df97 [+] profile.objects.PROFILE
[+] profile.objects.PROFILE_NICK_HISTORY
2025-04-02 17:46:28 +03:00
SantaSpeen dd550855c0 [+] AnixartAccountGuest
[+] AnixartAccount.login
[+] auth.objects
2025-04-02 17:43:46 +03:00
SantaSpeen 733da877fa [+] secrets.txt 2025-04-02 16:58:52 +03:00
SantaSpeen 14fb575548 [~] Надо бы сделать 2025-04-02 16:23:36 +03:00
SantaSpeen 8762cca288 Обновил импорты 2025-04-02 16:04:10 +03:00
SantaSpeen d288837bb4 [~] 2025-04-02 16:03:39 +03:00
SantaSpeen 3bd27840e1 [+] AnixartApiErrors
[+] Описание для AnixartAPIError
2025-04-02 16:03:27 +03:00
SantaSpeen 87fa813700 [>] {api/, request_handler} > api.py 2025-04-02 16:02:46 +03:00
SantaSpeen 29205efe4f [+] AnixartAccountSaved 2025-04-02 16:02:01 +03:00
SantaSpeen 68e45336ad [+] Версия ?.?.? 2025-04-02 15:35:06 +03:00
SantaSpeen ae1f97a07c [+] 0.3.0? 2025-04-02 15:33:25 +03:00
22 changed files with 677 additions and 415 deletions
+1
View File
@@ -132,3 +132,4 @@ dmypy.json
.idea/ .idea/
poetry.lock poetry.lock
secrets.txt
+3 -2
View File
@@ -2,8 +2,9 @@
from .__meta__ import * from .__meta__ import *
from .endpoints import * from .api import AnixartAPI
from .api.api import AnixartUserAccount, AnixartAPI
from . import auth
from . import enums from . import enums
from . import exceptions from . import exceptions
+153
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}>"
-78
View File
@@ -1,78 +0,0 @@
import logging
import requests
from ..auth import AnixartAuth
from ..exceptions import AnixartInitError, AnixartAPIRequestError
from ..request_handler import AnixartRequestsHandler
_log_name = "file:%-29s -> %s" % ("<anixart.api:%-4i>", "%s")
class AnixartUserAccount:
def __init__(self, login, password, config_file="anixart_data.json", **kwargs):
self.kwargs = kwargs
log_level = logging.CRITICAL
log_format = '[%(name)-43s] %(levelname)-5s: %(message)s'
if kwargs.get("loglevel") is not None:
log_level = kwargs.get("loglevel")
if kwargs.get("logformat") is not None:
log_format = kwargs.get("logformat")
logging.basicConfig(level=log_level, format=log_format)
init_log = logging.getLogger("anixart.api.AnixUserAccount")
init_log.debug(_log_name, 23, "__init__ - INIT")
self.login = login
self.password = password
if not isinstance(login, str) or not isinstance(password, str):
raise AnixartInitError("Use normal auth data. In string.")
self.token = None
self.id = None
self.config_file = config_file
self.session = requests.Session()
init_log.debug(_log_name, 32, f"{str(self)}")
init_log.debug(_log_name, 33, "__init__() - OK")
def __str__(self):
return f'AnixartUserAccount(login="{self.login}", password="{self.password}", ' \
f'config_file="{self.config_file}", kwargs="{self.kwargs}")'
def get_login(self):
return self.login
def get_password(self):
return self.password
def get_token(self):
return self.token
def get_id(self):
return self.id
class AnixartAPI:
def __init__(self, user: AnixartUserAccount):
init_log = logging.getLogger("anixart.api.AnixartAPI")
init_log.debug(_log_name, 56, "__init__ - INIT")
if not isinstance(user, AnixartUserAccount):
init_log.critical('Use anixart.api.AnixartUserAccount for user.')
raise AnixartInitError('Use class "AnixartUserAccount" for user.')
self.auth = AnixartAuth(user)
if user.token is None or user.id is None:
init_log.debug(_log_name, 62, "Singing in..")
self.auth.sing_in()
self.user = user
init_log.debug(_log_name, 65, "__init__ - OK.")
self.http_handler = AnixartRequestsHandler(user.token, user.session)
def __str__(self):
return f'AnixAPI({self.user})'
def execute(self, http_method, endpoint, **kwargs):
http_method = http_method.upper()
if http_method == "GET":
return self.http_handler.get(endpoint, **kwargs)
elif http_method == "POST":
return self.http_handler.post(endpoint, **kwargs)
else:
raise AnixartAPIRequestError("Allow only GET and POST requests.")
-65
View File
@@ -1,65 +0,0 @@
import requests
from ..auth import AnixartAuth
from ..request_handler import AnixartRequestsHandler
class AnixartUserAccount:
def __init__(self, login, password, config_file="anixart_data.json", **kwargs):
"""
Info:
Anixart login class object.
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Usage:
>>> user = AnixartUserAccount("login", "password", config_file="anixart_data.json")
>>> print(user.__login)
Availible params:
~~~~~~~~~~~~~~~~~
* login -> Your anixart nick
* password -> Your anixart password
* need_reg -> If you need new account, set True
* mail -> Real email for registration.
* config_file -> Patch to anixart login cache
:param login: Your anixart nick
:param password: Anixart password
:param need_reg: If you need new account, set True
:param email: Real email for registration
:param config_file: Patch to anixart login cache
:type login: str
:type password: str
:type need_reg: bool
:type email: str
:type config_file: str
:return: :class:`AnixUserAccount <anixart.api.AnixUserAccount>` object
"""
self.kwargs: dict = kwargs
self.login: str = login
self.password: str = password
self.config_file: str = config_file
self.token: str = None
self.id: int = None
self.session: requests.Session = requests.Session()
def __str__(self) -> str: ...
def get_login(self) -> str: ...
def get_password(self) -> str: ...
def get_token(self) -> str: ...
def get_id(self) -> int: ...
class AnixartAPI:
def __init__(self, user: AnixartUserAccount):
"""
Info:
Anixart API class object.
~~~~~~~~~~~~~~~~~~~~~~~~~
Usage:
>>> user = AnixartUserAccount("login", "password", config_file="anixart_data.json")
>>> anix = AnixartAPI(user)
:param user: :class:`AnixUserAccount <anixart.api.AnixUserAccount>` object
:return: :class:`AnixAPIRequests <anixart.api.AnixAPIRequests>` object
"""
self.auth = AnixartAuth(user)
self.user = user
self.http_handler = AnixartRequestsHandler(user.token, user.session)
def __str__(self) -> str: ...
def execute(self, http_method: str, endpoint: str) -> requests.Request: ...
+1 -1
View File
@@ -1 +1 @@
from .account import AnixartAccount, AnixartAccountToken from .accounts import AnixartAccount, AnixartAccountSaved, AnixartAccountGuest
+31 -18
View File
@@ -3,19 +3,31 @@ from pathlib import Path
import requests import requests
from anixart import endpoints
from anixart.exceptions import AnixartInitError from anixart.exceptions import AnixartInitError
class AnixartAccount: class AnixartAccount:
guest = False
def __init__(self, username: str, password: str): def __init__(self, username: str, password: str):
self._username = username self._username = username
self._password = password self._password = password
if not isinstance(username, str) or not isinstance(password, str): if not isinstance(username, str) or not isinstance(password, str):
raise AnixartInitError("Auth data must be strings.") raise AnixartInitError("Auth data must be strings.")
self._id = None
self._token = None self._token = None
self._session = requests.Session() self._session = requests.Session()
self._api = None
def _set_api(self, api):
self._api = api
@property
def id(self):
return self._id
@property @property
def username(self): def username(self):
return self._username return self._username
@@ -28,14 +40,14 @@ class AnixartAccount:
def token(self): def token(self):
return self._token return self._token
def to_file(self, filename: str | Path): def to_file(self, filename: str | Path) -> "AnixartAccountSaved":
"""Save the account information to a file.""" """Save the account information to a file."""
acc = AnixartAccountSaved.from_account(filename, self) acc = AnixartAccountSaved.from_account(filename, self)
acc.save() acc.save()
return acc return acc
@classmethod @classmethod
def from_file(cls, filename: str | Path): def from_file(cls, filename: str | Path) -> "AnixartAccountSaved":
"""Load the account information from a file.""" """Load the account information from a file."""
acc = AnixartAccountSaved(filename) acc = AnixartAccountSaved(filename)
acc.load() acc.load()
@@ -43,7 +55,11 @@ class AnixartAccount:
def login(self): def login(self):
"""Login to Anixart and return the token.""" """Login to Anixart and return the token."""
# TODO: Implement login logic here 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): def __str__(self):
return f'AnixartAccount(login={self._username!r}, password={"*" * len(self._password)!r})' return f'AnixartAccount(login={self._username!r}, password={"*" * len(self._password)!r})'
@@ -60,8 +76,7 @@ class AnixartAccountSaved(AnixartAccount):
def save(self): def save(self):
"""Save the account information to a file.""" """Save the account information to a file."""
data = { data = {
"username": self._username, "id": self._id,
"password": self._password,
"token": self._token "token": self._token
} }
with open(self._file, 'w') as f: with open(self._file, 'w') as f:
@@ -73,11 +88,11 @@ class AnixartAccountSaved(AnixartAccount):
raise AnixartInitError(f"Account file {self._file} does not exist.") raise AnixartInitError(f"Account file {self._file} does not exist.")
with open(self._file, 'r') as f: with open(self._file, 'r') as f:
data = json.load(f) data = json.load(f)
self._id = data.get("id")
self._username = data.get("username") self._username = data.get("username")
self._password = data.get("password")
self._token = data.get("token") self._token = data.get("token")
if not self._username or not self._password: if not self._id or not self._token:
raise AnixartInitError("Login and password must be provided in the account file.") raise AnixartInitError("id and token must be provided in the account file.")
@classmethod @classmethod
def from_account(cls, account_file: str | Path, account: AnixartAccount): def from_account(cls, account_file: str | Path, account: AnixartAccount):
@@ -97,16 +112,14 @@ class AnixartAccountSaved(AnixartAccount):
def __str__(self): def __str__(self):
return f'AnixartAccountSaved(account_file={self._file!r}")' return f'AnixartAccountSaved(account_file={self._file!r}")'
class AnixartAccountToken(AnixartAccount): class AnixartAccountGuest(AnixartAccount):
guest = True
def __init__(self):
super().__init__("", "")
self._token = ""
def __init__(self, token): def login(self): ...
super().__init__("mradx", "") # Пасхалка)
self._token = token
def login(self):
"""Login to Anixart and return information about the tokens."""
# TODO: Implement login logic here
pass
def __str__(self): def __str__(self):
return f'AnixartAccountToken(token={self._token!r}")' return f'AnixartAccountGuest(token={self._token!r}")'
+23
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})
-7
View File
@@ -1,7 +0,0 @@
from enum import IntEnum
class AnixartAuthError(IntEnum):
""" Error codes for AnixartApi authentication."""
INCORRECT_LOGIN = 1
INCORRECT_PASSWORD = 2
+41
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
)
-67
View File
@@ -1,67 +0,0 @@
import json
import logging
import os.path
from .endpoints import SING_IN, CHANGE_PASSWORD, PROFILE
from .exceptions import AnixartAuthError
from .request_handler import AnixartRequestsHandler
def _parse_response(data):
ready = data.json()
ready.update({"status_code": data.status_code})
code = ready['code']
if code != 0:
if code == 2:
raise AnixartAuthError("Incorrect login.")
if code == 3:
raise AnixartAuthError("Incorrect password.")
print("\n\n" + data.text + "\n\n")
raise AnixartAuthError("Unknown auth error.")
return ready
class AnixartAuth(AnixartRequestsHandler):
def __init__(self, user):
super(AnixartAuth, self).__init__(None, user.session, "anixart.auth.AnixAuth")
self.user = user
self.filename = user.config_file
def _save_config(self, data):
with open(self.filename, "w") as f:
json.dump(data, f)
return data
def _open_config(self):
if os.path.isfile(self.filename):
with open(self.filename, "r") as read_file:
data = json.load(read_file)
return data
else:
return None
def sing_in(self):
config = self._open_config()
if config:
uid = config.get("id")
token = config.get("token")
if not self.get(PROFILE.format(uid), {"token": token}).json().get("is_my_profile") or \
self.user.__login != config.get("login"):
logging.getLogger("anixart.api.AnixAPI").debug("Invalid config file. Re login.")
else:
self.user.id = uid
self.user.__token = token
return config
payload = {"login": self.user.__login, "password": self.user.__password}
res = self.post(SING_IN, payload)
ready = _parse_response(res)
uid = ready["profile"]["id"]
token = ready["profileToken"]["token"]
self.user.id = uid
self.user.__token = token
self._save_config({"id": uid, "token": token, "login": self.user.__login})
return ready
def change_password(self, old, new):
return self.get(CHANGE_PASSWORD, {"current": old, "new": new, "token": self.user.token})
+9
View File
@@ -0,0 +1,9 @@
from .auth.factories import AuthLoginFactory
endpoints_map = {
# Auth
"auth.login": AuthLoginFactory
# Profile
}
-3
View File
@@ -1,11 +1,9 @@
from enum import IntEnum from enum import IntEnum
class AnixartComment(IntEnum): class AnixartComment(IntEnum):
DISLIKE = 1 DISLIKE = 1
LIKE = 2 LIKE = 2
class AnixartProfileVotedSort(IntEnum): class AnixartProfileVotedSort(IntEnum):
LAST_FIRST = 1 LAST_FIRST = 1
OLD_FIRST = 2 OLD_FIRST = 2
@@ -15,7 +13,6 @@ class AnixartProfileVotedSort(IntEnum):
STAR_2 = 6 STAR_2 = 6
STAR_1 = 7 STAR_1 = 7
class AnixartLists(IntEnum): class AnixartLists(IntEnum):
WATCHING = 1 WATCHING = 1
IN_PLANS = 2 IN_PLANS = 2
+5 -3
View File
@@ -1,13 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
class AnixartBasError(Exception): ... class AnixartBaseError(Exception): ...
# Init errors # Init errors
class AnixartInitError(AnixartBasError, TypeError): ... class AnixartInitError(AnixartBaseError, TypeError): ...
# API errors # API errors
class AnixartAPIError(AnixartBasError): ... class AnixartAPIError(AnixartBaseError):
message = "unknown error"
code = 0
class AnixartAuthError(AnixartAPIError): ... class AnixartAuthError(AnixartAPIError): ...
+1
View File
@@ -0,0 +1 @@
from .factories import Profile, ProfileFull, ProfileVote, ProfileRoles, ProfileHistory, ProfileFriendsPreview
+80
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}, {})
+187
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
# }
-91
View File
@@ -1,91 +0,0 @@
# -*- coding: utf-8 -*-
import logging
import requests
from .__meta__ import __version__, __build__
from .endpoints import API_URL
from .exceptions import AnixartAPIRequestError, AnixartAPIError
_log_name = "file:%-28s -> %s" % ("<Anixart.request_handler:%-3i>", "%s")
def _parse_res_code(res, payload, http_method, http_headers):
json = res.json()
error = json.get("error")
code = json.get("code")
if res.status_code >= 400:
raise AnixartAPIRequestError(f"\n\nAnixartPyAPIWrapper: ERROR\n"
f"Send this info to author: https://t.me/SantaSpeen\n"
f"URL: {http_method} {res.url}\n"
f"Status code: {res.status_code}\n"
f"Res headers: {res.headers}\n"
f"Req headers: {http_headers}\n"
f"Server res: {json}\n"
f"Client req: {payload}\n")
if error:
raise AnixartAPIRequestError(f"Internal server error: {error}; Payload: {payload}")
if code:
if code == 0:
return
else:
raise AnixartAPIError(f"AnixartAPI send error code: {code}; Json: {json}")
class AnixartRequestsHandler:
def __init__(self, token: str = None, session: requests.Session = None,
_log_class="Anixart.request_handler.AnixartRequestsHandler"):
self.__log = logging.getLogger(_log_class)
self.__log.debug(_log_name, 44, f"__init__ - INIT from {self}")
if session:
self.__session = session
else:
self.__log.debug(_log_name, 48, "Create new session.")
self.__session = requests.Session()
self.__session.headers = {
'User-Agent': f'AnixartPyAPIWrapper/{__version__}-{__build__}'
f' (Linux; Android 12; SantaSpeen AnixartPyAPIWrapper Build/{__build__})'}
self.__token = token
def post(self, method: str, payload: dict = None, is_json: bool = False, **kwargs):
if payload is None:
payload = {}
url = API_URL + method
if payload.get("token") is None:
if self.__token is not None:
payload.update({"token": self.__token})
url += "?token=" + self.__token
else:
token = kwargs.get("token")
if token is not None:
payload.update({"token": token})
url += "?token=" + token
kwargs = {"url": url}
if is_json:
self.__session.headers.update({"Content-Type": "application/json; charset=UTF-8"})
self.__session.headers.update({"Content-Length": str(len(str(payload)))})
kwargs.update({"json": payload})
else:
kwargs.update({"data": payload})
self.__log.debug(_log_name, 79, f"{'json' if is_json else ''} POST {method}; {payload}")
res = self.__session.post(**kwargs)
_parse_res_code(res, payload, "POST", self.__session.headers)
self.__session.headers["Content-Type"] = ""
self.__session.headers["Content-Length"] = ""
return res
def get(self, method: str, payload: dict = None, **kwargs):
if payload is None:
payload = {}
if payload.get("token") is None:
if self.__token is not None:
payload.update({"token": self.__token})
else:
token = kwargs.get("token")
if token is not None:
payload.update({"token": token})
self.__log.debug(_log_name, 101, f"GET {method}; {payload}")
res = self.__session.get(API_URL + method, params=payload)
_parse_res_code(res, payload, "GET", self.__session.headers)
return res
+99 -68
View File
@@ -1,94 +1,125 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from dataclasses import dataclass, field
from typing import Literal, Any
from anixart.exceptions import AnixartAPIError
API_URL = "https://api.anixart.tv" @dataclass
class BaseParseFactory:
code: int
# ----------- # AUTH # ----------- # _errors: dict[int, str]
_exception: type[AnixartAPIError]
# POST def raise_if_error(self):
SING_UP = None # Удалено дабы исключить автореги if self.code != 0:
SING_IN = "/auth/signIn" error = self._errors.get(self.code)
if error:
raise error
else:
raise ValueError(f"Unknown error code: {self.code}")
# Not Checked @classmethod
# POST def from_dict(cls, data: dict) -> "BaseParseFactory":
_AUTH_SING_IN_WITH_GOOGLE = "/auth/google" # {googleIdToken} or {login, email, googleIdToken} return cls(
_AUTH_SING_IN_WITH_VK = "/auth/vk" # {vkAccessToken} code=data.get("code", 0),
_errors={},
_exception=AnixartAPIError,
)
# ----------- # PROFILE # ----------- #
# TODO PROFILE: SETTINGS, SETTINGS_RELEASE, SETTINGS_RELEASE_FIRST,
# SETTINGS_COMMENTS, SETTINGS_COLLECTION, EDIT_AVATAR, SETTINGS_RELEASE_LIST,
# SETTINGS_RELEASE_TYPE
# GET @dataclass
PROFILE = "/profile/{}" # + profile id class Endpoint:
PROFILE_NICK_HISTORY = "/profile/login/history/all/{}/{}" # profile id / page 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)
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 _json = False
FRIENDS_RQ_IN = "/profile/friend/requests/in/{}" # page _API_ENDPOINT = "https://api.anixart.tv/"
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 def __post_init__(self):
# Да, ребята из аниксарта не знают английский; ↓ if self.method not in ["GET", "POST"]:
# noinspection SpellCheckingInspection raise ValueError("Method must be either GET or POST.")
VOTE_UNVENTED = "/profile/vote/release/unvoted/{}" # page 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.")
LISTS = "/profile/list/all/{}/{}/{}" # profile id / list id / page @property
def is_get(self) -> bool:
return self.method == "GET"
SETTINGS_NOTIFICATION = "/profile/preference/notification/my" @property
SETTINGS_NOTIFICATION_RELEASE = "/profile/preference/notification/episode/edit" def is_post(self) -> bool:
SETTINGS_NOTIFICATION_RELEASE_FIRST = "/profile/preference/notification/episode/first/edit" return self.method == "POST"
SETTINGS_NOTIFICATION_COMMENTS = "/profile/preference/notification/comment/edit"
SETTINGS_NOTIFICATION_COLLECTION = "/profile/preference/notification/my/collection/comment/edit"
CHANGE_PASSWORD = "/profile/preference/password/change" @property
def is_json(self) -> bool:
return self._json
# POST def _post(self, method: str, **kwargs):
EDIT_STATUS = "/profile/preference/status/edit" headers = {}
EDIT_SOCIAL = "/profile/preference/social/edit" url = self._API_ENDPOINT + method
EDIT_AVATAR = "/profile/preference/avatar/edit" if token:=kwargs.get("token"):
kwargs["token"] = token
url += f"?token={token}"
# {"profileStatusNotificationPreferences":[0 - favorite, + all in AnixList]} req_settings = {"url": url}
SETTINGS_NOTIFICATION_RELEASE_LIST = "/profile/preference/notification/status/edit" if self._json:
# {"profileTypeNotificationPreferences":[type ids]} headers["Content-Type"] = "application/json; charset=UTF-8"
SETTINGS_NOTIFICATION_RELEASE_TYPE = "/profile/preference/notification/type/edit" req_settings.update({"json": kwargs})
else:
req_settings.update({"data": kwargs})
return req_settings, headers
# Not Checked def _get(self, method: str, **kwargs):
# GET headers = {}
PROFILE_SOCIAL = "/profile/social/{}" # profile id url = self._API_ENDPOINT + method
if token:=kwargs.get("token"):
kwargs["token"] = token
req_settings = {"url": url, "params": kwargs}
return req_settings, headers
FRIENDS_RECOMMENDATION = "/profile/friend/recommendations" def _check_arguments(self, **kwargs):
FRIENDS_RQ_HIDE = "profile/friend/request/hide/{}" # profile id """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}")
SETTINGS_PROFILE = "/profile/preference/my" def build_request(self, **kwargs) -> None | tuple[dict[str, dict[str, Any] | str], dict[Any, Any]] | tuple[
SETTINGS_PROFILE_CHANGE_EMAIL = "/profile/preference/email/change" # {current_password, current, new} dict[str, dict[str, Any] | str], dict[str, str]]:
SETTINGS_PROFILE_CHANGE_EMAIL_CONFIRM = "/profile/preference/email/change/confirm" # {current} """
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)
# /profile/preference/social
SETTINGS_PROFILE_STATUS_DELETE = "/profile/preference/status/delete"
# POST def endpoint(path: str, method: Literal["GET", "POST"], parse_factory: type[BaseParseFactory], required_args: dict[str, type], required_kwargs: dict[str, type]) -> Endpoint:
PROFILE_PROCESS = "/profile/process/{}" # profile id return Endpoint(path, method, parse_factory, required_args, required_kwargs)
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"
# ----------- # COLLECTION # ----------- # # ----------- # COLLECTION # ----------- #
+26 -4
View File
@@ -2,8 +2,31 @@
## Anixart API Wrapper ## 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 ### 28.09.2022
#### Version: 0.2.1, Build: 3 #### Version: 0.2.1
##### Changes: ##### Changes:
@@ -29,8 +52,7 @@ _Из прошлых версий_
- Выявить и удалить не используемые - Выявить и удалить не используемые
### 27.09.2022 ### 27.09.2022
[//]: # ( Да, я не билдил, это не ошибка ) #### Version: 0.1.0
#### Version: 0.1.0, Build: 1
##### Changes: ##### Changes:
@@ -46,7 +68,7 @@ _Из прошлых версий_
### 27.09.2022 ### 27.09.2022
#### Version: 0.0.1, Build: 1 #### Version: 0.0.1
##### Changes: ##### Changes:
+14 -5
View File
@@ -1,9 +1,18 @@
from anixart import AnixartAPI, AnixartUserAccount, PROFILE from anixart import AnixartAPI, AnixartAccount
from anixart import endpoints
from anixart.exceptions import AnixartAPIRequestError
from anixart.profile import Profile
user = AnixartUserAccount("SantaSpeen", "I_H@ve_Very_Secret_P@ssw0rd!") anix = AnixartAPI() # По умолчанию используется гость
anix = AnixartAPI(user)
# acc = AnixartAccount("SantaSpeen", "I_H@ve_Very_Secret_P@ssw0rd!")
# # id у аккаунта появляется только после
# anix.use_account(acc)
if __name__ == '__main__': if __name__ == '__main__':
me = anix.execute("GET", PROFILE.format(user.id)) try:
print(me.json()) raw = anix.get(endpoints.PROFILE, 1)
print(Profile.from_response(raw))
except AnixartAPIRequestError as e:
print(e.message)
print(e.code)
+2 -2
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "anixart" name = "anixart"
version = "0.2.2" version = "0.3.0.1"
description = "Wrapper for using the Anixart API." description = "Wrapper for using the Anixart API."
authors = [ authors = [
{name = "SantaSpeen",email = "santaspeen@gmail.com"} {name = "SantaSpeen",email = "santaspeen@gmail.com"}
@@ -16,7 +16,7 @@ repository = "https://github.com/SantaSpeen/anixart"
[tool.poetry] [tool.poetry]
classifiers = [ classifiers = [
"Development Status :: 3 - Alpha", "Development Status :: 2 - Pre-Alpha",
"Topic :: Software Development :: Libraries :: Python Modules" "Topic :: Software Development :: Libraries :: Python Modules"
] ]