diff --git a/.gitignore b/.gitignore index c7200b5..a8c5913 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,5 @@ venv __pycache__ OCAB.db src/paths.json -src/core/config.yaml -src/core/log/**/* +src/ocab_core/config.yaml +src/ocab_core/log/**/* diff --git a/README.md b/README.md index 2072957..f020bc3 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,11 @@ OCAB - это бот для Telegram, который призван помочь ## Дополнительные официальные модули -* `gpt` - модуль для генерации ответов на основе нейросети GPT-3.5. Позволяет боту отвечать на сообщения +* `yandexgpt` - модуль для генерации ответов на основе нейросети GPT-3.5. Позволяет боту отвечать на сообщения пользователей, используя нейросеть. Ключевой особенностью является построение линии контекста для нейросети, которая позволяет боту отвечать на вопросы, используя контекст предыдущих сообщений. Для этого используется модуль база данных хранящий историю сообщений. + Список модулей будет пополняться. Идеи для модулей можно оставлять в [issues](https://gitflic.ru/project/armatik/ocab/issue/create). diff --git a/docs/MODULES-SPEC.md b/docs/MODULES-SPEC.md new file mode 100644 index 0000000..12cdb26 --- /dev/null +++ b/docs/MODULES-SPEC.md @@ -0,0 +1,60 @@ +# Спецификация модулей + +> **Внимание!** +> +> Данная спецификация еще не закончена и активно разрабатывается. +> Могут быть значительные изменения (breaking changes). + +Каждый модуль представлен в виде папки, содержащей два обязательных файла: info.json и `__init__.py`. + +## Метаинформация о модуле (info.json) + +Этот файл содержит метаинформацию о модуле в формате JSON. Пример структуры info.json приведён ниже: + +```json +{ + "id": "standard.info", + "name": "Info", + "description": "Модуль с информацией", + "author": "OCAB Team", + "version": "1.0.0", + "privileged": false, + "dependencies": { + "standard.roles": "^1.0.0", + "standard.database": "^1.0.0" + } +} +``` + +- `id`: Уникальный идентификатор модуля. +- `name`: Название модуля. +- `description`: Описание функциональности модуля. +- `author`: Автор модуля. +- `version`: Версия модуля. +- `privileged`: Булево значение, указывающее, является ли модуль привилегированным. +- `dependencies`: Объект, описывающий зависимости модуля от других модулей с указанием версии. + +## Режимы выполнения модулей + +Непривилегированный режим (`privileged: false`): +- Модуль выполняется в доверенной среде на основе RestrictedPython (это накладывает ряд ограничений); +- Может использовать только определенный набор разрешенных пакетов +- Имеет доступ к пакету `ocab_core.modules_system.public_api` для взаимодействия с ботом. + +Привилегированный режим (`privileged: true`): +- Модуль выполняется без ограничений. +- Имеет полный доступ ко всем пакетам. +- Должен использоваться с осторожностью и только для модулей, требующих расширенных прав. + +## Жизненный цикл модуля + +1. Загрузка метаданных из `info.json` +2. Проверка зависимостей +3. Загрузка кода модуля из `__init__.py` +4. Вызов функции `module_init` (если она есть) + +## Взаимодействие между модулями + +Модули могут взаимодействовать друг с другом через [API](../src/ocab_core/modules_system/public_api/__init__.py), предоставляемое системой управления модулями. + +Например, есть функция `get_module`. Она позволяет получить модуль или предоставляемые им объекты по его идентификатору. diff --git a/init.py b/init.py deleted file mode 100644 index e4d52fa..0000000 --- a/init.py +++ /dev/null @@ -1,15 +0,0 @@ -from json import dumps -from pathlib import Path - -pwd = Path().cwd() -dir_core = pwd / "src" / "core" -dir_modules_standard = pwd / "src" / "modules" / "standard" -dir_modules_custom = pwd / "src" / "modules" / "custom" - -json = { - "core": str(dir_core), - "modules standard": str(dir_modules_standard), - "modules custom": str(dir_modules_custom), -} -with open("src/paths.json", "w", encoding="utf8") as f: - f.write(dumps(json, indent=4)) diff --git a/poetry.lock b/poetry.lock index 8b77791..6cbebf6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,13 +13,13 @@ files = [ [[package]] name = "aiogram" -version = "3.5.0" +version = "3.10.0" description = "Modern and fully asynchronous framework for Telegram Bot API" optional = false python-versions = ">=3.8" files = [ - {file = "aiogram-3.5.0-py3-none-any.whl", hash = "sha256:70b5804671b87214768a2a63f19f1457684bd0c6cb6abd23e73bb16207fd7e58"}, - {file = "aiogram-3.5.0.tar.gz", hash = "sha256:1793deb24f36a6fc7b678c31d9a831cef7972765710a47a3e139645a99facba4"}, + {file = "aiogram-3.10.0-py3-none-any.whl", hash = "sha256:dc43bfbe68c736cca48d91ffbc55a397df24b56c332206af850965619689beca"}, + {file = "aiogram-3.10.0.tar.gz", hash = "sha256:f500d4b309e3cc08a87ae5a053b229199034f382925de00aa2ed005d5e25d575"}, ] [package.dependencies] @@ -27,15 +27,16 @@ aiofiles = ">=23.2.1,<23.3.0" aiohttp = ">=3.9.0,<3.10.0" certifi = ">=2023.7.22" magic-filter = ">=1.0.12,<1.1" -pydantic = ">=2.4.1,<2.8" +pydantic = ">=2.4.1,<2.9" typing-extensions = ">=4.7.0,<=5.0" [package.extras] cli = ["aiogram-cli (>=1.0.3,<1.1.0)"] -dev = ["black (>=23.10.0,<23.11.0)", "isort (>=5.12.0,<5.13.0)", "mypy (>=1.6.1,<1.7.0)", "packaging (>=23.1,<24.0)", "pre-commit (>=3.5.0,<3.6.0)", "ruff (>=0.1.1,<0.2.0)", "toml (>=0.10.2,<0.11.0)"] +dev = ["black (>=24.4.2,<24.5.0)", "isort (>=5.13.2,<5.14.0)", "motor-types (>=1.0.0b4,<1.1.0)", "mypy (>=1.10.0,<1.11.0)", "packaging (>=24.1,<25.0)", "pre-commit (>=3.5,<4.0)", "ruff (>=0.4.9,<0.5.0)", "toml (>=0.10.2,<0.11.0)"] docs = ["furo (>=2023.9.10,<2023.10.0)", "markdown-include (>=0.8.1,<0.9.0)", "pygments (>=2.16.1,<2.17.0)", "pymdown-extensions (>=10.3,<11.0)", "sphinx (>=7.2.6,<7.3.0)", "sphinx-autobuild (>=2021.3.14,<2021.4.0)", "sphinx-copybutton (>=0.5.2,<0.6.0)", "sphinx-intl (>=2.1.0,<2.2.0)", "sphinx-substitution-extensions (>=2022.2.16,<2022.3.0)", "sphinxcontrib-towncrier (>=0.3.2a0,<0.4.0)", "towncrier (>=23.6.0,<23.7.0)"] fast = ["aiodns (>=3.0.0)", "uvloop (>=0.17.0)"] i18n = ["babel (>=2.13.0,<2.14.0)"] +mongo = ["motor (>=3.3.2,<3.4.0)"] proxy = ["aiohttp-socks (>=0.8.3,<0.9.0)"] redis = ["redis[hiredis] (>=5.0.1,<5.1.0)"] test = ["aresponses (>=2.1.6,<2.2.0)", "pycryptodomex (>=3.19.0,<3.20.0)", "pytest (>=7.4.2,<7.5.0)", "pytest-aiohttp (>=1.0.5,<1.1.0)", "pytest-asyncio (>=0.21.1,<0.22.0)", "pytest-cov (>=4.1.0,<4.2.0)", "pytest-html (>=4.0.2,<4.1.0)", "pytest-lazy-fixture (>=0.6.3,<0.7.0)", "pytest-mock (>=3.12.0,<3.13.0)", "pytest-mypy (>=0.10.3,<0.11.0)", "pytz (>=2023.3,<2024.0)"] @@ -151,13 +152,13 @@ frozenlist = ">=1.1.0" [[package]] name = "annotated-types" -version = "0.6.0" +version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" files = [ - {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, - {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] [[package]] @@ -249,13 +250,13 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2024.2.2" +version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] @@ -393,6 +394,21 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "dataclasses-json" +version = "0.6.7" +description = "Easily serialize dataclasses to and from JSON." +optional = false +python-versions = "<4.0,>=3.7" +files = [ + {file = "dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a"}, + {file = "dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0"}, +] + +[package.dependencies] +marshmallow = ">=3.18.0,<4.0.0" +typing-inspect = ">=0.4.0,<1" + [[package]] name = "distlib" version = "0.3.8" @@ -524,13 +540,13 @@ files = [ [[package]] name = "identify" -version = "2.5.36" +version = "2.6.0" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, - {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, + {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, + {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, ] [package.extras] @@ -599,6 +615,25 @@ profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +[[package]] +name = "marshmallow" +version = "3.21.3" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +optional = false +python-versions = ">=3.8" +files = [ + {file = "marshmallow-3.21.3-py3-none-any.whl", hash = "sha256:86ce7fb914aa865001a4b2092c4c2872d13bc347f3d42673272cabfdbad386f1"}, + {file = "marshmallow-3.21.3.tar.gz", hash = "sha256:4f57c5e050a54d66361e826f94fba213eb10b67b2fdb02c3e0343ce207ba1662"}, +] + +[package.dependencies] +packaging = ">=17.0" + +[package.extras] +dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"] +docs = ["alabaster (==0.7.16)", "autodocsumm (==0.2.12)", "sphinx (==7.3.7)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"] +tests = ["pytest", "pytz", "simplejson"] + [[package]] name = "mccabe" version = "0.7.0" @@ -777,12 +812,12 @@ files = [ [[package]] name = "peewee" -version = "3.17.3" +version = "3.17.6" description = "a little orm" optional = false python-versions = "*" files = [ - {file = "peewee-3.17.3.tar.gz", hash = "sha256:ef15f90b628e41a584be8306cdc3243c51f73ce88b06154d9572f6d0284a0169"}, + {file = "peewee-3.17.6.tar.gz", hash = "sha256:cea5592c6f4da1592b7cff8eaf655be6648a1f5857469e30037bf920c03fb8fb"}, ] [[package]] @@ -832,109 +867,119 @@ files = [ [[package]] name = "pydantic" -version = "2.7.1" +version = "2.8.2" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, - {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, + {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, + {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.18.2" -typing-extensions = ">=4.6.1" +pydantic-core = "2.20.1" +typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} [package.extras] email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.18.2" +version = "2.20.1" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"}, - {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"}, - {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"}, - {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"}, - {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"}, - {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"}, - {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, - {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, - {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, - {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, - {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, - {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, - {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, - {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, - {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, - {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, - {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, - {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, - {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, - {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, - {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"}, - {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"}, - {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"}, - {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"}, - {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"}, - {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"}, - {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"}, - {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"}, - {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"}, - {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"}, - {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"}, - {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, - {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, + {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, + {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, + {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, + {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, + {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, + {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, + {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, + {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, + {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, + {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, + {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, + {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, + {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, ] [package.dependencies] @@ -1027,13 +1072,13 @@ files = [ [[package]] name = "requests" -version = "2.31.0" +version = "2.32.3" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -1046,6 +1091,21 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "restrictedpython" +version = "7.1" +description = "RestrictedPython is a defined subset of the Python language which allows to provide a program input into a trusted environment." +optional = false +python-versions = ">=3.7, <3.13" +files = [ + {file = "RestrictedPython-7.1-py3-none-any.whl", hash = "sha256:56d0c73e5de1757702053383601b0fcd3fb2e428039ee1df860409ad67b17d2b"}, + {file = "RestrictedPython-7.1.tar.gz", hash = "sha256:875aeb51c139d78e34cef8605dc65309b449168060dd08551a1fe9edb47cb9a5"}, +] + +[package.extras] +docs = ["Sphinx", "sphinx-rtd-theme"] +test = ["pytest", "pytest-mock"] + [[package]] name = "rich" version = "13.7.1" @@ -1064,6 +1124,17 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "semver" +version = "3.0.2" +description = "Python helper for Semantic Versioning (https://semver.org)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "semver-3.0.2-py3-none-any.whl", hash = "sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4"}, + {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, +] + [[package]] name = "stevedore" version = "5.2.0" @@ -1080,24 +1151,39 @@ pbr = ">=2.0.0,<2.1.0 || >2.1.0" [[package]] name = "typing-extensions" -version = "4.11.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "typing-inspect" +version = "0.9.0" +description = "Runtime inspection utilities for typing module." +optional = false +python-versions = "*" +files = [ + {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, + {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, +] + +[package.dependencies] +mypy-extensions = ">=0.3.0" +typing-extensions = ">=3.7.4" + [[package]] name = "urllib3" -version = "2.2.1" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] @@ -1231,5 +1317,5 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" -python-versions = "^3.11.6" -content-hash = "5b43fa850045f857f8e7b84baa8a9d9b6c9e64758bf8525f4eb772574aafa5ca" +python-versions = ">=3.11.6,<3.13" +content-hash = "5df07f0efc29d67f3ef2952b7d26a58098c16d5a391f469258f91ba47ebb972f" diff --git a/pyproject.toml b/pyproject.toml index c9d3e5f..5f3a1ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,24 +1,36 @@ [tool.poetry] +package-mode = false name = "ocab" -version = "0.1.0" -description = "" +version = "2.0.0" +description = "OCAB is a modular Telegram bot" license = "GPL-3.0-only" authors = ["Семён Фомченков "] maintainers = [ "Илья Женецкий ", "qualimock ", - "Кирилл Уницаев fiersik.kouji@yandex.ru", + "Кирилл Уницаев ", + "Максим Слипенко " ] readme = "README.md" repository = "https://gitflic.ru/project/armatik/ocab" +packages = [{include = "scripts"}] + +[tool.poetry.urls] +"Bug Tracker" = "https://gitflic.ru/project/armatik/ocab/issue?status=OPEN" + +[tool.poetry.scripts] +test = 'scripts.test:main' +init = 'scripts.init:main' [tool.poetry.dependencies] -python = "^3.11.6" -aiogram = "^3.2.0" -peewee = "^3.17.0" +python = ">=3.11.6,<3.13" +aiogram = "^3.10.0" +peewee = "^3.17.6" pyyaml = "^6.0.1" -requests = "^2.31.0" - +requests = "^2.32.3" +restrictedpython = "^7.1" +dataclasses-json = "^0.6.7" +semver = "^3.0.2" [tool.poetry.group.dev.dependencies] flake8 = "^7.1.0" @@ -26,6 +38,7 @@ black = "^24.4.2" isort = "^5.13.2" bandit = "^1.7.9" pre-commit = "^3.7.1" +semver = "^3.0.2" [tool.black] line-length = 88 diff --git a/run_tests b/run_tests deleted file mode 100644 index 6009a83..0000000 --- a/run_tests +++ /dev/null @@ -1,4 +0,0 @@ -#! /bin/sh -cd src -python -m unittest discover -v -cd .. diff --git a/src/core/__init__.py b/scripts/__init__.py similarity index 100% rename from src/core/__init__.py rename to scripts/__init__.py diff --git a/scripts/init.py b/scripts/init.py new file mode 100644 index 0000000..45d14b7 --- /dev/null +++ b/scripts/init.py @@ -0,0 +1,21 @@ +from json import dumps +from pathlib import Path + + +def main(): + pwd = Path().cwd() + dir_core = pwd / "src" / "ocab_core" + dir_modules_standard = pwd / "src" / "ocab_modules" / "standard" + dir_modules_custom = pwd / "src" / "ocab_modules" / "custom" + + json = { + "core": str(dir_core), + "modules standard": str(dir_modules_standard), + "modules custom": str(dir_modules_custom), + } + with open("src/paths.json", "w", encoding="utf8") as f: + f.write(dumps(json, indent=4)) + + +if __name__ == "__main__": + main() diff --git a/scripts/test.py b/scripts/test.py new file mode 100644 index 0000000..d03200a --- /dev/null +++ b/scripts/test.py @@ -0,0 +1,9 @@ +import subprocess # nosec + + +def main(): + subprocess.run(["python", "-u", "-m", "unittest", "discover"]) # nosec + + +if __name__ == "__main__": + main() diff --git a/src/__init__.py b/src/__init__.py index 7da3d9b..0161b65 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,2 +1,2 @@ -import src.core -import src.service +import ocab_core +import service diff --git a/src/core/main.py b/src/core/main.py deleted file mode 100644 index da38dd2..0000000 --- a/src/core/main.py +++ /dev/null @@ -1,35 +0,0 @@ -import asyncio - -from aiogram import Bot, Dispatcher -from routers import include_routers - -from src.core.logger import log, setup_logger -from src.modules.standard.config.config import get_telegram_token -from src.modules.standard.database.db_api import connect_database, create_tables - - -async def main(): - bot = None - database = None - setup_logger() - - try: - bot = Bot(token=get_telegram_token()) - database, path = connect_database() - database.connect() - create_tables(database) - - dp = Dispatcher() - await include_routers(dp) - await dp.start_polling(bot) - except Exception as e: - log(e) - finally: - if bot is not None: - await bot.session.close() - if database is not None: - database.close() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/src/modules/standard/__init__.py b/src/modules/standard/__init__.py deleted file mode 100644 index 2cf9668..0000000 --- a/src/modules/standard/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import config, database, exceptions, roles diff --git a/src/modules/standard/config/__init__.py b/src/modules/standard/config/__init__.py deleted file mode 100644 index d63bc18..0000000 --- a/src/modules/standard/config/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import config diff --git a/src/modules/standard/database/__init__.py b/src/modules/standard/database/__init__.py deleted file mode 100644 index fa2df97..0000000 --- a/src/modules/standard/database/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import db_api, models diff --git a/src/modules/standard/exceptions/__init__.py b/src/modules/standard/exceptions/__init__.py deleted file mode 100644 index 6ceaccd..0000000 --- a/src/modules/standard/exceptions/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import module_exceptions diff --git a/src/modules/standard/exceptions/info.json b/src/modules/standard/exceptions/info.json deleted file mode 100644 index 79618c6..0000000 --- a/src/modules/standard/exceptions/info.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Exceptions", - "description": "Модуль с исключениями", - "author": "OCAB Team", - "version": "1.0" -} diff --git a/src/modules/standard/info/routers.py b/src/modules/standard/info/routers.py deleted file mode 100644 index 9594b91..0000000 --- a/src/modules/standard/info/routers.py +++ /dev/null @@ -1,10 +0,0 @@ -# flake8: noqa - -from aiogram import F, Router - -from src.modules.standard.info.handlers import get_chat_info, get_user_info - -router = Router() - -router.message.register(get_user_info, F.text.startswith("/info") == True) -router.message.register(get_chat_info, F.text.startswith("/chatinfo") == True) diff --git a/src/modules/standard/roles/info.json b/src/modules/standard/roles/info.json deleted file mode 100644 index 73dc7e6..0000000 --- a/src/modules/standard/roles/info.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Roles", - "description": "Модуль для работы с ролями", - "author": "OCAB Team", - "version": "1.0" -} diff --git a/src/modules/standard/welcome/__init__.py b/src/modules/standard/welcome/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/__init__.py b/src/ocab_core/__init__.py similarity index 100% rename from src/modules/__init__.py rename to src/ocab_core/__init__.py diff --git a/src/core/database/.gitkeep b/src/ocab_core/database/.gitkeep similarity index 100% rename from src/core/database/.gitkeep rename to src/ocab_core/database/.gitkeep diff --git a/src/core/example_config.yaml b/src/ocab_core/example_config.yaml similarity index 100% rename from src/core/example_config.yaml rename to src/ocab_core/example_config.yaml diff --git a/src/core/logger.py b/src/ocab_core/logger.py similarity index 75% rename from src/core/logger.py rename to src/ocab_core/logger.py index 4180a12..d12998c 100644 --- a/src/core/logger.py +++ b/src/ocab_core/logger.py @@ -1,6 +1,7 @@ import logging import os import time +import traceback def setup_logger(): @@ -26,4 +27,12 @@ async def log(message): Она асинхронная, хотя таковой на самом деле не является. """ + if isinstance(message, Exception): + error_message = f"Error: {str(message)}\n{traceback.format_exc()}" + logging.error(error_message) + else: + logging.info(message) + + +def log_new(message): logging.info(message) diff --git a/src/ocab_core/main.py b/src/ocab_core/main.py new file mode 100644 index 0000000..a573edc --- /dev/null +++ b/src/ocab_core/main.py @@ -0,0 +1,49 @@ +import asyncio +import traceback + +from aiogram import Bot, Dispatcher + +from ocab_core.logger import setup_logger +from ocab_core.modules_system import ModulesManager +from ocab_core.modules_system.loaders import FSLoader +from ocab_core.modules_system.loaders.unsafe_fs_loader import UnsafeFSLoader +from ocab_core.singleton import Singleton +from ocab_modules.standard.config.config import get_telegram_token +from service import paths + +bot_modules = [ + UnsafeFSLoader(f"{paths.modules_standard}/config"), + UnsafeFSLoader(f"{paths.modules_standard}/database"), + UnsafeFSLoader(f"{paths.modules_standard}/roles"), + FSLoader(f"{paths.modules_standard}/info"), +] + + +async def main(): + bot = None + database = None + setup_logger() + + app = Singleton() + + try: + bot = Bot(token=get_telegram_token()) + + app.dp = Dispatcher() + app.modules_manager = ModulesManager() + + for module_loader in bot_modules: + app.modules_manager.load(module_loader) + + await app.dp.start_polling(bot) + except Exception: + traceback.print_exc() + finally: + if bot is not None: + await bot.session.close() + if database is not None: + database.close() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/ocab_core/modules_system/__init__.py b/src/ocab_core/modules_system/__init__.py new file mode 100644 index 0000000..a30b035 --- /dev/null +++ b/src/ocab_core/modules_system/__init__.py @@ -0,0 +1 @@ +from .modules_manager import ModulesManager diff --git a/src/ocab_core/modules_system/loaders/__init__.py b/src/ocab_core/modules_system/loaders/__init__.py new file mode 100644 index 0000000..e41ccbb --- /dev/null +++ b/src/ocab_core/modules_system/loaders/__init__.py @@ -0,0 +1 @@ +from .fs_loader import FSLoader diff --git a/src/ocab_core/modules_system/loaders/base.py b/src/ocab_core/modules_system/loaders/base.py new file mode 100644 index 0000000..cc7d579 --- /dev/null +++ b/src/ocab_core/modules_system/loaders/base.py @@ -0,0 +1,24 @@ +import types +from dataclasses import dataclass + +from dataclasses_json import dataclass_json + + +@dataclass_json +@dataclass +class ModuleInfo: + id: str + name: str + description: str + version: str + author: str + privileged: bool + dependencies: dict + + +class AbstractLoader: + def info(self) -> ModuleInfo: + raise NotImplementedError + + def load(self) -> types.ModuleType: + raise NotImplementedError diff --git a/src/ocab_core/modules_system/loaders/fs_loader/FSLoader.py b/src/ocab_core/modules_system/loaders/fs_loader/FSLoader.py new file mode 100644 index 0000000..c83bc3e --- /dev/null +++ b/src/ocab_core/modules_system/loaders/fs_loader/FSLoader.py @@ -0,0 +1,73 @@ +import types +from pathlib import Path + +from RestrictedPython import compile_restricted_exec + +from ocab_core.modules_system.loaders.fs_loader.policy import ( + ALLOWED_IMPORTS, + BUILTINS, + RestrictedPythonPolicy, +) +from ocab_core.modules_system.loaders.unsafe_fs_loader import UnsafeFSLoader + + +class FSLoader(UnsafeFSLoader): + def __init__(self, path): + super().__init__(path) + self.builtins = BUILTINS.copy() + self.builtins["__import__"] = self._hook_import + + def load(self): + info = self.info() + if info.privileged: + raise Exception("Only non privileged modules are allowed to be imported") + return self._hook_import(".") + + def _resolve_module_from_path(self, module_name: str): + path = Path(self.path) + + if module_name != ".": + path = path.joinpath(module_name.replace(".", "/")) + + if path.is_dir(): + init_file_path = path / "__init__.py" + if not init_file_path.exists(): + raise FileNotFoundError(f"File {init_file_path} does not exist.") + file_path = init_file_path + else: + path = path.with_suffix(".py") + if path.is_file(): + file_path = path + else: + raise ValueError(f"Module not found: {module_name}") + + return file_path + + def _hook_import(self, name: str, *args, **kwargs): + for allowed in ALLOWED_IMPORTS: + if name == allowed or name.startswith(f"{allowed}."): + return __import__(name, *args, **kwargs) + + if name == "ocab_core.modules_system.public_api": + return __import__(name, *args, **kwargs) + + module_file_path = self._resolve_module_from_path(name) + + with open(module_file_path, "r") as f: + src = f.read() + + module = types.ModuleType(name) + module.__dict__.update( + { + "__builtins__": self.builtins, + } + ) + result = compile_restricted_exec(src, "", policy=RestrictedPythonPolicy) + + if result.errors: + for error in result.errors: + print(error) + + exec(result.code, module.__dict__) # nosec + + return module diff --git a/src/ocab_core/modules_system/loaders/fs_loader/__init__.py b/src/ocab_core/modules_system/loaders/fs_loader/__init__.py new file mode 100644 index 0000000..c05452a --- /dev/null +++ b/src/ocab_core/modules_system/loaders/fs_loader/__init__.py @@ -0,0 +1 @@ +from .FSLoader import FSLoader diff --git a/src/ocab_core/modules_system/loaders/fs_loader/policy.py b/src/ocab_core/modules_system/loaders/fs_loader/policy.py new file mode 100644 index 0000000..edf3f91 --- /dev/null +++ b/src/ocab_core/modules_system/loaders/fs_loader/policy.py @@ -0,0 +1,98 @@ +from _ast import AnnAssign +from typing import Any + +from RestrictedPython import ( + RestrictingNodeTransformer, + limited_builtins, + safe_builtins, + utility_builtins, +) +from RestrictedPython.Eval import default_guarded_getitem + + +class RestrictedPythonPolicy(RestrictingNodeTransformer): + def visit_AsyncFunctionDef(self, node): + return self.node_contents_visit(node) + + def visit_Await(self, node): + return self.node_contents_visit(node) + + def visit_AsyncFor(self, node): + return self.node_contents_visit(node) + + def visit_AsyncWith(self, node): + return self.node_contents_visit(node) + + """ + Не работает из-за getattr + + def visit_Match(self, node) -> Any: + return self.node_contents_visit(node) + + def visit_match_case(self, node) -> Any: + return self.node_contents_visit(node) + + def visit_MatchAs(self, node) -> Any: + return self.node_contents_visit(node) + + def visit_MatchValue(self, node) -> Any: + return self.node_contents_visit(node) + """ + + def visit_AnnAssign(self, node: AnnAssign) -> Any: + # missing in RestrictingNodeTransformer + # this doesn't need the logic that is in visit_Assign + # because it doesn't have a "targets" attribute, + # and node.target: Name | Attribute | Subscript + return self.node_contents_visit(node) + + # new Python 3.12 nodes + def visit_TypeAlias(self, node) -> Any: + # missing in RestrictingNodeTransformer + return self.node_contents_visit(node) + + def visit_TypeVar(self, node) -> Any: + # missing in RestrictingNodeTransformer + return self.node_contents_visit(node) + + def visit_TypeVarTuple(self, node) -> Any: + # missing in RestrictingNodeTransformer + return self.node_contents_visit(node) + + def visit_ParamSpec(self, node) -> Any: + # missing in RestrictingNodeTransformer + return self.node_contents_visit(node) + + +def _metaclass(name, bases, dict): + ob = type(name, bases, dict) + ob.__allow_access_to_unprotected_subobjects__ = 1 + ob._guarded_writes = 1 + return ob + + +ALLOWED_IMPORTS = [ + "typing", + "aiogram", + "warnings", +] + +BUILTINS = safe_builtins.copy() +BUILTINS.update(utility_builtins) +BUILTINS.update(limited_builtins) +BUILTINS["__metaclass__"] = _metaclass +BUILTINS["_getitem_"] = default_guarded_getitem +# BUILTINS["_write_"] = full_write_guard +BUILTINS["staticmethod"] = staticmethod + + +class GuardedDictType: + def __call__(self, *args, **kwargs): + return dict(*args, **kwargs) + + @staticmethod + def fromkeys(iterable, value=None): + return dict.fromkeys(iterable, value) + + +BUILTINS["dict"] = GuardedDictType() diff --git a/src/ocab_core/modules_system/loaders/unsafe_fs_loader/UnsafeFSLoader.py b/src/ocab_core/modules_system/loaders/unsafe_fs_loader/UnsafeFSLoader.py new file mode 100644 index 0000000..42b4487 --- /dev/null +++ b/src/ocab_core/modules_system/loaders/unsafe_fs_loader/UnsafeFSLoader.py @@ -0,0 +1,64 @@ +import importlib.util +import os +import sys +from pathlib import Path + +from ocab_core.modules_system.loaders.base import AbstractLoader, ModuleInfo + + +class UnsafeFSLoader(AbstractLoader): + def __init__(self, path): + self.path = path + + def info(self): + with open(os.path.join(self.path, "info.json"), "r") as f: + return ModuleInfo.from_json(f.read()) + + def _resolve_module_from_path(self, module_name: str): + path = Path(self.path) + + if module_name != ".": + path = path.joinpath(module_name.replace(".", "/")) + + if path.is_dir(): + init_file_path = path / "__init__.py" + if not init_file_path.exists(): + raise FileNotFoundError(f"File {init_file_path} does not exist.") + file_path = init_file_path + else: + path = path.with_suffix(".py") + if path.is_file(): + file_path = path + else: + raise ValueError(f"Module not found: {module_name}") + + return file_path + + def load(self): + self.info() + + full_path = self._resolve_module_from_path(".") + + if full_path.name == "__init__.py": + module_name = full_path.parent.name + path = full_path.parent.parent.absolute() + else: + module_name = full_path.stem + path = full_path.parent.absolute() + + # Добавляем директорию модуля в sys.path + sys.path.insert(0, str(path)) + + print(full_path.parent.absolute()) + print(module_name) + + # Загружаем спецификацию модуля + spec = importlib.util.spec_from_file_location(module_name, full_path) + + # Создаем модуль + module = importlib.util.module_from_spec(spec) + + # Выполняем модуль + spec.loader.exec_module(module) + + return module diff --git a/src/ocab_core/modules_system/loaders/unsafe_fs_loader/__init__.py b/src/ocab_core/modules_system/loaders/unsafe_fs_loader/__init__.py new file mode 100644 index 0000000..870053c --- /dev/null +++ b/src/ocab_core/modules_system/loaders/unsafe_fs_loader/__init__.py @@ -0,0 +1 @@ +from .UnsafeFSLoader import UnsafeFSLoader diff --git a/src/ocab_core/modules_system/modules_manager.py b/src/ocab_core/modules_system/modules_manager.py new file mode 100644 index 0000000..8d62f97 --- /dev/null +++ b/src/ocab_core/modules_system/modules_manager.py @@ -0,0 +1,66 @@ +import semver + +from ocab_core.modules_system.loaders.base import AbstractLoader + + +def is_version_compatible(version, requirement): + def parse_requirement(req): + if req.startswith("^"): + base_version = req[1:] + base_version_info = semver.VersionInfo.parse(base_version) + range_start = base_version_info + range_end = base_version_info.bump_major() + return [f">={range_start}", f"<{range_end}"] + else: + return [req] + + for r in parse_requirement(requirement): + if not semver.Version.parse(version).match(r): + return False + + return True + + +class ModulesManager: + def __init__(self): + self.modules = {} + + def load(self, loader: AbstractLoader): + info = loader.info() + + if info.id in self.modules: + return + + for dependency, version in info.dependencies.items(): + if dependency not in self.modules: + raise Exception( + f"Module {info.id} depends on {dependency}, but it is not loaded" + ) + loaded_dependency_info = self.modules[dependency]["info"] + if not is_version_compatible(loaded_dependency_info.version, version): + raise Exception( + f"Module {info.id} depends on {dependency}, " + f"but version {version} is not compatible" + ) + + module = loader.load() + + self.modules[info.id] = { + "info": info, + "module": module, + } + + if hasattr(module, "module_init"): + module.module_init() + + def get_by_id(self, module_id: str): + if module_id not in self.modules: + raise Exception(f"Module with id {module_id} not loaded") + + return self.modules[module_id]["module"] + + def get_info_by_id(self, module_id: str): + if module_id not in self.modules: + raise Exception(f"Module with id {module_id} not loaded") + + return self.modules[module_id]["info"] diff --git a/src/ocab_core/modules_system/public_api/__init__.py b/src/ocab_core/modules_system/public_api/__init__.py new file mode 100644 index 0000000..e43ac4a --- /dev/null +++ b/src/ocab_core/modules_system/public_api/__init__.py @@ -0,0 +1,3 @@ +from ocab_core.logger import log # noqa + +from .public_api import Storage, get_module, register_router diff --git a/src/ocab_core/modules_system/public_api/public_api.py b/src/ocab_core/modules_system/public_api/public_api.py new file mode 100644 index 0000000..8f7e235 --- /dev/null +++ b/src/ocab_core/modules_system/public_api/public_api.py @@ -0,0 +1,54 @@ +import types +from typing import Any, Tuple, Union + +from aiogram import Router + +from ocab_core.singleton import Singleton + + +def register_router(router: Router): + app = Singleton() + app.dp.include_router(router) + + +def get_module( + module_id: str, paths=None +) -> Union[types.ModuleType, Union[Any, None], Tuple[Union[Any, None], ...]]: + app = Singleton() + module = app.modules_manager.get_by_id(module_id) + + if paths is None: + return module + + if isinstance(paths, str): + paths = [paths] + + results = [] + + for path in paths: + current_obj = module + try: + parts = path.split(".") + for part in parts: + current_obj = getattr(current_obj, part) + results.append(current_obj) + except AttributeError: + results.append(None) + + if len(results) == 1: + return results[0] + else: + return tuple(results) + + +class Storage: + + @staticmethod + def set(key: str, value: Any): + storage = Singleton().storage + storage[key] = value + + @staticmethod + def get(key: str): + storage = Singleton().storage + return storage.get(key) diff --git a/src/core/routers.py b/src/ocab_core/routers.py similarity index 53% rename from src/core/routers.py rename to src/ocab_core/routers.py index f0c235b..198a9e0 100644 --- a/src/core/routers.py +++ b/src/ocab_core/routers.py @@ -1,8 +1,9 @@ from aiogram import Dispatcher -from src.modules.standard.admin.routers import router as admin_router -from src.modules.standard.info.routers import router as info_router -from src.modules.standard.message_processing.message_api import ( +from src.ocab_modules.standard.admin.routers import router as admin_router + +# from src.modules.standard.info.routers import router as info_router +from src.ocab_modules.standard.message_processing.message_api import ( router as process_message, ) @@ -12,6 +13,6 @@ async def include_routers(dp: Dispatcher): Подключение роутеров в бота dp.include_router() """ - dp.include_router(info_router) + # dp.include_router(info_router) dp.include_router(admin_router) dp.include_router(process_message) diff --git a/src/ocab_core/singleton.py b/src/ocab_core/singleton.py new file mode 100644 index 0000000..51d2003 --- /dev/null +++ b/src/ocab_core/singleton.py @@ -0,0 +1,19 @@ +from aiogram import Dispatcher + +from ocab_core.modules_system import ModulesManager + + +class SingletonMeta(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + instance = super().__call__(*args, **kwargs) + cls._instances[cls] = instance + return cls._instances[cls] + + +class Singleton(metaclass=SingletonMeta): + dp: Dispatcher = None + modules_manager: ModulesManager = None + storage = dict() diff --git a/src/modules/external/yandexgpt/__init__.py b/src/ocab_modules/__init__.py similarity index 100% rename from src/modules/external/yandexgpt/__init__.py rename to src/ocab_modules/__init__.py diff --git a/src/modules/external/__init__.py b/src/ocab_modules/external/__init__.py similarity index 100% rename from src/modules/external/__init__.py rename to src/ocab_modules/external/__init__.py diff --git a/src/modules/standard/database/models/__init__.py b/src/ocab_modules/external/yandexgpt/__init__.py similarity index 100% rename from src/modules/standard/database/models/__init__.py rename to src/ocab_modules/external/yandexgpt/__init__.py diff --git a/src/modules/external/yandexgpt/handlers.py b/src/ocab_modules/external/yandexgpt/handlers.py similarity index 79% rename from src/modules/external/yandexgpt/handlers.py rename to src/ocab_modules/external/yandexgpt/handlers.py index f8c8e4a..47b4255 100644 --- a/src/modules/external/yandexgpt/handlers.py +++ b/src/ocab_modules/external/yandexgpt/handlers.py @@ -4,14 +4,13 @@ import asyncio from aiogram import Bot from aiogram.types import Message -from src.core.logger import log -from src.modules.external.yandexgpt.yandexgpt import * -from src.modules.standard.config.config import ( +from ocab_modules.external.yandexgpt.yandexgpt import * +from ocab_modules.standard.config.config import ( get_yandexgpt_catalog_id, get_yandexgpt_prompt, get_yandexgpt_token, ) -from src.modules.standard.database.db_api import add_message +from ocab_modules.standard.database.db_api import add_message async def answer_to_message(message: Message, bot: Bot): diff --git a/src/modules/external/yandexgpt/info.json b/src/ocab_modules/external/yandexgpt/info.json similarity index 100% rename from src/modules/external/yandexgpt/info.json rename to src/ocab_modules/external/yandexgpt/info.json diff --git a/src/modules/external/yandexgpt/routers.py b/src/ocab_modules/external/yandexgpt/routers.py similarity index 84% rename from src/modules/external/yandexgpt/routers.py rename to src/ocab_modules/external/yandexgpt/routers.py index 6e2e0be..66c4230 100644 --- a/src/modules/external/yandexgpt/routers.py +++ b/src/ocab_modules/external/yandexgpt/routers.py @@ -1,7 +1,7 @@ # flake8: noqa from aiogram import F, Router -from src.modules.external.yandexgpt.handlers import answer_to_message +from src.ocab_modules.external.yandexgpt.handlers import answer_to_message router = Router() # Если сообщение содержит в начале текст "Гномик" или "гномик" или отвечает на сообщение бота, то вызывается функция answer_to_message diff --git a/src/modules/external/yandexgpt/yandexgpt.py b/src/ocab_modules/external/yandexgpt/yandexgpt.py similarity index 99% rename from src/modules/external/yandexgpt/yandexgpt.py rename to src/ocab_modules/external/yandexgpt/yandexgpt.py index 1d726d6..28bc195 100644 --- a/src/modules/external/yandexgpt/yandexgpt.py +++ b/src/ocab_modules/external/yandexgpt/yandexgpt.py @@ -5,7 +5,7 @@ import json import aiohttp import requests -from src.core.logger import log +from ocab_core.logger import log from ...standard.config.config import * from ...standard.database import * diff --git a/src/modules/standard/admin/__init__.py b/src/ocab_modules/standard/admin/__init__.py similarity index 100% rename from src/modules/standard/admin/__init__.py rename to src/ocab_modules/standard/admin/__init__.py diff --git a/src/modules/standard/admin/handlers.py b/src/ocab_modules/standard/admin/handlers.py similarity index 96% rename from src/modules/standard/admin/handlers.py rename to src/ocab_modules/standard/admin/handlers.py index bed369e..bcf3eeb 100644 --- a/src/modules/standard/admin/handlers.py +++ b/src/ocab_modules/standard/admin/handlers.py @@ -4,7 +4,7 @@ import time from aiogram import Bot from aiogram.types import Message -from src.modules.standard.config.config import get_default_chat_tag +from src.ocab_modules.standard.config.config import get_default_chat_tag async def delete_message(message: Message, bot: Bot): diff --git a/src/modules/standard/admin/info.json b/src/ocab_modules/standard/admin/info.json similarity index 100% rename from src/modules/standard/admin/info.json rename to src/ocab_modules/standard/admin/info.json diff --git a/src/modules/standard/admin/routers.py b/src/ocab_modules/standard/admin/routers.py similarity index 87% rename from src/modules/standard/admin/routers.py rename to src/ocab_modules/standard/admin/routers.py index 3d9fcd8..40115fb 100644 --- a/src/modules/standard/admin/routers.py +++ b/src/ocab_modules/standard/admin/routers.py @@ -1,13 +1,13 @@ # flake8: noqa from aiogram import F, Router -from src.modules.standard.admin.handlers import ( +from src.ocab_modules.standard.admin.handlers import ( chat_not_in_approve_list, delete_message, error_access, get_chat_id, ) -from src.modules.standard.filters.filters import ( +from src.ocab_modules.standard.filters.filters import ( ChatModerOrAdminFilter, ChatNotInApproveFilter, ) diff --git a/src/ocab_modules/standard/config/__init__.py b/src/ocab_modules/standard/config/__init__.py new file mode 100644 index 0000000..8dcce20 --- /dev/null +++ b/src/ocab_modules/standard/config/__init__.py @@ -0,0 +1 @@ +from .config import config diff --git a/src/modules/standard/config/config.py b/src/ocab_modules/standard/config/config.py similarity index 98% rename from src/modules/standard/config/config.py rename to src/ocab_modules/standard/config/config.py index b47b307..815e349 100644 --- a/src/modules/standard/config/config.py +++ b/src/ocab_modules/standard/config/config.py @@ -2,7 +2,7 @@ import yaml -from ....service import paths +from src.service import paths def get_config(is_test: bool = False) -> dict: diff --git a/src/modules/standard/config/info.json b/src/ocab_modules/standard/config/info.json similarity index 65% rename from src/modules/standard/config/info.json rename to src/ocab_modules/standard/config/info.json index c615aa5..81bdd92 100644 --- a/src/modules/standard/config/info.json +++ b/src/ocab_modules/standard/config/info.json @@ -1,6 +1,9 @@ { + "id": "standard.config", "name": "Config YAML", "description": "Модуль для работы с конфигурационным файлом бота (YAML)", "author": "OCAB Team", - "version": "1.0" + "version": "1.0.0", + "privileged": true, + "dependencies": {} } diff --git a/src/modules/standard/config/tests/config.yaml b/src/ocab_modules/standard/config/tests/config.yaml similarity index 100% rename from src/modules/standard/config/tests/config.yaml rename to src/ocab_modules/standard/config/tests/config.yaml diff --git a/src/modules/standard/config/tests/test_config.py b/src/ocab_modules/standard/config/tests/test_config.py similarity index 96% rename from src/modules/standard/config/tests/test_config.py rename to src/ocab_modules/standard/config/tests/test_config.py index 011d5c1..cf17556 100644 --- a/src/modules/standard/config/tests/test_config.py +++ b/src/ocab_modules/standard/config/tests/test_config.py @@ -1,6 +1,6 @@ import unittest -from src.modules.standard.config.config import get_config +from src.ocab_modules.standard.config.config import get_config yaml_load = get_config(is_test=True) diff --git a/src/modules/standard/database/README.md b/src/ocab_modules/standard/database/README.md similarity index 100% rename from src/modules/standard/database/README.md rename to src/ocab_modules/standard/database/README.md diff --git a/src/ocab_modules/standard/database/__init__.py b/src/ocab_modules/standard/database/__init__.py new file mode 100644 index 0000000..7daa72d --- /dev/null +++ b/src/ocab_modules/standard/database/__init__.py @@ -0,0 +1,5 @@ +from . import db_api, models + + +def module_init(): + db_api.connect_database() diff --git a/src/modules/standard/database/db_api.py b/src/ocab_modules/standard/database/db_api.py similarity index 94% rename from src/modules/standard/database/db_api.py rename to src/ocab_modules/standard/database/db_api.py index e96d4ff..fc76455 100644 --- a/src/modules/standard/database/db_api.py +++ b/src/ocab_modules/standard/database/db_api.py @@ -1,10 +1,12 @@ import peewee as pw from aiogram.types import Message -from ....service import paths -from ..exceptions.module_exceptions import MissingModuleName, NotExpectedModuleName +from src.service import paths + +from .exceptions import MissingModuleName, NotExpectedModuleName from .models.chat_stats import ChatStats from .models.chats import Chats +from .models.db import database_proxy from .models.messages import Messages from .models.user_stats import UserStats from .models.users import Users @@ -20,14 +22,12 @@ def connect_database(is_test: bool = False, module: str | None = None): raise NotExpectedModuleName() db_path = f"{paths.core}/database" - _database = pw.SqliteDatabase(f"{db_path}/OCAB.db") - Chats._meta.database = _database - Messages._meta.database = _database - Users._meta.database = _database - UserStats._meta.database = _database - ChatStats._meta.database = _database + database = pw.SqliteDatabase(f"{db_path}/OCAB.db") + database_proxy.initialize(database) + database.connect() + create_tables(database) - return _database, f"{db_path}/OCAB.db" + return database, f"{db_path}/OCAB.db" def create_tables(db: pw.SqliteDatabase): @@ -138,7 +138,7 @@ def get_chat_all_stat(chat_id): # Работа с таблицей пользователей -def get_user(user_id): +def get_user(user_id) -> Users | None: return Users.get_or_none(Users.id == user_id) @@ -196,7 +196,8 @@ def change_user_role(user_id, new_user_role): def get_message(message_chat_id, message_id): return Messages.get_or_none( - Messages.message_chat_id == message_chat_id, Messages.message_id == message_id + Messages.message_chat_id == message_chat_id, + Messages.message_id == message_id, ) diff --git a/src/modules/standard/exceptions/module_exceptions.py b/src/ocab_modules/standard/database/exceptions.py similarity index 100% rename from src/modules/standard/exceptions/module_exceptions.py rename to src/ocab_modules/standard/database/exceptions.py diff --git a/src/modules/standard/database/info.json b/src/ocab_modules/standard/database/info.json similarity index 54% rename from src/modules/standard/database/info.json rename to src/ocab_modules/standard/database/info.json index bfa88d4..1a5060e 100644 --- a/src/modules/standard/database/info.json +++ b/src/ocab_modules/standard/database/info.json @@ -1,6 +1,9 @@ { + "id": "standard.database", "name": "Database", "description": "Модуль для работы с БД", "author": "OCAB Team", - "version": "1.0" + "version": "1.0.0", + "privileged": true, + "dependencies": {} } diff --git a/src/modules/standard/database/tests/__init__.py b/src/ocab_modules/standard/database/models/__init__.py similarity index 100% rename from src/modules/standard/database/tests/__init__.py rename to src/ocab_modules/standard/database/models/__init__.py diff --git a/src/modules/standard/database/models/chat_stats.py b/src/ocab_modules/standard/database/models/chat_stats.py similarity index 69% rename from src/modules/standard/database/models/chat_stats.py rename to src/ocab_modules/standard/database/models/chat_stats.py index 1f62601..b681407 100644 --- a/src/modules/standard/database/models/chat_stats.py +++ b/src/ocab_modules/standard/database/models/chat_stats.py @@ -1,8 +1,11 @@ import peewee as pw +from .db import database_proxy + class ChatStats(pw.Model): - class Meta: ... + class Meta: + database = database_proxy chat_id = pw.IntegerField(null=False) date = pw.DateField(null=False) diff --git a/src/modules/standard/database/models/chats.py b/src/ocab_modules/standard/database/models/chats.py similarity index 69% rename from src/modules/standard/database/models/chats.py rename to src/ocab_modules/standard/database/models/chats.py index 8e2dd24..f47cd8b 100644 --- a/src/modules/standard/database/models/chats.py +++ b/src/ocab_modules/standard/database/models/chats.py @@ -1,8 +1,11 @@ import peewee as pw +from .db import database_proxy + class Chats(pw.Model): - class Meta: ... + class Meta: + database = database_proxy chat_name = pw.CharField(null=False) chat_type = pw.IntegerField(null=False, default=10) diff --git a/src/ocab_modules/standard/database/models/db.py b/src/ocab_modules/standard/database/models/db.py new file mode 100644 index 0000000..cc1f8f7 --- /dev/null +++ b/src/ocab_modules/standard/database/models/db.py @@ -0,0 +1,3 @@ +from peewee import DatabaseProxy + +database_proxy = DatabaseProxy() diff --git a/src/modules/standard/database/models/messages.py b/src/ocab_modules/standard/database/models/messages.py similarity index 80% rename from src/modules/standard/database/models/messages.py rename to src/ocab_modules/standard/database/models/messages.py index 926b58d..c532b0d 100644 --- a/src/modules/standard/database/models/messages.py +++ b/src/ocab_modules/standard/database/models/messages.py @@ -1,8 +1,11 @@ import peewee as pw +from .db import database_proxy + class Messages(pw.Model): - class Meta: ... + class Meta: + database = database_proxy message_chat_id = pw.IntegerField(null=False) message_id = pw.IntegerField(null=False) diff --git a/src/modules/standard/database/models/user_stats.py b/src/ocab_modules/standard/database/models/user_stats.py similarity index 73% rename from src/modules/standard/database/models/user_stats.py rename to src/ocab_modules/standard/database/models/user_stats.py index e656da4..e0e96f2 100644 --- a/src/modules/standard/database/models/user_stats.py +++ b/src/ocab_modules/standard/database/models/user_stats.py @@ -1,8 +1,11 @@ import peewee as pw +from .db import database_proxy + class UserStats(pw.Model): - class Meta: ... + class Meta: + database = database_proxy chat_id = pw.IntegerField(null=False) user_id = pw.IntegerField(null=False) diff --git a/src/modules/standard/database/models/users.py b/src/ocab_modules/standard/database/models/users.py similarity index 79% rename from src/modules/standard/database/models/users.py rename to src/ocab_modules/standard/database/models/users.py index 27a7fe1..2dfd407 100644 --- a/src/modules/standard/database/models/users.py +++ b/src/ocab_modules/standard/database/models/users.py @@ -1,8 +1,11 @@ import peewee as pw +from .db import database_proxy + class Users(pw.Model): - class Meta: ... + class Meta: + database = database_proxy user_tag = pw.CharField(null=True) user_name = pw.CharField(null=False) # до 255 символов diff --git a/src/modules/standard/filters/__init__.py b/src/ocab_modules/standard/database/tests/__init__.py similarity index 100% rename from src/modules/standard/filters/__init__.py rename to src/ocab_modules/standard/database/tests/__init__.py diff --git a/src/modules/standard/database/tests/database/file b/src/ocab_modules/standard/database/tests/database/file similarity index 100% rename from src/modules/standard/database/tests/database/file rename to src/ocab_modules/standard/database/tests/database/file diff --git a/src/modules/standard/database/tests/test_db.py b/src/ocab_modules/standard/database/tests/test_db.py similarity index 100% rename from src/modules/standard/database/tests/test_db.py rename to src/ocab_modules/standard/database/tests/test_db.py diff --git a/src/modules/standard/info/__init__.py b/src/ocab_modules/standard/filters/__init__.py similarity index 100% rename from src/modules/standard/info/__init__.py rename to src/ocab_modules/standard/filters/__init__.py diff --git a/src/modules/standard/filters/filters.py b/src/ocab_modules/standard/filters/filters.py similarity index 87% rename from src/modules/standard/filters/filters.py rename to src/ocab_modules/standard/filters/filters.py index ecdaef7..5de5e29 100644 --- a/src/modules/standard/filters/filters.py +++ b/src/ocab_modules/standard/filters/filters.py @@ -2,9 +2,9 @@ from aiogram import Bot from aiogram.filters import BaseFilter from aiogram.types import Message -from src.core.logger import log -from src.modules.standard.config.config import get_aproved_chat_id -from src.modules.standard.roles.roles import Roles +from ocab_core.logger import log +from ocab_modules.standard.config.config import get_aproved_chat_id +from ocab_modules.standard.roles.roles import Roles class ChatModerOrAdminFilter(BaseFilter): diff --git a/src/modules/standard/filters/info.json b/src/ocab_modules/standard/filters/info.json similarity index 100% rename from src/modules/standard/filters/info.json rename to src/ocab_modules/standard/filters/info.json diff --git a/src/ocab_modules/standard/info/__init__.py b/src/ocab_modules/standard/info/__init__.py new file mode 100644 index 0000000..69d0151 --- /dev/null +++ b/src/ocab_modules/standard/info/__init__.py @@ -0,0 +1,14 @@ +from aiogram import F, Router + +from ocab_core.modules_system.public_api import register_router + +from .handlers import get_chat_info, get_user_info + + +def module_init(): + router = Router() + + router.message.register(get_user_info, F.text.startswith("/info")) + router.message.register(get_chat_info, F.text.startswith("/chatinfo")) + + register_router(router) diff --git a/src/modules/standard/info/handlers.py b/src/ocab_modules/standard/info/handlers.py similarity index 68% rename from src/modules/standard/info/handlers.py rename to src/ocab_modules/standard/info/handlers.py index d1b60ba..40388fe 100644 --- a/src/modules/standard/info/handlers.py +++ b/src/ocab_modules/standard/info/handlers.py @@ -1,46 +1,59 @@ # flake8: noqa +from typing import Type from aiogram import Bot from aiogram.types import Message -from src.core.logger import log -from src.modules.standard.config.config import get_user_role_name -from src.modules.standard.database.db_api import * -from src.modules.standard.roles.roles import Roles +from ocab_core.modules_system.public_api import get_module, log + +from .interfaces import IDbApi, IRoles + +db_api: Type[IDbApi] = get_module( + "standard.database", + "db_api", +) + +Roles: Type[IRoles] = get_module("standard.roles", "Roles") async def get_info_answer_by_id(message: Message, bot: Bot, user_id: int): - if get_message_ai_model(message.chat.id, message.message_id) is not None: + ai_model = db_api.get_message_ai_model(message.chat.id, message.message_id) + if ai_model is not None: await message.reply( - "Это сообщение было сгенерировано ботом используя модель: " - + get_message_ai_model(message.chat.id, message.message_id) + "Это сообщение было сгенерировано ботом используя модель: " + ai_model ) - elif user_id == bot.id: + return + + if user_id == bot.id: await message.reply("Это сообщение было отправлено ботом") - elif get_user(user_id) is None: + return + + user = db_api.get_user(user_id) + + if user is None: await message.reply("Пользователь не найден") - # print(get_user(user_id)) - await log(f"Пользователь не найден: {user_id}, {get_user(user_id)}") - else: - roles = Roles() - answer = ( - f"Пользователь: {get_user_name(user_id)}\n" - f"Роль: {await roles.get_role_name(role_id=get_user_role(user_id))}\n" - f"Тег: @{get_user_tag(user_id)}\n" - f"Кол-во сообщений: {get_user_all_stats(user_id)}\n" - f"Репутация: {get_user_rep(user_id)}" - ) - await message.reply(answer) + await log(f"Пользователь не найден: {user_id}, {user}") + return + + roles = Roles() + answer = ( + f"Пользователь: {user.user_name}\n" + f"Роль: {await roles.get_role_name(role_id=user.user_role)}\n" + f"Тег: @{user.user_tag}\n" + f"Кол-во сообщений: {user.user_stats}\n" + f"Репутация: {user.user_rep}" + ) + await message.reply(answer) async def get_user_info(message: Message, bot: Bot): # Проверяем содержимое сообщения, если содержит вторым элементом тег пользователя, то выводим информацию о нем # Если сообщение отвечает на другое сообщение, то выводим информацию о пользователе, на чье сообщение был ответ - # Если это бот то выводим информацию что это бот и какая модель yandexgpt используется + # Если это бот то выводим информацию, что это бот и какая модель yandexgpt используется try: if len(message.text.split()) > 1 and message.text.split()[1].startswith("@"): user_tag = message.text.split()[1][1:] - user_id = get_user_id(user_tag) + user_id = db_api.get_user_id(user_tag) if user_id: await get_info_answer_by_id(message, bot, user_id) else: @@ -63,7 +76,7 @@ async def get_chat_info(message: Message, bot: Bot): answer = ( f"*Название чата:* {message.chat.title}\n" f"*ID чата:* `{message.chat.id}`\n \n" - f"*Суммарное количество сообщений в чате:* {get_chat_all_stat(message.chat.id)}\n" + f"*Суммарное количество сообщений в чате:* {db_api.get_chat_all_stat(message.chat.id)}\n" f"*Количество пользователей в чате:* {await bot.get_chat_member_count(message.chat.id)}\n" f"*Количество администраторов в чате:* {len(await bot.get_chat_administrators(message.chat.id))}" ) diff --git a/src/ocab_modules/standard/info/info.json b/src/ocab_modules/standard/info/info.json new file mode 100644 index 0000000..bd5b84b --- /dev/null +++ b/src/ocab_modules/standard/info/info.json @@ -0,0 +1,12 @@ +{ + "id": "standard.info", + "name": "Info", + "description": "Модуль с информацией", + "author": "OCAB Team", + "version": "1.0.0", + "privileged": false, + "dependencies": { + "standard.roles": "^1.0.0", + "standard.database": "^1.0.0" + } +} diff --git a/src/ocab_modules/standard/info/interfaces.py b/src/ocab_modules/standard/info/interfaces.py new file mode 100644 index 0000000..5bdec30 --- /dev/null +++ b/src/ocab_modules/standard/info/interfaces.py @@ -0,0 +1,47 @@ +from typing import Any + + +class IRoles: + user: str + moderator: str + admin: str + bot: str + + def __init__(self): + pass + + async def check_admin_permission(self, user_id: int) -> bool: + pass + + async def check_moderator_permission(self, user_id) -> bool: + pass + + async def get_role_name(self, role_id) -> str: + pass + + async def get_user_permission(self, user_id) -> str | None: + pass + + +class IUsers: + user_id: int + user_name: str + user_role: int + user_tag: str + user_stats: int + user_rep: int + + +class IDbApi: + + @staticmethod + def get_message_ai_model(message_chat_id: Any, message_id: Any) -> Any | None: + pass + + @staticmethod + def get_user(user_id: int) -> IUsers | None: + pass + + @staticmethod + def get_chat_all_stat(chat_id: int) -> int: + pass diff --git a/src/modules/standard/message_processing/__init__.py b/src/ocab_modules/standard/message_processing/__init__.py similarity index 100% rename from src/modules/standard/message_processing/__init__.py rename to src/ocab_modules/standard/message_processing/__init__.py diff --git a/src/modules/standard/message_processing/message_api.py b/src/ocab_modules/standard/message_processing/message_api.py similarity index 96% rename from src/modules/standard/message_processing/message_api.py rename to src/ocab_modules/standard/message_processing/message_api.py index a1ef218..06b6093 100644 --- a/src/modules/standard/message_processing/message_api.py +++ b/src/ocab_modules/standard/message_processing/message_api.py @@ -2,14 +2,14 @@ from aiogram import Bot, F, Router, types -from src.core.logger import log -from src.modules.external.yandexgpt.handlers import answer_to_message -from src.modules.standard.config.config import ( +from ocab_core.logger import log +from ocab_modules.external.yandexgpt.handlers import answer_to_message +from ocab_modules.standard.config.config import ( get_aproved_chat_id, get_yandexgpt_in_words, get_yandexgpt_start_words, ) -from src.modules.standard.database.db_api import * +from ocab_modules.standard.database.db_api import * async def chat_check(message: types.Message): diff --git a/src/modules/standard/moderation/__init__.py b/src/ocab_modules/standard/moderation/__init__.py similarity index 100% rename from src/modules/standard/moderation/__init__.py rename to src/ocab_modules/standard/moderation/__init__.py diff --git a/src/modules/standard/moderation/info.json b/src/ocab_modules/standard/moderation/info.json similarity index 100% rename from src/modules/standard/moderation/info.json rename to src/ocab_modules/standard/moderation/info.json diff --git a/src/modules/standard/moderation/moderation.py b/src/ocab_modules/standard/moderation/moderation.py similarity index 100% rename from src/modules/standard/moderation/moderation.py rename to src/ocab_modules/standard/moderation/moderation.py diff --git a/src/ocab_modules/standard/roles/__init__.py b/src/ocab_modules/standard/roles/__init__.py new file mode 100644 index 0000000..5cd56b0 --- /dev/null +++ b/src/ocab_modules/standard/roles/__init__.py @@ -0,0 +1 @@ +from .roles import Roles diff --git a/src/ocab_modules/standard/roles/info.json b/src/ocab_modules/standard/roles/info.json new file mode 100644 index 0000000..8a70a31 --- /dev/null +++ b/src/ocab_modules/standard/roles/info.json @@ -0,0 +1,12 @@ +{ + "id": "standard.roles", + "name": "Roles", + "description": "Модуль для работы с ролями", + "author": "OCAB Team", + "version": "1.0.0", + "privileged": true, + "dependencies": { + "standard.config": "^1.0.0", + "standard.database": "^1.0.0" + } +} diff --git a/src/modules/standard/roles/roles.py b/src/ocab_modules/standard/roles/roles.py similarity index 88% rename from src/modules/standard/roles/roles.py rename to src/ocab_modules/standard/roles/roles.py index 2bf3bb8..88e21c5 100644 --- a/src/modules/standard/roles/roles.py +++ b/src/ocab_modules/standard/roles/roles.py @@ -1,7 +1,7 @@ -from ..config.config import get_config -from ..database.db_api import get_user_role +from ocab_core.modules_system.public_api import get_module -yaml_load = get_config() +get_user_role = get_module("standard.database", "db_api.get_user_role") +config: dict = get_module("standard.config", "config") class Roles: @@ -9,7 +9,7 @@ class Roles: moderator = "MODERATOR" admin = "ADMIN" bot = "BOT" - __roles = yaml_load["ROLES"] + __roles = config["ROLES"] def __init__(self): self.user_role_id = self.__roles[self.user] diff --git a/src/modules/standard/roles/__init__.py b/src/ocab_modules/standard/roles/tests/__init__.py similarity index 100% rename from src/modules/standard/roles/__init__.py rename to src/ocab_modules/standard/roles/tests/__init__.py diff --git a/src/modules/standard/roles/tests/config.yaml b/src/ocab_modules/standard/roles/tests/config.yaml similarity index 100% rename from src/modules/standard/roles/tests/config.yaml rename to src/ocab_modules/standard/roles/tests/config.yaml diff --git a/src/modules/standard/roles/tests/test_roles.py b/src/ocab_modules/standard/roles/tests/test_roles.py similarity index 100% rename from src/modules/standard/roles/tests/test_roles.py rename to src/ocab_modules/standard/roles/tests/test_roles.py diff --git a/src/modules/standard/roles/tests/__init__.py b/src/ocab_modules/standard/welcome/__init__.py similarity index 100% rename from src/modules/standard/roles/tests/__init__.py rename to src/ocab_modules/standard/welcome/__init__.py diff --git a/src/modules/standard/welcome/handlers.py b/src/ocab_modules/standard/welcome/handlers.py similarity index 91% rename from src/modules/standard/welcome/handlers.py rename to src/ocab_modules/standard/welcome/handlers.py index 16102b9..caee6bb 100644 --- a/src/modules/standard/welcome/handlers.py +++ b/src/ocab_modules/standard/welcome/handlers.py @@ -9,10 +9,14 @@ from aiogram.types import Message from aiogram.types import inline_keyboard_button as types from aiogram.utils.keyboard import InlineKeyboardBuilder -from src.modules.standard.config.config import get_telegram_check_bot -from src.modules.standard.database.db_api import * -from src.modules.standard.moderation.moderation import ban_user, mute_user, unmute_user -from src.modules.standard.roles.roles import Roles +from src.ocab_modules.standard.config.config import get_telegram_check_bot +from src.ocab_modules.standard.database.db_api import * +from src.ocab_modules.standard.moderation.moderation import ( + ban_user, + mute_user, + unmute_user, +) +from src.ocab_modules.standard.roles.roles import Roles async def create_math_task(): diff --git a/src/modules/standard/welcome/info.json b/src/ocab_modules/standard/welcome/info.json similarity index 100% rename from src/modules/standard/welcome/info.json rename to src/ocab_modules/standard/welcome/info.json diff --git a/src/modules/standard/welcome/routers.py b/src/ocab_modules/standard/welcome/routers.py similarity index 84% rename from src/modules/standard/welcome/routers.py rename to src/ocab_modules/standard/welcome/routers.py index 2afc09f..57546dc 100644 --- a/src/modules/standard/welcome/routers.py +++ b/src/ocab_modules/standard/welcome/routers.py @@ -1,6 +1,6 @@ from aiogram import F, Router -from src.modules.standard.welcome.handlers import check_new_user +from src.ocab_modules.standard.welcome.handlers import check_new_user router = Router() diff --git a/src/modules/standard/welcome/welcome.py b/src/ocab_modules/standard/welcome/welcome.py similarity index 100% rename from src/modules/standard/welcome/welcome.py rename to src/ocab_modules/standard/welcome/welcome.py