From a415ab31f49df85e8896f3acccf709276824a472 Mon Sep 17 00:00:00 2001 From: Kseninia Mikhaylova Date: Tue, 6 Aug 2024 15:34:23 +0300 Subject: [PATCH] celery --- .dockerignore | 1 + back/Dockerfile | 20 ++- back/api/celery.py | 13 ++ back/api/settings.py | 2 + back/poetry.lock | 264 ++++++++++++++++++++++++++++++++++++- back/pyproject.toml | 5 +- back/tgbot/apps.py | 78 +---------- back/tgbot/tgbot.py | 10 +- back/tgbot/updater.py | 71 ++++++++++ back/tgbot/views.py | 20 +-- docker-compose.yml | 35 +++-- env/docker.env.example | 3 +- env/front.env.example | 2 +- front/pages/table/[id].vue | 53 ++++---- 14 files changed, 444 insertions(+), 133 deletions(-) create mode 100644 .dockerignore create mode 100644 back/api/celery.py create mode 100644 back/tgbot/updater.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..57a6851 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +back/.venv/ \ No newline at end of file diff --git a/back/Dockerfile b/back/Dockerfile index 66b967b..148efd3 100644 --- a/back/Dockerfile +++ b/back/Dockerfile @@ -4,19 +4,33 @@ FROM ci.svs-tech.pro/library/node:22-bookworm-slim ENV PYTHONUNBUFFERED 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 WORKDIR ${WORKING_DIR} RUN apt-get update RUN apt-get install curl -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 -COPY pyproject.toml poetry.lock ./ +COPY pyproject.toml ./ RUN poetry install + COPY . ${WORKING_DIR} -CMD ["poetry", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"] +# CMD ["poetry", "run", "/app/.venv/bin/celery"] diff --git a/back/api/celery.py b/back/api/celery.py new file mode 100644 index 0000000..680a2c1 --- /dev/null +++ b/back/api/celery.py @@ -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() \ No newline at end of file diff --git a/back/api/settings.py b/back/api/settings.py index 0f78bf0..45a15c7 100644 --- a/back/api/settings.py +++ b/back/api/settings.py @@ -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 # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators diff --git a/back/poetry.lock b/back/poetry.lock index ec9d46e..bcab18b 100644 --- a/back/poetry.lock +++ b/back/poetry.lock @@ -1,5 +1,19 @@ # 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]] name = "anyio" version = "4.4.0" @@ -62,6 +76,85 @@ files = [ [package.extras] 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]] name = "certifi" version = "2024.7.4" @@ -172,6 +265,69 @@ files = [ {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]] name = "colorama" version = "0.4.6" @@ -220,13 +376,13 @@ django = ">=3.2" [[package]] name = "django-filter" -version = "24.2" +version = "24.3" description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." optional = false python-versions = ">=3.8" files = [ - {file = "django-filter-24.2.tar.gz", hash = "sha256:48e5fc1da3ccd6ca0d5f9bb550973518ce977a4edde9d2a8a154a7f4f0b9f96e"}, - {file = "django_filter-24.2-py3-none-any.whl", hash = "sha256:df2ee9857e18d38bed203c8745f62a803fa0f31688c9fe6f8e868120b1848e48"}, + {file = "django_filter-24.3-py3-none-any.whl", hash = "sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64"}, + {file = "django_filter-24.3.tar.gz", hash = "sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3"}, ] [package.dependencies] @@ -341,6 +497,38 @@ files = [ {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]] name = "markdown" version = "3.6" @@ -475,6 +663,20 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa typing = ["typing-extensions"] 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]] name = "psutil" version = "5.9.8" @@ -584,6 +786,20 @@ files = [ {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]] name = "python-dotenv" version = "1.0.1" @@ -636,6 +852,24 @@ files = [ {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]] name = "requests" version = "2.32.3" @@ -767,7 +1001,29 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.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] lock-version = "2.0" python-versions = "^3.11" -content-hash = "e032592fbe04dbdaffbb40512f6dfa13bcd6f0673c9923e79ba0d852205e2c19" +content-hash = "6362300a16177d23be8b5603dfd1ba3cf5ce477bb51962661c8bcf818433cf26" diff --git a/back/pyproject.toml b/back/pyproject.toml index f539f8f..814637c 100644 --- a/back/pyproject.toml +++ b/back/pyproject.toml @@ -16,9 +16,10 @@ pillow = "^10.3.0" python-dotenv = "^1.0.1" requests = "^2.32.2" 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" djangoviz = "^0.1.1" +celery = { extras = ["redis"], version = "^5.4.0" } [tool.poetry.group.dev.dependencies] taskipy = "^1.12.2" @@ -29,4 +30,4 @@ build-backend = "poetry.core.masonry.api" [tool.taskipy.tasks] migrate = "python manage.py makemigrations && python manage.py migrate" -server = "python manage.py runserver 0.0.0.0:8000" \ No newline at end of file +server = "python manage.py runserver 0.0.0.0:8000" diff --git a/back/tgbot/apps.py b/back/tgbot/apps.py index e190ee7..b7f9574 100644 --- a/back/tgbot/apps.py +++ b/back/tgbot/apps.py @@ -1,90 +1,20 @@ from django.apps import AppConfig -import asyncio import threading -import queue 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): default_auto_field = "django.db.models.BigAutoField" name = "tgbot" 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( - target=(TgBotUpdater().run_func), + target=(tg_bot_updater_instance.run_func), name="tg_updater_thread", daemon=True, ).start() + tg_bot_updater_instance.is_run = True return super().ready() diff --git a/back/tgbot/tgbot.py b/back/tgbot/tgbot.py index 1862ba8..cc81325 100644 --- a/back/tgbot/tgbot.py +++ b/back/tgbot/tgbot.py @@ -29,7 +29,6 @@ from django.conf import settings from django.urls import reverse from django.db import models -from .apps import TgBotUpdater from .models import TgItem, TmcElement, TmcField from tmc.models import CustomTable, BaseCustomField, Territory, TerritoryItem @@ -65,20 +64,21 @@ class TgBot: await TgBot.app.initialize() async def admin_action(self, name, queryset): + from .updater import tg_bot_updater_instance if name == "admin_get_image": item = queryset try: 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: - TgBotUpdater.return_values[item] = None + tg_bot_updater_instance.return_values[item] = None if name == "admin_get_name": item = queryset try: 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: - TgBotUpdater.return_values[item] = None + tg_bot_updater_instance.return_values[item] = None async def set_handlers(self): TgBot.app.add_handler( diff --git a/back/tgbot/updater.py b/back/tgbot/updater.py new file mode 100644 index 0000000..05ece58 --- /dev/null +++ b/back/tgbot/updater.py @@ -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() \ No newline at end of file diff --git a/back/tgbot/views.py b/back/tgbot/views.py index 36187b6..89758e4 100644 --- a/back/tgbot/views.py +++ b/back/tgbot/views.py @@ -13,7 +13,7 @@ from rest_framework.decorators import action from django_filters.rest_framework import DjangoFilterBackend from .tgbot import TgBot -from .apps import TgBotUpdater +from .updater import tg_bot_updater_instance from .models import TgItem, TmcField from .serializers import TgItemSerializer, TmcFieldSerializer @@ -50,7 +50,7 @@ class TgItemViewSet(viewsets.ModelViewSet): @action(detail=False, methods=["post"], url_path=settings.TGBOT["WEBHOOK_URL"]) 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) ) return Response({"result": "ok"}) @@ -69,30 +69,30 @@ class TmcFieldViewset(viewsets.ModelViewSet): @action(detail=False, methods=["get"], url_path=r"get_name/(?P[^/.]+)") 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 = [] timer = 30 while timer > 0: sleeping = 1 timer -= sleeping time.sleep(sleeping) - if chat_id in TgBotUpdater.return_values: - response.append(TgBotUpdater.return_values[chat_id]) - del TgBotUpdater.return_values[chat_id] + if chat_id in tg_bot_updater_instance.return_values: + response.append(tg_bot_updater_instance.return_values[chat_id]) + del tg_bot_updater_instance.return_values[chat_id] break return Response(response) @action(detail=False, methods=["get"], url_path=r"get_image/(?P[^/.]+)") 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 = [] timer = 30 while timer > 0: sleeping = 1 timer -= sleeping time.sleep(sleeping) - if field_id in TgBotUpdater.return_values: - response.append(TgBotUpdater.return_values[field_id]) - del TgBotUpdater.return_values[field_id] + if field_id in tg_bot_updater_instance.return_values: + response.append(tg_bot_updater_instance.return_values[field_id]) + del tg_bot_updater_instance.return_values[field_id] break return Response(response) diff --git a/docker-compose.yml b/docker-compose.yml index b581b4d..0f45ace 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,28 +1,48 @@ services: + redis: + image: redis:7 + restart: always + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli","ping"] + back: build: context: ./back 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 expose: - "8000" - healthcheck: - test: curl -f http://localhost:8000 || exit 1 - interval: 5s - timeout: 3s - retries: 20 volumes: - ./env/back.env:/app/.env networks: - 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 + command: poetry run celery worker --loglevel=info + restart: always + volumes: + - ./env/back.env:/app/.env + depends_on: + back: + condition: service_healthy front: build: context: ./front dockerfile: Dockerfile - container_name: toinv-front restart: always volumes: - ./env/front.env:/app/.env @@ -37,7 +57,6 @@ services: nginx: image: ci.svs-tech.pro/nginx:1.25 - container_name: toinv-nginx restart: always ports: - "${WEB_PORT:-80}:80" diff --git a/env/docker.env.example b/env/docker.env.example index 0d4f2fd..d09cadd 100644 --- a/env/docker.env.example +++ b/env/docker.env.example @@ -1 +1,2 @@ -WEB_PORT=80 \ No newline at end of file +WEB_PORT=80 +REDIS_HOST=redis \ No newline at end of file diff --git a/env/front.env.example b/env/front.env.example index 2007927..9dd55ef 100644 --- a/env/front.env.example +++ b/env/front.env.example @@ -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' \ No newline at end of file diff --git a/front/pages/table/[id].vue b/front/pages/table/[id].vue index 3bd5598..382839a 100644 --- a/front/pages/table/[id].vue +++ b/front/pages/table/[id].vue @@ -51,30 +51,33 @@ const patchItem = async () => { \ No newline at end of file