From 4b951ebf7da4406ef061ea7d524279d9274e90a9 Mon Sep 17 00:00:00 2001 From: eclipse Date: Thu, 17 Jul 2025 23:55:34 +0200 Subject: [PATCH] started migrating ORM code from DB reflection to declarative mapping --- the_works/database.py | 11 +- the_works/models.py | 270 ++++++++++++++++++--------- the_works/old_models.py | 121 ++++++++++++ the_works/views/home.py | 6 +- the_works/views/text.py | 2 +- the_works/views/veroeffentlichung.py | 2 +- the_works/views/werk.py | 2 +- 7 files changed, 315 insertions(+), 99 deletions(-) create mode 100644 the_works/old_models.py diff --git a/the_works/database.py b/the_works/database.py index fdd805c..dd04987 100644 --- a/the_works/database.py +++ b/the_works/database.py @@ -1,10 +1,18 @@ from flask_sqlalchemy import SQLAlchemy +from the_works.models import Base -db = SQLAlchemy() +# Instantiate the SQLAlchemy around the database schema's declarative mapping +db = SQLAlchemy(model_class=Base) def init_db(app): + # initialize the Flask app with the flask_sqlalchemy extension db.init_app(app) + + # create nonexistent tables in DB with app.app_context(): + db.create_all() + +""" # check if database is empty and if so, populate it with fresh tables #TODO: does it make sense to try and create all tables by default? Existing tables wouldn't be overwritten but what about DB constraints ? if not len(db.metadata.tables): @@ -23,3 +31,4 @@ def init_db(app): # generate declarative table objects by reflecting the DB db.reflect() + """ \ No newline at end of file diff --git a/the_works/models.py b/the_works/models.py index a188895..3387762 100644 --- a/the_works/models.py +++ b/the_works/models.py @@ -1,93 +1,89 @@ -from the_works.database import db -from sqlalchemy.orm import relationship -from sqlalchemy.ext.associationproxy import association_proxy +# based on output of `sqlacodegen --generator declarative sqlite:///path/to/the_works.sqlite` + +from typing import List, Optional + +from sqlalchemy import Column, ForeignKey, Integer, LargeBinary, Table, Text, types +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship + from flask import url_for import sys -# add method to sqlalchemy.orm.decl_api.Model -def _asdict(self): - d = {} - for col in self.__table__.c: - if type(col.type) == db.types.BLOB: - d[col.key] = "Blob (NULL)" if self.__getattribute__(col.key) == None else f"Blob ({sys.getsizeof(self.__getattribute__(col.key))} Bytes)" - else: - value = str(self.__getattribute__(col.key)) - d[col.key] = value[:50] + '...' if len(value) > 50 else value - return d -db.Model._asdict = _asdict -# override repr() method from sqlalchemy.orm.decl_api.Model -def __repr__(self): - return f"{type(self).__name__}({str(self._asdict())})" -db.Model.__repr__ = __repr__ +class Base(DeclarativeBase): + def _asdict(self): + d = {} + for col in self.__table__.c: + if type(col.type) == types.BLOB: + d[col.key] = "Blob (NULL)" if self.__getattribute__(col.key) == None else f"Blob ({sys.getsizeof(self.__getattribute__(col.key))} Bytes)" + else: + value = str(self.__getattribute__(col.key)) + d[col.key] = value[:50] + '...' if len(value) > 50 else value + return d -class Text(db.Model): - __table__ = db.Model.metadata.tables['Text'] - reihe = relationship("Reihe", back_populates="text") - textform = relationship("Textform", back_populates="text") - sprache = relationship("Sprache", back_populates="text") - veroeffentlichung = relationship("Veroeffentlichung", back_populates="text") - text_genre = relationship("Text_Genre", back_populates="text", cascade="save-update, merge, delete, delete-orphan") - genres = association_proxy("text_genre", "Genre") + def __repr__(self): + return f"{type(self).__name__}({str(self._asdict())})" -class Werk(db.Model): - __table__ = db.Model.metadata.tables['Werk'] - reihe = relationship("Reihe", back_populates="werk") - verlag = relationship("Verlag", back_populates="werk") - werksform = relationship("Werksform", back_populates="werk") - veroeffentlichung = relationship("Veroeffentlichung", back_populates="werk") - werk_genre = relationship("Werk_Genre", back_populates="werk", cascade="save-update, merge, delete, delete-orphan") -# titelbild = relationship("Titelbild", back_populates="werk", cascade="all, delete-orphan", single_parent=True) - titelbild = relationship("Titelbild", back_populates="werk") - genres = association_proxy("werk_genre", "Genre") - werk_herausgeber = relationship("Werk_Herausgeber", back_populates="werk", cascade="save-update, merge, delete, delete-orphan") - herausgeber = association_proxy("werk_herausgeber", "Herausgeber") -class Veroeffentlichung(db.Model): - __table__ = db.Model.metadata.tables['Veroeffentlichung'] - text = relationship("Text", back_populates="veroeffentlichung") - werk = relationship("Werk", back_populates="veroeffentlichung") - pseudonym = relationship("Pseudonym", back_populates="veroeffentlichung") +class Genre(Base): + __tablename__ = 'Genre' -class Reihe(db.Model): - __table__ = db.Model.metadata.tables['Reihe'] - text = relationship("Text", back_populates="reihe") - werk = relationship("Werk", back_populates="reihe") - verlag = relationship("Verlag", back_populates="reihe") + ID: Mapped[int] = mapped_column(Integer, primary_key=True) + Genre: Mapped[str] -class Verlag(db.Model): - __table__ = db.Model.metadata.tables['Verlag'] - werk = relationship("Werk", back_populates="verlag") - reihe = relationship("Reihe", back_populates="verlag") + text: Mapped[List['Text_']] = relationship('Text_', secondary='Text_Genre', back_populates='genre') + werk: Mapped[List['Werk']] = relationship('Werk', secondary='Werk_Genre', back_populates='genre') -class Sprache(db.Model): - __table__ = db.Model.metadata.tables['Sprache'] - text = relationship("Text", back_populates="sprache") -class Textform(db.Model): - __table__ = db.Model.metadata.tables['Textform'] - text = relationship("Text", back_populates="textform") +class Herausgeber(Base): + __tablename__ = 'Herausgeber' -class Werksform(db.Model): - __table__ = db.Model.metadata.tables['Werksform'] - werk = relationship("Werk", back_populates="werksform") + ID: Mapped[int] = mapped_column(Integer, primary_key=True) + Name: Mapped[str] -class Genre(db.Model): - __table__ = db.Model.metadata.tables['Genre'] - text_genre = relationship("Text_Genre", back_populates="genre") - werk_genre = relationship("Werk_Genre", back_populates="genre") + werk: Mapped[List['Werk']] = relationship('Werk', secondary='Werk_Herausgeber', back_populates='herausgeber') -class Pseudonym(db.Model): - __table__ = db.Model.metadata.tables['Pseudonym'] - veroeffentlichung = relationship("Veroeffentlichung", back_populates="pseudonym") -class Herausgeber(db.Model): - __table__ = db.Model.metadata.tables['Herausgeber'] - werk_herausgeber = relationship("Werk_Herausgeber", back_populates="herausgeber") +class Pseudonym(Base): + __tablename__ = 'Pseudonym' -class Titelbild(db.Model): - __table__ = db.Model.metadata.tables['Titelbild'] - werk = relationship("Werk", back_populates="titelbild") + ID: Mapped[int] = mapped_column(Integer, primary_key=True) + Pseudonym: Mapped[str] + + veroeffentlichung: Mapped[List['Veroeffentlichung']] = relationship('Veroeffentlichung', back_populates='pseudonym') + + +class Sprache(Base): + __tablename__ = 'Sprache' + + ID: Mapped[int] = mapped_column(Integer, primary_key=True) + Sprache: Mapped[str] + + text: Mapped[List['Text_']] = relationship('Text_', back_populates='sprache') + + +class Textform(Base): + __tablename__ = 'Textform' + + ID: Mapped[int] = mapped_column(Integer, primary_key=True) + Textform: Mapped[str] + + text: Mapped[List['Text_']] = relationship('Text_', back_populates='textform') + + +class Titelbild(Base): + __tablename__ = 'Titelbild' + + ID: Mapped[int] = mapped_column(Integer, primary_key=True) + Mimetype: Mapped[str] + Dateiname: Mapped[str] + Dateigroesse: Mapped[int] + Breite: Mapped[int] + Hoehe: Mapped[int] + Bild: Mapped[bytes] + Thumbnail: Mapped[bytes] + sha256: Mapped[str] = mapped_column(Text, unique=True) + + werk: Mapped[List['Werk']] = relationship('Werk', back_populates='titelbild') def _asdict_with_urls(self): tb = self._asdict() @@ -95,27 +91,117 @@ class Titelbild(db.Model): tb["Thumbnail"] = url_for("titelbild.thumbnail", id=self.ID) return tb -class Text_Genre(db.Model): - __table__ = db.Model.metadata.tables['Text_Genre'] - text = relationship("Text", back_populates="text_genre") - genre = relationship("Genre", back_populates="text_genre") - def __init__(self, genre: int): - self.Genre = genre +class Verlag(Base): + __tablename__ = 'Verlag' -class Werk_Genre(db.Model): - __table__ = db.Model.metadata.tables['Werk_Genre'] - werk = relationship("Werk", back_populates="werk_genre") - genre = relationship("Genre", back_populates="werk_genre") + ID: Mapped[int] = mapped_column(Integer, primary_key=True) + Verlag: Mapped[str] - def __init__(self, genre: int): - self.Genre = genre + reihe: Mapped[List['Reihe']] = relationship('Reihe', back_populates='verlag') + werk: Mapped[List['Werk']] = relationship('Werk', back_populates='verlag') -class Werk_Herausgeber(db.Model): - __table__ = db.Model.metadata.tables['Werk_Herausgeber'] - werk = relationship("Werk", back_populates="werk_herausgeber") - herausgeber = relationship("Herausgeber", back_populates="werk_herausgeber") - def __init__(self, hrsg: int): - self.Herausgeber = hrsg +class Werksform(Base): + __tablename__ = 'Werksform' + ID: Mapped[int] = mapped_column(Integer, primary_key=True) + Werksform: Mapped[str] + + werk: Mapped[List['Werk']] = relationship('Werk', back_populates='werksform') + + +class Reihe(Base): + __tablename__ = 'Reihe' + + ID: Mapped[int] = mapped_column(Integer, primary_key=True) + Titel: Mapped[str] + Verlag: Mapped[Optional[str]] = mapped_column('Verlag', ForeignKey('Verlag.ID')) + + verlag: Mapped[Optional['Verlag']] = relationship('Verlag', back_populates='reihe') + text: Mapped[List['Text_']] = relationship('Text_', back_populates='reihe') + werk: Mapped[List['Werk']] = relationship('Werk', back_populates='reihe') + + +class Text_(Base): + __tablename__ = 'Text' + + ID: Mapped[int] = mapped_column(Integer, primary_key=True) + Titel: Mapped[str] + Untertitel: Mapped[Optional[str]] + Reihe: Mapped[Optional[int]] = mapped_column('Reihe', ForeignKey('Reihe.ID')) + Textform: Mapped[Optional[int]] = mapped_column('Textform', ForeignKey('Textform.ID')) + Sprache: Mapped[Optional[int]] = mapped_column('Sprache', ForeignKey('Sprache.ID')) + + genre: Mapped[List['Genre']] = relationship('Genre', secondary='Text_Genre', back_populates='text') + reihe: Mapped[Optional['Reihe']] = relationship('Reihe', back_populates='text') + sprache: Mapped[Optional['Sprache']] = relationship('Sprache', back_populates='text') + textform: Mapped[Optional['Textform']] = relationship('Textform', back_populates='text') + veroeffentlichung: Mapped[List['Veroeffentlichung']] = relationship('Veroeffentlichung', back_populates='text') + + +class Werk(Base): + __tablename__ = 'Werk' + + ID: Mapped[int] = mapped_column(Integer, primary_key=True) + Titel: Mapped[str] + Untertitel: Mapped[Optional[str]] + Werksform: Mapped[Optional[int]] = mapped_column('Werksform', ForeignKey('Werksform.ID')) + Verlag: Mapped[Optional[int]] = mapped_column('Verlag', ForeignKey('Verlag.ID')) + Reihe: Mapped[Optional[int]] = mapped_column('Reihe', ForeignKey('Reihe.ID')) + Reihennummer: Mapped[Optional[str]] + Erscheinungsdatum: Mapped[Optional[str]] + ISBN_13: Mapped[Optional[str]] + ISBN_10: Mapped[Optional[str]] + ISSN: Mapped[Optional[str]] + Preis: Mapped[Optional[str]] + Titelbild: Mapped[Optional[int]] = mapped_column('Titelbild', ForeignKey('Titelbild.ID')) + Klappentext: Mapped[Optional[str]] + Anmerkungen: Mapped[Optional[str]] + + genre: Mapped[List['Genre']] = relationship('Genre', secondary='Werk_Genre', back_populates='werk') + herausgeber: Mapped[List['Herausgeber']] = relationship('Herausgeber', secondary='Werk_Herausgeber', back_populates='werk') + reihe: Mapped[Optional['Reihe']] = relationship('Reihe', back_populates='werk') + titelbild: Mapped[Optional['Titelbild']] = relationship('Titelbild', back_populates='werk') + verlag: Mapped[Optional['Verlag']] = relationship('Verlag', back_populates='werk') + werksform: Mapped[Optional['Werksform']] = relationship('Werksform', back_populates='werk') + veroeffentlichung: Mapped[List['Veroeffentlichung']] = relationship('Veroeffentlichung', back_populates='werk') + + +t_Text_Genre = Table( + 'Text_Genre', + Base.metadata, + Column('Text', ForeignKey('Text.ID'), primary_key=True), + Column('Genre', ForeignKey('Genre.ID'), primary_key=True) +) + + +class Veroeffentlichung(Base): + __tablename__ = 'Veroeffentlichung' + + ID: Mapped[int] = mapped_column(Integer, primary_key=True) + Text: Mapped[int] = mapped_column('Text', ForeignKey('Text.ID')) + Werk: Mapped[int] = mapped_column('Werk', ForeignKey('Werk.ID')) + Pseudonym: Mapped[int] = mapped_column('Pseudonym', ForeignKey('Pseudonym.ID')) + AltTitel: Mapped[Optional[str]] + AltUntertitel: Mapped[Optional[str]] + + pseudonym: Mapped['Pseudonym'] = relationship('Pseudonym', back_populates='veroeffentlichung') + text: Mapped['Text_'] = relationship('Text_', back_populates='veroeffentlichung') + werk: Mapped['Werk'] = relationship('Werk', back_populates='veroeffentlichung') + + +t_Werk_Genre = Table( + 'Werk_Genre', + Base.metadata, + Column('Werk', ForeignKey('Werk.ID'), primary_key=True), + Column('Genre', ForeignKey('Genre.ID'), primary_key=True) +) + + +t_Werk_Herausgeber = Table( + 'Werk_Herausgeber', + Base.metadata, + Column('Herausgeber', ForeignKey('Herausgeber.ID'), primary_key=True), + Column('Werk', ForeignKey('Werk.ID'), primary_key=True) +) diff --git a/the_works/old_models.py b/the_works/old_models.py new file mode 100644 index 0000000..a188895 --- /dev/null +++ b/the_works/old_models.py @@ -0,0 +1,121 @@ +from the_works.database import db +from sqlalchemy.orm import relationship +from sqlalchemy.ext.associationproxy import association_proxy +from flask import url_for +import sys + +# add method to sqlalchemy.orm.decl_api.Model +def _asdict(self): + d = {} + for col in self.__table__.c: + if type(col.type) == db.types.BLOB: + d[col.key] = "Blob (NULL)" if self.__getattribute__(col.key) == None else f"Blob ({sys.getsizeof(self.__getattribute__(col.key))} Bytes)" + else: + value = str(self.__getattribute__(col.key)) + d[col.key] = value[:50] + '...' if len(value) > 50 else value + return d +db.Model._asdict = _asdict + +# override repr() method from sqlalchemy.orm.decl_api.Model +def __repr__(self): + return f"{type(self).__name__}({str(self._asdict())})" +db.Model.__repr__ = __repr__ + +class Text(db.Model): + __table__ = db.Model.metadata.tables['Text'] + reihe = relationship("Reihe", back_populates="text") + textform = relationship("Textform", back_populates="text") + sprache = relationship("Sprache", back_populates="text") + veroeffentlichung = relationship("Veroeffentlichung", back_populates="text") + text_genre = relationship("Text_Genre", back_populates="text", cascade="save-update, merge, delete, delete-orphan") + genres = association_proxy("text_genre", "Genre") + +class Werk(db.Model): + __table__ = db.Model.metadata.tables['Werk'] + reihe = relationship("Reihe", back_populates="werk") + verlag = relationship("Verlag", back_populates="werk") + werksform = relationship("Werksform", back_populates="werk") + veroeffentlichung = relationship("Veroeffentlichung", back_populates="werk") + werk_genre = relationship("Werk_Genre", back_populates="werk", cascade="save-update, merge, delete, delete-orphan") +# titelbild = relationship("Titelbild", back_populates="werk", cascade="all, delete-orphan", single_parent=True) + titelbild = relationship("Titelbild", back_populates="werk") + genres = association_proxy("werk_genre", "Genre") + werk_herausgeber = relationship("Werk_Herausgeber", back_populates="werk", cascade="save-update, merge, delete, delete-orphan") + herausgeber = association_proxy("werk_herausgeber", "Herausgeber") + +class Veroeffentlichung(db.Model): + __table__ = db.Model.metadata.tables['Veroeffentlichung'] + text = relationship("Text", back_populates="veroeffentlichung") + werk = relationship("Werk", back_populates="veroeffentlichung") + pseudonym = relationship("Pseudonym", back_populates="veroeffentlichung") + +class Reihe(db.Model): + __table__ = db.Model.metadata.tables['Reihe'] + text = relationship("Text", back_populates="reihe") + werk = relationship("Werk", back_populates="reihe") + verlag = relationship("Verlag", back_populates="reihe") + +class Verlag(db.Model): + __table__ = db.Model.metadata.tables['Verlag'] + werk = relationship("Werk", back_populates="verlag") + reihe = relationship("Reihe", back_populates="verlag") + +class Sprache(db.Model): + __table__ = db.Model.metadata.tables['Sprache'] + text = relationship("Text", back_populates="sprache") + +class Textform(db.Model): + __table__ = db.Model.metadata.tables['Textform'] + text = relationship("Text", back_populates="textform") + +class Werksform(db.Model): + __table__ = db.Model.metadata.tables['Werksform'] + werk = relationship("Werk", back_populates="werksform") + +class Genre(db.Model): + __table__ = db.Model.metadata.tables['Genre'] + text_genre = relationship("Text_Genre", back_populates="genre") + werk_genre = relationship("Werk_Genre", back_populates="genre") + +class Pseudonym(db.Model): + __table__ = db.Model.metadata.tables['Pseudonym'] + veroeffentlichung = relationship("Veroeffentlichung", back_populates="pseudonym") + +class Herausgeber(db.Model): + __table__ = db.Model.metadata.tables['Herausgeber'] + werk_herausgeber = relationship("Werk_Herausgeber", back_populates="herausgeber") + +class Titelbild(db.Model): + __table__ = db.Model.metadata.tables['Titelbild'] + werk = relationship("Werk", back_populates="titelbild") + + def _asdict_with_urls(self): + tb = self._asdict() + tb["Bild"] = url_for("titelbild.image", id=self.ID) + tb["Thumbnail"] = url_for("titelbild.thumbnail", id=self.ID) + return tb + +class Text_Genre(db.Model): + __table__ = db.Model.metadata.tables['Text_Genre'] + text = relationship("Text", back_populates="text_genre") + genre = relationship("Genre", back_populates="text_genre") + + def __init__(self, genre: int): + self.Genre = genre + +class Werk_Genre(db.Model): + __table__ = db.Model.metadata.tables['Werk_Genre'] + werk = relationship("Werk", back_populates="werk_genre") + genre = relationship("Genre", back_populates="werk_genre") + + def __init__(self, genre: int): + self.Genre = genre + +class Werk_Herausgeber(db.Model): + __table__ = db.Model.metadata.tables['Werk_Herausgeber'] + werk = relationship("Werk", back_populates="werk_herausgeber") + herausgeber = relationship("Herausgeber", back_populates="werk_herausgeber") + + def __init__(self, hrsg: int): + self.Herausgeber = hrsg + diff --git a/the_works/views/home.py b/the_works/views/home.py index 67ccd71..5b2e584 100644 --- a/the_works/views/home.py +++ b/the_works/views/home.py @@ -6,12 +6,12 @@ import inspect bp = Blueprint("home", __name__) -# prepare list of DB table classes to be searched by search_all() +# prepare list of ORM classes to be searched by search_all() tables = [] for name, obj in inspect.getmembers(the_works.models): - if "_" not in name and inspect.isclass(obj): + if inspect.isclass(obj) and issubclass(obj, the_works.models.Base) and obj.__name__ != "Base": tables.append(obj) -#print(tables) #DEBUG +print(f"tables is {tables}") #DEBUG @bp.route("/") def startpage(): diff --git a/the_works/views/text.py b/the_works/views/text.py index eee7f2d..643be2b 100644 --- a/the_works/views/text.py +++ b/the_works/views/text.py @@ -1,7 +1,7 @@ from flask import Blueprint, render_template, request, redirect, flash, url_for from sqlalchemy import select, insert, update, delete from the_works.database import db -from the_works.models import Text, Reihe, Sprache, Textform, Text_Genre, Genre +from the_works.models import Text_ as Text, Reihe, Sprache, Textform, t_Text_Genre, Genre bp = Blueprint("text", __name__) diff --git a/the_works/views/veroeffentlichung.py b/the_works/views/veroeffentlichung.py index 7ffcc9f..186363e 100644 --- a/the_works/views/veroeffentlichung.py +++ b/the_works/views/veroeffentlichung.py @@ -1,7 +1,7 @@ from flask import Blueprint, render_template, request, redirect, flash, url_for from sqlalchemy import select, insert, update, delete from the_works.database import db -from the_works.models import Veroeffentlichung, Text, Werk, Werksform, Pseudonym +from the_works.models import Veroeffentlichung, Text_ as Text, Werk, Werksform, Pseudonym bp = Blueprint("veroeffentlichung", __name__) diff --git a/the_works/views/werk.py b/the_works/views/werk.py index 57bb1fb..2b48b85 100644 --- a/the_works/views/werk.py +++ b/the_works/views/werk.py @@ -1,7 +1,7 @@ from flask import Blueprint, render_template, request, redirect, flash, url_for from sqlalchemy import select, insert, update, delete from the_works.database import db -from the_works.models import Werk, Reihe, Verlag, Werksform, Werk_Genre, Genre, Werk_Herausgeber, Herausgeber, Titelbild +from the_works.models import Werk, Reihe, Verlag, Werksform, t_Werk_Genre, Genre, t_Werk_Herausgeber, Herausgeber, Titelbild from the_works.views import titelbild as tb bp = Blueprint("werk", __name__)