Skip to content

Commit

Permalink
Improve compatibility with SQLAlchemy 2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
ykazakov committed Oct 9, 2024
1 parent 38b02c7 commit cbb3972
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 82 deletions.
16 changes: 8 additions & 8 deletions ngshare/alembic/versions/1921a169739b_add_file_size.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from alembic import op, context
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy import Column, INTEGER, TEXT
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, Text
from sqlalchemy.orm import declarative_base

# revision identifiers, used by Alembic.
revision = '1921a169739b'
Expand All @@ -25,16 +25,16 @@
class File(Base):
'A File (for assignment, submission file, or submission feedback)'
__tablename__ = 'files'
_id = Column(INTEGER, primary_key=True)
filename = Column(TEXT)
checksum = Column(TEXT)
size = Column(INTEGER)
actual_name = Column(TEXT)
_id = Column(Integer, primary_key=True)
filename = Column(Text)
checksum = Column(Text)
size = Column(Integer)
actual_name = Column(Text)


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('files', sa.Column('size', sa.INTEGER(), nullable=True))
op.add_column('files', sa.Column('size', sa.Integer(), nullable=True))
# https://alembic.sqlalchemy.org/en/latest/cookbook.html
# http://ominian.com/2019/07/11/data-migration-with-sqlalchemy-and-alembic/
if context.get_x_argument(as_dictionary=True).get('data', None):
Expand Down
135 changes: 74 additions & 61 deletions ngshare/database/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,59 +8,68 @@
from sqlalchemy import (
Table,
Column,
INTEGER,
TEXT,
Integer,
Text,
TIMESTAMP,
BOOLEAN,
Boolean,
ForeignKey,
)
from sqlalchemy.orm import relationship, backref
from sqlalchemy.orm import relationship, declarative_base, Mapped
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

# Assignment -> Course (One to Many)
assignment_files_assoc_table = Table(
'assignment_files_assoc_table',
Base.metadata,
Column('left_id', TEXT, ForeignKey('assignments._id'), primary_key=True),
Column('right_id', INTEGER, ForeignKey('files._id'), primary_key=True),
Column('left_id', Text, ForeignKey('assignments._id'), primary_key=True),
Column('right_id', Integer, ForeignKey('files._id'), primary_key=True),
)

# Submission -> Course (One to Many)
submission_files_assoc_table = Table(
'submission_files_assoc_table',
Base.metadata,
Column('left_id', TEXT, ForeignKey('submissions._id'), primary_key=True),
Column('right_id', INTEGER, ForeignKey('files._id'), primary_key=True),
Column('left_id', Text, ForeignKey('submissions._id'), primary_key=True),
Column('right_id', Integer, ForeignKey('files._id'), primary_key=True),
)

# Submission (feedback) -> Course (One to Many)
feedback_files_assoc_table = Table(
'feedback_files_assoc_table',
Base.metadata,
Column('left_id', TEXT, ForeignKey('submissions._id'), primary_key=True),
Column('right_id', INTEGER, ForeignKey('files._id'), primary_key=True),
Column('left_id', Text, ForeignKey('submissions._id'), primary_key=True),
Column('right_id', Integer, ForeignKey('files._id'), primary_key=True),
)


