0
0
mirror of https://gitflic.ru/project/maks1ms/ocab.git synced 2024-12-23 08:13:01 +03:00

Merged with private/new-module-system

This commit is contained in:
Maxim Slipenko 2024-07-10 19:30:23 +03:00
parent 2a2b9e15e8
commit 370b4fc648
92 changed files with 987 additions and 279 deletions

4
.gitignore vendored
View File

@ -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/**/*

View File

@ -28,10 +28,11 @@ OCAB - это бот для Telegram, который призван помочь
## Дополнительные официальные модули
* `gpt` - модуль для генерации ответов на основе нейросети GPT-3.5. Позволяет боту отвечать на сообщения
* `yandexgpt` - модуль для генерации ответов на основе нейросети GPT-3.5. Позволяет боту отвечать на сообщения
пользователей, используя нейросеть. Ключевой особенностью является построение линии контекста для нейросети,
которая позволяет боту отвечать на вопросы, используя контекст предыдущих сообщений. Для этого используется
модуль база данных хранящий историю сообщений.
<!--
* `bugzilla` - модуль для интеграции с BugZilla. Позволяет получать уведомления о новых багах в BugZilla, отслеживать их
статус, формировать стандартизированные сообщения для корректного описания багов. В будущем планируется интеграция с
API BugZilla для возможности создания багов из чата.
@ -40,6 +41,7 @@ OCAB - это бот для Telegram, который призван помочь
прочей информации о пакете.
* `notes` - модуль заметок. Позволяет сохранять заметки для пользователей и чатов. Заметки являются ссылками на
сообщения в чате.
-->
Список модулей будет пополняться. Идеи для модулей можно оставлять в [issues](https://gitflic.ru/project/armatik/ocab/issue/create).

60
docs/MODULES-SPEC.md Normal file
View File

@ -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`. Она позволяет получить модуль или предоставляемые им объекты по его идентификатору.

15
init.py
View File

@ -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))

312
poetry.lock generated
View File

@ -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"

View File

@ -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 = ["Семён Фомченков <s.fomchenkov@yandex.ru>"]
maintainers = [
"Илья Женецкий <ilya_zhenetskij@vk.com>",
"qualimock <qualimock@yandex.ru>",
"Кирилл Уницаев fiersik.kouji@yandex.ru",
"Кирилл Уницаев <fiersik.kouji@yandex.ru>",
"Максим Слипенко <maxim@slipenko.com>"
]
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

View File

@ -1,4 +0,0 @@
#! /bin/sh
cd src
python -m unittest discover -v
cd ..

21
scripts/init.py Normal file
View File

@ -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()

9
scripts/test.py Normal file
View File

@ -0,0 +1,9 @@
import subprocess # nosec
def main():
subprocess.run(["python", "-u", "-m", "unittest", "discover"]) # nosec
if __name__ == "__main__":
main()

View File

@ -1,2 +1,2 @@
import src.core
import src.service
import ocab_core
import service

View File

@ -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())

View File

@ -1 +0,0 @@
from . import config, database, exceptions, roles

View File

@ -1 +0,0 @@
from . import config

View File

@ -1 +0,0 @@
from . import db_api, models

View File

@ -1 +0,0 @@
from . import module_exceptions

View File

@ -1,6 +0,0 @@
{
"name": "Exceptions",
"description": "Модуль с исключениями",
"author": "OCAB Team",
"version": "1.0"
}

View File

@ -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)

View File

@ -1,6 +0,0 @@
{
"name": "Roles",
"description": "Модуль для работы с ролями",
"author": "OCAB Team",
"version": "1.0"
}

View File

@ -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)

49
src/ocab_core/main.py Normal file
View File

@ -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())

View File

@ -0,0 +1 @@
from .modules_manager import ModulesManager

View File

@ -0,0 +1 @@
from .fs_loader import FSLoader

View File

@ -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

View File

@ -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, "<string>", policy=RestrictedPythonPolicy)
if result.errors:
for error in result.errors:
print(error)
exec(result.code, module.__dict__) # nosec
return module

View File

@ -0,0 +1 @@
from .FSLoader import FSLoader

View File

@ -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()

View File

@ -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

View File

@ -0,0 +1 @@
from .UnsafeFSLoader import UnsafeFSLoader

View File

@ -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"]

View File

@ -0,0 +1,3 @@
from ocab_core.logger import log # noqa
from .public_api import Storage, get_module, register_router

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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):

View File

@ -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

View File

@ -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 *

View File

@ -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):

View File

@ -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,
)

View File

@ -0,0 +1 @@
from .config import config

View File

@ -2,7 +2,7 @@
import yaml
from ....service import paths
from src.service import paths
def get_config(is_test: bool = False) -> dict:

View File

@ -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": {}
}

View File

@ -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)

View File

@ -0,0 +1,5 @@
from . import db_api, models
def module_init():
db_api.connect_database()

View File

@ -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,
)

View File

@ -1,6 +1,9 @@
{
"id": "standard.database",
"name": "Database",
"description": "Модуль для работы с БД",
"author": "OCAB Team",
"version": "1.0"
"version": "1.0.0",
"privileged": true,
"dependencies": {}
}

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1,3 @@
from peewee import DatabaseProxy
database_proxy = DatabaseProxy()

View File

@ -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)

View File

@ -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)

View File

@ -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 символов

View File

@ -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):

View File

@ -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)

View File

@ -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))}"
)

View File

@ -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"
}
}

View File

@ -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

View File

@ -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):

View File

@ -0,0 +1 @@
from .roles import Roles

View File

@ -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"
}
}

View File

@ -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]

View File

@ -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():

View File

@ -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()