diff --git a/.gitignore b/.gitignore index 2753f877..efa93169 100644 --- a/.gitignore +++ b/.gitignore @@ -184,3 +184,4 @@ Temporary Items # Other rules .idea .shikithon_* +*.log.zip diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6e4fbda5..395b42a5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Пам-парам. Приветствую смотрящих -## Последнее изменение: 01.05.2022 +## Последнее изменение: 07.11.2022 Добро пожаловать в пометку контрибьютора. Здесь изложены некоторые правила, которые помогут правильно оформить код в соответствиями с правилами проекта @@ -52,43 +52,31 @@ _Кто-то дошел до этого момента? Удивительно.. Поскольку данный проект был создан и разрабатывается на macOS, сначала опишу что нужно сделать тем, кто хочет разрабатывать тоже на ней: -1. В первую очередь, необходимо установить нужные версии Python для проверки работоспособности. +1. В первую очередь, необходимо установить последнюю актуальную версию Python. Сделать это можно с помощью Homebrew и пару команд: - Для начала установите Homebrew, если он у вас отсутствует. Информация об установке находится [здесь](https://brew.sh/index_ru) - - Далее, используя две команды, установите две версии Python: ```brew install python@3.8``` и ```brew install python@3.10``` _(На данный момент, это самая последняя версия Python)_ + - Далее введите следующую команду: ```brew install python``` 2. Установите Poetry: ```brew install poetry``` 3. Находясь в папке проекта, введите в Терминале: ```poetry shell``` и ```poetry install```. Это запустит виртуальное окружение Poetry и установит необходимые зависимости. - > По умолчанию, Poetry выберет последнюю версию Python, чтобы использовать Python 3.8 в окружении, введите в Терминале: ```poetry env use 3.8``` 4. Если вы используете какие-нибудь IDE, не забудьте настроить виртуальное окружение и там Пользователям Windows нужно пройти пару этапов: -1. Установите [Python 3.8.10](https://www.python.org/downloads/release/python-3810/) и [последний Python](https://www.python.org/downloads/) на текущий момент - > Почему 3.8.10? А, просто для 3.8.13 нет нормальных установщиков :( - > - > _(Если конечно, вы хотите собрать Python из сурсов, то можете собрать и 3.8.13)_ +1. Установите [последнюю актуальную версию Python](https://www.python.org/downloads/) 2. Установите Poetry: - Если вы используете [Bash on Windows](https://python-poetry.org/docs/#osx--linux--bashonwindows-install-instructions) - Если вы используете [Powershell](https://python-poetry.org/docs/#windows-powershell-install-instructions) 3. Выполните пункты 3 и 4 из гайда выше для юзеров macOS - > Если вы не хотите устанавливать Python 3.8 в PATH, то можете запускать виртуальное окружение, передавая путь до ```python.exe``` - > - > Например: ```poetry env use "C:\Program Files\Python38\python.exe"``` Пользователям Linux нужно пройти пару этапов: -1. Установите Python 3.8.13 и Python 3.10 +1. Установите последнюю актуальную версию Python _(Лучше загуглить, чем писать тут как это делать под каждый дистрибутив)_ 2. Установите Poetry используя [официальную команду](https://python-poetry.org/docs/#osx--linux--bashonwindows-install-instructions) > Если же у вас не стоит нужного алиаса под команду ```python```, то не забудьте поменять вызов интерпретатора в конце команды 3. Выполните пункты 3 и 4 из гайда выше для юзеров macOS - > Если возникнут какие-либо проблемы с Python или Poetry, то к сожалению, вам придется решать их самостоятельно, так как разработчик не имеет опыта разработки на Linux :c - -> **Небольшое помечание:** -> -> Ввиду того, что тесты отсутствуют, минимальная проверка в виде работоспособности на Python 3.8 и самом последнем приветствуется. -> -> В противном случае, придется исправлять функционал для его работы на старой версии Python + > Если возникнут какие-либо проблемы с Python или Poetry, то к сожалению, вам придется решать их самостоятельно, + > так как разработчик не имеет опыта разработки на Linux :c #### Подготовка к отправке пулл реквеста @@ -99,15 +87,16 @@ _(Лучше загуглить, чем писать тут как это дел Чтож, вам осталось пройти пару этапов перед финальным ~~боссом~~ пулл реквестом: 1. Для начала, убедитесь, что ваши новые методы или классы имеют документацию _(можно даже базовую)_. - - Данный проект использует документацию типа reStructuredText, пример которой вы можете найти в интернете или в имеющихся файлах. + Данный проект использует документацию типа reStructuredText, пример которой вы можете найти в интернете + или в имеющихся файлах. 2. Запустите `pre-commit install` для установки необходимых хуков для проверки файлов при коммите. Если будут найдены какие-либо ошибки, вы будете уведомлены во время попытки коммита. - > Если же вас преследуют ошибки, которые никак не убираеютя, оставьте их и попробуйте сделать коммит с флагом `--no-verify`. + > Если же вас преследуют ошибки, которые никак не убираеютя, оставьте их и попробуйте + > сделать коммит с флагом `--no-verify`. > > В дальнейшем, если ваши изменения будут значимыми, после слияния, разработчик попробует решить ошибки самостоятельно - > _**(Но это не значит что нужно оставлять все ошибки!!1!!!111!! :C)**_ + > _**(Но это не значит что нужно оставлять все ошибки!!1!!!111!! :C)**_ Также можно запустить команду `pre-commit run --all-files` для запуска хуков без иницирования коммита. 3. Убедитесь, что проект собирается, введя в Терминал команду: ```poetry build``` diff --git a/README.md b/README.md index aef3b385..6bd2e668 100644 --- a/README.md +++ b/README.md @@ -9,27 +9,27 @@ > **Состояние библиотеки:** завершена основная разработка > -> На данный момент, библиотека поддерживает лишь синхронное взаимодействие с API, -> асинхронное взаимодействие и прочие улучшения будут добавлены в будущем +> На данный момент, библиотека находится в статусе поддержки (новые функции будут добавляться только по необходимости) +> +> Начиная с версии 2.0.0, библиотека поддерживает асинхронные запросы, отдельные пути к ресурсам API и многое другое -## Преимущество библиотеки +## Описание Данный враппер предоставляет базовую абстракцию, которая позволяет удобнее работать с методами API и их ответами. -Для каждого метода API существует свой метод класса, который благодаря библиотеке Pydantic, -возвращает удобную модель данных для работы. +Для каждого эндпоинта API существует свой объект с методами, которые благодаря библиотеке Pydantic, +возвращают удобную модель данных. Все данные, возвращаемые API Shikimori, валидируются и парсятся в модели, со всеми необходимыми полями, -а также дополнительными, которые могут вернуть некоторые методы API _(Например /users/whoami и /users/:id/info)_. - +а также дополнительными, которые могут вернуть некоторые методы API _(Например /users/whoami и /users/:id/info возвращают разные поля)_. Это позволяет не задумываться об обработке очередного ответа от сервера и сосредоточиться над реализацией своей идеи. -Также, данная библиотека поддерживает ранние версии Python, начиная с 3.8.10. +Также благодаря многочисленным проверкам при взамодействии с запросами, библиотека старается добиться максимально +безопасной работы с API: все ошибки API, переданных параметров, данных и т.д. обратываются и логируются и +возвращаются значения по умолчанию + +> Данная библиотека начинают свою поддержку с Python 3.8.10. -> Поддержка Python 3.6.x не имеет смысла, так как она не является актуальной на момент разработки, а Python 3.7.x -> не поддерживается на Apple Silicon _(Основная платформа, на которой разрабатывается данная библиотека)_. -> -> Поэтому в качестве минимальной версии был выбран Python 3.8.10 ## Установка ```pip install shikithon``` @@ -39,11 +39,13 @@ С использованием полного конфига: ```py +import asyncio + from typing import Dict from json import loads -from shikithon import API +from shikithon import ShikimoriAPI # Можно установить данные конфигурации в коде config = { @@ -59,19 +61,30 @@ config = { with open("config.json", "r", encoding="utf-8") as config_file: config_2: Dict[str, str] = loads(config_file.read()) -# Инициализация объекта API -shikimori = API(config) +async def main(): + # Инициализация объекта API + shikimori = ShikimoriAPI(config) + + # Запуск сессии + await shikimori.open() + + # Получение данных текущего пользователя через /users/whoami + user = await shikimori.users.current() + print(f"Current user is {user.nickname}") + + # Получение достижений пользователя через /achievements + # и вывод первого достижения + user_achievements = await shikimori.achievements.get(user.id) + if user_achievements: + print(user_achievements[0]) -# Получение данных текущего пользователя через /users/whoami -user = shikimori.current_user() -print(f"Current user is {user.nickname}") + # Закрытие сессии + await shikimori.close() -# Получение достижений пользователя через /achievements -# и вывод первого достижения -user_achievements = shikimori.achievements(user.id) -print(user_achievements[0]) +asyncio.run(main()) # >> Current user is SecondThundeR + # >> id=719972946 # >> neko_id='animelist' # >> level=1 @@ -87,7 +100,9 @@ print(user_achievements[0]) С использованием имени приложения: ```py -from shikithon import API +import asyncio + +from shikithon import ShikimoriAPI # Можно установить имя приложения в коде app_name = "..." @@ -96,19 +111,22 @@ app_name = "..." with open("config.txt", "r", encoding="utf-8") as config_file: app_name_2 = config_file.readline().strip() -# Инициализация объекта API -shikimori = API(app_name) +async def main(): + # Инициализация объекта API + async with ShikimoriAPI(app_name) as shikimori: + # Попытка получения данных текущего пользователя через /users/whoami + # При попытке доступа к защищенному методу, возвращает всегда None + user = await shikimori.users.current() + print(user) -# Попытка получения данных текущего пользователя через /users/whoami -# При попытке доступа к защищенному методу, возвращает всегда None -user = shikimori.current_user() -print(user) + # Получение достижений пользователя через /achievements + # и вывод первого достижения + # Можно получать достижения любого пользователя через ID + user_achievements = await shikimori.achievements.get(1) + if user_achievements: + print(user_achievements[0]) -# Получение достижений пользователя через /achievements -# и вывод первого достижения -# Можно получать достижения любого пользователя через ID -user_achievements = shikimori.achievements(1) -print(user_achievements[0]) +asyncio.run(main()) # >> None @@ -124,14 +142,61 @@ print(user_achievements[0]) # Для удобства она показана здесь раздельно ``` +Выполнение нескольких запросов одновременно с помощью метода multiple_requests: +```py +# В этом примере используется распаковка, но можно также получать весь массив с данными ответов +# в одной переменнной (chainsaw, lycoris_anime, ... -> data = await ...) +from shikithon import ShikimoriAPI + +config = ... + +shikimori = ShikimoriAPI(config) +await shikimori.open() + +chainsaw, lycoris_anime, lycoris_ranobe = await shikimori.multiple_requests([ + shikimori.animes.get_all(search="Бензопила"), + shikimori.animes.get_all(search="Ликорис"), + shikimori.ranobes.get_all(search="Ликорис") +]) +print(chainsaw) +print(lycoris_anime) +print(lycoris_ranobe) + +await shikimori.close() + +# [Anime(id=44511, name='Chainsaw Man', russian='Человек-бензопила', ...] +# [Anime(id=50709, name='Lycoris Recoil', russian='Ликорис Рикоил', ...] +# [Ranobe(id=151431, name='Lycoris Recoil: Ordinary Days', russian='Ликорис Рикоил: Повседневность', ...] + +# Также возможно использовать этот метод в "ограниченном режиме": +app_name = ... + +async with ShikimoriAPI(app_name) as shikimori: + lycoris_ranobe = await shikimori.multiple_requests([ + shikimori.ranobes.get_all(search="Ликорис") + ]) + print(lycoris_ranobe) + +# [Ranobe(id=151431, name='Lycoris Recoil: Ordinary Days', russian='Ликорис Рикоил: Повседневность', ...] +``` + > **Пара уточнений по использованию:** > -> - Не обязательно импортировать модели, если вы не используете функцию аннотации типов +> - Возможно вам придется импортировать модели для ручной аннотации возвращаемых моделей в PyCharm +> _(в нем немного некорретно работает наследование типа от функции)_ +> +> > - Поле `scopes` является строкой и разделяется "+", если значений несколько. > > Пример: `user_rates+messages+comments+topics+...` +> +> > - При отсутствии каких-либо полей в данных конфигурации, библиотека выдает исключение -> - Посмотреть список поддерживаемых методов API вместе с названиями для них в библиотеке, можно [здесь](https://github.com/SecondThundeR/shikithon/projects/1#column-18695394) +> +> +> - Если вы не хотите использовать логгирование библиотеки, передайте флаг logging=False в объект API. +> +> Пример: `shikithon = await ShikimoriAPI(config, logging=False)` ### Получение данных для конфигурации @@ -143,8 +208,8 @@ _(После этого, сохраните `app_name`, `client_id`, `client_sec **Теперь ваш файл конфигурации готов!** -> На первой инициализации, библиотека создаст кэш конфигурации в скрытом файле для дальнейших запросов. -> Если токены станут недоступны, библиотека автоматически обновит токены и кэшированный файл конфигурации +> На первой инициализации, библиотека создаст и сохранит собственный файл конфигурации для дальнейших запросов. +> Если токены станут недоступны, библиотека автоматически обновит токены и сохранненый файл конфигурации Также возможно использование библиотеки в "ограниченном режиме", используя только имя приложения для доступа к публичным методам API. @@ -165,11 +230,11 @@ _(После этого, сохраните `app_name`, `client_id`, `client_sec Данный проект использует пять библиотек: -- [requests](https://github.com/psf/requests) для запросов к API -[(Лицензия)](https://github.com/psf/requests/blob/main/LICENSE) +- [aiohttp](https://github.com/aio-libs/aiohttp) для асинхронных запросов к API +[(Лицензия)](https://github.com/aio-libs/aiohttp/blob/master/LICENSE.txt) - [pydantic](https://github.com/samuelcolvin/pydantic/) для валидации данных JSON и преобразования в модели [(Лицензия)](https://github.com/samuelcolvin/pydantic/blob/master/LICENSE) -- [ratelimit](https://github.com/tomasbasham/ratelimit) для огранчений количества запросов в минуту +- [ratelimit](https://github.com/tomasbasham/ratelimit) для огранчений количества запросов к API [(Лицензия)](https://github.com/tomasbasham/ratelimit/blob/master/LICENSE.txt) - [loguru](https://github.com/Delgan/loguru) для удобного логгирования [(Лицензия)](https://github.com/Delgan/loguru/blob/master/LICENSE) @@ -194,3 +259,8 @@ _(После этого, сохраните `app_name`, `client_id`, `client_sec Проект использует логотип сайта [Shikimori](https://shikimori.org) для логотипа в этом README.md. Все права принадлежат правообладателям и используются по принципу _fair use_. + +## Благодарности + +- [shiki4py](https://github.com/ren3104/Shiki4py) - взяты некоторые идеи по рефакторингу и добавлению поддержки асинхронных запросов +[(Лицензия)](https://github.com/ren3104/Shiki4py/blob/main/LICENSE) diff --git a/poetry.lock b/poetry.lock index f8feade6..7a8906c2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,37 @@ +[[package]] +name = "aiohttp" +version = "3.8.3" +description = "Async http client/server framework (asyncio)" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +aiosignal = ">=1.1.2" +async-timeout = ">=4.0.0a3,<5.0" +attrs = ">=17.3.0" +charset-normalizer = ">=2.0,<3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "cchardet"] + +[[package]] +name = "aiosignal" +version = "1.2.0" +description = "aiosignal: a list of registered asynchronous callbacks" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +frozenlist = ">=1.1.0" + [[package]] name = "astroid" -version = "2.12.5" +version = "2.12.12" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -15,13 +46,27 @@ wrapt = [ ] [[package]] -name = "certifi" -version = "2022.6.15" -description = "Python package for providing Mozilla's CA Bundle." +name = "async-timeout" +version = "4.0.2" +description = "Timeout context manager for asyncio programs" category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "attrs" +version = "22.1.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] +tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] + [[package]] name = "cfgv" version = "3.3.1" @@ -39,15 +84,15 @@ optional = false python-versions = ">=3.6.0" [package.extras] -unicode_backport = ["unicodedata2"] +unicode-backport = ["unicodedata2"] [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "decorator" @@ -59,11 +104,11 @@ python-versions = ">=3.5" [[package]] name = "dill" -version = "0.3.5.1" +version = "0.3.6" description = "serialize all of python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +python-versions = ">=3.7" [package.extras] graph = ["objgraph (>=1.7.2)"] @@ -88,9 +133,17 @@ python-versions = ">=3.7" docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] +[[package]] +name = "frozenlist" +version = "1.3.1" +description = "A list-like structure which implements collections.abc.MutableSequence" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "identify" -version = "2.5.3" +version = "2.5.8" description = "File identification library for Python" category = "dev" optional = false @@ -101,7 +154,7 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.3" +version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false @@ -117,17 +170,17 @@ python-versions = ">=3.6.1,<4.0" [package.extras] colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +pipfile-deprecated-finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] -requirements_deprecated_finder = ["pip-api", "pipreqs"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] [[package]] name = "lazy-object-proxy" -version = "1.7.1" +version = "1.8.0" description = "A fast and thorough lazy object proxy." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "loguru" @@ -152,6 +205,14 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "multidict" +version = "6.0.2" +description = "multidict implementation" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "nodeenv" version = "1.7.0" @@ -193,7 +254,7 @@ virtualenv = ">=20.0.8" [[package]] name = "pydantic" -version = "1.10.1" +version = "1.10.2" description = "Data validation and settings management using python type hints" category = "main" optional = false @@ -208,14 +269,14 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pylint" -version = "2.15.0" +version = "2.15.5" description = "python code static checker" category = "dev" optional = false python-versions = ">=3.7.2" [package.dependencies] -astroid = ">=2.12.4,<=2.14.0-dev0" +astroid = ">=2.12.12,<=2.14.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = ">=0.2" isort = ">=4.2.5,<6" @@ -230,7 +291,7 @@ spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] [[package]] -name = "PyYAML" +name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" category = "dev" @@ -245,35 +306,17 @@ category = "main" optional = false python-versions = "*" -[[package]] -name = "requests" -version = "2.28.1" -description = "Python HTTP for Humans." -category = "main" -optional = false -python-versions = ">=3.7, <4" - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] - [[package]] name = "setuptools" -version = "65.3.0" +version = "65.5.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -294,33 +337,20 @@ python-versions = ">=3.7" [[package]] name = "tomlkit" -version = "0.11.4" +version = "0.11.6" description = "Style preserving TOML library" category = "dev" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.6" [[package]] name = "typing-extensions" -version = "4.3.0" +version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false python-versions = ">=3.7" -[[package]] -name = "urllib3" -version = "1.26.12" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - [[package]] name = "validators" version = "0.20.0" @@ -337,19 +367,19 @@ test = ["flake8 (>=2.4.0)", "isort (>=4.2.2)", "pytest (>=2.2.3)"] [[package]] name = "virtualenv" -version = "20.16.4" +version = "20.16.6" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -distlib = ">=0.3.5,<1" +distlib = ">=0.3.6,<1" filelock = ">=3.4.1,<4" platformdirs = ">=2.4,<3" [package.extras] -docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] +docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] [[package]] @@ -379,19 +409,128 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "yarl" +version = "1.8.1" +description = "Yet another URL library" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + [metadata] lock-version = "1.1" python-versions = "^3.8.10" -content-hash = "4b4dd1e60c935369fec4948af1e796416763fb8a67677fd5b0ef7fba732d993d" +content-hash = "9f6305fd04a65e2b1a18967669eecf28be56fcb18b0c434140823cdb47dc72d8" [metadata.files] +aiohttp = [ + {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ba71c9b4dcbb16212f334126cc3d8beb6af377f6703d9dc2d9fb3874fd667ee9"}, + {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d24b8bb40d5c61ef2d9b6a8f4528c2f17f1c5d2d31fed62ec860f6006142e83e"}, + {file = "aiohttp-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f88df3a83cf9df566f171adba39d5bd52814ac0b94778d2448652fc77f9eb491"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97decbb3372d4b69e4d4c8117f44632551c692bb1361b356a02b97b69e18a62"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309aa21c1d54b8ef0723181d430347d7452daaff93e8e2363db8e75c72c2fb2d"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad5383a67514e8e76906a06741febd9126fc7c7ff0f599d6fcce3e82b80d026f"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20acae4f268317bb975671e375493dbdbc67cddb5f6c71eebdb85b34444ac46b"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05a3c31c6d7cd08c149e50dc7aa2568317f5844acd745621983380597f027a18"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d6f76310355e9fae637c3162936e9504b4767d5c52ca268331e2756e54fd4ca5"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:256deb4b29fe5e47893fa32e1de2d73c3afe7407738bd3c63829874661d4822d"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5c59fcd80b9049b49acd29bd3598cada4afc8d8d69bd4160cd613246912535d7"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:059a91e88f2c00fe40aed9031b3606c3f311414f86a90d696dd982e7aec48142"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2feebbb6074cdbd1ac276dbd737b40e890a1361b3cc30b74ac2f5e24aab41f7b"}, + {file = "aiohttp-3.8.3-cp310-cp310-win32.whl", hash = "sha256:5bf651afd22d5f0c4be16cf39d0482ea494f5c88f03e75e5fef3a85177fecdeb"}, + {file = "aiohttp-3.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:653acc3880459f82a65e27bd6526e47ddf19e643457d36a2250b85b41a564715"}, + {file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:86fc24e58ecb32aee09f864cb11bb91bc4c1086615001647dbfc4dc8c32f4008"}, + {file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75e14eac916f024305db517e00a9252714fce0abcb10ad327fb6dcdc0d060f1d"}, + {file = "aiohttp-3.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1fde0f44029e02d02d3993ad55ce93ead9bb9b15c6b7ccd580f90bd7e3de476"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ab94426ddb1ecc6a0b601d832d5d9d421820989b8caa929114811369673235c"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89d2e02167fa95172c017732ed7725bc8523c598757f08d13c5acca308e1a061"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02f9a2c72fc95d59b881cf38a4b2be9381b9527f9d328771e90f72ac76f31ad8"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7149272fb5834fc186328e2c1fa01dda3e1fa940ce18fded6d412e8f2cf76d"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:512bd5ab136b8dc0ffe3fdf2dfb0c4b4f49c8577f6cae55dca862cd37a4564e2"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7018ecc5fe97027214556afbc7c502fbd718d0740e87eb1217b17efd05b3d276"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:88c70ed9da9963d5496d38320160e8eb7e5f1886f9290475a881db12f351ab5d"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:da22885266bbfb3f78218dc40205fed2671909fbd0720aedba39b4515c038091"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:e65bc19919c910127c06759a63747ebe14f386cda573d95bcc62b427ca1afc73"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:08c78317e950e0762c2983f4dd58dc5e6c9ff75c8a0efeae299d363d439c8e34"}, + {file = "aiohttp-3.8.3-cp311-cp311-win32.whl", hash = "sha256:45d88b016c849d74ebc6f2b6e8bc17cabf26e7e40c0661ddd8fae4c00f015697"}, + {file = "aiohttp-3.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:96372fc29471646b9b106ee918c8eeb4cca423fcbf9a34daa1b93767a88a2290"}, + {file = "aiohttp-3.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c971bf3786b5fad82ce5ad570dc6ee420f5b12527157929e830f51c55dc8af77"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff25f48fc8e623d95eca0670b8cc1469a83783c924a602e0fbd47363bb54aaca"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e381581b37db1db7597b62a2e6b8b57c3deec95d93b6d6407c5b61ddc98aca6d"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db19d60d846283ee275d0416e2a23493f4e6b6028825b51290ac05afc87a6f97"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25892c92bee6d9449ffac82c2fe257f3a6f297792cdb18ad784737d61e7a9a85"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398701865e7a9565d49189f6c90868efaca21be65c725fc87fc305906be915da"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4a4fbc769ea9b6bd97f4ad0b430a6807f92f0e5eb020f1e42ece59f3ecfc4585"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:b29bfd650ed8e148f9c515474a6ef0ba1090b7a8faeee26b74a8ff3b33617502"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:1e56b9cafcd6531bab5d9b2e890bb4937f4165109fe98e2b98ef0dcfcb06ee9d"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ec40170327d4a404b0d91855d41bfe1fe4b699222b2b93e3d833a27330a87a6d"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2df5f139233060578d8c2c975128fb231a89ca0a462b35d4b5fcf7c501ebdbe1"}, + {file = "aiohttp-3.8.3-cp36-cp36m-win32.whl", hash = "sha256:f973157ffeab5459eefe7b97a804987876dd0a55570b8fa56b4e1954bf11329b"}, + {file = "aiohttp-3.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:437399385f2abcd634865705bdc180c8314124b98299d54fe1d4c8990f2f9494"}, + {file = "aiohttp-3.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:09e28f572b21642128ef31f4e8372adb6888846f32fecb288c8b0457597ba61a"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f3553510abdbec67c043ca85727396ceed1272eef029b050677046d3387be8d"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e168a7560b7c61342ae0412997b069753f27ac4862ec7867eff74f0fe4ea2ad9"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db4c979b0b3e0fa7e9e69ecd11b2b3174c6963cebadeecfb7ad24532ffcdd11a"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e164e0a98e92d06da343d17d4e9c4da4654f4a4588a20d6c73548a29f176abe2"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8a78079d9a39ca9ca99a8b0ac2fdc0c4d25fc80c8a8a82e5c8211509c523363"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:21b30885a63c3f4ff5b77a5d6caf008b037cb521a5f33eab445dc566f6d092cc"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4b0f30372cef3fdc262f33d06e7b411cd59058ce9174ef159ad938c4a34a89da"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:8135fa153a20d82ffb64f70a1b5c2738684afa197839b34cc3e3c72fa88d302c"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ad61a9639792fd790523ba072c0555cd6be5a0baf03a49a5dd8cfcf20d56df48"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:978b046ca728073070e9abc074b6299ebf3501e8dee5e26efacb13cec2b2dea0"}, + {file = "aiohttp-3.8.3-cp37-cp37m-win32.whl", hash = "sha256:0d2c6d8c6872df4a6ec37d2ede71eff62395b9e337b4e18efd2177de883a5033"}, + {file = "aiohttp-3.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:21d69797eb951f155026651f7e9362877334508d39c2fc37bd04ff55b2007091"}, + {file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ca9af5f8f5812d475c5259393f52d712f6d5f0d7fdad9acdb1107dd9e3cb7eb"}, + {file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d90043c1882067f1bd26196d5d2db9aa6d268def3293ed5fb317e13c9413ea4"}, + {file = "aiohttp-3.8.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d737fc67b9a970f3234754974531dc9afeea11c70791dcb7db53b0cf81b79784"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebf909ea0a3fc9596e40d55d8000702a85e27fd578ff41a5500f68f20fd32e6c"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5835f258ca9f7c455493a57ee707b76d2d9634d84d5d7f62e77be984ea80b849"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da37dcfbf4b7f45d80ee386a5f81122501ec75672f475da34784196690762f4b"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f44875f2804bc0511a69ce44a9595d5944837a62caecc8490bbdb0e18b1342"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:527b3b87b24844ea7865284aabfab08eb0faf599b385b03c2aa91fc6edd6e4b6"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5ba88df9aa5e2f806650fcbeedbe4f6e8736e92fc0e73b0400538fd25a4dd96"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e7b8813be97cab8cb52b1375f41f8e6804f6507fe4660152e8ca5c48f0436017"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:2dea10edfa1a54098703cb7acaa665c07b4e7568472a47f4e64e6319d3821ccf"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:713d22cd9643ba9025d33c4af43943c7a1eb8547729228de18d3e02e278472b6"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2d252771fc85e0cf8da0b823157962d70639e63cb9b578b1dec9868dd1f4f937"}, + {file = "aiohttp-3.8.3-cp38-cp38-win32.whl", hash = "sha256:66bd5f950344fb2b3dbdd421aaa4e84f4411a1a13fca3aeb2bcbe667f80c9f76"}, + {file = "aiohttp-3.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:84b14f36e85295fe69c6b9789b51a0903b774046d5f7df538176516c3e422446"}, + {file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16c121ba0b1ec2b44b73e3a8a171c4f999b33929cd2397124a8c7fcfc8cd9e06"}, + {file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8d6aaa4e7155afaf994d7924eb290abbe81a6905b303d8cb61310a2aba1c68ba"}, + {file = "aiohttp-3.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43046a319664a04b146f81b40e1545d4c8ac7b7dd04c47e40bf09f65f2437346"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599418aaaf88a6d02a8c515e656f6faf3d10618d3dd95866eb4436520096c84b"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a2964319d359f494f16011e23434f6f8ef0434acd3cf154a6b7bec511e2fb7"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73a4131962e6d91109bca6536416aa067cf6c4efb871975df734f8d2fd821b37"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598adde339d2cf7d67beaccda3f2ce7c57b3b412702f29c946708f69cf8222aa"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75880ed07be39beff1881d81e4a907cafb802f306efd6d2d15f2b3c69935f6fb"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0239da9fbafd9ff82fd67c16704a7d1bccf0d107a300e790587ad05547681c8"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4e3a23ec214e95c9fe85a58470b660efe6534b83e6cbe38b3ed52b053d7cb6ad"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:47841407cc89a4b80b0c52276f3cc8138bbbfba4b179ee3acbd7d77ae33f7ac4"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:54d107c89a3ebcd13228278d68f1436d3f33f2dd2af5415e3feaeb1156e1a62c"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c37c5cce780349d4d51739ae682dec63573847a2a8dcb44381b174c3d9c8d403"}, + {file = "aiohttp-3.8.3-cp39-cp39-win32.whl", hash = "sha256:f178d2aadf0166be4df834c4953da2d7eef24719e8aec9a65289483eeea9d618"}, + {file = "aiohttp-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:88e5be56c231981428f4f506c68b6a46fa25c4123a2e86d156c58a8369d31ab7"}, + {file = "aiohttp-3.8.3.tar.gz", hash = "sha256:3828fb41b7203176b82fe5d699e0d845435f2374750a44b480ea6b930f6be269"}, +] +aiosignal = [ + {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, + {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, +] astroid = [ - {file = "astroid-2.12.5-py3-none-any.whl", hash = "sha256:d612609242996c4365aeb0345e61edba34363eaaba55f1c0addf6a98f073bef6"}, - {file = "astroid-2.12.5.tar.gz", hash = "sha256:396c88d0a58d7f8daadf730b2ce90838bf338c6752558db719ec6f99c18ec20e"}, + {file = "astroid-2.12.12-py3-none-any.whl", hash = "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f"}, + {file = "astroid-2.12.12.tar.gz", hash = "sha256:1c00a14f5a3ed0339d38d2e2e5b74ea2591df5861c0936bb292b84ccf3a78d83"}, ] -certifi = [ - {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, - {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, +async-timeout = [ + {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, + {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, +] +attrs = [ + {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, + {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, @@ -402,16 +541,16 @@ charset-normalizer = [ {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, ] colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] decorator = [ {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] dill = [ - {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"}, - {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"}, + {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, + {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, ] distlib = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, @@ -421,56 +560,99 @@ filelock = [ {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, ] +frozenlist = [ + {file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5f271c93f001748fc26ddea409241312a75e13466b06c94798d1a341cf0e6989"}, + {file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c6ef8014b842f01f5d2b55315f1af5cbfde284eb184075c189fd657c2fd8204"}, + {file = "frozenlist-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:219a9676e2eae91cb5cc695a78b4cb43d8123e4160441d2b6ce8d2c70c60e2f3"}, + {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b47d64cdd973aede3dd71a9364742c542587db214e63b7529fbb487ed67cddd9"}, + {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2af6f7a4e93f5d08ee3f9152bce41a6015b5cf87546cb63872cc19b45476e98a"}, + {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a718b427ff781c4f4e975525edb092ee2cdef6a9e7bc49e15063b088961806f8"}, + {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c56c299602c70bc1bb5d1e75f7d8c007ca40c9d7aebaf6e4ba52925d88ef826d"}, + {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:717470bfafbb9d9be624da7780c4296aa7935294bd43a075139c3d55659038ca"}, + {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:31b44f1feb3630146cffe56344704b730c33e042ffc78d21f2125a6a91168131"}, + {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c3b31180b82c519b8926e629bf9f19952c743e089c41380ddca5db556817b221"}, + {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:d82bed73544e91fb081ab93e3725e45dd8515c675c0e9926b4e1f420a93a6ab9"}, + {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49459f193324fbd6413e8e03bd65789e5198a9fa3095e03f3620dee2f2dabff2"}, + {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:94e680aeedc7fd3b892b6fa8395b7b7cc4b344046c065ed4e7a1e390084e8cb5"}, + {file = "frozenlist-1.3.1-cp310-cp310-win32.whl", hash = "sha256:fabb953ab913dadc1ff9dcc3a7a7d3dc6a92efab3a0373989b8063347f8705be"}, + {file = "frozenlist-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:eee0c5ecb58296580fc495ac99b003f64f82a74f9576a244d04978a7e97166db"}, + {file = "frozenlist-1.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0bc75692fb3770cf2b5856a6c2c9de967ca744863c5e89595df64e252e4b3944"}, + {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086ca1ac0a40e722d6833d4ce74f5bf1aba2c77cbfdc0cd83722ffea6da52a04"}, + {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b51eb355e7f813bcda00276b0114c4172872dc5fb30e3fea059b9367c18fbcb"}, + {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74140933d45271c1a1283f708c35187f94e1256079b3c43f0c2267f9db5845ff"}, + {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee4c5120ddf7d4dd1eaf079af3af7102b56d919fa13ad55600a4e0ebe532779b"}, + {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97d9e00f3ac7c18e685320601f91468ec06c58acc185d18bb8e511f196c8d4b2"}, + {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6e19add867cebfb249b4e7beac382d33215d6d54476bb6be46b01f8cafb4878b"}, + {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a027f8f723d07c3f21963caa7d585dcc9b089335565dabe9c814b5f70c52705a"}, + {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:61d7857950a3139bce035ad0b0945f839532987dfb4c06cfe160254f4d19df03"}, + {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:53b2b45052e7149ee8b96067793db8ecc1ae1111f2f96fe1f88ea5ad5fd92d10"}, + {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bbb1a71b1784e68870800b1bc9f3313918edc63dbb8f29fbd2e767ce5821696c"}, + {file = "frozenlist-1.3.1-cp37-cp37m-win32.whl", hash = "sha256:ab6fa8c7871877810e1b4e9392c187a60611fbf0226a9e0b11b7b92f5ac72792"}, + {file = "frozenlist-1.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f89139662cc4e65a4813f4babb9ca9544e42bddb823d2ec434e18dad582543bc"}, + {file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4c0c99e31491a1d92cde8648f2e7ccad0e9abb181f6ac3ddb9fc48b63301808e"}, + {file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61e8cb51fba9f1f33887e22488bad1e28dd8325b72425f04517a4d285a04c519"}, + {file = "frozenlist-1.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc2f3e368ee5242a2cbe28323a866656006382872c40869b49b265add546703f"}, + {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58fb94a01414cddcdc6839807db77ae8057d02ddafc94a42faee6004e46c9ba8"}, + {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:022178b277cb9277d7d3b3f2762d294f15e85cd2534047e68a118c2bb0058f3e"}, + {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:572ce381e9fe027ad5e055f143763637dcbac2542cfe27f1d688846baeef5170"}, + {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19127f8dcbc157ccb14c30e6f00392f372ddb64a6ffa7106b26ff2196477ee9f"}, + {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42719a8bd3792744c9b523674b752091a7962d0d2d117f0b417a3eba97d1164b"}, + {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2743bb63095ef306041c8f8ea22bd6e4d91adabf41887b1ad7886c4c1eb43d5f"}, + {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:fa47319a10e0a076709644a0efbcaab9e91902c8bd8ef74c6adb19d320f69b83"}, + {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52137f0aea43e1993264a5180c467a08a3e372ca9d378244c2d86133f948b26b"}, + {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:f5abc8b4d0c5b556ed8cd41490b606fe99293175a82b98e652c3f2711b452988"}, + {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1e1cf7bc8cbbe6ce3881863671bac258b7d6bfc3706c600008925fb799a256e2"}, + {file = "frozenlist-1.3.1-cp38-cp38-win32.whl", hash = "sha256:0dde791b9b97f189874d654c55c24bf7b6782343e14909c84beebd28b7217845"}, + {file = "frozenlist-1.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:9494122bf39da6422b0972c4579e248867b6b1b50c9b05df7e04a3f30b9a413d"}, + {file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31bf9539284f39ff9398deabf5561c2b0da5bb475590b4e13dd8b268d7a3c5c1"}, + {file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0c8c803f2f8db7217898d11657cb6042b9b0553a997c4a0601f48a691480fab"}, + {file = "frozenlist-1.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da5ba7b59d954f1f214d352308d1d86994d713b13edd4b24a556bcc43d2ddbc3"}, + {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74e6b2b456f21fc93ce1aff2b9728049f1464428ee2c9752a4b4f61e98c4db96"}, + {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526d5f20e954d103b1d47232e3839f3453c02077b74203e43407b962ab131e7b"}, + {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b499c6abe62a7a8d023e2c4b2834fce78a6115856ae95522f2f974139814538c"}, + {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab386503f53bbbc64d1ad4b6865bf001414930841a870fc97f1546d4d133f141"}, + {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f63c308f82a7954bf8263a6e6de0adc67c48a8b484fab18ff87f349af356efd"}, + {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:12607804084d2244a7bd4685c9d0dca5df17a6a926d4f1967aa7978b1028f89f"}, + {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:da1cdfa96425cbe51f8afa43e392366ed0b36ce398f08b60de6b97e3ed4affef"}, + {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f810e764617b0748b49a731ffaa525d9bb36ff38332411704c2400125af859a6"}, + {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:35c3d79b81908579beb1fb4e7fcd802b7b4921f1b66055af2578ff7734711cfa"}, + {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c92deb5d9acce226a501b77307b3b60b264ca21862bd7d3e0c1f3594022f01bc"}, + {file = "frozenlist-1.3.1-cp39-cp39-win32.whl", hash = "sha256:5e77a8bd41e54b05e4fb2708dc6ce28ee70325f8c6f50f3df86a44ecb1d7a19b"}, + {file = "frozenlist-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:625d8472c67f2d96f9a4302a947f92a7adbc1e20bedb6aff8dbc8ff039ca6189"}, + {file = "frozenlist-1.3.1.tar.gz", hash = "sha256:3a735e4211a04ccfa3f4833547acdf5d2f863bfeb01cfd3edaffbc251f15cec8"}, +] identify = [ - {file = "identify-2.5.3-py2.py3-none-any.whl", hash = "sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893"}, - {file = "identify-2.5.3.tar.gz", hash = "sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44"}, + {file = "identify-2.5.8-py2.py3-none-any.whl", hash = "sha256:48b7925fe122720088aeb7a6c34f17b27e706b72c61070f27fe3789094233440"}, + {file = "identify-2.5.8.tar.gz", hash = "sha256:7a214a10313b9489a0d61467db2856ae8d0b8306fc923e03a9effa53d8aedc58"}, ] idna = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] lazy-object-proxy = [ - {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, - {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, + {file = "lazy-object-proxy-1.8.0.tar.gz", hash = "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-win32.whl", hash = "sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-win32.whl", hash = "sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win32.whl", hash = "sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-win32.whl", hash = "sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-win32.whl", hash = "sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0"}, + {file = "lazy_object_proxy-1.8.0-pp37-pypy37_pp73-any.whl", hash = "sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891"}, + {file = "lazy_object_proxy-1.8.0-pp38-pypy38_pp73-any.whl", hash = "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec"}, + {file = "lazy_object_proxy-1.8.0-pp39-pypy39_pp73-any.whl", hash = "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8"}, ] loguru = [ {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"}, @@ -480,6 +662,67 @@ mccabe = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +multidict = [ + {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"}, + {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"}, + {file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"}, + {file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"}, + {file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"}, + {file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"}, + {file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"}, + {file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"}, + {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"}, + {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"}, + {file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"}, + {file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"}, + {file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"}, + {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"}, + {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"}, + {file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"}, + {file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"}, + {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, + {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, +] nodeenv = [ {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, @@ -493,48 +736,48 @@ pre-commit = [ {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, ] pydantic = [ - {file = "pydantic-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:221166d99726238f71adc4fa9f3e94063a10787574b966f86a774559e709ac5a"}, - {file = "pydantic-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a90e85d95fd968cd7cae122e0d3e0e1f6613bc88c1ff3fe838ac9785ea4b1c4c"}, - {file = "pydantic-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2157aaf5718c648eaec9e654a34179ae42ffc363dc3ad058538a4f3ecbd9341"}, - {file = "pydantic-1.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6142246fc9adb51cadaeb84fb52a86f3adad4c6a7b0938a5dd0b1356b0088217"}, - {file = "pydantic-1.10.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:60dad97a09b6f44690c05467a4f397b62bfc2c839ac39102819d6979abc2be0d"}, - {file = "pydantic-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d6f5bcb59d33ec46621dae76e714c53035087666cac80c81c9047a84f3ff93d0"}, - {file = "pydantic-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:522906820cd60e63c7960ba83078bf2d2ad2dd0870bf68248039bcb1ec3eb0a4"}, - {file = "pydantic-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d545c89d88bdd5559db17aeb5a61a26799903e4bd76114779b3bf1456690f6ce"}, - {file = "pydantic-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad2374b5b3b771dcc6e2f6e0d56632ab63b90e9808b7a73ad865397fcdb4b2cd"}, - {file = "pydantic-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90e02f61b7354ed330f294a437d0bffac9e21a5d46cb4cc3c89d220e497db7ac"}, - {file = "pydantic-1.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc5ffe7bd0b4778fa5b7a5f825c52d6cfea3ae2d9b52b05b9b1d97e36dee23a8"}, - {file = "pydantic-1.10.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7acb7b66ffd2bc046eaff0063df84c83fc3826722d5272adaeadf6252e17f691"}, - {file = "pydantic-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7e6786ed5faa559dea5a77f6d2de9a08d18130de9344533535d945f34bdcd42e"}, - {file = "pydantic-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:c7bf8ff1d18186eb0cbe42bd9bfb4cbf7fde1fd01b8608925458990c21f202f0"}, - {file = "pydantic-1.10.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:14a5babda137a294df7ad5f220986d79bbb87fdeb332c6ded61ce19da7f5f3bf"}, - {file = "pydantic-1.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5659cb9c6b3d27fc0067025c4f5a205f5e838232a4a929b412781117c2343d44"}, - {file = "pydantic-1.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d70fb91b03c32d2e857b071a22a5225e6b625ca82bd2cc8dd729d88e0bd200"}, - {file = "pydantic-1.10.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9a93be313e40f12c6f2cb84533b226bbe23d0774872e38d83415e6890215e3a6"}, - {file = "pydantic-1.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d55aeb01bb7bd7c7e1bd904668a4a2ffcbb1c248e7ae9eb40a272fd7e67dd98b"}, - {file = "pydantic-1.10.1-cp37-cp37m-win_amd64.whl", hash = "sha256:43d41b6f13706488e854729955ba8f740e6ec375cd16b72b81dc24b9d84f0d15"}, - {file = "pydantic-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f31ffe0e38805a0e6410330f78147bb89193b136d7a5f79cae60d3e849b520a6"}, - {file = "pydantic-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8eee69eda7674977b079a21e7bf825b59d8bf15145300e8034ed3eb239ac444f"}, - {file = "pydantic-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f927bff6c319fc92e0a2cbeb2609b5c1cd562862f4b54ec905e353282b7c8b1"}, - {file = "pydantic-1.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb1bc3f8fef6ba36977108505e90558911e7fbccb4e930805d5dd90891b56ff4"}, - {file = "pydantic-1.10.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96ab6ce1346d14c6e581a69c333bdd1b492df9cf85ad31ad77a8aa42180b7e09"}, - {file = "pydantic-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:444cf220a12134da1cd42fe4f45edff622139e10177ce3d8ef2b4f41db1291b2"}, - {file = "pydantic-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:dbfbff83565b4514dd8cebc8b8c81a12247e89427ff997ad0a9da7b2b1065c12"}, - {file = "pydantic-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5327406f4bfd5aee784e7ad2a6a5fdd7171c19905bf34cb1994a1ba73a87c468"}, - {file = "pydantic-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1072eae28bf034a311764c130784e8065201a90edbca10f495c906737b3bd642"}, - {file = "pydantic-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce901335667a68dfbc10dd2ee6c0d676b89210d754441c2469fbc37baf7ee2ed"}, - {file = "pydantic-1.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54d6465cd2112441305faf5143a491b40de07a203116b5755a2108e36b25308d"}, - {file = "pydantic-1.10.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2b5e5e7a0ec96704099e271911a1049321ba1afda92920df0769898a7e9a1298"}, - {file = "pydantic-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ae43704358304da45c1c3dd7056f173c618b252f91594bcb6d6f6b4c6c284dee"}, - {file = "pydantic-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:2d7da49229ffb1049779a5a6c1c50a26da164bd053cf8ee9042197dc08a98259"}, - {file = "pydantic-1.10.1-py3-none-any.whl", hash = "sha256:f8b10e59c035ff3dcc9791619d6e6c5141e0fa5cbe264e19e267b8d523b210bf"}, - {file = "pydantic-1.10.1.tar.gz", hash = "sha256:d41bb80347a8a2d51fbd6f1748b42aca14541315878447ba159617544712f770"}, + {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"}, + {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"}, + {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"}, + {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559"}, + {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236"}, + {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c"}, + {file = "pydantic-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644"}, + {file = "pydantic-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f"}, + {file = "pydantic-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a"}, + {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525"}, + {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283"}, + {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42"}, + {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52"}, + {file = "pydantic-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c"}, + {file = "pydantic-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5"}, + {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c"}, + {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254"}, + {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5"}, + {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d"}, + {file = "pydantic-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2"}, + {file = "pydantic-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13"}, + {file = "pydantic-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116"}, + {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624"}, + {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1"}, + {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9"}, + {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965"}, + {file = "pydantic-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e"}, + {file = "pydantic-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488"}, + {file = "pydantic-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41"}, + {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b"}, + {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe"}, + {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d"}, + {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda"}, + {file = "pydantic-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6"}, + {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"}, + {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"}, ] pylint = [ - {file = "pylint-2.15.0-py3-none-any.whl", hash = "sha256:4b124affc198b7f7c9b5f9ab690d85db48282a025ef9333f51d2d7281b92a6c3"}, - {file = "pylint-2.15.0.tar.gz", hash = "sha256:4f3f7e869646b0bd63b3dfb79f3c0f28fc3d2d923ea220d52620fd625aed92b0"}, + {file = "pylint-2.15.5-py3-none-any.whl", hash = "sha256:c2108037eb074334d9e874dc3c783752cc03d0796c88c9a9af282d0f161a1004"}, + {file = "pylint-2.15.5.tar.gz", hash = "sha256:3b120505e5af1d06a5ad76b55d8660d44bf0f2fc3c59c2bdd94e39188ee3a4df"}, ] -PyYAML = [ +pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, @@ -542,6 +785,13 @@ PyYAML = [ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, @@ -572,13 +822,9 @@ PyYAML = [ ratelimit = [ {file = "ratelimit-2.2.1.tar.gz", hash = "sha256:af8a9b64b821529aca09ebaf6d8d279100d766f19e90b5059ac6a718ca6dee42"}, ] -requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, -] setuptools = [ - {file = "setuptools-65.3.0-py3-none-any.whl", hash = "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82"}, - {file = "setuptools-65.3.0.tar.gz", hash = "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57"}, + {file = "setuptools-65.5.1-py3-none-any.whl", hash = "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31"}, + {file = "setuptools-65.5.1.tar.gz", hash = "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, @@ -589,23 +835,19 @@ tomli = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] tomlkit = [ - {file = "tomlkit-0.11.4-py3-none-any.whl", hash = "sha256:25d4e2e446c453be6360c67ddfb88838cfc42026322770ba13d1fbd403a93a5c"}, - {file = "tomlkit-0.11.4.tar.gz", hash = "sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83"}, + {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, + {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, ] typing-extensions = [ - {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, - {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, -] -urllib3 = [ - {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, - {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] validators = [ {file = "validators-0.20.0.tar.gz", hash = "sha256:24148ce4e64100a2d5e267233e23e7afeb55316b47d30faae7eb6e7292bc226a"}, ] virtualenv = [ - {file = "virtualenv-20.16.4-py3-none-any.whl", hash = "sha256:035ed57acce4ac35c82c9d8802202b0e71adac011a511ff650cbcf9635006a22"}, - {file = "virtualenv-20.16.4.tar.gz", hash = "sha256:014f766e4134d0008dcaa1f95bafa0fb0f575795d07cae50b1bee514185d6782"}, + {file = "virtualenv-20.16.6-py3-none-any.whl", hash = "sha256:186ca84254abcbde98180fd17092f9628c5fe742273c02724972a1d8a2035108"}, + {file = "virtualenv-20.16.6.tar.gz", hash = "sha256:530b850b523c6449406dfba859d6345e48ef19b8439606c5d74d7d3c9e14d76e"}, ] win32-setctime = [ {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, @@ -681,3 +923,64 @@ yapf = [ {file = "yapf-0.32.0-py2.py3-none-any.whl", hash = "sha256:8fea849025584e486fd06d6ba2bed717f396080fd3cc236ba10cb97c4c51cf32"}, {file = "yapf-0.32.0.tar.gz", hash = "sha256:a3f5085d37ef7e3e004c4ba9f9b3e40c54ff1901cd111f05145ae313a7c67d1b"}, ] +yarl = [ + {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28"}, + {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3"}, + {file = "yarl-1.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9de955d98e02fab288c7718662afb33aab64212ecb368c5dc866d9a57bf48880"}, + {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ec362167e2c9fd178f82f252b6d97669d7245695dc057ee182118042026da40"}, + {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20df6ff4089bc86e4a66e3b1380460f864df3dd9dccaf88d6b3385d24405893b"}, + {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5999c4662631cb798496535afbd837a102859568adc67d75d2045e31ec3ac497"}, + {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed19b74e81b10b592084a5ad1e70f845f0aacb57577018d31de064e71ffa267a"}, + {file = "yarl-1.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e4808f996ca39a6463f45182e2af2fae55e2560be586d447ce8016f389f626f"}, + {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2d800b9c2eaf0684c08be5f50e52bfa2aa920e7163c2ea43f4f431e829b4f0fd"}, + {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6628d750041550c5d9da50bb40b5cf28a2e63b9388bac10fedd4f19236ef4957"}, + {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f5af52738e225fcc526ae64071b7e5342abe03f42e0e8918227b38c9aa711e28"}, + {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:76577f13333b4fe345c3704811ac7509b31499132ff0181f25ee26619de2c843"}, + {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c03f456522d1ec815893d85fccb5def01ffaa74c1b16ff30f8aaa03eb21e453"}, + {file = "yarl-1.8.1-cp310-cp310-win32.whl", hash = "sha256:ea30a42dc94d42f2ba4d0f7c0ffb4f4f9baa1b23045910c0c32df9c9902cb272"}, + {file = "yarl-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:9130ddf1ae9978abe63808b6b60a897e41fccb834408cde79522feb37fb72fb0"}, + {file = "yarl-1.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0ab5a138211c1c366404d912824bdcf5545ccba5b3ff52c42c4af4cbdc2c5035"}, + {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0fb2cb4204ddb456a8e32381f9a90000429489a25f64e817e6ff94879d432fc"}, + {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85cba594433915d5c9a0d14b24cfba0339f57a2fff203a5d4fd070e593307d0b"}, + {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca7e596c55bd675432b11320b4eacc62310c2145d6801a1f8e9ad160685a231"}, + {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f77539733e0ec2475ddcd4e26777d08996f8cd55d2aef82ec4d3896687abda"}, + {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29e256649f42771829974e742061c3501cc50cf16e63f91ed8d1bf98242e5507"}, + {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7fce6cbc6c170ede0221cc8c91b285f7f3c8b9fe28283b51885ff621bbe0f8ee"}, + {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:59ddd85a1214862ce7c7c66457f05543b6a275b70a65de366030d56159a979f0"}, + {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:12768232751689c1a89b0376a96a32bc7633c08da45ad985d0c49ede691f5c0d"}, + {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:b19255dde4b4f4c32e012038f2c169bb72e7f081552bea4641cab4d88bc409dd"}, + {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6c8148e0b52bf9535c40c48faebb00cb294ee577ca069d21bd5c48d302a83780"}, + {file = "yarl-1.8.1-cp37-cp37m-win32.whl", hash = "sha256:de839c3a1826a909fdbfe05f6fe2167c4ab033f1133757b5936efe2f84904c07"}, + {file = "yarl-1.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:dd032e8422a52e5a4860e062eb84ac94ea08861d334a4bcaf142a63ce8ad4802"}, + {file = "yarl-1.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:19cd801d6f983918a3f3a39f3a45b553c015c5aac92ccd1fac619bd74beece4a"}, + {file = "yarl-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6347f1a58e658b97b0a0d1ff7658a03cb79bdbda0331603bed24dd7054a6dea1"}, + {file = "yarl-1.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c0da7e44d0c9108d8b98469338705e07f4bb7dab96dbd8fa4e91b337db42548"}, + {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5587bba41399854703212b87071c6d8638fa6e61656385875f8c6dff92b2e461"}, + {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31a9a04ecccd6b03e2b0e12e82131f1488dea5555a13a4d32f064e22a6003cfe"}, + {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:205904cffd69ae972a1707a1bd3ea7cded594b1d773a0ce66714edf17833cdae"}, + {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea513a25976d21733bff523e0ca836ef1679630ef4ad22d46987d04b372d57fc"}, + {file = "yarl-1.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0b51530877d3ad7a8d47b2fff0c8df3b8f3b8deddf057379ba50b13df2a5eae"}, + {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2b8f245dad9e331540c350285910b20dd913dc86d4ee410c11d48523c4fd546"}, + {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ab2a60d57ca88e1d4ca34a10e9fb4ab2ac5ad315543351de3a612bbb0560bead"}, + {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:449c957ffc6bc2309e1fbe67ab7d2c1efca89d3f4912baeb8ead207bb3cc1cd4"}, + {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a165442348c211b5dea67c0206fc61366212d7082ba8118c8c5c1c853ea4d82e"}, + {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b3ded839a5c5608eec8b6f9ae9a62cb22cd037ea97c627f38ae0841a48f09eae"}, + {file = "yarl-1.8.1-cp38-cp38-win32.whl", hash = "sha256:c1445a0c562ed561d06d8cbc5c8916c6008a31c60bc3655cdd2de1d3bf5174a0"}, + {file = "yarl-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:56c11efb0a89700987d05597b08a1efcd78d74c52febe530126785e1b1a285f4"}, + {file = "yarl-1.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e80ed5a9939ceb6fda42811542f31c8602be336b1fb977bccb012e83da7e4936"}, + {file = "yarl-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6afb336e23a793cd3b6476c30f030a0d4c7539cd81649683b5e0c1b0ab0bf350"}, + {file = "yarl-1.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c322cbaa4ed78a8aac89b2174a6df398faf50e5fc12c4c191c40c59d5e28357"}, + {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fae37373155f5ef9b403ab48af5136ae9851151f7aacd9926251ab26b953118b"}, + {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5395da939ffa959974577eff2cbfc24b004a2fb6c346918f39966a5786874e54"}, + {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:076eede537ab978b605f41db79a56cad2e7efeea2aa6e0fa8f05a26c24a034fb"}, + {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1a50e461615747dd93c099f297c1994d472b0f4d2db8a64e55b1edf704ec1c"}, + {file = "yarl-1.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7de89c8456525650ffa2bb56a3eee6af891e98f498babd43ae307bd42dca98f6"}, + {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4a88510731cd8d4befaba5fbd734a7dd914de5ab8132a5b3dde0bbd6c9476c64"}, + {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2d93a049d29df172f48bcb09acf9226318e712ce67374f893b460b42cc1380ae"}, + {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:21ac44b763e0eec15746a3d440f5e09ad2ecc8b5f6dcd3ea8cb4773d6d4703e3"}, + {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d0272228fabe78ce00a3365ffffd6f643f57a91043e119c289aaba202f4095b0"}, + {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99449cd5366fe4608e7226c6cae80873296dfa0cde45d9b498fefa1de315a09e"}, + {file = "yarl-1.8.1-cp39-cp39-win32.whl", hash = "sha256:8b0af1cf36b93cee99a31a545fe91d08223e64390c5ecc5e94c39511832a4bb6"}, + {file = "yarl-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:de49d77e968de6626ba7ef4472323f9d2e5a56c1d85b7c0e2a190b2173d3b9be"}, + {file = "yarl-1.8.1.tar.gz", hash = "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf"}, +] diff --git a/pyproject.toml b/pyproject.toml index c6b0c8d2..75a177dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "shikithon" -version = "1.0.2" +version = "2.0.0" description = "Yet another Python wrapper for Shikimori API" authors = [ "SecondThundeR " @@ -28,16 +28,19 @@ generate-setup-file = false [tool.yapf] based_on_style = "google" +[tool.isort] +profile = "google" + [tool.poetry.dependencies] python = "^3.8.10" -requests = "^2.28.1" pydantic = "^1.10.1" ratelimit = "^2.2.1" loguru = "^0.6.0" validators = "^0.20.0" +aiohttp = "^3.8.3" [tool.poetry.dev-dependencies] -pylint = "^2.15.0" +pylint = "^2.15.5" pre-commit = "^2.20.0" yapf = "^0.32.0" isort = "^5.10.1" diff --git a/shikithon/__init__.py b/shikithon/__init__.py index f8b8843b..f0e02c10 100644 --- a/shikithon/__init__.py +++ b/shikithon/__init__.py @@ -1,5 +1,5 @@ """Contains package version and some magic for importing API object.""" -from shikithon.api import API +from .api import ShikimoriAPI -__version__ = '1.0.2' -__all__ = ['API'] +__version__ = '2.0.0' +__all__ = ['ShikimoriAPI'] diff --git a/shikithon/api.py b/shikithon/api.py index 7e327d11..e62fd329 100644 --- a/shikithon/api.py +++ b/shikithon/api.py @@ -3,3678 +3,152 @@ This is main module with a class for interacting with the Shikimori API. """ +from __future__ import annotations + import sys -from json import dumps -from time import sleep, time -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Callable, Dict, List, Optional, TypeVar, Union from loguru import logger -from ratelimit import limits, sleep_and_retry -from requests import JSONDecodeError, Session - -from shikithon.config_cache import ConfigCache -from shikithon.decorators import method_endpoint, protected_method -from shikithon.endpoints import Endpoints -from shikithon.enums.anime import (AnimeCensorship, AnimeDuration, AnimeKind, - AnimeList, AnimeOrder, AnimeRating, - AnimeStatus) -from shikithon.enums.club import (CommentPolicy, ImageUploadPolicy, JoinPolicy, - PagePolicy, TopicPolicy) -from shikithon.enums.comment import CommentableType -from shikithon.enums.favorite import FavoriteLinkedType -from shikithon.enums.history import TargetType -from shikithon.enums.manga import (MangaCensorship, MangaKind, MangaList, - MangaOrder, MangaStatus) -from shikithon.enums.message import MessageType -from shikithon.enums.person import PersonKind -from shikithon.enums.ranobe import (RanobeCensorship, RanobeList, RanobeOrder, - RanobeStatus) -from shikithon.enums.request import RequestType -from shikithon.enums.response import ResponseCode -from shikithon.enums.style import OwnerType -from shikithon.enums.topic import ForumType, TopicLinkedType, TopicType -from shikithon.enums.user_rate import (UserRateStatus, UserRateTarget, - UserRateType) -from shikithon.enums.video import VideoKind -from shikithon.exceptions import (AccessTokenException, MissingAppVariable, - MissingAuthCode, MissingConfigData) -from shikithon.models.abuse_response import AbuseResponse -from shikithon.models.achievement import Achievement -from shikithon.models.anime import Anime -from shikithon.models.ban import Ban -from shikithon.models.calendar_event import CalendarEvent -from shikithon.models.character import Character -from shikithon.models.club import Club -from shikithon.models.club_image import ClubImage -from shikithon.models.comment import Comment -from shikithon.models.constants import (AnimeConstants, ClubConstants, - MangaConstants, SmileyConstants, - UserRateConstants) -from shikithon.models.created_user_image import CreatedUserImage -from shikithon.models.creator import Creator -from shikithon.models.dialog import Dialog -from shikithon.models.favourites import Favourites -from shikithon.models.forum import Forum -from shikithon.models.franchise_tree import FranchiseTree -from shikithon.models.genre import Genre -from shikithon.models.history import History -from shikithon.models.link import Link -from shikithon.models.manga import Manga -from shikithon.models.message import Message -from shikithon.models.people import People -from shikithon.models.publisher import Publisher -from shikithon.models.ranobe import Ranobe -from shikithon.models.relation import Relation -from shikithon.models.screenshot import Screenshot -from shikithon.models.studio import Studio -from shikithon.models.style import Style -from shikithon.models.topic import Topic -from shikithon.models.unread_messages import UnreadMessages -from shikithon.models.user import User -from shikithon.models.user_list import UserList -from shikithon.models.user_rate import UserRate -from shikithon.models.video import Video -from shikithon.utils import Utils - -SHIKIMORI_API_URL = 'https://shikimori.one/api' -SHIKIMORI_API_URL_V2 = 'https://shikimori.one/api/v2' -SHIKIMORI_OAUTH_URL = 'https://shikimori.one/oauth' -DEFAULT_REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob' - -ONE_MINUTE = 60 -MAX_CALLS_PER_MINUTE = 90 -RATE_LIMIT_RPS_COOLDOWN = 1 -TOKEN_EXPIRE_TIME = 86400 - -class API: +from .base_client import Client +from .resources import AbuseRequests +from .resources import Achievements +from .resources import Animes +from .resources import Appears +from .resources import Bans +from .resources import Calendar +from .resources import Characters +from .resources import Clubs +from .resources import Comments +from .resources import Constants +from .resources import Dialogs +from .resources import Favorites +from .resources import Forums +from .resources import Friends +from .resources import Genres +from .resources import Mangas +from .resources import Messages +from .resources import Publishers +from .resources import Ranobes +from .resources import Stats +from .resources import Studios +from .resources import Styles +from .resources import Topics +from .resources import UserImages +from .resources import UserRates +from .resources import Users +from .resources.people import People + +RT = TypeVar('RT') + + +class ShikimoriAPI: """ Main class for interacting with the API. - Has most of the methods that simplify the configuration and validation - of the object and convenient methods for getting data from the API - - **Note:** Due to problems with some methods, - when the session header contains a User-Agent and authorization, - __init__ sets only the User-Agent, - and all protected methods independently - provide a header with a token + Current API class uses base client for interacting with API. + Also, all API methods splitted up to resources for convinient usage. """ - def __init__(self, config: Union[str, Dict[str, str]]): + def __init__(self, + config: Union[str, Dict[str, str]], + logging: Optional[bool] = True): """ - Initialization and updating of variables - required to interact with the Shikimori API + Shikimori API class initialization. - This magic method calls config and variables validator, - as well as updating session object header - and getting access/refresh tokens + This magic method inits client and all resources + for interacting with. :param config: Config file for API class or app name :type config: Union[str, Dict[str, str]] - """ - logger.configure(handlers=[ - { - 'sink': sys.stderr, - 'level': 'INFO', - 'format': '{time} | {level} | {message}' - }, - { - 'sink': 'shikithon_{time}.log', - 'level': 'DEBUG', - 'format': '{time} | {level} | {file}.{function}: {message}', - 'rotation': '1 MB', - 'compression': 'zip' - }, - ]) - logger.info('Initializing API object') - - self._endpoints = Endpoints(SHIKIMORI_API_URL, SHIKIMORI_API_URL_V2, - SHIKIMORI_OAUTH_URL) - self._session = Session() - - self._restricted_mode = False - self._app_name = '' - self._client_id = '' - self._client_secret = '' - self._redirect_uri = '' - self._scopes = '' - self._auth_code = '' - self._access_token = '' - self._refresh_token = '' - self._token_expire = -1 - - self._init_config(config) - logger.info('Successfully initialized API object') - - @property - def restricted_mode(self) -> bool: - """ - Returns current restrict mode of API object. - - If true, API object can access only public methods - - :return: Current restrict mode - :rtype: bool - """ - return self._restricted_mode - - @restricted_mode.setter - def restricted_mode(self, restricted_mode: bool): - """ - Sets new restrict mode of API object. - - :param restricted_mode: New restrict mode - :type restricted_mode: bool - """ - self._restricted_mode = restricted_mode - - @property - def scopes_list(self) -> List[str]: - """ - Returns list of scopes. - - :return: List of scopes - :rtype: List[str] - """ - return self._scopes.split('+') - - @property - def config(self) -> Dict[str, str]: - """ - Returns current API variables as config dictionary. - - :return: Current API variables as config dictionary - :rtype: Dict[str, str] - """ - logger.debug('Exporting current API config') - return { - 'app_name': self._app_name, - 'client_id': self._client_id, - 'client_secret': self._client_secret, - 'redirect_uri': self._redirect_uri, - 'scopes': self._scopes, - 'auth_code': self._auth_code, - 'access_token': self._access_token, - 'refresh_token': self._refresh_token, - 'token_expire': str(self._token_expire) - } - @config.setter - def config(self, config: Dict[str, str]): - """ - Sets new API variables from config dictionary. - - This method calls init_config - to reconfigure the object - - :param config: Config dictionary - :type config: Dict[str, str] - """ - logger.info('Setting new API config') - self._init_config(config) - - @property - def _tokens(self) -> Tuple[str, str]: - """ - Returns access/refresh tokens as tuple. - - :return: Access and refresh tokens tuple - :rtype: Tuple[str, str] - """ - return self._access_token, self._refresh_token - - @_tokens.setter - def _tokens(self, tokens_data: Tuple[str, str]): - """ - Sets new access/refresh tokens from tuple. - - :param tokens_data: New access and refresh tokens tuple - :type tokens_data: Tuple[str, str] - """ - self._access_token = tokens_data[0] - self._refresh_token = tokens_data[1] - - @property - def _user_agent(self) -> Dict[str, str]: - """ - Returns current session User-Agent. + :param logging: Logging flag + :type logging: Optional[bool] + """ + if logging: + logger.configure(handlers=[ + { + 'sink': sys.stderr, + 'level': 'INFO', + 'format': '{time} | {level} | {message}' + }, + { + 'sink': 'shikithon_{time}.log', + 'level': 'DEBUG', + 'format': '{time} | {level} | ' + '{file}.{function}: {message}', + 'rotation': '1 MB', + 'compression': 'zip' + }, + ]) + if not logging: + logger.disable('shikithon') - :return: Session User-Agent - :rtype: Dict[str, str] - """ - return {'User-Agent': self._session.headers['User-Agent']} + logger.info('Initializing API object') - @_user_agent.setter - def _user_agent(self, app_name: str): - """ - Update session headers and set user agent. + self._client = Client(config) + + self.achievements = Achievements(self._client) + self.animes = Animes(self._client) + self.appears = Appears(self._client) + self.bans = Bans(self._client) + self.calendar = Calendar(self._client) + self.characters = Characters(self._client) + self.clubs = Clubs(self._client) + self.comments = Comments(self._client) + self.constants = Constants(self._client) + self.dialogs = Dialogs(self._client) + self.favorites = Favorites(self._client) + self.forums = Forums(self._client) + self.friends = Friends(self._client) + self.genres = Genres(self._client) + self.mangas = Mangas(self._client) + self.messages = Messages(self._client) + self.people = People(self._client) + self.publishers = Publishers(self._client) + self.ranobes = Ranobes(self._client) + self.stats = Stats(self._client) + self.studios = Studios(self._client) + self.styles = Styles(self._client) + self.topics = Topics(self._client) + self.user_images = UserImages(self._client) + self.user_rates = UserRates(self._client) + self.users = Users(self._client) + self.abuse_requests = AbuseRequests(self._client) - :param app_name: OAuth App name - :type app_name: str - """ - self._session.headers.update({'User-Agent': app_name}) + logger.info('Successfully initialized API object') @property - def _authorization_header(self) -> Dict[str, str]: - """ - Returns user agent and authorization token headers dictionary. - - Needed for accessing Shikimori protected resources - - :return: Dictionary with proper user agent and autorization token - :rtype: Dict[str, str] - """ - header = self._user_agent - header['Authorization'] = f'Bearer {self._access_token}' - return header - - @logger.catch(onerror=lambda _: sys.exit(1)) - def _init_config(self, config: Union[str, Dict[str, str]]): - """ - Special method for initializing an object. - - This method calls several methods: - - - Validation of config and variables - - Customizing the session header user agent - - Getting access/refresh tokens if they are missing - - Refresh current tokens if they are not valid - - Otherwise, if only app name is provided, setting it - - :param config: Config dictionary or app name - :type config: Union[str, Dict[str, str]] - """ - logger.debug('Initializing API config') - self._validate_config(config) - self._validate_vars() - logger.debug('Setting User-Agent with current app name') - self._user_agent = self._app_name - - if isinstance(config, dict) and not self._access_token: - logger.debug('No tokens found') - tokens_data = self._get_access_token() - self._update_tokens(tokens_data) - - if self.token_expired(): - logger.debug('Token has expired. Refreshing...') - self.refresh_tokens() - - @logger.catch(onerror=lambda _: sys.exit(1)) - def _validate_config(self, config: Union[str, Dict[str, str]]): - """ - Validates passed config dictionary and sets - API variables. - - If config is string, sets only app name and change value - of restrict mode of API object. - - Also, if config is dictionary and method detects - a cached configuration file, it replaces passed configuration - dictionary with the cached one. - - Raises MissingConfigData if some variables - are missing in config dictionary - - :param config: Config dictionary or app name for validation - :type config: Union[str, Dict[str, str]] - - :raises MissingConfigData: If any field is missing - (Not raises if there is a cache config) - """ - logger.debug('Validating API config') - if isinstance(config, str): - logger.debug('Detected app_name only. Switching to restricted mode') - self._app_name = config - self.restricted_mode = True - return - - try: - logger.debug('Checking for cached config') - cached_config, config_cached = ConfigCache.cache_config_validation( - config['app_name'], config['auth_code']) - - if config_cached: - logger.debug('Replacing passed config with cached one') - config = cached_config - - logger.debug('Extracting access tokens from config') - self._access_token = cached_config['access_token'] - self._refresh_token = cached_config['refresh_token'] - self._token_expire = int(cached_config['token_expire']) - - logger.debug('Extracting app related variables from config') - self._app_name = config['app_name'] - self._client_id = config['client_id'] - self._client_secret = config['client_secret'] - self._redirect_uri = config['redirect_uri'] - self._scopes = config['scopes'] - self._auth_code = config['auth_code'] - except KeyError as err: - raise MissingConfigData() from err - - @logger.catch(onerror=lambda _: sys.exit(1)) - def _validate_vars(self): - """ - Validates variables and throws exception - if some vars are set to empty string. - - **Note:** Why throwing exception without catching it? - - This decision was made in order to prevent - future problems with the API due to incorrect variables. - Raising exception at the beginning of initialization - immediately indicates errors with the configuration dictionary - and future unnecessary checks related to this variables - - Also some notes about this method: - - - If redirect URI set to empty string, set to default URI. - - If authorization code set to empty string, - returns URL for getting auth code. - - If restricted mode is True, stops validation after app name check. - - :raises MissingAppVariable: If app variable in config is missing - :raises MissingAuthCode: If auth code is set to empty string - """ - if not self._app_name: - raise MissingAppVariable('name') - - if self.restricted_mode: - return - - if not self._client_id: - raise MissingAppVariable('Client ID') - - if not self._client_secret: - raise MissingAppVariable('Client Secret') - - if not self._redirect_uri: - self._redirect_uri = DEFAULT_REDIRECT_URI - - if not self._scopes: - raise MissingAppVariable('scopes') - - if self._auth_code: - return - - auth_link = self._endpoints.authorization_link(self._client_id, - self._redirect_uri, - self._scopes) - raise MissingAuthCode(auth_link) - - @logger.catch(onerror=lambda _: sys.exit(1)) - def _get_access_token(self, refresh_token: bool = False) -> Tuple[str, str]: - """ - Returns access/refresh tokens from API request. - - If refresh_token flag is set to True, - returns refreshed access/refresh tokens. - - :param refresh_token: Flag for token refresh - :type refresh_token: bool - - :return: New access/refresh tokens tuple - :rtype: Tuple[str, str] - - :raises AccessTokenException: If token request failed - """ - data_body = { - 'client_id': self._client_id, - 'client_secret': self._client_secret - } - - if refresh_token: - logger.info('Refreshing current tokens') - data_body['grant_type'] = 'refresh_token' - data_body['refresh_token'] = self._refresh_token - else: - logger.info('Getting new tokens') - data_body['grant_type'] = 'authorization_code' - data_body['code'] = self._auth_code - data_body['redirect_uri'] = self._redirect_uri - - oauth_json: Dict[str, - Any] = self._request(self._endpoints.oauth_token, - data=data_body, - request_type=RequestType.POST, - output_logging=False) - - try: - logger.debug('Returning new access and refresh tokens') - return oauth_json['access_token'], oauth_json['refresh_token'] - except KeyError as err: - error_info = dumps(oauth_json) - raise AccessTokenException(error_info) from err - - @logger.catch(onerror=lambda _: sys.exit(1)) - def _update_tokens(self, tokens_data: Tuple[str, str]): - """ - Set new tokens and update token expire time. - - **Note:** This method also updates cache config file for - future use - - :param tokens_data: Tuple with access and refresh tokens - :type tokens_data: Tuple[str, str] - """ - logger.debug('Updating current tokens') - self._tokens = tokens_data - self._cache_config() - - @logger.catch(onerror=lambda _: sys.exit(1)) - def refresh_tokens(self): - """ - Manages tokens refreshing and caching. - - This method gets new access/refresh tokens and - updates them in current instance, as well as - caching new config. - """ - tokens_data = self._get_access_token(refresh_token=True) - self._update_tokens(tokens_data) - - def token_expired(self): - """ - Checks if current access token is expired. - - :return: Result of token expiration check - :rtype: bool - """ - logger.debug('Checking if current time is greater ' - 'than current token expire time') - return int(time()) > self._token_expire - - @logger.catch(onerror=lambda _: sys.exit(1)) - def _cache_config(self): - """Updates token expire time and caches new config.""" - self._token_expire = Utils.get_new_expire_time(TOKEN_EXPIRE_TIME) - ConfigCache.save_config(self.config) - logger.debug('New expiration time has been set ' - 'and cached configuration has been updated') - - @logger.catch - @sleep_and_retry - @limits(calls=MAX_CALLS_PER_MINUTE, period=ONE_MINUTE) - def _request( - self, - url: str, - data: Optional[Dict[str, str]] = None, - files: Optional[Dict[str, Tuple[str, bytes, str]]] = None, - headers: Optional[Dict[str, str]] = None, - query: Optional[Dict[str, str]] = None, - request_type: RequestType = RequestType.GET, - output_logging: bool = True, - ) -> Optional[Union[List[Dict[str, Any]], Dict[str, Any], str]]: - """ - Create request and return response JSON. - - This method uses ratelimit library for rate limiting - requests (Shikimori API limit: 90rpm) - - For 5rps limit, there is a check for 429 status code. - When triggered, halt request for 0.5 second and retry - - **Note:** To address duplication of methods - for different request methods, this method - uses RequestType enum - - :param url: URL for making request - :type url: str - - :param data: Request body data - :type data: Optional[Dict[str, str]] - - :param files: Binary data for request - :type files: Optional[Dict[str, Tuple[str, bytes, str]]] - - :param headers: Custom headers for request - :type headers: Optional[Dict[str, str]] + def closed(self) -> bool: + """Check if client is closed.""" + return self._client.closed - :param query: Query data for request - :type query: Optional[Dict[str, str]] - - :param request_type: Type of current request - :type request_type: RequestType - - :param output_logging: Parameter for logging JSON response - :type output_logging: bool - - :return: Response JSON, text or status code - :rtype: Optional[Union[List[Dict[str, Any]], Dict[str, Any], str]] - """ - - logger.info(f'{request_type.value} {url}') - if output_logging: - logger.debug( - f'Request info details: {data=}, {files=}, {headers=}, {query=}' - ) - - if request_type == RequestType.GET: - response = self._session.get(url, headers=headers, params=query) - elif request_type == RequestType.POST: - response = self._session.post(url, - files=files, - headers=headers, - params=query, - json=data) - elif request_type == RequestType.PUT: - response = self._session.put(url, - files=files, - headers=headers, - params=query, - json=data) - elif request_type == RequestType.PATCH: - response = self._session.patch(url, - files=files, - headers=headers, - params=query, - json=data) - elif request_type == RequestType.DELETE: - response = self._session.delete(url, - headers=headers, - params=query, - json=data) - else: - logger.debug('Unknown request_type. Returning None') - return None - - if response.status_code == ResponseCode.RETRY_LATER.value: - logger.debug('Hit RPS cooldown. Waiting on request repeat') - sleep(RATE_LIMIT_RPS_COOLDOWN) - return self._request(url, data, files, headers, query, request_type, - output_logging) - - try: - logger.debug('Extracting JSON from response') - json_response = response.json() - if output_logging: - logger.debug( - 'Successful extraction. ' - f'Here are the details of the response: {json_response}') - return json_response - except JSONDecodeError: - logger.debug('Can\'t extract JSON. Returning status_code/text') - return response.status_code if not response.text else response.text - - @logger.catch - def _semi_protected_method(self, api_name: str) -> Optional[Dict[str, str]]: - """ - This method utilizes protected method decoration logic - for such methods, which uses access tokens in some situations. - - :param api_name: Name of API endpoint for calling as protected - :type api_name: str - - :return: Authorization header with correct tokens or None - :rtype: Optional[Dict[str, str]] - """ - logger.debug(f'Checking the possibility of using "{api_name}" ' - f'as protected method') - - if self.restricted_mode: - logger.debug(f'It is not possible to use "{api_name}" ' - 'as the protected method ' - 'due to the restricted mode') - return None - - if self.token_expired(): - logger.debug('Token has expired. Refreshing...') - self.refresh_tokens() - - logger.debug('All checks for use of the protected ' - 'method have been passed') - return self._authorization_header - - @method_endpoint('/api/achievements') - def achievements(self, user_id: int) -> Optional[List[Achievement]]: - """ - Returns achievements of user by ID. - - :param user_id: User ID for getting achievements - :type user_id: int - - :return: List of achievements - :rtype: Optional[List[Achievement]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.achievements, - query=Utils.generate_query_dict(user_id=user_id)) - return Utils.validate_return_data(response, data_model=Achievement) - - @method_endpoint('/api/animes') - def animes(self, - page: Optional[int] = None, - limit: Optional[int] = None, - order: Optional[str] = None, - kind: Optional[Union[str, List[str]]] = None, - status: Optional[Union[str, List[str]]] = None, - season: Optional[Union[str, List[str]]] = None, - score: Optional[int] = None, - duration: Optional[Union[str, List[str]]] = None, - rating: Optional[Union[str, List[str]]] = None, - genre: Optional[Union[int, List[int]]] = None, - studio: Optional[Union[int, List[int]]] = None, - franchise: Optional[Union[int, List[int]]] = None, - censored: Optional[str] = None, - my_list: Optional[Union[str, List[str]]] = None, - ids: Optional[Union[int, List[int]]] = None, - exclude_ids: Optional[Union[int, List[int]]] = None, - search: Optional[str] = None) -> Optional[List[Anime]]: - """ - Returns animes list. - - :param page: Number of page - :type page: Optional[int] - - :param limit: Number of results limit - :type limit: Optional[int] - - :param order: Type of order in list - :type order: Optional[str] - - :param kind: Type(s) of anime topics - :type kind: Optional[Union[str, List[str]]] - - :param status: Type(s) of anime status - :type status: Optional[Union[str, List[str]]] - - :param season: Name(s) of anime seasons - :type season: Optional[Union[str, List[str]]] - - :param score: Minimal anime score - :type score: Optional[int] - - :param duration: Duration size(s) of anime - :type duration: Optional[Union[str, List[str]]] - - :param rating: Type of anime rating(s) - :type rating: Optional[Union[str, List[str]]] - - :param genre: Genre(s) ID - :type genre: Optional[Union[int, List[int]]] - - :param studio: Studio(s) ID - :type studio: Optional[Union[int, List[int]]] - - :param franchise: Franchise(s) ID - :type franchise: Optional[Union[int, List[int]]] - - :param censored: Type of anime censorship - :type censored: Optional[str] - - :param my_list: Status(-es) of anime in current user list - **Note:** If app is in restricted mode, - this parameter won't affect on response. - :type my_list: Optional[Union[str, List[str]]] - - :param ids: Anime(s) ID to include - :type ids: Optional[Union[int, List[int]]] - - :param exclude_ids: Anime(s) ID to exclude - :type exclude_ids: Optional[Union[int, List[int]]] - - :param search: Search phrase to filter animes by name - :type search: Optional[str] - - :return: Animes list - :rtype: Optional[List[Anime]] - """ - if not Utils.validate_enum_params({ - AnimeOrder: order, - AnimeKind: kind, - AnimeStatus: status, - AnimeDuration: duration, - AnimeRating: rating, - AnimeCensorship: censored, - AnimeList: my_list, - }): - return None - - validated_numbers = Utils.query_numbers_validator(page=[page, 100000], - limit=[limit, 50], - score=[score, 9]) - - headers = self._user_agent - - if my_list: - headers = self._semi_protected_method('/api/animes') - - response: List[Dict[str, Any]] = self._request( - self._endpoints.animes, - headers=headers, - query=Utils.generate_query_dict(page=validated_numbers['page'], - limit=validated_numbers['limit'], - order=order, - kind=kind, - status=status, - season=season, - score=validated_numbers['score'], - duration=duration, - rating=rating, - genre=genre, - studio=studio, - franchise=franchise, - censored=censored, - mylist=my_list, - ids=ids, - exclude_ids=exclude_ids, - search=search)) - return Utils.validate_return_data(response, data_model=Anime) - - @method_endpoint('/api/animes/:id') - def anime(self, anime_id: int) -> Optional[Anime]: - """ - Returns info about certain anime. - - :param anime_id: Anime ID to get info - :type anime_id: int - - :return: Anime info - :rtype: Optional[Anime] - """ - response: Dict[str, - Any] = self._request(self._endpoints.anime(anime_id)) - return Utils.validate_return_data(response, data_model=Anime) - - @method_endpoint('/api/animes/:id/roles') - def anime_creators(self, anime_id: int) -> Optional[List[Creator]]: - """ - Returns creators info of certain anime. - - :param anime_id: Anime ID to get creators - :type anime_id: int - - :return: List of anime creators - :rtype: Optional[List[Creator]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.anime_roles(anime_id)) - return Utils.validate_return_data(response, data_model=Creator) - - @method_endpoint('/api/animes/:id/similar') - def similar_animes(self, anime_id: int) -> Optional[List[Anime]]: - """ - Returns list of similar animes for certain anime. - - :param anime_id: Anime ID to get similar animes - :type anime_id: int - - :return: List of similar animes - :rtype: Optional[List[Anime]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.similar_animes(anime_id)) - return Utils.validate_return_data(response, data_model=Anime) - - @method_endpoint('/api/animes/:id/related') - def anime_related_content(self, anime_id: int) -> Optional[List[Relation]]: - """ - Returns list of related content of certain anime. - - :param anime_id: Anime ID to get related content - :type anime_id: int - - :return: List of relations - :rtype: Optional[List[Relation]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.anime_related_content(anime_id)) - return Utils.validate_return_data(response, data_model=Relation) - - @method_endpoint('/api/animes/:id/screenshots') - def anime_screenshots(self, anime_id: int) -> Optional[List[Screenshot]]: - """ - Returns list of screenshot links of certain anime. - - :param anime_id: Anime ID to get screenshot links - :type anime_id: int - - :return: List of screenshot links - :rtype: Optional[List[Screenshot]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.anime_screenshots(anime_id)) - return Utils.validate_return_data(response, data_model=Screenshot) - - @method_endpoint('/api/animes/:id/franchise') - def anime_franchise_tree(self, anime_id: int) -> Optional[FranchiseTree]: - """ - Returns franchise tree of certain anime. - - :param anime_id: Anime ID to get franchise tree - :type anime_id: int - - :return: Franchise tree of certain anime - :rtype: Optional[FranchiseTree] - """ - response: Dict[str, Any] = self._request( - self._endpoints.anime_franchise_tree(anime_id)) - return Utils.validate_return_data(response, data_model=FranchiseTree) - - @method_endpoint('/api/animes/:id/external_links') - def anime_external_links(self, anime_id: int) -> Optional[List[Link]]: - """ - Returns list of external links of certain anime. - - :param anime_id: Anime ID to get external links - :type anime_id: int - - :return: List of external links - :rtype: Optional[List[Link]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.anime_external_links(anime_id)) - return Utils.validate_return_data(response, data_model=Link) - - @method_endpoint('/api/animes/:id/topics') - def anime_topics(self, - anime_id: int, - page: Optional[int] = None, - limit: Optional[int] = None, - kind: Optional[str] = None, - episode: Optional[int] = None) -> Optional[List[Topic]]: - """ - Returns list of topics of certain anime. - - If some data are not provided, using default values. - - :param anime_id: Anime ID to get topics - :type anime_id: int - - :param page: Number of page - :type page: Optional[int] - - :param limit: Number of results limit - :type limit: Optional[int] - - :param kind: Kind of anime - :type kind: Optional[str] - - :param episode: Number of anime episode - :type episode: Optional[int] - - :return: List of topics - :rtype: Optional[List[Topic]] - """ - if not Utils.validate_enum_params({AnimeKind: kind}): - return None - - validated_numbers = Utils.query_numbers_validator(page=[page, 100000], - limit=[limit, 30]) - - response: List[Dict[str, Any]] = self._request( - self._endpoints.anime_topics(anime_id), - query=Utils.generate_query_dict(page=validated_numbers['page'], - limit=validated_numbers['limit'], - kind=kind, - episode=episode)) - return Utils.validate_return_data(response, data_model=Topic) - - @method_endpoint('/api/animes/:anime_id/videos') - def anime_videos(self, anime_id: int) -> Optional[List[Video]]: - """ - Returns anime videso. - - :param anime_id: Anime ID to get videos - :type anime_id: int - - :return: Anime videos list - :rtype: Optional[List[Video]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.anime_videos(anime_id)) - return Utils.validate_return_data(response, data_model=Video) - - @method_endpoint('/api/animes/:anime_id/videos') - @protected_method('content') - def create_anime_video(self, anime_id: int, kind: str, name: str, - url: str) -> Optional[Video]: - """ - Creates anime video. - - :param anime_id: Anime ID to create video - :type anime_id: int - - :param kind: Kind of video - :type kind: str - - :param name: Name of video - :type name: str - - :param url: URL of video - :type url: str - - :return: Created video info - :rtype: Optional[Video] - """ - if not Utils.validate_enum_params({VideoKind: kind}): - return None - - data_dict: Dict[str, Any] = Utils.generate_data_dict(dict_name='video', - kind=kind, - name=name, - url=url) - response: Dict[str, Any] = self._request( - self._endpoints.anime_videos(anime_id), - headers=self._authorization_header, - data=data_dict, - request_type=RequestType.POST) - return Utils.validate_return_data(response, data_model=Video) - - @method_endpoint('/api/animes/:anime_id/videos/:id') - @protected_method('content') - def delete_anime_video(self, anime_id: int, video_id: int) -> bool: - """ - Deletes anime video. - - :param anime_id: Anime ID to delete video - :type anime_id: int - - :param video_id: Video ID to delete - :type video_id: str - - :return: Status of video deletion - :rtype: bool - """ - response: Dict[str, - Any] = self._request(self._endpoints.anime_video( - anime_id, video_id), - headers=self._authorization_header, - request_type=RequestType.DELETE) - return Utils.validate_return_data(response) - - @method_endpoint('/api/appears') - @protected_method() - def appears(self, comment_ids: List[str]) -> bool: - """ - Marks comments or topics as read. - - This method uses generate_query_dict for data dict, - because there is no need for nested dictionary - - :param comment_ids: IDs of comments or topics to mark - :type comment_ids: List[str] - - :return: Status of mark - :rtype: bool - """ - response: Union[Dict[str, Any], int] = self._request( - self._endpoints.appears, - headers=self._authorization_header, - data=Utils.generate_query_dict(ids=comment_ids), - request_type=RequestType.POST) - return Utils.validate_return_data(response, - response_code=ResponseCode.SUCCESS) - - @method_endpoint('/api/bans') - def bans(self, - page: Optional[int] = None, - limit: Optional[int] = None) -> Optional[List[Ban]]: - """ - Returns list of recent bans on Shikimori. - - :param page: Number of page - :type page: Optional[int] - - :param limit: Number of results limit - :type limit: Optional[int] - - :return: List of recent bans - :rtype: Optional[List[Ban]] - """ - validated_numbers = Utils.query_numbers_validator( - page=[page, 100000], - limit=[limit, 30], - ) + async def multiple_requests( + self, + *requests: List[Callable[..., + RT]]) -> List[Union[BaseException, RT]]: + """Make multiple requests. - response: List[Dict[str, Any]] = self._request( - self._endpoints.bans_list, - query=Utils.generate_query_dict(page=validated_numbers['page'], - limit=validated_numbers['limit'])) - return Utils.validate_return_data(response, data_model=Ban) + :param requests: List of requests + :type requests: List[Callable[..., RT]] - @method_endpoint('/api/calendar') - def calendar( - self, - censored: Optional[str] = None) -> Optional[List[CalendarEvent]]: + :return: List of results + :rtype: List[Union[BaseException, RT]] """ - Returns current calendar events. + return await self._client.multiple_requests(*requests) - :param censored: Status of censorship for events - :type censored: Optional[str] + async def open(self) -> ShikimoriAPI: + """Open client and return self.""" + await self._client.open() + return self - :return: List of calendar events - :rtype: Optional[List[CalendarEvent]] - """ - if not Utils.validate_enum_params({AnimeCensorship: censored}): - return None + async def close(self) -> None: + """Close client.""" + await self._client.close() - response: List[Dict[str, Any]] = self._request( - self._endpoints.calendar, - query=Utils.generate_query_dict(censored=censored)) - return Utils.validate_return_data(response, data_model=CalendarEvent) + async def __aenter__(self) -> ShikimoriAPI: + """Async context manager entry point.""" + return await self.open() - @method_endpoint('/api/characters/:id') - def character(self, character_id: int) -> Optional[Character]: - """ - Returns character info by ID. - - :param character_id: ID of character to get info - :type character_id: int - - :return: Character info - :rtype: Optional[Character] - """ - response: Dict[str, Any] = self._request( - self._endpoints.character(character_id)) - return Utils.validate_return_data(response, data_model=Character) - - @method_endpoint('/api/characters/search') - def character_search(self, - search: Optional[str] = None - ) -> Optional[List[Character]]: - """ - Returns list of found characters. - - :param search: Search query for characters - :type search: Optional[str] - - :return: List of found characters - :rtype: Optional[List[Character]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.character_search, - query=Utils.generate_query_dict(search=search)) - return Utils.validate_return_data(response, data_model=Character) - - @method_endpoint('/api/clubs') - def clubs(self, - page: Optional[int] = None, - limit: Optional[int] = None, - search: Optional[str] = None) -> Optional[List[Club]]: - """ - Returns clubs list. - - :param page: Number of page - :type page: Optional[int] - - :param limit: Number of results limit - :type limit: Optional[int] - - :param search: Search phrase to filter clubs by name - :type search: Optional[str] - - :return: Clubs list - :rtype: Optional[List[Club]] - """ - validated_numbers = Utils.query_numbers_validator( - page=[page, 100000], - limit=[limit, 30], - ) - - response: List[Dict[str, Any]] = self._request( - self._endpoints.clubs, - query=Utils.generate_query_dict(page=validated_numbers['page'], - limit=validated_numbers['limit'], - search=search)) - return Utils.validate_return_data(response, data_model=Club) - - @method_endpoint('/api/clubs/:id') - def club(self, club_id: int) -> Optional[Club]: - """ - Returns info about club. - - :param club_id: Club ID to get info - :type club_id: int - - :return: Info about club - :rtype: Optional[Club] - """ - response: Dict[str, Any] = self._request(self._endpoints.club(club_id)) - return Utils.validate_return_data(response, data_model=Club) - - @method_endpoint('/api/clubs/:id') - @protected_method('clubs') - def club_update( - self, - club_id: int, - name: Optional[str] = None, - join_policy: Optional[str] = None, - description: Optional[str] = None, - display_images: Optional[bool] = None, - comment_policy: Optional[str] = None, - topic_policy: Optional[str] = None, - page_policy: Optional[str] = None, - image_upload_policy: Optional[str] = None, - is_censored: Optional[bool] = None, - anime_ids: Optional[List[int]] = None, - manga_ids: Optional[List[int]] = None, - ranobe_ids: Optional[List[int]] = None, - character_ids: Optional[List[int]] = None, - club_ids: Optional[List[int]] = None, - admin_ids: Optional[List[int]] = None, - collection_ids: Optional[List[int]] = None, - banned_user_ids: Optional[List[int]] = None) -> Optional[Club]: - """ - Update info/settings about/of club. - - :param club_id: Club ID to modify/update - :type club_id: int - - :param name: New name of club - :type name: Optional[str] - - :param description: New description of club - :type description: Optional[str] - - :param display_images: New display images status of club - :type display_images: Optional[bool] - - :param is_censored: New censored status of club - :type is_censored: Optional[bool] - - :param join_policy: New join policy of club - :type join_policy: Optional[str] - - :param comment_policy: New comment policy of club - :type comment_policy: Optional[str] - - :param topic_policy: New topic policy of club - :type topic_policy: Optional[str] - - :param page_policy: New page policy of club - :type page_policy: Optional[str] - - :param image_upload_policy: New image upload policy of club - :type image_upload_policy: Optional[str] - - :param anime_ids: New anime ids of club - :type anime_ids: Optional[List[int]] - - :param manga_ids: New manga ids of club - :type manga_ids: Optional[List[int]] - - :param ranobe_ids: New ranobe ids of club - :type ranobe_ids: Optional[List[int]] - - :param character_ids: New character ids of club - :type character_ids: Optional[List[int]] - - :param club_ids: New club ids of club - :type club_ids: Optional[List[int]] - - :param admin_ids: New admin ids of club - :type admin_ids: Optional[List[int]] - - :param collection_ids: New collection ids of club - :type collection_ids: Optional[List[int]] - - :param banned_user_ids: New banned user ids of club - :type banned_user_ids: Optional[List[int]] - - :return: Updated club info - :rtype: Optional[Club] - """ - if not Utils.validate_enum_params({ - JoinPolicy: join_policy, - CommentPolicy: comment_policy, - TopicPolicy: topic_policy, - PagePolicy: page_policy, - ImageUploadPolicy: image_upload_policy - }): - return None - - response: Dict[str, Any] = self._request( - self._endpoints.club(club_id), - headers=self._authorization_header, - data=Utils.generate_data_dict( - dict_name='club', - name=name, - join_policy=join_policy, - description=description, - display_images=display_images, - comment_policy=comment_policy, - topic_policy=topic_policy, - page_policy=page_policy, - image_upload_policy=image_upload_policy, - is_censored=is_censored, - anime_ids=anime_ids, - manga_ids=manga_ids, - ranobe_ids=ranobe_ids, - character_ids=character_ids, - club_ids=club_ids, - admin_ids=admin_ids, - collection_ids=collection_ids, - banned_user_ids=banned_user_ids), - request_type=RequestType.PATCH) - return Utils.validate_return_data(response, data_model=Club) - - @method_endpoint('/api/clubs/:id/animes') - def club_animes(self, club_id: int) -> Optional[List[Anime]]: - """ - Returns anime list of club. - - :param club_id: Club ID to get anime list - :type club_id: int - - :return: Club anime list - :rtype: Optional[List[Anime]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.club_animes(club_id)) - return Utils.validate_return_data(response, data_model=Anime) - - @method_endpoint('/api/clubs/:id/mangas') - def club_mangas(self, club_id: int) -> Optional[List[Manga]]: - """ - Returns manga list of club. - - :param club_id: Club ID to get manga list - :type club_id: int - - :return: Club manga list - :rtype: Optional[List[Manga]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.club_mangas(club_id)) - return Utils.validate_return_data(response, data_model=Manga) - - @method_endpoint('/api/clubs/:id/ranobe') - def club_ranobe(self, club_id: int) -> Optional[List[Ranobe]]: - """ - Returns ranobe list of club. - - :param club_id: Club ID to get ranobe list - :type club_id: int - - :return: Club ranobe list - :rtype: Optional[List[Ranobe]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.club_ranobe(club_id)) - return Utils.validate_return_data(response, data_model=Ranobe) - - @method_endpoint('/api/clubs/:id/characters') - def club_characters(self, club_id: int) -> Optional[List[Character]]: - """ - Returns character list of club. - - :param club_id: Club ID to get character list - :type club_id: int - - :return: Club character list - :rtype: Optional[List[Character]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.club_characters(club_id)) - return Utils.validate_return_data(response, data_model=Character) - - @method_endpoint('/api/clubs/:id/members') - def club_members(self, club_id: int) -> Optional[List[User]]: - """ - Returns member list of club. - - :param club_id: Club ID to get member list - :type club_id: int - - :return: Club member list - :rtype: Optional[List[User]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.club_members(club_id)) - return Utils.validate_return_data(response, data_model=User) - - @method_endpoint('/api/clubs/:id/images') - def club_images(self, club_id: int) -> Optional[List[ClubImage]]: - """ - Returns images of club. - - :param club_id: Club ID to get images - :type club_id: int - - :return: Club's images - :rtype: Optional[List[ClubImage]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.club_images(club_id)) - return Utils.validate_return_data(response, data_model=ClubImage) - - @method_endpoint('/api/clubs/:id/join') - @protected_method('clubs') - def club_join(self, club_id: int): - """ - Joins club by ID. - - :param club_id: Club ID to join - :type club_id: int - - :return: Status of join - :rtype: bool - """ - response: Union[Dict[str, Any], - int] = self._request(self._endpoints.club_join(club_id), - headers=self._authorization_header, - request_type=RequestType.POST) - return Utils.validate_return_data(response) - - @method_endpoint('/api/clubs/:id/leave') - @protected_method('clubs') - def club_leave(self, club_id: int) -> bool: - """ - Leaves club by ID. - - :param club_id: Club ID to leave - :type club_id: int - - :return: Status of leave - :rtype: bool - """ - response: Union[Dict[str, Any], int] = self._request( - self._endpoints.club_leave(club_id), - headers=self._authorization_header, - request_type=RequestType.POST) - return Utils.validate_return_data(response) - - @method_endpoint('/api/comments') - def comments(self, - commentable_id: int, - commentable_type: str, - page: Optional[int] = None, - limit: Optional[int] = None, - desc: Optional[int] = None) -> Optional[List[Comment]]: - """ - Returns list of comments. - - :param commentable_id: ID of entity to get comment - :type commentable_id: int - - :param commentable_type: Type of entity to get comment - :type commentable_type: str - - :param page: Number of page - :type page: Optional[int] - - :param limit: Number of results limit - :type limit: Optional[int] - - :param desc: Status of description in request. Can be 1 or 0 - :type desc: Optional[int] - - :return: List of comments - :rtype: Optional[List[Comment]] - """ - if not Utils.validate_enum_params({CommentableType: commentable_type}): - return None - - validated_numbers = Utils.query_numbers_validator( - page=[page, 100000], - limit=[limit, 30], - ) - - response: List[Dict[str, Any]] = self._request( - self._endpoints.comments, - query=Utils.generate_query_dict(page=validated_numbers['page'], - limit=validated_numbers['limit'], - commentable_id=commentable_id, - commentable_type=commentable_type, - desc=desc)) - return Utils.validate_return_data(response, data_model=Comment) - - @method_endpoint('/api/comments/:id') - def comment(self, comment_id: int) -> Optional[Comment]: - """ - Returns comment info. - - :param comment_id: ID of comment - :type comment_id: int - - :return: Comment info - :rtype: Optional[Comment] - """ - response: Dict[str, - Any] = self._request(self._endpoints.comment(comment_id)) - return Utils.validate_return_data(response, data_model=Comment) - - @method_endpoint('/api/comments') - @protected_method('comments') - def create_comment(self, - body: str, - commentable_id: int, - commentable_type: str, - is_offtopic: Optional[bool] = None, - broadcast: Optional[bool] = None) -> Optional[Comment]: - """ - Creates comment. - - When commentable_type set to Anime, Manga, Character or Person, - comment is attached to commentable main topic. - - :param body: Body of comment - :type body: str - - :param commentable_id: ID of entity to comment on - :type commentable_id: int - - :param commentable_type: Type of entity to comment on - :type commentable_type: str - - :param is_offtopic: Status of offtopic - :type is_offtopic: Optional[bool] - - :param broadcast: Broadcast comment in club’s topic status - :type broadcast: Optional[bool] - - :return: Created comment info - :rtype: Optional[Comment] - """ - if not Utils.validate_enum_params({CommentableType: commentable_type}): - return None - - data_dict: Dict[str, Any] = Utils.generate_data_dict( - dict_name='comment', - body=body, - commentable_id=commentable_id, - commentable_type=commentable_type, - is_offtopic=is_offtopic) - - if broadcast: - logger.debug('Adding a broadcast value to a data_dict') - data_dict['broadcast'] = broadcast - - response: Dict[str, - Any] = self._request(self._endpoints.comments, - headers=self._authorization_header, - data=data_dict, - request_type=RequestType.POST) - return Utils.validate_return_data(response, data_model=Comment) - - @method_endpoint('/api/comments/:id') - @protected_method('comments') - def update_comment(self, comment_id: int, body: str) -> Optional[Comment]: - """ - Updates comment. - - :param comment_id: ID of comment to update - :type comment_id: int - - :param body: New body of comment - :type body: str - - :return: Updated comment info - :rtype: Optional[Comment] - """ - response: Dict[str, Any] = self._request( - self._endpoints.comment(comment_id), - headers=self._authorization_header, - data=Utils.generate_data_dict(dict_name='comment', body=body), - request_type=RequestType.PATCH) - return Utils.validate_return_data(response, data_model=Comment) - - @method_endpoint('/api/comments/:id') - @protected_method('comments') - def delete_comment(self, comment_id: int) -> bool: - """ - Deletes comment. - - :param comment_id: ID of comment to delete - :type comment_id: int - - :return: Status of comment deletion - :rtype: bool - """ - response: Dict[str, - Any] = self._request(self._endpoints.comment(comment_id), - headers=self._authorization_header, - request_type=RequestType.DELETE) - return Utils.validate_return_data(response) - - @method_endpoint('/api/constants/anime') - def anime_constants(self) -> Optional[AnimeConstants]: - """ - Returns anime constants values. - - :return: Anime constants values - :rtype: Optional[AnimeConstants] - """ - response: Dict[str, - Any] = self._request(self._endpoints.anime_constants) - return Utils.validate_return_data(response, data_model=AnimeConstants) - - @method_endpoint('/api/constants/manga') - def manga_constants(self) -> Optional[MangaConstants]: - """ - Returns manga constants values. - - :return: Manga constants values - :rtype: Optional[MangaConstants] - """ - response: Dict[str, - Any] = self._request(self._endpoints.manga_constants) - return Utils.validate_return_data(response, data_model=MangaConstants) - - @method_endpoint('/api/constants/user_rate') - def user_rate_constants(self) -> Optional[UserRateConstants]: - """ - Returns user rate constants values. - - :return: User rate constants values - :rtype: Optional[UserRateConstants] - """ - response: Dict[str, - Any] = self._request(self._endpoints.user_rate_constants) - return Utils.validate_return_data(response, - data_model=UserRateConstants) - - @method_endpoint('/api/constants/club') - def club_constants(self) -> Optional[ClubConstants]: - """ - Returns club constants values. - - :return: Club constants values - :rtype: Optional[ClubConstants] - """ - response: Dict[str, Any] = self._request(self._endpoints.club_constants) - return Utils.validate_return_data(response, data_model=ClubConstants) - - @method_endpoint('/api/constants/smileys') - def smileys_constants(self) -> Optional[List[SmileyConstants]]: - """ - Returns list of smileys constants values. - - :return: List of smileys constants values - :rtype: Optional[List[SmileyConstants]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.smileys_constants) - return Utils.validate_return_data(response, data_model=SmileyConstants) - - @method_endpoint('/api/dialogs') - @protected_method('messages') - def dialogs(self) -> Optional[List[Dialog]]: - """ - Returns list of current user's dialogs. - - :return: List of dialogs - :rtype: Optional[List[Dialog]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.dialogs, headers=self._authorization_header) - return Utils.validate_return_data(response, data_model=Dialog) - - @method_endpoint('/api/dialogs/:id') - @protected_method('messages') - def dialog(self, user_id: Union[int, str]) -> Optional[List[Message]]: - """ - Returns list of current user's messages with certain user. - - :param user_id: ID/Nickname of the user to get dialog - :type user_id: Union[int, str] - - :return: List of messages - :rtype: Optional[List[Message]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.dialog(user_id), headers=self._authorization_header) - return Utils.validate_return_data(response, data_model=Message) - - @method_endpoint('/api/dialogs/:id') - @protected_method('messages') - def delete_dialog(self, user_id: Union[int, str]) -> bool: - """ - Deletes dialog of current user with certain user. - - :param user_id: ID/Nickname of the user to delete dialog - :type user_id: Union[int, str] - - :return: Status of message deletion - :rtype: bool - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.dialog(user_id), - headers=self._authorization_header, - request_type=RequestType.DELETE) - return Utils.validate_return_data(response) - - @method_endpoint('/api/favorites/:linked_type/:linked_id(/:kind)') - @protected_method() - def create_favorite(self, - linked_type: str, - linked_id: int, - kind: str = PersonKind.NONE.value) -> bool: - """ - Creates a favorite. - - :param linked_type: Type of object for making favorite - :type linked_type: str - - :param linked_id: ID of linked type - :type linked_id: int - - :param kind: Kind of linked type - (Required when linked_type is 'Person') - :type kind: str - - :return: Status of favorite create - :rtype: bool - """ - if not Utils.validate_enum_params({ - FavoriteLinkedType: linked_type, - PersonKind: kind - }): - return False - - response: Dict[str, - Any] = self._request(self._endpoints.favorites_create( - linked_type, linked_id, kind), - headers=self._authorization_header, - request_type=RequestType.POST) - return Utils.validate_return_data(response) - - @method_endpoint('/api/favorites/:linked_type/:linked_id') - @protected_method() - def destroy_favorite(self, linked_type: str, linked_id: int) -> bool: - """ - Destroys a favorite. - - :param linked_type: Type of object for destroying from favorite - :type linked_type: str - - :param linked_id: ID of linked type - :type linked_id: int - - :return: Status of favorite destroy - :rtype: bool - """ - if not Utils.validate_enum_params({FavoriteLinkedType: linked_type}): - return False - - response: Dict[str, - Any] = self._request(self._endpoints.favorites_destroy( - linked_type, linked_id), - headers=self._authorization_header, - request_type=RequestType.DELETE) - return Utils.validate_return_data(response) - - @method_endpoint('/api/favorites/:id/reorder') - @protected_method() - def reorder_favorite(self, - favorite_id: int, - new_index: Optional[int] = None) -> bool: - """ - Reorders a favorite to the new index. - - :param favorite_id: ID of a favorite to reorder - :type favorite_id: int - - :param new_index: Index of a new position of favorite - :type new_index: Optional[int] - - :return: Status of reorder - :rtype: bool - """ - response: Union[Dict[str, Any], int] = self._request( - self._endpoints.favorites_reorder(favorite_id), - headers=self._authorization_header, - query=Utils.generate_query_dict(new_index=new_index), - request_type=RequestType.POST) - return Utils.validate_return_data(response, - response_code=ResponseCode.SUCCESS) - - @method_endpoint('/api/forums') - def forums(self) -> Optional[List[Forum]]: - """ - Returns list of forums. - - :returns: List of forums - :rtype: Optional[List[Forum]] - """ - response: List[Dict[str, Any]] = self._request(self._endpoints.forums) - return Utils.validate_return_data(response, data_model=Forum) - - @method_endpoint('/api/friends/:id') - @protected_method('friends') - def create_friend(self, friend_id: int): - """ - Creates (adds) new friend by ID. - - :param friend_id: ID of a friend to create (add) - :type friend_id: int - - :return: Status of create (addition) - :rtype: bool - """ - response: Union[Dict[str, Any], - int] = self._request(self._endpoints.friend(friend_id), - headers=self._authorization_header, - request_type=RequestType.POST) - return Utils.validate_return_data(response) - - @method_endpoint('/api/friends/:id') - @protected_method('friends') - def destroy_friend(self, friend_id: int): - """ - Destroys (removes) current friend by ID. - - :param friend_id: ID of a friend to destroy (remove) - :type friend_id: int - - :return: Status of destroy (removal) - :rtype: bool - """ - response: Union[Dict[str, Any], - int] = self._request(self._endpoints.friend(friend_id), - headers=self._authorization_header, - request_type=RequestType.DELETE) - return Utils.validate_return_data(response) - - @method_endpoint('/api/genres') - def genres(self) -> Optional[List[Genre]]: - """ - Returns list of genres. - - :return: List of genres - :rtype: Optional[List[Genre]] - """ - response: List[Dict[str, Any]] = self._request(self._endpoints.genres) - return Utils.validate_return_data(response, data_model=Genre) - - @method_endpoint('/api/mangas') - def mangas(self, - page: Optional[int] = None, - limit: Optional[int] = None, - order: Optional[str] = None, - kind: Optional[Union[str, List[str]]] = None, - status: Optional[Union[str, List[str]]] = None, - season: Optional[Union[str, List[str]]] = None, - score: Optional[int] = None, - genre: Optional[Union[int, List[int]]] = None, - publisher: Optional[Union[int, List[int]]] = None, - franchise: Optional[Union[int, List[int]]] = None, - censored: Optional[str] = None, - my_list: Optional[Union[str, List[str]]] = None, - ids: Optional[Union[int, List[int]]] = None, - exclude_ids: Optional[Union[int, List[int]]] = None, - search: Optional[str] = None) -> Optional[List[Manga]]: - """ - Returns mangas list. - - :param page: Number of page - :type page: Optional[int] - - :param limit: Number of results limit - :type limit: Optional[int] - - :param order: Type of order in list - :type order: Optional[str] - - :param kind: Type(s) of manga topic - :type kind: Optional[Union[str, List[str]] - - :param status: Type(s) of manga status - :type status: Optional[Union[str, List[str]]] - - :param season: Name(s) of manga seasons - :type season: Optional[Union[str, List[str]]] - - :param score: Minimal manga score - :type score: Optional[int] - - :param publisher: Publisher(s) ID - :type publisher: Optional[Union[int, List[int]] - - :param genre: Genre(s) ID - :type genre: Optional[Union[int, List[int]] - - :param franchise: Franchise(s) ID - :type franchise: Optional[Union[int, List[int]] - - :param censored: Type of manga censorship - :type censored: Optional[str] - - :param my_list: Status(-es) of manga in current user list - **Note:** If app in restricted mode, - this won't affect on response. - :type my_list: Optional[Union[str, List[str]]] - - :param ids: Manga(s) ID to include - :type ids: Optional[Union[int, List[int]] - - :param exclude_ids: Manga(s) ID to exclude - :type exclude_ids: Optional[Union[int, List[int]] - - :param search: Search phrase to filter mangas by name - :type search: Optional[str] - - :return: List of Mangas - :rtype: Optional[List[Manga]] - """ - if not Utils.validate_enum_params({ - MangaOrder: order, - MangaKind: kind, - MangaStatus: status, - MangaCensorship: censored, - MangaList: my_list - }): - return None - - validated_numbers = Utils.query_numbers_validator(page=[page, 100000], - limit=[limit, 50], - score=[score, 9]) - - headers: Dict[str, str] = self._user_agent - - if my_list: - headers = self._semi_protected_method('/api/mangas') - - response: List[Dict[str, Any]] = self._request( - self._endpoints.mangas, - headers=headers, - query=Utils.generate_query_dict(page=validated_numbers['page'], - limit=validated_numbers['limit'], - order=order, - kind=kind, - status=status, - season=season, - score=validated_numbers['score'], - genre=genre, - publisher=publisher, - franchise=franchise, - censored=censored, - mylist=my_list, - ids=ids, - exclude_ids=exclude_ids, - search=search)) - return Utils.validate_return_data(response, data_model=Manga) - - @method_endpoint('/api/mangas/:id') - def manga(self, manga_id: int) -> Optional[Manga]: - """ - Returns info about certain manga. - - :param manga_id: Manga ID to get info - :type manga_id: int - - :return: Manga info - :rtype: Optional[Manga] - """ - response: Dict[str, - Any] = self._request(self._endpoints.manga(manga_id)) - return Utils.validate_return_data(response, data_model=Manga) - - @method_endpoint('/api/mangas/:id/roles') - def manga_creators(self, manga_id: int) -> Optional[List[Creator]]: - """ - Returns creators info of certain manga. - - :param manga_id: Manga ID to get creators - :type manga_id: int - - :return: List of manga creators - :rtype: Optional[List[Creator]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.manga_roles(manga_id)) - return Utils.validate_return_data(response, data_model=Creator) - - @method_endpoint('/api/mangas/:id/similar') - def similar_mangas(self, manga_id: int) -> Optional[List[Manga]]: - """ - Returns list of similar mangas for certain manga. - - :param manga_id: Manga ID to get similar mangas - :type manga_id: int - - :return: List of similar mangas - :rtype: Optional[List[Manga]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.similar_mangas(manga_id)) - return Utils.validate_return_data(response, data_model=Manga) - - @method_endpoint('/api/mangas/:id/related') - def manga_related_content(self, manga_id: int) -> Optional[List[Relation]]: - """ - Returns list of related content of certain manga. - - :param manga_id: Manga ID to get related content - :type manga_id: int - - :return: List of relations - :rtype: Optional[List[Relation]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.manga_related_content(manga_id)) - return Utils.validate_return_data(response, data_model=Relation) - - @method_endpoint('/api/mangas/:id/franchise') - def manga_franchise_tree(self, manga_id: int) -> Optional[FranchiseTree]: - """ - Returns franchise tree of certain manga. - - :param manga_id: Manga ID to get franchise tree - :type manga_id: int - - :return: Franchise tree of certain manga - :rtype: Optional[FranchiseTree] - """ - response: Dict[str, Any] = self._request( - self._endpoints.manga_franchise_tree(manga_id)) - return Utils.validate_return_data(response, data_model=FranchiseTree) - - @method_endpoint('/api/mangas/:id/external_links') - def manga_external_links(self, manga_id: int) -> Optional[List[Link]]: - """ - Returns list of external links of certain manga. - - :param manga_id: Manga ID to get external links - :type manga_id: int - - :return: List of external links - :rtype: Optional[List[Link]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.manga_external_links(manga_id)) - return Utils.validate_return_data(response, data_model=Link) - - @method_endpoint('/api/mangas/:id/topics') - def manga_topics(self, - manga_id: int, - page: Optional[int] = None, - limit: Optional[int] = None) -> Optional[List[Topic]]: - """ - Returns list of topics of certain manga. - - If some data are not provided, using default values. - - :param manga_id: Manga ID to get topics - :type manga_id: int - - :param page: Number of page - :type page: Optional[int] - - :param limit: Number of results limit - :type limit: Optional[int] - - :return: List of topics - :rtype: Optional[List[Topic]] - """ - validated_numbers = Utils.query_numbers_validator( - page=[page, 100000], - limit=[limit, 30], - ) - - response: List[Dict[str, Any]] = self._request( - self._endpoints.manga_topics(manga_id), - query=Utils.generate_query_dict(page=validated_numbers['page'], - limit=validated_numbers['limit'])) - return Utils.validate_return_data(response, data_model=Topic) - - @method_endpoint('/api/messages/:id') - @protected_method('messages') - def message(self, message_id: int) -> Optional[Message]: - """ - Returns message info. - - :param message_id: ID of message to get info - :type message_id: int - - :return: Message info - :rtype: Optional[Message] - """ - response: Dict[str, - Any] = self._request(self._endpoints.message(message_id), - headers=self._authorization_header) - return Utils.validate_return_data(response, data_model=Message) - - @method_endpoint('/api/messages') - @protected_method('messages') - def create_message(self, body: str, from_id: int, - to_id: int) -> Optional[Message]: - """ - Creates message. - - :param body: Body of message - :type body: str - - :param from_id: Sender ID - :type from_id: int - - :param to_id: Reciver ID - :type to_id: int - - :return: Created message info - :rtype: Optional[Message] - """ - response: Dict[str, Any] = self._request( - self._endpoints.messages, - headers=self._authorization_header, - data=Utils.generate_data_dict(dict_name='message', - body=body, - from_id=from_id, - kind='Private', - to_id=to_id), - request_type=RequestType.POST) - return Utils.validate_return_data(response, data_model=Message) - - @method_endpoint('/api/messages/:id') - @protected_method('messages') - def update_message(self, message_id: int, body: str) -> Optional[Message]: - """ - Updates message. - - :param message_id: ID of message to update - :type message_id: int - - :param body: New body of message - :type body: str - - :return: Updated message info - :rtype: Optional[Message] - """ - response: Dict[str, Any] = self._request( - self._endpoints.message(message_id), - headers=self._authorization_header, - data=Utils.generate_data_dict(dict_name='message', body=body), - request_type=RequestType.PATCH) - return Utils.validate_return_data(response, data_model=Message) - - @method_endpoint('/api/messages/:id') - @protected_method('messages') - def delete_message(self, message_id: int) -> bool: - """ - Deletes message. - - :param message_id: ID of message to delete - :type message_id: int - - :return: Status of message deletion - :rtype: bool - """ - response: Union[Dict[str, Any], int] = self._request( - self._endpoints.message(message_id), - headers=self._authorization_header, - request_type=RequestType.DELETE) - return Utils.validate_return_data(response, - response_code=ResponseCode.NO_CONTENT) - - @method_endpoint('/api/messages/mark_read') - @protected_method('messages') - def mark_messages_read(self, - message_ids: Optional[Union[int, List[int]]] = None, - is_read: Optional[bool] = None) -> bool: - """ - Marks read/unread selected messages. - - This method uses generate_query_dict for data dict, - because there is no need for nested dictionary - - :param message_ids: ID(s) of messages to mark read/unread - :type message_ids: Optional[Union[int, List[int]]] - - :param is_read: Status of message (read/unread) - :type is_read: Optional[bool] - - :return: Status of messages read/unread - :rtype: bool - """ - response: Union[Dict[str, Any], int] = self._request( - self._endpoints.messages_mark_read, - headers=self._authorization_header, - data=Utils.generate_query_dict(ids=message_ids, is_read=is_read), - request_type=RequestType.POST) - return Utils.validate_return_data(response, - response_code=ResponseCode.SUCCESS) - - @method_endpoint('/api/messages/read_all') - @protected_method('messages') - def read_all_messages(self, message_type: str) -> bool: - """ - Reads all messages on current user's account. - - This method uses generate_query_dict for data dict, - because there is no need for nested dictionary - - **Note:** This methods accepts as type only 'news' and - 'notifications' - - :param message_type: Type of messages to read - :type message_type: str - - :return: Status of messages read - :rtype: bool - """ - if not Utils.validate_enum_params({MessageType: message_type}): - return False - - response: Union[Dict[str, Any], int] = self._request( - self._endpoints.messages_read_all, - headers=self._authorization_header, - data=Utils.generate_query_dict(type=message_type), - request_type=RequestType.POST) - return Utils.validate_return_data(response, - response_code=ResponseCode.SUCCESS) - - @method_endpoint('/api/messages/delete_all') - @protected_method('messages') - def delete_all_messages(self, message_type: str) -> bool: - """ - Deletes all messages on current user's account. - - This method uses generate_query_dict for data dict, - because there is no need for nested dictionary - - **Note:** This methods accepts as type only 'news' and - 'notifications' - - :param message_type: Type of messages to delete - :type message_type: str - - :return: Status of messages deletion - :rtype: bool - """ - if not Utils.validate_enum_params({MessageType: message_type}): - return False - - response: Union[Dict[str, Any], int] = self._request( - self._endpoints.messages_delete_all, - headers=self._authorization_header, - data=Utils.generate_query_dict(type=message_type), - request_type=RequestType.POST) - return Utils.validate_return_data(response, - response_code=ResponseCode.SUCCESS) - - @method_endpoint('/api/people/:id') - def people(self, people_id: int) -> Optional[People]: - """ - Returns info about a person. - - :param people_id: ID of person to get info - :type people_id: int - - :return: Info about a person - :rtype: Optional[People] - """ - response: Dict[str, - Any] = self._request(self._endpoints.people(people_id)) - return Utils.validate_return_data(response, data_model=People) - - @method_endpoint('/api/people/search') - def people_search( - self, - search: Optional[str] = None, - people_kind: Optional[str] = None) -> Optional[List[People]]: - """ - Returns list of found persons. - - **Note:** This API method only allows 'seyu', - 'mangaka' or 'producer' as kind parameter - - :param search: Search query for persons - :type search: Optional[str] - - :param people_kind: Kind of person for searching - :type people_kind: Optional[str] - - :return: List of found persons - :rtype: Optional[List[People]] - """ - if not Utils.validate_enum_params({PersonKind: people_kind}): - return None - - response: List[Dict[str, Any]] = self._request( - self._endpoints.people_search, - query=Utils.generate_query_dict(search=search, kind=people_kind)) - return Utils.validate_return_data(response, data_model=People) - - @method_endpoint('/api/publishers') - def publishers(self) -> Optional[List[Publisher]]: - """ - Returns list of publishers. - - :return: List of publishers - :rtype: Optional[List[Publisher]] - """ - response: List[Dict[str, - Any]] = self._request(self._endpoints.publishers) - return Utils.validate_return_data(response, data_model=Publisher) - - @method_endpoint('/api/ranobe') - def ranobes(self, - page: Optional[int] = None, - limit: Optional[int] = None, - order: Optional[str] = None, - status: Optional[Union[str, List[str]]] = None, - season: Optional[Union[str, List[str]]] = None, - score: Optional[int] = None, - genre: Optional[Union[int, List[int]]] = None, - publisher: Optional[Union[int, List[int]]] = None, - franchise: Optional[Union[int, List[int]]] = None, - censored: Optional[str] = None, - my_list: Optional[Union[str, List[str]]] = None, - ids: Optional[Union[int, List[int]]] = None, - exclude_ids: Optional[Union[int, List[int]]] = None, - search: Optional[str] = None) -> Optional[List[Ranobe]]: - """ - Returns ranobe list. - - :param page: Number of page - :type page: Optional[int] - - :param limit: Number of results limit - :type limit: Optional[int] - - :param order: Type of order in list - :type order: Optional[str] - - :param status: Type(s) of ranobe status - :type status: Optional[Union[str, List[str]]] - - :param season: Name(s) of ranobe seasons - :type season: Optional[Union[str, List[str]]] - - :param score: Minimal ranobe score - :type score: Optional[int] - - :param publisher: Publisher(s) ID - :type publisher: Optional[Union[int, List[int]] - - :param genre: Genre(s) ID - :type genre: Optional[Union[int, List[int]] - - :param franchise: Franchise(s) ID - :type franchise: Optional[Union[int, List[int]] - - :param censored: Type of ranobe censorship - :type censored: Optional[str] - - :param my_list: Status(-es) of ranobe in current user list - **Note:** If app in restricted mode, - this won't affect on response. - :type my_list: Optional[Union[str, List[str]]] - - :param ids: Ranobe(s) ID to include - :type ids: Optional[Union[int, List[int]] - - :param exclude_ids: Ranobe(s) ID to exclude - :type exclude_ids: Optional[Union[int, List[int]] - - :param search: Search phrase to filter ranobe by name - :type search: Optional[str] - - :return: List of Ranobe - :rtype: Optional[List[Ranobe]] - """ - if not Utils.validate_enum_params({ - RanobeOrder: order, - RanobeStatus: status, - RanobeList: my_list, - RanobeCensorship: censored - }): - return None - - validated_numbers = Utils.query_numbers_validator(page=[page, 100000], - limit=[limit, 50], - score=[score, 9]) - - headers: Dict[str, str] = self._user_agent - - if my_list: - headers = self._semi_protected_method('/api/ranobe') - - response: List[Dict[str, Any]] = self._request( - self._endpoints.ranobes, - headers=headers, - query=Utils.generate_query_dict(page=validated_numbers['page'], - limit=validated_numbers['limit'], - order=order, - status=status, - season=season, - score=validated_numbers['score'], - genre=genre, - publisher=publisher, - franchise=franchise, - censored=censored, - mylist=my_list, - ids=ids, - exclude_ids=exclude_ids, - search=search)) - return Utils.validate_return_data(response, data_model=Ranobe) - - @method_endpoint('/api/ranobe/:id') - def ranobe(self, ranobe_id: int) -> Optional[Ranobe]: - """ - Returns info about certain ranobe. - - :param ranobe_id: Ranobe ID to get info - :type ranobe_id: int - - :return: Ranobe info - :rtype: Optional[Ranobe] - """ - response: Dict[str, - Any] = self._request(self._endpoints.ranobe(ranobe_id)) - return Utils.validate_return_data(response, data_model=Ranobe) - - @method_endpoint('/api/ranobe/:id/roles') - def ranobe_creators(self, ranobe_id: int) -> Optional[List[Creator]]: - """ - Returns creators info of certain ranobe. - - :param ranobe_id: Ranobe ID to get creators - :type ranobe_id: int - - :return: List of ranobe creators - :rtype: Optional[List[Creator]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.ranobe_roles(ranobe_id)) - return Utils.validate_return_data(response, data_model=Creator) - - @method_endpoint('/api/ranobe/:id/similar') - def similar_ranobes(self, ranobe_id: int) -> Optional[List[Ranobe]]: - """ - Returns list of similar ranobes for certain ranobe. - - :param ranobe_id: Ranobe ID to get similar ranobes - :type ranobe_id: int - - :return: List of similar ranobes - :rtype: Optional[List[Ranobe]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.similar_ranobes(ranobe_id)) - return Utils.validate_return_data(response, data_model=Ranobe) - - @method_endpoint('/api/ranobe/:id/related') - def ranobe_related_content(self, - ranobe_id: int) -> Optional[List[Relation]]: - """ - Returns list of related content of certain ranobe. - - :param ranobe_id: Ranobe ID to get related content - :type ranobe_id: int - - :return: List of relations - :rtype: Optional[List[Relation]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.ranobe_related_content(ranobe_id)) - return Utils.validate_return_data(response, data_model=Relation) - - @method_endpoint('/api/ranobe/:id/franchise') - def ranobe_franchise_tree(self, ranobe_id: int) -> Optional[FranchiseTree]: - """ - Returns franchise tree of certain ranobe. - - :param ranobe_id: Ranobe ID to get franchise tree - :type ranobe_id: int - - :return: Franchise tree of certain ranobe - :rtype: Optional[FranchiseTree] - """ - response: Dict[str, Any] = self._request( - self._endpoints.ranobe_franchise_tree(ranobe_id)) - return Utils.validate_return_data(response, data_model=FranchiseTree) - - @method_endpoint('/api/ranobe/:id/external_links') - def ranobe_external_links(self, ranobe_id: int) -> Optional[List[Link]]: - """ - Returns list of external links of certain ranobe. - - :param ranobe_id: Ranobe ID to get external links - :type ranobe_id: int - - :return: List of external links - :rtype: Optional[List[Link]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.ranobe_external_links(ranobe_id)) - return Utils.validate_return_data(response, data_model=Link) - - @method_endpoint('/api/ranobe/:id/topics') - def ranobe_topics(self, - ranobe_id: int, - page: Optional[int] = None, - limit: Optional[int] = None) -> Optional[List[Topic]]: - """ - Returns list of topics of certain ranobe. - - If some data are not provided, using default values. - - :param ranobe_id: Ranobe ID to get topics - :type ranobe_id: int - - :param page: Number of page - :type page: Optional[int] - - :param limit: Number of results limit - :type limit: Optional[int] - - :return: List of topics - :rtype: Optional[List[Topic]] - """ - validated_numbers = Utils.query_numbers_validator( - page=[page, 100000], - limit=[limit, 30], - ) - - response: List[Dict[str, Any]] = self._request( - self._endpoints.ranobe_topics(ranobe_id), - query=Utils.generate_query_dict(page=validated_numbers['page'], - limit=validated_numbers['limit'])) - return Utils.validate_return_data(response, data_model=Topic) - - @method_endpoint('/api/stats/active_users') - def active_users(self) -> Optional[List[int]]: - """ - Returns list of IDs of active users. - - :return: List of IDs of active users - :rtype: Optional[List[int]] - """ - response: List[int] = self._request(self._endpoints.active_users) - return Utils.validate_return_data(response) - - @method_endpoint('/api/studios') - def studios(self) -> Optional[List[Studio]]: - """ - Returns list of studios. - - :return: List of studios - :rtype: Optional[List[Studio]] - """ - response: List[Dict[str, Any]] = self._request(self._endpoints.studios) - return Utils.validate_return_data(response, data_model=Studio) - - @method_endpoint('/api/styles/:id') - def style(self, style_id: int) -> Optional[Style]: - """ - Returns info about style. - - :param style_id: Style ID to get info - :type style_id: int - - :return: Info about style - :rtype: Optional[Style] - """ - response: Dict[str, - Any] = self._request(self._endpoints.style(style_id)) - return Utils.validate_return_data(response, data_model=Style) - - @method_endpoint('/api/styles/preview') - @protected_method() - def preview_style(self, css: str) -> Optional[Style]: - """ - Previews style with passed CSS code. - - :param css: CSS code to preview - :type css: str - - :return: Info about previewed style - :rtype: Optional[Style] - """ - response: Dict[str, Any] = self._request( - self._endpoints.style_preview, - headers=self._authorization_header, - data=Utils.generate_data_dict(dict_name='style', css=css), - request_type=RequestType.POST) - return Utils.validate_return_data(response, data_model=Style) - - @method_endpoint('/api/styles') - @protected_method() - def create_style(self, css: str, name: str, owner_id: int, - owner_type: str) -> Optional[Style]: - """ - Creates new style. - - :param css: CSS code for style - :type css: str - - :param name: Style name - :type name: str - - :param owner_id: User/Club ID for style ownership - :type owner_id: int - - :param owner_type: Type of owner (User/Club) - :type owner_type: str - - :return: Info about previewed style - :rtype: Optional[Style] - """ - if not Utils.validate_enum_params({OwnerType: owner_type}): - return None - - response: Dict[str, Any] = self._request( - self._endpoints.styles, - headers=self._authorization_header, - data=Utils.generate_data_dict(dict_name='style', - css=css, - name=name, - owner_id=owner_id, - owner_type=owner_type), - request_type=RequestType.POST) - return Utils.validate_return_data(response, data_model=Style) - - @method_endpoint('/api/styles/:id') - @protected_method() - def update_style(self, style_id: int, css: Optional[str], - name: Optional[str]) -> Optional[Style]: - """ - Updates existing style. - - :param style_id: ID of existing style for edit - :type style_id: int - - :param css: New CSS code for style - :type css: Optional[str] - - :param name: New style name - :type name: Optional[str] - - :return: Info about updated style - :rtype: Optional[Style] - """ - response: Dict[str, Any] = self._request( - self._endpoints.style(style_id), - headers=self._authorization_header, - data=Utils.generate_data_dict(dict_name='style', css=css, - name=name), - request_type=RequestType.PATCH) - return Utils.validate_return_data(response, data_model=Style) - - @method_endpoint('/api/topics') - def topics(self, - page: Optional[int] = None, - limit: Optional[int] = None, - forum: Optional[str] = None, - linked_id: Optional[int] = None, - linked_type: Optional[str] = None, - topic_type: Optional[str] = None) -> Optional[List[Topic]]: - """ - Returns list of topics. - - :param page: Number of page - :type page: Optional[int] - - :param limit: Number of results limit - :type limit: Optional[int] - - :param forum: Number of results limit - :type forum: Optional[str] - - :param linked_id: ID of linked topic (Used together with linked_type) - :type linked_id: Optional[int] - - :param linked_type: Type of linked topic (Used together with linked_id) - :type linked_type: Optional[str] - - :param topic_type: Type of topic. - :type topic_type: Optional[str] - - :return: List of topics - :rtype: Optional[List[Topic]] - """ - if not Utils.validate_enum_params({ - ForumType: forum, - TopicLinkedType: linked_type, - TopicType: topic_type - }): - return None - - validated_numbers = Utils.query_numbers_validator( - page=[page, 100000], - limit=[limit, 30], - ) - - response: List[Dict[str, Any]] = self._request( - self._endpoints.topics, - query=Utils.generate_query_dict(page=validated_numbers['page'], - limit=validated_numbers['limit'], - forum=forum, - linked_id=linked_id, - linked_type=linked_type, - type=topic_type)) - return Utils.validate_return_data(response, data_model=Topic) - - @method_endpoint('/api/topics/updates') - def updates_topics(self, - page: Optional[int] = None, - limit: Optional[int] = None) -> Optional[List[Topic]]: - """ - Returns list of NewsTopics about database updates. - - :param page: Number of page - :type page: Optional[int] - - :param limit: Number of results limit - :type limit: Optional[int] - - :return: List of topics - :rtype: Optional[List[Topic]] - """ - validated_numbers = Utils.query_numbers_validator( - page=[page, 100000], - limit=[limit, 30], - ) - - response: List[Dict[str, Any]] = self._request( - self._endpoints.updates_topics, - query=Utils.generate_query_dict( - page=validated_numbers['page'], - limit=validated_numbers['limit'], - )) - return Utils.validate_return_data(response, data_model=Topic) - - @method_endpoint('/api/topics/hot') - def hot_topics(self, limit: Optional[int] = None) -> Optional[List[Topic]]: - """ - Returns list of hot topics. - - :param limit: Number of results limit - :type limit: Optional[int] - - :return: List of topics - :rtype: Optional[List[Topic]] - """ - validated_numbers = Utils.query_numbers_validator(limit=[limit, 10]) - - response: List[Dict[str, Any]] = self._request( - self._endpoints.hot_topics, - query=Utils.generate_query_dict(limit=validated_numbers['limit'],)) - return Utils.validate_return_data(response, data_model=Topic) - - @method_endpoint('/api/topics/:id') - def topic(self, topic_id: int) -> Optional[Topic]: - """ - Returns info about topic. - - :param topic_id: ID of topic to get - :type topic_id: int - - :return: Info about topic - :rtype: Optional[Topic] - """ - response: Dict[str, - Any] = self._request(self._endpoints.topic(topic_id)) - return Utils.validate_return_data(response, data_model=Topic) - - @method_endpoint('/api/topics') - @protected_method('topics') - def create_topic(self, - body: str, - forum_id: int, - title: str, - user_id: int, - linked_id: Optional[int] = None, - linked_type: Optional[str] = None) -> Optional[Topic]: - """ - Creates topic. - - :param body: Body of topic - :type body: str - - :param forum_id: ID of forum to post - :type forum_id: int - - :param title: Title of topic - :type title: str - - :param user_id: ID of topic creator - :type user_id: int - - :param linked_id: ID of linked topic (Used together with linked_type) - :type linked_type: Optional[int] - - :param linked_type: Type of linked topic (Used together with linked_id) - :type linked_type: Optional[str] - - :return: Created topic info - :rtype: Optional[Topic] - """ - if not Utils.validate_enum_params({TopicLinkedType: linked_type}): - return None - - response: Dict[str, Any] = self._request( - self._endpoints.topics, - headers=self._authorization_header, - data=Utils.generate_data_dict(dict_name='topic', - body=body, - forum_id=forum_id, - linked_id=linked_id, - linked_type=linked_type, - title=title, - type=str( - TopicType.REGULAR_TOPIC.value), - user_id=user_id), - request_type=RequestType.POST) - return Utils.validate_return_data(response, data_model=Topic) - - @method_endpoint('/api/topics/:id') - @protected_method('topics') - def update_topic(self, - topic_id: int, - body: str, - title: str, - linked_id: Optional[int] = None, - linked_type: Optional[str] = None) -> Optional[Topic]: - """ - Updated topic. - - :param topic_id: ID of topic to update - :type topic_id: int - - :param body: Body of topic - :type body: str - - :param title: Title of topic - :type title: str - - :param linked_id: ID of linked topic (Used together with linked_type) - :type linked_type: Optional[int] - - :param linked_type: Type of linked topic (Used together with linked_id) - :type linked_type: Optional[str] - - :return: Updated topic info - :rtype: Optional[Topic] - """ - if not Utils.validate_enum_params({TopicLinkedType: linked_type}): - return None - - response: Dict[str, Any] = self._request( - self._endpoints.topic(topic_id), - headers=self._authorization_header, - data=Utils.generate_data_dict(dict_name='topic', - body=body, - linked_id=linked_id, - linked_type=linked_type, - title=title), - request_type=RequestType.PATCH) - return Utils.validate_return_data(response, data_model=Topic) - - @method_endpoint('/api/topics/:id') - @protected_method('topics') - def delete_topic(self, topic_id: int) -> Optional[bool]: - """ - Deletes topic. - - :param topic_id: ID of topic to delete - :type topic_id: int - - :return: Status of topic deletion - :rtype: bool - """ - response: Union[Dict[str, Any], - int] = self._request(self._endpoints.topic(topic_id), - headers=self._authorization_header, - request_type=RequestType.DELETE) - return Utils.validate_return_data(response) - - @method_endpoint('/api/user_images') - @protected_method('comments') - def create_user_image( - self, - image_path: str, - linked_type: Optional[str] = None) -> Optional[CreatedUserImage]: - """ - Creates an user image. - - :param image_path: Path or URL to image to create on server - :type image_path: str - - :param linked_type: Type of linked image - :type linked_type: Optional[str] - - :return: Created image info - :rtype: Optional[CreatedUserImage] - """ - response: Union[Dict[str, Any], int] = self._request( - self._endpoints.user_images, - headers=self._authorization_header, - files=Utils.get_image_data(image_path), - data=Utils.generate_data_dict(linked_type=linked_type), - request_type=RequestType.POST) - return Utils.validate_return_data(response, data_model=CreatedUserImage) - - @method_endpoint('/api/v2/user_rates') - def user_rates(self, - user_id: int, - target_id: Optional[int] = None, - target_type: Optional[str] = None, - status: Optional[str] = None, - page: Optional[int] = None, - limit: Optional[int] = None) -> Optional[List[UserRate]]: - """ - Returns list of user rates. - - **Note:** When passing target_id, target_type is required. - - Also there is a strange API behavior, so when pass nothing, - endpoint not working. - However, docs shows that page/limit ignored when user_id is set (bruh) - - :param user_id: ID of user to get rates for - :type user_id: int - - :param target_id: ID of anime/manga to get rates for - :type target_id: Optional[int] - - :param target_type: Type of target_id to get rates for - :type target_type: Optional[str] - - :param status: Status of target_type to get rates for - :type target_type: Optional[str] - - :param page: Number of page - :type page: Optional[int] - - :param limit: Number of results limit - (This field is ignored when user_id is set) - :type limit: Optional[int] - - :return: List with info about user rates - (This field is ignored when user_id is set) - :rtype: Optional[List[UserRate]] - """ - if target_id is not None and target_type is None: - logger.warning('target_type is required when passing target_id') - return None - - if not Utils.validate_enum_params({ - UserRateTarget: target_type, - UserRateStatus: status - }): - return None - - validated_numbers = Utils.query_numbers_validator( - page=[page, 100000], - limit=[limit, 1000], - ) - - response: List[Dict[str, Any]] = self._request( - self._endpoints.user_rates, - query=Utils.generate_query_dict(user_id=user_id, - target_id=target_id, - target_type=target_type, - status=status, - page=validated_numbers['page'], - limit=validated_numbers['limit'])) - return Utils.validate_return_data(response, data_model=UserRate) - - @method_endpoint('/api/v2/user_rates/:id') - def user_rate(self, rate_id: int) -> Optional[UserRate]: - """ - Returns info about user rate. - - :param rate_id: ID of rate to get - :type rate_id: int - - :return: Info about user rate - :rtype: Optional[UserRate] - """ - response: Dict[str, - Any] = self._request(self._endpoints.user_rate(rate_id)) - return Utils.validate_return_data(response, data_model=UserRate) - - @method_endpoint('/api/v2/user_rates') - @protected_method('user_rates') - def create_user_rate(self, - user_id: int, - target_id: int, - target_type: str, - status: Optional[str] = None, - score: Optional[int] = None, - chapters: Optional[int] = None, - episodes: Optional[int] = None, - volumes: Optional[int] = None, - rewatches: Optional[int] = None, - text: Optional[str] = None) -> Optional[UserRate]: - """ - Creates new user rate and return info about it. - - :param user_id: ID of user to create user rate for - :type user_id: int - - :param target_id: ID of target to create user rate for - :type target_id: int - - :param target_type: Type of target_id to create user rate for - (Anime or Manga) - :type target_type: str - - :param status: Status of target - :type status: Optional[str] - - :param score: Score of target - :type score: Optional[int] - - :param chapters: Watched/read chapters of target - :type chapters: Optional[int] - - :param episodes: Watched/read episodes of target - :type episodes: Optional[int] - - :param volumes: Watched/read volumes of target - :type volumes: Optional[int] - - :param rewatches: Number of target rewatches - :type rewatches: Optional[int] - - :param text: Text note for user rate - :type text: Optional[str] - - :return: Info about new user rate - :rtype: Optional[UserRate] - """ - if not Utils.validate_enum_params({ - UserRateTarget: target_type, - UserRateStatus: status - }): - return None - - validated_numbers = Utils.query_numbers_validator(score=[score, 10]) - - response: Dict[str, Any] = self._request( - self._endpoints.user_rates, - headers=self._authorization_header, - data=Utils.generate_data_dict(dict_name='user_rate', - user_id=user_id, - target_id=target_id, - target_type=target_type, - status=status, - score=validated_numbers['score'], - chapters=chapters, - episodes=episodes, - volumes=volumes, - rewatches=rewatches, - text=text), - request_type=RequestType.POST) - return Utils.validate_return_data(response, data_model=UserRate) - - @method_endpoint('/api/v2/user_rates/:id') - @protected_method('user_rates') - def update_user_rate(self, - rate_id: int, - status: Optional[str] = None, - score: Optional[int] = None, - chapters: Optional[int] = None, - episodes: Optional[int] = None, - volumes: Optional[int] = None, - rewatches: Optional[int] = None, - text: Optional[str] = None) -> Optional[UserRate]: - """ - Updates user rate and return new info about it. - - :param rate_id: ID of user rate to edit - :type rate_id: int - - :param status: Status of target - :type status: Optional[str] - - :param score: Score of target - :type score: Optional[int] - - :param chapters: Watched/read chapters of target - :type chapters: Optional[int] - - :param episodes: Watched/read episodes of target - :type episodes: Optional[int] - - :param volumes: Watched/read volumes of target - :type volumes: Optional[int] - - :param rewatches: Number of target rewatches - :type rewatches: Optional[int] - - :param text: Text note for user rate - :type text: Optional[str] - - :return: Info about new user rate - :rtype: Optional[UserRate] - """ - if not Utils.validate_enum_params({UserRateStatus: status}): - return None - - validated_numbers = Utils.query_numbers_validator(score=[score, 10]) - - response: Dict[str, Any] = self._request( - self._endpoints.user_rate(rate_id), - headers=self._authorization_header, - data=Utils.generate_data_dict(dict_name='user_rate', - status=status, - score=validated_numbers['score'], - chapters=chapters, - episodes=episodes, - volumes=volumes, - rewatches=rewatches, - text=text), - request_type=RequestType.PATCH) - return Utils.validate_return_data(response, data_model=UserRate) - - @method_endpoint('/api/v2/user_rates/:id/increment') - @protected_method('user_rates') - def increment_user_rate(self, rate_id: int) -> Optional[UserRate]: - """ - Increments user rate episode/chapters and return updated info. - - :param rate_id: ID of user rate to increment episode/chapters - :type rate_id: int - - :return: Info about updated user rate - :rtype: Optional[UserRate] - """ - response: Dict[str, Any] = self._request( - self._endpoints.user_rate_increment(rate_id), - headers=self._authorization_header, - request_type=RequestType.POST) - return Utils.validate_return_data(response, data_model=UserRate) - - @method_endpoint('/api/v2/user_rates/:id') - @protected_method('user_rates') - def delete_user_rate(self, rate_id: int) -> bool: - """ - Deletes user rate. - - :param rate_id: ID of user rate to delete - :type rate_id: int - - :return: Status of user rate deletion - :rtype: bool - """ - response: Union[Dict[str, Any], - int] = self._request(self._endpoints.user_rate(rate_id), - headers=self._authorization_header, - request_type=RequestType.DELETE) - return Utils.validate_return_data(response, - response_code=ResponseCode.NO_CONTENT) - - @method_endpoint('/api/users_rates/:type/cleanup') - @protected_method('user_rates') - def delete_entire_user_rates(self, user_rate_type: str) -> bool: - """ - Deletes all user rates. - - :param user_rate_type: Type of user rates to delete - :type user_rate_type: str - - :return: Status of user rates deletion - :rtype: bool - """ - if not Utils.validate_enum_params({UserRateType: user_rate_type}): - return False - - response: Union[Dict[str, Any], int] = self._request( - self._endpoints.user_rates_cleanup(user_rate_type), - headers=self._authorization_header, - request_type=RequestType.DELETE) - return Utils.validate_return_data(response) - - @method_endpoint('/api/user_rates/:type/reset') - @protected_method('user_rates') - def reset_all_user_rates(self, user_rate_type: str) -> bool: - """ - Resets all user rates. - - :param user_rate_type: Type of user rates to reset - :type user_rate_type: UserRateType - - :return: Status of user rates reset - :rtype: bool - """ - if not Utils.validate_enum_params({UserRateType: user_rate_type}): - return False - - response: Union[Dict[str, Any], int] = self._request( - self._endpoints.user_rates_reset(user_rate_type), - headers=self._authorization_header, - request_type=RequestType.DELETE) - return Utils.validate_return_data(response) - - @method_endpoint('/api/users') - def users(self, - page: Optional[int] = None, - limit: Optional[int] = None) -> Optional[List[User]]: - """ - Returns list of users. - - :param page: Number of page - :type page: Optional[int] - - :param limit: Number of results limit - :type limit: Optional[int] - - :return: List of users - :rtype: Optional[List[User]] - """ - validated_numbers = Utils.query_numbers_validator( - page=[page, 100000], - limit=[limit, 100], - ) - - response: List[Dict[str, Any]] = self._request( - self._endpoints.users, - query=Utils.generate_query_dict(page=validated_numbers['page'], - limit=validated_numbers['limit'])) - return Utils.validate_return_data(response, data_model=User) - - @method_endpoint('/api/users/:id') - def user(self, - user_id: Union[str, int], - is_nickname: Optional[bool] = None) -> Optional[User]: - """ - Returns info about user. - - :param user_id: User ID/Nickname to get info - :type user_id: Union[str, int] - - :param is_nickname: Specify if passed user_id is nickname - :type is_nickname: Optional[bool] - - :return: Info about user - :rtype: Optional[User] - """ - response: Dict[str, Any] = self._request( - self._endpoints.user(user_id), - query=Utils.generate_query_dict(is_nickname=is_nickname)) - return Utils.validate_return_data(response, data_model=User) - - @method_endpoint('/api/users/:id/info') - def user_info(self, - user_id: Union[str, int], - is_nickname: Optional[bool] = None) -> Optional[User]: - """ - Returns user's brief info. - - :param user_id: User ID/Nickname to get brief info - :type user_id: Union[int, str] - - :param is_nickname: Specify if passed user_id is nickname - :type is_nickname: Optional[bool] - - :return: User's brief info - :rtype: Optional[User] - """ - response: Dict[str, Any] = self._request( - self._endpoints.user_info(user_id), - query=Utils.generate_query_dict(is_nickname=is_nickname)) - return Utils.validate_return_data(response, data_model=User) - - @method_endpoint('/api/users/whoami') - @protected_method() - def current_user(self) -> Optional[User]: - """ - Returns brief info about current user. - - Current user evaluated depending on authorization code. - - :return: Current user brief info - :rtype: Optional[User] - """ - response: Dict[str, - Any] = self._request(self._endpoints.whoami, - headers=self._authorization_header) - return Utils.validate_return_data(response, data_model=User) - - @method_endpoint('/api/users/sign_out') - @protected_method() - def user_sign_out(self): - """Sends sign out request to API.""" - self._request(self._endpoints.sign_out, - headers=self._authorization_header) - - @method_endpoint('/api/users/:id/friends') - def user_friends( - self, - user_id: Union[str, int], - is_nickname: Optional[bool] = None) -> Optional[List[User]]: - """ - Returns user's friends. - - :param user_id: User ID/Nickname to get friends - :type user_id: Union[int, str] - - :param is_nickname: Specify if passed user_id is nickname - :type is_nickname: Optional[bool] - - :return: List of user's friends - :rtype: Optional[List[User]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.user_friends(user_id), - query=Utils.generate_query_dict(is_nickname=is_nickname)) - return Utils.validate_return_data(response, data_model=User) - - @method_endpoint('/api/users/:id/clubs') - def user_clubs(self, - user_id: Union[int, str], - is_nickname: Optional[bool] = None) -> Optional[List[Club]]: - """ - Returns user's clubs. - - :param user_id: User ID/Nickname to get clubs - :type user_id: Union[int, str] - - :param is_nickname: Specify if passed user_id is nickname - :type is_nickname: Optional[bool] - - :return: List of user's clubs - :rtype: Optional[List[Club]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.user_clubs(user_id), - query=Utils.generate_query_dict(is_nickname=is_nickname)) - return Utils.validate_return_data(response, data_model=Club) - - @method_endpoint('/api/users/:id/anime_rates') - def user_anime_rates( - self, - user_id: Union[int, str], - is_nickname: Optional[bool] = None, - page: Optional[int] = None, - limit: Optional[int] = None, - status: Optional[str] = None, - censored: Optional[str] = None) -> Optional[List[UserList]]: - """ - Returns user's anime list. - - :param user_id: User ID/Nickname to get anime list - :type user_id: Optional[int, str] - - :param is_nickname: Specify if passed user_id is nickname - :type is_nickname: Optional[bool] - - :param page: Number of page - :type page: Optional[int] - - :param limit: Number of results limit - :type limit: Optional[int] - - :param status: Status of status of anime in list - :type status: Optional[str] - - :param censored: Type of anime censorship - :type censored: Optional[str] - - :return: User's anime list - :rtype: Optional[List[UserList]] - """ - if not Utils.validate_enum_params({ - AnimeList: status, - AnimeCensorship: censored - }): - return None - - validated_numbers = Utils.query_numbers_validator( - page=[page, 100000], - limit=[limit, 5000], - ) - - response: List[Dict[str, Any]] = self._request( - self._endpoints.user_anime_rates(user_id), - query=Utils.generate_query_dict(is_nickname=is_nickname, - page=validated_numbers['page'], - limit=validated_numbers['limit'], - status=status, - censored=censored)) - return Utils.validate_return_data(response, data_model=UserList) - - @method_endpoint('/api/users/:id/manga_rates') - def user_manga_rates( - self, - user_id: Union[int, str], - is_nickname: Optional[bool] = None, - page: Optional[int] = None, - limit: Optional[int] = None, - censored: Optional[str] = None) -> Optional[List[UserList]]: - """ - Returns user's manga list. - - :param user_id: User ID/Nickname to get manga list - :type user_id: Union[int, str] - - :param is_nickname: Specify if passed user_id is nickname - :type is_nickname: Optional[bool] - - :param page: Number of page - :type page: Optional[int] - - :param limit: Number of results limit - :type limit: Optional[int] - - :param censored: Type of manga censorship - :type censored: Optional[str] - - :return: User's manga list - :rtype: Optional[List[UserList]] - """ - if not Utils.validate_enum_params({AnimeCensorship: censored}): - return None - - validated_numbers = Utils.query_numbers_validator( - page=[page, 100000], - limit=[limit, 5000], - ) - - response: List[Dict[str, Any]] = self._request( - self._endpoints.user_manga_rates(user_id), - query=Utils.generate_query_dict(is_nickname=is_nickname, - page=validated_numbers['page'], - limit=validated_numbers['limit'], - censored=censored)) - return Utils.validate_return_data(response, data_model=UserList) - - @method_endpoint('/api/users/:id/favourites') - def user_favourites( - self, - user_id: Union[int, str], - is_nickname: Optional[bool] = None) -> Optional[Favourites]: - """ - Returns user's favourites. - - :param user_id: User ID/Nickname to get favourites - :type user_id: Union[int, str] - - :param is_nickname: Specify if passed user_id is nickname - :type is_nickname: Optional[bool] - - :return: User's favourites - :rtype: Optional[Favourites] - """ - response: Dict[str, Any] = self._request( - self._endpoints.user_favourites(user_id), - query=Utils.generate_query_dict(is_nickname=is_nickname)) - return Utils.validate_return_data(response, data_model=Favourites) - - @method_endpoint('/api/users/:id/messages') - @protected_method('messages') - def current_user_messages( - self, - user_id: Union[int, str], - is_nickname: Optional[bool] = None, - page: Optional[int] = None, - limit: Optional[int] = None, - message_type: str = MessageType.NEWS.value - ) -> Optional[List[Message]]: - """ - Returns current user's messages by type. - - :param user_id: Current user ID/Nickname to get messages - :type user_id: Union[int, str] - - :param is_nickname: Specify if passed user_id is nickname - :type is_nickname: Optional[bool] - - :param page: Number of page - :type page: Optional[int] - - :param limit: Number of page limits - :type limit: Optional[int] - - :param message_type: Type of message - :type message_type: str - - :return: Current user's messages - :rtype: Optional[List[Message]] - """ - if not Utils.validate_enum_params({MessageType: message_type}): - return None - - validated_numbers = Utils.query_numbers_validator( - page=[page, 100000], - limit=[limit, 100], - ) - - response: List[Dict[str, Any]] = self._request( - self._endpoints.user_messages(user_id), - headers=self._authorization_header, - query=Utils.generate_query_dict(is_nickname=is_nickname, - page=validated_numbers['page'], - limit=validated_numbers['limit'], - type=message_type)) - return Utils.validate_return_data(response, data_model=Message) - - @method_endpoint('/api/users/:id/unread_messages') - @protected_method('messages') - def current_user_unread_messages( - self, - user_id: Union[int, str], - is_nickname: Optional[bool] = None) -> Optional[UnreadMessages]: - """ - Returns current user's unread messages counter. - - :param user_id: Current user ID/Nickname to get unread messages - :type user_id: Union[int, str] - - :param is_nickname: Specify if passed user_id is nickname - :type is_nickname: Optional[bool] - - :return: Current user's unread messages counters - :rtype: Optional[UnreadMessages] - """ - response: Dict[str, Any] = self._request( - self._endpoints.user_unread_messages(user_id), - headers=self._authorization_header, - query=Utils.generate_query_dict(is_nickname=is_nickname)) - return Utils.validate_return_data(response, data_model=UnreadMessages) - - @method_endpoint('/api/users/:id/history') - def user_history( - self, - user_id: Union[int, str], - is_nickname: Optional[bool] = None, - page: Optional[int] = None, - limit: Optional[int] = None, - target_id: Optional[int] = None, - target_type: Optional[str] = None) -> Optional[List[History]]: - """ - Returns history of user. - - :param user_id: User ID/Nickname to get history - :type user_id: Union[int, str] - - :param is_nickname: Specify if passed user_id is nickname - :type is_nickname: Optional[bool] - - :param page: Number of page - :type page: Optional[int] - - :param limit: Number of results limit - :type limit: Optional[int] - - :param target_id: ID of anime/manga in history - :type target_id: Optional[int] - - :param target_type: Type of target (Anime/Manga) - :type target_type: Optional[str] - - :return: User's history - :rtype: Optional[List[History]] - """ - if not Utils.validate_enum_params({TargetType: target_type}): - return None - - validated_numbers = Utils.query_numbers_validator( - page=[page, 100000], - limit=[limit, 100], - ) - - response: List[Dict[str, Any]] = self._request( - self._endpoints.user_history(user_id), - query=Utils.generate_query_dict(is_nickname=is_nickname, - page=validated_numbers['page'], - limit=validated_numbers['limit'], - target_id=target_id, - target_type=target_type)) - return Utils.validate_return_data(response, data_model=History) - - @method_endpoint('/api/users/:id/bans') - def user_bans(self, - user_id: Union[int, str], - is_nickname: Optional[bool] = None) -> Optional[List[Ban]]: - """ - Returns list of bans of user. - - :param user_id: User ID/Nickname to get list of bans - :type user_id: Union[int, str] - - :param is_nickname: Specify if passed user_id is nickname - :type is_nickname: Optional[bool] - - :return: User's bans - :rtype: Optional[List[Ban]] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.user_bans(user_id), - query=Utils.generate_query_dict(is_nickname=is_nickname)) - return Utils.validate_return_data(response, data_model=Ban) - - @method_endpoint('/api/v2/topics/:topic_id/ignore') - @protected_method('topics') - def ignore_topic(self, topic_id: int) -> bool: - """ - Set topic as ignored. - - :param topic_id: ID of topic to ignore - :type topic_id: int - - :return: True if topic was ignored, False otherwise - :rtype: bool - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.topic_ignore(topic_id), - headers=self._authorization_header, - request_type=RequestType.POST) - return Utils.validate_return_data(response) is True - - @method_endpoint('/api/v2/topics/:topic_id/ignore') - @protected_method('topics') - def unignore_topic(self, topic_id: int) -> bool: - """ - Set topic as unignored. - - :param topic_id: ID of topic to unignore - :type topic_id: int - - :return: True if topic was unignored, False otherwise - :rtype: bool - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.topic_ignore(topic_id), - headers=self._authorization_header, - request_type=RequestType.DELETE) - return Utils.validate_return_data(response) is False - - @method_endpoint('/api/v2/users/:user_id/ignore') - @protected_method('ignores') - def ignore_user(self, user_id: int) -> bool: - """ - Set user as ignored. - - :param user_id: ID of topic to ignore - :type user_id: int - - :return: True if user was ignored, False otherwise - :rtype: bool - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.user_ignore(user_id), - headers=self._authorization_header, - request_type=RequestType.POST) - return Utils.validate_return_data(response) is True - - @method_endpoint('/api/v2/users/:user_id/ignore') - @protected_method('ignores') - def unignore_user(self, user_id: int) -> bool: - """ - Set user as unignored. - - :param user_id: ID of user to unignore - :type user_id: int - - :return: True if user was unignored, False otherwise - :rtype: bool - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.user_ignore(user_id), - headers=self._authorization_header, - request_type=RequestType.DELETE) - return Utils.validate_return_data(response) is False - - @method_endpoint('/api/v2/abuse_requests/offtopic') - def mark_comment_offtopic(self, comment_id: int) -> Optional[AbuseResponse]: - """ - Mark comment as offtopic. - - :param comment_id: ID of comment to mark as offtopic - :type comment_id: int - - :return: Object with info about abuse request - :rtype: Optional[AbuseResponse] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.abuse_offtopic, - data=Utils.generate_data_dict(comment_id=comment_id), - request_type=RequestType.POST) - return Utils.validate_return_data(response, data_model=AbuseResponse) - - @method_endpoint('/api/v2/abuse_requests/review') - def convert_comment_review(self, - comment_id: int) -> Optional[AbuseResponse]: - """ - Convert comment to review. - - :param comment_id: ID of comment to convert to review - :type comment_id: int - - :return: Object with info about abuse request - :rtype: Optional[AbuseResponse] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.abuse_review, - data=Utils.generate_data_dict(comment_id=comment_id), - request_type=RequestType.POST) - return Utils.validate_return_data(response, data_model=AbuseResponse) - - @method_endpoint('/api/v2/abuse_requests/abuse') - def create_violation_abuse_request(self, comment_id: int, - reason: str) -> Optional[AbuseResponse]: - """ - Create abuse about violation of site rules - - :param comment_id: ID of comment to create abuse request - :type comment_id: int - - :param reason: Additional info about violation - :type reason: str - - :return: Object with info about abuse request - :rtype: Optional[AbuseResponse] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.abuse_violation, - data=Utils.generate_data_dict(comment_id=comment_id, reason=reason), - request_type=RequestType.POST) - return Utils.validate_return_data(response, data_model=AbuseResponse) - - @method_endpoint('/api/v2/abuse_requests/spoiler') - def create_spoiler_abuse_request(self, comment_id: int, - reason: str) -> Optional[AbuseResponse]: - """ - Create abuse about spoiler in content. - - :param comment_id: ID of comment to create abuse request - :type comment_id: int - - :param reason: Additional info about spoiler - :type reason: str - - :return: Object with info about abuse request - :rtype: Optional[AbuseResponse] - """ - response: List[Dict[str, Any]] = self._request( - self._endpoints.abuse_spoiler, - data=Utils.generate_data_dict(comment_id=comment_id, reason=reason), - request_type=RequestType.POST) - return Utils.validate_return_data(response, data_model=AbuseResponse) + async def __aexit__(self, *args) -> None: + """Async context manager exit point.""" + await self.close() diff --git a/shikithon/base_client.py b/shikithon/base_client.py new file mode 100644 index 00000000..82bda053 --- /dev/null +++ b/shikithon/base_client.py @@ -0,0 +1,603 @@ +"""Base class for shikithon API class.""" +from __future__ import annotations + +import asyncio +from json import dumps +import sys +from time import time +from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union + +from aiohttp import ClientSession +from aiohttp import ContentTypeError +from loguru import logger +from ratelimit import limits +from ratelimit import sleep_and_retry + +from .endpoints import Endpoints +from .enums import RequestType +from .exceptions import AccessTokenException +from .exceptions import MissingAppVariable +from .exceptions import MissingAuthCode +from .exceptions import MissingConfigData +from .store import ConfigStore +from .utils import Utils + +ONE_MINUTE = 60 +MAX_CALLS_PER_MINUTE = 90 +ONE_SECOND = 1 +MAX_CALLS_PER_SECOND = 5 + +SHIKIMORI_API_URL = 'https://shikimori.one/api' +SHIKIMORI_API_URL_V2 = 'https://shikimori.one/api/v2' +SHIKIMORI_OAUTH_URL = 'https://shikimori.one/oauth' +DEFAULT_REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob' + +RT = TypeVar('RT') +TOKEN_EXPIRE_TIME = 86400 + + +class Client: + """Base client class for shikithon API class. + + Contains logic and methods for making requests to the shikimori API + as well as validating config and etc. + + **Note:** Due to problems with some methods, + when the session header contains a User-Agent and authorization, + __init__ sets only the User-Agent, + and all protected methods independently + provide a header with a token + """ + + def __init__(self, config: Union[str, Dict[str, str]]) -> None: + self.endpoints = Endpoints(SHIKIMORI_API_URL, SHIKIMORI_API_URL_V2, + SHIKIMORI_OAUTH_URL) + self._session = None + self._passed_config = config + + self._restricted_mode = False + self._app_name = '' + self._client_id = '' + self._client_secret = '' + self._redirect_uri = '' + self._scopes = '' + self._auth_code = '' + self._access_token = '' + self._refresh_token = '' + self._token_expire = -1 + + @property + def restricted_mode(self) -> bool: + """ + Returns current restrict mode of client object. + + If true, client object can access only public methods + + :return: Current restrict mode + :rtype: bool + """ + return self._restricted_mode + + @restricted_mode.setter + def restricted_mode(self, restricted_mode: bool): + """ + Sets new restrict mode of client object. + + :param restricted_mode: New restrict mode + :type restricted_mode: bool + """ + self._restricted_mode = restricted_mode + + @property + def scopes_list(self) -> List[str]: + """ + Returns list of scopes. + + :return: List of scopes + :rtype: List[str] + """ + return self._scopes.split('+') + + @property + def config(self) -> Dict[str, str]: + """ + Returns current client variables as config dictionary. + + :return: Current client variables as config dictionary + :rtype: Dict[str, str] + """ + logger.debug('Exporting current client config') + return { + 'app_name': self._app_name, + 'client_id': self._client_id, + 'client_secret': self._client_secret, + 'redirect_uri': self._redirect_uri, + 'scopes': self._scopes, + 'auth_code': self._auth_code, + 'access_token': self._access_token, + 'refresh_token': self._refresh_token, + 'token_expire': str(self._token_expire) + } + + @config.setter + def config(self, config: Dict[str, str]): + """ + Sets new client variables from config dictionary. + + This method calls init_config + to reconfigure the object + + :param config: Config dictionary + :type config: Dict[str, str] + """ + logger.info('Setting new client config') + self.init_config(config) + + @property + def tokens(self) -> Tuple[str, str]: + """ + Returns access/refresh tokens as tuple. + + :return: Access and refresh tokens tuple + :rtype: Tuple[str, str] + """ + return self._access_token, self._refresh_token + + @tokens.setter + def tokens(self, tokens_data: Tuple[str, str]): + """ + Sets new access/refresh tokens from tuple. + + :param tokens_data: New access and refresh tokens tuple + :type tokens_data: Tuple[str, str] + """ + self._access_token = tokens_data[0] + self._refresh_token = tokens_data[1] + + @property + def user_agent(self) -> Dict[str, str]: + """ + Returns current session User-Agent. + + :return: Session User-Agent + :rtype: Dict[str, str] + """ + return {'User-Agent': self._session.headers['User-Agent']} + + @user_agent.setter + def user_agent(self, app_name: str): + """ + Update session headers and set user agent. + + :param app_name: OAuth App name + :type app_name: str + """ + if self._session is not None: + self._session.headers.update({'User-Agent': app_name}) + + @property + def authorization_header(self) -> Dict[str, str]: + """ + Returns user agent and authorization token headers dictionary. + + Needed for accessing Shikimori protected resources + + :return: Dictionary with proper user agent and autorization token + :rtype: Dict[str, str] + """ + header = self.user_agent + header['Authorization'] = f'Bearer {self._access_token}' + return header + + @logger.catch(onerror=lambda _: sys.exit(1)) + async def init_config(self, config: Union[str, Dict[str, str]]): + """ + Special method for initializing an object. + + This method calls several methods: + + - Validation of config and variables + - Customizing the session header user agent + - Getting access/refresh tokens if they are missing + - Refresh current tokens if they are not valid + + Otherwise, if only app name is provided, setting it + + :param config: Config dictionary or app name + :type config: Union[str, Dict[str, str]] + """ + logger.debug('Initializing client config') + self.validate_config(config) + self.validate_vars() + logger.debug('Setting User-Agent with current app name') + self.user_agent = self._app_name + + if self._restricted_mode: + return + + if isinstance(config, dict) and not self._access_token: + logger.debug('No tokens found') + tokens_data = await self.get_access_token() + self.update_tokens(tokens_data) + + if self.token_expired(): + logger.debug('Token has expired. Refreshing...') + await self.refresh_tokens() + + @logger.catch(onerror=lambda _: sys.exit(1)) + def validate_config(self, config: Union[str, Dict[str, str]]): + """ + Validates passed config dictionary and sets + client variables. + + If config is string, sets only app name and change value + of restrict mode of API object. + + Also, if config is dictionary and method detects + a stored configuration file, it replaces passed configuration + dictionary with the stored one. + + Raises MissingConfigData if some variables + are missing in config dictionary + + :param config: Config dictionary or app name for validation + :type config: Union[str, Dict[str, str]] + + :raises MissingConfigData: If any field is missing + (Not raises if there is a stored config) + """ + logger.debug('Validating client config') + if isinstance(config, str): + logger.debug('Detected app_name only. Activating restricted mode') + self._app_name = config + self.restricted_mode = True + return + + try: + logger.debug('Checking for stored config') + stored_config, is_config_stored = ConfigStore.config_validation( + config['app_name'], config['auth_code']) + + if is_config_stored: + logger.debug('Replacing passed config with stored one') + config = stored_config + + logger.debug('Extracting access tokens from config') + self._access_token = stored_config['access_token'] + self._refresh_token = stored_config['refresh_token'] + self._token_expire = int(stored_config['token_expire']) + + logger.debug('Extracting app related variables from config') + self._app_name = config['app_name'] + self._client_id = config['client_id'] + self._client_secret = config['client_secret'] + self._redirect_uri = config['redirect_uri'] + self._scopes = config['scopes'] + self._auth_code = config['auth_code'] + except KeyError as err: + raise MissingConfigData() from err + + @logger.catch(onerror=lambda _: sys.exit(1)) + def validate_vars(self): + """ + Validates variables and throws exception + if some vars are set to empty string. + + **Note:** Why throwing exception without catching it? + + This decision was made in order to prevent + future problems with the API due to incorrect variables. + Raising exception at the beginning of initialization + immediately indicates errors with the configuration dictionary + and future unnecessary checks related to this variables + + Also some notes about this method: + + - If redirect URI set to empty string, set to default URI. + - If authorization code set to empty string, + returns URL for getting auth code. + - If restricted mode is True, stops validation after app name check. + + :raises MissingAppVariable: If app variable in config is missing + :raises MissingAuthCode: If auth code is set to empty string + """ + if not self._app_name: + raise MissingAppVariable('name') + + if self.restricted_mode: + return + + if not self._client_id: + raise MissingAppVariable('Client ID') + + if not self._client_secret: + raise MissingAppVariable('Client Secret') + + if not self._redirect_uri: + self._redirect_uri = DEFAULT_REDIRECT_URI + + if not self._scopes: + raise MissingAppVariable('scopes') + + if self._auth_code: + return + + auth_link = self.endpoints.authorization_link(self._client_id, + self._redirect_uri, + self._scopes) + raise MissingAuthCode(auth_link) + + @logger.catch(onerror=lambda _: sys.exit(1)) + async def get_access_token(self, + refresh_token: bool = False) -> Tuple[str, str]: + """ + Returns access/refresh tokens from API request. + + If refresh_token flag is set to True, + returns refreshed access/refresh tokens. + + :param refresh_token: Flag for token refresh + :type refresh_token: bool + + :return: New access/refresh tokens tuple + :rtype: Tuple[str, str] + + :raises AccessTokenException: If token request failed + """ + data_body = { + 'client_id': self._client_id, + 'client_secret': self._client_secret + } + + if refresh_token: + logger.info('Refreshing current tokens') + data_body['grant_type'] = 'refresh_token' + data_body['refresh_token'] = self._refresh_token + else: + logger.info('Getting new tokens') + data_body['grant_type'] = 'authorization_code' + data_body['code'] = self._auth_code + data_body['redirect_uri'] = self._redirect_uri + + oauth_json: Dict[str, Any] = await self.request( + self.endpoints.oauth_token, + data=data_body, + request_type=RequestType.POST, + output_logging=False) + + try: + logger.debug('Returning new access and refresh tokens') + return oauth_json['access_token'], oauth_json['refresh_token'] + except KeyError as err: + error_info = dumps(oauth_json) + raise AccessTokenException(error_info) from err + + @logger.catch(onerror=lambda _: sys.exit(1)) + def update_tokens(self, tokens_data: Tuple[str, str]): + """ + Set new tokens and update token expire time. + + **Note:** This method also updates cache config file for + future use + + :param tokens_data: Tuple with access and refresh tokens + :type tokens_data: Tuple[str, str] + """ + logger.debug('Updating current tokens') + self.tokens = tokens_data + self.store_config() + + @logger.catch(onerror=lambda _: sys.exit(1)) + async def refresh_tokens(self): + """ + Manages tokens refreshing and caching. + + This method gets new access/refresh tokens and + updates them in current instance, as well as + caching new config. + """ + tokens_data = await self.get_access_token(refresh_token=True) + self.update_tokens(tokens_data) + + def token_expired(self): + """ + Checks if current access token is expired. + + :return: Result of token expiration check + :rtype: bool + """ + logger.debug('Checking if current time is greater ' + 'than current token expire time') + return int(time()) > self._token_expire + + @logger.catch(onerror=lambda _: sys.exit(1)) + def store_config(self): + """Updates token expire time and stores new config.""" + self._token_expire = Utils.get_new_expire_time(TOKEN_EXPIRE_TIME) + ConfigStore.save_config(self.config) + logger.debug('Expiration time and stored config file have been updated') + + @logger.catch + def protected_method_headers( + self, endpoint_name: str) -> Optional[Dict[str, str]]: + """ + This method utilizes protected method decoration logic + for such methods, which uses access tokens in some situations. + + Example: Calling animes.get_all(my_list=...), + mangas.get_all(my_list=...) and ranobes.get_all(my_list=...) + requires access token. + + :param endpoint_name: Name of API endpoint for calling as protected + :type endpoint_name: str + + :return: Authorization header with correct tokens or None + :rtype: Optional[Dict[str, str]] + """ + logger.debug(f'Checking the possibility of using "{endpoint_name}" ' + f'as protected method') + + if self.restricted_mode: + logger.debug(f'It is not possible to use "{endpoint_name}" ' + 'as the protected method ' + 'due to the restricted mode') + return None + + if self.token_expired(): + logger.debug('Token has expired. Refreshing...') + self.refresh_tokens() + + logger.debug('All checks for use of the protected ' + 'method have been passed') + return self.authorization_header + + @property + def closed(self) -> bool: + """Check if session is closed.""" + return self._session is None + + async def open(self) -> Client: + """Open session and return self.""" + if self.closed: + self._session = ClientSession() + await self.init_config(self._passed_config) + return self + + async def close(self) -> None: + """Close session.""" + if not self.closed: + await self._session.close() + self._session = None + + @sleep_and_retry + @limits(calls=MAX_CALLS_PER_SECOND, period=ONE_SECOND) + @limits(calls=MAX_CALLS_PER_MINUTE, period=ONE_MINUTE) + async def request( + self, + url: str, + data: Optional[Dict[str, str]] = None, + bytes_data: Optional[bytes] = None, + headers: Optional[Dict[str, str]] = None, + query: Optional[Dict[str, str]] = None, + request_type: RequestType = RequestType.GET, + output_logging: bool = True, + ) -> Optional[Union[List[Dict[str, Any]], Dict[str, Any], str]]: + """ + Create request and return response JSON. + + This method uses ratelimit library for rate limiting + requests (Shikimori API limit: 90rpm and 5rps) + + **Note:** To address duplication of methods + for different request methods, this method + uses RequestType enum + + :param url: URL for making request + :type url: str + + :param data: Request body data + :type data: Optional[Dict[str, str]] + + :param bytes_data: Request body data in bytes + :type bytes_data: Optional[bytes] + + :param headers: Custom headers for request + :type headers: Optional[Dict[str, str]] + + :param query: Query data for request + :type query: Optional[Dict[str, str]] + + :param request_type: Type of current request + :type request_type: RequestType + + :param output_logging: Parameter for logging JSON response + :type output_logging: bool + + :return: Response JSON, text or status code + :rtype: Optional[Union[List[Dict[str, Any]], Dict[str, Any], str]] + """ + if self.closed: + return + + logger.info(f'{request_type.value} {url}') + if output_logging: + logger.debug(f'Request info details: {data=}, {headers=}, {query=}') + + if data is not None and bytes_data is not None: + logger.debug( + 'Request body data and bytes data are sent at the same time. ' + 'Splitting into one...') + bytes_data = {**bytes_data, **data} + data = None + + if request_type == RequestType.GET: + response = await self._session.get(url, + headers=headers, + params=query) + elif request_type == RequestType.POST: + response = await self._session.post(url, + data=bytes_data, + json=data, + headers=headers, + params=query) + elif request_type == RequestType.PUT: + response = await self._session.put(url, + data=bytes_data, + json=data, + headers=headers, + params=query) + elif request_type == RequestType.PATCH: + response = await self._session.patch(url, + data=bytes_data, + json=data, + headers=headers, + params=query) + elif request_type == RequestType.DELETE: + response = await self._session.delete(url, + data=bytes_data, + json=data, + headers=headers, + params=query) + else: + logger.debug('Unknown request_type. Returning None') + return None + + logger.debug('Extracting JSON from response') + try: + json_response = await response.json() + except ContentTypeError: + logger.debug('Response is not JSON. Returning status code/text') + return await Utils.extract_empty_response_data(response) + + if output_logging: + logger.debug( + 'Successful extraction. ' + f'Here are the details of the response: {json_response}') + if json_response is None and response.status == 200: + logger.debug('Response is empty. Returning status code/text') + return await Utils.extract_empty_response_data(response) + + return json_response + + async def multiple_requests(self, requests: List[Callable[..., RT]]): + """ + Make multiple requests to API at the same time. + + :param requests: List of requests + :type requests: List[Callable[..., RT]] + + :return: List of responses + :rtype: List[Union[BaseException, RT]] + """ + if self.closed: + return [] + + return await asyncio.gather(*requests, return_exceptions=True) + + async def __aenter__(self) -> Client: + """Async context manager entry point.""" + return await self.open() + + async def __aexit__(self, *args) -> None: + """Async context manager exit point.""" + await self.close() diff --git a/shikithon/decorators.py b/shikithon/decorators.py deleted file mode 100644 index 37a0fd7f..00000000 --- a/shikithon/decorators.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Custom decorators for API class.""" -from __future__ import annotations - -from typing import TYPE_CHECKING, Optional - -from loguru import logger - -if TYPE_CHECKING: - from shikithon.api import API - - -def protected_method(scope: Optional[str] = None): - """ - Decorator for protected API methods. - - This method is used for all protected methods - and checks the expiration of the access token. - - When the access token is no longer valid, - triggers the token update function. - - Also, this decorator take a scope parameter - for checking, if current app is allowed to access - protected method. - """ - - def protected_method_decorator(function): - - def protected_method_wrapper(api: API, *args, **kwargs): - """ - Decorator's wrapper function. - - Check for token expire time. - If needed, triggers token refresh function. - - :return: None if API object is in restricted mode - or if required scope is missing - :rtype: None - """ - logger.debug('Checking the possibility of using a protected method') - if api.restricted_mode: - logger.debug('It is not possible to use the protected method ' - 'due to the restricted mode') - return None - - if scope and scope not in api.scopes_list: - logger.debug(f'Protected method cannot be used due to the ' - f'absence of "{scope}" scope') - return None - - if api.token_expired(): - logger.debug('Token has expired. Refreshing...') - api.refresh_tokens() - logger.debug('All checks for use of the protected ' - 'method have been passed') - return function(api, *args, **kwargs) - - return protected_method_wrapper - - return protected_method_decorator - - -def method_endpoint(method_endpoint_name: str): - """ - Decorator for logging method endpoint. - """ - - def endpoint_logger_decorator(function): - - def endpoint_logger_wrapper(*args, **kwargs): - """ - Decorator's wrapper function. - Logs endpoint of method - """ - logger.debug(f'Executing "{method_endpoint_name}" method') - return function(*args, **kwargs) - - return endpoint_logger_wrapper - - return endpoint_logger_decorator diff --git a/shikithon/decorators/__init__.py b/shikithon/decorators/__init__.py new file mode 100644 index 00000000..4c7b8da9 --- /dev/null +++ b/shikithon/decorators/__init__.py @@ -0,0 +1,6 @@ +"""Custom decorators for API class.""" + +from .method_endpoint import method_endpoint +from .protected_method import protected_method + +__all__ = ['protected_method', 'method_endpoint'] diff --git a/shikithon/decorators/method_endpoint.py b/shikithon/decorators/method_endpoint.py new file mode 100644 index 00000000..0ccf0059 --- /dev/null +++ b/shikithon/decorators/method_endpoint.py @@ -0,0 +1,55 @@ +"""Decorator for method endpoint logging""" +from __future__ import annotations + +from typing import Any, Callable, Dict, Tuple, TypeVar + +from loguru import logger + +RT = TypeVar('RT') + + +def method_endpoint( + method_endpoint_name: str +) -> Callable[[Callable[..., RT]], Callable[..., RT]]: + """ + Decorator for logging method endpoint. + + :param method_endpoint_name: Name of method endpoint + :type method_endpoint_name: str + + :return: Decorator function + :rtype: Callable[[Callable[..., RT]], Callable[..., RT]] + """ + + def endpoint_logger_decorator( + function: Callable[..., RT]) -> Callable[..., RT]: + """Endpoint logger decorator. + + :param function: Function to decorate + :type function: Callable[..., RT] + + :return: Decorated function + :rtype: Callable[..., RT] + """ + + def endpoint_logger_wrapper(*args: Tuple[Any], + **kwargs: Dict[str, Any]) -> RT: + """ + Decorator's wrapper function. + Logs endpoint of method + + :param args: Positional arguments + :type args: Tuple[Any] + + :param kwargs: Keyword arguments + :type kwargs: Dict[str, Any] + + :return: Result of decorated function + :rtype: RT + """ + logger.debug(f'Executing "{method_endpoint_name}" method') + return function(*args, **kwargs) + + return endpoint_logger_wrapper + + return endpoint_logger_decorator diff --git a/shikithon/decorators/protected_method.py b/shikithon/decorators/protected_method.py new file mode 100644 index 00000000..ac72d793 --- /dev/null +++ b/shikithon/decorators/protected_method.py @@ -0,0 +1,102 @@ +"""Decorator for protected methods""" +from __future__ import annotations + +from typing import Any, Callable, Optional, Tuple, TypeVar + +from loguru import logger + +from ..resources.base_resource import BaseResource + +RT = TypeVar('RT') + + +def protected_method( + client_attr: str, + scope: Optional[str] = None, + fallback: Optional[Any] = None +) -> Callable[[Callable[..., RT]], Callable[..., RT]]: + """ + Decorator for protected API methods. + + This method is used for all protected methods + and checks the expiration of the access token. + + When the access token is no longer valid, + triggers the token update function. + + Also, this decorator take a scope parameter + for checking, if current app is allowed to access + protected method. + + :param client_attr: Name of client attribute + :type client_attr: str + + :param scope: Scope of app + :type scope: Optional[str] + + :param fallback: Fallback value + :type fallback: Optional[Any] + + :return: Decorator function + :rtype: Callable[[Callable[..., RT]], Callable[..., RT]] + """ + + def protected_method_decorator( + function: Callable[..., RT]) -> Callable[..., RT]: + """Protected method decorator. + + :param function: Function to decorate + :type function: Callable[..., RT] + + :return: Decorated function + :rtype: Callable[..., RT] + """ + + def protected_method_wrapper(self: BaseResource, *args: Tuple[Any], + **kwargs: Any) -> RT: + """ + Decorator's wrapper function. + + Check for token expire time. + If needed, triggers token refresh function. + + :param self: Resource instance + :type self: BaseResource + + :param args: Positional arguments + :type args: Tuple[Any] + + :param kwargs: Keyword arguments + :type kwargs: Any + + :return: Fallback function if API object is in restricted mode + or if required scope is missing + :rtype: RT + """ + client = getattr(self, client_attr) + logger.debug('Checking the possibility of using a protected method') + + async def fallback_function() -> RT: + return fallback + + if client.restricted_mode: + logger.debug('It is not possible to use the protected method ' + 'due to the restricted mode') + return fallback_function() + + if scope and scope not in client.scopes_list: + logger.debug(f'Protected method cannot be used due to the ' + f'absence of "{scope}" scope') + return fallback_function() + + if client.token_expired(): + logger.debug('Token has expired. Refreshing...') + client.refresh_tokens() + + logger.debug('All checks for use of the protected ' + 'method have been passed') + return function(self, *args, **kwargs) + + return protected_method_wrapper + + return protected_method_decorator diff --git a/shikithon/endpoints.py b/shikithon/endpoints.py index d4067a2c..96660231 100644 --- a/shikithon/endpoints.py +++ b/shikithon/endpoints.py @@ -6,7 +6,7 @@ """ from typing import Union -from shikithon.utils import Utils +from .utils import Utils class Endpoints: @@ -138,7 +138,7 @@ def authorization_link(self, client_id: str, redirect_uri: str, :return: Link for getting authorization code :rtype: str """ - query_str = Utils.prepare_query_dict({ + query_str = Utils.convert_to_query_string({ 'client_id': client_id, 'redirect_uri': redirect_uri, 'response_type': 'code', @@ -913,7 +913,7 @@ def ranobes(self) -> str: :return: Ranobes list endpoint link :rtype: str """ - return f'{self.base_url}/ranobes' + return f'{self.base_url}/ranobe' def ranobe(self, ranobe_id: int) -> str: """ diff --git a/shikithon/enums/__init__.py b/shikithon/enums/__init__.py new file mode 100644 index 00000000..64c2d059 --- /dev/null +++ b/shikithon/enums/__init__.py @@ -0,0 +1,51 @@ +"""Enums for shikithon API class.""" + +from .anime import AnimeCensorship +from .anime import AnimeDuration +from .anime import AnimeKind +from .anime import AnimeList +from .anime import AnimeOrder +from .anime import AnimeRating +from .anime import AnimeStatus +from .anime import AnimeTopicKind +from .club import CommentPolicy +from .club import ImageUploadPolicy +from .club import JoinPolicy +from .club import PagePolicy +from .club import TopicPolicy +from .comment import CommentableType +from .favorite import FavoriteLinkedType +from .history import TargetType +from .manga import MangaCensorship +from .manga import MangaKind +from .manga import MangaList +from .manga import MangaOrder +from .manga import MangaStatus +from .message import MessageType +from .person import PersonKind +from .ranobe import RanobeCensorship +from .ranobe import RanobeList +from .ranobe import RanobeOrder +from .ranobe import RanobeStatus +from .request import RequestType +from .response import ResponseCode +from .style import OwnerType +from .topic import ForumType +from .topic import TopicLinkedType +from .topic import TopicType +from .user_rate import UserRateStatus +from .user_rate import UserRateTarget +from .user_rate import UserRateType +from .video import VideoKind + +__all__ = [ + 'AnimeCensorship', 'AnimeDuration', 'AnimeKind', 'AnimeTopicKind', + 'AnimeList', 'AnimeOrder', 'AnimeRating', 'AnimeStatus', 'CommentPolicy', + 'ImageUploadPolicy', 'JoinPolicy', 'PagePolicy', 'TopicPolicy', + 'CommentableType', 'TargetType', 'FavoriteLinkedType', 'MangaCensorship', + 'MangaKind', 'MangaList', 'MangaOrder', 'MangaStatus', 'MessageType', + 'PersonKind', 'RanobeCensorship', 'RanobeList', 'RanobeOrder', + 'RanobeStatus', 'RequestType', 'ResponseCode', 'OwnerType', 'ForumType', + 'TopicLinkedType', 'TopicType', 'UserRateStatus', 'UserRateTarget', + 'UserRateType', 'VideoKind' +] diff --git a/shikithon/enums/anime.py b/shikithon/enums/anime.py index b3c32136..12b650f8 100644 --- a/shikithon/enums/anime.py +++ b/shikithon/enums/anime.py @@ -1,5 +1,5 @@ """Enums for /api/animes.""" -from shikithon.enums.enhanced_enum import EnhancedEnum +from .enhanced_enum import EnhancedEnum class AnimeOrder(EnhancedEnum): @@ -50,6 +50,13 @@ class AnimeStatus(EnhancedEnum): NOT_RELEASED = '!released' +class AnimeTopicKind(EnhancedEnum): + """Contains constants related for getting certain kind of anime topic.""" + ANONS = 'anons' + ONGOING = 'ongoing' + RELEASED = 'released' + + class AnimeDuration(EnhancedEnum): """Contains constants related for getting certain duration of anime.""" SHORT = 'S' diff --git a/shikithon/enums/club.py b/shikithon/enums/club.py index 6d2eae56..2d99c09f 100644 --- a/shikithon/enums/club.py +++ b/shikithon/enums/club.py @@ -1,5 +1,5 @@ """Enums for /api/clubs.""" -from shikithon.enums.enhanced_enum import EnhancedEnum +from .enhanced_enum import EnhancedEnum class JoinPolicy(EnhancedEnum): diff --git a/shikithon/enums/comment.py b/shikithon/enums/comment.py index eba6ca98..991a3f0b 100644 --- a/shikithon/enums/comment.py +++ b/shikithon/enums/comment.py @@ -1,5 +1,5 @@ """Enums for /api/comments.""" -from shikithon.enums.enhanced_enum import EnhancedEnum +from .enhanced_enum import EnhancedEnum class CommentableType(EnhancedEnum): diff --git a/shikithon/enums/favorite.py b/shikithon/enums/favorite.py index 5903e3e2..c18aa217 100644 --- a/shikithon/enums/favorite.py +++ b/shikithon/enums/favorite.py @@ -1,5 +1,5 @@ """Enums for /api/favorites.""" -from shikithon.enums.enhanced_enum import EnhancedEnum +from .enhanced_enum import EnhancedEnum class FavoriteLinkedType(EnhancedEnum): diff --git a/shikithon/enums/history.py b/shikithon/enums/history.py index 3cd461d6..1e56959b 100644 --- a/shikithon/enums/history.py +++ b/shikithon/enums/history.py @@ -1,5 +1,5 @@ """Enums for /api/users/:id/history""" -from shikithon.enums.enhanced_enum import EnhancedEnum +from .enhanced_enum import EnhancedEnum class TargetType(EnhancedEnum): diff --git a/shikithon/enums/manga.py b/shikithon/enums/manga.py index 89563e35..5e622dab 100644 --- a/shikithon/enums/manga.py +++ b/shikithon/enums/manga.py @@ -1,5 +1,5 @@ """Enums for /api/mangas.""" -from shikithon.enums.enhanced_enum import EnhancedEnum +from .enhanced_enum import EnhancedEnum class MangaOrder(EnhancedEnum): diff --git a/shikithon/enums/message.py b/shikithon/enums/message.py index c78e6946..a5caf202 100644 --- a/shikithon/enums/message.py +++ b/shikithon/enums/message.py @@ -1,5 +1,5 @@ """Enums for /api/users/:id/messages""" -from shikithon.enums.enhanced_enum import EnhancedEnum +from .enhanced_enum import EnhancedEnum class MessageType(EnhancedEnum): diff --git a/shikithon/enums/person.py b/shikithon/enums/person.py index 75cdbd93..f44380ce 100644 --- a/shikithon/enums/person.py +++ b/shikithon/enums/person.py @@ -4,7 +4,7 @@ Also PersonKind enum is use in /api/favorites, when linked_type is Person """ -from shikithon.enums.enhanced_enum import EnhancedEnum +from .enhanced_enum import EnhancedEnum class PersonKind(EnhancedEnum): diff --git a/shikithon/enums/ranobe.py b/shikithon/enums/ranobe.py index 89bf5ae0..ddc640c1 100644 --- a/shikithon/enums/ranobe.py +++ b/shikithon/enums/ranobe.py @@ -1,5 +1,5 @@ """Enums for /api/ranobe.""" -from shikithon.enums.enhanced_enum import EnhancedEnum +from .enhanced_enum import EnhancedEnum class RanobeOrder(EnhancedEnum): diff --git a/shikithon/enums/request.py b/shikithon/enums/request.py index 8473925d..f8032798 100644 --- a/shikithon/enums/request.py +++ b/shikithon/enums/request.py @@ -1,5 +1,5 @@ """Enums for types of requests.""" -from shikithon.enums.enhanced_enum import EnhancedEnum +from .enhanced_enum import EnhancedEnum class RequestType(EnhancedEnum): diff --git a/shikithon/enums/response.py b/shikithon/enums/response.py index 749431c0..b98864cc 100644 --- a/shikithon/enums/response.py +++ b/shikithon/enums/response.py @@ -1,5 +1,5 @@ """Enums for response status codes.""" -from shikithon.enums.enhanced_enum import EnhancedEnum +from .enhanced_enum import EnhancedEnum class ResponseCode(EnhancedEnum): diff --git a/shikithon/enums/style.py b/shikithon/enums/style.py index 86686cfa..61f604f8 100644 --- a/shikithon/enums/style.py +++ b/shikithon/enums/style.py @@ -1,5 +1,5 @@ """Enums for /api/styles.""" -from shikithon.enums.enhanced_enum import EnhancedEnum +from .enhanced_enum import EnhancedEnum class OwnerType(EnhancedEnum): diff --git a/shikithon/enums/topic.py b/shikithon/enums/topic.py index 8adf8058..11f6c8de 100644 --- a/shikithon/enums/topic.py +++ b/shikithon/enums/topic.py @@ -1,5 +1,5 @@ """Enums for /api/topics.""" -from shikithon.enums.enhanced_enum import EnhancedEnum +from .enhanced_enum import EnhancedEnum class TopicType(EnhancedEnum): diff --git a/shikithon/enums/user_rate.py b/shikithon/enums/user_rate.py index 2295ff4a..03f27619 100644 --- a/shikithon/enums/user_rate.py +++ b/shikithon/enums/user_rate.py @@ -1,5 +1,5 @@ """Enums for /api/user_rates.""" -from shikithon.enums.enhanced_enum import EnhancedEnum +from .enhanced_enum import EnhancedEnum class UserRateType(EnhancedEnum): diff --git a/shikithon/enums/video.py b/shikithon/enums/video.py index 0dc569f7..83d87967 100644 --- a/shikithon/enums/video.py +++ b/shikithon/enums/video.py @@ -1,5 +1,5 @@ """Enums for /api/animes/:anime_id/videos.""" -from shikithon.enums.enhanced_enum import EnhancedEnum +from .enhanced_enum import EnhancedEnum class VideoKind(EnhancedEnum): diff --git a/shikithon/exceptions.py b/shikithon/exceptions.py deleted file mode 100644 index cc6c9d8d..00000000 --- a/shikithon/exceptions.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Custom exceptions for API class.""" - - -class MissingConfigData(Exception): - - def __init__(self): - super().__init__('It is impossible to initialize an API object' - 'without missing variables. ' - 'Recheck your config and try again.') - - -class MissingAppVariable(Exception): - - def __init__(self, variable_name: str): - super().__init__(f'It is impossible to initialize an API object' - f'without missing variable "{variable_name}". ' - f'Recheck your config and try again.') - - -class MissingAuthCode(Exception): - - def __init__(self, auth_link: str): - super().__init__('It is impossible to initialize an API object' - 'without missing auth code. To get one, go to ' - f'{auth_link} and insert code in config.') - - -class AccessTokenException(Exception): - - def __init__(self, error_message: str): - super().__init__( - 'An error occurred while receiving tokens, ' - f'here is the information from the response: {error_message}') diff --git a/shikithon/exceptions/__init__.py b/shikithon/exceptions/__init__.py new file mode 100644 index 00000000..dd9188e0 --- /dev/null +++ b/shikithon/exceptions/__init__.py @@ -0,0 +1,14 @@ +"""Exceptions for shikithon API class. + +Used for raising on validation of config/app data. +""" + +from .access_token_exception import AccessTokenException +from .missing_app_variable import MissingAppVariable +from .missing_auth_code import MissingAuthCode +from .missing_config_data import MissingConfigData + +__all__ = [ + 'AccessTokenException', 'MissingAuthCode', 'MissingAppVariable', + 'MissingConfigData' +] diff --git a/shikithon/exceptions/access_token_exception.py b/shikithon/exceptions/access_token_exception.py new file mode 100644 index 00000000..125c8d4c --- /dev/null +++ b/shikithon/exceptions/access_token_exception.py @@ -0,0 +1,9 @@ +"""Exception for raising on access token errors.""" + + +class AccessTokenException(Exception): + + def __init__(self, error_message: str): + super().__init__( + 'An error occurred while receiving tokens, ' + f'here is the information from the response: {error_message}') diff --git a/shikithon/exceptions/missing_app_variable.py b/shikithon/exceptions/missing_app_variable.py new file mode 100644 index 00000000..55a2a540 --- /dev/null +++ b/shikithon/exceptions/missing_app_variable.py @@ -0,0 +1,9 @@ +"""Exception for raising on missing app variable.""" + + +class MissingAppVariable(Exception): + + def __init__(self, variable_name: str): + super().__init__(f'It is impossible to initialize an API object' + f'without missing variable "{variable_name}". ' + f'Recheck your config and try again.') diff --git a/shikithon/exceptions/missing_auth_code.py b/shikithon/exceptions/missing_auth_code.py new file mode 100644 index 00000000..7daf4ceb --- /dev/null +++ b/shikithon/exceptions/missing_auth_code.py @@ -0,0 +1,9 @@ +"""Exception for raising on missing authorization cade.""" + + +class MissingAuthCode(Exception): + + def __init__(self, auth_link: str): + super().__init__('It is impossible to initialize an API object' + 'without missing auth code. To get one, go to ' + f'{auth_link} and insert code in config.') diff --git a/shikithon/exceptions/missing_config_data.py b/shikithon/exceptions/missing_config_data.py new file mode 100644 index 00000000..9c98ce04 --- /dev/null +++ b/shikithon/exceptions/missing_config_data.py @@ -0,0 +1,9 @@ +"""Exception for raising on missing config data.""" + + +class MissingConfigData(Exception): + + def __init__(self): + super().__init__('It is impossible to initialize an API object' + 'without missing variables. ' + 'Recheck your config and try again.') diff --git a/shikithon/models/__init__.py b/shikithon/models/__init__.py new file mode 100644 index 00000000..117a74b6 --- /dev/null +++ b/shikithon/models/__init__.py @@ -0,0 +1,45 @@ +"""Models for the Shikimori API.""" + +from .abuse_response import AbuseResponse +from .achievement import Achievement +from .anime import Anime +from .ban import Ban +from .birthday import Birthday +from .calendar_event import CalendarEvent +from .character import Character +from .club import Club +from .club_image import ClubImage +from .comment import Comment +from .created_user_image import CreatedUserImage +from .creator import Creator +from .dialog import Dialog +from .favourites import Favourites +from .forum import Forum +from .franchise_tree import FranchiseTree +from .genre import Genre +from .history import History +from .link import Link +from .manga import Manga +from .message import Message +from .person import Person +from .publisher import Publisher +from .ranobe import Ranobe +from .relation import Relation +from .screenshot import Screenshot +from .studio import Studio +from .style import Style +from .topic import Topic +from .unread_messages import UnreadMessages +from .user import User +from .user_list import UserList +from .user_rate import UserRate +from .video import Video + +__all__ = [ + 'Anime', 'Achievement', 'AbuseResponse', 'Ban', 'Birthday', 'CalendarEvent', + 'Character', 'Club', 'ClubImage', 'Comment', 'CreatedUserImage', 'Creator', + 'Dialog', 'Favourites', 'Forum', 'FranchiseTree', 'Genre', 'History', + 'Link', 'Manga', 'Message', 'Person', 'Publisher', 'Ranobe', 'Relation', + 'Screenshot', 'Studio', 'Style', 'Topic', 'UnreadMessages', 'User', + 'UserList', 'UserRate', 'Video' +] diff --git a/shikithon/models/anime.py b/shikithon/models/anime.py index 82f6be5c..54c619ac 100644 --- a/shikithon/models/anime.py +++ b/shikithon/models/anime.py @@ -4,14 +4,14 @@ from pydantic import BaseModel -from shikithon.models.genre import Genre -from shikithon.models.image import Image -from shikithon.models.screenshot import Screenshot -from shikithon.models.studio import Studio -from shikithon.models.user_rate import UserRate -from shikithon.models.user_rate_score import UserRateScore -from shikithon.models.user_rate_status import UserRateStatus -from shikithon.models.video import Video +from .genre import Genre +from .image import Image +from .screenshot import Screenshot +from .studio import Studio +from .user_rate import UserRate +from .user_rate_score import UserRateScore +from .user_rate_status import UserRateStatus +from .video import Video class Anime(BaseModel): diff --git a/shikithon/models/ban.py b/shikithon/models/ban.py index 415b7fb8..d6d80c12 100644 --- a/shikithon/models/ban.py +++ b/shikithon/models/ban.py @@ -4,8 +4,8 @@ from pydantic import BaseModel -from shikithon.models.comment import Comment -from shikithon.models.user import User +from .comment import Comment +from .user import User class Ban(BaseModel): diff --git a/shikithon/models/birthday.py b/shikithon/models/birthday.py new file mode 100644 index 00000000..e5943980 --- /dev/null +++ b/shikithon/models/birthday.py @@ -0,0 +1,13 @@ +"""Submodel for people.py""" +from pydantic import BaseModel + + +class Birthday(BaseModel): + """Birthday model class. + + Used to represent birthday of person. + """ + + day: int + month: int + year: int diff --git a/shikithon/models/calendar_event.py b/shikithon/models/calendar_event.py index 78f6051d..4ddb63f9 100644 --- a/shikithon/models/calendar_event.py +++ b/shikithon/models/calendar_event.py @@ -4,7 +4,7 @@ from pydantic import BaseModel -from shikithon.models.anime import Anime +from .anime import Anime class CalendarEvent(BaseModel): diff --git a/shikithon/models/character.py b/shikithon/models/character.py index 1a9a2c26..05bc02ff 100644 --- a/shikithon/models/character.py +++ b/shikithon/models/character.py @@ -4,10 +4,10 @@ from pydantic import BaseModel -from shikithon.models.anime import Anime -from shikithon.models.image import Image -from shikithon.models.manga import Manga -from shikithon.models.seyu import Seyu +from .anime import Anime +from .image import Image +from .manga import Manga +from .seyu import Seyu class Character(BaseModel): diff --git a/shikithon/models/club.py b/shikithon/models/club.py index cf2de738..91190d03 100644 --- a/shikithon/models/club.py +++ b/shikithon/models/club.py @@ -1,7 +1,7 @@ """Model for /api/clubs""" from pydantic import BaseModel -from shikithon.models.logo import Logo +from .logo import Logo class Club(BaseModel): diff --git a/shikithon/models/comment.py b/shikithon/models/comment.py index 8a4f7e16..85ada7ff 100644 --- a/shikithon/models/comment.py +++ b/shikithon/models/comment.py @@ -4,7 +4,7 @@ from pydantic import BaseModel -from shikithon.models.user import User +from .user import User class Comment(BaseModel): diff --git a/shikithon/models/creator.py b/shikithon/models/creator.py index 8fb114e0..646098e2 100644 --- a/shikithon/models/creator.py +++ b/shikithon/models/creator.py @@ -3,8 +3,8 @@ from pydantic import BaseModel -from shikithon.models.character import Character -from shikithon.models.people import People +from .character import Character +from .person import Person class Creator(BaseModel): @@ -12,4 +12,4 @@ class Creator(BaseModel): roles: List[str] roles_russian: List[str] character: Optional[Character] - person: Optional[People] + person: Optional[Person] diff --git a/shikithon/models/dialog.py b/shikithon/models/dialog.py index ba42b175..5242866b 100644 --- a/shikithon/models/dialog.py +++ b/shikithon/models/dialog.py @@ -1,8 +1,8 @@ """Model for api/dialogs""" from pydantic import BaseModel -from shikithon.models.message import Message -from shikithon.models.user import User +from .message import Message +from .user import User class Dialog(BaseModel): diff --git a/shikithon/models/favourites.py b/shikithon/models/favourites.py index bbfd06b1..adc21af2 100644 --- a/shikithon/models/favourites.py +++ b/shikithon/models/favourites.py @@ -3,7 +3,7 @@ from pydantic import BaseModel -from shikithon.models.favourite import Favourite +from .favourite import Favourite class Favourites(BaseModel): diff --git a/shikithon/models/franchise_tree.py b/shikithon/models/franchise_tree.py index 29a091ed..b537f0af 100644 --- a/shikithon/models/franchise_tree.py +++ b/shikithon/models/franchise_tree.py @@ -3,8 +3,8 @@ from pydantic import BaseModel -from shikithon.models.tree_link import TreeLink -from shikithon.models.tree_node import TreeNode +from .tree_link import TreeLink +from .tree_node import TreeNode class FranchiseTree(BaseModel): diff --git a/shikithon/models/history.py b/shikithon/models/history.py index 6b59abaa..ef52be0c 100644 --- a/shikithon/models/history.py +++ b/shikithon/models/history.py @@ -4,8 +4,8 @@ from pydantic import BaseModel -from shikithon.models.anime import Anime -from shikithon.models.manga import Manga +from .anime import Anime +from .manga import Manga class History(BaseModel): diff --git a/shikithon/models/linked_topic.py b/shikithon/models/linked_topic.py index e3a2422a..85498278 100644 --- a/shikithon/models/linked_topic.py +++ b/shikithon/models/linked_topic.py @@ -3,7 +3,7 @@ from pydantic import BaseModel -from shikithon.models.image import Image +from .image import Image class LinkedTopic(BaseModel): diff --git a/shikithon/models/manga.py b/shikithon/models/manga.py index aecf8939..235cf3ae 100644 --- a/shikithon/models/manga.py +++ b/shikithon/models/manga.py @@ -3,7 +3,7 @@ from pydantic import BaseModel -from shikithon.models.image import Image +from .image import Image class Manga(BaseModel): diff --git a/shikithon/models/message.py b/shikithon/models/message.py index e2c2b2fb..0426ac57 100644 --- a/shikithon/models/message.py +++ b/shikithon/models/message.py @@ -2,10 +2,11 @@ from datetime import datetime from typing import Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel +from pydantic import Field -from shikithon.models.linked_topic import LinkedTopic -from shikithon.models.user import User +from .linked_topic import LinkedTopic +from .user import User class Message(BaseModel): diff --git a/shikithon/models/people_roles.py b/shikithon/models/people_roles.py index 12fff8fb..e8bf06ac 100644 --- a/shikithon/models/people_roles.py +++ b/shikithon/models/people_roles.py @@ -3,8 +3,8 @@ from pydantic import BaseModel -from shikithon.models.anime import Anime -from shikithon.models.character import Character +from .anime import Anime +from .character import Character class PeopleRoles(BaseModel): diff --git a/shikithon/models/people_works.py b/shikithon/models/people_works.py index ba26654f..79c040a3 100644 --- a/shikithon/models/people_works.py +++ b/shikithon/models/people_works.py @@ -3,8 +3,8 @@ from pydantic import BaseModel -from shikithon.models.anime import Anime -from shikithon.models.manga import Manga +from .anime import Anime +from .manga import Manga class PeopleWorks(BaseModel): diff --git a/shikithon/models/people.py b/shikithon/models/person.py similarity index 79% rename from shikithon/models/people.py rename to shikithon/models/person.py index 6bec68db..ca4c0f43 100644 --- a/shikithon/models/people.py +++ b/shikithon/models/person.py @@ -4,12 +4,13 @@ from pydantic import BaseModel -from shikithon.models.image import Image -from shikithon.models.people_roles import PeopleRoles -from shikithon.models.people_works import PeopleWorks +from .birthday import Birthday +from .image import Image +from .people_roles import PeopleRoles +from .people_works import PeopleWorks -class People(BaseModel): +class Person(BaseModel): """Represents person entity.""" id: int name: str @@ -18,7 +19,7 @@ class People(BaseModel): url: str japanese: Optional[str] job_title: Optional[str] - birthday: Optional[str] + birthday: Optional[Birthday] website: Optional[str] groupped_roles: Optional[List[Tuple[str, int]]] roles: Optional[List[PeopleRoles]] diff --git a/shikithon/models/ranobe.py b/shikithon/models/ranobe.py index 0fd6ced1..8654046e 100644 --- a/shikithon/models/ranobe.py +++ b/shikithon/models/ranobe.py @@ -3,11 +3,11 @@ from pydantic import BaseModel -from shikithon.models.genre import Genre -from shikithon.models.image import Image -from shikithon.models.user_rate import UserRate -from shikithon.models.user_rate_score import UserRateScore -from shikithon.models.user_rate_status import UserRateStatus +from .genre import Genre +from .image import Image +from .user_rate import UserRate +from .user_rate_score import UserRateScore +from .user_rate_status import UserRateStatus class Ranobe(BaseModel): diff --git a/shikithon/models/rating_list.py b/shikithon/models/rating_list.py index 417a1175..6d6a365d 100644 --- a/shikithon/models/rating_list.py +++ b/shikithon/models/rating_list.py @@ -3,7 +3,7 @@ from pydantic import BaseModel -from shikithon.models.rating import Rating +from .rating import Rating class RatingList(BaseModel): diff --git a/shikithon/models/relation.py b/shikithon/models/relation.py index 6d383a5b..3da6b1d6 100644 --- a/shikithon/models/relation.py +++ b/shikithon/models/relation.py @@ -3,8 +3,8 @@ from pydantic import BaseModel -from shikithon.models.anime import Anime -from shikithon.models.manga import Manga +from .anime import Anime +from .manga import Manga class Relation(BaseModel): diff --git a/shikithon/models/score_list.py b/shikithon/models/score_list.py index f8eaf35e..517dbe62 100644 --- a/shikithon/models/score_list.py +++ b/shikithon/models/score_list.py @@ -3,7 +3,7 @@ from pydantic import BaseModel -from shikithon.models.score import Score +from .score import Score class ScoreList(BaseModel): diff --git a/shikithon/models/seyu.py b/shikithon/models/seyu.py index a5414d34..0d2e6346 100644 --- a/shikithon/models/seyu.py +++ b/shikithon/models/seyu.py @@ -1,7 +1,7 @@ """Submodel for character.py""" from pydantic import BaseModel -from shikithon.models.image import Image +from .image import Image class Seyu(BaseModel): diff --git a/shikithon/models/stats.py b/shikithon/models/stats.py index b9399fc6..2185d35c 100644 --- a/shikithon/models/stats.py +++ b/shikithon/models/stats.py @@ -1,13 +1,14 @@ """Submodel for user.py""" from typing import Dict, List, Optional, Union -from pydantic import BaseModel, Field +from pydantic import BaseModel +from pydantic import Field -from shikithon.models.activity import Activity -from shikithon.models.rating_list import RatingList -from shikithon.models.score_list import ScoreList -from shikithon.models.status_list import StatusList -from shikithon.models.type_list import TypeList +from .activity import Activity +from .rating_list import RatingList +from .score_list import ScoreList +from .status_list import StatusList +from .type_list import TypeList class Stats(BaseModel): diff --git a/shikithon/models/status_list.py b/shikithon/models/status_list.py index 4b81d27b..6bee8110 100644 --- a/shikithon/models/status_list.py +++ b/shikithon/models/status_list.py @@ -3,7 +3,7 @@ from pydantic import BaseModel -from shikithon.models.status import Status +from .status import Status class StatusList(BaseModel): diff --git a/shikithon/models/style.py b/shikithon/models/style.py index b950eaa2..e0b72eb2 100644 --- a/shikithon/models/style.py +++ b/shikithon/models/style.py @@ -7,11 +7,11 @@ class Style(BaseModel): """Represents style entity.""" - id: int - owner_id: int - owner_type: str + id: Optional[int] + owner_id: Optional[int] + owner_type: Optional[str] name: str css: str compiled_css: Optional[str] - created_at: datetime - updated_at: datetime + created_at: Optional[datetime] + updated_at: Optional[datetime] diff --git a/shikithon/models/topic.py b/shikithon/models/topic.py index 59436194..093d3364 100644 --- a/shikithon/models/topic.py +++ b/shikithon/models/topic.py @@ -4,10 +4,11 @@ from pydantic import BaseModel -from shikithon.models.anime import Anime -from shikithon.models.forum import Forum -from shikithon.models.manga import Manga -from shikithon.models.user import User +from .anime import Anime +from .club import Club +from .forum import Forum +from .manga import Manga +from .user import User class Topic(BaseModel): @@ -24,7 +25,7 @@ class Topic(BaseModel): type: Optional[str] linked_id: Optional[int] linked_type: Optional[str] - linked: Union[Anime, Manga] + linked: Optional[Union[Club, Anime, Manga]] viewed: Optional[bool] last_comment_viewed: Optional[bool] event: Optional[str] diff --git a/shikithon/models/tree_node.py b/shikithon/models/tree_node.py index dfd98337..7827b802 100644 --- a/shikithon/models/tree_node.py +++ b/shikithon/models/tree_node.py @@ -1,4 +1,6 @@ """Submodel for franchise_tree.py""" +from typing import Optional + from pydantic import BaseModel @@ -9,6 +11,6 @@ class TreeNode(BaseModel): name: str image_url: str url: str - year: int + year: Optional[int] kind: str weight: int diff --git a/shikithon/models/type_list.py b/shikithon/models/type_list.py index 7e69132d..6fa0df9f 100644 --- a/shikithon/models/type_list.py +++ b/shikithon/models/type_list.py @@ -3,7 +3,7 @@ from pydantic import BaseModel -from shikithon.models.type import Type +from .type import Type class TypeList(BaseModel): diff --git a/shikithon/models/user.py b/shikithon/models/user.py index ccf0a63f..bc026df4 100644 --- a/shikithon/models/user.py +++ b/shikithon/models/user.py @@ -4,8 +4,8 @@ from pydantic import BaseModel -from shikithon.models.stats import Stats -from shikithon.models.user_image import UserImage +from .stats import Stats +from .user_image import UserImage class User(BaseModel): diff --git a/shikithon/models/user_list.py b/shikithon/models/user_list.py index 5d8e9fa4..684814db 100644 --- a/shikithon/models/user_list.py +++ b/shikithon/models/user_list.py @@ -4,9 +4,9 @@ from pydantic import BaseModel -from shikithon.models.anime import Anime -from shikithon.models.manga import Manga -from shikithon.models.user import User +from .anime import Anime +from .manga import Manga +from .user import User class UserList(BaseModel): diff --git a/shikithon/py.typed b/shikithon/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/shikithon/resources/__init__.py b/shikithon/resources/__init__.py new file mode 100644 index 00000000..f548882e --- /dev/null +++ b/shikithon/resources/__init__.py @@ -0,0 +1,59 @@ +"""API resources for shikithon API class.""" + +from .abuse_requests import AbuseRequests +from .achievements import Achievements +from .animes import Animes +from .appears import Appears +from .bans import Bans +from .calendar import Calendar +from .characters import Characters +from .clubs import Clubs +from .comments import Comments +from .constants import Constants +from .dialogs import Dialogs +from .favorites import Favorites +from .forums import Forums +from .friends import Friends +from .genres import Genres +from .mangas import Mangas +from .messages import Messages +from .people import Person +from .publishers import Publishers +from .ranobes import Ranobes +from .stats import Stats +from .studios import Studios +from .styles import Styles +from .topics import Topics +from .user_images import UserImages +from .user_rates import UserRates +from .users import Users + +__all__ = [ + 'Achievements', + 'Animes', + 'Appears', + 'Bans', + 'Calendar', + 'Characters', + 'Clubs', + 'Comments', + 'Constants', + 'Dialogs', + 'Favorites', + 'Forums', + 'Friends', + 'Genres', + 'Mangas', + 'Messages', + 'Person', + 'Publishers', + 'Ranobes', + 'Stats', + 'Studios', + 'Styles', + 'Topics', + 'UserImages', + 'UserRates', + 'Users', + 'AbuseRequests', +] diff --git a/shikithon/resources/abuse_requests.py b/shikithon/resources/abuse_requests.py new file mode 100644 index 00000000..bd69396a --- /dev/null +++ b/shikithon/resources/abuse_requests.py @@ -0,0 +1,92 @@ +"""Represents /api/v2/abuse_requests resource.""" +from typing import Any, Dict, List, Optional + +from ..decorators import method_endpoint +from ..enums import RequestType +from ..models import AbuseResponse +from ..utils import Utils +from .base_resource import BaseResource + + +class AbuseRequests(BaseResource): + """AbuseRequests resource class. + + Used to represent /api/v2/abuse_requests resource. + """ + + @method_endpoint('/api/v2/abuse_requests/offtopic') + async def comment_offtopic(self, + comment_id: int) -> Optional[AbuseResponse]: + """ + Mark comment as offtopic. + + :param comment_id: ID of comment to mark as offtopic + :type comment_id: int + + :return: Object with info about abuse request + :rtype: Optional[AbuseResponse] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.abuse_offtopic, + data=Utils.create_data_dict(comment_id=comment_id), + request_type=RequestType.POST) + return Utils.validate_response_data(response, data_model=AbuseResponse) + + @method_endpoint('/api/v2/abuse_requests/review') + async def comment_review(self, comment_id: int) -> Optional[AbuseResponse]: + """ + Convert comment to review. + + :param comment_id: ID of comment to convert to review + :type comment_id: int + + :return: Object with info about abuse request + :rtype: Optional[AbuseResponse] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.abuse_review, + data=Utils.create_data_dict(comment_id=comment_id), + request_type=RequestType.POST) + return Utils.validate_response_data(response, data_model=AbuseResponse) + + @method_endpoint('/api/v2/abuse_requests/abuse') + async def violation_request(self, comment_id: int, + reason: str) -> Optional[AbuseResponse]: + """ + Create abuse about violation of site rules + + :param comment_id: ID of comment to create abuse request + :type comment_id: int + + :param reason: Additional info about violation + :type reason: str + + :return: Object with info about abuse request + :rtype: Optional[AbuseResponse] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.abuse_violation, + data=Utils.create_data_dict(comment_id=comment_id, reason=reason), + request_type=RequestType.POST) + return Utils.validate_response_data(response, data_model=AbuseResponse) + + @method_endpoint('/api/v2/abuse_requests/spoiler') + async def spoiler_abuse_request(self, comment_id: int, + reason: str) -> Optional[AbuseResponse]: + """ + Create abuse about spoiler in content. + + :param comment_id: ID of comment to create abuse request + :type comment_id: int + + :param reason: Additional info about spoiler + :type reason: str + + :return: Object with info about abuse request + :rtype: Optional[AbuseResponse] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.abuse_spoiler, + data=Utils.create_data_dict(comment_id=comment_id, reason=reason), + request_type=RequestType.POST) + return Utils.validate_response_data(response, data_model=AbuseResponse) diff --git a/shikithon/resources/achievements.py b/shikithon/resources/achievements.py new file mode 100644 index 00000000..4d22dd3e --- /dev/null +++ b/shikithon/resources/achievements.py @@ -0,0 +1,39 @@ +"""Represents /api/achievements resource.""" + +from typing import Any, Dict, List + +from loguru import logger + +from ..decorators import method_endpoint +from ..models import Achievement +from ..utils import Utils +from .base_resource import BaseResource + + +class Achievements(BaseResource): + """Achievements resource class. + + Used to represent /api/achievements resource. + """ + + @method_endpoint('/api/achievements') + async def get(self, user_id: int) -> List[Achievement]: + """ + Returns achievements of user by ID. + + :param user_id: User ID for getting achievements + :type user_id: int + + :return: List of achievements + :rtype: List[Achievement] + """ + if not isinstance(user_id, int): + logger.error('/api/achievements accept only user_id as int') + return [] + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.achievements, + query=Utils.create_query_dict(user_id=user_id)) + return Utils.validate_response_data(response, + data_model=Achievement, + fallback=[]) diff --git a/shikithon/resources/animes.py b/shikithon/resources/animes.py new file mode 100644 index 00000000..8ecc5922 --- /dev/null +++ b/shikithon/resources/animes.py @@ -0,0 +1,387 @@ +"""Represents /api/animes and /api/animes/:anime_id/videos resource.""" +from typing import Any, Dict, List, Optional, Union + +from ..decorators import method_endpoint +from ..decorators import protected_method +from ..enums import AnimeCensorship +from ..enums import AnimeDuration +from ..enums import AnimeKind +from ..enums import AnimeList +from ..enums import AnimeOrder +from ..enums import AnimeRating +from ..enums import AnimeStatus +from ..enums import AnimeTopicKind +from ..enums import RequestType +from ..enums import VideoKind +from ..models import Anime +from ..models import Creator +from ..models import FranchiseTree +from ..models import Link +from ..models import Relation +from ..models import Screenshot +from ..models import Topic +from ..models import Video +from ..utils import Utils +from .base_resource import BaseResource + + +class Animes(BaseResource): + """Anime resource class. + + Used to represent /api/animes and /api/animes/:anime_id/videos resource. + """ + + @method_endpoint('/api/animes') + async def get_all(self, + page: Optional[int] = None, + limit: Optional[int] = None, + order: Optional[str] = None, + kind: Optional[Union[str, List[str]]] = None, + status: Optional[Union[str, List[str]]] = None, + season: Optional[Union[str, List[str]]] = None, + score: Optional[int] = None, + duration: Optional[Union[str, List[str]]] = None, + rating: Optional[Union[str, List[str]]] = None, + genre: Optional[Union[int, List[int]]] = None, + studio: Optional[Union[int, List[int]]] = None, + franchise: Optional[Union[int, List[int]]] = None, + censored: Optional[str] = None, + my_list: Optional[Union[str, List[str]]] = None, + ids: Optional[Union[int, List[int]]] = None, + exclude_ids: Optional[Union[int, List[int]]] = None, + search: Optional[str] = None) -> List[Anime]: + """ + Returns animes list. + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + :type limit: Optional[int] + + :param order: Type of order in list + :type order: Optional[str] + + :param kind: Type(s) of anime topics + :type kind: Optional[Union[str, List[str]]] + + :param status: Type(s) of anime status + :type status: Optional[Union[str, List[str]]] + + :param season: Name(s) of anime seasons + :type season: Optional[Union[str, List[str]]] + + :param score: Minimal anime score + :type score: Optional[int] + + :param duration: Duration size(s) of anime + :type duration: Optional[Union[str, List[str]]] + + :param rating: Type of anime rating(s) + :type rating: Optional[Union[str, List[str]]] + + :param genre: Genre(s) ID + :type genre: Optional[Union[int, List[int]]] + + :param studio: Studio(s) ID + :type studio: Optional[Union[int, List[int]]] + + :param franchise: Franchise(s) ID + :type franchise: Optional[Union[int, List[int]]] + + :param censored: Type of anime censorship + :type censored: Optional[str] + + :param my_list: Status(-es) of anime in current user list. + If app is in restricted mode, + this parameter won't affect on response. + :type my_list: Optional[Union[str, List[str]]] + + :param ids: Anime(s) ID to include + :type ids: Optional[Union[int, List[int]]] + + :param exclude_ids: Anime(s) ID to exclude + :type exclude_ids: Optional[Union[int, List[int]]] + + :param search: Search phrase to filter animes by name + :type search: Optional[str] + + :return: Animes list + :rtype: List[Anime] + """ + if not Utils.validate_enum_params({ + AnimeOrder: order, + AnimeKind: kind, + AnimeStatus: status, + AnimeDuration: duration, + AnimeRating: rating, + AnimeCensorship: censored, + AnimeList: my_list, + }): + return [] + + validated_numbers = Utils.query_numbers_validator(page=[page, 100000], + limit=[limit, 50], + score=[score, 9]) + + headers = self._client.user_agent + + if my_list: + headers = self._client.protected_method_headers('/api/animes') + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.animes, + headers=headers, + query=Utils.create_query_dict(page=validated_numbers['page'], + limit=validated_numbers['limit'], + order=order, + kind=kind, + status=status, + season=season, + score=validated_numbers['score'], + duration=duration, + rating=rating, + genre=genre, + studio=studio, + franchise=franchise, + censored=censored, + mylist=my_list, + ids=ids, + exclude_ids=exclude_ids, + search=search)) + return Utils.validate_response_data(response, + data_model=Anime, + fallback=[]) + + @method_endpoint('/api/animes/:id') + async def get(self, anime_id: int) -> Optional[Anime]: + """ + Returns info about certain anime. + + :param anime_id: Anime ID to get info + :type anime_id: int + + :return: Anime info + :rtype: Optional[Anime] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.anime(anime_id)) + return Utils.validate_response_data(response, data_model=Anime) + + @method_endpoint('/api/animes/:id/roles') + async def creators(self, anime_id: int) -> List[Creator]: + """ + Returns creators info of certain anime. + + :param anime_id: Anime ID to get creators + :type anime_id: int + + :return: List of anime creators + :rtype: List[Creator] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.anime_roles(anime_id)) + return Utils.validate_response_data(response, + data_model=Creator, + fallback=[]) + + @method_endpoint('/api/animes/:id/similar') + async def similar(self, anime_id: int) -> List[Anime]: + """ + Returns list of similar animes for certain anime. + + :param anime_id: Anime ID to get similar animes + :type anime_id: int + + :return: List of similar animes + :rtype: List[Anime] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.similar_animes(anime_id)) + return Utils.validate_response_data(response, + data_model=Anime, + fallback=[]) + + @method_endpoint('/api/animes/:id/related') + async def related_content(self, anime_id: int) -> List[Relation]: + """ + Returns list of related content of certain anime. + + :param anime_id: Anime ID to get related content + :type anime_id: int + + :return: List of relations + :rtype: List[Relation] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.anime_related_content(anime_id)) + return Utils.validate_response_data(response, + data_model=Relation, + fallback=[]) + + @method_endpoint('/api/animes/:id/screenshots') + async def screenshots(self, anime_id: int) -> List[Screenshot]: + """ + Returns list of screenshot links of certain anime. + + :param anime_id: Anime ID to get screenshot links + :type anime_id: int + + :return: List of screenshot links + :rtype: List[Screenshot] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.anime_screenshots(anime_id)) + return Utils.validate_response_data(response, + data_model=Screenshot, + fallback=[]) + + @method_endpoint('/api/animes/:id/franchise') + async def franchise_tree(self, anime_id: int) -> Optional[FranchiseTree]: + """ + Returns franchise tree of certain anime. + + :param anime_id: Anime ID to get franchise tree + :type anime_id: int + + :return: Franchise tree of certain anime + :rtype: Optional[FranchiseTree] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.anime_franchise_tree(anime_id)) + return Utils.validate_response_data(response, data_model=FranchiseTree) + + @method_endpoint('/api/animes/:id/external_links') + async def external_links(self, anime_id: int) -> List[Link]: + """ + Returns list of external links of certain anime. + + :param anime_id: Anime ID to get external links + :type anime_id: int + + :return: List of external links + :rtype: List[Link] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.anime_external_links(anime_id)) + return Utils.validate_response_data(response, + data_model=Link, + fallback=[]) + + @method_endpoint('/api/animes/:id/topics') + async def topics(self, + anime_id: int, + page: Optional[int] = None, + limit: Optional[int] = None, + kind: Optional[str] = None, + episode: Optional[int] = None) -> List[Topic]: + """ + Returns list of topics of certain anime. + + :param anime_id: Anime ID to get topics + :type anime_id: int + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + :type limit: Optional[int] + + :param kind: Kind of anime + :type kind: Optional[str] + + :param episode: Number of anime episode + :type episode: Optional[int] + + :return: List of topics + :rtype: List[Topic] + """ + if not Utils.validate_enum_params({AnimeTopicKind: kind}): + return [] + + validated_numbers = Utils.query_numbers_validator(page=[page, 100000], + limit=[limit, 30]) + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.anime_topics(anime_id), + query=Utils.create_query_dict(page=validated_numbers['page'], + limit=validated_numbers['limit'], + kind=kind, + episode=episode)) + return Utils.validate_response_data(response, + data_model=Topic, + fallback=[]) + + @method_endpoint('/api/animes/:anime_id/videos') + async def videos(self, anime_id: int) -> List[Video]: + """ + Returns list of anime videos. + + :param anime_id: Anime ID to get videos + :type anime_id: int + + :return: Anime videos list + :rtype: List[Video] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.anime_videos(anime_id)) + return Utils.validate_response_data(response, + data_model=Video, + fallback=[]) + + @method_endpoint('/api/animes/:anime_id/videos') + @protected_method('_client', 'content') + async def create_video(self, anime_id: int, kind: str, name: str, + url: str) -> Optional[Video]: + """ + Creates anime video. + + :param anime_id: Anime ID to create video + :type anime_id: int + + :param kind: Kind of video + :type kind: str + + :param name: Name of video + :type name: str + + :param url: URL of video + :type url: str + + :return: Created video info + :rtype: Optional[Video] + """ + if not Utils.validate_enum_params({VideoKind: kind}): + return None + + data_dict: Dict[str, Any] = Utils.create_data_dict(dict_name='video', + kind=kind, + name=name, + url=url) + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.anime_videos(anime_id), + headers=self._client.authorization_header, + data=data_dict, + request_type=RequestType.POST) + return Utils.validate_response_data(response, data_model=Video) + + @method_endpoint('/api/animes/:anime_id/videos/:id') + @protected_method('_client', 'content') + async def delete_video(self, anime_id: int, video_id: int) -> bool: + """ + Deletes anime video. + + :param anime_id: Anime ID to delete video + :type anime_id: int + + :param video_id: Video ID to delete + :type video_id: str + + :return: Status of video deletion + :rtype: bool + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.anime_video(anime_id, video_id), + headers=self._client.authorization_header, + request_type=RequestType.DELETE) + return Utils.validate_response_data(response) diff --git a/shikithon/resources/appears.py b/shikithon/resources/appears.py new file mode 100644 index 00000000..91d7d442 --- /dev/null +++ b/shikithon/resources/appears.py @@ -0,0 +1,51 @@ +"""Represents /api/appears resource.""" + +from typing import Any, Dict, List, Union + +from loguru import logger + +from ..decorators import method_endpoint +from ..decorators import protected_method +from ..enums import RequestType +from ..enums import ResponseCode +from ..utils import Utils +from .base_resource import BaseResource + + +class Appears(BaseResource): + """Appears resource class. + + Used to represent /api/appears resource. + """ + + @method_endpoint('/api/appears') + @protected_method('_client', fallback=False) + async def mark(self, ids: List[str]) -> bool: + """ + Marks comments or topics as read. + + This method uses generate_query_dict for data dict, + because there is no need for nested dictionary + + :param ids: IDs of comments or topics to mark + :type ids: List[str] + + :return: Status of mark + :rtype: bool + """ + if not isinstance(ids, list): + logger.error('/api/appears accept only list of ids') + return False + + if not ids: + logger.warning('List of ids to mark is empty') + return False + + response: Union[Dict[str, Any], int] = await self._client.request( + self._client.endpoints.appears, + headers=self._client.authorization_header, + data=Utils.create_query_dict(ids=ids), + request_type=RequestType.POST) + return Utils.validate_response_data(response, + response_code=ResponseCode.SUCCESS, + fallback=False) diff --git a/shikithon/resources/bans.py b/shikithon/resources/bans.py new file mode 100644 index 00000000..bbec9015 --- /dev/null +++ b/shikithon/resources/bans.py @@ -0,0 +1,41 @@ +"""Represents /api/bans resource.""" +from typing import Any, Dict, List, Optional + +from ..decorators import method_endpoint +from ..models import Ban +from ..utils import Utils +from .base_resource import BaseResource + + +class Bans(BaseResource): + """Bans resource class. + + Used to represent /api/bans resource. + """ + + @method_endpoint('/api/bans') + async def get(self, + page: Optional[int] = None, + limit: Optional[int] = None) -> List[Ban]: + """ + Returns list of recent bans on Shikimori. + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + :type limit: Optional[int] + + :return: List of recent bans + :rtype: List[Ban] + """ + validated_numbers = Utils.query_numbers_validator( + page=[page, 100000], + limit=[limit, 30], + ) + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.bans_list, + query=Utils.create_query_dict(page=validated_numbers['page'], + limit=validated_numbers['limit'])) + return Utils.validate_response_data(response, data_model=Ban) diff --git a/shikithon/resources/base_resource.py b/shikithon/resources/base_resource.py new file mode 100644 index 00000000..12d0bf03 --- /dev/null +++ b/shikithon/resources/base_resource.py @@ -0,0 +1,8 @@ +"""Base class for API resources.""" +from ..base_client import Client + + +class BaseResource: + + def __init__(self, client: Client) -> None: + self._client = client diff --git a/shikithon/resources/calendar.py b/shikithon/resources/calendar.py new file mode 100644 index 00000000..07bfaab7 --- /dev/null +++ b/shikithon/resources/calendar.py @@ -0,0 +1,36 @@ +"""Represents /api/calendar resource.""" +from typing import Any, Dict, List, Optional + +from ..decorators import method_endpoint +from ..enums import AnimeCensorship +from ..models import CalendarEvent +from ..utils import Utils +from .base_resource import BaseResource + + +class Calendar(BaseResource): + """Calendar resource class. + + Used to represent /api/calendar resource. + """ + + @method_endpoint('/api/calendar') + async def get(self, censored: Optional[str] = None) -> List[CalendarEvent]: + """ + Returns current calendar events. + + :param censored: Status of censorship for events (true/false) + :type censored: Optional[str] + + :return: List of calendar events + :rtype: List[CalendarEvent] + """ + if not Utils.validate_enum_params({AnimeCensorship: censored}): + return [] + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.calendar, + query=Utils.create_query_dict(censored=censored)) + return Utils.validate_response_data(response, + data_model=CalendarEvent, + fallback=[]) diff --git a/shikithon/resources/characters.py b/shikithon/resources/characters.py new file mode 100644 index 00000000..8ece5735 --- /dev/null +++ b/shikithon/resources/characters.py @@ -0,0 +1,45 @@ +"""Represents /api/characters resource.""" +from typing import Any, Dict, List, Optional + +from ..decorators import method_endpoint +from ..models import Character +from ..utils import Utils +from .base_resource import BaseResource + + +class Characters(BaseResource): + """Characters resource class. + + Used to represent /api/characters resource. + """ + + @method_endpoint('/api/characters/:id') + async def get(self, character_id: int) -> Optional[Character]: + """ + Returns character info by ID. + + :param character_id: ID of character to get info + :type character_id: int + + :return: Character info + :rtype: Optional[Character] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.character(character_id)) + return Utils.validate_response_data(response, data_model=Character) + + @method_endpoint('/api/characters/search') + async def search(self, search: Optional[str] = None) -> List[Character]: + """ + Returns list of found characters. + + :param search: Search query for characters + :type search: Optional[str] + + :return: List of found characters + :rtype: List[Character] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.character_search, + query=Utils.create_query_dict(search=search)) + return Utils.validate_response_data(response, data_model=Character) diff --git a/shikithon/resources/clubs.py b/shikithon/resources/clubs.py new file mode 100644 index 00000000..9e5d554c --- /dev/null +++ b/shikithon/resources/clubs.py @@ -0,0 +1,334 @@ +"""Represents /api/clubs resource.""" +from typing import Any, Dict, List, Optional, Union + +from ..decorators import method_endpoint +from ..decorators import protected_method +from ..enums import CommentPolicy +from ..enums import ImageUploadPolicy +from ..enums import JoinPolicy +from ..enums import PagePolicy +from ..enums import RequestType +from ..enums import ResponseCode +from ..enums import TopicPolicy +from ..models import Anime +from ..models import Character +from ..models import Club +from ..models import ClubImage +from ..models import Manga +from ..models import Ranobe +from ..models import User +from ..utils import Utils +from .base_resource import BaseResource + + +class Clubs(BaseResource): + """Clubs resource class. + + Used to represent /api/clubs resource. + """ + + @method_endpoint('/api/clubs') + async def get_all(self, + page: Optional[int] = None, + limit: Optional[int] = None, + search: Optional[str] = None) -> List[Club]: + """ + Returns clubs list. + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + :type limit: Optional[int] + + :param search: Search phrase to filter clubs by name + :type search: Optional[str] + + :return: Clubs list + :rtype: List[Club] + """ + validated_numbers = Utils.query_numbers_validator( + page=[page, 100000], + limit=[limit, 30], + ) + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.clubs, + query=Utils.create_query_dict(page=validated_numbers['page'], + limit=validated_numbers['limit'], + search=search)) + return Utils.validate_response_data(response, + data_model=Club, + fallback=[]) + + @method_endpoint('/api/clubs/:id') + async def get(self, club_id: int) -> Optional[Club]: + """ + Returns info about club. + + :param club_id: Club ID to get info + :type club_id: int + + :return: Info about club + :rtype: Optional[Club] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.club(club_id)) + return Utils.validate_response_data(response, data_model=Club) + + @method_endpoint('/api/clubs/:id') + @protected_method('_client', 'clubs') + async def update( + self, + club_id: int, + name: Optional[str] = None, + description: Optional[str] = None, + join_policy: Optional[str] = None, + display_images: Optional[bool] = None, + comment_policy: Optional[str] = None, + topic_policy: Optional[str] = None, + page_policy: Optional[str] = None, + image_upload_policy: Optional[str] = None, + is_censored: Optional[bool] = None, + anime_ids: Optional[List[int]] = None, + manga_ids: Optional[List[int]] = None, + ranobe_ids: Optional[List[int]] = None, + character_ids: Optional[List[int]] = None, + club_ids: Optional[List[int]] = None, + admin_ids: Optional[List[int]] = None, + collection_ids: Optional[List[int]] = None, + banned_user_ids: Optional[List[int]] = None) -> Optional[Club]: + """ + Update info/settings about/of club. + + :param club_id: Club ID to modify/update + :type club_id: int + + :param name: New name of club + :type name: Optional[str] + + :param description: New description of club + :type description: Optional[str] + + :param join_policy: New join policy of club + :type join_policy: Optional[str] + + :param display_images: New display images status of club + :type display_images: Optional[bool] + + :param comment_policy: New comment policy of club + :type comment_policy: Optional[str] + + :param topic_policy: New topic policy of club + :type topic_policy: Optional[str] + + :param page_policy: New page policy of club + :type page_policy: Optional[str] + + :param image_upload_policy: New image upload policy of club + :type image_upload_policy: Optional[str] + + :param is_censored: New censored status of club + :type is_censored: Optional[bool] + + :param anime_ids: New anime ids of club + :type anime_ids: Optional[List[int]] + + :param manga_ids: New manga ids of club + :type manga_ids: Optional[List[int]] + + :param ranobe_ids: New ranobe ids of club + :type ranobe_ids: Optional[List[int]] + + :param character_ids: New character ids of club + :type character_ids: Optional[List[int]] + + :param club_ids: New club ids of club + :type club_ids: Optional[List[int]] + + :param admin_ids: New admin ids of club + :type admin_ids: Optional[List[int]] + + :param collection_ids: New collection ids of club + :type collection_ids: Optional[List[int]] + + :param banned_user_ids: New banned user ids of club + :type banned_user_ids: Optional[List[int]] + + :return: Updated club info + :rtype: Optional[Club] + """ + if not Utils.validate_enum_params({ + JoinPolicy: join_policy, + CommentPolicy: comment_policy, + TopicPolicy: topic_policy, + PagePolicy: page_policy, + ImageUploadPolicy: image_upload_policy + }): + return None + + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.club(club_id), + headers=self._client.authorization_header, + data=Utils.create_data_dict(dict_name='club', + name=name, + join_policy=join_policy, + description=description, + display_images=display_images, + comment_policy=comment_policy, + topic_policy=topic_policy, + page_policy=page_policy, + image_upload_policy=image_upload_policy, + is_censored=is_censored, + anime_ids=anime_ids, + manga_ids=manga_ids, + ranobe_ids=ranobe_ids, + character_ids=character_ids, + club_ids=club_ids, + admin_ids=admin_ids, + collection_ids=collection_ids, + banned_user_ids=banned_user_ids), + request_type=RequestType.PATCH) + return Utils.validate_response_data(response, data_model=Club) + + @method_endpoint('/api/clubs/:id/animes') + async def animes(self, club_id: int) -> List[Anime]: + """ + Returns anime list of club. + + :param club_id: Club ID to get anime list + :type club_id: int + + :return: Club anime list + :rtype: List[Anime] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.club_animes(club_id)) + return Utils.validate_response_data(response, + data_model=Anime, + fallback=[]) + + @method_endpoint('/api/clubs/:id/mangas') + async def mangas(self, club_id: int) -> List[Manga]: + """ + Returns manga list of club. + + :param club_id: Club ID to get manga list + :type club_id: int + + :return: Club manga list + :rtype: List[Manga] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.club_mangas(club_id)) + return Utils.validate_response_data(response, + data_model=Manga, + fallback=[]) + + @method_endpoint('/api/clubs/:id/ranobe') + async def ranobe(self, club_id: int) -> List[Ranobe]: + """ + Returns ranobe list of club. + + :param club_id: Club ID to get ranobe list + :type club_id: int + + :return: Club ranobe list + :rtype: List[Ranobe] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.club_ranobe(club_id)) + return Utils.validate_response_data(response, + data_model=Ranobe, + fallback=[]) + + @method_endpoint('/api/clubs/:id/characters') + async def characters(self, club_id: int) -> List[Character]: + """ + Returns character list of club. + + :param club_id: Club ID to get character list + :type club_id: int + + :return: Club character list + :rtype: List[Character] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.club_characters(club_id)) + return Utils.validate_response_data(response, + data_model=Character, + fallback=[]) + + @method_endpoint('/api/clubs/:id/members') + async def members(self, club_id: int) -> List[User]: + """ + Returns member list of club. + + :param club_id: Club ID to get member list + :type club_id: int + + :return: Club member list + :rtype: List[User] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.club_members(club_id)) + return Utils.validate_response_data(response, + data_model=User, + fallback=[]) + + @method_endpoint('/api/clubs/:id/images') + async def images(self, club_id: int) -> List[ClubImage]: + """ + Returns images of club. + + :param club_id: Club ID to get images + :type club_id: int + + :return: Club's images + :rtype: List[ClubImage] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.club_images(club_id)) + return Utils.validate_response_data(response, + data_model=ClubImage, + fallback=[]) + + @method_endpoint('/api/clubs/:id/join') + @protected_method('_client', 'clubs', fallback=False) + async def join(self, club_id: int) -> bool: + """ + Joins club by ID. + + :param club_id: Club ID to join + :type club_id: int + + :return: Status of join + :rtype: bool + """ + response: Union[Dict[str, Any], int] = await self._client.request( + self._client.endpoints.club_join(club_id), + headers=self._client.authorization_header, + request_type=RequestType.POST) + return Utils.validate_response_data(response, + response_code=ResponseCode.SUCCESS, + fallback=False) + + @method_endpoint('/api/clubs/:id/leave') + @protected_method('_client', 'clubs', fallback=False) + async def leave(self, club_id: int) -> bool: + """ + Leaves club by ID. + + :param club_id: Club ID to leave + :type club_id: int + + :return: Status of leave + :rtype: bool + """ + response: Union[Dict[str, Any], int] = await self._client.request( + self._client.endpoints.club_leave(club_id), + headers=self._client.authorization_header, + request_type=RequestType.POST) + return Utils.validate_response_data(response, + response_code=ResponseCode.SUCCESS, + fallback=False) diff --git a/shikithon/resources/comments.py b/shikithon/resources/comments.py new file mode 100644 index 00000000..5b4c1515 --- /dev/null +++ b/shikithon/resources/comments.py @@ -0,0 +1,174 @@ +"""Represents /api/comments resource.""" +from typing import Any, Dict, List, Optional + +from loguru import logger + +from ..decorators import method_endpoint +from ..decorators import protected_method +from ..enums import CommentableType +from ..enums import RequestType +from ..models import Comment +from ..utils import Utils +from .base_resource import BaseResource + + +class Comments(BaseResource): + """Comments resource class. + + Used to represent /api/comments resource. + """ + + @method_endpoint('/api/comments') + async def get_all(self, + commentable_id: int, + commentable_type: str, + page: Optional[int] = None, + limit: Optional[int] = None, + desc: Optional[int] = None) -> List[Comment]: + """ + Returns list of comments. + + :param commentable_id: ID of entity to get comment + :type commentable_id: int + + :param commentable_type: Type of entity to get comment + :type commentable_type: str + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + :type limit: Optional[int] + + :param desc: Status of description in request. Can be 1 or 0 + :type desc: Optional[int] + + :return: List of comments + :rtype: List[Comment] + """ + if not Utils.validate_enum_params({CommentableType: commentable_type}): + return [] + + validated_numbers = Utils.query_numbers_validator( + page=[page, 100000], + limit=[limit, 30], + ) + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.comments, + query=Utils.create_query_dict(page=validated_numbers['page'], + limit=validated_numbers['limit'], + commentable_id=commentable_id, + commentable_type=commentable_type, + desc=desc)) + return Utils.validate_response_data(response, + data_model=Comment, + fallback=[]) + + @method_endpoint('/api/comments/:id') + async def get(self, comment_id: int) -> Optional[Comment]: + """ + Returns comment info. + + :param comment_id: ID of comment + :type comment_id: int + + :return: Comment info + :rtype: Optional[Comment] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.comment(comment_id)) + return Utils.validate_response_data(response, data_model=Comment) + + @method_endpoint('/api/comments') + @protected_method('_client', 'comments') + async def create(self, + body: str, + commentable_id: int, + commentable_type: str, + is_offtopic: Optional[bool] = None, + broadcast: Optional[bool] = None) -> Optional[Comment]: + """ + Creates comment. + + When commentable_type set to Anime, Manga, Character or Person, + comment is attached to commentable main topic. + + :param body: Body of comment + :type body: str + + :param commentable_id: ID of entity to comment on + :type commentable_id: int + + :param commentable_type: Type of entity to comment on + :type commentable_type: str + + :param is_offtopic: Status of offtopic + :type is_offtopic: Optional[bool] + + :param broadcast: Broadcast comment in club’s topic status + :type broadcast: Optional[bool] + + :return: Created comment info + :rtype: Optional[Comment] + """ + if not Utils.validate_enum_params({CommentableType: commentable_type}): + return None + + data_dict: Dict[str, Any] = Utils.create_data_dict( + dict_name='comment', + body=body, + commentable_id=commentable_id, + commentable_type=commentable_type, + is_offtopic=is_offtopic) + + if broadcast: + logger.debug('Adding a broadcast value to a data_dict') + data_dict['broadcast'] = broadcast + + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.comments, + headers=self._client.authorization_header, + data=data_dict, + request_type=RequestType.POST) + return Utils.validate_response_data(response, data_model=Comment) + + @method_endpoint('/api/comments/:id') + @protected_method('_client', 'comments') + async def update(self, comment_id: int, body: str) -> Optional[Comment]: + """ + Updates comment. + + :param comment_id: ID of comment to update + :type comment_id: int + + :param body: New body of comment + :type body: str + + :return: Updated comment info + :rtype: Optional[Comment] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.comment(comment_id), + headers=self._client.authorization_header, + data=Utils.create_data_dict(dict_name='comment', body=body), + request_type=RequestType.PATCH) + return Utils.validate_response_data(response, data_model=Comment) + + @method_endpoint('/api/comments/:id') + @protected_method('_client', 'comments', fallback=False) + async def delete(self, comment_id: int) -> bool: + """ + Deletes comment. + + :param comment_id: ID of comment to delete + :type comment_id: int + + :return: Status of comment deletion + :rtype: bool + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.comment(comment_id), + headers=self._client.authorization_header, + request_type=RequestType.DELETE) + return Utils.validate_response_data(response, fallback=False) diff --git a/shikithon/resources/constants.py b/shikithon/resources/constants.py new file mode 100644 index 00000000..1c8cce6c --- /dev/null +++ b/shikithon/resources/constants.py @@ -0,0 +1,80 @@ +"""Represents /api/constants resource.""" +from typing import Any, Dict, List, Optional + +from ..decorators import method_endpoint +from ..models.constants import AnimeConstants +from ..models.constants import ClubConstants +from ..models.constants import MangaConstants +from ..models.constants import SmileyConstants +from ..models.constants import UserRateConstants +from ..utils import Utils +from .base_resource import BaseResource + + +class Constants(BaseResource): + """Constants resource class. + + Used to represent /api/constants resource. + """ + + @method_endpoint('/api/constants/anime') + async def anime(self) -> Optional[AnimeConstants]: + """ + Returns anime constants values. + + :return: Anime constants values + :rtype: Optional[AnimeConstants] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.anime_constants) + return Utils.validate_response_data(response, data_model=AnimeConstants) + + @method_endpoint('/api/constants/manga') + async def manga(self) -> Optional[MangaConstants]: + """ + Returns manga constants values. + + :return: Manga constants values + :rtype: Optional[MangaConstants] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.manga_constants) + return Utils.validate_response_data(response, data_model=MangaConstants) + + @method_endpoint('/api/constants/user_rate') + async def user_rate(self) -> Optional[UserRateConstants]: + """ + Returns user rate constants values. + + :return: User rate constants values + :rtype: Optional[UserRateConstants] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.user_rate_constants) + return Utils.validate_response_data(response, + data_model=UserRateConstants) + + @method_endpoint('/api/constants/club') + async def club(self) -> Optional[ClubConstants]: + """ + Returns club constants values. + + :return: Club constants values + :rtype: Optional[ClubConstants] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.club_constants) + return Utils.validate_response_data(response, data_model=ClubConstants) + + @method_endpoint('/api/constants/smileys') + async def smileys(self) -> List[SmileyConstants]: + """ + Returns list of smileys constants values. + + :return: List of smileys constants values + :rtype: List[SmileyConstants] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.smileys_constants) + return Utils.validate_response_data(response, + data_model=SmileyConstants) diff --git a/shikithon/resources/dialogs.py b/shikithon/resources/dialogs.py new file mode 100644 index 00000000..69023b61 --- /dev/null +++ b/shikithon/resources/dialogs.py @@ -0,0 +1,70 @@ +"""Represents /api/dialogs resource.""" +from typing import Any, Dict, List, Union + +from ..decorators import method_endpoint +from ..decorators import protected_method +from ..enums import RequestType +from ..models import Dialog +from ..models import Message +from ..utils import Utils +from .base_resource import BaseResource + + +class Dialogs(BaseResource): + """Dialogs resource class. + + Used to represent /api/dialogs resource. + """ + + @method_endpoint('/api/dialogs') + @protected_method('_client', 'messages', fallback=[]) + async def get_all(self) -> List[Dialog]: + """ + Returns list of current user's dialogs. + + :return: List of dialogs + :rtype: List[Dialog] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.dialogs, + headers=self._client.authorization_header) + return Utils.validate_response_data(response, + data_model=Dialog, + fallback=[]) + + @method_endpoint('/api/dialogs/:id') + @protected_method('_client', 'messages', fallback=[]) + async def get(self, user_id: Union[int, str]) -> List[Message]: + """ + Returns list of current user's messages with certain user. + + :param user_id: ID/Nickname of the user to get dialog + :type user_id: Union[int, str] + + :return: List of messages + :rtype: List[Message] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.dialog(user_id), + headers=self._client.authorization_header) + return Utils.validate_response_data(response, + data_model=Message, + fallback=[]) + + @method_endpoint('/api/dialogs/:id') + @protected_method('_client', 'messages', fallback=False) + async def delete(self, user_id: Union[int, str]) -> bool: + """ + Deletes dialog of current user with certain user. + + :param user_id: ID/Nickname of the user to delete dialog + :type user_id: Union[int, str] + + :return: Status of message deletion + :rtype: bool + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.dialog(user_id), + headers=self._client.authorization_header, + request_type=RequestType.DELETE) + return Utils.validate_response_data(response, fallback=False) diff --git a/shikithon/resources/favorites.py b/shikithon/resources/favorites.py new file mode 100644 index 00000000..db78c0b9 --- /dev/null +++ b/shikithon/resources/favorites.py @@ -0,0 +1,104 @@ +"""Represents /api/favorites resource.""" +from typing import Any, Dict, Optional, Union + +from ..decorators import method_endpoint +from ..decorators import protected_method +from ..enums import FavoriteLinkedType +from ..enums import PersonKind +from ..enums import RequestType +from ..enums import ResponseCode +from ..utils import Utils +from .base_resource import BaseResource + + +class Favorites(BaseResource): + """Favorites resource class. + + Used to represent /api/favorites resource. + """ + + @method_endpoint('/api/favorites/:linked_type/:linked_id(/:kind)') + @protected_method('_client', fallback=False) + async def create(self, + linked_type: str, + linked_id: int, + kind: str = PersonKind.NONE.value) -> bool: + """ + Creates a favorite. + + :param linked_type: Type of object for making favorite + :type linked_type: str + + :param linked_id: ID of linked type + :type linked_id: int + + :param kind: Kind of linked type + (Required when linked_type is 'Person') + :type kind: str + + :return: Status of favorite create + :rtype: bool + """ + if not Utils.validate_enum_params({ + FavoriteLinkedType: linked_type, + PersonKind: kind + }): + return False + + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.favorites_create(linked_type, linked_id, + kind), + headers=self._client.authorization_header, + request_type=RequestType.POST) + return Utils.validate_response_data(response, fallback=False) + + @method_endpoint('/api/favorites/:linked_type/:linked_id') + @protected_method('_client', fallback=False) + async def destroy(self, linked_type: str, linked_id: int) -> bool: + """ + Destroys a favorite. + + :param linked_type: Type of object for destroying from favorite + :type linked_type: str + + :param linked_id: ID of linked type + :type linked_id: int + + :return: Status of favorite destroy + :rtype: bool + """ + if not Utils.validate_enum_params({FavoriteLinkedType: linked_type}): + return False + + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.favorites_destroy(linked_type, linked_id), + headers=self._client.authorization_header, + request_type=RequestType.DELETE) + return Utils.validate_response_data(response, fallback=False) + + @method_endpoint('/api/favorites/:id/reorder') + @protected_method('_client', fallback=False) + async def reorder(self, + favorite_id: int, + new_index: Optional[int] = None) -> bool: + """ + Reorders a favorite to the new index. + + :param favorite_id: ID of a favorite to reorder + :type favorite_id: int + + :param new_index: Index of a new position of favorite. + If skipped, sets favorite to the first position + :type new_index: Optional[int] + + :return: Status of reorder + :rtype: bool + """ + response: Union[Dict[str, Any], int] = await self._client.request( + self._client.endpoints.favorites_reorder(favorite_id), + headers=self._client.authorization_header, + query=Utils.create_query_dict(new_index=new_index), + request_type=RequestType.POST) + return Utils.validate_response_data(response, + response_code=ResponseCode.SUCCESS, + fallback=False) diff --git a/shikithon/resources/forums.py b/shikithon/resources/forums.py new file mode 100644 index 00000000..9e20f842 --- /dev/null +++ b/shikithon/resources/forums.py @@ -0,0 +1,28 @@ +"""Represents /api/forums resource.""" +from typing import Any, Dict, List + +from ..decorators import method_endpoint +from ..models import Forum +from ..utils import Utils +from .base_resource import BaseResource + + +class Forums(BaseResource): + """Forums resource class. + + Used to represent /api/forums resource. + """ + + @method_endpoint('/api/forums') + async def get(self) -> List[Forum]: + """ + Returns list of forums. + + :returns: List of forums + :rtype: List[Forum] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.forums) + return Utils.validate_response_data(response, + data_model=Forum, + fallback=[]) diff --git a/shikithon/resources/friends.py b/shikithon/resources/friends.py new file mode 100644 index 00000000..899c12f1 --- /dev/null +++ b/shikithon/resources/friends.py @@ -0,0 +1,51 @@ +"""Represents /api/friends resource.""" +from typing import Any, Dict, Union + +from ..decorators import method_endpoint +from ..decorators import protected_method +from ..enums import RequestType +from ..utils import Utils +from .base_resource import BaseResource + + +class Friends(BaseResource): + """Friends resource class. + + Used to represent /api/friends resource. + """ + + @method_endpoint('/api/friends/:id') + @protected_method('_client', 'friends', fallback=False) + async def create(self, user_id: int) -> bool: + """ + Creates (adds) new friend by ID. + + :param user_id: ID of a user to create (add) + :type user_id: int + + :return: Status of create (addition) + :rtype: bool + """ + response: Union[Dict[str, Any], int] = await self._client.request( + self._client.endpoints.friend(user_id), + headers=self._client.authorization_header, + request_type=RequestType.POST) + return Utils.validate_response_data(response, fallback=False) + + @method_endpoint('/api/friends/:id') + @protected_method('_client', 'friends', fallback=False) + async def destroy(self, user_id: int) -> bool: + """ + Destroys (removes) current friend by ID. + + :param user_id: ID of a user to destroy (remove) + :type user_id: int + + :return: Status of destroy (removal) + :rtype: bool + """ + response: Union[Dict[str, Any], int] = await self._client.request( + self._client.endpoints.friend(user_id), + headers=self._client.authorization_header, + request_type=RequestType.DELETE) + return Utils.validate_response_data(response, fallback=False) diff --git a/shikithon/resources/genres.py b/shikithon/resources/genres.py new file mode 100644 index 00000000..0cb344f7 --- /dev/null +++ b/shikithon/resources/genres.py @@ -0,0 +1,28 @@ +"""Represents /api/genres resource.""" +from typing import Any, Dict, List + +from ..decorators import method_endpoint +from ..models import Genre +from ..utils import Utils +from .base_resource import BaseResource + + +class Genres(BaseResource): + """Genres resource class. + + Used to represent /api/genres resource. + """ + + @method_endpoint('/api/genres') + async def get(self) -> List[Genre]: + """ + Returns list of genres. + + :return: List of genres + :rtype: List[Genre] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.genres) + return Utils.validate_response_data(response, + data_model=Genre, + fallback=[]) diff --git a/shikithon/resources/mangas.py b/shikithon/resources/mangas.py new file mode 100644 index 00000000..21dd30b6 --- /dev/null +++ b/shikithon/resources/mangas.py @@ -0,0 +1,263 @@ +"""Represents /api/mangas resource.""" +from typing import Any, Dict, List, Optional, Union + +from ..decorators import method_endpoint +from ..enums import MangaCensorship +from ..enums import MangaKind +from ..enums import MangaList +from ..enums import MangaOrder +from ..enums import MangaStatus +from ..models import Creator +from ..models import FranchiseTree +from ..models import Link +from ..models import Manga +from ..models import Relation +from ..models import Topic +from ..utils import Utils +from .base_resource import BaseResource + + +class Mangas(BaseResource): + """Mangas resource class. + + Used to represent /api/mangas resource. + """ + + @method_endpoint('/api/mangas') + async def get_all(self, + page: Optional[int] = None, + limit: Optional[int] = None, + order: Optional[str] = None, + kind: Optional[Union[str, List[str]]] = None, + status: Optional[Union[str, List[str]]] = None, + season: Optional[Union[str, List[str]]] = None, + score: Optional[int] = None, + genre: Optional[Union[int, List[int]]] = None, + publisher: Optional[Union[int, List[int]]] = None, + franchise: Optional[Union[int, List[int]]] = None, + censored: Optional[str] = None, + my_list: Optional[Union[str, List[str]]] = None, + ids: Optional[Union[int, List[int]]] = None, + exclude_ids: Optional[Union[int, List[int]]] = None, + search: Optional[str] = None) -> List[Manga]: + """ + Returns mangas list. + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + :type limit: Optional[int] + + :param order: Type of order in list + :type order: Optional[str] + + :param kind: Type(s) of manga topic + :type kind: Optional[Union[str, List[str]] + + :param status: Type(s) of manga status + :type status: Optional[Union[str, List[str]]] + + :param season: Name(s) of manga seasons + :type season: Optional[Union[str, List[str]]] + + :param score: Minimal manga score + :type score: Optional[int] + + :param publisher: Publisher(s) ID + :type publisher: Optional[Union[int, List[int]] + + :param genre: Genre(s) ID + :type genre: Optional[Union[int, List[int]] + + :param franchise: Franchise(s) ID + :type franchise: Optional[Union[int, List[int]] + + :param censored: Type of manga censorship + :type censored: Optional[str] + + :param my_list: Status(-es) of manga in current user list. + If app is in restricted mode, + this parameter won't affect on response. + :type my_list: Optional[Union[str, List[str]]] + + :param ids: Manga(s) ID to include + :type ids: Optional[Union[int, List[int]] + + :param exclude_ids: Manga(s) ID to exclude + :type exclude_ids: Optional[Union[int, List[int]] + + :param search: Search phrase to filter mangas by name + :type search: Optional[str] + + :return: List of Mangas + :rtype: List[Manga] + """ + if not Utils.validate_enum_params({ + MangaOrder: order, + MangaKind: kind, + MangaStatus: status, + MangaCensorship: censored, + MangaList: my_list + }): + return [] + + validated_numbers = Utils.query_numbers_validator(page=[page, 100000], + limit=[limit, 50], + score=[score, 9]) + + headers: Dict[str, str] = self._client.user_agent + + if my_list: + headers = self._client.protected_method_headers('/api/mangas') + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.mangas, + headers=headers, + query=Utils.create_query_dict(page=validated_numbers['page'], + limit=validated_numbers['limit'], + order=order, + kind=kind, + status=status, + season=season, + score=validated_numbers['score'], + genre=genre, + publisher=publisher, + franchise=franchise, + censored=censored, + mylist=my_list, + ids=ids, + exclude_ids=exclude_ids, + search=search)) + return Utils.validate_response_data(response, + data_model=Manga, + fallback=[]) + + @method_endpoint('/api/mangas/:id') + async def get(self, manga_id: int) -> Optional[Manga]: + """ + Returns info about certain manga. + + :param manga_id: Manga ID to get info + :type manga_id: int + + :return: Manga info + :rtype: Optional[Manga] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.manga(manga_id)) + return Utils.validate_response_data(response, data_model=Manga) + + @method_endpoint('/api/mangas/:id/roles') + async def creators(self, manga_id: int) -> List[Creator]: + """ + Returns creators info of certain manga. + + :param manga_id: Manga ID to get creators + :type manga_id: int + + :return: List of manga creators + :rtype: List[Creator] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.manga_roles(manga_id)) + return Utils.validate_response_data(response, + data_model=Creator, + fallback=[]) + + @method_endpoint('/api/mangas/:id/similar') + async def similar(self, manga_id: int) -> List[Manga]: + """ + Returns list of similar mangas for certain manga. + + :param manga_id: Manga ID to get similar mangas + :type manga_id: int + + :return: List of similar mangas + :rtype: List[Manga] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.similar_mangas(manga_id)) + return Utils.validate_response_data(response, data_model=Manga) + + @method_endpoint('/api/mangas/:id/related') + async def related_content(self, manga_id: int) -> List[Relation]: + """ + Returns list of related content of certain manga. + + :param manga_id: Manga ID to get related content + :type manga_id: int + + :return: List of relations + :rtype: List[Relation] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.manga_related_content(manga_id)) + return Utils.validate_response_data(response, + data_model=Relation, + fallback=[]) + + @method_endpoint('/api/mangas/:id/franchise') + async def franchise_tree(self, manga_id: int) -> Optional[FranchiseTree]: + """ + Returns franchise tree of certain manga. + + :param manga_id: Manga ID to get franchise tree + :type manga_id: int + + :return: Franchise tree of certain manga + :rtype: Optional[FranchiseTree] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.manga_franchise_tree(manga_id)) + return Utils.validate_response_data(response, data_model=FranchiseTree) + + @method_endpoint('/api/mangas/:id/external_links') + async def external_links(self, manga_id: int) -> List[Link]: + """ + Returns list of external links of certain manga. + + :param manga_id: Manga ID to get external links + :type manga_id: int + + :return: List of external links + :rtype: List[Link] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.manga_external_links(manga_id)) + return Utils.validate_response_data(response, + data_model=Link, + fallback=[]) + + @method_endpoint('/api/mangas/:id/topics') + async def topics(self, + manga_id: int, + page: Optional[int] = None, + limit: Optional[int] = None) -> List[Topic]: + """ + Returns list of topics of certain manga. + + :param manga_id: Manga ID to get topics + :type manga_id: int + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + :type limit: Optional[int] + + :return: List of topics + :rtype: List[Topic] + """ + validated_numbers = Utils.query_numbers_validator( + page=[page, 100000], + limit=[limit, 30], + ) + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.manga_topics(manga_id), + query=Utils.create_query_dict(page=validated_numbers['page'], + limit=validated_numbers['limit'])) + return Utils.validate_response_data(response, + data_model=Topic, + fallback=[]) diff --git a/shikithon/resources/messages.py b/shikithon/resources/messages.py new file mode 100644 index 00000000..44878be0 --- /dev/null +++ b/shikithon/resources/messages.py @@ -0,0 +1,186 @@ +"""Represents /api/messages resource.""" +from typing import Any, Dict, List, Optional, Union + +from ..decorators import method_endpoint +from ..decorators import protected_method +from ..enums import MessageType +from ..enums import RequestType +from ..enums import ResponseCode +from ..models import Message +from ..utils import Utils +from .base_resource import BaseResource + + +class Messages(BaseResource): + """Messages resource class. + + Used to represent /api/messages resource. + """ + + @method_endpoint('/api/messages/:id') + @protected_method('_client', 'messages') + async def get(self, message_id: int) -> Optional[Message]: + """ + Returns message info. + + :param message_id: ID of message to get info + :type message_id: int + + :return: Message info + :rtype: Optional[Message] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.message(message_id), + headers=self._client.authorization_header) + return Utils.validate_response_data(response, data_model=Message) + + @method_endpoint('/api/messages') + @protected_method('_client', 'messages') + async def create(self, body: str, from_id: int, + to_id: int) -> Optional[Message]: + """ + Creates message. + + :param body: Body of message + :type body: str + + :param from_id: Sender ID + :type from_id: int + + :param to_id: Reciver ID + :type to_id: int + + :return: Created message info + :rtype: Optional[Message] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.messages, + headers=self._client.authorization_header, + data=Utils.create_data_dict(dict_name='message', + body=body, + from_id=from_id, + kind='Private', + to_id=to_id), + request_type=RequestType.POST) + return Utils.validate_response_data(response, data_model=Message) + + @method_endpoint('/api/messages/:id') + @protected_method('_client', 'messages') + async def update(self, message_id: int, body: str) -> Optional[Message]: + """ + Updates message. + + :param message_id: ID of message to update + :type message_id: int + + :param body: New body of message + :type body: str + + :return: Updated message info or None if message cannot be updated + :rtype: Optional[Message] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.message(message_id), + headers=self._client.authorization_header, + data=Utils.create_data_dict(dict_name='message', body=body), + request_type=RequestType.PATCH) + return Utils.validate_response_data(response, data_model=Message) + + @method_endpoint('/api/messages/:id') + @protected_method('_client', 'messages', fallback=False) + async def delete(self, message_id: int) -> bool: + """ + Deletes message. + + :param message_id: ID of message to delete + :type message_id: int + + :return: Status of message deletion + :rtype: bool + """ + response: Union[Dict[str, Any], int] = await self._client.request( + self._client.endpoints.message(message_id), + headers=self._client.authorization_header, + request_type=RequestType.DELETE) + return Utils.validate_response_data( + response, response_code=ResponseCode.NO_CONTENT, fallback=False) + + @method_endpoint('/api/messages/mark_read') + @protected_method('_client', 'messages', fallback=False) + async def mark_read(self, + message_ids: Optional[Union[int, List[int]]] = None, + is_read: Optional[bool] = None) -> bool: + """ + Marks read/unread selected messages. + + :param message_ids: ID(s) of messages to mark read/unread + :type message_ids: Optional[Union[int, List[int]]] + + :param is_read: Status of message (read/unread) + :type is_read: Optional[bool] + + :return: Status of messages read/unread + :rtype: bool + """ + response: Union[Dict[str, Any], int] = await self._client.request( + self._client.endpoints.messages_mark_read, + headers=self._client.authorization_header, + data=Utils.create_query_dict(ids=message_ids, is_read=is_read), + request_type=RequestType.POST) + return Utils.validate_response_data(response, + response_code=ResponseCode.SUCCESS, + fallback=False) + + @method_endpoint('/api/messages/read_all') + @protected_method('_client', 'messages', fallback=False) + async def read_all(self, message_type: str) -> bool: + """ + Reads all messages on current user's account. + + **Note:** This methods accepts as type only 'news' and + 'notifications' + + :param message_type: Type of messages to read + :type message_type: str + + :return: Status of messages read + :rtype: bool + """ + if not Utils.validate_enum_params({MessageType: message_type}): + return False + + response: Union[Dict[str, Any], int] = await self._client.request( + self._client.endpoints.messages_read_all, + headers=self._client.authorization_header, + data=Utils.create_query_dict(type=message_type), + request_type=RequestType.POST) + return Utils.validate_response_data(response, + response_code=ResponseCode.SUCCESS, + fallback=False) + + @method_endpoint('/api/messages/delete_all') + @protected_method('_client', 'messages', fallback=False) + async def delete_all(self, message_type: str) -> bool: + """ + Deletes all messages on current user's account. + + **Note:** This methods accepts as type only 'news' and + 'notifications' + + :param message_type: Type of messages to delete + :type message_type: str + + :return: Status of messages deletion + :rtype: bool + """ + if not Utils.validate_enum_params({MessageType: message_type}): + return False + + response: Union[Dict[str, Any], int] = await self._client.request( + self._client.endpoints.messages_delete_all, + headers=self._client.authorization_header, + data=Utils.create_query_dict(type=message_type), + request_type=RequestType.POST) + return Utils.validate_response_data(response, + response_code=ResponseCode.SUCCESS, + fallback=False) diff --git a/shikithon/resources/people.py b/shikithon/resources/people.py new file mode 100644 index 00000000..2bb34345 --- /dev/null +++ b/shikithon/resources/people.py @@ -0,0 +1,59 @@ +"""Represents /api/people resource.""" +from typing import Any, Dict, List, Optional + +from ..decorators import method_endpoint +from ..enums import PersonKind +from ..models import Person +from ..utils import Utils +from .base_resource import BaseResource + + +class People(BaseResource): + """People resource class. + + Used to represent /api/people resource. + """ + + @method_endpoint('/api/people/:id') + async def get(self, people_id: int) -> Optional[Person]: + """ + Returns info about a person. + + :param people_id: ID of person to get info + :type people_id: int + + :return: Info about a person + :rtype: Optional[Person] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.people(people_id)) + return Utils.validate_response_data(response, data_model=Person) + + @method_endpoint('/api/people/search') + async def search(self, + search: Optional[str] = None, + people_kind: Optional[str] = None) -> List[Person]: + """ + Returns list of found persons. + + **Note:** This API method only allows 'seyu', + 'mangaka' or 'producer' as kind parameter + + :param search: Search query for persons + :type search: Optional[str] + + :param people_kind: Kind of person for searching + :type people_kind: Optional[str] + + :return: List of found persons + :rtype: List[Person] + """ + if not Utils.validate_enum_params({PersonKind: people_kind}): + return [] + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.people_search, + query=Utils.create_query_dict(search=search, kind=people_kind)) + return Utils.validate_response_data(response, + data_model=Person, + fallback=[]) diff --git a/shikithon/resources/publishers.py b/shikithon/resources/publishers.py new file mode 100644 index 00000000..c8eee8ae --- /dev/null +++ b/shikithon/resources/publishers.py @@ -0,0 +1,28 @@ +"""Represents /api/publishers resource.""" +from typing import Any, Dict, List + +from ..decorators import method_endpoint +from ..models import Publisher +from ..utils import Utils +from .base_resource import BaseResource + + +class Publishers(BaseResource): + """Publishers resource class. + + Used to represent /api/publishers resource. + """ + + @method_endpoint('/api/publishers') + async def get(self) -> List[Publisher]: + """ + Returns list of publishers. + + :return: List of publishers + :rtype: List[Publisher] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.publishers) + return Utils.validate_response_data(response, + data_model=Publisher, + fallback=[]) diff --git a/shikithon/resources/ranobes.py b/shikithon/resources/ranobes.py new file mode 100644 index 00000000..7c2db803 --- /dev/null +++ b/shikithon/resources/ranobes.py @@ -0,0 +1,258 @@ +"""Represents /api/ranobes resource.""" +from typing import Any, Dict, List, Optional, Union + +from ..decorators import method_endpoint +from ..enums import RanobeCensorship +from ..enums import RanobeList +from ..enums import RanobeOrder +from ..enums import RanobeStatus +from ..models import Creator +from ..models import FranchiseTree +from ..models import Link +from ..models import Ranobe +from ..models import Relation +from ..models import Topic +from ..utils import Utils +from .base_resource import BaseResource + + +class Ranobes(BaseResource): + """Ranobes resource class. + + Used to represent /api/ranobes resource. + """ + + @method_endpoint('/api/ranobe') + async def get_all(self, + page: Optional[int] = None, + limit: Optional[int] = None, + order: Optional[str] = None, + status: Optional[Union[str, List[str]]] = None, + season: Optional[Union[str, List[str]]] = None, + score: Optional[int] = None, + genre: Optional[Union[int, List[int]]] = None, + publisher: Optional[Union[int, List[int]]] = None, + franchise: Optional[Union[int, List[int]]] = None, + censored: Optional[str] = None, + my_list: Optional[Union[str, List[str]]] = None, + ids: Optional[Union[int, List[int]]] = None, + exclude_ids: Optional[Union[int, List[int]]] = None, + search: Optional[str] = None) -> List[Ranobe]: + """ + Returns ranobe list. + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + :type limit: Optional[int] + + :param order: Type of order in list + :type order: Optional[str] + + :param status: Type(s) of ranobe status + :type status: Optional[Union[str, List[str]]] + + :param season: Name(s) of ranobe seasons + :type season: Optional[Union[str, List[str]]] + + :param score: Minimal ranobe score + :type score: Optional[int] + + :param publisher: Publisher(s) ID + :type publisher: Optional[Union[int, List[int]] + + :param genre: Genre(s) ID + :type genre: Optional[Union[int, List[int]] + + :param franchise: Franchise(s) ID + :type franchise: Optional[Union[int, List[int]] + + :param censored: Type of ranobe censorship + :type censored: Optional[str] + + :param my_list: Status(-es) of ranobe in current user list. + If app is in restricted mode, + this parameter won't affect on response. + :type my_list: Optional[Union[str, List[str]]] + + :param ids: Ranobe(s) ID to include + :type ids: Optional[Union[int, List[int]] + + :param exclude_ids: Ranobe(s) ID to exclude + :type exclude_ids: Optional[Union[int, List[int]] + + :param search: Search phrase to filter ranobe by name + :type search: Optional[str] + + :return: List of Ranobe + :rtype: List[Ranobe] + """ + if not Utils.validate_enum_params({ + RanobeOrder: order, + RanobeStatus: status, + RanobeList: my_list, + RanobeCensorship: censored + }): + return [] + + validated_numbers = Utils.query_numbers_validator(page=[page, 100000], + limit=[limit, 50], + score=[score, 9]) + + headers: Dict[str, str] = self._client.user_agent + + if my_list: + headers = self._client.protected_method_headers('/api/ranobe') + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.ranobes, + headers=headers, + query=Utils.create_query_dict(page=validated_numbers['page'], + limit=validated_numbers['limit'], + order=order, + status=status, + season=season, + score=validated_numbers['score'], + genre=genre, + publisher=publisher, + franchise=franchise, + censored=censored, + mylist=my_list, + ids=ids, + exclude_ids=exclude_ids, + search=search)) + return Utils.validate_response_data(response, + data_model=Ranobe, + fallback=[]) + + @method_endpoint('/api/ranobe/:id') + async def get(self, ranobe_id: int) -> Optional[Ranobe]: + """ + Returns info about certain ranobe. + + :param ranobe_id: Ranobe ID to get info + :type ranobe_id: int + + :return: Ranobe info + :rtype: Optional[Ranobe] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.ranobe(ranobe_id)) + return Utils.validate_response_data(response, data_model=Ranobe) + + @method_endpoint('/api/ranobe/:id/roles') + async def creators(self, ranobe_id: int) -> List[Creator]: + """ + Returns creators info of certain ranobe. + + :param ranobe_id: Ranobe ID to get creators + :type ranobe_id: int + + :return: List of ranobe creators + :rtype: List[Creator] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.ranobe_roles(ranobe_id)) + return Utils.validate_response_data(response, + data_model=Creator, + fallback=[]) + + @method_endpoint('/api/ranobe/:id/similar') + async def similar(self, ranobe_id: int) -> List[Ranobe]: + """ + Returns list of similar ranobes for certain ranobe. + + :param ranobe_id: Ranobe ID to get similar ranobes + :type ranobe_id: int + + :return: List of similar ranobes + :rtype: List[Ranobe] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.similar_ranobes(ranobe_id)) + return Utils.validate_response_data(response, + data_model=Ranobe, + fallback=[]) + + @method_endpoint('/api/ranobe/:id/related') + async def related_content(self, ranobe_id: int) -> List[Relation]: + """ + Returns list of related content of certain ranobe. + + :param ranobe_id: Ranobe ID to get related content + :type ranobe_id: int + + :return: List of relations + :rtype: List[Relation] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.ranobe_related_content(ranobe_id)) + return Utils.validate_response_data(response, + data_model=Relation, + fallback=[]) + + @method_endpoint('/api/ranobe/:id/franchise') + async def franchise_tree(self, ranobe_id: int) -> Optional[FranchiseTree]: + """ + Returns franchise tree of certain ranobe. + + :param ranobe_id: Ranobe ID to get franchise tree + :type ranobe_id: int + + :return: Franchise tree of certain ranobe + :rtype: Optional[FranchiseTree] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.ranobe_franchise_tree(ranobe_id)) + return Utils.validate_response_data(response, data_model=FranchiseTree) + + @method_endpoint('/api/ranobe/:id/external_links') + async def external_links(self, ranobe_id: int) -> List[Link]: + """ + Returns list of external links of certain ranobe. + + :param ranobe_id: Ranobe ID to get external links + :type ranobe_id: int + + :return: List of external links + :rtype: List[Link] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.ranobe_external_links(ranobe_id)) + return Utils.validate_response_data(response, + data_model=Link, + fallback=[]) + + @method_endpoint('/api/ranobe/:id/topics') + async def topics(self, + ranobe_id: int, + page: Optional[int] = None, + limit: Optional[int] = None) -> List[Topic]: + """ + Returns list of topics of certain ranobe. + + :param ranobe_id: Ranobe ID to get topics + :type ranobe_id: int + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + :type limit: Optional[int] + + :return: List of topics + :rtype: List[Topic] + """ + validated_numbers = Utils.query_numbers_validator( + page=[page, 100000], + limit=[limit, 30], + ) + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.ranobe_topics(ranobe_id), + query=Utils.create_query_dict(page=validated_numbers['page'], + limit=validated_numbers['limit'])) + return Utils.validate_response_data(response, + data_model=Topic, + fallback=[]) diff --git a/shikithon/resources/stats.py b/shikithon/resources/stats.py new file mode 100644 index 00000000..6419c43e --- /dev/null +++ b/shikithon/resources/stats.py @@ -0,0 +1,25 @@ +"""Represents /api/stats resource.""" +from typing import List + +from ..decorators import method_endpoint +from ..utils import Utils +from .base_resource import BaseResource + + +class Stats(BaseResource): + """Stats resource class. + + Used to represent /api/stats resource. + """ + + @method_endpoint('/api/stats/active_users') + async def active_users(self) -> List[int]: + """ + Returns list of IDs of active users. + + :return: List of IDs of active users + :rtype: List[int] + """ + response: List[int] = await self._client.request( + self._client.endpoints.active_users) + return Utils.validate_response_data(response, fallback=[]) diff --git a/shikithon/resources/studios.py b/shikithon/resources/studios.py new file mode 100644 index 00000000..a1d12db0 --- /dev/null +++ b/shikithon/resources/studios.py @@ -0,0 +1,28 @@ +"""Represents /api/studios resource.""" +from typing import Any, Dict, List + +from ..decorators import method_endpoint +from ..models import Studio +from ..utils import Utils +from .base_resource import BaseResource + + +class Studios(BaseResource): + """Studios resource class. + + Used to represent /api/studios resource. + """ + + @method_endpoint('/api/studios') + async def get(self) -> List[Studio]: + """ + Returns list of studios. + + :return: List of studios + :rtype: List[Studio] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.studios) + return Utils.validate_response_data(response, + data_model=Studio, + fallback=[]) diff --git a/shikithon/resources/styles.py b/shikithon/resources/styles.py new file mode 100644 index 00000000..61138065 --- /dev/null +++ b/shikithon/resources/styles.py @@ -0,0 +1,117 @@ +"""Represents /api/styles resource.""" +from typing import Any, Dict, Optional + +from loguru import logger + +from ..decorators import method_endpoint +from ..enums import OwnerType +from ..enums import RequestType +from ..models import Style +from ..utils import Utils +from .base_resource import BaseResource + + +class Styles(BaseResource): + """Styles resource class. + + Used to represent /api/styles resource. + """ + + @method_endpoint('/api/styles/:id') + async def get(self, style_id: int) -> Optional[Style]: + """ + Returns info about style. + + :param style_id: Style ID to get info + :type style_id: int + + :return: Info about style + :rtype: Optional[Style] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.style(style_id)) + return Utils.validate_response_data(response, data_model=Style) + + @method_endpoint('/api/styles/preview') + async def preview(self, css: str) -> Optional[Style]: + """ + Previews style with passed CSS code. + + :param css: CSS code to preview + :type css: str + + :return: Info about previewed style + :rtype: Optional[Style] + """ + if not css: + logger.warning('No CSS code passed to preview') + return None + + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.style_preview, + headers=self._client.authorization_header, + data=Utils.create_data_dict(dict_name='style', css=css), + request_type=RequestType.POST) + return Utils.validate_response_data(response, data_model=Style) + + @method_endpoint('/api/styles') + async def create(self, css: str, name: str, owner_id: int, + owner_type: str) -> Optional[Style]: + """ + Creates new style. + + :param css: CSS code for style + :type css: str + + :param name: Style name + :type name: str + + :param owner_id: User/Club ID for style ownership + :type owner_id: int + + :param owner_type: Type of owner (User/Club) + :type owner_type: str + + :return: Info about previewed style + :rtype: Optional[Style] + """ + if not Utils.validate_enum_params({OwnerType: owner_type}): + return None + + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.styles, + headers=self._client.authorization_header, + data=Utils.create_data_dict(dict_name='style', + css=css, + name=name, + owner_id=owner_id, + owner_type=owner_type), + request_type=RequestType.POST) + return Utils.validate_response_data(response, data_model=Style) + + @method_endpoint('/api/styles/:id') + async def update(self, + style_id: int, + css: Optional[str] = None, + name: Optional[str] = None) -> Optional[Style]: + """ + Updates existing style. + + :param style_id: ID of existing style for edit + :type style_id: int + + :param css: New CSS code for style + :type css: Optional[str] + + :param name: New style name + :type name: Optional[str] + + :return: Info about updated style + :rtype: Optional[Style] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.style(style_id), + headers=self._client.authorization_header, + data=Utils.create_data_dict(dict_name='style', css=css, name=name), + request_type=RequestType.PATCH) + return Utils.validate_response_data(response, data_model=Style) diff --git a/shikithon/resources/topics.py b/shikithon/resources/topics.py new file mode 100644 index 00000000..6accd453 --- /dev/null +++ b/shikithon/resources/topics.py @@ -0,0 +1,289 @@ +"""Represents /api/topics and /api/v2/topics resource.""" +from typing import Any, Dict, List, Optional, Union + +from ..decorators import method_endpoint +from ..decorators import protected_method +from ..enums import ForumType +from ..enums import RequestType +from ..enums import ResponseCode +from ..enums import TopicLinkedType +from ..enums import TopicType +from ..models import Topic +from ..utils import Utils +from .base_resource import BaseResource + + +class Topics(BaseResource): + """Topics resource class. + + Used to represent /api/topics and /api/v2/topics resource. + """ + + @method_endpoint('/api/topics') + async def get_all(self, + page: Optional[int] = None, + limit: Optional[int] = None, + forum: Optional[str] = None, + linked_id: Optional[int] = None, + linked_type: Optional[str] = None, + topic_type: Optional[str] = None) -> List[Topic]: + """ + Returns list of topics. + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + :type limit: Optional[int] + + :param forum: Number of results limit + :type forum: Optional[str] + + :param linked_id: ID of linked topic (Used together with linked_type) + :type linked_id: Optional[int] + + :param linked_type: Type of linked topic (Used together with linked_id) + :type linked_type: Optional[str] + + :param topic_type: Type of topic. + :type topic_type: Optional[str] + + :return: List of topics + :rtype: List[Topic] + """ + if not Utils.validate_enum_params({ + ForumType: forum, + TopicLinkedType: linked_type, + TopicType: topic_type + }): + return [] + + validated_numbers = Utils.query_numbers_validator( + page=[page, 100000], + limit=[limit, 30], + ) + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.topics, + query=Utils.create_query_dict(page=validated_numbers['page'], + limit=validated_numbers['limit'], + forum=forum, + linked_id=linked_id, + linked_type=linked_type, + type=topic_type)) + return Utils.validate_response_data(response, + data_model=Topic, + fallback=[]) + + @method_endpoint('/api/topics/updates') + async def updates(self, + page: Optional[int] = None, + limit: Optional[int] = None) -> List[Topic]: + """ + Returns list of NewsTopics about database updates. + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + :type limit: Optional[int] + + :return: List of topics + :rtype: List[Topic] + """ + validated_numbers = Utils.query_numbers_validator( + page=[page, 100000], + limit=[limit, 30], + ) + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.updates_topics, + query=Utils.create_query_dict(page=validated_numbers['page'], + limit=validated_numbers['limit'])) + return Utils.validate_response_data(response, + data_model=Topic, + fallback=[]) + + @method_endpoint('/api/topics/hot') + async def hot(self, limit: Optional[int] = None) -> List[Topic]: + """ + Returns list of hot topics. + + :param limit: Number of results limit + :type limit: Optional[int] + + :return: List of topics + :rtype: List[Topic] + """ + validated_numbers = Utils.query_numbers_validator(limit=[limit, 10]) + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.hot_topics, + query=Utils.create_query_dict(limit=validated_numbers['limit'])) + return Utils.validate_response_data(response, + data_model=Topic, + fallback=[]) + + @method_endpoint('/api/topics/:id') + async def get(self, topic_id: int) -> Optional[Topic]: + """ + Returns info about topic. + + :param topic_id: ID of topic to get + :type topic_id: int + + :return: Info about topic + :rtype: Optional[Topic] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.topic(topic_id)) + return Utils.validate_response_data(response, data_model=Topic) + + @method_endpoint('/api/topics') + @protected_method('_client', 'topics') + async def create(self, + body: str, + forum_id: int, + title: str, + user_id: int, + linked_id: Optional[int] = None, + linked_type: Optional[str] = None) -> Optional[Topic]: + """ + Creates topic. + + :param body: Body of topic + :type body: str + + :param forum_id: ID of forum to post + :type forum_id: int + + :param title: Title of topic + :type title: str + + :param user_id: ID of topic creator + :type user_id: int + + :param linked_id: ID of linked topic (Used together with linked_type) + :type linked_type: Optional[int] + + :param linked_type: Type of linked topic (Used together with linked_id) + :type linked_type: Optional[str] + + :return: Created topic info + :rtype: Optional[Topic] + """ + if not Utils.validate_enum_params({TopicLinkedType: linked_type}): + return None + + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.topics, + headers=self._client.authorization_header, + data=Utils.create_data_dict(dict_name='topic', + body=body, + forum_id=forum_id, + linked_id=linked_id, + linked_type=linked_type, + title=title, + type=str(TopicType.REGULAR_TOPIC.value), + user_id=user_id), + request_type=RequestType.POST) + return Utils.validate_response_data(response, data_model=Topic) + + @method_endpoint('/api/topics/:id') + @protected_method('_client', 'topics') + async def update(self, + topic_id: int, + body: Optional[str] = None, + title: Optional[str] = None, + linked_id: Optional[int] = None, + linked_type: Optional[str] = None) -> Optional[Topic]: + """ + Updates topic. + + :param topic_id: ID of topic to update + :type topic_id: int + + :param body: Body of topic + :type body: Optional[str] + + :param title: Title of topic + :type title: Optional[str] + + :param linked_id: ID of linked topic (Used together with linked_type) + :type linked_type: Optional[int] + + :param linked_type: Type of linked topic (Used together with linked_id) + :type linked_type: Optional[str] + + :return: Updated topic info + :rtype: Optional[Topic] + """ + if not Utils.validate_enum_params({TopicLinkedType: linked_type}): + return None + + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.topic(topic_id), + headers=self._client.authorization_header, + data=Utils.create_data_dict(dict_name='topic', + body=body, + linked_id=linked_id, + linked_type=linked_type, + title=title), + request_type=RequestType.PATCH) + return Utils.validate_response_data(response, + response_code=ResponseCode.SUCCESS, + data_model=Topic) + + @method_endpoint('/api/topics/:id') + @protected_method('_client', 'topics', fallback=False) + async def delete(self, topic_id: int) -> bool: + """ + Deletes topic. + + :param topic_id: ID of topic to delete + :type topic_id: int + + :return: Status of topic deletion + :rtype: bool + """ + response: Union[Dict[str, Any], int] = await self._client.request( + self._client.endpoints.topic(topic_id), + headers=self._client.authorization_header, + request_type=RequestType.DELETE) + return Utils.validate_response_data(response, fallback=False) + + @method_endpoint('/api/v2/topics/:topic_id/ignore') + @protected_method('_client', 'topics', fallback=False) + async def ignore(self, topic_id: int) -> bool: + """ + Set topic as ignored. + + :param topic_id: ID of topic to ignore + :type topic_id: int + + :return: True if topic was ignored, False otherwise + :rtype: bool + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.topic_ignore(topic_id), + headers=self._client.authorization_header, + request_type=RequestType.POST) + return Utils.validate_response_data(response, fallback=False) is True + + @method_endpoint('/api/v2/topics/:topic_id/ignore') + @protected_method('_client', 'topics', fallback=True) + async def unignore(self, topic_id: int) -> bool: + """ + Set topic as unignored. + + :param topic_id: ID of topic to unignore + :type topic_id: int + + :return: True if topic was unignored, False otherwise + :rtype: bool + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.topic_ignore(topic_id), + headers=self._client.authorization_header, + request_type=RequestType.DELETE) + return Utils.validate_response_data(response, fallback=True) is False diff --git a/shikithon/resources/user_images.py b/shikithon/resources/user_images.py new file mode 100644 index 00000000..c936021a --- /dev/null +++ b/shikithon/resources/user_images.py @@ -0,0 +1,44 @@ +"""Represents /api/user_images resource.""" +from typing import Any, Dict, Optional, Union + +from ..decorators import method_endpoint +from ..decorators import protected_method +from ..enums import RequestType +from ..models import CreatedUserImage +from ..utils import Utils +from .base_resource import BaseResource + + +class UserImages(BaseResource): + """UserImages resource class. + + Used to represent /api/user_images resource. + """ + + @method_endpoint('/api/user_images') + @protected_method('_client', 'comments') + async def create( + self, + image_path: str, + linked_type: Optional[str] = None) -> Optional[CreatedUserImage]: + """ + Creates an user image. + + :param image_path: Path or URL to image to create on server + :type image_path: str + + :param linked_type: Type of linked image + :type linked_type: Optional[str] + + :return: Created image info + :rtype: Optional[CreatedUserImage] + """ + image_data = await Utils.get_image_data(image_path) + response: Union[Dict[str, Any], int] = await self._client.request( + self._client.endpoints.user_images, + headers=self._client.authorization_header, + data=Utils.create_data_dict(linked_type=linked_type), + bytes_data=image_data, + request_type=RequestType.POST) + return Utils.validate_response_data(response, + data_model=CreatedUserImage) diff --git a/shikithon/resources/user_rates.py b/shikithon/resources/user_rates.py new file mode 100644 index 00000000..cf43c077 --- /dev/null +++ b/shikithon/resources/user_rates.py @@ -0,0 +1,317 @@ +"""Represents /api/user_rates and /api/v2/user_rates resource.""" +from typing import Any, Dict, List, Optional, Union + +from loguru import logger + +from ..decorators import method_endpoint +from ..decorators import protected_method +from ..enums import RequestType +from ..enums import ResponseCode +from ..enums import UserRateStatus +from ..enums import UserRateTarget +from ..enums import UserRateType +from ..models import UserRate +from ..utils import Utils +from .base_resource import BaseResource + + +class UserRates(BaseResource): + """UserRates resource class. + + Used to represent /api/user_rates and /api/v2/user_rates resource. + """ + + @method_endpoint('/api/v2/user_rates') + async def get_all(self, + user_id: int, + target_id: Optional[int] = None, + target_type: Optional[str] = None, + status: Optional[str] = None, + page: Optional[int] = None, + limit: Optional[int] = None) -> List[UserRate]: + """ + Returns list of user rates. + + **Note:** When passing target_id, target_type is required. + + Also there is a strange API behavior, so when pass nothing, + endpoint not working. + However, docs shows that page/limit ignored when user_id is set (bruh) + + :param user_id: ID of user to get rates for + :type user_id: int + + :param target_id: ID of anime/manga to get rates for + :type target_id: Optional[int] + + :param target_type: Type of target_id to get rates for + :type target_type: Optional[str] + + :param status: Status of target_type to get rates for + :type target_type: Optional[str] + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + (This field is ignored when user_id is set) + :type limit: Optional[int] + + :return: List with info about user rates + :rtype: List[UserRate] + """ + if target_id is not None and target_type is None: + logger.warning('target_type is required when passing target_id') + return [] + + if not Utils.validate_enum_params({ + UserRateTarget: target_type, + UserRateStatus: status + }): + return [] + + validated_numbers = Utils.query_numbers_validator( + page=[page, 100000], + limit=[limit, 1000], + ) + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.user_rates, + query=Utils.create_query_dict(user_id=user_id, + target_id=target_id, + target_type=target_type, + status=status, + page=validated_numbers['page'], + limit=validated_numbers['limit'])) + return Utils.validate_response_data(response, + data_model=UserRate, + fallback=[]) + + @method_endpoint('/api/v2/user_rates/:id') + async def get(self, rate_id: int) -> Optional[UserRate]: + """ + Returns info about user rate. + + :param rate_id: ID of rate to get + :type rate_id: int + + :return: Info about user rate + :rtype: Optional[UserRate] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.user_rate(rate_id)) + return Utils.validate_response_data(response, data_model=UserRate) + + @method_endpoint('/api/v2/user_rates') + @protected_method('_client', 'user_rates') + async def create(self, + user_id: int, + target_id: int, + target_type: str, + status: Optional[str] = None, + score: Optional[int] = None, + chapters: Optional[int] = None, + episodes: Optional[int] = None, + volumes: Optional[int] = None, + rewatches: Optional[int] = None, + text: Optional[str] = None) -> Optional[UserRate]: + """ + Creates new user rate and return info about it. + + :param user_id: ID of user to create user rate for + :type user_id: int + + :param target_id: ID of target to create user rate for + :type target_id: int + + :param target_type: Type of target_id to create user rate for + (Anime or Manga) + :type target_type: str + + :param status: Status of target + :type status: Optional[str] + + :param score: Score of target + :type score: Optional[int] + + :param chapters: Watched/read chapters of target + :type chapters: Optional[int] + + :param episodes: Watched/read episodes of target + :type episodes: Optional[int] + + :param volumes: Watched/read volumes of target + :type volumes: Optional[int] + + :param rewatches: Number of target rewatches + :type rewatches: Optional[int] + + :param text: Text note for user rate + :type text: Optional[str] + + :return: Info about new user rate + :rtype: Optional[UserRate] + """ + if not Utils.validate_enum_params({ + UserRateTarget: target_type, + UserRateStatus: status + }): + return None + + validated_numbers = Utils.query_numbers_validator(score=[score, 10]) + + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.user_rates, + headers=self._client.authorization_header, + data=Utils.create_data_dict(dict_name='user_rate', + user_id=user_id, + target_id=target_id, + target_type=target_type, + status=status, + score=validated_numbers['score'], + chapters=chapters, + episodes=episodes, + volumes=volumes, + rewatches=rewatches, + text=text), + request_type=RequestType.POST) + return Utils.validate_response_data(response, data_model=UserRate) + + @method_endpoint('/api/v2/user_rates/:id') + @protected_method('_client', 'user_rates') + async def update(self, + rate_id: int, + status: Optional[str] = None, + score: Optional[int] = None, + chapters: Optional[int] = None, + episodes: Optional[int] = None, + volumes: Optional[int] = None, + rewatches: Optional[int] = None, + text: Optional[str] = None) -> Optional[UserRate]: + """ + Updates user rate and return new info about it. + + :param rate_id: ID of user rate to edit + :type rate_id: int + + :param status: Status of target + :type status: Optional[str] + + :param score: Score of target + :type score: Optional[int] + + :param chapters: Watched/read chapters of target + :type chapters: Optional[int] + + :param episodes: Watched/read episodes of target + :type episodes: Optional[int] + + :param volumes: Watched/read volumes of target + :type volumes: Optional[int] + + :param rewatches: Number of target rewatches + :type rewatches: Optional[int] + + :param text: Text note for user rate + :type text: Optional[str] + + :return: Info about new user rate + :rtype: Optional[UserRate] + """ + if not Utils.validate_enum_params({UserRateStatus: status}): + return None + + validated_numbers = Utils.query_numbers_validator(score=[score, 10]) + + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.user_rate(rate_id), + headers=self._client.authorization_header, + data=Utils.create_data_dict(dict_name='user_rate', + status=status, + score=validated_numbers['score'], + chapters=chapters, + episodes=episodes, + volumes=volumes, + rewatches=rewatches, + text=text), + request_type=RequestType.PATCH) + return Utils.validate_response_data(response, data_model=UserRate) + + @method_endpoint('/api/v2/user_rates/:id/increment') + @protected_method('_client', 'user_rates') + async def increment(self, rate_id: int) -> Optional[UserRate]: + """ + Increments user rate episode/chapters and return updated info. + + :param rate_id: ID of user rate to increment episode/chapters + :type rate_id: int + + :return: Info about updated user rate + :rtype: Optional[UserRate] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.user_rate_increment(rate_id), + headers=self._client.authorization_header, + request_type=RequestType.POST) + return Utils.validate_response_data(response, data_model=UserRate) + + @method_endpoint('/api/v2/user_rates/:id') + @protected_method('_client', 'user_rates', fallback=False) + async def delete(self, rate_id: int) -> bool: + """ + Deletes user rate. + + :param rate_id: ID of user rate to delete + :type rate_id: int + + :return: Status of user rate deletion + :rtype: bool + """ + response: Union[Dict[str, Any], int] = await self._client.request( + self._client.endpoints.user_rate(rate_id), + headers=self._client.authorization_header, + request_type=RequestType.DELETE) + return Utils.validate_response_data( + response, response_code=ResponseCode.NO_CONTENT, fallback=False) + + @method_endpoint('/api/users_rates/:type/cleanup') + @protected_method('_client', 'user_rates', fallback=False) + async def delete_all(self, user_rate_type: str) -> bool: + """ + Deletes all user rates. + + :param user_rate_type: Type of user rates to delete + :type user_rate_type: str + + :return: Status of user rates deletion + :rtype: bool + """ + if not Utils.validate_enum_params({UserRateType: user_rate_type}): + return False + + response: Union[Dict[str, Any], int] = await self._client.request( + self._client.endpoints.user_rates_cleanup(user_rate_type), + headers=self._client.authorization_header, + request_type=RequestType.DELETE) + return Utils.validate_response_data(response, fallback=False) + + @method_endpoint('/api/user_rates/:type/reset') + @protected_method('_client', 'user_rates', fallback=False) + async def reset_all(self, user_rate_type: str) -> bool: + """ + Resets all user rates. + + :param user_rate_type: Type of user rates to reset + :type user_rate_type: UserRateType + + :return: Status of user rates reset + :rtype: bool + """ + if not Utils.validate_enum_params({UserRateType: user_rate_type}): + return False + + response: Union[Dict[str, Any], int] = await self._client.request( + self._client.endpoints.user_rates_reset(user_rate_type), + headers=self._client.authorization_header, + request_type=RequestType.DELETE) + return Utils.validate_response_data(response, fallback=False) diff --git a/shikithon/resources/users.py b/shikithon/resources/users.py new file mode 100644 index 00000000..fd8dce35 --- /dev/null +++ b/shikithon/resources/users.py @@ -0,0 +1,479 @@ +"""Represents /api/users and /api/v2/users resource.""" +from typing import Any, Dict, List, Optional, Union + +from ..decorators import method_endpoint +from ..decorators import protected_method +from ..enums import AnimeCensorship +from ..enums import AnimeList +from ..enums import MessageType +from ..enums import RequestType +from ..enums import TargetType +from ..models import Ban +from ..models import Club +from ..models import Favourites +from ..models import History +from ..models import Message +from ..models import UnreadMessages +from ..models import User +from ..models import UserList +from ..utils import Utils +from .base_resource import BaseResource + + +class Users(BaseResource): + """Users resource class. + + Used to represent /api/users and /api/v2/users resource. + """ + + @method_endpoint('/api/users') + async def get_all(self, + page: Optional[int] = None, + limit: Optional[int] = None) -> List[User]: + """ + Returns list of users. + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + :type limit: Optional[int] + + :return: List of users + :rtype: List[User] + """ + validated_numbers = Utils.query_numbers_validator( + page=[page, 100000], + limit=[limit, 100], + ) + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.users, + query=Utils.create_query_dict(page=validated_numbers['page'], + limit=validated_numbers['limit'])) + return Utils.validate_response_data(response, + data_model=User, + fallback=[]) + + @method_endpoint('/api/users/:id') + async def get(self, + user_id: Union[str, int], + is_nickname: Optional[bool] = None) -> Optional[User]: + """ + Returns info about user. + + :param user_id: User ID/Nickname to get info + :type user_id: Union[str, int] + + :param is_nickname: Specify if passed user_id is nickname + :type is_nickname: Optional[bool] + + :return: Info about user + :rtype: Optional[User] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.user(user_id), + query=Utils.create_query_dict(is_nickname=is_nickname)) + return Utils.validate_response_data(response, data_model=User) + + @method_endpoint('/api/users/:id/info') + async def info(self, + user_id: Union[str, int], + is_nickname: Optional[bool] = None) -> Optional[User]: + """ + Returns user's brief info. + + :param user_id: User ID/Nickname to get brief info + :type user_id: Union[int, str] + + :param is_nickname: Specify if passed user_id is nickname + :type is_nickname: Optional[bool] + + :return: User's brief info + :rtype: Optional[User] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.user_info(user_id), + query=Utils.create_query_dict(is_nickname=is_nickname)) + return Utils.validate_response_data(response, data_model=User) + + @method_endpoint('/api/users/whoami') + @protected_method('_client') + async def current(self) -> Optional[User]: + """ + Returns brief info about current user. + + Current user evaluated depending on authorization code. + + :return: Current user brief info + :rtype: Optional[User] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.whoami, + headers=self._client.authorization_header) + return Utils.validate_response_data(response, data_model=User) + + @method_endpoint('/api/users/sign_out') + @protected_method('_client', fallback=False) + async def sign_out(self) -> bool: + """ + Sends sign out request to API. + + :return: True if request was successful, False otherwise + :rtype: bool + """ + response: str = await self._client.request( + self._client.endpoints.sign_out, + headers=self._client.authorization_header) + return response == 'signed out' + + @method_endpoint('/api/users/:id/friends') + async def friends(self, + user_id: Union[str, int], + is_nickname: Optional[bool] = None) -> List[User]: + """ + Returns user's friends. + + :param user_id: User ID/Nickname to get friends + :type user_id: Union[int, str] + + :param is_nickname: Specify if passed user_id is nickname + :type is_nickname: Optional[bool] + + :return: List of user's friends + :rtype: List[User] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.user_friends(user_id), + query=Utils.create_query_dict(is_nickname=is_nickname)) + return Utils.validate_response_data(response, + data_model=User, + fallback=[]) + + @method_endpoint('/api/users/:id/clubs') + async def clubs(self, + user_id: Union[int, str], + is_nickname: Optional[bool] = None) -> List[Club]: + """ + Returns user's clubs. + + :param user_id: User ID/Nickname to get clubs + :type user_id: Union[int, str] + + :param is_nickname: Specify if passed user_id is nickname + :type is_nickname: Optional[bool] + + :return: List of user's clubs + :rtype: List[Club] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.user_clubs(user_id), + query=Utils.create_query_dict(is_nickname=is_nickname)) + return Utils.validate_response_data(response, + data_model=Club, + fallback=[]) + + @method_endpoint('/api/users/:id/anime_rates') + async def anime_rates(self, + user_id: Union[int, str], + is_nickname: Optional[bool] = None, + page: Optional[int] = None, + limit: Optional[int] = None, + status: Optional[str] = None, + censored: Optional[str] = None) -> List[UserList]: + """ + Returns user's anime list. + + :param user_id: User ID/Nickname to get anime list + :type user_id: Optional[int, str] + + :param is_nickname: Specify if passed user_id is nickname + :type is_nickname: Optional[bool] + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + :type limit: Optional[int] + + :param status: Status of status of anime in list + :type status: Optional[str] + + :param censored: Type of anime censorship + :type censored: Optional[str] + + :return: User's anime list + :rtype: List[UserList] + """ + if not Utils.validate_enum_params({ + AnimeList: status, + AnimeCensorship: censored + }): + return [] + + validated_numbers = Utils.query_numbers_validator( + page=[page, 100000], + limit=[limit, 5000], + ) + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.user_anime_rates(user_id), + query=Utils.create_query_dict(is_nickname=is_nickname, + page=validated_numbers['page'], + limit=validated_numbers['limit'], + status=status, + censored=censored)) + return Utils.validate_response_data(response, + data_model=UserList, + fallback=[]) + + @method_endpoint('/api/users/:id/manga_rates') + async def manga_rates(self, + user_id: Union[int, str], + is_nickname: Optional[bool] = None, + page: Optional[int] = None, + limit: Optional[int] = None, + censored: Optional[str] = None) -> List[UserList]: + """ + Returns user's manga list. + + :param user_id: User ID/Nickname to get manga list + :type user_id: Union[int, str] + + :param is_nickname: Specify if passed user_id is nickname + :type is_nickname: Optional[bool] + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + :type limit: Optional[int] + + :param censored: Type of manga censorship + :type censored: Optional[str] + + :return: User's manga list + :rtype: List[UserList] + """ + if not Utils.validate_enum_params({AnimeCensorship: censored}): + return [] + + validated_numbers = Utils.query_numbers_validator( + page=[page, 100000], + limit=[limit, 5000], + ) + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.user_manga_rates(user_id), + query=Utils.create_query_dict(is_nickname=is_nickname, + page=validated_numbers['page'], + limit=validated_numbers['limit'], + censored=censored)) + return Utils.validate_response_data(response, + data_model=UserList, + fallback=[]) + + @method_endpoint('/api/users/:id/favourites') + async def favourites( + self, + user_id: Union[int, str], + is_nickname: Optional[bool] = None) -> Optional[Favourites]: + """ + Returns user's favourites. + + :param user_id: User ID/Nickname to get favourites + :type user_id: Union[int, str] + + :param is_nickname: Specify if passed user_id is nickname + :type is_nickname: Optional[bool] + + :return: User's favourites + :rtype: Optional[Favourites] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.user_favourites(user_id), + query=Utils.create_query_dict(is_nickname=is_nickname)) + return Utils.validate_response_data(response, data_model=Favourites) + + @method_endpoint('/api/users/:id/messages') + @protected_method('_client', 'messages') + async def messages( + self, + user_id: Union[int, str], + is_nickname: Optional[bool] = None, + page: Optional[int] = None, + limit: Optional[int] = None, + message_type: str = MessageType.NEWS.value) -> List[Message]: + """ + Returns current user's messages by type. + + :param user_id: Current user ID/Nickname to get messages + :type user_id: Union[int, str] + + :param is_nickname: Specify if passed user_id is nickname + :type is_nickname: Optional[bool] + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of page limits + :type limit: Optional[int] + + :param message_type: Type of message + :type message_type: str + + :return: Current user's messages + :rtype: List[Message] + """ + if not Utils.validate_enum_params({MessageType: message_type}): + return [] + + validated_numbers = Utils.query_numbers_validator( + page=[page, 100000], + limit=[limit, 100], + ) + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.user_messages(user_id), + headers=self._client.authorization_header, + query=Utils.create_query_dict(is_nickname=is_nickname, + page=validated_numbers['page'], + limit=validated_numbers['limit'], + type=message_type)) + return Utils.validate_response_data(response, + data_model=Message, + fallback=[]) + + @method_endpoint('/api/users/:id/unread_messages') + @protected_method('_client', 'messages') + async def unread_messages( + self, + user_id: Union[int, str], + is_nickname: Optional[bool] = None) -> Optional[UnreadMessages]: + """ + Returns current user's unread messages counter. + + :param user_id: Current user ID/Nickname to get unread messages + :type user_id: Union[int, str] + + :param is_nickname: Specify if passed user_id is nickname + :type is_nickname: Optional[bool] + + :return: Current user's unread messages counters + :rtype: Optional[UnreadMessages] + """ + response: Dict[str, Any] = await self._client.request( + self._client.endpoints.user_unread_messages(user_id), + headers=self._client.authorization_header, + query=Utils.create_query_dict(is_nickname=is_nickname)) + return Utils.validate_response_data(response, data_model=UnreadMessages) + + @method_endpoint('/api/users/:id/history') + async def history(self, + user_id: Union[int, str], + is_nickname: Optional[bool] = None, + page: Optional[int] = None, + limit: Optional[int] = None, + target_id: Optional[int] = None, + target_type: Optional[str] = None) -> List[History]: + """ + Returns history of user. + + :param user_id: User ID/Nickname to get history + :type user_id: Union[int, str] + + :param is_nickname: Specify if passed user_id is nickname + :type is_nickname: Optional[bool] + + :param page: Number of page + :type page: Optional[int] + + :param limit: Number of results limit + :type limit: Optional[int] + + :param target_id: ID of anime/manga in history + :type target_id: Optional[int] + + :param target_type: Type of target (Anime/Manga) + :type target_type: Optional[str] + + :return: User's history + :rtype: List[History] + """ + if not Utils.validate_enum_params({TargetType: target_type}): + return [] + + validated_numbers = Utils.query_numbers_validator( + page=[page, 100000], + limit=[limit, 100], + ) + + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.user_history(user_id), + query=Utils.create_query_dict(is_nickname=is_nickname, + page=validated_numbers['page'], + limit=validated_numbers['limit'], + target_id=target_id, + target_type=target_type)) + return Utils.validate_response_data(response, + data_model=History, + fallback=[]) + + @method_endpoint('/api/users/:id/bans') + async def bans(self, + user_id: Union[int, str], + is_nickname: Optional[bool] = None) -> List[Ban]: + """ + Returns list of bans of user. + + :param user_id: User ID/Nickname to get list of bans + :type user_id: Union[int, str] + + :param is_nickname: Specify if passed user_id is nickname + :type is_nickname: Optional[bool] + + :return: User's bans + :rtype: List[Ban] + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.user_bans(user_id), + query=Utils.create_query_dict(is_nickname=is_nickname)) + return Utils.validate_response_data(response, + data_model=Ban, + fallback=[]) + + @method_endpoint('/api/v2/users/:user_id/ignore') + @protected_method('_client', 'ignores', fallback=False) + async def ignore(self, user_id: int) -> bool: + """ + Set user as ignored. + + :param user_id: ID of topic to ignore + :type user_id: int + + :return: True if user was ignored, False otherwise + :rtype: bool + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.user_ignore(user_id), + headers=self._client.authorization_header, + request_type=RequestType.POST) + return Utils.validate_response_data(response, fallback=False) is True + + @method_endpoint('/api/v2/users/:user_id/ignore') + @protected_method('_client', 'ignores', fallback=True) + async def unignore(self, user_id: int) -> bool: + """ + Set user as unignored. + + :param user_id: ID of user to unignore + :type user_id: int + + :return: True if user was unignored, False otherwise + :rtype: bool + """ + response: List[Dict[str, Any]] = await self._client.request( + self._client.endpoints.user_ignore(user_id), + headers=self._client.authorization_header, + request_type=RequestType.DELETE) + return Utils.validate_response_data(response, fallback=True) is False diff --git a/shikithon/store/__init__.py b/shikithon/store/__init__.py new file mode 100644 index 00000000..a10336ce --- /dev/null +++ b/shikithon/store/__init__.py @@ -0,0 +1,5 @@ +"""Config store for shikithon library.""" + +from .config_store import ConfigStore + +__all__ = ['ConfigStore'] diff --git a/shikithon/config_cache.py b/shikithon/store/config_store.py similarity index 61% rename from shikithon/config_cache.py rename to shikithon/store/config_store.py index 8b4bc914..3049d611 100644 --- a/shikithon/config_cache.py +++ b/shikithon/store/config_store.py @@ -1,33 +1,34 @@ """ -Config caching module. +Config storing module. -This module handles saving API config to cache file -to restore on next object initializaion +This module handles saving API config to own file +to use on next object initializaion """ -from json import dumps, loads +from json import dumps +from json import loads from os import remove from os.path import exists from typing import Any, Dict, Optional, Tuple from loguru import logger -from shikithon.utils import Utils +from ..utils import Utils -class ConfigCache: +class ConfigStore: """ - Config caching class. + Config storing class. This class has several methods for - saving/restoring config from a cache file. + saving/restoring config from a file. In addition, there is a method - to check if config cache file is exists. + to check if config file is exists. On saving, class takes ".shikithon_" and combining with the current app_name - for generating cache file name. + for generating config file name. This allows to use multiple configs without getting new authorization codes for old configs. @@ -49,47 +50,46 @@ def config_name(app_name: str) -> str: :return: Config filename :rtype: str """ - logger.debug('Generating config cache filename') - return '.shikithon_' + Utils.convert_app_name(app_name) + logger.debug('Generating config filename') + return '.shikithon_' + Utils.convert_app_name_to_filename(app_name) @staticmethod - def cache_config_validation( - config_app_name: str, - config_auth_code: str) -> Tuple[Dict[str, Any], bool]: + def config_validation(app_name: str, + auth_code: str) -> Tuple[Dict[str, Any], bool]: """ - Gets cached config and validates it. + Gets stored config and validates it. - :param config_app_name: Current app name - :type config_app_name: str + :param app_name: Current app name + :type app_name: str - :param config_auth_code: Current authorization code - :type config_auth_code: str + :param auth_code: Current authorization code + :type auth_code: str :return: Tuple with config and validation status. - On successful validation, returns cached config and True, + On successful validation, returns stored config and True, otherwise, empty dictionary and False :rtype: Tuple[Dict[str, Any], bool] """ - cache_config = ConfigCache.get_config(config_app_name) - if cache_config is None: - logger.debug('There are no cached config') + store_config = ConfigStore.get_config(app_name) + if store_config is None: + logger.debug('There are no stored config') return {}, False - logger.debug('Found cached config. Checking...') + logger.debug('Found stored config. Checking...') - if config_auth_code and cache_config['auth_code'] != config_auth_code: - logger.debug('Mismatch of provided and cached auth codes. ' - 'Deleting old cached config') - ConfigCache.delete_config(config_app_name) + if auth_code and store_config['auth_code'] != auth_code: + logger.debug('Mismatch of provided and stored auth codes. ' + 'Deleting old stored config') + ConfigStore.delete_config(app_name) return {}, False - logger.debug('Cached config is valid') - return cache_config, True + logger.debug('Stored config is valid') + return store_config, True @staticmethod def get_config(app_name: str) -> Optional[Dict[str, str]]: """ - Returns current config from cache file. + Returns current config from file. :param app_name: App name for config load :type app_name: str @@ -97,7 +97,7 @@ def get_config(app_name: str) -> Optional[Dict[str, str]]: :return: Config dictionary or None :rtype: Optional[Dict[str, str]] """ - config_name = ConfigCache.config_name(app_name) + config_name = ConfigStore.config_name(app_name) logger.debug(f'Getting "{config_name}" config') if exists(config_name): @@ -105,13 +105,13 @@ def get_config(app_name: str) -> Optional[Dict[str, str]]: config: Dict[str, str] = loads(config_file.read()) return config - logger.warning('Cache file doesn\'t exist. Returning None') + logger.warning('Config file doesn\'t exist. Returning None') return None @staticmethod def save_config(config: Dict[str, str]) -> bool: """ - Creates new cache file and saves current config. + Creates new file and saves current config. :param config: Current config dictionary :type config: Dict[str, str] @@ -119,7 +119,7 @@ def save_config(config: Dict[str, str]) -> bool: :return: True if save succeeded, False otherwise :rtype: bool """ - config_name = ConfigCache.config_name(config['app_name']) + config_name = ConfigStore.config_name(config['app_name']) logger.debug(f'Saving config to "{config_name}"') try: @@ -141,7 +141,7 @@ def delete_config(app_name: str) -> bool: :return: True if delete succeeded, False otherwise :rtype: bool """ - config_name = ConfigCache.config_name(app_name) + config_name = ConfigStore.config_name(app_name) logger.debug(f'Deleting config "{config_name}"') try: diff --git a/shikithon/utils/__init__.py b/shikithon/utils/__init__.py new file mode 100644 index 00000000..aa9e5f14 --- /dev/null +++ b/shikithon/utils/__init__.py @@ -0,0 +1,5 @@ +"""Utils for shikithon library.""" + +from .utils import Utils + +__all__ = ['Utils'] diff --git a/shikithon/utils.py b/shikithon/utils/utils.py similarity index 73% rename from shikithon/utils.py rename to shikithon/utils/utils.py index d7839a9b..cbb4c715 100644 --- a/shikithon/utils.py +++ b/shikithon/utils/utils.py @@ -5,16 +5,16 @@ with all the necessary utility methods to work with the library """ -from io import BytesIO from time import time -from typing import Any, Dict, List, Optional, Tuple, Type, Union +from typing import Any, Dict, List, Optional, Type, Union +from aiohttp import ClientResponse +from aiohttp import ClientSession from loguru import logger -from requests import get from validators import url as is_url -from shikithon.enums.enhanced_enum import EnhancedEnum -from shikithon.enums.response import ResponseCode +from ..enums.enhanced_enum import EnhancedEnum +from ..enums.response import ResponseCode LOWER_LIMIT_NUMBER = 1 @@ -28,7 +28,7 @@ class Utils: """ @staticmethod - def prepare_query_dict(query_dict: Dict[str, str]) -> str: + def convert_to_query_string(query_dict: Dict[str, str]) -> str: """ Convert query dict to query string for endpoint link. @@ -45,10 +45,10 @@ def prepare_query_dict(query_dict: Dict[str, str]) -> str: return f'?{query_dict_str}' @staticmethod - def convert_app_name(app_name: str) -> str: + def convert_app_name_to_filename(app_name: str) -> str: """ Converts app name to snake case for use in - config cache filename. + config store filename. :param app_name: Current OAuth app name :type app_name: str @@ -56,13 +56,13 @@ def convert_app_name(app_name: str) -> str: :return: Converted app name for filename :rtype: str """ - logger.debug(f'Converting {app_name=} for cached config') + logger.debug(f'Converting {app_name=} for stored config') return '_'.join(app_name.lower().split(' ')) @staticmethod def get_new_expire_time(time_expire_constant: int) -> int: """ - Generates new token expire time. + Gets new token expire time. :param time_expire_constant: Token lifetime value :type time_expire_constant: int @@ -75,9 +75,7 @@ def get_new_expire_time(time_expire_constant: int) -> int: return int(time()) + time_expire_constant @staticmethod - def get_image_data( - image_path: str - ) -> Dict[str, Tuple[str, Union[BytesIO, bytes], str]]: + async def get_image_data(image_path: str) -> Dict[str, bytes]: """ Extract image data from image path. If image_path is a link, fetch the image data from the link. @@ -86,31 +84,32 @@ def get_image_data( :type image_path: str :return: Image data - :rtype: Dict[str, Tuple[str, Union[BytesIO, bytes], str]] + :rtype: Dict[str, bytes] """ if isinstance(is_url(image_path), bool): - image_response = get(image_path) - image_data = BytesIO(image_response.content) + async with ClientSession() as session: + async with session.get(image_path) as image_response: + image_data = await image_response.read() else: with open(image_path, 'rb') as image_file: image_data = image_file.read() - return {'image': (image_path, image_data, 'multipart/form-data')} + return {'image': image_data} @staticmethod - def generate_query_dict( + def create_query_dict( **params_data: Optional[Union[str, bool, int, List[Union[int, str]]]] ) -> Dict[str, str]: """ - Returns valid query dict for API requests. + Creates query dict for API requests. - This methods checks for data type and converts to valid one. + This methods checks for data types and converts to valid one. :param params_data: API methods parameters data :type params_data: Optional[Union[str, bool, int, List[Union[int, str]]]] - :return: Valid query dictionary + :return: Query dictionary :rtype: Dict[str, str] """ logger.debug( @@ -124,31 +123,25 @@ def generate_query_dict( elif isinstance(data, int): query_dict[key] = str(data) elif isinstance(data, list): - formatted_data: List[str] = [] - for item in data: - if isinstance(item, int): - formatted_data.append(str(item)) - elif isinstance(item, str) and item.isdigit(): - formatted_data.append(item) - query_dict[key] = ','.join(formatted_data) + query_dict[key] = ','.join(data) else: query_dict[key] = data logger.debug(f'Generated query dictionary: {query_dict=}') return query_dict @staticmethod - def generate_data_dict( + def create_data_dict( **dict_data: Optional[Union[str, bool, int, List[int]]] ) -> Union[Dict[str, str], Dict[str, Dict[str, str]]]: """ - Returns valid data dict for API requests. + Creates data dict for API requests. - This methods checks for data type and converts to valid one. + This methods checks for data types and converts to valid one. :param dict_data: API methods body data :type dict_data: Optional[Union[str, bool, int, List[int]]] - :return: Valid data dictionary + :return: Data dictionary :rtype: Optional[Union[str, bool, int, List[int]]] """ logger.debug( @@ -173,13 +166,7 @@ def generate_data_dict( elif isinstance(data, int): new_data_dict[data_dict_name][key] = str(data) elif isinstance(data, list): - formatted_data: List[str] = [] - for item in data: - if isinstance(item, int): - formatted_data.append(str(item)) - elif isinstance(item, str) and item.isdigit(): - formatted_data.append(item) - new_data_dict[data_dict_name][key] = ','.join(formatted_data) + new_data_dict[data_dict_name][key] = ','.join(data) else: new_data_dict[data_dict_name][key] = data logger.debug(f'Generated data dictionary: {new_data_dict=}') @@ -191,15 +178,15 @@ def validate_enum_params( List[str]]]) -> bool: """ Validates string parameter with enum values. - Function gets dictionary with enum and string values. - If string value is in enum values, function returns True. - If not, throws logger.warning() and returns False + + If string value is in enum values, function returns True, + otherwise False :param enum_params: Dictionary with values to validate. :type enum_params: Dict[Type[EnhancedEnum], Union[str, List[str]]]) - :return: Result of validation + :return: Validator result :rtype: bool """ enums_counter = 0 @@ -226,16 +213,15 @@ def validate_enum_params( f'\nAccepted values: {enum_values}') return False - if enums_counter > 0: - logger.debug(f'All ({enums_counter}) enum parameters are valid') - else: - logger.debug('There are no enum parameters to check') + logger.debug( + f'All ({enums_counter}) enum parameters are valid' + if enums_counter > 0 else 'There are no enum parameters to check') return True @staticmethod - def validate_query_number(query_number: Optional[int], - upper_limit: int) -> Optional[int]: + def get_validated_query_number(query_number: Optional[int], + upper_limit: int) -> Optional[int]: """ Validates query number. @@ -269,6 +255,11 @@ def validate_query_number(query_number: Optional[int], f'Returning {upper_limit=}') return upper_limit + if isinstance(query_number, float): + logger.debug(f'Query number ("{query_number}") is float. ' + f'Converting to int') + query_number = int(query_number) + logger.debug(f'Returning passed query number ("{query_number}")') return query_number @@ -302,17 +293,19 @@ def query_numbers_validator(**query_numbers: List[Optional[int]] validated_numbers: Dict[str, Optional[int]] = {} for name, data in query_numbers.items(): logger.debug(f'Checking "{name}" parameter') - validated_numbers[name] = (Utils.validate_query_number( + validated_numbers[name] = (Utils.get_validated_query_number( data[0], data[1])) return validated_numbers @staticmethod - def validate_return_data( + def validate_response_data( response_data: Union[List[Dict[str, Any]], Dict[str, Any], List[Any], int], data_model: Optional[Type[Any]] = None, - response_code: Optional[ResponseCode] = None - ) -> Optional[Union[Type[Any], List[Type[Any]], List[Any], bool]]: + response_code: Optional[ResponseCode] = None, + fallback: Optional[Any] = None + ) -> Optional[Union[Type[Any], List[Type[Any]], List[Any], Dict[str, Any], + bool]]: """ Validates passed response data and returns parsed models. @@ -328,32 +321,53 @@ def validate_return_data( (Used only when response_data is int) :type response_code: Optional[ResponseCode] + :param fallback: Fallback value to return + :type fallback: Optional[Any] + :return: Parsed response data - :rtype: Optional[Union[Type[Any], List[Type[Any]], bool]] + :rtype: Optional[Union[Type[Any], List[Type[Any]], + List[Any], Dict[str, Any], bool]] """ logger.debug(f'Validating return data: {response_data=}, ' f'{data_model=}, {response_code=}') + if not response_data: - logger.debug('Response data is empty. Returning None') - return None + logger.debug('Response data is empty. Returning it...') + return response_data if isinstance(response_data, int): logger.debug('Response data is int. Returning value ' 'of response code comparison') return response_data == response_code.value + if isinstance(response_data, dict) and response_data.get('errors'): + logger.debug('Response data contains unexpected errors. ' + 'Returning fallback value...') + logger.warning(f'Errors list: {response_data.get("errors")}') + return fallback + + if isinstance(response_data, list) \ + and len(response_data) == 1: + if isinstance(response_data[0], str) \ + and response_data[0].find('Invalid') != -1: + logger.debug('Response data contains info about invalid data. ' + 'Returning fallback value') + logger.warning(response_data[0]) + return fallback + if 'errors' in response_data or 'code' in response_data: - logger.debug('Response data contains errors info. Returning None') - return None + logger.debug( + 'Response data contains errors info. Returning fallback value') + return fallback if 'notice' in response_data or 'success' in response_data: logger.debug('Response data contains success info. Returning True') return True - if 'is_ignored' in response_data: + if 'is_ignored' in response_data and data_model is None: logger.debug('Response data contains is_ignored. ' 'Returning status of is_ignored') - return response_data['is_ignored'] + return response_data.get('is_ignored') if data_model is None: logger.debug("Data model isn't passed. Returning response data") @@ -362,3 +376,11 @@ def validate_return_data( logger.debug('Data model is passed. Returning parsed data') return [data_model(**item) for item in response_data] if isinstance( response_data, list) else data_model(**response_data) + + @staticmethod + async def extract_empty_response_data( + response: ClientResponse) -> Union[str, int]: + response_text = await response.text() + response_status = response.status + return response_status \ + if not response_text else response_text