Merge branch 'move_to_declarative'

This commit is contained in:
eclipse 2025-07-24 11:24:50 +02:00
commit b8edb958d3
28 changed files with 932 additions and 200 deletions

13
.flaskenv Normal file
View File

@ -0,0 +1,13 @@
# Non-critical configuration values
# Read automatically if python-dotenv is installed
FLASK_APP = "the_works"
FLASK_ENV = "development"
FLASK_SECRET_KEY = "f8148ee5d95b0a67122b1cab9993f637a6bf29528f584a9f1575af1a55566748"
FLASK_TESTING = False
#FLASK_MAX_CONTENT_LENGTH = 1024 * 1024
FLASK_DEBUG = True
FLASK_SQLALCHEMY_DATABASE_URI = "sqlite:///../the_works.sqlite"
FLASK_SQLALCHEMY_ECHO = False
FLASK_SQLALCHEMY_RECORD_QUERIES = True

View File

@ -1,32 +1,69 @@
# The Works
# the_works a publication management tool for writers
This software project
* supports managing texts and publications with multiple languages, pseudonyms, genres, publishers, series, editions, and more
* supports storage of cover images
* focuses on ease-of-use, safety, and speed
* written and field-tested by an actual writer
the_works also is
* based on common technologies like Python, SQL, and HTML/JavaScript
* underlying frameworks are Flask and SQLAlchemy
* includes test coverage (using pytest)
## Configuration
The file `.flaskenv` contains the default configuration. Flask reads the file at startup and adds its key-value-pairs to the runtime environment as environment variables. When the Flask app object is being created in `__init__.py`, all environment variables that start with the prefix "FLASK_" get added to the app configuration.
This is true for any prefixed environment variable, not just the ones from `.flaskenv`. It is therefore possible to set additional config parameters by hand before running the app. Just make sure to prefix the variable name with "FLASK_".
Configuration values from the runtime environment can be overridden by using Flask's `-e` command line switch to pass a second config file to the app. This file gets processed the same way as `.flaskenv`, which means that all its keys must be prefixed with "FLASK_". These vars take precedence over the default configuration.
Finally, you can override config settings with Python during the Flask app's instantiation through the factory. To do this, simply pass a dictionary with (unprefixed) key-value-pairs to `create_app()` method as named parameter `config`. Settings passed this way take precedence over those from the default configuration and additional config files.
## Flask commands
Execute commands with `python -m flask --app the_works <command>`
Execute commands with `python -m flask <command>`. You don't need to specify `--app the_works` as long as the environment variable "FLASK_APP" is set to "the_works"; the default configuration file does this.
Available commands:
* `run`: Serve app (don't use for production).
<!--* `init-db`: Create empty SQLite database `works.sqlite` in project root. BE CAREFUL: If a database already exists, it will be deleted with everything in it. // 5/25: ich hab die Fkt. wieder rausgenommen, aber ich könnte sie eigentlich prima wieder einbauen … -->
* `shell`: start a shell within the app context (I can i.e. import specific table models and test the data structures returned by ORM methods like select())
* `shell`: start a shell within the app context (I can i.e. import specific table models and test ORM data structures)
*
## Dependencies
### Python Packages
flask
flask-sqlalchemy
python-dotenv
Pillow
pytest
flask-debugtoolbar (optional)
Required pip packages
### CSS and Javascript
* flask
* flask-sqlalchemy
* python-dotenv
* Pillow
* pytest
PicoCSS (regular version) + SwitchColorMode.js (from Yohn's fork)
DataTables.[js|css]
Optional pip packages
* flask-debugtoolbar (optional)
* sqlacodegen (optional; only used from the command line during development)
### CSS and Javascript resources
* any regular (not classless) stylesheet from [PicoCSS](https://picocss.com) for general styling
* `SwitchColorMode.js` from [Yohn's fork of PicoCSS](https://yohn.github.io/PicoCSS/) to enable color mode switching
* [DataTables](https://datatables.net/) (JS and CSS components) to enable ordering and filtering result tables
* DataTables requires [jQuery](https://jquery.com/), which can either be bundled with DataTables itself or installed as a separate resource
### Icons
@ -37,6 +74,17 @@ some icons from heroicons.com
## Other useful stuff
### Generate SQLAlchemy code from an existing database
Right now the_works reflects an existing database in order to infer the underlying data models for SQLAlchemy. Of course, this only works if all the tables already exist in the database. If the_works is run with an empty database (this happens when running tests, for example), the app will create fresh tables in the database. The necessary information about the tables can be generated from an existing database with the help of [sqlacodegen](https://pypi.org/project/sqlacodegen/). Just run this command inside the project's root directory:
`sqlacodegen --generator tables sqlite:///path/to/good/db.sqlite > ./the_works/tables.py`
The tool sqlacodegen can also generate Python code declaring the data models directly. This would make the use of reflection obsolete. The command would be:
`sqlacodegen --generator declarative sqlite:///path/to/good/db.sqlite > outputfile.py`
### Export database schema
Method 1: `sqlite3 the_works.sqlite .schema > outputfile.sql`
@ -48,13 +96,6 @@ Method 2: Open DB in SQLitebrowser and use File -> Export -> Database to SQL fil
* overwrite old schema (DROP TABLE, then CREATE TABLE)
### Generate declarative SQLAlchemy code
Right now the_works reflects an existing database in order to infer the underlying data models for SQLAlchemy. If I wanted to declare the data models directly instead of using reflection, I would need declarative code. This code can be generated from an existing database with the help of [sqlacodegen](https://pypi.org/project/sqlacodegen/):
`sqlacodegen sqlite:///path/to/db.sqlite > outputfile.py`
### Generate `requirements.txt`
I use [pipreqs](https://pypi.org/project/pipreqs/) to generate the file `requirements.txt`. The package scans all source files for import statements and uses those to extract all required Pip packages.

39
tests/conftest.py Normal file
View File

@ -0,0 +1,39 @@
import pytest
from the_works import create_app
from the_works.database import db as _db
TEST_DATABASE_URI = "sqlite:///:memory:"
@pytest.fixture()
def app():
test_config = {
"ENV": "Testing",
"SQLALCHEMY_DATABASE_URI": TEST_DATABASE_URI,
"SECRET_KEY": "This is my very secret key",
"TESTING": True
}
app = create_app(test_config)
# other setup can go here
yield app
# clean up / reset resources here
@pytest.fixture(scope="function")
def db(app):
with app.app_context():
yield _db
_db.drop_all()
_db.create_all()
@pytest.fixture()
def client(app):
return app.test_client()
@pytest.fixture()
def runner(app):
return app.test_cli_runner()

View File

@ -0,0 +1,27 @@
from sqlalchemy import select
from the_works.database import db
from the_works.models import Genre
from sqlalchemy.exc import IntegrityError
import pytest
def test_genre_create(client, app):
"""Integrated testing of adding a Genre record."""
response = client.post("/genre/create", data={"form_Genre": "Test-Genre"}, follow_redirects=True)
# assert there was exactly 1 redirect
assert len(response.history) == 1
# assert the redirect led to the correct page
assert response.request.path == "/genre/all"
assert response.status_code == 200
# assert record was successfully added to DB
with app.app_context():
genre = db.session.scalars(select(Genre).where(Genre.Genre == "Test-Genre")).all()
assert len(genre) == 1
assert isinstance(genre[0], Genre)
# assert uniqueness of records
with pytest.raises(IntegrityError) as excinfo:
response = client.post("/genre/create", data={"form_Genre": "Test-Genre"})
assert "UNIQUE constraint failed" in str(excinfo.value)

3
tests/unit/test_home.py Normal file
View File

@ -0,0 +1,3 @@
def test_request_home_startpage(client):
response = client.get("/")
assert response.data.startswith(b"<!DOCTYPE html>\n")

View File

@ -0,0 +1,58 @@
from the_works.models import Genre
import pytest
def test_genre_all(client, db, mocker):
"""Test view all() from genre.py."""
# mock database function
# Note: The original method returns an sqlalchemy.engine.Result.ScalarResult, not a list, but the template code
# uses the return value in a way that works for both ScalarResult and list
mocker.patch("the_works.database.db.session.scalars", return_value=[
Genre(ID=4, Genre="bla"),
Genre(ID=26, Genre="blubb")
])
# test case: get request
response = client.get("/genre")
assert response.status_code == 200
assert response.data.count(b'<tr id="genre-') == 2
# test case: post request
response = client.post("/genre")
assert response.status_code == 405
def test_genre_create(client, db, mocker):
"""Test view create() from genre.py."""
# mock database function
mocker.patch("the_works.database.db.session.add")
# test a POST request with good data
response = client.post("/genre/create", data={"form_Genre": "Testname"}, follow_redirects=True)
# exactly 1 redirect
assert len(response.history) == 1
# redirect to the right page
assert response.request.path == "/genre/all"
assert response.status_code == 200
# test a POST request with no form data
with pytest.raises(ValueError) as excinfo:
response = client.post("/genre/create", data={})
assert "value can't be empty" in str(excinfo.value)
# test a POST request with bad form data
with pytest.raises(ValueError) as excinfo:
response = client.post("/genre/create", data={"wrong_key": "Genrename"})
assert "value can't be empty" in str(excinfo.value)
# test a POST request with empty form data
with pytest.raises(ValueError) as excinfo:
response = client.post("/genre/create", data={"form_genre": ""})
assert "value can't be empty" in str(excinfo.value)
# test a GET request
response = client.get("/genre/create", query_string={"form_Genre": "GET-Genre"})
assert response.status_code == 405

View File

@ -0,0 +1,4 @@
def test_home_startpage(client):
response = client.get("/")
assert response.status_code == 200
assert response.data.startswith(b"<!DOCTYPE html>\n")

View File

@ -1,25 +1,35 @@
import os
from dotenv import load_dotenv
from flask import Flask
from the_works.database import init_db
from flask_debugtoolbar import DebugToolbarExtension
def create_app():
# this import is not strictly necessary but it forces pipreqs-to include dotenv when generating `requirements.txt`
import dotenv
from the_works.database import init_db
from the_works.views import home, text, werk, verlag, sprache, textform, werksform, genre, pseudonym, reihe, herausgeber, veroeffentlichung, titelbild
#from flask_debugtoolbar import DebugToolbarExtension
def create_app(config=None):
app = Flask(__name__)
# read config values
load_dotenv()
# read all config values from environment that are prefixed with "FLASK_"
app.config.from_prefixed_env()
if os.getenv("SQLALCHEMY_DATABASE_DIALECT") == "sqlite":
app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///" + os.path.abspath(app.root_path + "/..") + "/" + os.getenv("SQLALCHEMY_DATABASE_SQLITE_FILENAME")
else:
exit("no SQLite database URI given; exiting")
# some #DEBUG configuration
# toolbar = DebugToolbarExtension(app) #DEBUG
# app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False #DEBUG
# use config from function parameter if present
if config:
app.config.update(config)
# some #DEBUG output
print(f"Current Environment: {app.config['ENV'] if 'ENV' in app.config.keys() else 'ENV is not set'}") #DEBUG
print(app.config) #DEBUG
# initialize database
init_db(app)
# register blueprints
from the_works.views import home, text, werk, verlag, sprache, textform, werksform, genre, pseudonym, reihe, herausgeber, veroeffentlichung, titelbild
app.register_blueprint(genre.bp)
app.register_blueprint(herausgeber.bp)
app.register_blueprint(home.bp)
@ -34,24 +44,15 @@ def create_app():
app.register_blueprint(veroeffentlichung.bp)
app.register_blueprint(titelbild.bp)
app.config["MAX_CONTENT_LENGTH"] = 2 * 1024 * 1024
### DEBUG
# toolbar = DebugToolbarExtension(app)
app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False
app.config["SQLALCHEMY_ECHO"] = True
app.config['SQLALCHEMY_RECORD_QUERIES'] = os.getenv("SQLALCHEMY_RECORD_QUERIES")
print(f"Current Environment: " + app.config['ENVIRONMENT'])
# register helper function
app.jinja_env.globals.update(sizeof_fmt=sizeof_fmt)
return app
# helper function to print formatted file size from https://stackoverflow.com/a/1094933
def sizeof_fmt(num, suffix="B"):
if type(num) == "String":
# helper function to print formatted file size; [source](https://stackoverflow.com/a/1094933)
def sizeof_fmt(num: int | str, suffix: str = "B") -> str:
if isinstance(num, str):
num = int(num)
for unit in ("", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"):
if abs(num) < 1024.0:

View File

@ -1,8 +1,13 @@
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.reflect()
db.create_all()

View File

@ -1,121 +1,279 @@
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
from typing import List, Optional
from sqlalchemy import ForeignKey, types
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, validates
from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
from flask import url_for
# 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) -> dict:
d = {}
for col in self.__table__.c:
if isinstance(col.type, types.BLOB):
d[col.key] = "Blob (NULL)" if self.__getattribute__(col.key) is None else f"Blob ({sys.getsizeof(self.__getattribute__(col.key))} Bytes)"
else:
if isinstance(value := self.__getattribute__(col.key), str) and len(value) > 50:
d[col.key] = value[:48] + '...'
else:
d[col.key] = 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) -> str:
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")
def validate_not_empty(self, value):
if not value:
raise ValueError("value can't be empty")
return value
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 Genre(Base):
__tablename__ = 'Genre'
class Verlag(db.Model):
__table__ = db.Model.metadata.tables['Verlag']
werk = relationship("Werk", back_populates="verlag")
reihe = relationship("Reihe", back_populates="verlag")
ID: Mapped[int] = mapped_column(primary_key=True)
Genre: Mapped[str] = mapped_column(unique=True)
class Sprache(db.Model):
__table__ = db.Model.metadata.tables['Sprache']
text = relationship("Text", back_populates="sprache")
texte: Mapped[List['Text_Genre']] = relationship(back_populates='genre')
werke: Mapped[List['Werk_Genre']] = relationship(back_populates='genre')
class Textform(db.Model):
__table__ = db.Model.metadata.tables['Textform']
text = relationship("Text", back_populates="textform")
@validates("Genre")
def validate_genre(self, key, value):
return self.validate_not_empty(value)
class Werksform(db.Model):
__table__ = db.Model.metadata.tables['Werksform']
werk = relationship("Werk", back_populates="werksform")
class Herausgeber(Base):
__tablename__ = 'Herausgeber'
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")
ID: Mapped[int] = mapped_column(primary_key=True)
Name: Mapped[str] = mapped_column(unique=True)
class Pseudonym(db.Model):
__table__ = db.Model.metadata.tables['Pseudonym']
veroeffentlichung = relationship("Veroeffentlichung", back_populates="pseudonym")
werke: Mapped[List['Werk_Herausgeber']] = relationship(back_populates='herausgeber')
class Herausgeber(db.Model):
__table__ = db.Model.metadata.tables['Herausgeber']
werk_herausgeber = relationship("Werk_Herausgeber", back_populates="herausgeber")
@validates("Herausgeber")
def validate_herausgeber(self, key, value):
return self.validate_not_empty(value)
class Titelbild(db.Model):
__table__ = db.Model.metadata.tables['Titelbild']
werk = relationship("Werk", back_populates="titelbild")
def _asdict_with_urls(self):
tb = self._asdict()
class Pseudonym(Base):
__tablename__ = 'Pseudonym'
ID: Mapped[int] = mapped_column(primary_key=True)
Pseudonym: Mapped[str] = mapped_column(unique=True)
veroeffentlichung: Mapped[List['Veroeffentlichung']] = relationship(back_populates='pseudonym')
@validates("Pseudonym")
def validate_pseudonym(self, key, value):
return self.validate_not_empty(value)
class Sprache(Base):
__tablename__ = 'Sprache'
ID: Mapped[int] = mapped_column(primary_key=True)
Sprache: Mapped[str] = mapped_column(unique=True)
text: Mapped[List['Text']] = relationship(back_populates='sprache')
@validates("Sprache")
def validate_sprache(self, key, value):
return self.validate_not_empty(value)
class Textform(Base):
__tablename__ = 'Textform'
ID: Mapped[int] = mapped_column(primary_key=True)
Textform: Mapped[str] = mapped_column(unique=True)
text: Mapped[List['Text']] = relationship(back_populates='textform')
@validates("Textform")
def validate_textform(self, key, value):
return self.validate_not_empty(value)
class Titelbild(Base):
__tablename__ = 'Titelbild'
ID: Mapped[int] = mapped_column(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(unique=True)
werk: Mapped[List['Werk']] = relationship(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")
@validates("sha256")
def validate_titelbild(self, key, value):
return self.validate_not_empty(value)
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")
class Verlag(Base):
__tablename__ = 'Verlag'
def __init__(self, genre: int):
self.Genre = genre
ID: Mapped[int] = mapped_column(primary_key=True)
Verlag: Mapped[str] = mapped_column(unique=True)
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")
reihe: Mapped[List['Reihe']] = relationship(back_populates='verlag')
werk: Mapped[List['Werk']] = relationship(back_populates='verlag')
def __init__(self, hrsg: int):
self.Herausgeber = hrsg
@validates("Verlag")
def validate_verlag(self, key, value):
return self.validate_not_empty(value)
class Werksform(Base):
__tablename__ = 'Werksform'
ID: Mapped[int] = mapped_column(primary_key=True)
Werksform: Mapped[str] = mapped_column(unique=True)
werk: Mapped[List['Werk']] = relationship(back_populates='werksform')
@validates("Werksform")
def validate_werksform(self, key, value):
return self.validate_not_empty(value)
class Reihe(Base):
__tablename__ = 'Reihe'
ID: Mapped[int] = mapped_column(primary_key=True)
Titel: Mapped[str]
Verlag: Mapped[Optional[str]] = mapped_column(ForeignKey('Verlag.ID'))
verlag: Mapped['Verlag'] = relationship(back_populates='reihe')
text: Mapped[List['Text']] = relationship(back_populates='reihe')
werk: Mapped[List['Werk']] = relationship(back_populates='reihe')
@validates("Titel")
def validate_titel(self, key, value):
return self.validate_not_empty(value)
class Text(Base):
__tablename__ = 'Text'
# regular columns
ID: Mapped[int] = mapped_column(primary_key=True)
Titel: Mapped[str]
Untertitel: Mapped[Optional[str]]
# many-to-one
Reihe: Mapped[Optional[int]] = mapped_column(ForeignKey('Reihe.ID'))
reihe: Mapped[Optional["Reihe"]] = relationship(back_populates="text")
Sprache: Mapped[int] = mapped_column(ForeignKey('Sprache.ID'))
sprache: Mapped['Sprache'] = relationship(back_populates='text')
Textform: Mapped[int] = mapped_column(ForeignKey('Textform.ID'))
textform: Mapped['Textform'] = relationship(back_populates='text')
# one-to-many
veroeffentlichung: Mapped[List['Veroeffentlichung']] = relationship(back_populates='text')
# many-to-many
genres: Mapped[List['Text_Genre']] = relationship(back_populates='text', cascade="all, delete-orphan")
genre_ids: AssociationProxy[List["Genre"]] = association_proxy("genres", "Genre", creator=lambda genre_id: Text_Genre(Genre=genre_id))
@validates("Titel")
def validate_titel(self, key, value):
return self.validate_not_empty(value)
class Werk(Base):
__tablename__ = 'Werk'
# regular columns
ID: Mapped[int] = mapped_column(primary_key=True)
Titel: Mapped[str]
Untertitel: Mapped[Optional[str]]
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]]
Klappentext: Mapped[Optional[str]]
Anmerkungen: Mapped[Optional[str]]
# many-to-one
Reihe: Mapped[Optional[int]] = mapped_column(ForeignKey('Reihe.ID'))
reihe: Mapped[Optional['Reihe']] = relationship(back_populates='werk')
Titelbild: Mapped[Optional[int]] = mapped_column(ForeignKey('Titelbild.ID'))
titelbild: Mapped[Optional['Titelbild']] = relationship(back_populates='werk')
Verlag: Mapped[Optional[int]] = mapped_column(ForeignKey('Verlag.ID'))
verlag: Mapped[Optional['Verlag']] = relationship(back_populates='werk')
Werksform: Mapped[int] = mapped_column(ForeignKey('Werksform.ID'))
werksform: Mapped['Werksform'] = relationship(back_populates='werk')
# one-to-many
veroeffentlichung: Mapped[List['Veroeffentlichung']] = relationship(back_populates='werk')
# many-to-many
genres: Mapped[List['Werk_Genre']] = relationship(back_populates='werk', cascade='all, delete-orphan')
genre_ids: AssociationProxy[List["Genre"]] = association_proxy("genres", "Genre", creator=lambda genre_id: Werk_Genre(Genre=genre_id))
herausgeber: Mapped[List['Werk_Herausgeber']] = relationship(back_populates='werk', cascade="all, delete-orphan")
herausgeber_ids: AssociationProxy[List['Herausgeber']] = association_proxy("herausgeber", "Herausgeber", creator=lambda hrsg_id: Werk_Herausgeber(Herausgeber=hrsg_id))
@validates("Titel")
def validate_titel(self, key, value):
return self.validate_not_empty(value)
class Veroeffentlichung(Base):
__tablename__ = 'Veroeffentlichung'
# regular columns
ID: Mapped[int] = mapped_column(primary_key=True)
AltTitel: Mapped[Optional[str]]
AltUntertitel: Mapped[Optional[str]]
# many-to-one
Pseudonym: Mapped[int] = mapped_column(ForeignKey('Pseudonym.ID'))
pseudonym: Mapped['Pseudonym'] = relationship(back_populates='veroeffentlichung')
Text: Mapped[int] = mapped_column(ForeignKey('Text.ID'))
text: Mapped['Text'] = relationship(back_populates='veroeffentlichung')
Werk: Mapped[int] = mapped_column(ForeignKey('Werk.ID'))
werk: Mapped['Werk'] = relationship(back_populates='veroeffentlichung')
class Text_Genre(Base):
__tablename__ = 'Text_Genre'
Text: Mapped[int] = mapped_column(ForeignKey("Text.ID"), primary_key=True)
Genre: Mapped[int] = mapped_column(ForeignKey("Genre.ID"), primary_key=True)
text: Mapped['Text'] = relationship(back_populates="genres")
genre: Mapped['Genre'] = relationship(back_populates="texte")
class Werk_Genre(Base):
__tablename__ = 'Werk_Genre'
Werk: Mapped[int] = mapped_column(ForeignKey('Werk.ID'), primary_key=True)
Genre: Mapped[int] = mapped_column(ForeignKey("Genre.ID"), primary_key=True)
werk: Mapped['Werk'] = relationship(back_populates="genres")
genre: Mapped['Genre'] = relationship(back_populates="werke")
class Werk_Herausgeber(Base):
__tablename__ = 'Werk_Herausgeber'
Werk: Mapped[int] = mapped_column(ForeignKey('Werk.ID'), primary_key=True)
Herausgeber: Mapped[int] = mapped_column(ForeignKey("Herausgeber.ID"), primary_key=True)
werk: Mapped['Werk'] = relationship(back_populates="herausgeber")
herausgeber: Mapped['Herausgeber'] = relationship(back_populates="werke")

121
the_works/old_models.py Normal file
View File

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

125
the_works/tables.py Normal file
View File

@ -0,0 +1,125 @@
# this is the verbatim output of `sqlacodegen --generator tables sqlite:///path/to/the_works.sqlite`, run inside project root
from sqlalchemy import Column, ForeignKey, Integer, LargeBinary, MetaData, Table, Text
metadata = MetaData()
t_Genre = Table(
'Genre', metadata,
Column('ID', Integer, primary_key=True),
Column('Genre', Text, nullable=False)
)
t_Herausgeber = Table(
'Herausgeber', metadata,
Column('ID', Integer, primary_key=True),
Column('Name', Text, nullable=False)
)
t_Pseudonym = Table(
'Pseudonym', metadata,
Column('ID', Integer, primary_key=True),
Column('Pseudonym', Text, nullable=False)
)
t_Sprache = Table(
'Sprache', metadata,
Column('ID', Integer, primary_key=True),
Column('Sprache', Text, nullable=False)
)
t_Textform = Table(
'Textform', metadata,
Column('ID', Integer, primary_key=True),
Column('Textform', Text, nullable=False)
)
t_Titelbild = Table(
'Titelbild', metadata,
Column('ID', Integer, primary_key=True),
Column('Mimetype', Text, nullable=False),
Column('Dateiname', Text, nullable=False),
Column('Dateigroesse', Integer, nullable=False),
Column('Breite', Integer, nullable=False),
Column('Hoehe', Integer, nullable=False),
Column('Bild', LargeBinary, nullable=False),
Column('Thumbnail', LargeBinary, nullable=False),
Column('sha256', Text, nullable=False, unique=True)
)
t_Verlag = Table(
'Verlag', metadata,
Column('ID', Integer, primary_key=True),
Column('Verlag', Text, nullable=False)
)
t_Werksform = Table(
'Werksform', metadata,
Column('ID', Integer, primary_key=True),
Column('Werksform', Text, nullable=False)
)
t_Reihe = Table(
'Reihe', metadata,
Column('ID', Integer, primary_key=True),
Column('Titel', Text, nullable=False),
Column('Verlag', ForeignKey('Verlag.ID'))
)
t_Text = Table(
'Text', metadata,
Column('ID', Integer, primary_key=True),
Column('Titel', Text, nullable=False),
Column('Untertitel', Text),
Column('Reihe', ForeignKey('Reihe.ID')),
Column('Textform', ForeignKey('Textform.ID')),
Column('Sprache', ForeignKey('Sprache.ID'))
)
t_Werk = Table(
'Werk', metadata,
Column('ID', Integer, primary_key=True),
Column('Titel', Text, nullable=False),
Column('Untertitel', Text),
Column('Werksform', ForeignKey('Werksform.ID')),
Column('Verlag', ForeignKey('Verlag.ID')),
Column('Reihe', ForeignKey('Reihe.ID')),
Column('Reihennummer', Text),
Column('Erscheinungsdatum', Text),
Column('ISBN_13', Text),
Column('ISBN_10', Text),
Column('ISSN', Text),
Column('Preis', Text),
Column('Titelbild', ForeignKey('Titelbild.ID')),
Column('Klappentext', Text),
Column('Anmerkungen', Text)
)
t_Text_Genre = Table(
'Text_Genre', metadata,
Column('Text', ForeignKey('Text.ID'), primary_key=True),
Column('Genre', ForeignKey('Genre.ID'), primary_key=True)
)
t_Veroeffentlichung = Table(
'Veroeffentlichung', metadata,
Column('ID', Integer, primary_key=True),
Column('Text', ForeignKey('Text.ID'), nullable=False),
Column('Werk', ForeignKey('Werk.ID'), nullable=False),
Column('AltTitel', Text),
Column('AltUntertitel', Text),
Column('Pseudonym', ForeignKey('Pseudonym.ID'), nullable=False)
)
t_Werk_Genre = Table(
'Werk_Genre', metadata,
Column('Werk', ForeignKey('Werk.ID'), primary_key=True),
Column('Genre', ForeignKey('Genre.ID'), primary_key=True)
)
t_Werk_Herausgeber = Table(
'Werk_Herausgeber', metadata,
Column('Herausgeber', ForeignKey('Herausgeber.ID'), primary_key=True),
Column('Werk', ForeignKey('Werk.ID'), primary_key=True)
)

View File

@ -1,5 +1,5 @@
from flask import Blueprint, render_template, request, redirect, flash, url_for
from sqlalchemy import select, insert, update, delete
from sqlalchemy import select
from the_works.database import db
from the_works.models import Genre
@ -12,7 +12,7 @@ def all():
@bp.route("/genre/create", methods=["POST"])
def create():
db.session.add(Genre(Genre = request.form["form_Genre"]))
db.session.add(Genre(Genre = request.form.get("form_Genre", default=None)))
db.session.commit()
flash("Eintrag erfolgreich hinzugefügt")
return redirect(url_for("genre.all"), code=303)
@ -20,7 +20,7 @@ def create():
@bp.route("/genre/update/<int:id>", methods=["POST"])
def update(id):
genre = db.session.get(Genre, id)
genre.Genre = request.form["form_Genre"]
genre.Genre = request.form.get("form_Genre", default=None)
db.session.commit()
flash("Eintrag erfolgreich geändert")
return redirect(url_for("genre.all"), code=303)

View File

@ -1,5 +1,5 @@
from flask import Blueprint, render_template, request, redirect, flash, url_for
from sqlalchemy import select, insert, update, delete
from sqlalchemy import select
from the_works.database import db
from the_works.models import Herausgeber

View File

@ -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():
@ -40,11 +40,11 @@ def search_all():
continue
if matchCase:
if s in row[0].__getattribute__(column):
hits.append(row[0]._asdict())
hits.append(row[0].asdict())
break
else:
if s.lower() in row[0].__getattribute__(column).lower():
hits.append(row[0]._asdict())
hits.append(row[0].asdict())
break
if hits != []:
result[table.__table__.fullname] = hits

View File

@ -1,5 +1,5 @@
from flask import Blueprint, render_template, request, redirect, flash, url_for
from sqlalchemy import select, insert, update, delete
from sqlalchemy import select
from the_works.database import db
from the_works.models import Pseudonym

View File

@ -1,5 +1,5 @@
from flask import Blueprint, render_template, request, redirect, flash, url_for
from sqlalchemy import select, insert, update, delete
from sqlalchemy import select
from the_works.database import db
from the_works.models import Reihe, Verlag

View File

@ -1,5 +1,5 @@
from flask import Blueprint, render_template, request, redirect, flash, url_for
from sqlalchemy import select, insert, update, delete
from sqlalchemy import select
from the_works.database import db
from the_works.models import Sprache

View File

@ -1,7 +1,7 @@
from flask import Blueprint, render_template, request, redirect, flash, url_for
from sqlalchemy import select, insert, update, delete
from sqlalchemy import select
from the_works.database import db
from the_works.models import Text, Reihe, Sprache, Textform, Text_Genre, Genre
from the_works.models import Text, Reihe, Sprache, Textform, Genre
bp = Blueprint("text", __name__)
@ -23,8 +23,7 @@ def all():
"tf_id": row.Text.Textform,
"Sprache": row.Sprache.Sprache,
"s_id": row.Text.Sprache,
"Genre_list": [tg.genre.Genre for tg in row.Text.text_genre],
"g_id_list": row.Text.genres
"Genre_list": [tg.genre.Genre for tg in row.Text.genres]
})
return render_template("views/text.html", texte=texte)
@ -35,15 +34,11 @@ def read(id):
return render_template("views/text_detail.html", text={"ID": 0}, reihen=db.session.scalars(select(Reihe)), textformen=db.session.scalars(select(Textform)), sprachen=db.session.scalars(select(Sprache)), genres=db.session.scalars(select(Genre)))
# all other ids -> update existing entry
t = db.session.get(Text, id)
text = {
"ID": t.ID,
"Titel": t.Titel,
"Untertitel": t.Untertitel or "",
"Reihe": t.Reihe or "",
"Textform": t.Textform,
"Sprache": t.Sprache,
"Genres": t.genres
}
if not t:
raise ValueError(f"Text with ID {id} not found")
text = t.asdict()
text["Genres"] = t.genre_ids
return render_template("views/text_detail.html", text=text, reihen=db.session.scalars(select(Reihe)), textformen=db.session.scalars(select(Textform)), sprachen=db.session.scalars(select(Sprache)), genres=db.session.scalars(select(Genre)))
@bp.route("/text/create", methods=["POST"])
@ -56,7 +51,7 @@ def create():
Sprache = request.form["form_Sprache"]
)
for g in request.form.getlist("form_Genres"):
text.genres.append(g)
text.genre_ids.append(int(g))
db.session.add(text)
db.session.commit()
flash("Eintrag erfolgreich hinzugefügt")
@ -75,11 +70,11 @@ def update(id):
text.Sprache = request.form["form_Sprache"]
# update genre list by removing genres not in form selection and adding selected ones not currently in list
form_set = set(map(lambda g: int(g), request.form.getlist("form_Genre")))
for g in set(text.genres) - form_set:
text.genres.remove(g)
for g in form_set - set(text.genres):
text.genres.append(g)
form_set = set(map(int, request.form.getlist("form_Genre")))
for g in set(text.genre_ids) - form_set:
text.genre_ids.remove(g)
for g in form_set - set(text.genre_ids):
text.genre_ids.append(g)
# commit changes
db.session.commit()

View File

@ -1,5 +1,5 @@
from flask import Blueprint, render_template, request, redirect, flash, url_for
from sqlalchemy import select, insert, update, delete
from sqlalchemy import select
from the_works.database import db
from the_works.models import Textform

View File

@ -14,7 +14,7 @@ bp = Blueprint("titelbild", __name__)
@bp.route("/titelbild")
@bp.route("/titelbild/all")
def all():
return render_template("views/titelbild.html", titelbilder=map(lambda t: t._asdict_with_urls(), db.session.scalars(select(Titelbild))))
return render_template("views/titelbild.html", titelbilder=map(lambda t: t.asdict_with_urls(), db.session.scalars(select(Titelbild))))
@bp.route("/titelbild/image/<int:id>")

View File

@ -1,5 +1,5 @@
from flask import Blueprint, render_template, request, redirect, flash, url_for
from sqlalchemy import select, insert, update, delete
from sqlalchemy import select
from the_works.database import db
from the_works.models import Verlag

View File

@ -1,5 +1,5 @@
from flask import Blueprint, render_template, request, redirect, flash, url_for
from sqlalchemy import select, insert, update, delete
from sqlalchemy import select
from the_works.database import db
from the_works.models import Veroeffentlichung, Text, Werk, Werksform, Pseudonym

View File

@ -1,8 +1,7 @@
from flask import Blueprint, render_template, request, redirect, flash, url_for
from sqlalchemy import select, insert, update, delete
from sqlalchemy import select
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.views import titelbild as tb
from the_works.models import Werk, Reihe, Verlag, Werksform, Genre, Herausgeber, Titelbild
bp = Blueprint("werk", __name__)
@ -29,11 +28,11 @@ def all():
"ISSN": row.Werk.ISSN or "",
"Preis": row.Werk.Preis or "",
# "Titelbild": url_for("titelbild.thumbnail", id=row.Werk.Titelbild) if row.Werk.Titelbild else "",
"Titelbild": db.session.get(Titelbild, row.Werk.Titelbild)._asdict_with_urls() if row.Werk.Titelbild else "",
"Titelbild": db.session.get(Titelbild, row.Werk.Titelbild).asdict_with_urls() if row.Werk.Titelbild else "",
"Klappentext": row.Werk.Klappentext or "",
"Anmerkungen": row.Werk.Anmerkungen or "",
"Herausgeber_list": [wh.herausgeber.Name for wh in row.Werk.werk_herausgeber],
"Genre_list": [wg.genre.Genre for wg in row.Werk.werk_genre],
"Herausgeber_list": [wh.herausgeber.Name for wh in row.Werk.herausgeber],
"Genre_list": [wg.genre.Genre for wg in row.Werk.genres],
})
return render_template("views/werk.html", werke=werke)
@ -41,17 +40,19 @@ def all():
@bp.route("/werk/read/<int:id>")
def read(id):
# prepare Titelbilder as dict including URLs for thumbnail and full pic
titelbilder = map(lambda t: t._asdict_with_urls(), db.session.scalars(select(Titelbild)))
titelbilder = map(lambda t: t.asdict_with_urls(), db.session.scalars(select(Titelbild)))
# id of zero -> return empty data
if id == 0:
return render_template("views/werk_detail.html", werk={"ID": 0, "Erscheinungsdatum": ""}, reihen=db.session.scalars(select(Reihe)), verlage=db.session.scalars(select(Verlag)), werksformen=db.session.scalars(select(Werksform)), genres=db.session.scalars(select(Genre)), hrsg=db.session.scalars(select(Herausgeber)), titelbilder=titelbilder)
# all other ids -> read existing entry from DB and return as dict
werk = db.session.get(Werk, id)
if not werk:
w = db.session.get(Werk, id)
if not w:
raise ValueError(f"Werk with ID {id} not found")
werk = werk._asdict()
werk = w.asdict()
werk["Genres"] = w.genre_ids
werk["Herausgeber"] = w.herausgeber_ids
return render_template("views/werk_detail.html", werk=werk, reihen=db.session.scalars(select(Reihe)), verlage=db.session.scalars(select(Verlag)), werksformen=db.session.scalars(select(Werksform)), genres=db.session.scalars(select(Genre)), hrsg=db.session.scalars(select(Herausgeber)), titelbilder=titelbilder)
@ -75,9 +76,9 @@ def create():
Anmerkungen = request.form["form_Anmerkungen"] or None
)
for g in request.form.getlist("form_Genre"):
werk.genres.append(g)
werk.genre_ids.append(g)
for h in request.form.getlist("form_Herausgeber"):
werk.herausgeber.append(h)
werk.herausgeber_ids.append(h)
db.session.add(werk)
db.session.commit()
flash("Eintrag erfolgreich hinzugefügt")
@ -106,18 +107,18 @@ def update(id):
werk.Anmerkungen = request.form["form_Anmerkungen"] or None
# update associated values: Genre
form_set = set(map(lambda g: int(g), request.form.getlist("form_Genre")))
for g in set(werk.genres) - form_set:
werk.genres.remove(g)
for g in form_set - set(werk.genres):
werk.genres.append(g)
form_set = set(map(int, request.form.getlist("form_Genre")))
for g in set(werk.genre_ids) - form_set:
werk.genre_ids.remove(g)
for g in form_set - set(werk.genre_ids):
werk.genre_ids.append(g)
# update associated values: Herausgeber
form_set = set(map(lambda h: int(h), request.form.getlist("form_Herausgeber")))
for h in set(werk.herausgeber) - form_set:
werk.herausgeber.remove(h)
for h in form_set - set(werk.herausgeber):
werk.herausgeber.append(h)
form_set = set(map(int, request.form.getlist("form_Herausgeber")))
for h in set(werk.herausgeber_ids) - form_set:
werk.herausgeber_ids.remove(h)
for h in form_set - set(werk.herausgeber_ids):
werk.herausgeber_ids.append(h)
# commit changes
db.session.commit()

View File

@ -1,5 +1,5 @@
from flask import Blueprint, render_template, request, redirect, flash, url_for
from sqlalchemy import select, insert, update, delete
from sqlalchemy import select
from the_works.database import db
from the_works.models import Werksform

14
tmp.py Normal file
View File

@ -0,0 +1,14 @@
from flask import Flask
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite://"
app.config["SQLALCHEMY_ECHO"] = True
from flask_sqlalchemy import SQLAlchemy
from the_works.models import Base
db = SQLAlchemy(model_class=Base)
db.init_app(app)
with app.app_context():
db.create_all()

View File

@ -1,3 +1,5 @@
# File contains 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

125
utils/sqlacodegen_tables.py Normal file
View File

@ -0,0 +1,125 @@
# File contains output of `sqlacodegen --generator tables sqlite:///path/to/the_works.sqlite`
from sqlalchemy import Column, ForeignKey, Integer, LargeBinary, MetaData, Table, Text
metadata = MetaData()
t_Genre = Table(
'Genre', metadata,
Column('ID', Integer, primary_key=True),
Column('Genre', Text, nullable=False)
)
t_Herausgeber = Table(
'Herausgeber', metadata,
Column('ID', Integer, primary_key=True),
Column('Name', Text, nullable=False)
)
t_Pseudonym = Table(
'Pseudonym', metadata,
Column('ID', Integer, primary_key=True),
Column('Pseudonym', Text, nullable=False)
)
t_Sprache = Table(
'Sprache', metadata,
Column('ID', Integer, primary_key=True),
Column('Sprache', Text, nullable=False)
)
t_Textform = Table(
'Textform', metadata,
Column('ID', Integer, primary_key=True),
Column('Textform', Text, nullable=False)
)
t_Titelbild = Table(
'Titelbild', metadata,
Column('ID', Integer, primary_key=True),
Column('Mimetype', Text, nullable=False),
Column('Dateiname', Text, nullable=False),
Column('Dateigroesse', Integer, nullable=False),
Column('Breite', Integer, nullable=False),
Column('Hoehe', Integer, nullable=False),
Column('Bild', LargeBinary, nullable=False),
Column('Thumbnail', LargeBinary, nullable=False),
Column('sha256', Text, nullable=False, unique=True)
)
t_Verlag = Table(
'Verlag', metadata,
Column('ID', Integer, primary_key=True),
Column('Verlag', Text, nullable=False)
)
t_Werksform = Table(
'Werksform', metadata,
Column('ID', Integer, primary_key=True),
Column('Werksform', Text, nullable=False)
)
t_Reihe = Table(
'Reihe', metadata,
Column('ID', Integer, primary_key=True),
Column('Titel', Text, nullable=False),
Column('Verlag', ForeignKey('Verlag.ID'))
)
t_Text = Table(
'Text', metadata,
Column('ID', Integer, primary_key=True),
Column('Titel', Text, nullable=False),
Column('Untertitel', Text),
Column('Reihe', ForeignKey('Reihe.ID')),
Column('Textform', ForeignKey('Textform.ID')),
Column('Sprache', ForeignKey('Sprache.ID'))
)
t_Werk = Table(
'Werk', metadata,
Column('ID', Integer, primary_key=True),
Column('Titel', Text, nullable=False),
Column('Untertitel', Text),
Column('Werksform', ForeignKey('Werksform.ID')),
Column('Verlag', ForeignKey('Verlag.ID')),
Column('Reihe', ForeignKey('Reihe.ID')),
Column('Reihennummer', Text),
Column('Erscheinungsdatum', Text),
Column('ISBN_13', Text),
Column('ISBN_10', Text),
Column('ISSN', Text),
Column('Preis', Text),
Column('Titelbild', ForeignKey('Titelbild.ID')),
Column('Klappentext', Text),
Column('Anmerkungen', Text)
)
t_Text_Genre = Table(
'Text_Genre', metadata,
Column('Text', ForeignKey('Text.ID'), primary_key=True),
Column('Genre', ForeignKey('Genre.ID'), primary_key=True)
)
t_Veroeffentlichung = Table(
'Veroeffentlichung', metadata,
Column('ID', Integer, primary_key=True),
Column('Text', ForeignKey('Text.ID'), nullable=False),
Column('Werk', ForeignKey('Werk.ID'), nullable=False),
Column('AltTitel', Text),
Column('AltUntertitel', Text),
Column('Pseudonym', ForeignKey('Pseudonym.ID'), nullable=False)
)
t_Werk_Genre = Table(
'Werk_Genre', metadata,
Column('Werk', ForeignKey('Werk.ID'), primary_key=True),
Column('Genre', ForeignKey('Genre.ID'), primary_key=True)
)
t_Werk_Herausgeber = Table(
'Werk_Herausgeber', metadata,
Column('Herausgeber', ForeignKey('Herausgeber.ID'), primary_key=True),
Column('Werk', ForeignKey('Werk.ID'), primary_key=True)
)