From 29aca4e7e9d3feddba8ca37214837eff051f2ff7 Mon Sep 17 00:00:00 2001 From: "Andrey K. Choi" Date: Tue, 12 Aug 2025 21:02:23 +0900 Subject: [PATCH] init commit --- .drone.yml | 40 +++++ .env | 13 ++ .gitignore | 176 ------------------- .history/.env_20250812203631 | 13 ++ .history/.env_20250812203658 | 13 ++ .history/.env_20250812203723 | 13 ++ .history/.env_20250812203740 | 13 ++ .history/.env_20250812204515 | 13 ++ .history/.env_20250812204538 | 13 ++ .history/.env_20250812204549 | 13 ++ .history/.gitignore_20250812203631 | 13 ++ .history/.gitignore_20250812205206 | 14 ++ .history/.gitignore_20250812205412 | 15 ++ .history/.gitignore_20250812205847 | 0 .history/.gitignore_20250812205903 | 191 +++++++++++++++++++++ .history/.gitignore_20250812205906 | 191 +++++++++++++++++++++ .history/.gitignore_20250812205947 | 14 ++ .history/.gitignore_20250812205949 | 14 ++ .history/.gitignore_20250812205953 | 0 .history/alembic_20250812205533.env | 0 .history/alembic_20250812205541.env | 108 ++++++++++++ .history/docker-compose_20250812203631.yml | 55 ++++++ .history/docker-compose_20250812203841.yml | 55 ++++++ .history/docker-compose_20250812204453.yml | 57 ++++++ .history/docker-compose_20250812204732.yml | 60 +++++++ .history/docker-compose_20250812204816.yml | 60 +++++++ .history/docker-compose_20250812205020.yml | 67 ++++++++ Makefile | 29 ++++ docker-compose.yml | 67 ++++++++ services/bot/Dockerfile | 11 ++ services/bot/alembic.ini | 3 + services/bot/alembic/env.py | 39 +++++ services/bot/alembic/versions/0001_init.py | 59 +++++++ services/bot/app/core/config.py | 5 + services/bot/app/main.py | 14 ++ services/bot/entrypoint.sh | 26 +++ services/bot/requirements.txt | 7 + 37 files changed, 1318 insertions(+), 176 deletions(-) create mode 100644 .drone.yml create mode 100644 .env create mode 100644 .history/.env_20250812203631 create mode 100644 .history/.env_20250812203658 create mode 100644 .history/.env_20250812203723 create mode 100644 .history/.env_20250812203740 create mode 100644 .history/.env_20250812204515 create mode 100644 .history/.env_20250812204538 create mode 100644 .history/.env_20250812204549 create mode 100644 .history/.gitignore_20250812203631 create mode 100644 .history/.gitignore_20250812205206 create mode 100644 .history/.gitignore_20250812205412 create mode 100644 .history/.gitignore_20250812205847 create mode 100644 .history/.gitignore_20250812205903 create mode 100644 .history/.gitignore_20250812205906 create mode 100644 .history/.gitignore_20250812205947 create mode 100644 .history/.gitignore_20250812205949 create mode 100644 .history/.gitignore_20250812205953 create mode 100644 .history/alembic_20250812205533.env create mode 100644 .history/alembic_20250812205541.env create mode 100644 .history/docker-compose_20250812203631.yml create mode 100644 .history/docker-compose_20250812203841.yml create mode 100644 .history/docker-compose_20250812204453.yml create mode 100644 .history/docker-compose_20250812204732.yml create mode 100644 .history/docker-compose_20250812204816.yml create mode 100644 .history/docker-compose_20250812205020.yml create mode 100644 Makefile create mode 100644 docker-compose.yml create mode 100644 services/bot/Dockerfile create mode 100644 services/bot/alembic.ini create mode 100644 services/bot/alembic/env.py create mode 100644 services/bot/alembic/versions/0001_init.py create mode 100644 services/bot/app/core/config.py create mode 100644 services/bot/app/main.py create mode 100755 services/bot/entrypoint.sh create mode 100644 services/bot/requirements.txt diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..64e2716 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,40 @@ +kind: pipeline +type: docker +name: build_test_local + +steps: + - name: build + image: docker:27 + volumes: + - name: dockersock + path: /var/run + commands: + - docker compose -f docker-compose.yml build --pull + + - name: migrate + image: docker:27 + volumes: + - name: dockersock + path: /var/run + commands: + - docker compose up -d db + - | + for i in $(seq 1 60); do + if docker compose ps db | grep -q "(healthy)"; then + echo "DB healthy"; break + fi + echo "Waiting DB... $i" + sleep 2 + done + - docker compose run --rm bot alembic upgrade head + + - name: lint + image: python:3.12-slim + commands: + - pip install ruff==0.5.7 + - ruff check services/bot + +volumes: + - name: dockersock + host: + path: /var/run/docker.sock diff --git a/.env b/.env new file mode 100644 index 0000000..2191eb0 --- /dev/null +++ b/.env @@ -0,0 +1,13 @@ +COMPOSE_PROJECT_NAME=soulmate_bot_v1 +TZ=Asia/Seoul + +MYSQL_DATABASE=seoulmate +MYSQL_USER=match_admin +MYSQL_PASSWORD=3uHU66gnRloyQ2zQ9L516R4DPSiwUzJyCD9h5zCVcuBykJlLm3 +MYSQL_ROOT_PASSWORD=eiKVznWb13FZMCoXvlFRIfozzP752qWkMTEE7kzlus3vSUdPtg + +DATABASE_URL=mysql+pymysql://match_user:match_password_change_me@db:3306/match_agency?charset=utf8mb4 + +BOT_TOKEN=7839684534:AAGAUmO756P1T81q8ImndWgIBU9OsZkY06s +PRIMARY_ADMIN_TELEGRAM_ID=556399210 +PYTHONUNBUFFERED=1 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 36b13f1..e69de29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,176 +0,0 @@ -# ---> Python -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc - diff --git a/.history/.env_20250812203631 b/.history/.env_20250812203631 new file mode 100644 index 0000000..dfc1656 --- /dev/null +++ b/.history/.env_20250812203631 @@ -0,0 +1,13 @@ +COMPOSE_PROJECT_NAME=korea-marriage-bot +TZ=Asia/Seoul + +MYSQL_DATABASE=match_agency +MYSQL_USER=match_user +MYSQL_PASSWORD=match_password_change_me +MYSQL_ROOT_PASSWORD=root_password_change_me + +DATABASE_URL=mysql+pymysql://match_user:match_password_change_me@db:3306/match_agency?charset=utf8mb4 + +BOT_TOKEN=replace_with_your_bot_token +PRIMARY_ADMIN_TELEGRAM_ID=123456789 +PYTHONUNBUFFERED=1 diff --git a/.history/.env_20250812203658 b/.history/.env_20250812203658 new file mode 100644 index 0000000..1e2860d --- /dev/null +++ b/.history/.env_20250812203658 @@ -0,0 +1,13 @@ +COMPOSE_PROJECT_NAME=korea-marriage-bot +TZ=Asia/Seoul + +MYSQL_DATABASE=match_agency +MYSQL_USER=match_user +MYSQL_PASSWORD=match_password_change_me +MYSQL_ROOT_PASSWORD=root_password_change_me + +DATABASE_URL=mysql+pymysql://match_user:match_password_change_me@db:3306/match_agency?charset=utf8mb4 + +BOT_TOKEN=replace_with_your_bot_token +PRIMARY_ADMIN_TELEGRAM_ID=556399210 +PYTHONUNBUFFERED=1 diff --git a/.history/.env_20250812203723 b/.history/.env_20250812203723 new file mode 100644 index 0000000..f02eee9 --- /dev/null +++ b/.history/.env_20250812203723 @@ -0,0 +1,13 @@ +COMPOSE_PROJECT_NAME=korea-marriage-bot +TZ=Asia/Seoul + +MYSQL_DATABASE=match_agency +MYSQL_USER=match_user +MYSQL_PASSWORD=match_password_change_me +MYSQL_ROOT_PASSWORD=root_password_change_me + +DATABASE_URL=mysql+pymysql://match_user:match_password_change_me@db:3306/match_agency?charset=utf8mb4 + +BOT_TOKEN=7839684534:AAGAUmO756P1T81q8ImndWgIBU9OsZkY06s +PRIMARY_ADMIN_TELEGRAM_ID=556399210 +PYTHONUNBUFFERED=1 \ No newline at end of file diff --git a/.history/.env_20250812203740 b/.history/.env_20250812203740 new file mode 100644 index 0000000..e013be9 --- /dev/null +++ b/.history/.env_20250812203740 @@ -0,0 +1,13 @@ +COMPOSE_PROJECT_NAME=soulmate_bot_v1 +TZ=Asia/Seoul + +MYSQL_DATABASE=match_agency +MYSQL_USER=match_user +MYSQL_PASSWORD=match_password_change_me +MYSQL_ROOT_PASSWORD=root_password_change_me + +DATABASE_URL=mysql+pymysql://match_user:match_password_change_me@db:3306/match_agency?charset=utf8mb4 + +BOT_TOKEN=7839684534:AAGAUmO756P1T81q8ImndWgIBU9OsZkY06s +PRIMARY_ADMIN_TELEGRAM_ID=556399210 +PYTHONUNBUFFERED=1 \ No newline at end of file diff --git a/.history/.env_20250812204515 b/.history/.env_20250812204515 new file mode 100644 index 0000000..195a2f5 --- /dev/null +++ b/.history/.env_20250812204515 @@ -0,0 +1,13 @@ +COMPOSE_PROJECT_NAME=soulmate_bot_v1 +TZ=Asia/Seoul + +MYSQL_DATABASE=seoulmate +MYSQL_USER=match_admin +MYSQL_PASSWORD=match_password_change_ +MYSQL_ROOT_PASSWORD=root_password_change_me + +DATABASE_URL=mysql+pymysql://match_user:match_password_change_me@db:3306/match_agency?charset=utf8mb4 + +BOT_TOKEN=7839684534:AAGAUmO756P1T81q8ImndWgIBU9OsZkY06s +PRIMARY_ADMIN_TELEGRAM_ID=556399210 +PYTHONUNBUFFERED=1 \ No newline at end of file diff --git a/.history/.env_20250812204538 b/.history/.env_20250812204538 new file mode 100644 index 0000000..d1eb0bc --- /dev/null +++ b/.history/.env_20250812204538 @@ -0,0 +1,13 @@ +COMPOSE_PROJECT_NAME=soulmate_bot_v1 +TZ=Asia/Seoul + +MYSQL_DATABASE=seoulmate +MYSQL_USER=match_admin +MYSQL_PASSWORD=3uHU66gnRloyQ2zQ9L516R4DPSiwUzJyCD9h5zCVcuBykJlLm3 +MYSQL_ROOT_PASSWORD=root_password_change_me + +DATABASE_URL=mysql+pymysql://match_user:match_password_change_me@db:3306/match_agency?charset=utf8mb4 + +BOT_TOKEN=7839684534:AAGAUmO756P1T81q8ImndWgIBU9OsZkY06s +PRIMARY_ADMIN_TELEGRAM_ID=556399210 +PYTHONUNBUFFERED=1 \ No newline at end of file diff --git a/.history/.env_20250812204549 b/.history/.env_20250812204549 new file mode 100644 index 0000000..2191eb0 --- /dev/null +++ b/.history/.env_20250812204549 @@ -0,0 +1,13 @@ +COMPOSE_PROJECT_NAME=soulmate_bot_v1 +TZ=Asia/Seoul + +MYSQL_DATABASE=seoulmate +MYSQL_USER=match_admin +MYSQL_PASSWORD=3uHU66gnRloyQ2zQ9L516R4DPSiwUzJyCD9h5zCVcuBykJlLm3 +MYSQL_ROOT_PASSWORD=eiKVznWb13FZMCoXvlFRIfozzP752qWkMTEE7kzlus3vSUdPtg + +DATABASE_URL=mysql+pymysql://match_user:match_password_change_me@db:3306/match_agency?charset=utf8mb4 + +BOT_TOKEN=7839684534:AAGAUmO756P1T81q8ImndWgIBU9OsZkY06s +PRIMARY_ADMIN_TELEGRAM_ID=556399210 +PYTHONUNBUFFERED=1 \ No newline at end of file diff --git a/.history/.gitignore_20250812203631 b/.history/.gitignore_20250812203631 new file mode 100644 index 0000000..7880c87 --- /dev/null +++ b/.history/.gitignore_20250812203631 @@ -0,0 +1,13 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +.env.local +node_modules/ +*.log +docker-data/ +volumes/ +alembic/*.pyc +alembic/versions/__pycache__/ +.DS_Store +Thumbs.db diff --git a/.history/.gitignore_20250812205206 b/.history/.gitignore_20250812205206 new file mode 100644 index 0000000..399c642 --- /dev/null +++ b/.history/.gitignore_20250812205206 @@ -0,0 +1,14 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +.env.local +.env +node_modules/ +*.log +docker-data/ +volumes/ +alembic/*.pyc +alembic/versions/__pycache__/ +.DS_Store +Thumbs.db diff --git a/.history/.gitignore_20250812205412 b/.history/.gitignore_20250812205412 new file mode 100644 index 0000000..1c7ea28 --- /dev/null +++ b/.history/.gitignore_20250812205412 @@ -0,0 +1,15 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +.env.local +.env +node_modules/ +*.log +docker-data/ +volumes/ +alembic/*.pyc +alembic/versions/__pycache__/ +.DS_Store +Thumbs.db +.history \ No newline at end of file diff --git a/.history/.gitignore_20250812205847 b/.history/.gitignore_20250812205847 new file mode 100644 index 0000000..e69de29 diff --git a/.history/.gitignore_20250812205903 b/.history/.gitignore_20250812205903 new file mode 100644 index 0000000..2652216 --- /dev/null +++ b/.history/.gitignore_20250812205903 @@ -0,0 +1,191 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + + +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +.env.local +.env +node_modules/ +*.log +docker-data/ +volumes/ +alembic/*.pyc +alembic/versions/__pycache__/ +.DS_Store +Thumbs.db diff --git a/.history/.gitignore_20250812205906 b/.history/.gitignore_20250812205906 new file mode 100644 index 0000000..2652216 --- /dev/null +++ b/.history/.gitignore_20250812205906 @@ -0,0 +1,191 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + + +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +.env.local +.env +node_modules/ +*.log +docker-data/ +volumes/ +alembic/*.pyc +alembic/versions/__pycache__/ +.DS_Store +Thumbs.db diff --git a/.history/.gitignore_20250812205947 b/.history/.gitignore_20250812205947 new file mode 100644 index 0000000..399c642 --- /dev/null +++ b/.history/.gitignore_20250812205947 @@ -0,0 +1,14 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +.env.local +.env +node_modules/ +*.log +docker-data/ +volumes/ +alembic/*.pyc +alembic/versions/__pycache__/ +.DS_Store +Thumbs.db diff --git a/.history/.gitignore_20250812205949 b/.history/.gitignore_20250812205949 new file mode 100644 index 0000000..399c642 --- /dev/null +++ b/.history/.gitignore_20250812205949 @@ -0,0 +1,14 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +.env.local +.env +node_modules/ +*.log +docker-data/ +volumes/ +alembic/*.pyc +alembic/versions/__pycache__/ +.DS_Store +Thumbs.db diff --git a/.history/.gitignore_20250812205953 b/.history/.gitignore_20250812205953 new file mode 100644 index 0000000..e69de29 diff --git a/.history/alembic_20250812205533.env b/.history/alembic_20250812205533.env new file mode 100644 index 0000000..e69de29 diff --git a/.history/alembic_20250812205541.env b/.history/alembic_20250812205541.env new file mode 100644 index 0000000..5663479 --- /dev/null +++ b/.history/alembic_20250812205541.env @@ -0,0 +1,108 @@ +# 1) Создаём структуру Alembic +mkdir -p services/bot/alembic/versions + +# 2) env.py +cat > services/bot/alembic/env.py <<'PY' +from __future__ import annotations +import os +from logging.config import fileConfig +from sqlalchemy import engine_from_config, pool +from alembic import context + +config = context.config +# Подменяем URL из переменных окружения контейнера +if os.getenv("DATABASE_URL"): + config.set_main_option("sqlalchemy.url", os.getenv("DATABASE_URL")) + +fileConfig(config.config_file_name) + +# Метаданные моделей +from app.models.base import Base # noqa: E402 +target_metadata = Base.metadata + +def run_migrations_offline(): + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url, target_metadata=target_metadata, literal_binds=True, + dialect_opts={"paramstyle": "named"}) + with context.begin_transaction(): + context.run_migrations() + +def run_migrations_online(): + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() +PY + +# 3) Первая миграция +cat > services/bot/alembic/versions/0001_init.py <<'PY' +from alembic import op +import sqlalchemy as sa + +revision = '0001_init' +down_revision = None +branch_labels = None +depends_on = None + +def upgrade(): + op.create_table( + 'admins', + sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True), + sa.Column('telegram_id', sa.Integer(), unique=True, index=True), + sa.Column('full_name', sa.String(length=120), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), + ) + + op.create_table( + 'candidates', + sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True), + sa.Column('telegram_id', sa.Integer(), unique=True, index=True), + sa.Column('username', sa.String(length=100), nullable=True), + sa.Column('full_name', sa.String(length=120), nullable=True), + sa.Column('gender', sa.String(length=20), nullable=True), + sa.Column('birth_date', sa.Date(), nullable=True), + sa.Column('height_cm', sa.Float(), nullable=True), + sa.Column('weight_kg', sa.Float(), nullable=True), + sa.Column('country', sa.String(length=80), nullable=True), + sa.Column('city', sa.String(length=120), nullable=True), + sa.Column('citizenship', sa.String(length=80), nullable=True), + sa.Column('visa_status', sa.String(length=60), nullable=True), + sa.Column('languages', sa.String(length=200), nullable=True), + sa.Column('education', sa.String(length=120), nullable=True), + sa.Column('occupation', sa.String(length=120), nullable=True), + sa.Column('income_range', sa.String(length=60), nullable=True), + sa.Column('religion', sa.String(length=80), nullable=True), + sa.Column('marital_status', sa.String(length=60), nullable=True), + sa.Column('has_children', sa.Boolean(), nullable=True), + sa.Column('children_notes', sa.String(length=200), nullable=True), + sa.Column('smoking', sa.String(length=20), nullable=True), + sa.Column('alcohol', sa.String(length=20), nullable=True), + sa.Column('health_notes', sa.String(length=300), nullable=True), + sa.Column('hobbies_tags', sa.String(length=300), nullable=True), + sa.Column('hobbies_free', sa.Text(), nullable=True), + sa.Column('goal', sa.String(length=120), nullable=True), + sa.Column('partner_prefs', sa.Text(), nullable=True), + sa.Column('avatar_file_id', sa.String(length=200), nullable=True), + sa.Column('gallery_file_ids', sa.Text(), nullable=True), + sa.Column('consent_personal', sa.Boolean(), nullable=False, server_default=sa.text('0')), + sa.Column('consent_policy', sa.Boolean(), nullable=False, server_default=sa.text('0')), + sa.Column('is_verified', sa.Boolean(), nullable=False, server_default=sa.text('0')), + sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('1')), + sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), + sa.Column('updated_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')), + ) + +def downgrade(): + op.drop_table('candidates') + op.drop_table('admins') +PY diff --git a/.history/docker-compose_20250812203631.yml b/.history/docker-compose_20250812203631.yml new file mode 100644 index 0000000..84ffa5a --- /dev/null +++ b/.history/docker-compose_20250812203631.yml @@ -0,0 +1,55 @@ +services: + db: + image: mariadb:11.6 + restart: unless-stopped + environment: + TZ: ${TZ} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u2462276MYSQL_USER -p2462276MYSQL_PASSWORD --silent"] + interval: 5s + timeout: 5s + retries: 20 + volumes: + - db_data:/var/lib/mysql + networks: + - appnet + + bot: + build: + context: ./services/bot + dockerfile: Dockerfile + args: + - PY_VERSION=3.12 + depends_on: + db: + condition: service_healthy + env_file: + - ./.env + volumes: + - ./services/bot:/app + command: ["/app/entrypoint.sh"] + networks: + - appnet + + adminer: + image: adminer:latest + ports: + - "8081:8080" + environment: + TZ: ${TZ} + depends_on: + db: + condition: service_healthy + networks: + - appnet + +volumes: + db_data: + +networks: + appnet: + driver: bridge diff --git a/.history/docker-compose_20250812203841.yml b/.history/docker-compose_20250812203841.yml new file mode 100644 index 0000000..cdd1128 --- /dev/null +++ b/.history/docker-compose_20250812203841.yml @@ -0,0 +1,55 @@ +services: + db: + image: mariadb:11.6 + restart: unless-stopped + environment: + TZ: ${TZ} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u2462276MYSQL_USER -p2462276MYSQL_PASSWORD --silent"] + interval: 5s + timeout: 5s + retries: 20 + volumes: + - db_data:/var/lib/mysql + networks: + - appnet + + bot: + build: + context: ./services/bot + dockerfile: Dockerfile + args: + - PY_VERSION=3.12 + depends_on: + db: + condition: service_healthy + env_file: + - ./.env + volumes: + - ./services/bot:/app + command: ["/app/entrypoint.sh"] + networks: + - appnet + + adminer: + image: adminer:latest + ports: + - "8081:8080" + environment: + TZ: ${TZ} + depends_on: + db: + condition: service_healthy + networks: + - appnet + +volumes: + db_data: + +networks: + appnet: + driver: bridge diff --git a/.history/docker-compose_20250812204453.yml b/.history/docker-compose_20250812204453.yml new file mode 100644 index 0000000..09a8ef4 --- /dev/null +++ b/.history/docker-compose_20250812204453.yml @@ -0,0 +1,57 @@ +services: + db: + image: mariadb:11.6 + restart: unless-stopped + environment: + TZ: ${TZ} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$MYSQL_ROOT_PASSWORD --silent"] + interval: 5s + timeout: 5s + retries: 20 + start_period: 40s + + volumes: + - db_data:/var/lib/mysql + networks: + - appnet + + bot: + build: + context: ./services/bot + dockerfile: Dockerfile + args: + - PY_VERSION=3.12 + depends_on: + db: + condition: service_healthy + env_file: + - ./.env + volumes: + - ./services/bot:/app + command: ["/app/entrypoint.sh"] + networks: + - appnet + + adminer: + image: adminer:latest + ports: + - "8081:8080" + environment: + TZ: ${TZ} + depends_on: + db: + condition: service_healthy + networks: + - appnet + +volumes: + db_data: + +networks: + appnet: + driver: bridge diff --git a/.history/docker-compose_20250812204732.yml b/.history/docker-compose_20250812204732.yml new file mode 100644 index 0000000..90d4ea5 --- /dev/null +++ b/.history/docker-compose_20250812204732.yml @@ -0,0 +1,60 @@ +services: + db: + image: mariadb:11.6 + restart: unless-stopped + environment: + TZ: ${TZ} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$MYSQL_ROOT_PASSWORD --silent"] + interval: 5s + timeout: 5s + retries: 20 + start_period: 40s + command: [ + "--character-set-server=utf8mb4", + "--collation-server=utf8mb4_unicode_ci" + ] + volumes: + - db_data:/var/lib/mysql + networks: + - appnet + + bot: + build: + context: ./services/bot + dockerfile: Dockerfile + args: + - PY_VERSION=3.12 + depends_on: + db: + condition: service_healthy + env_file: + - ./.env + volumes: + - ./services/bot:/app + command: ["/app/entrypoint.sh"] + networks: + - appnet + + adminer: + image: adminer:latest + ports: + - "8081:8080" + environment: + TZ: ${TZ} + depends_on: + db: + condition: service_healthy + networks: + - appnet + +volumes: + db_data: + +networks: + appnet: + driver: bridge diff --git a/.history/docker-compose_20250812204816.yml b/.history/docker-compose_20250812204816.yml new file mode 100644 index 0000000..c16f2a7 --- /dev/null +++ b/.history/docker-compose_20250812204816.yml @@ -0,0 +1,60 @@ +services: + db: + image: mariadb:11.6 + restart: unless-stopped + environment: + TZ: ${TZ} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$MYSQL_ROOT_PASSWORD --silent"] + interval: 5s + timeout: 5s + retries: 20 + start_period: 40s + command: [ + "--character-set-server=utf8mb4", + "--collation-server=utf8mb4_unicode_ci" + ] + volumes: + - db_data:/var/lib/mysql + networks: + - appnet + + bot: + build: + context: ./services/bot + dockerfile: Dockerfile + args: + - PY_VERSION=3.12 + depends_on: + db: + condition: service_healthy + env_file: + - ./.env + volumes: + - ./services/bot:/app + command: ["/app/entrypoint.sh"] + networks: + - appnet + + adminer: + image: adminer:latest + ports: + - "8081:8080" + environment: + TZ: ${TZ} + depends_on: + db: + condition: service_healthy + networks: + - appnet + +volumes: + db_data: + +networks: + appnet: + driver: bridge diff --git a/.history/docker-compose_20250812205020.yml b/.history/docker-compose_20250812205020.yml new file mode 100644 index 0000000..7271dc8 --- /dev/null +++ b/.history/docker-compose_20250812205020.yml @@ -0,0 +1,67 @@ +services: + db: + image: mariadb:11.6 + restart: unless-stopped + environment: + TZ: ${TZ} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + healthcheck: + test: + [ + "CMD-SHELL", + "([ -x /usr/bin/mariadb-admin ] && mariadb-admin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent) || \ + ([ -x /usr/bin/mysqladmin ] && mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent) || \ + (command -v bash >/dev/null 2>&1 && bash -c '/dev/null 2>&1)" + ] + interval: 5s + timeout: 5s + retries: 20 + start_period: 40s + + command: [ + "--character-set-server=utf8mb4", + "--collation-server=utf8mb4_unicode_ci" + ] + volumes: + - db_data:/var/lib/mysql + networks: + - appnet + + bot: + build: + context: ./services/bot + dockerfile: Dockerfile + args: + - PY_VERSION=3.12 + depends_on: + db: + condition: service_healthy + env_file: + - ./.env + volumes: + - ./services/bot:/app + command: ["/app/entrypoint.sh"] + networks: + - appnet + + adminer: + image: adminer:latest + ports: + - "8081:8080" + environment: + TZ: ${TZ} + depends_on: + db: + condition: service_healthy + networks: + - appnet + +volumes: + db_data: + +networks: + appnet: + driver: bridge diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7122318 --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +SHELL := /bin/bash + +.PHONY: up down logs build stop restart botsh dbsh migrate + +up: + docker compose up -d + +down: + docker compose down + +build: + docker compose build --pull + +logs: + docker compose logs -f --tail=200 + +stop: + docker compose stop + +restart: down up + +botsh: + docker compose exec bot bash || true + +dbsh: + docker compose exec db bash || true + +migrate: + docker compose run --rm bot alembic upgrade head diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7271dc8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,67 @@ +services: + db: + image: mariadb:11.6 + restart: unless-stopped + environment: + TZ: ${TZ} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + healthcheck: + test: + [ + "CMD-SHELL", + "([ -x /usr/bin/mariadb-admin ] && mariadb-admin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent) || \ + ([ -x /usr/bin/mysqladmin ] && mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent) || \ + (command -v bash >/dev/null 2>&1 && bash -c '/dev/null 2>&1)" + ] + interval: 5s + timeout: 5s + retries: 20 + start_period: 40s + + command: [ + "--character-set-server=utf8mb4", + "--collation-server=utf8mb4_unicode_ci" + ] + volumes: + - db_data:/var/lib/mysql + networks: + - appnet + + bot: + build: + context: ./services/bot + dockerfile: Dockerfile + args: + - PY_VERSION=3.12 + depends_on: + db: + condition: service_healthy + env_file: + - ./.env + volumes: + - ./services/bot:/app + command: ["/app/entrypoint.sh"] + networks: + - appnet + + adminer: + image: adminer:latest + ports: + - "8081:8080" + environment: + TZ: ${TZ} + depends_on: + db: + condition: service_healthy + networks: + - appnet + +volumes: + db_data: + +networks: + appnet: + driver: bridge diff --git a/services/bot/Dockerfile b/services/bot/Dockerfile new file mode 100644 index 0000000..c2e8cf6 --- /dev/null +++ b/services/bot/Dockerfile @@ -0,0 +1,11 @@ +ARG PY_VERSION=3.12 +FROM python:${PY_VERSION}-slim +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 +WORKDIR /app +RUN apt-get update && apt-get install -y --no-install-recommends tzdata \ + && rm -rf /var/lib/apt/lists/* +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +RUN chmod +x entrypoint.sh +CMD ["./entrypoint.sh"] diff --git a/services/bot/alembic.ini b/services/bot/alembic.ini new file mode 100644 index 0000000..f8de3ff --- /dev/null +++ b/services/bot/alembic.ini @@ -0,0 +1,3 @@ +[alembic] +script_location = alembic +sqlalchemy.url = ${DATABASE_URL} diff --git a/services/bot/alembic/env.py b/services/bot/alembic/env.py new file mode 100644 index 0000000..a24cd53 --- /dev/null +++ b/services/bot/alembic/env.py @@ -0,0 +1,39 @@ +from __future__ import annotations +import os +from logging.config import fileConfig +from sqlalchemy import engine_from_config, pool +from alembic import context + +config = context.config +# Подменяем URL из переменных окружения контейнера +if os.getenv("DATABASE_URL"): + config.set_main_option("sqlalchemy.url", os.getenv("DATABASE_URL")) + +fileConfig(config.config_file_name) + +# Метаданные моделей +from app.models.base import Base # noqa: E402 +target_metadata = Base.metadata + +def run_migrations_offline(): + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url, target_metadata=target_metadata, literal_binds=True, + dialect_opts={"paramstyle": "named"}) + with context.begin_transaction(): + context.run_migrations() + +def run_migrations_online(): + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/services/bot/alembic/versions/0001_init.py b/services/bot/alembic/versions/0001_init.py new file mode 100644 index 0000000..6749f49 --- /dev/null +++ b/services/bot/alembic/versions/0001_init.py @@ -0,0 +1,59 @@ +from alembic import op +import sqlalchemy as sa + +revision = '0001_init' +down_revision = None +branch_labels = None +depends_on = None + +def upgrade(): + op.create_table( + 'admins', + sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True), + sa.Column('telegram_id', sa.Integer(), unique=True, index=True), + sa.Column('full_name', sa.String(length=120), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), + ) + + op.create_table( + 'candidates', + sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True), + sa.Column('telegram_id', sa.Integer(), unique=True, index=True), + sa.Column('username', sa.String(length=100), nullable=True), + sa.Column('full_name', sa.String(length=120), nullable=True), + sa.Column('gender', sa.String(length=20), nullable=True), + sa.Column('birth_date', sa.Date(), nullable=True), + sa.Column('height_cm', sa.Float(), nullable=True), + sa.Column('weight_kg', sa.Float(), nullable=True), + sa.Column('country', sa.String(length=80), nullable=True), + sa.Column('city', sa.String(length=120), nullable=True), + sa.Column('citizenship', sa.String(length=80), nullable=True), + sa.Column('visa_status', sa.String(length=60), nullable=True), + sa.Column('languages', sa.String(length=200), nullable=True), + sa.Column('education', sa.String(length=120), nullable=True), + sa.Column('occupation', sa.String(length=120), nullable=True), + sa.Column('income_range', sa.String(length=60), nullable=True), + sa.Column('religion', sa.String(length=80), nullable=True), + sa.Column('marital_status', sa.String(length=60), nullable=True), + sa.Column('has_children', sa.Boolean(), nullable=True), + sa.Column('children_notes', sa.String(length=200), nullable=True), + sa.Column('smoking', sa.String(length=20), nullable=True), + sa.Column('alcohol', sa.String(length=20), nullable=True), + sa.Column('health_notes', sa.String(length=300), nullable=True), + sa.Column('hobbies_tags', sa.String(length=300), nullable=True), + sa.Column('hobbies_free', sa.Text(), nullable=True), + sa.Column('goal', sa.String(length=120), nullable=True), + sa.Column('partner_prefs', sa.Text(), nullable=True), + sa.Column('avatar_file_id', sa.String(length=200), nullable=True), + sa.Column('gallery_file_ids', sa.Text(), nullable=True), + sa.Column('consent_personal', sa.Boolean(), nullable=False, server_default=sa.text('0')), + sa.Column('consent_policy', sa.Boolean(), nullable=False, server_default=sa.text('0')), + sa.Column('is_verified', sa.Boolean(), nullable=False, server_default=sa.text('0')), + sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('1')), + sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), + sa.Column('updated_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')), + ) + +def downgrade(): + op.drop_table('candidates') + op.drop_table('admins') diff --git a/services/bot/app/core/config.py b/services/bot/app/core/config.py new file mode 100644 index 0000000..eb69437 --- /dev/null +++ b/services/bot/app/core/config.py @@ -0,0 +1,5 @@ +import os +class Settings: + BOT_TOKEN = os.getenv("BOT_TOKEN", "") + DATABASE_URL = os.getenv("DATABASE_URL", "") +settings = Settings() diff --git a/services/bot/app/main.py b/services/bot/app/main.py new file mode 100644 index 0000000..7817e20 --- /dev/null +++ b/services/bot/app/main.py @@ -0,0 +1,14 @@ +import asyncio +from telegram.ext import Application, CommandHandler +from app.core.config import settings +async def start(update, context): + await update.message.reply_text("Привет! Бот запущен.") +async def main(): + app = Application.builder().token(settings.BOT_TOKEN).build() + app.add_handler(CommandHandler("start", start)) + await app.initialize() + await app.start() + await app.updater.start_polling() + await app.updater.wait_until_closed() +if __name__ == "__main__": + asyncio.run(main()) diff --git a/services/bot/entrypoint.sh b/services/bot/entrypoint.sh new file mode 100755 index 0000000..015cc69 --- /dev/null +++ b/services/bot/entrypoint.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail +if [ -f "/app/.env" ]; then + set -a; source /app/.env; set +a +fi +echo "Waiting for DB..." +python - <<'PY' +import os, time, pymysql +url = os.environ["DATABASE_URL"] +u = url.split("://",1)[1].split("@",1) +auth, host_db = u[0], u[1] +host, port_db = host_db.split("/",1)[0], host_db.split("/",1)[1] +host, port = host.split(":")[0], int(host.split(":")[1] or 3306) +user, password = auth.split(":")[0], ":".join(auth.split(":")[1:]) +db = port_db.split("?")[0] +for i in range(60): + try: + conn = pymysql.connect(host=host, port=port, user=user, password=password, database=db) + conn.close() + break + except Exception as e: + print("Waiting DB...", e) + time.sleep(2) +PY +alembic upgrade head +exec python -m app.main diff --git a/services/bot/requirements.txt b/services/bot/requirements.txt new file mode 100644 index 0000000..112dc83 --- /dev/null +++ b/services/bot/requirements.txt @@ -0,0 +1,7 @@ +python-telegram-bot==21.6 +SQLAlchemy==2.0.30 +alembic==1.13.2 +pymysql==1.1.1 +python-dotenv==1.0.1 +tenacity==9.0.0 +ruff==0.5.7