0
0
mirror of https://gitflic.ru/project/maks1ms/ocab.git synced 2025-01-12 01:31:05 +03:00
This commit is contained in:
Maxim Slipenko 2024-07-20 12:01:00 +03:00
parent e8b5f79d99
commit d52864a231
26 changed files with 1241 additions and 94 deletions

377
poetry.lock generated
View File

@ -301,6 +301,28 @@ d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"] uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "blinker"
version = "1.8.2"
description = "Fast, simple object-to-object and broadcast signaling"
optional = false
python-versions = ">=3.8"
files = [
{file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"},
{file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"},
]
[[package]]
name = "cachelib"
version = "0.9.0"
description = "A collection of cache libraries in the same API interface."
optional = false
python-versions = ">=3.7"
files = [
{file = "cachelib-0.9.0-py3-none-any.whl", hash = "sha256:811ceeb1209d2fe51cd2b62810bd1eccf70feba5c52641532498be5c675493b3"},
{file = "cachelib-0.9.0.tar.gz", hash = "sha256:38222cc7c1b79a23606de5c2607f4925779e37cdcea1c2ad21b8bae94b5425a5"},
]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2024.7.4" version = "2024.7.4"
@ -479,6 +501,128 @@ pyyaml = ">=5.3.1"
requests = ">=2.23.0" requests = ">=2.23.0"
rich = "*" rich = "*"
[[package]]
name = "dash"
version = "2.17.1"
description = "A Python framework for building reactive web-apps. Developed by Plotly."
optional = false
python-versions = ">=3.8"
files = [
{file = "dash-2.17.1-py3-none-any.whl", hash = "sha256:3eefc9ac67003f93a06bc3e500cae0a6787c48e6c81f6f61514239ae2da414e4"},
{file = "dash-2.17.1.tar.gz", hash = "sha256:ee2d9c319de5dcc1314085710b72cd5fa63ff994d913bf72979b7130daeea28e"},
]
[package.dependencies]
dash-core-components = "2.0.0"
dash-html-components = "2.0.0"
dash-table = "5.0.0"
Flask = ">=1.0.4,<3.1"
importlib-metadata = "*"
nest-asyncio = "*"
plotly = ">=5.0.0"
requests = "*"
retrying = "*"
setuptools = "*"
typing-extensions = ">=4.1.1"
Werkzeug = "<3.1"
[package.extras]
celery = ["celery[redis] (>=5.1.2)", "redis (>=3.5.3)"]
ci = ["black (==22.3.0)", "dash-dangerously-set-inner-html", "dash-flow-example (==0.0.5)", "flake8 (==7.0.0)", "flaky (==3.8.1)", "flask-talisman (==1.0.0)", "jupyterlab (<4.0.0)", "mimesis (<=11.1.0)", "mock (==4.0.3)", "numpy (<=1.26.3)", "openpyxl", "orjson (==3.10.3)", "pandas (>=1.4.0)", "pyarrow", "pylint (==3.0.3)", "pytest-mock", "pytest-rerunfailures", "pytest-sugar (==0.9.6)", "pyzmq (==25.1.2)", "xlrd (>=2.0.1)"]
compress = ["flask-compress"]
dev = ["PyYAML (>=5.4.1)", "coloredlogs (>=15.0.1)", "fire (>=0.4.0)"]
diskcache = ["diskcache (>=5.2.1)", "multiprocess (>=0.70.12)", "psutil (>=5.8.0)"]
testing = ["beautifulsoup4 (>=4.8.2)", "cryptography", "dash-testing-stub (>=0.0.2)", "lxml (>=4.6.2)", "multiprocess (>=0.70.12)", "percy (>=2.0.2)", "psutil (>=5.8.0)", "pytest (>=6.0.2)", "requests[security] (>=2.21.0)", "selenium (>=3.141.0,<=4.2.0)", "waitress (>=1.4.4)"]
[[package]]
name = "dash-bootstrap-components"
version = "1.6.0"
description = "Bootstrap themed components for use in Plotly Dash"
optional = false
python-versions = "<4,>=3.8"
files = [
{file = "dash_bootstrap_components-1.6.0-py3-none-any.whl", hash = "sha256:97f0f47b38363f18863e1b247462229266ce12e1e171cfb34d3c9898e6e5cd1e"},
{file = "dash_bootstrap_components-1.6.0.tar.gz", hash = "sha256:960a1ec9397574792f49a8241024fa3cecde0f5930c971a3fc81f016cbeb1095"},
]
[package.dependencies]
dash = ">=2.0.0"
[package.extras]
pandas = ["numpy", "pandas"]
[[package]]
name = "dash-core-components"
version = "2.0.0"
description = "Core component suite for Dash"
optional = false
python-versions = "*"
files = [
{file = "dash_core_components-2.0.0-py3-none-any.whl", hash = "sha256:52b8e8cce13b18d0802ee3acbc5e888cb1248a04968f962d63d070400af2e346"},
{file = "dash_core_components-2.0.0.tar.gz", hash = "sha256:c6733874af975e552f95a1398a16c2ee7df14ce43fa60bb3718a3c6e0b63ffee"},
]
[[package]]
name = "dash-extensions"
version = "1.0.18"
description = "Extensions for Plotly Dash."
optional = false
python-versions = "<4,>=3.9"
files = [
{file = "dash_extensions-1.0.18-py3-none-any.whl", hash = "sha256:17f4469670bd70ce12fac1a05baaae119fb65eee7b012af47aff7377d0399eeb"},
{file = "dash_extensions-1.0.18.tar.gz", hash = "sha256:a6b6c0952b3af7ae84c418fea4b43cbd0bd4e82f20d91f1573380b8a3d90df0a"},
]
[package.dependencies]
dash = ">=2.17.0"
dataclass-wizard = ">=0.22.2,<0.23.0"
Flask-Caching = ">=2.1.0,<3.0.0"
jsbeautifier = ">=1.14.3,<2.0.0"
more-itertools = ">=10.2.0,<11.0.0"
pydantic = ">=2.7.1,<3.0.0"
ruff = ">=0.4.5,<0.5.0"
[package.extras]
mantine = ["dash-mantine-components (>=0.14.3,<0.15.0)"]
[[package]]
name = "dash-html-components"
version = "2.0.0"
description = "Vanilla HTML components for Dash"
optional = false
python-versions = "*"
files = [
{file = "dash_html_components-2.0.0-py3-none-any.whl", hash = "sha256:b42cc903713c9706af03b3f2548bda4be7307a7cf89b7d6eae3da872717d1b63"},
{file = "dash_html_components-2.0.0.tar.gz", hash = "sha256:8703a601080f02619a6390998e0b3da4a5daabe97a1fd7a9cebc09d015f26e50"},
]
[[package]]
name = "dash-table"
version = "5.0.0"
description = "Dash table"
optional = false
python-versions = "*"
files = [
{file = "dash_table-5.0.0-py3-none-any.whl", hash = "sha256:19036fa352bb1c11baf38068ec62d172f0515f73ca3276c79dee49b95ddc16c9"},
{file = "dash_table-5.0.0.tar.gz", hash = "sha256:18624d693d4c8ef2ddec99a6f167593437a7ea0bf153aa20f318c170c5bc7308"},
]
[[package]]
name = "dataclass-wizard"
version = "0.22.3"
description = "Marshal dataclasses to/from JSON. Use field properties with initial values. Construct a dataclass schema with JSON input."
optional = false
python-versions = "*"
files = [
{file = "dataclass-wizard-0.22.3.tar.gz", hash = "sha256:4c46591782265058f1148cfd1f54a3a91221e63986fdd04c9d59f4ced61f4424"},
{file = "dataclass_wizard-0.22.3-py2.py3-none-any.whl", hash = "sha256:63751203e54b9b9349212cc185331da73c1adc99c51312575eb73bb5c00c1962"},
]
[package.extras]
dev = ["Sphinx (==5.3.0)", "bump2version (==1.0.1)", "coverage (>=6.2)", "dataclass-factory (==2.12)", "dataclasses-json (==0.5.6)", "flake8 (>=3)", "jsons (==1.6.1)", "pip (>=21.3.1)", "pytest (==7.0.1)", "pytest-cov (==3.0.0)", "pytest-mock (>=3.6.1)", "pytimeparse (==1.1.8)", "sphinx-issues (==3.0.1)", "sphinx-issues (==4.0.0)", "tox (==3.24.5)", "twine (==3.8.0)", "watchdog[watchmedo] (==2.1.6)", "wheel (==0.37.1)", "wheel (==0.42.0)"]
timedelta = ["pytimeparse (>=1.1.7)"]
yaml = ["PyYAML (>=5.3)"]
[[package]] [[package]]
name = "dataclasses-json" name = "dataclasses-json"
version = "0.6.7" version = "0.6.7"
@ -525,6 +669,16 @@ idna = ["idna (>=3.6)"]
trio = ["trio (>=0.23)"] trio = ["trio (>=0.23)"]
wmi = ["wmi (>=1.5.1)"] wmi = ["wmi (>=1.5.1)"]
[[package]]
name = "editorconfig"
version = "0.12.4"
description = "EditorConfig File Locator and Interpreter for Python"
optional = false
python-versions = "*"
files = [
{file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"},
]
[[package]] [[package]]
name = "email-validator" name = "email-validator"
version = "2.2.0" version = "2.2.0"
@ -614,6 +768,43 @@ mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.12.0,<2.13.0" pycodestyle = ">=2.12.0,<2.13.0"
pyflakes = ">=3.2.0,<3.3.0" pyflakes = ">=3.2.0,<3.3.0"
[[package]]
name = "flask"
version = "3.0.3"
description = "A simple framework for building complex web applications."
optional = false
python-versions = ">=3.8"
files = [
{file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"},
{file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"},
]
[package.dependencies]
blinker = ">=1.6.2"
click = ">=8.1.3"
itsdangerous = ">=2.1.2"
Jinja2 = ">=3.1.2"
Werkzeug = ">=3.0.0"
[package.extras]
async = ["asgiref (>=3.2)"]
dotenv = ["python-dotenv"]
[[package]]
name = "flask-caching"
version = "2.3.0"
description = "Adds caching support to Flask applications."
optional = false
python-versions = ">=3.8"
files = [
{file = "Flask_Caching-2.3.0-py3-none-any.whl", hash = "sha256:51771c75682e5abc1483b78b96d9131d7941dc669b073852edfa319dd4e29b6e"},
{file = "flask_caching-2.3.0.tar.gz", hash = "sha256:d7e4ca64a33b49feb339fcdd17e6ba25f5e01168cf885e53790e885f83a4d2cf"},
]
[package.dependencies]
cachelib = ">=0.9.0,<0.10.0"
Flask = "*"
[[package]] [[package]]
name = "flet" name = "flet"
version = "0.23.2" version = "0.23.2"
@ -947,6 +1138,25 @@ files = [
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
] ]
[[package]]
name = "importlib-metadata"
version = "8.0.0"
description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"},
{file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"},
]
[package.dependencies]
zipp = ">=0.5"
[package.extras]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
perf = ["ipython"]
test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"]
[[package]] [[package]]
name = "isort" name = "isort"
version = "5.13.2" version = "5.13.2"
@ -961,6 +1171,17 @@ files = [
[package.extras] [package.extras]
colors = ["colorama (>=0.4.6)"] colors = ["colorama (>=0.4.6)"]
[[package]]
name = "itsdangerous"
version = "2.2.0"
description = "Safely pass data to untrusted environments and back."
optional = false
python-versions = ">=3.8"
files = [
{file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"},
{file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"},
]
[[package]] [[package]]
name = "jinja2" name = "jinja2"
version = "3.1.4" version = "3.1.4"
@ -978,6 +1199,20 @@ MarkupSafe = ">=2.0"
[package.extras] [package.extras]
i18n = ["Babel (>=2.7)"] i18n = ["Babel (>=2.7)"]
[[package]]
name = "jsbeautifier"
version = "1.15.1"
description = "JavaScript unobfuscator and beautifier."
optional = false
python-versions = "*"
files = [
{file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"},
]
[package.dependencies]
editorconfig = ">=0.12.2"
six = ">=1.13.0"
[[package]] [[package]]
name = "magic-filter" name = "magic-filter"
version = "1.0.12" version = "1.0.12"
@ -1126,6 +1361,17 @@ files = [
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
] ]
[[package]]
name = "more-itertools"
version = "10.3.0"
description = "More routines for operating on iterables, beyond itertools"
optional = false
python-versions = ">=3.8"
files = [
{file = "more-itertools-10.3.0.tar.gz", hash = "sha256:e5d93ef411224fbcef366a6e8ddc4c5781bc6359d43412a65dd5964e46111463"},
{file = "more_itertools-10.3.0-py3-none-any.whl", hash = "sha256:ea6a02e24a9161e51faad17a8782b92a0df82c12c1c8886fec7f0c3fa1a1b320"},
]
[[package]] [[package]]
name = "multidict" name = "multidict"
version = "6.0.5" version = "6.0.5"
@ -1236,6 +1482,17 @@ files = [
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
] ]
[[package]]
name = "nest-asyncio"
version = "1.6.0"
description = "Patch asyncio to allow nested event loops"
optional = false
python-versions = ">=3.5"
files = [
{file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"},
{file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"},
]
[[package]] [[package]]
name = "nodeenv" name = "nodeenv"
version = "1.9.1" version = "1.9.1"
@ -1322,6 +1579,21 @@ docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
type = ["mypy (>=1.8)"] type = ["mypy (>=1.8)"]
[[package]]
name = "plotly"
version = "5.22.0"
description = "An open-source, interactive data visualization library for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "plotly-5.22.0-py3-none-any.whl", hash = "sha256:68fc1901f098daeb233cc3dd44ec9dc31fb3ca4f4e53189344199c43496ed006"},
{file = "plotly-5.22.0.tar.gz", hash = "sha256:859fdadbd86b5770ae2466e542b761b247d1c6b49daed765b95bb8c7063e7469"},
]
[package.dependencies]
packaging = "*"
tenacity = ">=6.2.0"
[[package]] [[package]]
name = "pre-commit" name = "pre-commit"
version = "3.7.1" version = "3.7.1"
@ -1710,6 +1982,20 @@ files = [
docs = ["Sphinx", "sphinx-rtd-theme"] docs = ["Sphinx", "sphinx-rtd-theme"]
test = ["pytest", "pytest-mock"] test = ["pytest", "pytest-mock"]
[[package]]
name = "retrying"
version = "1.3.4"
description = "Retrying"
optional = false
python-versions = "*"
files = [
{file = "retrying-1.3.4-py3-none-any.whl", hash = "sha256:8cc4d43cb8e1125e0ff3344e9de678fefd85db3b750b81b2240dc0183af37b35"},
{file = "retrying-1.3.4.tar.gz", hash = "sha256:345da8c5765bd982b1d1915deb9102fd3d1f7ad16bd84a9700b85f64d24e8f3e"},
]
[package.dependencies]
six = ">=1.7.0"
[[package]] [[package]]
name = "rich" name = "rich"
version = "13.7.1" version = "13.7.1"
@ -1728,6 +2014,32 @@ pygments = ">=2.13.0,<3.0.0"
[package.extras] [package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"] jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.4.10"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac"},
{file = "ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6"},
{file = "ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631"},
{file = "ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef"},
{file = "ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815"},
{file = "ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695"},
{file = "ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca"},
{file = "ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7"},
{file = "ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0"},
{file = "ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804"},
]
[[package]] [[package]]
name = "semver" name = "semver"
version = "3.0.2" version = "3.0.2"
@ -1739,6 +2051,22 @@ files = [
{file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"},
] ]
[[package]]
name = "setuptools"
version = "71.0.1"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "setuptools-71.0.1-py3-none-any.whl", hash = "sha256:1eb8ef012efae7f6acbc53ec0abde4bc6746c43087fd215ee09e1df48998711f"},
{file = "setuptools-71.0.1.tar.gz", hash = "sha256:c51d7fd29843aa18dad362d4b4ecd917022131425438251f4e3d766c964dd1ad"},
]
[package.extras]
core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (<7.4)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
[[package]] [[package]]
name = "shellingham" name = "shellingham"
version = "1.5.4" version = "1.5.4"
@ -1803,6 +2131,21 @@ files = [
[package.dependencies] [package.dependencies]
pbr = ">=2.0.0,<2.1.0 || >2.1.0" pbr = ">=2.0.0,<2.1.0 || >2.1.0"
[[package]]
name = "tenacity"
version = "8.5.0"
description = "Retry code until it succeeds"
optional = false
python-versions = ">=3.8"
files = [
{file = "tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687"},
{file = "tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78"},
]
[package.extras]
doc = ["reno", "sphinx"]
test = ["pytest", "tornado (>=4.5)", "typeguard"]
[[package]] [[package]]
name = "text-unidecode" name = "text-unidecode"
version = "1.3" version = "1.3"
@ -2186,6 +2529,23 @@ files = [
{file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"},
] ]
[[package]]
name = "werkzeug"
version = "3.0.3"
description = "The comprehensive WSGI web application library."
optional = false
python-versions = ">=3.8"
files = [
{file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"},
{file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"},
]
[package.dependencies]
MarkupSafe = ">=2.1.1"
[package.extras]
watchdog = ["watchdog (>=2.3)"]
[[package]] [[package]]
name = "wsproto" name = "wsproto"
version = "1.2.0" version = "1.2.0"
@ -2303,7 +2663,22 @@ files = [
idna = ">=2.0" idna = ">=2.0"
multidict = ">=4.0" multidict = ">=4.0"
[[package]]
name = "zipp"
version = "3.19.2"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.8"
files = [
{file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"},
{file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"},
]
[package.extras]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.11.6,<3.13" python-versions = ">=3.11.6,<3.13"
content-hash = "3dfc8d87dd19fe6222428891e73a1bc61edf045596fdecbc7897ecd1356d8b3d" content-hash = "60d3a08ec1ea70b53fe4e2f3ccc112c30e88a1908acf4af93a24e186f3addea8"

View File

@ -38,6 +38,10 @@ semver = "^3.0.2"
hypercorn = "^0.17.3" hypercorn = "^0.17.3"
flet = "^0.23.2" flet = "^0.23.2"
fastapi = "^0.111.1" fastapi = "^0.111.1"
setuptools = "^71.0.1"
dash = "^2.17.1"
dash-extensions = "^1.0.18"
dash-bootstrap-components = "^1.6.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
flake8 = "^7.1.0" flake8 = "^7.1.0"

View File

@ -1,6 +1,7 @@
import logging import logging
import os import os
import time
# import time
import traceback import traceback
@ -8,13 +9,13 @@ def setup_logger():
""" """
Настройка логирования Настройка логирования
""" """
current_date = time.strftime("%d-%m-%Y") # current_date = time.strftime("%d-%m-%Y")
log_dir = os.path.join(os.path.dirname(__file__), "log") log_dir = os.path.join(os.path.dirname(__file__), "log")
os.makedirs(log_dir, exist_ok=True) os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, f"log-{current_date}.log") # log_file = os.path.join(log_dir, f"log-{current_date}.log")
logging.basicConfig( logging.basicConfig(
filename=log_file, # filename=log_file,
level=logging.INFO, level=logging.INFO,
format="%(asctime)s %(message)s", format="%(asctime)s %(message)s",
datefmt="%H:%M:%S", datefmt="%H:%M:%S",

View File

@ -11,6 +11,8 @@ from ocab_core.logger import log, setup_logger
from ocab_core.modules_system import ModulesManager from ocab_core.modules_system import ModulesManager
from ocab_core.modules_system.loaders import FSLoader, UnsafeFSLoader from ocab_core.modules_system.loaders import FSLoader, UnsafeFSLoader
from ocab_core.singleton import Singleton from ocab_core.singleton import Singleton
# TODO: заменить на get_module("standard.config")
from ocab_modules.standard.config.config import get_telegram_token from ocab_modules.standard.config.config import get_telegram_token
ocab_modules_path = get_module_directory("ocab_modules") ocab_modules_path = get_module_directory("ocab_modules")
@ -26,16 +28,17 @@ def ocab_modules_loader(namespace: str, module_name: str, safe=True):
bot_modules = [ bot_modules = [
ocab_modules_loader("standard", "config", safe=False), ocab_modules_loader("standard", "config", safe=False),
ocab_modules_loader("standard", "database", safe=False), ocab_modules_loader("standard", "database", safe=False),
ocab_modules_loader("standard", "fsm_database_storage", safe=False), # ocab_modules_loader("standard", "fsm_database_storage", safe=False),
ocab_modules_loader("standard", "roles", safe=False), ocab_modules_loader("standard", "roles", safe=False),
ocab_modules_loader("external", "yandexgpt", safe=False), ocab_modules_loader("external", "yandexgpt", safe=False),
ocab_modules_loader("standard", "miniapp", safe=False), #
ocab_modules_loader("standard", "command_helper"), ocab_modules_loader("standard", "command_helper"),
ocab_modules_loader("standard", "info"), # ocab_modules_loader("standard", "info"),
ocab_modules_loader("standard", "filters"), # ocab_modules_loader("standard", "filters"),
ocab_modules_loader("external", "create_report_apps"), # ocab_modules_loader("external", "create_report_apps"),
ocab_modules_loader("standard", "admin"), # ocab_modules_loader("standard", "admin"),
ocab_modules_loader("standard", "message_processing"), ocab_modules_loader("standard", "message_processing"),
ocab_modules_loader("standard", "miniapp", safe=False),
] ]
@ -48,6 +51,9 @@ async def long_polling_mode():
async def webhook_mode(): async def webhook_mode():
singleton = Singleton() singleton = Singleton()
app = FastAPI() app = FastAPI()
app.mount("/webapp", singleton.storage["webapp"])
await register_bot_webhook(app, singleton.bot, singleton.dp) await register_bot_webhook(app, singleton.bot, singleton.dp)
await singleton.bot.set_webhook( await singleton.bot.set_webhook(
"https://mackerel-pumped-foal.ngrok-free.app/webhook" "https://mackerel-pumped-foal.ngrok-free.app/webhook"
@ -71,7 +77,6 @@ async def init_app():
await singleton.modules_manager.load(module_loader) await singleton.modules_manager.load(module_loader)
singleton.dp = Dispatcher(storage=singleton.storage["_fsm_storage"]) singleton.dp = Dispatcher(storage=singleton.storage["_fsm_storage"])
singleton.dp.include_routers(*singleton.storage["_routers"]) singleton.dp.include_routers(*singleton.storage["_routers"])
for middleware in singleton.storage["_outer_message_middlewares"]: for middleware in singleton.storage["_outer_message_middlewares"]:

View File

@ -1,9 +1,27 @@
import types import types
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, List, Optional, Union
from dataclasses_json import dataclass_json from dataclasses_json import dataclass_json
@dataclass_json
@dataclass
class DependencyInfo:
version: str
uses: Optional[List[str]] = None
DependencyType = Union[str, DependencyInfo]
@dataclass_json
@dataclass
class Dependencies:
required: Optional[Dict[str, DependencyType]] = None
optional: Optional[Dict[str, DependencyType]] = None
@dataclass_json @dataclass_json
@dataclass @dataclass
class ModuleInfo: class ModuleInfo:
@ -11,9 +29,10 @@ class ModuleInfo:
name: str name: str
description: str description: str
version: str version: str
author: str | list[str] author: Union[str, List[str]]
privileged: bool privileged: bool
dependencies: dict dependencies: Dependencies
pythonDependencies: Optional[Dependencies] = None
class AbstractLoader: class AbstractLoader:

View File

@ -3,6 +3,7 @@ from pathlib import Path
from RestrictedPython import compile_restricted_exec from RestrictedPython import compile_restricted_exec
# from ocab_core.logger import log
from ocab_core.modules_system.loaders.unsafe_fs_loader import UnsafeFSLoader from ocab_core.modules_system.loaders.unsafe_fs_loader import UnsafeFSLoader
from ocab_core.modules_system.safe.policy import ( from ocab_core.modules_system.safe.policy import (
ALLOWED_IMPORTS, ALLOWED_IMPORTS,
@ -16,13 +17,30 @@ class FSLoader(UnsafeFSLoader):
super().__init__(path) super().__init__(path)
self.builtins = BUILTINS.copy() self.builtins = BUILTINS.copy()
self.builtins["__import__"] = self._hook_import self.builtins["__import__"] = self._hook_import
self.module_info = self.info()
self.allowed_python_dependencies = self._get_allowed_python_dependencies()
def load(self): def load(self):
info = self.info() if self.module_info.privileged:
if info.privileged:
raise Exception("Only non privileged modules are allowed to be imported") raise Exception("Only non privileged modules are allowed to be imported")
self.module_id = self.module_info.id
return self._hook_import(".") return self._hook_import(".")
def _get_allowed_python_dependencies(self):
allowed = {}
if self.module_info.pythonDependencies:
if self.module_info.pythonDependencies.required:
allowed.update(self.module_info.pythonDependencies.required)
if self.module_info.pythonDependencies.optional:
allowed.update(self.module_info.pythonDependencies.optional)
for allowed_module in ALLOWED_IMPORTS:
allowed[allowed_module] = "*"
return allowed
def _resolve_module_from_path(self, module_name: str): def _resolve_module_from_path(self, module_name: str):
path = Path(self.path) path = Path(self.path)
@ -44,12 +62,14 @@ class FSLoader(UnsafeFSLoader):
return file_path return file_path
def _hook_import(self, name: str, *args, **kwargs): 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": if name == "ocab_core.modules_system.public_api":
return __import__(name, *args, **kwargs) module = __import__(name, *args, **kwargs)
module.__ocab_module_id__ = self.module_id
return module
for key in self.allowed_python_dependencies.keys():
if name == key or name.startswith(f"{key}."):
return __import__(name, *args, **kwargs)
module_file_path = self._resolve_module_from_path(name) module_file_path = self._resolve_module_from_path(name)
@ -58,9 +78,7 @@ class FSLoader(UnsafeFSLoader):
module = types.ModuleType(name) module = types.ModuleType(name)
module.__dict__.update( module.__dict__.update(
{ {"__builtins__": self.builtins, "__ocab_module_id__": self.module_id}
"__builtins__": self.builtins,
}
) )
result = compile_restricted_exec(src, "<string>", policy=RestrictedPythonPolicy) result = compile_restricted_exec(src, "<string>", policy=RestrictedPythonPolicy)

View File

@ -1,8 +1,13 @@
import inspect import inspect
import pkg_resources
import semver import semver
from ocab_core.modules_system.loaders.base import AbstractLoader from ocab_core.modules_system.loaders.base import (
AbstractLoader,
DependencyInfo,
ModuleInfo,
)
def is_version_compatible(version, requirement): def is_version_compatible(version, requirement):
@ -23,6 +28,46 @@ def is_version_compatible(version, requirement):
return True return True
def check_python_dependencies(info: ModuleInfo):
if info.pythonDependencies and info.pythonDependencies.required:
for dependency, req in info.pythonDependencies.required.items():
try:
installed_version = pkg_resources.get_distribution(dependency).version
except pkg_resources.DistributionNotFound:
raise Exception(
f"Module {info.id} requires {dependency},"
f"but it is not installed"
)
if isinstance(req, str):
required_version = req
elif isinstance(req, DependencyInfo):
required_version = req.version
else:
raise ValueError(f"Invalid dependency specification for {dependency}")
if not is_version_compatible(installed_version, required_version):
raise Exception(
f"Module {info.id} depends on {dependency} {required_version}, "
f"but version {installed_version} is installed"
)
def check_dependency_uses(
loaded_dependency, required_uses, dependent_module_id, dependency_id
):
module = loaded_dependency.get("module")
if not module:
raise Exception(f"Module object not found for dependency {dependency_id}")
for required_attr in required_uses:
if not hasattr(module, required_attr):
raise Exception(
f"Module {dependent_module_id} requires '{required_attr}' "
f"from {dependency_id}, but it is not available"
)
async def await_if_async(module, method_name): async def await_if_async(module, method_name):
if hasattr(module, method_name): if hasattr(module, method_name):
method = getattr(module, method_name) method = getattr(module, method_name)
@ -43,33 +88,54 @@ class ModulesManager:
if any(mod["info"].id == info.id for mod in self.modules): if any(mod["info"].id == info.id for mod in self.modules):
return return
# Check dependencies self.check_module_dependencies(info)
for dependency, version in info.dependencies.items(): check_python_dependencies(info)
loaded_dependency = next(
(mod for mod in self.modules if mod["info"].id == dependency), None
)
if not loaded_dependency:
raise Exception(
f"Module {info.id} depends on {dependency}, but it is not loaded"
)
loaded_dependency_info = loaded_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_info = {
"info": info,
"module": None,
}
self.modules.append(module_info)
module = loader.load() module = loader.load()
module_info["module"] = module
self.modules.append(
{
"info": info,
"module": module,
}
)
await await_if_async(module, "module_init") await await_if_async(module, "module_init")
def check_module_dependencies(self, info: ModuleInfo):
if info.dependencies.required:
for dependency, req in info.dependencies.required.items():
loaded_dependency = next(
(mod for mod in self.modules if mod["info"].id == dependency), None
)
if not loaded_dependency:
raise Exception(
f"Module {info.id} depends on {dependency},"
f"but it is not loaded"
)
loaded_dependency_info = loaded_dependency["info"]
if isinstance(req, str):
required_version = req
elif isinstance(req, DependencyInfo):
required_version = req.version
if req.uses:
check_dependency_uses(
loaded_dependency, req.uses, info.id, dependency
)
else:
raise ValueError(
f"Invalid dependency specification for {dependency}"
)
if not is_version_compatible(
loaded_dependency_info.version, required_version
):
raise Exception(
f"Module {info.id} depends on {dependency} {required_version}, "
f"but version {loaded_dependency_info.version} is loaded"
)
async def late_init(self): async def late_init(self):
for m in self.modules: for m in self.modules:
module = m["module"] module = m["module"]

View File

@ -6,6 +6,7 @@ from .public_api import (
get_module, get_module,
register_outer_message_middleware, register_outer_message_middleware,
register_router, register_router,
set_chat_menu_button,
set_my_commands, set_my_commands,
) )
from .utils import Utils from .utils import Utils

View File

@ -1,3 +1,4 @@
import inspect
import types import types
from typing import Any, Tuple, Union from typing import Any, Tuple, Union
@ -5,9 +6,16 @@ from aiogram import BaseMiddleware, Router
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.fsm.storage.base import StorageKey from aiogram.fsm.storage.base import StorageKey
# from ocab_core.logger import log
from ocab_core.modules_system.loaders.base import DependencyInfo
from ocab_core.singleton import Singleton from ocab_core.singleton import Singleton
async def set_chat_menu_button(menu_button):
app = Singleton()
await app.bot.set_chat_menu_button(menu_button=menu_button)
def register_router(router: Router): def register_router(router: Router):
app = Singleton() app = Singleton()
app.storage["_routers"].append(router) app.storage["_routers"].append(router)
@ -45,10 +53,41 @@ def set_fsm(storage):
def get_module( def get_module(
module_id: str, paths=None module_id: str, paths=None
) -> Union[types.ModuleType, Union[Any, None], Tuple[Union[Any, None], ...]]: ) -> Union[types.ModuleType, Union[Any, None], Tuple[Union[Any, None], ...]]:
caller_globals = inspect.currentframe().f_back.f_globals
app = Singleton() app = Singleton()
allowed_uses = None
if "__ocab_module_id__" in caller_globals:
caller_module_id = caller_globals["__ocab_module_id__"]
caller_module_info = app.modules_manager.get_info_by_id(caller_module_id)
if caller_module_info and caller_module_info.dependencies:
dependency = None
if caller_module_info.dependencies.required:
dependency = caller_module_info.dependencies.required.get(module_id)
if not dependency and caller_module_info.dependencies.optional:
dependency = caller_module_info.dependencies.optional.get(module_id)
if (
dependency
and isinstance(dependency, DependencyInfo)
and dependency.uses
):
allowed_uses = set(dependency.uses)
module = app.modules_manager.get_by_id(module_id) module = app.modules_manager.get_by_id(module_id)
if not module:
raise ModuleNotFoundError(f"Module {module_id} not found")
if paths is None: if paths is None:
if allowed_uses is not None:
raise PermissionError(
f"Direct access to module {module_id} is "
f"not allowed for {caller_module_id}. Specify allowed attributes."
)
return module return module
if isinstance(paths, str): if isinstance(paths, str):
@ -61,9 +100,16 @@ def get_module(
try: try:
parts = path.split(".") parts = path.split(".")
for part in parts: for part in parts:
if allowed_uses is not None and part not in allowed_uses:
raise AttributeError(
f"Access to '{part}' is not allowed "
+ f"for module {caller_module_id}"
)
current_obj = getattr(current_obj, part) current_obj = getattr(current_obj, part)
results.append(current_obj) results.append(current_obj)
except AttributeError: except AttributeError as e:
if "is not allowed" in str(e):
raise PermissionError(str(e))
results.append(None) results.append(None)
if len(results) == 1: if len(results) == 1:

View File

@ -1,6 +1,8 @@
import types
from _ast import AnnAssign from _ast import AnnAssign
from typing import Any from typing import Any
import flet as ft
from aiogram import Bot from aiogram import Bot
from RestrictedPython import ( from RestrictedPython import (
RestrictingNodeTransformer, RestrictingNodeTransformer,
@ -9,8 +11,8 @@ from RestrictedPython import (
utility_builtins, utility_builtins,
) )
from RestrictedPython.Eval import default_guarded_getitem, default_guarded_getiter from RestrictedPython.Eval import default_guarded_getitem, default_guarded_getiter
from RestrictedPython.Guards import ( from RestrictedPython.Guards import ( # guarded_setattr,; full_write_guard,
full_write_guard, _write_wrapper,
guarded_unpack_sequence, guarded_unpack_sequence,
safer_getattr, safer_getattr,
) )
@ -95,6 +97,39 @@ def safes_getattr(object, name, default=None, getattr=safer_getattr):
return getattr(object, name, default) return getattr(object, name, default)
trusted_settters_classes = [ft.Page, ft.View]
def safes_setattr(self, key, value):
if (
isinstance(getattr(type(self), key, None), property)
and getattr(type(self), key).fset is not None
):
getattr(type(self), key).fset(self, value)
return
def write_guard():
# ed scope abuse!
# safetypes and Wrapper variables are used by guard()
safetypes = {dict, list}
Wrapper = _write_wrapper()
def guard(ob):
# Don't bother wrapping simple types, or objects that claim to
# handle their own write security.
if type(ob) in safetypes or hasattr(ob, "_guarded_writes"):
return ob
if type(ob) in trusted_settters_classes:
setattr(ob, "__guarded_setattr__", types.MethodType(safes_setattr, ob))
# Hand the object to the Wrapper instance, then return the instance.
return Wrapper(ob)
return guard
BUILTINS = safe_builtins.copy() BUILTINS = safe_builtins.copy()
BUILTINS.update(utility_builtins) BUILTINS.update(utility_builtins)
BUILTINS.update(limited_builtins) BUILTINS.update(limited_builtins)
@ -103,7 +138,7 @@ BUILTINS["__metaclass__"] = _metaclass
BUILTINS["_getitem_"] = default_guarded_getitem BUILTINS["_getitem_"] = default_guarded_getitem
BUILTINS["_getattr_"] = safes_getattr BUILTINS["_getattr_"] = safes_getattr
BUILTINS["_getiter_"] = default_guarded_getiter BUILTINS["_getiter_"] = default_guarded_getiter
BUILTINS["_write_"] = full_write_guard BUILTINS["_write_"] = write_guard()
BUILTINS["_unpack_sequence_"] = guarded_unpack_sequence BUILTINS["_unpack_sequence_"] = guarded_unpack_sequence
BUILTINS["staticmethod"] = staticmethod BUILTINS["staticmethod"] = staticmethod
BUILTINS["tuple"] = tuple BUILTINS["tuple"] = tuple

View File

@ -6,6 +6,8 @@
"version": "1.0.0", "version": "1.0.0",
"privileged": false, "privileged": false,
"dependencies": { "dependencies": {
"standard.filters": "^1.0.0" "required": {
"standard.filters": "^1.0.0"
}
} }
} }

View File

@ -1,12 +1,14 @@
{ {
"id": "standard.command_helper", "id": "standard.command_helper",
"name": "Command helper", "name": "Command helper",
"description": "Модуль для отображения команд при вводе '/'", "description": "Модуль для отображения команд при вводе '/'",
"author": "OCAB Team", "author": "OCAB Team",
"version": "1.0.0", "version": "1.0.0",
"privileged": false, "privileged": false,
"dependencies": { "dependencies": {
"standard.roles": "^1.0.0", "required": {
"standard.database": "^1.0.0" "standard.roles": "^1.0.0",
"standard.database": "^1.0.0"
} }
}
} }

View File

@ -5,3 +5,4 @@ from .config import (
get_yandexgpt_in_words, get_yandexgpt_in_words,
get_yandexgpt_start_words, get_yandexgpt_start_words,
) )
from .main import module_late_init

View File

@ -1,22 +1,67 @@
# flake8: noqa # flake8: noqa
import yaml from .config_manager import ConfigManager
from src.service import paths config = ConfigManager(
config_path="/home/maxim/dev/alt-gnome-infrastructure/ocab/src/ocab_core/config.yaml"
)
def get_config(is_test: bool = False) -> dict: def register_settings(settings_manager: ConfigManager):
if is_test: # TELEGRAM settings
path = f"{paths.modules_standard}/config/tests" settings_manager.register_setting(
else: ["TELEGRAM", "TOKEN"], "", "string", is_private=True
path = paths.core )
path = f"{path}/config.yaml" settings_manager.register_setting(
["TELEGRAM", "APPROVED_CHAT_ID"],
"-123456789 | -012345678",
"string",
pretty_name="ID разрешенных чатов",
description='Чаты, в которых будет работать бот. "|" - разделитель',
)
settings_manager.register_setting(
["TELEGRAM", "ADMINCHATID"],
-12345678,
"number",
pretty_name="ID чата администраторов",
)
settings_manager.register_setting(
["TELEGRAM", "DEFAULT_CHAT_TAG"],
"@alt_gnome_chat",
"string",
pretty_name="Основной чат",
)
settings_manager.register_setting(["TELEGRAM", "CHECK_BOT"], True, "checkbox")
with open(path, "r") as file: # YANDEXGPT settings
return yaml.full_load(file) settings_manager.register_setting(
["YANDEXGPT", "TOKEN"], "", "string", is_private=True
)
settings_manager.register_setting(
["YANDEXGPT", "TOKEN_FOR_REQUEST"], 8000, "number"
)
settings_manager.register_setting(["YANDEXGPT", "TOKEN_FOR_ANSWER"], 2000, "number")
settings_manager.register_setting(
["YANDEXGPT", "CATALOGID"], "", "string", is_private=True
)
settings_manager.register_setting(
["YANDEXGPT", "PROMPT"], "Ты чат-бот ...", "string"
)
settings_manager.register_setting(
["YANDEXGPT", "STARTWORD"], "Бот| Бот, | бот | бот,", "string"
)
settings_manager.register_setting(
["YANDEXGPT", "INWORD"], "помогите | не работает", "string"
)
# ROLES settings
settings_manager.register_setting(["ROLES", "ADMIN"], 2, "number")
settings_manager.register_setting(["ROLES", "MODERATOR"], 1, "number")
settings_manager.register_setting(["ROLES", "USER"], 0, "number")
settings_manager.register_setting(["ROLES", "BOT"], 3, "number")
config = get_config() register_settings(config)
def get_telegram_token() -> str: def get_telegram_token() -> str:
@ -76,4 +121,4 @@ def get_yandexgpt_token_for_answer() -> int:
def get_access_rights() -> dict: def get_access_rights() -> dict:
return get_config()["ACCESS_RIGHTS"] return config["ACCESS_RIGHTS"]

View File

@ -0,0 +1,259 @@
from typing import Any, Dict, List, Optional, Union
import yaml
# from ocab_core.modules_system.public_api import get_module, log
try:
import dash_bootstrap_components as dbc
from dash_extensions.enrich import Input, Output, dcc, html
DASH_AVAILABLE = True
except ImportError:
DASH_AVAILABLE = False
class ConfigManager:
def __init__(self, config_path: str):
self._config_path = config_path
self._config = self.load_config()
self._registered_settings = dict()
self._registered_settings_meta = dict()
self._update_callbacks = []
self._update_required = False
@property
def config(self):
return self._config
@config.setter
def config(self, value):
self._config = value
def load_config(self) -> Dict[str, Any]:
with open(self._config_path, "r") as file:
return yaml.safe_load(file)
def save_config(self):
with open(self._config_path, "w") as file:
yaml.dump(self._config, file)
def register_setting(
self,
key: Union[str, List[str]],
default_value: Any,
setting_type: str,
is_private: bool = False,
pretty_name: str = None,
description: str = None,
options: Optional[List[str]] = None,
):
if isinstance(key, str):
key = [key]
current = self._registered_settings
for k in key[:-1]:
if k not in current:
current[k] = {}
current = current[k]
current[key[-1]] = self.get_nested_setting(self._config, key, default_value)
self.set_nested_setting(
self._registered_settings_meta,
key,
{
"type": setting_type,
"is_private": is_private,
"options": options,
"pretty_name": pretty_name,
"description": description,
},
)
def get_nested_setting(
self, config: dict, keys: List[str], default: Any = None
) -> Any:
current = config
for key in keys:
if key in current:
current = current[key]
else:
return default
return current
def set_nested_setting(self, config: dict, keys: List[str], value: Any):
current = config
for key in keys[:-1]:
if key not in current:
current[key] = {}
current = current[key]
current[keys[-1]] = value
def __getitem__(self, key):
if key in self._registered_settings:
return self._registered_settings[key]
raise KeyError(key)
def update_setting(self, key: Union[str, List[str]], value: Any):
if isinstance(key, str):
key = [key]
self.set_nested_setting(self._registered_settings, key, value)
self.set_nested_setting(self._config, key, value)
self.save_config()
def get_settings_layout(self):
from dash_extensions.enrich import DashBlueprint
bp = DashBlueprint()
def create_layout():
def create_nested_layout(settings: dict, key_list=None):
if key_list is None:
key_list = []
components = []
for key, value in settings.items():
current_key_list = key_list.copy()
current_key_list.append(key)
if isinstance(value, dict):
nested = create_nested_layout(value, current_key_list)
if len(nested) > 0:
components.append(
dbc.Card(
[
dbc.CardHeader(html.H3(key, className="mb-0")),
dbc.CardBody(nested),
],
className="mb-3",
)
)
else:
meta = self.get_nested_setting(
self._registered_settings_meta, current_key_list
)
if not meta.get("is_private"):
row = []
label_text = meta.get("pretty_name", key)
if meta.get("type") != "checkbox":
row.append(dbc.Label(label_text))
component_id = {
"type": "setting",
"key": "-".join(current_key_list),
}
if meta.get("type") == "string":
component = dbc.Input(
id=component_id, type="text", value=value
)
elif meta.get("type") == "number":
component = dbc.Input(
id=component_id, type="number", value=value
)
elif meta.get("type") == "checkbox":
component = dbc.Col(
[
dbc.Checkbox(
id=component_id,
value=value,
label=dbc.Label(
label_text,
style={"margin-right": "10px"},
check=True,
),
)
]
)
elif meta.get("type") == "select":
options = [
{"label": opt, "value": opt}
for opt in meta.get("options", [])
]
component = dcc.Dropdown(
id=component_id, options=options, value=value
)
else:
continue
row.append(component)
if meta.get("description"):
row.append(dbc.FormText(meta.get("description")))
components.append(dbc.Row(row, className="mb-3"))
return components
settings_components = create_nested_layout(self._registered_settings)
layout = html.Div(
[
html.H1("Настройки"),
dbc.Form(settings_components),
dbc.Button(
"Сохранить",
id="save-settings",
color="primary",
className="mt-3",
n_clicks=0,
),
html.Div(id="settings-update-trigger", style={"display": "none"}),
html.Span(
id="save-confirmation", style={"verticalAlign": "middle"}
),
dcc.Store(id="settings-store"),
],
style={
"padding": "20px",
},
)
return layout
bp.layout = create_layout
self.setup_callbacks(bp)
return bp
def setup_callbacks(self, app):
# ws = WebSocket(app, url="/ws")
@app.callback(
Output("save-confirmation", "children"),
# Output("settings-store", "data"),
Input("save-settings", "n_clicks"),
# State({"type": "setting", "key": ALL}, "value"),
# State({"type": "setting", "key": ALL}, "id"),
running=[(Output("save-settings", "disabled"), True, False)],
)
def save_settings(n_clicks, values, ids):
if n_clicks > 0:
# updated_settings = {}
# print(ids)
# for value, id_dict in zip(values, ids):
# key = id_dict["key"]
# self.update_setting(key.split("-"), value)
# updated_settings[key] = value
return "Настройки сохранены!" # , json.dumps(updated_settings)
return "" # , None
# @app.callback(
# Output({"type": "setting", "key": ALL}, "value"),
# Input("settings-store", "data"),
# )
# def update_settings_from_store(data):
# if data:
# updated_settings = json.loads(data)
# print(
# [current_value for key, current_value in updated_settings.items()]
# )
# return [
# current_value for key, current_value in updated_settings.items()
# ]
# raise dash.exceptions.PreventUpdate()

View File

@ -5,5 +5,14 @@
"author": "OCAB Team", "author": "OCAB Team",
"version": "1.0.0", "version": "1.0.0",
"privileged": true, "privileged": true,
"dependencies": {} "dependencies": {
"optional": {
"standard.miniapp": "^1.0.0"
}
},
"pythonDependencies": {
"optional": {
"flet": "^0.23.2"
}
}
} }

View File

@ -0,0 +1,35 @@
from ocab_core.modules_system.public_api import get_module, log
from .config import config
def register_settings_page():
try:
register_page = get_module("standard.miniapp", "register_page")
#
# def setup_callbacks_wrapper(config_manager):
# def setup(app):
# config_manager.setup_callbacks(app)
#
# return setup
#
# register_page(
# name="Настройки",
# path="/settings",
# layout=config.get_settings_layout(),
# setup_callbacks=setup_callbacks_wrapper(config),
# )
register_page(
name="Настройки", path="/settings", blueprint=config.get_settings_layout()
)
pass
except Exception as e:
log(str(e))
pass
def module_late_init():
register_settings_page()
pass

View File

@ -6,7 +6,9 @@
"version": "1.0.0", "version": "1.0.0",
"privileged": false, "privileged": false,
"dependencies": { "dependencies": {
"standard.roles": "^1.0.0", "required": {
"standard.config": "^1.0.0" "standard.roles": "^1.0.0",
"standard.config": "^1.0.0"
}
} }
} }

View File

@ -1,11 +1,13 @@
{ {
"id": "standard.fsm_database_storage", "id": "standard.fsm_database_storage",
"name": "FSM Database Storage", "name": "FSM Database Storage",
"description": "Очень полезный модуль", "description": "Очень полезный модуль",
"author": "OCAB Team", "author": "OCAB Team",
"version": "1.0.0", "version": "1.0.0",
"privileged": false, "privileged": false,
"dependencies": { "dependencies": {
"standard.database": "^1.0.0" "required": {
"standard.database": "^1.0.0"
} }
}
} }

View File

@ -6,8 +6,10 @@
"version": "1.0.0", "version": "1.0.0",
"privileged": false, "privileged": false,
"dependencies": { "dependencies": {
"standard.roles": "^1.0.0", "required": {
"standard.database": "^1.0.0", "standard.roles": "^1.0.0",
"standard.command_helper": "^1.0.0" "standard.database": "^1.0.0",
"standard.command_helper": "^1.0.0"
}
} }
} }

View File

@ -6,8 +6,10 @@
"version": "1.0.0", "version": "1.0.0",
"privileged": false, "privileged": false,
"dependencies": { "dependencies": {
"standard.roles": "^1.0.0", "required": {
"standard.database": "^1.0.0", "standard.roles": "^1.0.0",
"standard.command_helper": "^1.0.0" "standard.database": "^1.0.0",
"standard.command_helper": "^1.0.0"
}
} }
} }

View File

@ -1 +1,2 @@
from .main import module_init from .lib import register_page
from .main import module_init, module_late_init

View File

@ -5,5 +5,17 @@
"author": "OCAB Team", "author": "OCAB Team",
"version": "1.0.0", "version": "1.0.0",
"privileged": false, "privileged": false,
"dependencies": {} "dependencies": {
"required": {
"standard.config": {
"version": "^1.0.0",
"uses": []
}
}
},
"pythonDependencies": {
"required": {
"flet": "^0.23.2"
}
}
} }

View File

@ -0,0 +1,172 @@
from collections import OrderedDict
import dash
import dash_bootstrap_components as dbc
from dash_extensions.enrich import DashBlueprint, DashProxy, Input, Output, dcc, html
from dash_extensions.pages import setup_page_components
from flask import Flask
pages = OrderedDict()
def register_page(name, path, blueprint):
pages[path] = {
"name": name,
"blueprint": blueprint,
}
def register_home_page():
page = DashBlueprint()
page.layout = html.Div([html.H1("Главная")])
register_page("Главная", path="/", blueprint=page)
register_home_page()
def create_dash_app(requests_pathname_prefix: str = None) -> dash.Dash:
server = Flask(__name__)
app = DashProxy(
pages_folder="",
use_pages=True,
suppress_callback_exceptions=True,
external_stylesheets=[
dbc.themes.BOOTSTRAP,
dbc.icons.BOOTSTRAP,
],
external_scripts=[
"https://telegram.org/js/telegram-web-app.js"
], # Add Telegram Mini Apps script to <head>
server=server,
requests_pathname_prefix=requests_pathname_prefix,
meta_tags=[
{"name": "viewport", "content": "width=device-width, initial-scale=1"},
],
)
app.enable_dev_tools(
dev_tools_ui=True,
dev_tools_serve_dev_bundles=True,
)
# Register pages
for path, page in pages.items():
# dash.register_page(page["name"], path=path, layout=page["layout"])
page["blueprint"].register(app, path, prefix="a")
# Create sidebar
sidebar = dbc.Offcanvas(
id="offcanvas",
title="Меню",
is_open=False,
children=[
dbc.Nav(
[
dbc.NavLink(
page["name"],
href=f"{requests_pathname_prefix}{path.lstrip('/')}",
id={"type": "nav-link", "index": path},
)
for path, page in pages.items()
],
vertical=True,
pills=True,
),
],
)
# Create navbar
navbar = dbc.Navbar(
dbc.Container(
[
dbc.Button(
html.I(className="bi bi-list"),
id="open-offcanvas",
color="light",
className="me-2",
),
dbc.NavbarBrand("OCAB"),
]
),
color="primary",
dark=True,
)
# Define app layout
app.layout = html.Div(
[
dcc.Location(id="url", refresh=False),
dcc.Store(id="user-data", storage_type="session"),
dcc.Interval(
id="init-telegram-interval",
interval=100,
n_intervals=0,
max_intervals=1,
),
# WebSocket(url="/ws"),
html.Div(id="telegram-login-info"),
navbar,
sidebar,
dash.page_container,
setup_page_components(),
]
)
# Clientside callback to initialize Telegram Mini Apps and get user data
app.clientside_callback(
"""
function(n_intervals) {
return new Promise((resolve, reject) => {
resolve("test");
if (window.Telegram && window.Telegram.WebApp) {
const webapp = window.Telegram.WebApp;
webapp.ready();
if (webapp.initDataUnsafe && webapp.initDataUnsafe.user) {
resolve(webapp.initDataUnsafe.user);
} else {
reject("User not authorized");
}
} else {
reject("Telegram Mini Apps not available");
}
});
}
""",
Output("user-data", "data"),
Input("init-telegram-interval", "n_intervals"),
)
# Открытие на кнопку меню
app.clientside_callback(
"""
function(n_clicks) {
if (n_clicks == null) {
return false;
}
return true;
}
""",
Output(
"offcanvas",
"is_open",
),
Input("open-offcanvas", "n_clicks"),
)
# Закрываем offcanvas при клике на ссылку в меню
app.clientside_callback(
"""
function(n_clicks) {
if (n_clicks == null) {
return true;
}
return false;
}
""",
Output("offcanvas", "is_open", allow_duplicate=True),
Input({"type": "nav-link", "index": dash.dependencies.ALL}, "n_clicks"),
prevent_initial_call="initial_duplicate",
)
return app

View File

@ -1,2 +1,31 @@
from aiogram import types
from fastapi.middleware.wsgi import WSGIMiddleware
from ocab_core.modules_system.public_api import Storage, set_chat_menu_button
def get_link():
pass
def module_init(): def module_init():
pass pass
def register_page():
pass
async def module_late_init():
from .lib import create_dash_app
dash_app = create_dash_app(requests_pathname_prefix="/webapp/")
Storage.set("webapp", WSGIMiddleware(dash_app.server))
web_app_info = types.WebAppInfo(
url="https://mackerel-pumped-foal.ngrok-free.app/webapp"
)
menu_button = types.MenuButtonWebApp(text="Меню", web_app=web_app_info)
await set_chat_menu_button(menu_button)

View File

@ -6,7 +6,9 @@
"version": "1.0.0", "version": "1.0.0",
"privileged": true, "privileged": true,
"dependencies": { "dependencies": {
"standard.config": "^1.0.0", "required": {
"standard.database": "^1.0.0" "standard.config": "^1.0.0",
"standard.database": "^1.0.0"
}
} }
} }