From e57c638683243cf390cdc482e5ce2b4720d6d8dc Mon Sep 17 00:00:00 2001 From: h7x4 Date: Mon, 1 May 2023 01:37:52 +0200 Subject: [PATCH] Misc changes: - Update database models to the new declarative mapping syntax See https://docs.sqlalchemy.org/en/20/orm/mapping_styles.html#orm-declarative-mapping - Make use of Flask-SQLAlchemy to manage database connections - Remove cli command interface in favor of just deleting the sqlite file and reseeding when no tables are detected. - Add list of iso639_1 language codes and names, and add to seed list - Other changes to file structure. --- .../arbeidsrom_smal_hylle_5.csv | 0 .../arbeidsrom_smal_hylle_5.jsonl | 0 data/iso639_1.csv | 185 ++++++++++++++++++ run.sh | 2 +- worblehat/__init__.py | 103 ---------- worblehat/database.py | 30 +-- worblehat/flaskapp.py | 40 ++++ worblehat/models/Author.py | 34 ++++ worblehat/models/Base.py | 40 ++++ worblehat/models/Bookcase.py | 37 ++-- worblehat/models/BookcaseItem.py | 62 ++++++ worblehat/models/BookcaseLocation.py | 40 ++++ worblehat/models/Category.py | 44 +++-- worblehat/models/Item.py | 27 --- worblehat/models/Language.py | 31 +-- worblehat/models/Location.py | 19 -- worblehat/models/MediaType.py | 34 ++-- worblehat/models/__init__.py | 8 + worblehat/models/mixins/UidMixin.py | 30 +++ worblehat/models/mixins/UniqueNameMixin.py | 30 +++ worblehat/models/mixins/XrefMixin.py | 6 + worblehat/models/mixins/__init__.py | 2 + worblehat/models/xref_tables/Item_Author.py | 15 ++ worblehat/models/xref_tables/Item_Category.py | 15 ++ worblehat/models/xref_tables/__init__.py | 2 + worblehat/seed_test_data.py | 42 ++++ 26 files changed, 651 insertions(+), 227 deletions(-) rename {data_fetcher/bokhyller => data}/arbeidsrom_smal_hylle_5.csv (100%) rename data_fetcher/bokhyller/arbeidsrom_smal_hylle_5.dat => data/arbeidsrom_smal_hylle_5.jsonl (100%) create mode 100644 data/iso639_1.csv create mode 100644 worblehat/flaskapp.py create mode 100644 worblehat/models/Author.py create mode 100644 worblehat/models/Base.py create mode 100644 worblehat/models/BookcaseItem.py create mode 100644 worblehat/models/BookcaseLocation.py delete mode 100644 worblehat/models/Item.py delete mode 100644 worblehat/models/Location.py create mode 100644 worblehat/models/__init__.py create mode 100644 worblehat/models/mixins/UidMixin.py create mode 100644 worblehat/models/mixins/UniqueNameMixin.py create mode 100644 worblehat/models/mixins/XrefMixin.py create mode 100644 worblehat/models/mixins/__init__.py create mode 100644 worblehat/models/xref_tables/Item_Author.py create mode 100644 worblehat/models/xref_tables/Item_Category.py create mode 100644 worblehat/models/xref_tables/__init__.py create mode 100644 worblehat/seed_test_data.py diff --git a/data_fetcher/bokhyller/arbeidsrom_smal_hylle_5.csv b/data/arbeidsrom_smal_hylle_5.csv similarity index 100% rename from data_fetcher/bokhyller/arbeidsrom_smal_hylle_5.csv rename to data/arbeidsrom_smal_hylle_5.csv diff --git a/data_fetcher/bokhyller/arbeidsrom_smal_hylle_5.dat b/data/arbeidsrom_smal_hylle_5.jsonl similarity index 100% rename from data_fetcher/bokhyller/arbeidsrom_smal_hylle_5.dat rename to data/arbeidsrom_smal_hylle_5.jsonl diff --git a/data/iso639_1.csv b/data/iso639_1.csv new file mode 100644 index 0000000..118787b --- /dev/null +++ b/data/iso639_1.csv @@ -0,0 +1,185 @@ +code,name +aa,Afar +ab,Abkhazian +ae,Avestan +af,Afrikaans +ak,Akan +am,Amharic +an,Aragonese +ar,Arabic +as,Assamese +av,Avaric +ay,Aymara +az,Azerbaijani +ba,Bashkir +be,Belarusian +bg,Bulgarian +bh,Bihari +bi,Bislama +bm,Bambara +bn,Bengali +bo,Tibetan +br,Breton +bs,Bosnian +ca,Catalan +ce,Chechen +ch,Chamorro +co,Corsican +cr,Cree +cs,Czech +cu,Church Slavic +cv,Chuvash +cy,Welsh +da,Danish +de,German +dv,Divehi +dz,Dzongkha +ee,Ewe +el,Greek +en,English +eo,Esperanto +es,Spanish +et,Estonian +eu,Basque +fa,Persian +ff,Fulah +fi,Finnish +fj,Fijian +fo,Faroese +fr,French +fy,Western Frisian +ga,Irish +gd,Gaelic +gl,Galician +gn,Guarani +gu,Gujarati +gv,Manx +ha,Hausa +he,Hebrew +hi,Hindi +ho,Hiri Motu +hr,Croatian +ht,Haitian +hu,Hungarian +hy,Armenian +hz,Herero +ia,Interlingua +id,Indonesian +ie,Interlingue +ig,Igbo +ii,Sichuan Yi +ik,Inupiaq +io,Ido +is,Icelandic +it,Italian +iu,Inuktitut +ja,Japanese +jv,Javanese +ka,Georgian +kg,Kongo +ki,Kikuyu +kj,Kwanyama +kk,Kazakh +kl,Kalaallisut +km,Central Khmer +kn,Kannada +ko,Korean +kr,Kanuri +ks,Kashmiri +ku,Kurdish +kv,Komi +kw,Cornish +ky,Kirghiz +la,Latin +lb,Luxembourgish +lg,Ganda +li,Limburgan +ln,Lingala +lo,Lao +lt,Lithuanian +lu,Luba-Katanga +lv,Latvian +mg,Malagasy +mh,Marshallese +mi,Maori +mk,Macedonian +ml,Malayalam +mn,Mongolian +mr,Marathi +ms,Malay +mt,Maltese +my,Burmese +na,Nauru +nb,Norwegian Bokmål +nd,North Ndebele +ne,Nepali +ng,Ndonga +nl,Dutch +nn,Norwegian Nynorsk +no,Norwegian +nr,South Ndebele +nv,Navajo +ny,Chichewa +oc,Occitan +oj,Ojibwa +om,Oromo +or,Oriya +os,Ossetian +pa,Panjabi +pi,Pali +pl,Polish +ps,Pushto +pt,Portuguese +qu,Quechua +rm,Romansh +rn,Rundi +ro,Romanian +ru,Russian +rw,Kinyarwanda +sa,Sanskrit +sc,Sardinian +sd,Sindhi +se,Northern Sami +sg,Sango +si,Sinhala +sk,Slovak +sl,Slovenian +sm,Samoan +sn,Shona +so,Somali +sq,Albanian +sr,Serbian +ss,Swati +st,Southern Sotho +su,Sundanese +sv,Swedish +sw,Swahili +ta,Tamil +te,Telugu +tg,Tajik +th,Thai +ti,Tigrinya +tk,Turkmen +tl,Tagalog +tn,Tswana +to,Tonga +tr,Turkish +ts,Tsonga +tt,Tatar +tw,Twi +ty,Tahitian +ug,Uighur +uk,Ukrainian +ur,Urdu +uz,Uzbek +ve,Venda +vi,Vietnamese +vo,Volapük +wa,Walloon +wo,Wolof +xh,Xhosa +yi,Yiddish +yo,Yoruba +za,Zhuang +zh,Chinese +zu,Zulu \ No newline at end of file diff --git a/run.sh b/run.sh index 0f60fcb..4ef0f97 100755 --- a/run.sh +++ b/run.sh @@ -2,4 +2,4 @@ # FLASK_APP=app.py FLASK_DEBUG=1 FLASK_ENV=development python3 -m flask run --host=localhost --port=5000 --debugger --reload -flask --app worblehat --debug run --host=localhost --port=5000 --debugger --reload +flask --app worblehat.flaskapp --debug run --host=localhost --port=5000 --debugger --reload diff --git a/worblehat/__init__.py b/worblehat/__init__.py index efc82da..e69de29 100644 --- a/worblehat/__init__.py +++ b/worblehat/__init__.py @@ -1,103 +0,0 @@ -from flask import Flask -from flask_sqlalchemy import SQLAlchemy -from flask_admin import Admin -from flask_admin.contrib.sqla import ModelView - -from os import environ, path -from dotenv import load_dotenv - -from worblehat.database import db_session, init_db, drop_db - -def create_app(): - app = Flask(__name__, instance_relative_config=True) - app.config.from_object('config.Config') - configure_database(app) - configure_admin(app) - configure_blueprints(app) - - @app.route('/') - def index(): - return 'Hello World!' - - return app - - -def configure_database(app): - @app.cli.command("initdb") - def initdb_command(): - init_db() - print("Initialized the database.") - - @app.cli.command("resetdb") - def resetdb_command(): - drop_db() - print("Cleared the database.") - init_db() - from worblehat.models.MediaType import MediaType - from worblehat.models.Bookcase import Bookcase - from worblehat.models.Location import Location - from worblehat.models.Language import Language - - media_types = [ - MediaType(name='Book', description='A physical book'), - MediaType(name='Comic', description='A comic book'), - MediaType(name='Video Game', description='A digital game for computers or games consoles'), - MediaType(name='Tabletop Game', description='A physical game with cards, boards or similar') - ] - - bookcases = [ - Bookcase(name='A', description='The first bookcase'), - Bookcase(name='B', description='The second bookcase'), - ] - - locations = [ - Location(name='1-1', description='The first location', bookcase=bookcases[0]), - Location(name='1-2', description='The second location', bookcase=bookcases[0]), - Location(name='1-1', description='The first location', bookcase=bookcases[1]), - Location(name='1-2', description='The second location', bookcase=bookcases[1]), - ] - - languages = [ - Language(name='English', shortname='en'), - Language(name='Norwegian', shortname='no'), - Language(name='Japanese', shortname='ja'), - Language(name='Swedish', shortname='sv'), - Language(name='German', shortname='de'), - Language(name='Russian', shortname='ru'), - Language(name='Danish', shortname='da') - ] - - db_session.add_all(media_types) - db_session.add_all(bookcases) - db_session.add_all(locations) - db_session.add_all(languages) - db_session.commit() - print("Added media types, bookcases and locations.") - - - - @app.teardown_appcontext - def shutdown_session(exception=None): - db_session.remove() - -def configure_admin(app): - admin = Admin(app, name='Worblehat', template_mode='bootstrap3') - - from worblehat.models.Category import Category - from worblehat.models.Item import Item - from worblehat.models.MediaType import MediaType - from worblehat.models.Location import Location - from worblehat.models.Bookcase import Bookcase - - admin.add_view(ModelView(Category, db_session)) - admin.add_view(ModelView(Item, db_session)) - admin.add_view(ModelView(MediaType, db_session)) - admin.add_view(ModelView(Location, db_session)) - admin.add_view(ModelView(Bookcase, db_session)) - -def configure_blueprints(app): - from worblehat.blueprints.main import main - blueprints = [main] - - for bp in blueprints: - app.register_blueprint(bp) \ No newline at end of file diff --git a/worblehat/database.py b/worblehat/database.py index 28077ad..2e1eeb6 100644 --- a/worblehat/database.py +++ b/worblehat/database.py @@ -1,29 +1,3 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import scoped_session, sessionmaker -from sqlalchemy.ext.declarative import declarative_base +from flask_sqlalchemy import SQLAlchemy - -engine = create_engine('sqlite:///db.sqlite', convert_unicode=True) -db_session = scoped_session(sessionmaker(autocommit=False, - autoflush=False, - bind=engine)) -Base = declarative_base() -Base.query = db_session.query_property() - -def init_db(): - # import all modules here that might define models so that - # they will be registered properly on the metadata. Otherwise - # you will have to import them first before calling init_db() - - from .models.Category import Category - from .models.Item import Item - from .models.MediaType import MediaType - from .models.Location import Location - from .models.Bookcase import Bookcase - from .models.Language import Language - - Base.metadata.create_all(bind=engine) - - -def drop_db(): - Base.metadata.drop_all(bind=engine) +db = SQLAlchemy() \ No newline at end of file diff --git a/worblehat/flaskapp.py b/worblehat/flaskapp.py new file mode 100644 index 0000000..4d12e09 --- /dev/null +++ b/worblehat/flaskapp.py @@ -0,0 +1,40 @@ +from flask import Flask +from flask_admin import Admin +from flask_admin.contrib.sqla import ModelView +from sqlalchemy import inspect + +from os import environ, path +from dotenv import load_dotenv + +from .database import db +from .models import * + +from .blueprints.main import main +from .seed_test_data import seed_data + +def create_app(): + app = Flask(__name__, instance_relative_config=True) + app.config.from_object('config.Config') + + db.init_app(app) + + with app.app_context(): + if not inspect(db.engine).has_table('Bookcase'): + Base.metadata.create_all(db.engine) + seed_data(db) + + configure_admin(app) + + app.register_blueprint(main) + + return app + +def configure_admin(app): + admin = Admin(app, name='Worblehat', template_mode='bootstrap3') + admin.add_view(ModelView(Author, db.session)) + admin.add_view(ModelView(Bookcase, db.session)) + admin.add_view(ModelView(BookcaseItem, db.session)) + admin.add_view(ModelView(BookcaseLocation, db.session)) + admin.add_view(ModelView(Category, db.session)) + admin.add_view(ModelView(Language, db.session)) + admin.add_view(ModelView(MediaType, db.session)) \ No newline at end of file diff --git a/worblehat/models/Author.py b/worblehat/models/Author.py new file mode 100644 index 0000000..d2b0608 --- /dev/null +++ b/worblehat/models/Author.py @@ -0,0 +1,34 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + +from sqlalchemy import ( + Integer, + ForeignKey, +) +from sqlalchemy.orm import ( + Mapped, + mapped_column, + relationship, +) + +from .Base import Base +from .mixins import ( + UidMixin, + UniqueNameMixin, +) +from .xref_tables import Item_Author + +if TYPE_CHECKING: + from .BookcaseItem import BookcaseItem + +class Author(Base, UidMixin, UniqueNameMixin): + items: Mapped[set[BookcaseItem]] = relationship( + secondary = Item_Author.__table__, + back_populates = 'authors', + ) + + def __init__( + self, + name: str, + ): + self.name = name \ No newline at end of file diff --git a/worblehat/models/Base.py b/worblehat/models/Base.py new file mode 100644 index 0000000..e879c23 --- /dev/null +++ b/worblehat/models/Base.py @@ -0,0 +1,40 @@ +from sqlalchemy import MetaData +from sqlalchemy.orm import ( + DeclarativeBase, + declared_attr, +) +from sqlalchemy.orm.collections import ( + InstrumentedDict, + InstrumentedList, + InstrumentedSet, +) + +class Base(DeclarativeBase): + metadata = MetaData( + naming_convention={ + "ix": "ix_%(column_0_label)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_`%(constraint_name)s`", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" + } + ) + + @declared_attr.directive + def __tablename__(cls) -> str: + return cls.__name__ + + def __repr__(self) -> str: + columns = ", ".join( + f"{k}={repr(v)}" for k, v in self.__dict__.items() if not any([ + k.startswith("_"), + + # Ensure that we don't try to print out the entire list of + # relationships, which could create an infinite loop + isinstance(v, Base), + isinstance(v, InstrumentedList), + isinstance(v, InstrumentedSet), + isinstance(v, InstrumentedDict), + ]) + ) + return f"<{self.__class__.__name__}({columns})>" \ No newline at end of file diff --git a/worblehat/models/Bookcase.py b/worblehat/models/Bookcase.py index 2a04509..c9c4865 100644 --- a/worblehat/models/Bookcase.py +++ b/worblehat/models/Bookcase.py @@ -1,16 +1,31 @@ -from sqlalchemy import Column, Integer, String, ForeignKey, Boolean -from sqlalchemy.orm import relationship -from worblehat.database import Base +from __future__ import annotations +from typing import TYPE_CHECKING +from sqlalchemy import Text +from sqlalchemy.orm import ( + Mapped, + mapped_column, + relationship, +) -class Bookcase(Base): - __tablename__ = 'bookcases' - id = Column(Integer, primary_key=True) - name = Column(String(10), nullable=False) - description = Column(String(255)) +from .Base import Base +from .mixins import ( + UidMixin, + UniqueNameMixin, +) +if TYPE_CHECKING: + from .BookcaseLocation import BookcaseLocation - locations = relationship('Location', back_populates='bookcase') +class Bookcase(Base, UidMixin, UniqueNameMixin): + description: Mapped[str | None] = mapped_column(Text) - def __repr__(self): - return '' % self.name + locations: Mapped[list[BookcaseLocation]] = relationship(back_populates='bookcase') + + def __init__( + self, + name: str, + description: str | None = None, + ): + self.name = name + self.description = description diff --git a/worblehat/models/BookcaseItem.py b/worblehat/models/BookcaseItem.py new file mode 100644 index 0000000..0471c8b --- /dev/null +++ b/worblehat/models/BookcaseItem.py @@ -0,0 +1,62 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + +from sqlalchemy import ( + Integer, + String, + ForeignKey, +) +from sqlalchemy.orm import ( + Mapped, + mapped_column, + relationship, +) + +from .Base import Base +from .mixins import ( + UidMixin, + UniqueNameMixin, +) +from .xref_tables import ( + Item_Category, + Item_Author, +) +if TYPE_CHECKING: + from .Author import Author + from .BookcaseLocation import BookcaseLocation + from .Category import Category + from .Language import Language + from .MediaType import MediaType + +class BookcaseItem(Base, UidMixin, UniqueNameMixin): + # NOTE: This is kept non-unique in case we have + # multiple copies of the same book. + isbn: Mapped[int | None] = mapped_column(String, index=True) + owner: Mapped[str] = mapped_column(String, default='PVV') + + fk_media_type_uid: Mapped[int] = mapped_column(Integer, ForeignKey('MediaType.uid')) + fk_bookcase_location_uid: Mapped[int | None] = mapped_column(Integer, ForeignKey('BookcaseLocation.uid')) + fk_language_uid: Mapped[int] = mapped_column(Integer, ForeignKey('Language.uid')) + + media_type: Mapped[MediaType] = relationship(back_populates='items') + location: Mapped[BookcaseLocation] = relationship(back_populates='items') + language: Mapped[Language] = relationship() + + categories: Mapped[set[Category]] = relationship( + secondary = Item_Category.__table__, + back_populates = 'items', + ) + authors: Mapped[set[Author]] = relationship( + secondary = Item_Author.__table__, + back_populates = 'items', + ) + + def __init__( + self, + name: str, + isbn: int | None = None, + owner: str = 'PVV', + ): + self.name = name + self.isbn = isbn + self.owner = owner \ No newline at end of file diff --git a/worblehat/models/BookcaseLocation.py b/worblehat/models/BookcaseLocation.py new file mode 100644 index 0000000..843f350 --- /dev/null +++ b/worblehat/models/BookcaseLocation.py @@ -0,0 +1,40 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + +from sqlalchemy import ( + Integer, + ForeignKey, + Text, +) +from sqlalchemy.orm import ( + Mapped, + mapped_column, + relationship, +) + +from .Base import Base +from .mixins import ( + UidMixin, + UniqueNameMixin, +) +if TYPE_CHECKING: + from .Bookcase import Bookcase + from .BookcaseItem import BookcaseItem + +class BookcaseLocation(Base, UidMixin, UniqueNameMixin): + description: Mapped[str | None] = mapped_column(Text) + + fk_bookcase_uid: Mapped[int] = mapped_column(Integer, ForeignKey('Bookcase.uid')) + + bookcase: Mapped[Bookcase] = relationship(back_populates='locations') + items: Mapped[set[BookcaseItem]] = relationship(back_populates='location') + + def __init__( + self, + name: str, + bookcase: Bookcase, + description: str | None = None, + ): + self.name = name + self.bookcase = bookcase + self.description = description \ No newline at end of file diff --git a/worblehat/models/Category.py b/worblehat/models/Category.py index fefb62a..776028b 100644 --- a/worblehat/models/Category.py +++ b/worblehat/models/Category.py @@ -1,18 +1,34 @@ -from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, Table -from sqlalchemy.orm import relationship -from worblehat.database import Base +from __future__ import annotations +from typing import TYPE_CHECKING -category_association = Table('category_association', Base.metadata, - Column('category_id', Integer, ForeignKey('categories.id')), - Column('item_id', Integer, ForeignKey('items.id')) +from sqlalchemy import Text +from sqlalchemy.orm import ( + Mapped, + mapped_column, + relationship, ) -class Category(Base): - __tablename__ = 'categories' - id = Column(Integer, primary_key=True) - name = Column(String(80), unique=True) - description = Column(String(255)) +from .Base import Base +from .mixins import ( + UidMixin, + UniqueNameMixin, +) +from .xref_tables import Item_Category +if TYPE_CHECKING: + from .BookcaseItem import BookcaseItem - items = relationship('Item', secondary=category_association, back_populates='categories') - def __repr__(self): - return '' % self.name +class Category(Base, UidMixin, UniqueNameMixin): + description: Mapped[str | None] = mapped_column(Text) + + items: Mapped[set[BookcaseItem]] = relationship( + secondary=Item_Category.__table__, + back_populates='categories', + ) + + def __init__( + self, + name: str, + description: str | None = None, + ): + self.name = name + self.description = description \ No newline at end of file diff --git a/worblehat/models/Item.py b/worblehat/models/Item.py deleted file mode 100644 index 9185acb..0000000 --- a/worblehat/models/Item.py +++ /dev/null @@ -1,27 +0,0 @@ -from sqlalchemy import Column, Integer, String, ForeignKey, Boolean -from sqlalchemy.orm import relationship -from worblehat.database import Base -from .Language import Language - -class Item(Base): - __tablename__ = 'items' - id = Column(Integer, primary_key=True) - - title = Column(String(255), nullable=False) - owner = Column(String(255)) - isbn = Column(String(255)) - - media_id = Column(Integer, ForeignKey('media_types.id'), nullable=False) - media = relationship('MediaType', back_populates='items') - - location_id = Column(Integer, ForeignKey('locations.id'), nullable=False) - location = relationship('Location', back_populates='items') - - language_id = Column(Integer, ForeignKey('languages.id'), nullable=False) - language = relationship('Language', back_populates='items') - - categories = relationship('Category', secondary='category_association', back_populates='items') - - def __repr__(self): - return '' % (self.media.name, self.title) - diff --git a/worblehat/models/Language.py b/worblehat/models/Language.py index f4c23b9..0da2e96 100644 --- a/worblehat/models/Language.py +++ b/worblehat/models/Language.py @@ -1,16 +1,23 @@ -from sqlalchemy import Column, Integer, String, ForeignKey, Boolean -from sqlalchemy.orm import relationship -from worblehat.database import Base +# from sqlalchemy import Column, Integer, String, ForeignKey, Boolean +# from sqlalchemy.orm import relationship -class Language(Base): - __tablename__ = 'languages' - id = Column(Integer, primary_key=True) +from sqlalchemy import String +from sqlalchemy.orm import ( + Mapped, + mapped_column, +) - name = Column(String(32), nullable=False) - shortname = Column(String(2), nullable=False) - flag = Column(String(3)) - items = relationship('Item', back_populates='language') +from .Base import Base +from .mixins import UidMixin, UniqueNameMixin - def __repr__(self): - return '' % self.name +class Language(Base, UidMixin, UniqueNameMixin): + iso639_1_code: Mapped[str] = mapped_column(String(2)) + + def __init__( + self, + name: str, + iso639_1_code: str, + ): + self.name = name + self.iso639_1_code = iso639_1_code diff --git a/worblehat/models/Location.py b/worblehat/models/Location.py deleted file mode 100644 index a923785..0000000 --- a/worblehat/models/Location.py +++ /dev/null @@ -1,19 +0,0 @@ -from sqlalchemy import Column, Integer, String, ForeignKey, Boolean -from sqlalchemy.orm import relationship -from worblehat.database import Base - - -class Location(Base): - __tablename__ = 'locations' - id = Column(Integer, primary_key=True) - name = Column(String(100), nullable=False) - description = Column(String(255)) - - bookcase_id = Column(Integer, ForeignKey('bookcases.id'), nullable=False) - bookcase = relationship('Bookcase', back_populates='locations') - - items = relationship('Item', back_populates='location') - - def __repr__(self): - return '' % (self.bookcase.name, self.name) - diff --git a/worblehat/models/MediaType.py b/worblehat/models/MediaType.py index a59c054..94b1904 100644 --- a/worblehat/models/MediaType.py +++ b/worblehat/models/MediaType.py @@ -1,19 +1,29 @@ -from sqlalchemy import Column, Integer, String, ForeignKey, Boolean -from sqlalchemy.orm import relationship -from worblehat.database import Base +from __future__ import annotations +from typing import TYPE_CHECKING +from sqlalchemy import Text +from sqlalchemy.orm import ( + Mapped, + mapped_column, + relationship, +) -class MediaType(Base): - __tablename__ = 'media_types' - id = Column(Integer, primary_key=True) - name = Column(String(80), nullable=False) - description = Column(String(255)) - items = relationship('Item', back_populates='media', lazy=True) +from .Base import Base +from .mixins import UidMixin, UniqueNameMixin +if TYPE_CHECKING: + from .BookcaseItem import BookcaseItem - def __init__(self, name, description): +class MediaType(Base, UidMixin, UniqueNameMixin): + description: Mapped[str | None] = mapped_column(Text) + + items: Mapped[set[BookcaseItem]] = relationship(back_populates='media_type') + + def __init__( + self, + name: str, + description: str | None = None, + ): self.name = name self.description = description - def __repr__(self): - return '' % self.name diff --git a/worblehat/models/__init__.py b/worblehat/models/__init__.py new file mode 100644 index 0000000..085c4b7 --- /dev/null +++ b/worblehat/models/__init__.py @@ -0,0 +1,8 @@ +from .Author import Author +from .Base import Base +from .Bookcase import Bookcase +from .BookcaseItem import BookcaseItem +from .BookcaseLocation import BookcaseLocation +from .Category import Category +from .Language import Language +from .MediaType import MediaType \ No newline at end of file diff --git a/worblehat/models/mixins/UidMixin.py b/worblehat/models/mixins/UidMixin.py new file mode 100644 index 0000000..29d2e13 --- /dev/null +++ b/worblehat/models/mixins/UidMixin.py @@ -0,0 +1,30 @@ +from typing_extensions import Self + +from sqlalchemy import Integer +from sqlalchemy.orm import ( + Mapped, + mapped_column, +) + +from worblehat.database import db + +class UidMixin(object): + uid: Mapped[int] = mapped_column(Integer, primary_key=True) + + @classmethod + def get_by_uid(cls, uid: int) -> Self | None: + """ + NOTE: + This is a flask_sqlalchemy specific method. + It will not work outside of a request context. + """ + return db.session.query(cls).where(cls.uid == uid).one_or_none() + + @classmethod + def get_by_uid_or_404(cls, uid: int) -> Self: + """ + NOTE: + This is a flask_sqlalchemy specific method. + It will not work outside of a request context. + """ + return db.session.query(cls).where(cls.uid == uid).one_or_404() \ No newline at end of file diff --git a/worblehat/models/mixins/UniqueNameMixin.py b/worblehat/models/mixins/UniqueNameMixin.py new file mode 100644 index 0000000..1972e2d --- /dev/null +++ b/worblehat/models/mixins/UniqueNameMixin.py @@ -0,0 +1,30 @@ +from typing_extensions import Self + +from sqlalchemy import Text +from sqlalchemy.orm import ( + Mapped, + mapped_column, +) + +from worblehat.database import db + +class UniqueNameMixin(object): + name: Mapped[str] = mapped_column(Text, unique=True, index=True) + + @classmethod + def get_by_name(cls, name: str) -> Self | None: + """ + NOTE: + This is a flask_sqlalchemy specific method. + It will not work outside of a request context. + """ + return db.session.query(cls).where(cls.name == name).one_or_none() + + @classmethod + def get_by_uid_or_404(cls, name: str) -> Self: + """ + NOTE: + This is a flask_sqlalchemy specific method. + It will not work outside of a request context. + """ + return db.session.query(cls).where(cls.name == name).one_or_404() \ No newline at end of file diff --git a/worblehat/models/mixins/XrefMixin.py b/worblehat/models/mixins/XrefMixin.py new file mode 100644 index 0000000..ea9d510 --- /dev/null +++ b/worblehat/models/mixins/XrefMixin.py @@ -0,0 +1,6 @@ +from sqlalchemy.orm import declared_attr + +class XrefMixin(object): + @declared_attr.directive + def __tablename__(cls) -> str: + return f'xref_{cls.__name__.lower()}' diff --git a/worblehat/models/mixins/__init__.py b/worblehat/models/mixins/__init__.py new file mode 100644 index 0000000..190d20a --- /dev/null +++ b/worblehat/models/mixins/__init__.py @@ -0,0 +1,2 @@ +from .UidMixin import UidMixin +from .UniqueNameMixin import UniqueNameMixin diff --git a/worblehat/models/xref_tables/Item_Author.py b/worblehat/models/xref_tables/Item_Author.py new file mode 100644 index 0000000..502b383 --- /dev/null +++ b/worblehat/models/xref_tables/Item_Author.py @@ -0,0 +1,15 @@ +from sqlalchemy import ( + Integer, + ForeignKey, +) +from sqlalchemy.orm import ( + Mapped, + mapped_column, +) + +from ..Base import Base +from ..mixins.XrefMixin import XrefMixin + +class Item_Author(Base, XrefMixin): + fk_item_uid: Mapped[int] = mapped_column(ForeignKey('BookcaseItem.uid'), primary_key=True) + fk_author_uid: Mapped[int] = mapped_column(ForeignKey('Author.uid'), primary_key=True) \ No newline at end of file diff --git a/worblehat/models/xref_tables/Item_Category.py b/worblehat/models/xref_tables/Item_Category.py new file mode 100644 index 0000000..4912b87 --- /dev/null +++ b/worblehat/models/xref_tables/Item_Category.py @@ -0,0 +1,15 @@ +from sqlalchemy import ( + Integer, + ForeignKey, +) +from sqlalchemy.orm import ( + Mapped, + mapped_column, +) + +from ..Base import Base +from ..mixins.XrefMixin import XrefMixin + +class Item_Category(Base, XrefMixin): + fk_item_uid: Mapped[int] = mapped_column(ForeignKey('BookcaseItem.uid'), primary_key=True) + fk_category_uid: Mapped[int] = mapped_column(ForeignKey('Category.uid'), primary_key=True) \ No newline at end of file diff --git a/worblehat/models/xref_tables/__init__.py b/worblehat/models/xref_tables/__init__.py new file mode 100644 index 0000000..0d2dd16 --- /dev/null +++ b/worblehat/models/xref_tables/__init__.py @@ -0,0 +1,2 @@ +from .Item_Author import Item_Author +from .Item_Category import Item_Category \ No newline at end of file diff --git a/worblehat/seed_test_data.py b/worblehat/seed_test_data.py new file mode 100644 index 0000000..57b4f29 --- /dev/null +++ b/worblehat/seed_test_data.py @@ -0,0 +1,42 @@ +import csv +from pathlib import Path + +from flask_sqlalchemy import SQLAlchemy + +from .models import ( + Bookcase, + BookcaseLocation, + Language, + MediaType, +) + +def seed_data(db: SQLAlchemy): + media_types = [ + MediaType(name='Book', description='A physical book'), + MediaType(name='Comic', description='A comic book'), + MediaType(name='Video Game', description='A digital game for computers or games consoles'), + MediaType(name='Tabletop Game', description='A physical game with cards, boards or similar') + ] + + bookcases = [ + Bookcase(name='A', description='The first bookcase'), + Bookcase(name='B', description='The second bookcase'), + ] + + locations = [ + BookcaseLocation(name='1-1', description='The first location', bookcase=bookcases[0]), + BookcaseLocation(name='1-2', description='The second location', bookcase=bookcases[0]), + BookcaseLocation(name='2-1', description='The third location', bookcase=bookcases[1]), + BookcaseLocation(name='2-2', description='The fourth location', bookcase=bookcases[1]), + ] + + with open(Path(__file__).parent.parent / 'data' / 'iso639_1.csv') as f: + reader = csv.reader(f) + languages = [Language(name, code) for (code, name) in reader] + + db.session.add_all(media_types) + db.session.add_all(bookcases) + db.session.add_all(locations) + db.session.add_all(languages) + db.session.commit() + print("Added test media types, bookcases and locations.") \ No newline at end of file