Compare commits

...

9 Commits

Author SHA1 Message Date
stijndcl bacd2d77fb Fix typing 2022-06-17 01:51:06 +02:00
stijndcl eb71470edc Create ufora-related revision 2022-06-17 01:36:47 +02:00
stijndcl b23160b8e2 Add debug print 2022-06-17 01:14:22 +02:00
stijndcl eb182b71f4 Create connection fixture 2022-06-17 01:02:36 +02:00
stijndcl 53a3e0e75a Fix env variable 2022-06-17 00:54:00 +02:00
stijndcl 21aeb80c13 Add default token 2022-06-17 00:50:00 +02:00
stijndcl 9193e73af9 Add password 2022-06-17 00:47:10 +02:00
stijndcl 00e805d535 Fiddle with action 2022-06-17 00:43:55 +02:00
stijndcl 6d61056dc4 Add missing alembic file 2022-06-16 00:40:37 +02:00
10 changed files with 299 additions and 20 deletions

View File

@ -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:
@ -24,8 +24,21 @@ jobs:
tests:
needs: [dependencies]
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_DB: didier_action
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v2
with:
@ -41,11 +54,15 @@ 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
DB_PASSWORD: postgres
linting:
needs: [dependencies]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v2
with:
@ -65,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:
@ -85,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:

3
.gitignore vendored
View File

@ -154,3 +154,6 @@ cython_debug/
# PyCharm
.idea/
# SQLite testing database
tests.db

105
alembic.ini 100644
View File

@ -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

View File

@ -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 ###

View File

@ -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)

View File

@ -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: UforaCourse = relationship("UforaCourse", back_populates="announcements", uselist=False)

View File

@ -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 = [
@ -18,4 +21,10 @@ max-line-length = 120
good-names = ["i"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_mode = "auto"
env = [
"DB_NAME = didier_action",
"DB_USERNAME = postgres",
"DB_HOST = localhost",
"DISC_TOKEN = token"
]

View File

@ -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")

40
tests/conftest.py 100644
View File

@ -0,0 +1,40 @@
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")
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")
@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()

View File

@ -1,2 +1,2 @@
def test_dummy():
def test_dummy(tables):
assert True