class User(Base):
'A JupyterHub user; can be either instructor or student, or both'
__tablename__ = 'users'
id = Column(TEXT, primary_key=True)
teaching = association_proxy(
id = Column(Text, primary_key=True)
teaching: Mapped[list['Course']] = association_proxy(
'inst_assoc',
'course',
creator=lambda course: InstructorAssociation(course=course),
cascade_scalar_deletes=True,
)
taking = association_proxy(
inst_assoc: Mapped[list['InstructorAssociation']] = relationship(
'InstructorAssociation',
back_populates='user',
cascade='save-update, merge, delete, delete-orphan',
)
taking: Mapped[list['Course']] = association_proxy(
'student_assoc',
'course',
creator=lambda course: StudentAssociation(course=course),
cascade_scalar_deletes=True,
)
student_assoc: Mapped[list['StudentAssociation']] = relationship(
'StudentAssociation',
back_populates='user',
cascade='save-update, merge, delete, delete-orphan',
)

def __init__(self, name):
'Initialize with JupyterHub user name'
Expand All @@ -69,7 +78,7 @@ def __init__(self, name):
def __str__(self):
return '<User %s>' % self.id

def dump(self):
def dump(self) -> dict:
'Dump data to dict'
return {
'id': self.id,
Expand All @@ -95,27 +104,37 @@ class Course(Base):
'An nbgrader course'
__tablename__ = 'courses'
# in case course name needs to be changed
_id = Column(INTEGER, primary_key=True)
id = Column(TEXT, unique=True)
instructors = association_proxy(
_id = Column(Integer, primary_key=True)
id = Column(Text, unique=True)
instructors: Mapped[list['User']] = association_proxy(
'inst_assoc',
'user',
creator=lambda user: InstructorAssociation(
user=user,
),
cascade_scalar_deletes=True,
)
students = association_proxy(
inst_assoc: Mapped[list['InstructorAssociation']] = relationship(
'InstructorAssociation',
back_populates='course',
cascade='save-update, merge, delete, delete-orphan',
)
students: Mapped[list['User']] = association_proxy(
'student_assoc',
'user',
creator=lambda user: StudentAssociation(
user=user,
),
cascade_scalar_deletes=True,
)
assignments = relationship('Assignment', backref='course')
student_assoc: Mapped[list['StudentAssociation']] = relationship(
'StudentAssociation',
back_populates='course',
cascade='save-update, merge, delete, delete-orphan',
)
assignments = relationship('Assignment', back_populates='course')

def dump(self):
def dump(self) -> dict:
'Dump data to dict'
return {
'_id': self._id,
Expand Down Expand Up @@ -146,12 +165,13 @@ class Assignment(Base):
'An nbgrader assignment'
__tablename__ = 'assignments'
# in case assignment name needs to be changed
_id = Column(INTEGER, primary_key=True)
id = Column(TEXT)
course_id = Column(INTEGER, ForeignKey('courses._id'))
submissions = relationship('Submission', backref='assignment')
_id = Column(Integer, primary_key=True)
id = Column(Text)
course_id = Column(Integer, ForeignKey('courses._id'))
course = relationship('Course', back_populates='assignments')
submissions = relationship('Submission', back_populates='assignment')
files = relationship('File', secondary=assignment_files_assoc_table)
released = BOOLEAN()
released = False
due = Column(TIMESTAMP)
# TODO: timezoon

Expand All @@ -163,7 +183,7 @@ def __init__(self, name, course):
def __str__(self):
return '<Assignment %s>' % self.id

def dump(self):
def dump(self) -> dict:
'Dump data to dict'
return {
'_id': self._id,
Expand All @@ -187,10 +207,11 @@ def delete(self, db):
class Submission(Base):
'A submission for an assignment'
__tablename__ = 'submissions'
_id = Column(INTEGER, primary_key=True)
assignment_id = Column(INTEGER, ForeignKey('assignments._id'))
_id = Column(Integer, primary_key=True)
assignment_id = Column(Integer, ForeignKey('assignments._id'))
assignment = relationship('Assignment', back_populates='submissions')
timestamp = Column(TIMESTAMP)
student_id = Column(TEXT, ForeignKey('users.id'))
student_id = Column(Text, ForeignKey('users.id'))
files = relationship('File', secondary=submission_files_assoc_table)
feedbacks = relationship('File', secondary=feedback_files_assoc_table)
student = relationship('User')
Expand All @@ -204,7 +225,7 @@ def __init__(self, student, assignment):
def __str__(self):
return '<Submission %d>' % self._id

def dump(self):
def dump(self) -> dict:
'Dump data to dict'
return {
'_id': self._id,
Expand All @@ -228,11 +249,11 @@ def delete(self, db):
class File(Base):
'A File (for assignment, submission file, or submission feedback)'
__tablename__ = 'files'
_id = Column(INTEGER, primary_key=True)
filename = Column(TEXT)
checksum = Column(TEXT)
size = Column(INTEGER)
actual_name = Column(TEXT)
_id = Column(Integer, primary_key=True)
filename = Column(Text)
checksum = Column(Text)
size = Column(Integer)
actual_name = Column(Text)

def __init__(self, filename, contents, actual_name=None):
'Initialize with file name and content; auto-compute md5 and size'
Expand All @@ -244,7 +265,7 @@ def __init__(self, filename, contents, actual_name=None):
def __str__(self):
return '<File %s>' % self.filename

def dump(self):
def dump(self) -> dict:
'Dump data to dict'
return {
'_id': self._id,
Expand All @@ -264,22 +285,18 @@ def delete(self, db):
class InstructorAssociation(Base):
'Relationship between instructor and course, many to many, with extra data'
__tablename__ = 'instructor_assoc_table'
left_id = Column(TEXT, ForeignKey('users.id'), primary_key=True)
right_id = Column(TEXT, ForeignKey('courses.id'), primary_key=True)
first_name = Column(TEXT)
last_name = Column(TEXT)
email = Column(TEXT)
left_id = Column(Text, ForeignKey('users.id'), primary_key=True)
right_id = Column(Text, ForeignKey('courses.id'), primary_key=True)
first_name = Column(Text)
last_name = Column(Text)
email = Column(Text)
user = relationship(
User,
backref=backref(
'inst_assoc', cascade='save-update, merge, delete, delete-orphan'
),
back_populates='inst_assoc',
)
course = relationship(
Course,
backref=backref(
'inst_assoc', cascade='save-update, merge, delete, delete-orphan'
),
back_populates='inst_assoc',
)

@staticmethod
Expand All @@ -291,7 +308,7 @@ def find(db, instructor, course):
.one_or_none()
)

def dump(self):
def dump(self) -> dict:
'Dump data to dict'
return {
'left_id': self.left_id,
Expand All @@ -306,22 +323,18 @@ def dump(self):
class StudentAssociation(Base):
'Relationship between student and course, many to many, with extra data'
__tablename__ = 'student_assoc_table'
left_id = Column(TEXT, ForeignKey('users.id'), primary_key=True)
right_id = Column(TEXT, ForeignKey('courses.id'), primary_key=True)
first_name = Column(TEXT)
last_name = Column(TEXT)
email = Column(TEXT)
left_id = Column(Text, ForeignKey('users.id'), primary_key=True)
right_id = Column(Text, ForeignKey('courses.id'), primary_key=True)
first_name = Column(Text)
last_name = Column(Text)
email = Column(Text)
user = relationship(
User,
backref=backref(
'student_assoc', cascade='save-update, merge, delete, delete-orphan'
),
back_populates='student_assoc',
)
course = relationship(
Course,
backref=backref(
'student_assoc', cascade='save-update, merge, delete, delete-orphan'
),
back_populates='student_assoc',
)

@staticmethod
Expand All @@ -333,7 +346,7 @@ def find(db, student, course):
.one_or_none()
)

def dump(self):
def dump(self) -> dict:
'Dump data to dict'
return {
'left_id': self.left_id,
Expand Down
23 changes: 11 additions & 12 deletions ngshare/test_dbutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
from collections import namedtuple
import pytest

from sqlalchemy import create_engine, Column, INTEGER, TEXT
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine, Column, Integer, Text
from sqlalchemy.orm import sessionmaker, declarative_base

from . import dbutil

Expand Down Expand Up @@ -58,18 +57,18 @@ def test_add_file_size():

class File_old(Base_old):
__tablename__ = 'files'
_id = Column(INTEGER, primary_key=True)
filename = Column(TEXT)
checksum = Column(TEXT)
actual_name = Column(TEXT)
_id = Column(Integer, primary_key=True)
filename = Column(Text)
checksum = Column(Text)
actual_name = Column(Text)

class File_new(Base_new):
__tablename__ = 'files'
_id = Column(INTEGER, primary_key=True)
filename = Column(TEXT)
checksum = Column(TEXT)
size = Column(INTEGER)
actual_name = Column(TEXT)
_id = Column(Integer, primary_key=True)
filename = Column(Text)
checksum = Column(Text)
size = Column(Integer)
actual_name = Column(Text)

# Add data
db = sessionmaker(bind=create_engine(tempdb_url))()
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def get_version(rel_path):
],
python_requires='>=3.8',
install_requires=[
'SQLAlchemy>=1.3.12',
'SQLAlchemy>=1.4.0',
'alembic>=1.3.2',
'tornado>=6.0.3',
'jupyterhub>=1.1.0',
Expand Down

0 comments on commit cbb3972

Please sign in to comment.