This commit is contained in:
Kseninia Mikhaylova 2024-08-06 15:34:23 +03:00
parent 5f027e2066
commit a415ab31f4
14 changed files with 444 additions and 133 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
back/.venv/

View File

@ -4,19 +4,33 @@ FROM ci.svs-tech.pro/library/node:22-bookworm-slim
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONDONTWRITEBYTECODE 1
# Configure Poetry
ENV POETRY_VERSION=1.8.3
ENV POETRY_HOME=/opt/poetry
ENV POETRY_VENV=/opt/poetry-venv
ENV POETRY_CACHE_DIR=/opt/.cache
ENV WORKING_DIR=/app ENV WORKING_DIR=/app
WORKDIR ${WORKING_DIR} WORKDIR ${WORKING_DIR}
RUN apt-get update RUN apt-get update
RUN apt-get install curl -y RUN apt-get install curl -y
RUN apt-get install python3-pip -y RUN apt-get install python3-pip -y
RUN apt-get install python3-poetry -y RUN apt-get install python3-venv -y
ENV PATH="${PATH}:${POETRY_VENV}/bin"
# Install poetry separated from system interpreter
RUN python3 -m venv $POETRY_VENV \
&& $POETRY_VENV/bin/pip install -U pip setuptools \
&& $POETRY_VENV/bin/pip install poetry==${POETRY_VERSION}
RUN poetry -vvv --version RUN poetry -vvv --version
COPY pyproject.toml poetry.lock ./ COPY pyproject.toml ./
RUN poetry install RUN poetry install
COPY . ${WORKING_DIR} COPY . ${WORKING_DIR}
CMD ["poetry", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"] # CMD ["poetry", "run", "/app/.venv/bin/celery"]

13
back/api/celery.py Normal file
View File

@ -0,0 +1,13 @@
import os
from django.conf import settings
from celery import Celery
from datetime import timedelta
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings")
celery_app = Celery("api")
celery_app.config_from_object(f'django.conf:settings', namespace='CELERY')
celery_app.autodiscover_tasks()

View File

@ -118,6 +118,8 @@ DATABASES = {
} }
} }
CELERY_BROKER_URL = f'redis://{os.getenv("REDIS_HOST")}:6379/0'
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
# Password validation # Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators

264
back/poetry.lock generated
View File

