From 6d61056dc4d9e4cfbdbd37c149c76d4b3235383b Mon Sep 17 00:00:00 2001 From: stijndcl Date: Thu, 16 Jun 2022 00:40:37 +0200 Subject: [PATCH 1/9] Add missing alembic file --- alembic.ini | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 alembic.ini diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..7fa7c98 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,105 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S From 00e805d53582e801630d68c0b7e46d487bd8749a Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 17 Jun 2022 00:43:55 +0200 Subject: [PATCH 2/9] Fiddle with action --- .github/workflows/python.yml | 15 +++++++++++++++ .gitignore | 3 +++ database/engine.py | 34 ++++++++++++++++++++++------------ pyproject.toml | 7 ++++++- settings.py | 1 + tests/conftest.py | 15 +++++++++++++++ tests/test_dummy.py | 7 +++++-- 7 files changed, 67 insertions(+), 15 deletions(-) create mode 100644 tests/conftest.py diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 33624a9..9ded31d 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -24,6 +24,18 @@ jobs: tests: needs: [dependencies] runs-on: ubuntu-latest + services: + postgres: + image: postgres:14 + env: + POSTGRES_DB: didier_action + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 steps: - uses: actions/checkout@v2 - name: Setup Python @@ -41,6 +53,9 @@ jobs: run: pip3 install -r requirements.txt -r requirements-dev.txt - name: Run Pytest run: pytest tests + env: + DB_TEST_SQLITE: false + DB_NAME: didier_action linting: needs: [dependencies] runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index a14d6d0..518f9fa 100644 --- a/.gitignore +++ b/.gitignore @@ -154,3 +154,6 @@ cython_debug/ # PyCharm .idea/ + +# SQLite testing database +tests.db \ No newline at end of file diff --git a/database/engine.py b/database/engine.py index fbdbfd2..2c1815f 100644 --- a/database/engine.py +++ b/database/engine.py @@ -6,17 +6,27 @@ from sqlalchemy.orm import sessionmaker import settings -encoded_password = quote_plus(settings.DB_PASSWORD) -engine = create_async_engine( - URL.create( - drivername="postgresql+asyncpg", - username=settings.DB_USERNAME, - password=encoded_password, - host=settings.DB_HOST, - port=settings.DB_PORT, - database=settings.DB_NAME, - ), - pool_pre_ping=True, -) +# Run local tests against SQLite instead of Postgres +if settings.DB_TEST_SQLITE: + engine = create_async_engine( + URL.create( + drivername="sqlite+aiosqlite", + database="tests.db", + ), + connect_args={"check_same_thread": False}, + ) +else: + encoded_password = quote_plus(settings.DB_PASSWORD) + engine = create_async_engine( + URL.create( + drivername="postgresql+asyncpg", + username=settings.DB_USERNAME, + password=encoded_password, + host=settings.DB_HOST, + port=settings.DB_PORT, + database=settings.DB_NAME, + ), + pool_pre_ping=True, + ) DBSession = sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession, expire_on_commit=False) diff --git a/pyproject.toml b/pyproject.toml index a5ef1e7..c4f2266 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,4 +18,9 @@ max-line-length = 120 good-names = ["i"] [tool.pytest.ini_options] -asyncio_mode = "auto" \ No newline at end of file +asyncio_mode = "auto" +env = [ + "DB_NAME = didier-tests", + "DB_USERNAME = postgres", + "DB_HOST = postgres" +] \ No newline at end of file diff --git a/settings.py b/settings.py index 5ef9350..cd7e541 100644 --- a/settings.py +++ b/settings.py @@ -14,6 +14,7 @@ DB_USERNAME: str = env.str("DB_USERNAME", "postgres") DB_PASSWORD: str = env.str("DB_PASSWORD", "") DB_HOST: str = env.str("DB_HOST", "localhost") DB_PORT: int = env.int("DB_PORT", "5432") +DB_TEST_SQLITE: bool = env.bool("DB_TEST_SQLITE", True) """Discord""" DISCORD_TOKEN: str = env.str("DISC_TOKEN") diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..40d3189 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +import pytest + +from alembic import command, config + + +@pytest.fixture(scope="session") +def tables(): + """Initialize a database before the tests, and then tear it down again + Starts from an empty database and runs through all the migrations to check those as well + while we're at it + """ + alembic_config = config.Config("alembic.ini") + command.upgrade(alembic_config, "head") + yield + command.downgrade(alembic_config, "base") diff --git a/tests/test_dummy.py b/tests/test_dummy.py index f4f5361..5f57aa2 100644 --- a/tests/test_dummy.py +++ b/tests/test_dummy.py @@ -1,2 +1,5 @@ -def test_dummy(): - assert True +import settings + + +def test_dummy(tables): + assert settings.DB_TEST_SQLITE From 9193e73af908bdaa0b129559ff3f86027636865e Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 17 Jun 2022 00:47:10 +0200 Subject: [PATCH 3/9] Add password --- .github/workflows/python.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 9ded31d..9f91f94 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -29,6 +29,7 @@ jobs: image: postgres:14 env: POSTGRES_DB: didier_action + POSTGRES_PASSWORD: postgres options: >- --health-cmd pg_isready --health-interval 10s @@ -56,6 +57,7 @@ jobs: env: DB_TEST_SQLITE: false DB_NAME: didier_action + DB_PASSWORD: postgres linting: needs: [dependencies] runs-on: ubuntu-latest From 21aeb80c13a0ba8a613822d43abd7afe3cb5d9de Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 17 Jun 2022 00:50:00 +0200 Subject: [PATCH 4/9] Add default token --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c4f2266..684e485 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,5 +22,6 @@ asyncio_mode = "auto" env = [ "DB_NAME = didier-tests", "DB_USERNAME = postgres", - "DB_HOST = postgres" + "DB_HOST = postgres", + "DISC_TOKEN = token" ] \ No newline at end of file From 53a3e0e75aec1b057ba9e354d1c448c03f77047e Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 17 Jun 2022 00:54:00 +0200 Subject: [PATCH 5/9] Fix env variable --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 684e485..aada43c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,8 @@ good-names = ["i"] [tool.pytest.ini_options] asyncio_mode = "auto" env = [ - "DB_NAME = didier-tests", + "DB_NAME = didier_action", "DB_USERNAME = postgres", - "DB_HOST = postgres", + "DB_HOST = localhost", "DISC_TOKEN = token" ] \ No newline at end of file From eb182b71f47397bfc9b40bd89d1fcd2c3ee45bd7 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 17 Jun 2022 01:02:36 +0200 Subject: [PATCH 6/9] Create connection fixture --- pyproject.toml | 2 +- tests/conftest.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index aada43c..bf0e7fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,4 +24,4 @@ env = [ "DB_USERNAME = postgres", "DB_HOST = localhost", "DISC_TOKEN = token" -] \ No newline at end of file +] diff --git a/tests/conftest.py b/tests/conftest.py index 40d3189..37e249c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,12 @@ +import os +from typing import AsyncGenerator + import pytest from alembic import command, config +from sqlalchemy.ext.asyncio import AsyncSession + +from database.engine import engine @pytest.fixture(scope="session") @@ -9,7 +15,27 @@ def tables(): Starts from an empty database and runs through all the migrations to check those as well while we're at it """ + print("CWD: ", os.getcwd()) alembic_config = config.Config("alembic.ini") command.upgrade(alembic_config, "head") yield command.downgrade(alembic_config, "base") + + +@pytest.fixture +async def database_session(tables) -> AsyncGenerator[AsyncSession, None]: + """Fixture to create a session for every test + Rollbacks the transaction afterwards so that the future tests start with a clean database + """ + connection = await engine.connect() + transaction = await connection.begin() + session = AsyncSession(bind=connection, expire_on_commit=False) + + yield session + + # Clean up session & rollback transactions + await session.close() + if transaction.is_valid: + await transaction.rollback() + + await connection.close() From b23160b8e2114878e1e91e9fb8d2621f3a1b5da3 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 17 Jun 2022 01:14:22 +0200 Subject: [PATCH 7/9] Add debug print --- .github/workflows/python.yml | 10 +++++----- tests/conftest.py | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 9f91f94..382bc76 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -7,7 +7,7 @@ jobs: dependencies: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v2 with: @@ -38,7 +38,7 @@ jobs: ports: - 5432:5432 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v2 with: @@ -62,7 +62,7 @@ jobs: needs: [dependencies] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v2 with: @@ -82,7 +82,7 @@ jobs: needs: [dependencies] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v2 with: @@ -102,7 +102,7 @@ jobs: needs: [dependencies] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v2 with: diff --git a/tests/conftest.py b/tests/conftest.py index 37e249c..1d237cd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,7 @@ def tables(): while we're at it """ print("CWD: ", os.getcwd()) + print("Contents: ", list(os.listdir(os.getcwd()))) alembic_config = config.Config("alembic.ini") command.upgrade(alembic_config, "head") yield From eb71470edcda56b8fa76519bd39f02814edaeda0 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 17 Jun 2022 01:36:47 +0200 Subject: [PATCH 8/9] Create ufora-related revision --- ...6_initial_migration_ufora_announcements.py | 52 +++++++++++++++++++ database/models.py | 44 +++++++++++++++- tests/conftest.py | 2 - 3 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 alembic/versions/9e8ce58c0a26_initial_migration_ufora_announcements.py diff --git a/alembic/versions/9e8ce58c0a26_initial_migration_ufora_announcements.py b/alembic/versions/9e8ce58c0a26_initial_migration_ufora_announcements.py new file mode 100644 index 0000000..7b7d3f8 --- /dev/null +++ b/alembic/versions/9e8ce58c0a26_initial_migration_ufora_announcements.py @@ -0,0 +1,52 @@ +"""Initial migration: Ufora announcements + +Revision ID: 9e8ce58c0a26 +Revises: +Create Date: 2022-06-17 01:36:02.767151 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9e8ce58c0a26' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('ufora_courses', + sa.Column('course_id', sa.Integer(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('code', sa.Text(), nullable=False), + sa.Column('year', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('course_id'), + sa.UniqueConstraint('code'), + sa.UniqueConstraint('name') + ) + op.create_table('ufora_announcements', + sa.Column('announcement_id', sa.Integer(), nullable=False), + sa.Column('course_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['course_id'], ['ufora_courses.course_id'], ), + sa.PrimaryKeyConstraint('announcement_id') + ) + op.create_table('ufora_course_aliases', + sa.Column('alias_id', sa.Integer(), nullable=False), + sa.Column('alias', sa.Text(), nullable=False), + sa.Column('course_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['course_id'], ['ufora_courses.course_id'], ), + sa.PrimaryKeyConstraint('alias_id'), + sa.UniqueConstraint('alias') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('ufora_course_aliases') + op.drop_table('ufora_announcements') + op.drop_table('ufora_courses') + # ### end Alembic commands ### diff --git a/database/models.py b/database/models.py index 8dc24d0..b6347a4 100644 --- a/database/models.py +++ b/database/models.py @@ -1,5 +1,47 @@ from __future__ import annotations -from sqlalchemy.orm import declarative_base +from sqlalchemy import Column, Integer, Text, ForeignKey +from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() + + +class UforaCourse(Base): + """A course on Ufora""" + + __tablename__ = "ufora_courses" + + course_id: int = Column(Integer, primary_key=True) + name: str = Column(Text, nullable=False, unique=True) + code: str = Column(Text, nullable=False, unique=True) + year: int = Column(Integer, nullable=False) + + announcements: list[UforaAnnouncement] = relationship( + "UforaAnnouncement", back_populates="course", cascade="all, delete-orphan" + ) + aliases: list[UforaCourseAlias] = relationship( + "UforaCourseAlias", back_populates="course", cascade="all, delete-orphan" + ) + + +class UforaCourseAlias(Base): + """An alias for a course on Ufora that we use to refer to them""" + + __tablename__ = "ufora_course_aliases" + + alias_id: int = Column(Integer, primary_key=True) + alias: str = Column(Text, nullable=False, unique=True) + course_id: int = Column(Integer, ForeignKey("ufora_courses.course_id")) + + course: UforaCourse = relationship("UforaCourse", back_populates="aliases", uselist=False) + + +class UforaAnnouncement(Base): + """An announcement sent on Ufora""" + + __tablename__ = "ufora_announcements" + + announcement_id = Column(Integer, primary_key=True) + course_id = Column(Integer, ForeignKey("ufora_courses.course_id")) + + course = relationship("UforaCourse", back_populates="announcements", uselist=False) diff --git a/tests/conftest.py b/tests/conftest.py index 1d237cd..1ae9878 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,8 +15,6 @@ def tables(): Starts from an empty database and runs through all the migrations to check those as well while we're at it """ - print("CWD: ", os.getcwd()) - print("Contents: ", list(os.listdir(os.getcwd()))) alembic_config = config.Config("alembic.ini") command.upgrade(alembic_config, "head") yield From bacd2d77fb7c7234c4c511c49500fd6a99eee887 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 17 Jun 2022 01:51:06 +0200 Subject: [PATCH 9/9] Fix typing --- database/models.py | 2 +- pyproject.toml | 3 +++ tests/test_dummy.py | 5 +---- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/database/models.py b/database/models.py index b6347a4..3d778eb 100644 --- a/database/models.py +++ b/database/models.py @@ -44,4 +44,4 @@ class UforaAnnouncement(Base): announcement_id = Column(Integer, primary_key=True) course_id = Column(Integer, ForeignKey("ufora_courses.course_id")) - course = relationship("UforaCourse", back_populates="announcements", uselist=False) + course: UforaCourse = relationship("UforaCourse", back_populates="announcements", uselist=False) diff --git a/pyproject.toml b/pyproject.toml index bf0e7fd..a1a3d6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,9 @@ line-length = 120 plugins = [ "sqlalchemy.ext.mypy.plugin" ] +[[tool.mypy.overrides]] +module = "discord.*" +ignore_missing_imports = true [tool.pylint.master] disable = [ diff --git a/tests/test_dummy.py b/tests/test_dummy.py index 5f57aa2..569bcda 100644 --- a/tests/test_dummy.py +++ b/tests/test_dummy.py @@ -1,5 +1,2 @@ -import settings - - def test_dummy(tables): - assert settings.DB_TEST_SQLITE + assert True