From 3c0f96b6c173de91406fc4c32ffa513e36da6282 Mon Sep 17 00:00:00 2001 From: Sergey Natalenko Date: Wed, 22 Nov 2023 15:23:32 +0300 Subject: [PATCH] Update workflows --- .github/workflows/check.yml | 30 +++-------- .github/workflows/release.yml | 42 +++++++++++++++ Makefile | 15 ++++-- docker-compose.yaml | 1 + docker/pre-start.sh | 2 +- .../bot/dialogs/admins/url/update/__init__.py | 2 - .../dialogs/users/registration/__init__.py | 2 - inclusive_dance_bot/db/__main__.py | 34 ++++++++++++ inclusive_dance_bot/{ => db}/alembic.ini | 2 +- inclusive_dance_bot/db/base.py | 25 ++++++++- inclusive_dance_bot/db/factory.py | 20 ------- inclusive_dance_bot/db/migrations/env.py | 3 -- inclusive_dance_bot/db/mixins.py | 24 --------- inclusive_dance_bot/db/models.py | 3 +- inclusive_dance_bot/db/utils.py | 53 +++++++++++++++++++ inclusive_dance_bot/deps.py | 2 +- inclusive_dance_bot/dto.py | 1 - inclusive_dance_bot/init_data.py | 2 +- poetry.lock | 17 +++++- pyproject.toml | 2 + test.py | 1 + tests/conftest.py | 18 +++++-- 22 files changed, 209 insertions(+), 92 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 inclusive_dance_bot/db/__main__.py rename inclusive_dance_bot/{ => db}/alembic.ini (93%) delete mode 100644 inclusive_dance_bot/db/factory.py delete mode 100644 inclusive_dance_bot/db/mixins.py create mode 100644 inclusive_dance_bot/db/utils.py create mode 100644 test.py diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 5f9b3b6..cb9d509 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -17,14 +17,6 @@ env: POSTGRES_PORT: 5432 POSTGRES_DB: inclusive_dance_bot - REDIS_HOST: redis - REDIS_PORT: 6379 - REDIS_PASSWORD: 389jf928fhf10 - REDIS_DB: '3' - - PROJECT_PATH: ./inclusive_dance_bot/ - TEST_PATH: ./tests/ - jobs: checking: runs-on: ubuntu-22.04 @@ -38,27 +30,24 @@ jobs: python-version: '3.11' - name: Install dependencies - run: | - python -m pip install -U pip poetry - poetry config virtualenvs.create false - poetry install + run: make develop - name: Run flake8 - run: flake8 --max-line-length 88 --format=default ${{ env.PROJECT_PATH }} 2>&1 | tee flake8.txt + run: make flake - name: Run black - run: black ${{ env.PROJECT_PATH }} --check + run: make black - name: Run bandit - run: bandit -r -ll -iii ${{ env.PROJECT_PATH }} -f json -o ./bandit.json + run: make bandit - name: Run mypy - run: mypy --config-file ./pyproject.toml ${{ env.PROJECT_PATH }} + run: make mypy test: name: Run service tests with pytest runs-on: ubuntu-22.04 - container: python:3.11-slim + container: python:3.11 needs: checking services: postgres: @@ -78,10 +67,7 @@ jobs: uses: actions/checkout@v3 - name: Install dependencies - run: | - python -m pip install -U pip poetry - poetry config virtualenvs.create false - poetry install + run: make develop - name: Run pytest - run: python -m pytest ${{ env.TEST_PATH }} --junitxml=./junit.xml --cov=${{ env.PROJECT_PATH }} --cov-report=xml + run: make test-ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..698cadb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,42 @@ +name: Make release + +env: + PROJECT_NAME: inclusive_dance_bot + +on: + workflow_dispatch: + push: + branches: + - master + +jobs: + checking: + name: Check repository + uses: ./.github/workflows/check.yml + + build_and_push: + name: Build and push AMD64 and ARM64 images + needs: checking + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + push: true + platforms: linux/amd64,linux/arm64 + tags: andytakker/${{ env.PROJECT_NAME }}:latest diff --git a/Makefile b/Makefile index 02408e0..45f55ea 100644 --- a/Makefile +++ b/Makefile @@ -14,16 +14,16 @@ help: ##@Help Show this help lint-ci: flake black bandit mypy ##@Linting Run all linters in CI flake: ##@Linting Run flake8 - flake8 --max-line-length 88 --format=default $(PROJECT_PATH) 2>&1 | tee flake8.txt + .venv/bin/flake8 --max-line-length 88 --format=default $(PROJECT_PATH) 2>&1 | tee flake8.txt black: ##@Linting Run black - black $(PROJECT_PATH) --check + .venv/bin/black $(PROJECT_PATH) --check bandit: ##@Linting Run bandit - bandit -r -ll -iii $(PROJECT_PATH) -f json -o ./bandit.json + .venv/bin/bandit -r -ll -iii $(PROJECT_PATH) -f json -o ./bandit.json mypy: ##@Linting Run mypy - mypy --config-file ./pyproject.toml $(PROJECT_PATH) + .venv/bin/mypy --config-file ./pyproject.toml $(PROJECT_PATH) test: ##@Test Run tests with pytest pytest -vvx $(TEST_PATH) @@ -31,9 +31,14 @@ test: ##@Test Run tests with pytest test-ci: ##@Test Run tests with pytest and coverage in CI pytest $(TEST_PATH) --junitxml=./junit.xml --cov=$(PROJECT_PATH) --cov-report=xml +develop: # + python -m venv .venv + .venv/bin/pip install -U pip poetry + .venv/bin/poetry config virtualenvs.create false + .venv/bin/poetry install local: ##@Develop Run db and redis containers - docker-compose -f docker-compose.dev.yaml up db redis -d + docker-compose -f docker-compose.dev.yaml up --force-recreate --renew-anon-volumes --build local_down: ##@Develop Stop containers with delete volumes docker-compose -f docker-compose.dev.yaml down -v \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 590f76f..e970474 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -26,6 +26,7 @@ services: POSTGRES_USER: $POSTGRES_USER POSTGRES_PASSWORD: $POSTGRES_PASSWORD POSTGRES_DB: $POSTGRES_DB + POSTGRES_URL: postgresql+asyncpg://$POSTGRES_USER:$POSTGRES_PASSWORD@db:5432/$POSTGRES_DB REDIS_HOST: redis REDIS_PORT: 6379 diff --git a/docker/pre-start.sh b/docker/pre-start.sh index 10bd5ee..5457138 100644 --- a/docker/pre-start.sh +++ b/docker/pre-start.sh @@ -1,4 +1,4 @@ #! /usr/bin/env bash -alembic -c /app/inclusive_dance_bot/alembic.ini upgrade head +python -m inclusive_dance_bot.db python /app/inclusive_dance_bot/init_data.py \ No newline at end of file diff --git a/inclusive_dance_bot/bot/dialogs/admins/url/update/__init__.py b/inclusive_dance_bot/bot/dialogs/admins/url/update/__init__.py index 475747f..964a164 100644 --- a/inclusive_dance_bot/bot/dialogs/admins/url/update/__init__.py +++ b/inclusive_dance_bot/bot/dialogs/admins/url/update/__init__.py @@ -1,5 +1,3 @@ -from typing import Any - from aiogram_dialog import Dialog from inclusive_dance_bot.bot.dialogs.admins.url.update import change_slug, change_value diff --git a/inclusive_dance_bot/bot/dialogs/users/registration/__init__.py b/inclusive_dance_bot/bot/dialogs/users/registration/__init__.py index 8ae7251..19908ce 100644 --- a/inclusive_dance_bot/bot/dialogs/users/registration/__init__.py +++ b/inclusive_dance_bot/bot/dialogs/users/registration/__init__.py @@ -1,5 +1,3 @@ -from textwrap import dedent - from aiogram_dialog import Dialog from inclusive_dance_bot.bot.dialogs.users.registration import ( diff --git a/inclusive_dance_bot/db/__main__.py b/inclusive_dance_bot/db/__main__.py new file mode 100644 index 0000000..44acbbf --- /dev/null +++ b/inclusive_dance_bot/db/__main__.py @@ -0,0 +1,34 @@ +import argparse +import logging +import os + +from alembic.config import CommandLine + +from inclusive_dance_bot.db.utils import make_alembic_config + +DEFAULT_PG_URL = "postgresql://user:secret@localhost/inclusive_dance_bot" + + +def main() -> None: + logging.basicConfig(level=logging.DEBUG) + + alembic = CommandLine() + alembic.parser.formatter_class = argparse.ArgumentDefaultsHelpFormatter + alembic.parser.add_argument( + "--pg-url", + default=os.getenv("POSTGRES_URL", DEFAULT_PG_URL), + help="Database URL [env var: POSTGRES_URL]", + ) + + options = alembic.parser.parse_args() + if "cmd" not in options: + alembic.parser.error("too few arguments") + exit(128) + else: + config = make_alembic_config(options) + alembic.run_cmd(config, options) + exit() + + +if __name__ == "__main__": + main() diff --git a/inclusive_dance_bot/alembic.ini b/inclusive_dance_bot/db/alembic.ini similarity index 93% rename from inclusive_dance_bot/alembic.ini rename to inclusive_dance_bot/db/alembic.ini index cb88ada..1aaab6e 100644 --- a/inclusive_dance_bot/alembic.ini +++ b/inclusive_dance_bot/db/alembic.ini @@ -1,5 +1,5 @@ [alembic] -script_location = %(here)s/db/migrations +script_location = %(here)s/migrations file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(rev)s_%%(slug)s prepend_sys_path = . version_path_separator = os diff --git a/inclusive_dance_bot/db/base.py b/inclusive_dance_bot/db/base.py index 8fa4dd3..571813d 100644 --- a/inclusive_dance_bot/db/base.py +++ b/inclusive_dance_bot/db/base.py @@ -1,7 +1,9 @@ import re +from datetime import datetime -from sqlalchemy import MetaData -from sqlalchemy.orm import as_declarative, declared_attr +import pytz +from sqlalchemy import DateTime, MetaData, text +from sqlalchemy.orm import Mapped, as_declarative, declared_attr, mapped_column convention = { "all_column_names": lambda constraint, table: "_".join( @@ -25,3 +27,22 @@ class Base: def __tablename__(cls) -> str: name_list = re.findall(r"[A-Z][a-z\d]*", cls.__name__) return "_".join(name_list).lower() + + +def now_with_tz() -> datetime: + return datetime.now(tz=pytz.UTC) + + +class TimestampMixin: + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=now_with_tz, + server_default=text("TIMEZONE('utc', now())"), + nullable=False, + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=now_with_tz, + onupdate=now_with_tz, + server_default=text("TIMEZONE('utc', now())"), + ) diff --git a/inclusive_dance_bot/db/factory.py b/inclusive_dance_bot/db/factory.py deleted file mode 100644 index 60da829..0000000 --- a/inclusive_dance_bot/db/factory.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Any - -from sqlalchemy.ext.asyncio import ( - AsyncEngine, - AsyncSession, - async_sessionmaker, - create_async_engine, -) - - -def create_engine(connection_uri: str, **engine_kwargs: Any) -> AsyncEngine: - return create_async_engine(url=connection_uri, **engine_kwargs, pool_pre_ping=True) - - -def create_session_factory(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]: - return async_sessionmaker( - bind=engine, - class_=AsyncSession, - expire_on_commit=False, - ) diff --git a/inclusive_dance_bot/db/migrations/env.py b/inclusive_dance_bot/db/migrations/env.py index 2d88f94..c1ef47f 100644 --- a/inclusive_dance_bot/db/migrations/env.py +++ b/inclusive_dance_bot/db/migrations/env.py @@ -6,7 +6,6 @@ from sqlalchemy.engine import Connection from sqlalchemy.ext.asyncio import async_engine_from_config -from inclusive_dance_bot.config import Settings from inclusive_dance_bot.db.models import Base config = context.config @@ -16,8 +15,6 @@ fileConfig(config.config_file_name) -settings = Settings() -config.set_main_option("sqlalchemy.url", settings.build_db_connection_uri()) target_metadata = Base.metadata diff --git a/inclusive_dance_bot/db/mixins.py b/inclusive_dance_bot/db/mixins.py deleted file mode 100644 index 26fef01..0000000 --- a/inclusive_dance_bot/db/mixins.py +++ /dev/null @@ -1,24 +0,0 @@ -from datetime import datetime - -import pytz -from sqlalchemy import DateTime, text -from sqlalchemy.orm import Mapped, mapped_column - - -def now_with_tz() -> datetime: - return datetime.now(tz=pytz.UTC) - - -class TimestampMixin: - created_at: Mapped[datetime] = mapped_column( - DateTime(timezone=True), - default=now_with_tz, - server_default=text("TIMEZONE('utc', now())"), - nullable=False, - ) - updated_at: Mapped[datetime] = mapped_column( - DateTime(timezone=True), - default=now_with_tz, - onupdate=now_with_tz, - server_default=text("TIMEZONE('utc', now())"), - ) diff --git a/inclusive_dance_bot/db/models.py b/inclusive_dance_bot/db/models.py index db4e2f8..09d05e2 100644 --- a/inclusive_dance_bot/db/models.py +++ b/inclusive_dance_bot/db/models.py @@ -4,8 +4,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy_utils import ChoiceType -from inclusive_dance_bot.db.base import Base -from inclusive_dance_bot.db.mixins import TimestampMixin +from inclusive_dance_bot.db.base import Base, TimestampMixin from inclusive_dance_bot.enums import FeedbackType, MailingStatus, SubmenuType diff --git a/inclusive_dance_bot/db/utils.py b/inclusive_dance_bot/db/utils.py new file mode 100644 index 0000000..f075e31 --- /dev/null +++ b/inclusive_dance_bot/db/utils.py @@ -0,0 +1,53 @@ +import os +from argparse import Namespace +from pathlib import Path +from typing import Any + +from alembic.config import Config +from sqlalchemy.ext.asyncio import ( + AsyncEngine, + AsyncSession, + async_sessionmaker, + create_async_engine, +) + +import inclusive_dance_bot + +PROJECT_PATH = Path(inclusive_dance_bot.__file__).parent.parent.resolve() + + +def create_engine(connection_uri: str, **engine_kwargs: Any) -> AsyncEngine: + return create_async_engine(url=connection_uri, **engine_kwargs, pool_pre_ping=True) + + +def create_session_factory(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]: + return async_sessionmaker( + bind=engine, + class_=AsyncSession, + expire_on_commit=False, + ) + + +def make_alembic_config(cmd_opts: Namespace, base_path: Path = PROJECT_PATH) -> Config: + if not os.path.isabs(cmd_opts.config): + cmd_opts.config = str(base_path / "inclusive_dance_bot/db" / cmd_opts.config) + + config = Config( + file_=cmd_opts.config, + ini_section=cmd_opts.name, + cmd_opts=cmd_opts, + ) + + alembic_location = config.get_main_option("script_location") + if not alembic_location: + raise ValueError + print(alembic_location) + if not os.path.isabs(alembic_location): + config.set_main_option("script_location", str(base_path / alembic_location)) + + if cmd_opts.pg_url: + config.set_main_option("sqlalchemy.url", cmd_opts.pg_url) + + config.attributes["configure_logger"] = False + + return config diff --git a/inclusive_dance_bot/deps.py b/inclusive_dance_bot/deps.py index 01c4215..65e355b 100644 --- a/inclusive_dance_bot/deps.py +++ b/inclusive_dance_bot/deps.py @@ -6,8 +6,8 @@ from inclusive_dance_bot.bot.factory import get_bot from inclusive_dance_bot.config import Settings -from inclusive_dance_bot.db.factory import create_engine, create_session_factory from inclusive_dance_bot.db.uow.main import UnitOfWork +from inclusive_dance_bot.db.utils import create_engine, create_session_factory def config_deps(app_settings: Settings) -> None: diff --git a/inclusive_dance_bot/dto.py b/inclusive_dance_bot/dto.py index cfbd0eb..3a7db6c 100644 --- a/inclusive_dance_bot/dto.py +++ b/inclusive_dance_bot/dto.py @@ -5,7 +5,6 @@ from datetime import datetime from typing import TYPE_CHECKING -from inclusive_dance_bot.db.models import Mailing from inclusive_dance_bot.enums import FeedbackType, MailingStatus, SubmenuType if TYPE_CHECKING: diff --git a/inclusive_dance_bot/init_data.py b/inclusive_dance_bot/init_data.py index 5273f37..72fa43f 100644 --- a/inclusive_dance_bot/init_data.py +++ b/inclusive_dance_bot/init_data.py @@ -2,8 +2,8 @@ import logging from inclusive_dance_bot.config import Settings -from inclusive_dance_bot.db.factory import create_engine, create_session_factory from inclusive_dance_bot.db.uow.main import UnitOfWork +from inclusive_dance_bot.db.utils import create_engine, create_session_factory from inclusive_dance_bot.enums import SubmenuType from inclusive_dance_bot.exceptions import ( SubmenuAlreadyExistsError, diff --git a/poetry.lock b/poetry.lock index 3ff6d66..28e6d7d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -601,6 +601,21 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] development = ["black", "flake8", "mypy", "pytest", "types-colorama"] +[[package]] +name = "configargparse" +version = "1.7" +description = "A drop-in replacement for argparse that allows options to also be set via config files and/or environment variables." +optional = false +python-versions = ">=3.5" +files = [ + {file = "ConfigArgParse-1.7-py3-none-any.whl", hash = "sha256:d249da6591465c6c26df64a9f73d2536e743be2f244eb3ebe61114af2f94f86b"}, + {file = "ConfigArgParse-1.7.tar.gz", hash = "sha256:e7067471884de5478c58a511e529f0f9bd1c66bfef1dea90935438d6c23306d1"}, +] + +[package.extras] +test = ["PyYAML", "mock", "pytest"] +yaml = ["PyYAML"] + [[package]] name = "coverage" version = "7.3.2" @@ -2176,4 +2191,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "3e754d55279130bbdc03190a89c21b98cbb4b7fd298f81aafc6cdc217e652501" +content-hash = "4ed699093b3c5b50b7cacf5273f4f7cde9c4efb677d5470a47e2ccfc19b72b3f" diff --git a/pyproject.toml b/pyproject.toml index b2cfe0d..187460f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ greenlet = "^3.0.0" aiomisc = {extras = ["uvloop"], version = "^17.3.23"} aiomisc-dependency = "^0.1.20" aiomisc-pytest = "^1.1.1" +configargparse = "^1.7" [tool.poetry.group.dev.dependencies] mypy = "^1.5.1" @@ -40,6 +41,7 @@ pytest-subtests = "^0.11.0" [tool.poetry.scripts] bot = "inclusive_dance_bot.__main__:main" init_data = "inclusive_dance_bot.init_data:main" +migrate = "inclusive_dance_bot.db.__main__:main" [build-system] requires = ["poetry-core"] diff --git a/test.py b/test.py new file mode 100644 index 0000000..7bfcdea --- /dev/null +++ b/test.py @@ -0,0 +1 @@ +print(__file__) diff --git a/tests/conftest.py b/tests/conftest.py index a249ae9..301226c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import asyncio from pathlib import Path +from types import SimpleNamespace import pytest from alembic.config import Config as AlembicConfig @@ -8,7 +9,11 @@ from inclusive_dance_bot.config import Settings from inclusive_dance_bot.db.base import Base -from inclusive_dance_bot.db.factory import create_engine, create_session_factory +from inclusive_dance_bot.db.utils import ( + create_engine, + create_session_factory, + make_alembic_config, +) from tests.factories import FACTORIES from tests.utils import prepare_new_database, run_async_migrations @@ -47,9 +52,14 @@ def settings() -> Settings: @pytest.fixture(scope="session") def alembic_config(settings: Settings) -> AlembicConfig: - alembic_cfg = AlembicConfig(PROJECT_PATH / "inclusive_dance_bot" / "alembic.ini") - alembic_cfg.set_main_option("sqlalchemy.url", settings.build_db_connection_uri()) - return alembic_cfg + cmd_options = SimpleNamespace( + config="alembic.ini", + name="alembic", + pg_url=settings.build_db_connection_uri(), + raiseerr=False, + x=None, + ) + return make_alembic_config(cmd_options) @pytest.fixture(scope="session")