@ -1,5 +1,19 @@
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
[[package]]
name = "amqp"
version = "5.2.0"
description = "Low-level AMQP client for Python (fork of amqplib)."
optional = false
python-versions = ">=3.6"
files = [
{file = "amqp-5.2.0-py3-none-any.whl", hash = "sha256:827cb12fb0baa892aad844fd95258143bce4027fdac4fccddbc43330fd281637"},
{file = "amqp-5.2.0.tar.gz", hash = "sha256:a1ecff425ad063ad42a486c902807d1482311481c8ad95a72694b2975e75f7fd"},
]
[package.dependencies]
vine = ">=5.0.0,<6.0.0"
[[package]] [[package]]
name = "anyio" name = "anyio"
version = "4.4.0" version = "4.4.0"
@ -62,6 +76,85 @@ files = [
[package.extras] [package.extras]
tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
[[package]]
name = "async-timeout"
version = "4.0.3"
description = "Timeout context manager for asyncio programs"
optional = false
python-versions = ">=3.7"
files = [
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
]
[[package]]
name = "billiard"
version = "4.2.0"
description = "Python multiprocessing fork with improvements and bugfixes"
optional = false
python-versions = ">=3.7"
files = [
{file = "billiard-4.2.0-py3-none-any.whl", hash = "sha256:07aa978b308f334ff8282bd4a746e681b3513db5c9a514cbdd810cbbdc19714d"},
{file = "billiard-4.2.0.tar.gz", hash = "sha256:9a3c3184cb275aa17a732f93f65b20c525d3d9f253722d26a82194803ade5a2c"},
]
[[package]]
name = "celery"
version = "5.4.0"
description = "Distributed Task Queue."
optional = false
python-versions = ">=3.8"
files = [
{file = "celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64"},
{file = "celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706"},
]
[package.dependencies]
billiard = ">=4.2.0,<5.0"
click = ">=8.1.2,<9.0"
click-didyoumean = ">=0.3.0"
click-plugins = ">=1.1.1"
click-repl = ">=0.2.0"
kombu = ">=5.3.4,<6.0"
python-dateutil = ">=2.8.2"
redis = {version = ">=4.5.2,<4.5.5 || >4.5.5,<6.0.0", optional = true, markers = "extra == \"redis\""}
tzdata = ">=2022.7"
vine = ">=5.1.0,<6.0"
[package.extras]
arangodb = ["pyArango (>=2.0.2)"]
auth = ["cryptography (==42.0.5)"]
azureblockblob = ["azure-storage-blob (>=12.15.0)"]
brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"]
cassandra = ["cassandra-driver (>=3.25.0,<4)"]
consul = ["python-consul2 (==0.1.5)"]
cosmosdbsql = ["pydocumentdb (==2.3.5)"]
couchbase = ["couchbase (>=3.0.0)"]
couchdb = ["pycouchdb (==1.14.2)"]
django = ["Django (>=2.2.28)"]
dynamodb = ["boto3 (>=1.26.143)"]
elasticsearch = ["elastic-transport (<=8.13.0)", "elasticsearch (<=8.13.0)"]
eventlet = ["eventlet (>=0.32.0)"]
gcs = ["google-cloud-storage (>=2.10.0)"]
gevent = ["gevent (>=1.5.0)"]
librabbitmq = ["librabbitmq (>=2.0.0)"]
memcache = ["pylibmc (==1.6.3)"]
mongodb = ["pymongo[srv] (>=4.0.2)"]
msgpack = ["msgpack (==1.0.8)"]
pymemcache = ["python-memcached (>=1.61)"]
pyro = ["pyro4 (==4.82)"]
pytest = ["pytest-celery[all] (>=1.0.0)"]
redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"]
s3 = ["boto3 (>=1.26.143)"]
slmq = ["softlayer-messaging (>=1.0.3)"]
solar = ["ephem (==4.1.5)"]
sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"]
sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"]
tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"]
yaml = ["PyYAML (>=3.10)"]
zookeeper = ["kazoo (>=1.3.1)"]
zstd = ["zstandard (==0.22.0)"]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2024.7.4" version = "2024.7.4"
@ -172,6 +265,69 @@ files = [
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
] ]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "click-didyoumean"
version = "0.3.1"
description = "Enables git-like *did-you-mean* feature in click"
optional = false
python-versions = ">=3.6.2"
files = [
{file = "click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c"},
{file = "click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463"},
]
[package.dependencies]
click = ">=7"
[[package]]
name = "click-plugins"
version = "1.1.1"
description = "An extension module for click to enable registering CLI commands via setuptools entry-points."
optional = false
python-versions = "*"
files = [
{file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"},
{file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"},
]
[package.dependencies]
click = ">=4.0"
[package.extras]
dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"]
[[package]]
name = "click-repl"
version = "0.3.0"
description = "REPL plugin for Click"
optional = false
python-versions = ">=3.6"
files = [
{file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"},
{file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"},
]
[package.dependencies]
click = ">=7.0"
prompt-toolkit = ">=3.0.36"
[package.extras]
testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"]
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
@ -220,13 +376,13 @@ django = ">=3.2"
[[package]] [[package]]
name = "django-filter" name = "django-filter"
version = "24.2" version = "24.3"
description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "django-filter-24.2.tar.gz", hash = "sha256:48e5fc1da3ccd6ca0d5f9bb550973518ce977a4edde9d2a8a154a7f4f0b9f96e"}, {file = "django_filter-24.3-py3-none-any.whl", hash = "sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64"},
{file = "django_filter-24.2-py3-none-any.whl", hash = "sha256:df2ee9857e18d38bed203c8745f62a803fa0f31688c9fe6f8e868120b1848e48"}, {file = "django_filter-24.3.tar.gz", hash = "sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3"},
] ]
[package.dependencies] [package.dependencies]
@ -341,6 +497,38 @@ files = [
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
] ]
[[package]]
name = "kombu"
version = "5.3.7"
description = "Messaging library for Python."
optional = false
python-versions = ">=3.8"
files = [
{file = "kombu-5.3.7-py3-none-any.whl", hash = "sha256:5634c511926309c7f9789f1433e9ed402616b56836ef9878f01bd59267b4c7a9"},
{file = "kombu-5.3.7.tar.gz", hash = "sha256:011c4cd9a355c14a1de8d35d257314a1d2456d52b7140388561acac3cf1a97bf"},
]
[package.dependencies]
amqp = ">=5.1.1,<6.0.0"
vine = "*"
[package.extras]
azureservicebus = ["azure-servicebus (>=7.10.0)"]
azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"]
confluentkafka = ["confluent-kafka (>=2.2.0)"]
consul = ["python-consul2"]
librabbitmq = ["librabbitmq (>=2.0.0)"]
mongodb = ["pymongo (>=4.1.1)"]
msgpack = ["msgpack"]
pyro = ["pyro4"]
qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"]
redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2)"]
slmq = ["softlayer-messaging (>=1.0.3)"]
sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"]
sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"]
yaml = ["PyYAML (>=3.10)"]
zookeeper = ["kazoo (>=2.8.0)"]
[[package]] [[package]]
name = "markdown" name = "markdown"
version = "3.6" version = "3.6"
@ -475,6 +663,20 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
typing = ["typing-extensions"] typing = ["typing-extensions"]
xmp = ["defusedxml"] xmp = ["defusedxml"]
[[package]]
name = "prompt-toolkit"
version = "3.0.47"
description = "Library for building powerful interactive command lines in Python"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"},
{file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"},
]
[package.dependencies]
wcwidth = "*"
[[package]] [[package]]
name = "psutil" name = "psutil"
version = "5.9.8" version = "5.9.8"
@ -584,6 +786,20 @@ files = [
{file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"},
] ]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
]
[package.dependencies]
six = ">=1.5"
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.0.1" version = "1.0.1"
@ -636,6 +852,24 @@ files = [
{file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"},
] ]
[[package]]
name = "redis"
version = "5.0.8"
description = "Python client for Redis database and key-value store"
optional = false
python-versions = ">=3.7"
files = [
{file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"},
{file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"},
]
[package.dependencies]
async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""}
[package.extras]
hiredis = ["hiredis (>1.0.0)"]
ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.3" version = "2.32.3"
@ -767,7 +1001,29 @@ h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"] zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "vine"
version = "5.1.0"
description = "Python promises."
optional = false
python-versions = ">=3.6"
files = [
{file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"},
{file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"},
]
[[package]]
name = "wcwidth"
version = "0.2.13"
description = "Measures the displayed width of unicode strings in a terminal"
optional = false
python-versions = "*"
files = [
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "e032592fbe04dbdaffbb40512f6dfa13bcd6f0673c9923e79ba0d852205e2c19" content-hash = "6362300a16177d23be8b5603dfd1ba3cf5ce477bb51962661c8bcf818433cf26"

View File

@ -16,9 +16,10 @@ pillow = "^10.3.0"
python-dotenv = "^1.0.1" python-dotenv = "^1.0.1"
requests = "^2.32.2" requests = "^2.32.2"
django-cors-headers = "^4.3.1" django-cors-headers = "^4.3.1"
python-telegram-bot = {extras = ["job-queue"], version = "^21.3"} python-telegram-bot = { extras = ["job-queue"], version = "^21.3" }
more-itertools = "^10.3.0" more-itertools = "^10.3.0"
djangoviz = "^0.1.1" djangoviz = "^0.1.1"
celery = { extras = ["redis"], version = "^5.4.0" }
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
taskipy = "^1.12.2" taskipy = "^1.12.2"

View File

@ -1,90 +1,20 @@
from django.apps import AppConfig from django.apps import AppConfig
import asyncio
import threading import threading
import queue
import os import os
class TgBotUpdater:
is_run = False
app = None
tgbot_class = None
my_queue = queue.Queue()
return_values = dict()
import logging
logger = logging.getLogger(__name__)
def __init__(self) -> None:
self.loop = asyncio.get_event_loop()
try:
from .tgbot import TgBot
self.tgbot_class = TgBot()
except Exception as e:
self.logger.error(e)
TgBotUpdater.is_run = True
async def _set_webhook(self):
await self.tgbot_class.set_webhook()
async def _start_app(self):
await self.tgbot_class.start_app()
async def _set_hadlers(self):
await self.tgbot_class.set_handlers()
async def _run_func(self):
from .tgbot import TgBot
while hasattr(TgBot, "app"):
# self.logger.info(f"check updates in {await TgBot.app.bot.get_webhook_info()}")
if not TgBotUpdater.my_queue.empty():
item = TgBotUpdater.my_queue.get()
if (
isinstance(item, dict)
and "name" in item
and item["name"].startswith("admin_")
):
await self.tgbot_class.admin_action(item["name"], item["queryset"])
else:
try:
await TgBot.app.process_update(item)
except Exception as e:
print(f"Error in tg thread {e}")
await TgBot.app.process_update(item)
TgBotUpdater.my_queue.task_done()
await asyncio.sleep(1)
async def main(self):
await asyncio.gather(
self._set_webhook(),
self._set_hadlers(),
self._start_app(),
self._run_func(),
)
def run_func(self):
asyncio.set_event_loop(self.loop)
self.loop.run_until_complete(self.main())
self.loop.close()
class TgbotConfig(AppConfig): class TgbotConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"
name = "tgbot" name = "tgbot"
def ready(self): def ready(self):
if not TgBotUpdater.is_run and os.environ.get("RUN_MAIN", None) == "true": from .updater import tg_bot_updater_instance
if not tg_bot_updater_instance.is_run and os.environ.get("RUN_MAIN", None) == "true":
threading.Thread( threading.Thread(
target=(TgBotUpdater().run_func), target=(tg_bot_updater_instance.run_func),
name="tg_updater_thread", name="tg_updater_thread",
daemon=True, daemon=True,
).start() ).start()
tg_bot_updater_instance.is_run = True
return super().ready() return super().ready()

View File

@ -29,7 +29,6 @@ from django.conf import settings
from django.urls import reverse from django.urls import reverse
from django.db import models from django.db import models
from .apps import TgBotUpdater
from .models import TgItem, TmcElement, TmcField from .models import TgItem, TmcElement, TmcField
from tmc.models import CustomTable, BaseCustomField, Territory, TerritoryItem from tmc.models import CustomTable, BaseCustomField, Territory, TerritoryItem
@ -65,20 +64,21 @@ class TgBot:
await TgBot.app.initialize() await TgBot.app.initialize()
async def admin_action(self, name, queryset): async def admin_action(self, name, queryset):
from .updater import tg_bot_updater_instance
if name == "admin_get_image": if name == "admin_get_image":
item = queryset item = queryset
try: try:
result = await TgBot.app.bot.get_file(item) result = await TgBot.app.bot.get_file(item)
TgBotUpdater.return_values[item] = result.file_path tg_bot_updater_instance.return_values[item] = result.file_path
except Exception as e: except Exception as e:
TgBotUpdater.return_values[item] = None tg_bot_updater_instance.return_values[item] = None
if name == "admin_get_name": if name == "admin_get_name":
item = queryset item = queryset
try: try:
result = await TgBot.app.bot.get_chat(item) result = await TgBot.app.bot.get_chat(item)
TgBotUpdater.return_values[item] = result.effective_name tg_bot_updater_instance.return_values[item] = result.effective_name
except Exception as e: except Exception as e:
TgBotUpdater.return_values[item] = None tg_bot_updater_instance.return_values[item] = None
async def set_handlers(self): async def set_handlers(self):
TgBot.app.add_handler( TgBot.app.add_handler(

71
back/tgbot/updater.py Normal file
View File

@ -0,0 +1,71 @@
import asyncio
import queue
from .tgbot import TgBot
class TgBotUpdater:
is_run = False
app = None
tgbot_class = None
my_queue = queue.Queue()
return_values = dict()
import logging
logger = logging.getLogger(__name__)
def __init__(self) -> None:
self.loop = asyncio.get_event_loop()
try:
self.tgbot_class = TgBot()
except Exception as e:
self.logger.error(e)
async def _set_webhook(self):
await self.tgbot_class.set_webhook()
async def _start_app(self):
await self.tgbot_class.start_app()
async def _set_hadlers(self):
await self.tgbot_class.set_handlers()
async def _run_func(self):
from .tgbot import TgBot
while hasattr(TgBot, "app"):
# self.logger.info(f"check updates in {await TgBot.app.bot.get_webhook_info()}")
if not self.my_queue.empty():
item = self.my_queue.get()
if (
isinstance(item, dict)
and "name" in item
and item["name"].startswith("admin_")
):
await self.tgbot_class.admin_action(item["name"], item["queryset"])
else:
try:
await TgBot.app.process_update(item)
except Exception as e:
print(f"Error in tg thread {e}")
await TgBot.app.process_update(item)
self.my_queue.task_done()
await asyncio.sleep(1)
async def main(self):
await asyncio.gather(
self._set_webhook(),
self._set_hadlers(),
self._start_app(),
self._run_func(),
)
def run_func(self):
asyncio.set_event_loop(self.loop)
self.loop.run_until_complete(self.main())
self.loop.close()
tg_bot_updater_instance = TgBotUpdater()

View File

@ -13,7 +13,7 @@ from rest_framework.decorators import action
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from .tgbot import TgBot from .tgbot import TgBot
from .apps import TgBotUpdater from .updater import tg_bot_updater_instance
from .models import TgItem, TmcField from .models import TgItem, TmcField
from .serializers import TgItemSerializer, TmcFieldSerializer from .serializers import TgItemSerializer, TmcFieldSerializer
@ -50,7 +50,7 @@ class TgItemViewSet(viewsets.ModelViewSet):
@action(detail=False, methods=["post"], url_path=settings.TGBOT["WEBHOOK_URL"]) @action(detail=False, methods=["post"], url_path=settings.TGBOT["WEBHOOK_URL"])
def send_tg_data(self, request): def send_tg_data(self, request):
TgBotUpdater.my_queue.put( tg_bot_updater_instance.my_queue.put(
Update.de_json(data=json.loads(request.body), bot=TgBot.app.bot) Update.de_json(data=json.loads(request.body), bot=TgBot.app.bot)
) )
return Response({"result": "ok"}) return Response({"result": "ok"})
@ -69,30 +69,30 @@ class TmcFieldViewset(viewsets.ModelViewSet):
@action(detail=False, methods=["get"], url_path=r"get_name/(?P<chat_id>[^/.]+)") @action(detail=False, methods=["get"], url_path=r"get_name/(?P<chat_id>[^/.]+)")
def get_name(self, request, chat_id): def get_name(self, request, chat_id):
TgBotUpdater.my_queue.put({"name": "admin_get_name", "queryset": chat_id}) tg_bot_updater_instance.my_queue.put({"name": "admin_get_name", "queryset": chat_id})
response = [] response = []
timer = 30 timer = 30
while timer > 0: while timer > 0:
sleeping = 1 sleeping = 1
timer -= sleeping timer -= sleeping
time.sleep(sleeping) time.sleep(sleeping)
if chat_id in TgBotUpdater.return_values: if chat_id in tg_bot_updater_instance.return_values:
response.append(TgBotUpdater.return_values[chat_id]) response.append(tg_bot_updater_instance.return_values[chat_id])
del TgBotUpdater.return_values[chat_id] del tg_bot_updater_instance.return_values[chat_id]
break break
return Response(response) return Response(response)
@action(detail=False, methods=["get"], url_path=r"get_image/(?P<field_id>[^/.]+)") @action(detail=False, methods=["get"], url_path=r"get_image/(?P<field_id>[^/.]+)")
def get_image(self, request, field_id): def get_image(self, request, field_id):
TgBotUpdater.my_queue.put({"name": "admin_get_image", "queryset": field_id}) tg_bot_updater_instance.my_queue.put({"name": "admin_get_image", "queryset": field_id})
response = [] response = []
timer = 30 timer = 30
while timer > 0: while timer > 0:
sleeping = 1 sleeping = 1
timer -= sleeping timer -= sleeping
time.sleep(sleeping) time.sleep(sleeping)
if field_id in TgBotUpdater.return_values: if field_id in tg_bot_updater_instance.return_values:
response.append(TgBotUpdater.return_values[field_id]) response.append(tg_bot_updater_instance.return_values[field_id])
del TgBotUpdater.return_values[field_id] del tg_bot_updater_instance.return_values[field_id]
break break
return Response(response) return Response(response)

View File

@ -1,28 +1,48 @@
services: services:
redis:
image: redis:7
restart: always
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli","ping"]
back: back:
build: build:
context: ./back context: ./back
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: toinv-back image: ci.svs-tech.pro/toinv_back:latest
command: poetry run python manage.py runserver 0.0.0.0:8000
restart: always restart: always
expose: expose:
- "8000" - "8000"
healthcheck:
test: curl -f http://localhost:8000 || exit 1
interval: 5s
timeout: 3s
retries: 20
volumes: volumes:
- ./env/back.env:/app/.env - ./env/back.env:/app/.env
networks: networks:
- toinv-network - toinv-network
healthcheck:
test: curl -f http://localhost:8000/ || exit 1
interval: 2s
timeout: 3s
retries: 20
celery:
build:
context: ./back
dockerfile: Dockerfile
image: ci.svs-tech.pro/toinv_back:latest image: ci.svs-tech.pro/toinv_back:latest
command: poetry run celery worker --loglevel=info
restart: always
volumes:
- ./env/back.env:/app/.env
depends_on:
back:
condition: service_healthy
front: front:
build: build:
context: ./front context: ./front
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: toinv-front
restart: always restart: always
volumes: volumes:
- ./env/front.env:/app/.env - ./env/front.env:/app/.env
@ -37,7 +57,6 @@ services:
nginx: nginx:
image: ci.svs-tech.pro/nginx:1.25 image: ci.svs-tech.pro/nginx:1.25
container_name: toinv-nginx
restart: always restart: always
ports: ports:
- "${WEB_PORT:-80}:80" - "${WEB_PORT:-80}:80"

View File

@ -1 +1,2 @@
WEB_PORT=80 WEB_PORT=80
REDIS_HOST=redis

View File

@ -1,2 +1,2 @@
NUXT_PUBLIC_API_BASE='http://localhost:8000/api' NUXT_PUBLIC_API_BASE='http://localhost/api'
NUXT_PUBLIC_TGBOT='svstech_inventory_bot' NUXT_PUBLIC_TGBOT='svstech_inventory_bot'

View File

@ -51,30 +51,33 @@ const patchItem = async () => {
</script> </script>
<template> <template>
<UForm :state="state" v-if="item"> <UForm :state="state" v-if="item">
<UFormGroup label="Название"> <div class="grid grid-cols-4 gap-2 items-end w-1/2">
<UInput v-model="state.name" /> <UFormGroup label="Название" class="col-span-3">
</UFormGroup> <UInput v-model="state.name" />
<UButton @click="patchItem">Сохранить</UButton> </UFormGroup>
<UFormGroup label="Локация"> <UButton @click="patchItem">Сохранить</UButton>
<USelectMenu v-model="state.location" :options="terdeep" :searchable="search" value-attribute="id" <UFormGroup label="Локация" class="col-span-3">
option-attribute="name" /> <USelectMenu v-model="state.location" :options="terdeep" :searchable="search" value-attribute="id"
</UFormGroup> option-attribute="name" />
<UFormGroup label="ТМЦ" v-if="state.tmc"> </UFormGroup>
<div v-for="el in state.tmc"> <UFormGroup label="ТМЦ" v-if="state.tmc" class="col-span-4">
<strong>{{ el.name }}</strong> <div v-for="el in state.tmc" class="ml-4">
<ul> <strong>{{ el.name }}</strong>
<li v-for="field in el.fields" class="grid grid-cols-3 gap-4"> <ul>
<span class="col-span-1 flex flex-col gap-2 items-start"> <li v-for="field in el.fields" class="grid grid-cols-3 gap-4">
{{ field.name }} <span class="col-span-1 flex flex-col gap-2 items-start">
<UInput v-model="field.text" :placeholder="`Введите ${field.comment || 'с изображения'}`" /> {{ field.name }}
<UButton @click="patchField(field)">Сохранить</UButton> <UInput v-model="field.text"
</span> :placeholder="`Введите ${field.comment || 'с изображения'}`" />
<span class="col-span-2"> <UButton @click="patchField(field)">Сохранить</UButton>
<GetImage :file_id="field.file_id" type="img" /> </span>
</span> <span class="col-span-2">
</li> <GetImage :file_id="field.file_id" type="img" />
</ul> </span>
</div> </li>
</UFormGroup> </ul>
</div>
</UFormGroup>
</div>
</UForm> </UForm>
</template> </template>