mirror of
https://github.com/SantaSpeen/anixart.git
synced 2026-02-16 02:20:47 +00:00
Compare commits
36 Commits
v0.2.1-alp
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 723cf98475 | |||
| 0682bdb42b | |||
| 63bf159679 | |||
| 8a944319d6 | |||
| aee5c542b4 | |||
| c740242c3f | |||
| 2f18c5ce85 | |||
| 9c94b68d55 | |||
| bb852d2b3a | |||
| d9a577ee14 | |||
| 52ebb42aa2 | |||
| d726088002 | |||
| 577e8dbf2a | |||
| c68735df97 | |||
| dd550855c0 | |||
| 733da877fa | |||
| 14fb575548 | |||
| 8762cca288 | |||
| d288837bb4 | |||
| 3bd27840e1 | |||
| 87fa813700 | |||
| 29205efe4f | |||
| 68e45336ad | |||
| ae1f97a07c | |||
| 85c7605b2c | |||
| 82874814e0 | |||
| 44e0485f99 | |||
| daec2199b6 | |||
| fbb738fd1c | |||
| c4d758f65a | |||
| 04be5ed2a9 | |||
| 87ff4463c4 | |||
| 403e7c8b79 | |||
| 2f0e4e3942 | |||
| 1c8d56dca6 | |||
| 326034fda9 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -130,3 +130,6 @@ dmypy.json
|
||||
|
||||
# PyCharm
|
||||
.idea/
|
||||
|
||||
poetry.lock
|
||||
secrets.txt
|
||||
2
LICENSE
2
LICENSE
@@ -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:
|
||||
|
||||
|
||||
29
README.md
29
README.md
@@ -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)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .__version__ import __license__, __description__
|
||||
from .__version__ import __version__, __url__, __build__, __title__, __author__, __author_email__, __copyright__
|
||||
from .__meta__ import *
|
||||
|
||||
from .endpoints import *
|
||||
from .api import AnixartAPI
|
||||
|
||||
from .api.api import AnixartUserAccount, AnixartAPI
|
||||
from . import auth
|
||||
|
||||
from . import enums
|
||||
from . import exceptions
|
||||
|
||||
153
anixart/api.py
Normal file
153
anixart/api.py
Normal 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,78 +0,0 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from ..auth import AnixartAuth
|
||||
from ..errors 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.")
|
||||
@@ -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,67 +0,0 @@
|
||||
import json
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
from .endpoints import SING_IN, CHANGE_PASSWORD, PROFILE
|
||||
from .errors 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})
|
||||
1
anixart/auth/__init__.py
Normal file
1
anixart/auth/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .accounts import AnixartAccount, AnixartAccountSaved, AnixartAccountGuest
|
||||
125
anixart/auth/account.py
Normal file
125
anixart/auth/account.py
Normal 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
23
anixart/auth/endpoints.py
Normal 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
41
anixart/auth/factories.py
Normal 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
9
anixart/endpoints_map.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from .auth.factories import AuthLoginFactory
|
||||
|
||||
endpoints_map = {
|
||||
# Auth
|
||||
"auth.login": AuthLoginFactory
|
||||
|
||||
# Profile
|
||||
|
||||
}
|
||||
21
anixart/enums.py
Normal file
21
anixart/enums.py
Normal 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
|
||||
@@ -1,16 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
class AnixartInitError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AnixartAuthError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AnixartAPIRequestError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AnixartAPIError(Exception):
|
||||
pass
|
||||
17
anixart/exceptions.py
Normal file
17
anixart/exceptions.py
Normal 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): ...
|
||||
|
||||
1
anixart/profile/__init__.py
Normal file
1
anixart/profile/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .factories import Profile, ProfileFull, ProfileVote, ProfileRoles, ProfileHistory, ProfileFriendsPreview
|
||||
80
anixart/profile/endpoints.py
Normal file
80
anixart/profile/endpoints.py
Normal 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
anixart/profile/factories.py
Normal file
187
anixart/profile/factories.py
Normal 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
|
||||
# }
|
||||
@@ -1,91 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from .__version__ import __version__, __build__
|
||||
from .endpoints import API_URL
|
||||
from .errors 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
|
||||
@@ -1,116 +1,125 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Literal, Any
|
||||
|
||||
class AnixartComment:
|
||||
DISLIKE = 1
|
||||
LIKE = 2
|
||||
from anixart.exceptions import AnixartAPIError
|
||||
|
||||
|
||||
class AnixartProfileVotedSort:
|
||||
LAST_FIRST = 1
|
||||
OLD_FIRST = 2
|
||||
STAR_5 = 3
|
||||
STAR_4 = 4
|
||||
STAR_3 = 5
|
||||
STAR_2 = 6
|
||||
STAR_1 = 7
|
||||
@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,
|
||||
)
|
||||
|
||||
|
||||
class AnixartLists:
|
||||
WATCHING = 1
|
||||
IN_PLANS = 2
|
||||
WATCHED = 3
|
||||
POSTPONED = 4
|
||||
DROPPED = 5
|
||||
@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)
|
||||
|
||||
|
||||
API_URL = "https://api.anixart.tv"
|
||||
_json = False
|
||||
_API_ENDPOINT = "https://api.anixart.tv/"
|
||||
|
||||
# ----------- # AUTH # ----------- #
|
||||
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.")
|
||||
|
||||
# POST
|
||||
SING_UP = None # Удалено дабы исключить автореги
|
||||
SING_IN = "/auth/signIn"
|
||||
@property
|
||||
def is_get(self) -> bool:
|
||||
return self.method == "GET"
|
||||
|
||||
# Not Checked
|
||||
# POST
|
||||
_AUTH_SING_IN_WITH_GOOGLE = "/auth/google" # {googleIdToken} or {login, email, googleIdToken}
|
||||
_AUTH_SING_IN_WITH_VK = "/auth/vk" # {vkAccessToken}
|
||||
@property
|
||||
def is_post(self) -> bool:
|
||||
return self.method == "POST"
|
||||
|
||||
# ----------- # PROFILE # ----------- #
|
||||
# TODO PROFILE: SETTINGS, SETTINGS_RELEASE, SETTINGS_RELEASE_FIRST,
|
||||
# SETTINGS_COMMENTS, SETTINGS_COLLECTION, EDIT_AVATAR, SETTINGS_RELEASE_LIST,
|
||||
# SETTINGS_RELEASE_TYPE
|
||||
@property
|
||||
def is_json(self) -> bool:
|
||||
return self._json
|
||||
|
||||
# GET
|
||||
PROFILE = "/profile/{}" # + profile id
|
||||
PROFILE_NICK_HISTORY = "/profile/login/history/all/{}/{}" # profile id / page
|
||||
def _post(self, method: str, **kwargs):
|
||||
headers = {}
|
||||
url = self._API_ENDPOINT + method
|
||||
if token:=kwargs.get("token"):
|
||||
kwargs["token"] = token
|
||||
url += f"?token={token}"
|
||||
|
||||
PROFILE_BLACKLIST = "/profile/blocklist/all/{}" # page
|
||||
PROFILE_BLACKLIST_ADD = "/profile/blocklist/add/{}" # profile id
|
||||
PROFILE_BLACKLIST_REMOVE = "/profile/blocklist/remove/{}" # profile id
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
VOTE_VOTED = "/profile/vote/release/voted/{}/{}" # profile id / page
|
||||
# Да, ребята из аниксарта не знают английский; ↓
|
||||
# noinspection SpellCheckingInspection
|
||||
VOTE_UNVENTED = "/profile/vote/release/unvoted/{}" # page
|
||||
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}")
|
||||
|
||||
LISTS = "/profile/list/all/{}/{}/{}" # profile id / list id / page
|
||||
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)
|
||||
|
||||
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"
|
||||
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)
|
||||
|
||||
# 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"
|
||||
|
||||
# ----------- # COLLECTION # ----------- #
|
||||
|
||||
@@ -2,8 +2,31 @@
|
||||
|
||||
## 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, Build: 3
|
||||
#### Version: 0.2.1
|
||||
|
||||
##### Changes:
|
||||
|
||||
@@ -29,8 +52,7 @@ _Из прошлых версий_
|
||||
- Выявить и удалить не используемые
|
||||
|
||||
### 27.09.2022
|
||||
[//]: # ( Да, я не билдил, это не ошибка )
|
||||
#### Version: 0.1.0, Build: 1
|
||||
#### Version: 0.1.0
|
||||
|
||||
##### Changes:
|
||||
|
||||
@@ -46,7 +68,7 @@ _Из прошлых версий_
|
||||
|
||||
|
||||
### 27.09.2022
|
||||
#### Version: 0.0.1, Build: 1
|
||||
#### Version: 0.0.1
|
||||
|
||||
##### Changes:
|
||||
|
||||
|
||||
@@ -17,4 +17,4 @@
|
||||
* 2
|
||||
* ...
|
||||
3. [CHANGELOG](./CHANGELOG.md)
|
||||
4. [LICENSE](./License.md)
|
||||
4. [LICENSE](./LICENSE.md)
|
||||
@@ -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(user)
|
||||
anix = AnixartAPI() # По умолчанию используется гость
|
||||
|
||||
# acc = AnixartAccount("SantaSpeen", "I_H@ve_Very_Secret_P@ssw0rd!")
|
||||
# # id у аккаунта появляется только после
|
||||
# anix.use_account(acc)
|
||||
|
||||
if __name__ == '__main__':
|
||||
me = anix.execute("GET", PROFILE.format(user.id))
|
||||
print(me.json())
|
||||
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
3
examples/readme.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## Директория с примерами
|
||||
|
||||
* Пример авторизации и вывода информации о себе: [auth.py](./auth.py)
|
||||
3
poetry.toml
Normal file
3
poetry.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[virtualenvs]
|
||||
create = true
|
||||
in-project = true
|
||||
25
pyproject.toml
Normal file
25
pyproject.toml
Normal 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"
|
||||
@@ -1 +0,0 @@
|
||||
requests~=2.28.1
|
||||
58
setup.py
58
setup.py
@@ -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 :: 3 - Alpha",
|
||||
"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",
|
||||
)
|
||||
Reference in New Issue
Block a user