From 8a3eebaab5c99ba69d05e138fe2952f58887255d Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Tue, 22 Apr 2025 00:51:41 +0200 Subject: [PATCH] move schema to separate uv project --- kontor-api/Makefile | 2 +- kontor-api/src/kontor_api.egg-info/PKG-INFO | 19 + .../src/kontor_api.egg-info/SOURCES.txt | 22 + .../kontor_api.egg-info/dependency_links.txt | 1 + .../src/kontor_api.egg-info/requires.txt | 13 + .../src/kontor_api.egg-info/top_level.txt | 4 + kontor-api/src/models/comics/artist.py | 3 +- kontor-api/src/models/comics/comic.py | 3 +- kontor-api/src/models/media/file.py | 2 +- kontor-api/src/routers/__init__.py | 26 ++ kontor-api/src/routers/comic.py | 3 +- kontor-api/src/routers/media.py | 3 +- kontor-api/src/routers/tysc.py | 4 +- kontor-api/src/schema/__init__.py | 35 -- kontor-api/src/schema/admin.py | 77 ---- kontor-api/src/schema/base.py | 30 -- kontor-api/src/schema/comic.py | 99 ----- kontor-api/src/schema/database.py | 391 ------------------ kontor-api/src/schema/media.py | 100 ----- kontor-api/src/schema/metadata.py | 41 -- kontor-api/src/schema/tysc.py | 99 ----- kontor-api/uv.lock | 19 + kontor-schema/.gitignore | 1 + kontor-schema/pyproject.toml | 6 +- kontor-schema/src/kontor_schema/admin.py | 11 +- kontor-schema/src/kontor_schema/base.py | 7 +- kontor-schema/src/kontor_schema/comic.py | 11 +- kontor-schema/src/kontor_schema/database.py | 19 +- kontor-schema/src/kontor_schema/media.py | 20 +- kontor-schema/src/kontor_schema/metadata.py | 7 +- kontor-schema/src/kontor_schema/tysc.py | 7 +- kontor-schema/uv.lock | 153 +++++++ kontor-scripts/.python-version | 1 + kontor-scripts/Makefile | 19 + kontor-scripts/README.md | 3 + {scripts => kontor-scripts}/check_kontor.py | 76 ++-- {scripts => kontor-scripts}/config.py | 0 .../copy_to_mariadb.py | 0 {scripts => kontor-scripts}/copy_to_sqlite.py | 0 {scripts => kontor-scripts}/db_structure.py | 0 {scripts => kontor-scripts}/download.py | 29 +- {scripts => kontor-scripts}/export.py | 4 +- {scripts => kontor-scripts}/import.py | 0 .../json_to_mariadb.py | 0 {scripts => kontor-scripts}/kontor.py | 0 kontor-scripts/pyproject.toml | 20 + {scripts => kontor-scripts}/read_list.py | 0 {scripts => kontor-scripts}/update_title.py | 0 kontor-scripts/uv.lock | 333 +++++++++++++++ {scripts => scripts.bak}/__init__.py | 0 {scripts => scripts.bak}/pyproject.toml | 0 {scripts => scripts.bak}/schema/__init__.py | 0 {scripts => scripts.bak}/schema/admin.py | 0 {scripts => scripts.bak}/schema/base.py | 0 .../src => scripts.bak}/schema/bookshelf.py | 0 {scripts => scripts.bak}/schema/comic.py | 0 {scripts => scripts.bak}/schema/database.py | 0 {scripts => scripts.bak}/schema/media.py | 0 {scripts => scripts.bak}/schema/metadata.py | 0 {scripts => scripts.bak}/schema/tysc.py | 0 {scripts => scripts.bak}/setup.py | 0 {scripts => scripts.bak}/uv.lock | 0 scripts/Makefile | 31 -- scripts/schema/bookshelf.py | 50 --- 64 files changed, 745 insertions(+), 1059 deletions(-) create mode 100644 kontor-api/src/kontor_api.egg-info/PKG-INFO create mode 100644 kontor-api/src/kontor_api.egg-info/SOURCES.txt create mode 100644 kontor-api/src/kontor_api.egg-info/dependency_links.txt create mode 100644 kontor-api/src/kontor_api.egg-info/requires.txt create mode 100644 kontor-api/src/kontor_api.egg-info/top_level.txt delete mode 100644 kontor-api/src/schema/__init__.py delete mode 100644 kontor-api/src/schema/admin.py delete mode 100644 kontor-api/src/schema/base.py delete mode 100644 kontor-api/src/schema/comic.py delete mode 100644 kontor-api/src/schema/database.py delete mode 100644 kontor-api/src/schema/media.py delete mode 100644 kontor-api/src/schema/metadata.py delete mode 100644 kontor-api/src/schema/tysc.py create mode 100644 kontor-schema/.gitignore create mode 100644 kontor-scripts/.python-version create mode 100644 kontor-scripts/Makefile create mode 100644 kontor-scripts/README.md rename {scripts => kontor-scripts}/check_kontor.py (72%) rename {scripts => kontor-scripts}/config.py (100%) rename {scripts => kontor-scripts}/copy_to_mariadb.py (100%) rename {scripts => kontor-scripts}/copy_to_sqlite.py (100%) rename {scripts => kontor-scripts}/db_structure.py (100%) rename {scripts => kontor-scripts}/download.py (83%) rename {scripts => kontor-scripts}/export.py (94%) rename {scripts => kontor-scripts}/import.py (100%) rename {scripts => kontor-scripts}/json_to_mariadb.py (100%) rename {scripts => kontor-scripts}/kontor.py (100%) create mode 100644 kontor-scripts/pyproject.toml rename {scripts => kontor-scripts}/read_list.py (100%) rename {scripts => kontor-scripts}/update_title.py (100%) create mode 100644 kontor-scripts/uv.lock rename {scripts => scripts.bak}/__init__.py (100%) rename {scripts => scripts.bak}/pyproject.toml (100%) rename {scripts => scripts.bak}/schema/__init__.py (100%) rename {scripts => scripts.bak}/schema/admin.py (100%) rename {scripts => scripts.bak}/schema/base.py (100%) rename {kontor-api/src => scripts.bak}/schema/bookshelf.py (100%) rename {scripts => scripts.bak}/schema/comic.py (100%) rename {scripts => scripts.bak}/schema/database.py (100%) rename {scripts => scripts.bak}/schema/media.py (100%) rename {scripts => scripts.bak}/schema/metadata.py (100%) rename {scripts => scripts.bak}/schema/tysc.py (100%) rename {scripts => scripts.bak}/setup.py (100%) rename {scripts => scripts.bak}/uv.lock (100%) delete mode 100644 scripts/Makefile delete mode 100644 scripts/schema/bookshelf.py diff --git a/kontor-api/Makefile b/kontor-api/Makefile index 02424b0..b7063f5 100644 --- a/kontor-api/Makefile +++ b/kontor-api/Makefile @@ -10,5 +10,5 @@ docker: clean docker build --target=production -t kontor-api -t kontor-api:0.1.0-SNAPSHOT . dev: - uv run fastapi dev src/main.py --port 8008 + DB_HOST=localhost uv run fastapi dev src/main.py --port 8008 diff --git a/kontor-api/src/kontor_api.egg-info/PKG-INFO b/kontor-api/src/kontor_api.egg-info/PKG-INFO new file mode 100644 index 0000000..02715e1 --- /dev/null +++ b/kontor-api/src/kontor_api.egg-info/PKG-INFO @@ -0,0 +1,19 @@ +Metadata-Version: 2.4 +Name: kontor-api +Version: 0.1.0 +Summary: Add your description here +Requires-Python: >=3.13 +Description-Content-Type: text/markdown +Requires-Dist: beautifulsoup4>=4.13.4 +Requires-Dist: fastapi[standard]>=0.115.12 +Requires-Dist: httpx==0.24.1 +Requires-Dist: mariadb>=1.1.12 +Requires-Dist: pathlib>=1.0.1 +Requires-Dist: platformdirs>=4.3.7 +Requires-Dist: pytest==7.4.0 +Requires-Dist: pytest-cov>=6.1.1 +Requires-Dist: pyyaml>=6.0.2 +Requires-Dist: requests>=2.32.3 +Requires-Dist: sqlalchemy>=2.0.40 +Requires-Dist: sqlmodel>=0.0.24 +Requires-Dist: kontor.schema>=0.1.0 diff --git a/kontor-api/src/kontor_api.egg-info/SOURCES.txt b/kontor-api/src/kontor_api.egg-info/SOURCES.txt new file mode 100644 index 0000000..69b723b --- /dev/null +++ b/kontor-api/src/kontor_api.egg-info/SOURCES.txt @@ -0,0 +1,22 @@ +README.md +pyproject.toml +src/__init__.py +src/main.py +src/kontor_api.egg-info/PKG-INFO +src/kontor_api.egg-info/SOURCES.txt +src/kontor_api.egg-info/dependency_links.txt +src/kontor_api.egg-info/requires.txt +src/kontor_api.egg-info/top_level.txt +src/models/__init__.py +src/models/comics/__init__.py +src/models/comics/artist.py +src/models/comics/comic.py +src/models/media/__init__.py +src/models/media/file.py +src/models/tysc/__init__.py +src/models/tysc/sport.py +src/routers/__init__.py +src/routers/comic.py +src/routers/media.py +src/routers/tysc.py +tests/test_main.py \ No newline at end of file diff --git a/kontor-api/src/kontor_api.egg-info/dependency_links.txt b/kontor-api/src/kontor_api.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/kontor-api/src/kontor_api.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/kontor-api/src/kontor_api.egg-info/requires.txt b/kontor-api/src/kontor_api.egg-info/requires.txt new file mode 100644 index 0000000..15c93d0 --- /dev/null +++ b/kontor-api/src/kontor_api.egg-info/requires.txt @@ -0,0 +1,13 @@ +beautifulsoup4>=4.13.4 +fastapi[standard]>=0.115.12 +httpx==0.24.1 +mariadb>=1.1.12 +pathlib>=1.0.1 +platformdirs>=4.3.7 +pytest==7.4.0 +pytest-cov>=6.1.1 +pyyaml>=6.0.2 +requests>=2.32.3 +sqlalchemy>=2.0.40 +sqlmodel>=0.0.24 +kontor.schema>=0.1.0 diff --git a/kontor-api/src/kontor_api.egg-info/top_level.txt b/kontor-api/src/kontor_api.egg-info/top_level.txt new file mode 100644 index 0000000..579e547 --- /dev/null +++ b/kontor-api/src/kontor_api.egg-info/top_level.txt @@ -0,0 +1,4 @@ +__init__ +main +models +routers diff --git a/kontor-api/src/models/comics/artist.py b/kontor-api/src/models/comics/artist.py index 0dea1b5..3094b6d 100644 --- a/kontor-api/src/models/comics/artist.py +++ b/kontor-api/src/models/comics/artist.py @@ -1,10 +1,9 @@ from typing import List, Dict from uuid import UUID +from kontor_schema import Artist from pydantic import BaseModel -from src.schema import Artist - class ArtistCreation(BaseModel): name: str diff --git a/kontor-api/src/models/comics/comic.py b/kontor-api/src/models/comics/comic.py index 76824b2..427d35c 100644 --- a/kontor-api/src/models/comics/comic.py +++ b/kontor-api/src/models/comics/comic.py @@ -1,10 +1,9 @@ from typing import List, Dict from uuid import UUID +from kontor_schema import Comic from pydantic import BaseModel -from src.schema import Comic - class ComicResponse(BaseModel): id: UUID diff --git a/kontor-api/src/models/media/file.py b/kontor-api/src/models/media/file.py index af01717..daf1eaa 100644 --- a/kontor-api/src/models/media/file.py +++ b/kontor-api/src/models/media/file.py @@ -1,6 +1,6 @@ from uuid import UUID -from src.schema.media import MediaFile +from kontor_schema import MediaFile from pydantic import BaseModel diff --git a/kontor-api/src/routers/__init__.py b/kontor-api/src/routers/__init__.py index e69de29..08c7afb 100644 --- a/kontor-api/src/routers/__init__.py +++ b/kontor-api/src/routers/__init__.py @@ -0,0 +1,26 @@ +import logging +import os +from typing import Annotated + +from fastapi import Depends +from kontor_schema import Base +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, Session + +connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format( + os.environ.get('DB_USER', 'kontor'), + os.environ.get('DB_PASSWORD', 'kontor'), + os.environ.get('DB_HOST', 'mariadb'), + os.environ.get('DB_PORT', 3306), + os.environ.get('DB_NAME', 'kontor') +)) +engine = create_engine(connect_string) +SessionLocal = sessionmaker(bind=engine) +Base.metadata.create_all(bind=engine, checkfirst=True) + +def get_db(): + logging.info("get_db") + with SessionLocal() as db: + yield db + +SessionDep = Annotated[Session, Depends(get_db)] diff --git a/kontor-api/src/routers/comic.py b/kontor-api/src/routers/comic.py index 2132b87..4831c65 100644 --- a/kontor-api/src/routers/comic.py +++ b/kontor-api/src/routers/comic.py @@ -1,11 +1,12 @@ from uuid import UUID from typing import List from fastapi import APIRouter, HTTPException, status +from kontor_schema import Comic, Artist from sqlalchemy import select from src.models.comics.comic import ComicResponse, ComicDetailsResponse, get_comic_details from src.models.comics.artist import ArtistCreation, ArtistDetailResponse, ArtistResponse, get_artist_details -from src.schema import Comic, SessionDep, Artist +from src.routers import SessionDep router = APIRouter( prefix="/comic", diff --git a/kontor-api/src/routers/media.py b/kontor-api/src/routers/media.py index fb09443..4046d82 100644 --- a/kontor-api/src/routers/media.py +++ b/kontor-api/src/routers/media.py @@ -3,10 +3,11 @@ from typing import List from uuid import uuid4, UUID from fastapi import APIRouter, status, HTTPException +from kontor_schema import MediaFile from sqlalchemy import select, Sequence from src.models.media.file import MediaFileResponse, Link, get_file_details -from src.schema import MediaFile, SessionDep +from src.routers import SessionDep router = APIRouter( prefix="/media", diff --git a/kontor-api/src/routers/tysc.py b/kontor-api/src/routers/tysc.py index 54db6b9..62eaef5 100644 --- a/kontor-api/src/routers/tysc.py +++ b/kontor-api/src/routers/tysc.py @@ -1,8 +1,9 @@ from typing import List from fastapi import APIRouter +from kontor_schema import Sport from src.models.tysc.sport import SportResponse -from src.schema import Sport, SessionDep +from src.routers import SessionDep router = APIRouter( prefix="/tysc", @@ -17,4 +18,3 @@ def get_all_sports(db: SessionDep) -> List[SportResponse]: for sport in sports: results.append(SportResponse(id=sport.id, name=sport.name)) return results - diff --git a/kontor-api/src/schema/__init__.py b/kontor-api/src/schema/__init__.py deleted file mode 100644 index 48cde37..0000000 --- a/kontor-api/src/schema/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -import logging -import os -from typing import Annotated - - -from fastapi import Depends -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker, Session - -from .admin import User, Token, Role, AuthorizationMatrix, ModuleData, MailAccount, Mail -from .bookshelf import Article, Book, Author, BookshelfPublisher, ArticleAuthor, BookAuthor -from .comic import Comic, Artist, Publisher, Issue, StoryArc, TradePaperback, Volume, ComicWork, WorkType -from .metadata import MetaDataTable, MetaDataColumn -from .tysc import Card, CardSet, Sport, Team, FieldPosition, Rooster, Player, Vendor -from .media import MediaFile, MediaArticle, MediaVideo -from .base import Base -from .database import KontorDB, ColumnEntry - -connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format( - os.environ.get('DB_USER', 'kontor'), - os.environ.get('DB_PASSWORD', 'kontor'), - os.environ.get('DB_HOST', 'mariadb'), - os.environ.get('DB_PORT', 3306), - os.environ.get('DB_NAME', 'kontor') -)) -engine = create_engine(connect_string) -SessionLocal = sessionmaker(bind=engine) -Base.metadata.create_all(bind=engine, checkfirst=True) - -def get_db(): - logging.info("get_db") - with SessionLocal() as db: - yield db - -SessionDep = Annotated[Session, Depends(get_db)] diff --git a/kontor-api/src/schema/admin.py b/kontor-api/src/schema/admin.py deleted file mode 100644 index dd89d68..0000000 --- a/kontor-api/src/schema/admin.py +++ /dev/null @@ -1,77 +0,0 @@ -from datetime import datetime - -from sqlalchemy import Boolean, Column, ForeignKey, Integer, String -from sqlalchemy.orm import relationship, mapped_column, Mapped - -from .base import Base, BaseMixin - - -class User(Base, BaseMixin): - __tablename__ = 'user' - first_name = Column(String(255)) - last_name = Column(String(255)) - user_name = Column(String(255), nullable=False) - email = Column(String(255)) - password = Column(String(255)) - enabled = Column(Boolean) - matrix = relationship("AuthorizationMatrix") - tokens = relationship("Token") - - def get_full_name(self) -> str: - full_name = "" - if self.first_name is not None: - full_name += self.first_name - if self.last_name is not None: - if len(full_name) > 0: - full_name += " " - full_name += self.last_name - return full_name - - -class Token(Base, BaseMixin): - __tablename__ = "token" - token = Column(String(255), nullable=False, unique=True) - name = Column(String(255)) - last_used_date: Mapped[datetime] = mapped_column() - enabled = Column(Boolean) - user_id = Column(String(255), ForeignKey("user.id"), nullable=False) - user = relationship("User", back_populates="tokens") - - -class Role(Base, BaseMixin): - __tablename__ = "role" - name = Column(String(255), nullable=False) - matrix = relationship("AuthorizationMatrix") - - -class AuthorizationMatrix(Base, BaseMixin): - __tablename__ = "authorization_matrix" - user_id = Column(String, ForeignKey("user.id"), nullable=False) - user = relationship("User", back_populates="matrix") - role_id = Column(String, ForeignKey("role.id"), nullable=False) - role = relationship("Role", back_populates="matrix") - - -class ModuleData(Base, BaseMixin): - __tablename__ = "module_data" - module_name = Column(String(255), nullable=False) - import_data = Column(Boolean) - - -class MailAccount(Base, BaseMixin): - __tablename__ = "mail_account" - host = Column(String(255)) - port = Column(Integer) - protocol = Column(String(255)) - user_name = Column(String(255)) - password = Column(String(255)) - start_tls = Column(Boolean) - - -class Mail(Base, BaseMixin): - __tablename__ = "mail" - folder: Mapped[str] = mapped_column() - subject: Mapped[str] = mapped_column() - body: Mapped[str] = mapped_column() - sent_date: Mapped[datetime] = mapped_column() - received_date: Mapped[datetime] = mapped_column() diff --git a/kontor-api/src/schema/base.py b/kontor-api/src/schema/base.py deleted file mode 100644 index 9ac6b98..0000000 --- a/kontor-api/src/schema/base.py +++ /dev/null @@ -1,30 +0,0 @@ -import uuid -from datetime import datetime - -from sqlalchemy import Boolean, func, Column, String -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column - - -class Base(DeclarativeBase): - pass - - -class BaseMixin: - id = Column(String(255), primary_key=True, default=uuid.uuid4()) - # id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4()) - # created_date = Column(DateTime) - created_date: Mapped[datetime] = mapped_column(default=func.now()) - # last_modified_date = Column(DateTime) - last_modified_date: Mapped[datetime] = mapped_column(default=func.now()) - # version = Column(Integer) - version: Mapped[int] = mapped_column(default=0) - - -class BaseVideoMixin: - cloud_link = Column(String(255)) - file_name = Column(String(255)) - path = Column(String(255)) - review = Column(Boolean) - title = Column(String(255)) - url = Column(String(255), unique=True) - should_download = Column(Boolean) diff --git a/kontor-api/src/schema/comic.py b/kontor-api/src/schema/comic.py deleted file mode 100644 index cbdc803..0000000 --- a/kontor-api/src/schema/comic.py +++ /dev/null @@ -1,99 +0,0 @@ -from sqlalchemy import Boolean, Column, ForeignKey, Integer, String -from sqlalchemy.orm import relationship - -from .base import Base, BaseMixin - - -class Publisher(Base, BaseMixin): - __tablename__ = "publisher" - name = Column(String(length=255), unique=True) - comics = relationship("Comic") - - def __repr__(self): - return f'Publisher({self.id} {self.name})' - - def __str__(self): - return self.__repr__() - - -class Comic(Base, BaseMixin): - __tablename__ = 'comic' - title = Column(String(length=255), unique=True) - publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False) - publisher = relationship("Publisher", back_populates="comics") - current_order = Column(Boolean) - completed = Column(Boolean) - issues = relationship("Issue") - story_arcs = relationship("StoryArc") - trade_paperbacks = relationship("TradePaperback") - volumes = relationship("Volume") - comic_works = relationship("ComicWork") - - def __repr__(self): - return f'Comic({self.id} {self.version} {self.title} {self.publisher.name})' - - def __str__(self): - return f'{self.title}({self.id})' - - -class Volume(Base, BaseMixin): - __tablename__ = "volume" - name = Column(String(length=255), nullable=False) - comic_id = Column(String, ForeignKey("comic.id"), nullable=False) - comic = relationship("Comic", back_populates="volumes") - issues = relationship("Issue") - - -class TradePaperback(Base, BaseMixin): - __tablename__ = "trade_paperback" - name = Column(String(length=255), nullable=False) - issue_start = Column(Integer) - issue_end = Column(Integer) - comic_id = Column(String, ForeignKey("comic.id"), nullable=False) - comic = relationship("Comic", back_populates="trade_paperbacks") - - -class StoryArc(Base, BaseMixin): - __tablename__ = "story_arc" - name = Column(String(length=255), nullable=False) - comic_id = Column(String, ForeignKey("comic.id"), nullable=False) - comic = relationship("Comic", back_populates="story_arcs") - - -class Issue(Base, BaseMixin): - __tablename__ = "issue" - issue_number = Column(String(255)) - in_stock = Column(Boolean) - is_read = Column(Boolean) - comic_id = Column(String, ForeignKey("comic.id"), nullable=False) - comic = relationship("Comic", back_populates="issues") - volume_id = Column(String, ForeignKey("volume.id"), nullable=True) - volume = relationship("Volume", back_populates="issues") - - -class Artist(Base, BaseMixin): - __tablename__ = "artist" - name = Column(String(length=255), nullable=False) - comic_works = relationship("ComicWork") - - -class WorkType(Base, BaseMixin): - __tablename__ = "worktype" - name = Column(String(length=255), nullable=False, unique=True) - comic_works = relationship("ComicWork") - - def __repr__(self): - return f'Worktype({self.id} {self.version} {self.name} {len(self.comic_works)})' - - def __str__(self): - return f'{self.name}({self.id})' - - -class ComicWork(Base, BaseMixin): - __tablename__ = "comic_work" - comic_id = Column(String, ForeignKey("comic.id"), nullable=False) - comic = relationship("Comic", back_populates="comic_works") - artist_id = Column(String, ForeignKey("artist.id"), nullable=False) - artist = relationship("Artist", back_populates="comic_works") - work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False) - work_type = relationship("WorkType", back_populates="comic_works") diff --git a/kontor-api/src/schema/database.py b/kontor-api/src/schema/database.py deleted file mode 100644 index 51aee51..0000000 --- a/kontor-api/src/schema/database.py +++ /dev/null @@ -1,391 +0,0 @@ -import json -import logging -import uuid -from datetime import datetime -from enum import Enum, auto -from logging import Logger -from pathlib import Path -from typing import Any - -from sqlalchemy import select -from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import sessionmaker - -from .tysc import Card, CardSet, Rooster, Team, FieldPosition, Player, Vendor, Sport -from .comic import Issue, TradePaperback, StoryArc, Volume, ComicWork, Artist, Comic, Publisher, WorkType -from .bookshelf import ArticleAuthor, BookAuthor, BookshelfPublisher, Article, Book, Author -from .admin import Mail, MailAccount, ModuleData, Role, User, Token, AuthorizationMatrix -from .metadata import MetaDataTable, MetaDataColumn -from .media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile - - -class ColumnEntry(Enum): - COLUMN_NAME = 'column' - COLUMN_LABEL = 'label' - COLUMN_ORDER = 'order' - COLUMN_REF_COLUMN = 'ref_column' - COLUMN_TYPE = 'type' - COLUMN_WIDGET = 'widget' - - -class StatusType(Enum): - UNKNOWN = auto() - FILE_NAME = auto() - FILE_ID = auto() - DUPLICATE = auto() - CLOUD_LINK = auto() - CLOUD_LINK_ID = auto() - - -class KontorDB: - - def __init__(self, db_engine: Any): - self.engine = db_engine - self.registry = {} - self.init_registry() - - def init_registry(self): - self.registry[Card.__tablename__] = Card - self.registry[CardSet.__tablename__] = CardSet - self.registry[Rooster.__tablename__] = Rooster - self.registry[Team.__tablename__] = Team - self.registry[FieldPosition.__tablename__] = FieldPosition - self.registry[Player.__tablename__] = Player - self.registry[Vendor.__tablename__] = Vendor - self.registry[Sport.__tablename__] = Sport - self.registry[Issue.__tablename__] = Issue - self.registry[TradePaperback.__tablename__] = TradePaperback - self.registry[StoryArc.__tablename__] = StoryArc - self.registry[Volume.__tablename__] = Volume - self.registry[ComicWork.__tablename__] = ComicWork - self.registry[Artist.__tablename__] = Artist - self.registry[Comic.__tablename__] = Comic - self.registry[Publisher.__tablename__] = Publisher - self.registry[WorkType.__tablename__] = WorkType - self.registry[ArticleAuthor.__tablename__] = ArticleAuthor - self.registry[BookAuthor.__tablename__] = BookAuthor - self.registry[BookshelfPublisher.__tablename__] = BookshelfPublisher - self.registry[Article.__tablename__] = Article - self.registry[Book.__tablename__] = Book - self.registry[Author.__tablename__] = Author - self.registry[MediaFile.__tablename__] = MediaFile - self.registry[MediaActor.__tablename__] = MediaActor - self.registry[MediaActorFile.__tablename__] = MediaActorFile - self.registry[MediaArticle.__tablename__] = MediaArticle - self.registry[MediaVideo.__tablename__] = MediaVideo - self.registry[MetaDataColumn.__tablename__] = MetaDataColumn - self.registry[MetaDataTable.__tablename__] = MetaDataTable - self.registry[AuthorizationMatrix.__tablename__] = AuthorizationMatrix - self.registry[Token.__tablename__] = Token - self.registry[User.__tablename__] = User - self.registry[Role.__tablename__] = Role - self.registry[ModuleData.__tablename__] = ModuleData - self.registry[MailAccount.__tablename__] = MailAccount - self.registry[Mail.__tablename__] = Mail - - def get_table_names(self) -> list: - result = [] - __session__ = sessionmaker(self.engine) - with __session__() as session: - tables = session.scalars(select(MetaDataTable)).all() - result = [table.table_name for table in tables] - return result - - def get_table_by_name(self, table_name: str) -> dict: - result = {} - __session__ = sessionmaker(self.engine) - _filter = {'table_name': table_name} - with __session__() as session: - table = session.query(MetaDataTable).filter_by(**_filter).one() - result['id'] = table.id - result['table_name'] = table.table_name - return result - - def get_column_meta_data(self, table_name: str, view_only=True) -> dict: - meta_data = {} - order = 0 - __session__ = sessionmaker(self.engine) - columns = list() - table_info = self.get_table_by_name(table_name) - _filters = {'table_id': table_info['id']} - if view_only: - _filters['is_shown'] = True - with __session__() as session: - columns = session.query(MetaDataColumn).filter_by(**_filters).all() - for column in columns: - # self.log.info("get_column_meta_data: %s %s %d", column.column_name, column.column_label, column.column_order) - meta_data[order] = { - ColumnEntry.COLUMN_NAME: column.column_name, - ColumnEntry.COLUMN_LABEL: column.column_label, - ColumnEntry.COLUMN_ORDER: column.column_order, - ColumnEntry.COLUMN_REF_COLUMN: column.ref_column, - ColumnEntry.COLUMN_TYPE: column.column_type - } - order += 1 - return meta_data - - def get_columns(self, table_name: str) -> dict: - columns = {} - __session__ = sessionmaker(self.engine) - table_info = self.get_table_by_name(table_name) - _filters = {'table_id': table_info['id']} - with __session__() as session: - for column in session.query(MetaDataColumn).filter_by(**_filters).all(): - columns[column.column_name] = { - ColumnEntry.COLUMN_ORDER: column.column_order, - ColumnEntry.COLUMN_TYPE: column.column_type - } - return columns - - def get_filters(self, table_name: str) -> dict: - _filter_map = {} - __session__ = sessionmaker(self.engine) - table_info = self.get_table_by_name(table_name) - _filters = {'table_id': table_info['id'], 'show_filter': True} - with __session__() as session: - for column in session.query(MetaDataColumn).filter_by(**_filters).all(): - _filter_map[column.column_name] = { - ColumnEntry.COLUMN_LABEL: column.filter_label, - ColumnEntry.COLUMN_WIDGET: None - } - return _filter_map - - def data(self, table_name: str, columns: dict, filters: dict) -> list: - data = [] - __session__ = sessionmaker(self.engine) - table = self.registry[table_name] - with __session__() as session: - entries = [] - if len(filters) == 0: - entries = session.scalars(select(table)).all() - else: - entries = session.scalars(select(table).filter_by(**filters)).all() - for entry in entries: - # self.log.info("data: %s", entry) - row = [] - for order in columns.keys(): - column_name = columns[order][ColumnEntry.COLUMN_NAME] - ref_column = columns[order][ColumnEntry.COLUMN_REF_COLUMN] - if str(column_name).endswith("_id"): - ref_table = column_name[:-3] - ref = getattr(entry, ref_table) - value = getattr(ref, ref_column) - row.append(value) - else: - row.append(getattr(entry, column_name)) - data.append(row) - # self.log.info("data: %s", data) - return data - - def export_db(self, export_type: str, export_file_name: str) -> dict: - results = {} - db = {} - export_table_list = self.get_table_names() - for table in export_table_list: - columns = self.get_column_meta_data(table, view_only=False) - if table in self.registry: - model = self.registry[table] - else: - logging.info(f"table {table} is not registered") - continue - __session__ = sessionmaker(self.engine) - with __session__() as session: - rows = session.query(model).all() - entries = [] - for row in rows: - # print(row) - entry = {} - for order in columns: - # print(columns[order]) - column_name = columns[order][ColumnEntry.COLUMN_NAME] - # print(f"get value {column_name} from {row} of table {table}") - try: - value = getattr(row, column_name) - if isinstance(value, datetime): - entry[column_name] = str(value) - else: - entry[column_name] = value - except AttributeError: - pass - entries.append(entry) - db[table] = entries - results[table] = len(entries) - match export_type: - case "JSON": - json_dump = json.dumps(db, indent=4) - with open(export_file_name, "w") as dump_file: - dump_file.write(json_dump) - case "YAML": - pass - case "SQLite": - pass - logging.info(f"{len(results)} tables exported") - return results - - def import_db(self, import_file_name: str) -> dict: - result = {} - import_file = Path(import_file_name) - if not import_file.exists(): - logging.info(f"File {import_file_name} does not exist. Do nothing.") - return result - match import_file.suffix: - case '.json': - print("read json file") - with open(import_file_name, 'r') as json_file: - json_load = json.load(json_file) - for table in json_load: - logging.info(f"{table}: {len(json_load[table])}") - result[table] = self.import_table(table, json_load[table]) - case '.yml': - print("read yaml file") - case '.yaml': - print("read yaml file") - case '.db': - print("read sqlite file") - return result - - def import_table(self, table_name: str, items:list) -> dict: - result = {} - updated = [] - added = [] - remaining = [] - existing_ids = self.get_ids(table_name) - logging.info(f"found {len(existing_ids)} existing ids for table {table_name}") - for item in items: - current_id = item['id'] - # print(f"import item: {item}") - found_item = None - __session__ = sessionmaker(self.engine) - with __session__() as session: - found_item = session.get(self.registry[table_name], current_id) - # print(f"found item: {found_item}") - if found_item is not None: - changed = self.update_entry(table_name, current_id, item) - updated.append(item) - if changed: - logging.info(f"{current_id} has changed") - updated.append(item) - existing_ids.remove(current_id) - else: - try: - self.add_entry(table_name, item) - added.append(item) - except IntegrityError as error: - logging.info(f"Could not add item, due to: {error.detail}") - if len(existing_ids) > 0: - print(f"remaining items for {table_name}: {existing_ids}") - remaining.extend(existing_ids) - result['updated'] = updated - result['added'] = added - result['remaining'] = remaining - return result - - def get_ids(self, table_name: str) -> list: - existing_ids = [] - __session__ = sessionmaker(self.engine) - with __session__() as session: - items = session.query(self.registry[table_name]).all() - for item in items: - existing_ids.append(getattr(item, 'id')) - return existing_ids - - def add_entry(self, table_name: str, update_item: dict): - logging.debug(f"add entry to table {table_name} with {update_item}") - __session__ = sessionmaker(self.engine) - with __session__() as session: - add_item = self.registry[table_name]() - for key in update_item.keys(): - update_value = update_item[key] - setattr(add_item, key, update_value) - session.add(add_item) - session.commit() - - def update_entry(self, table_name, current_id, update_item: dict) -> bool: - # self.log.info("update entry to table %s", table_name) - __session__ = sessionmaker(self.engine) - with __session__() as session: - existing_item = session.query(self.registry[table_name]).get(current_id) - changed = False - for key in update_item.keys(): - update_value = update_item[key] - existing_value = getattr(existing_item, key) - if type(existing_value) is not type(update_value): - existing_value = str(existing_value) - if existing_value != update_value: - logging.info(f"{key} has changed: {existing_value} != {update_value}") - setattr(existing_item, key, update_value) - session.commit() - changed = True - logging.info(f"update {key} with {update_value}") - return changed - - def add_link(self, link: str) -> dict: - result = {} - __session__ = sessionmaker(self.engine) - with __session__() as session: - media_file = MediaFile() - media_file.id = str(uuid.uuid4()) - media_file.created_date = datetime.now() - media_file.last_modified_date = datetime.now() - media_file.version = 0 - media_file.url = link - media_file.review = True - media_file.should_download = True - try: - session.add(media_file) - session.commit() - result['added'] = {'url': media_file.url, 'title': media_file.title, 'review': media_file.review, 'download': media_file.should_download} - except IntegrityError as error: - session.rollback() - result['error'] = error.orig - return result - - def update_titles(self) -> dict: - update_list = {} - __session__ = sessionmaker(self.engine) - _filter = { 'review': True} - with __session__() as session: - links = session.query(MediaFile).filter_by(**_filter).all() - for link in links: - url = link.url - if url is None: - continue - link.update_title() - session.commit() - update_list[link.id] = link.title - return update_list - - def get_download_list(self) -> list: - download_list = [] - __session__ = sessionmaker(self.engine) - _filter = { 'should_download': True} - with __session__() as session: - links = session.query(MediaFile).filter_by(**_filter).all() - for link in links: - url = link.url - if url is None: - continue - download_list.append(link.id) - return download_list - - def download_file(self, entry_id: str, download_dir = "/data/media", dl_tool = "yt-dlp") -> str: - __session__ = sessionmaker(self.engine) - with __session__() as session: - link = session.query(MediaFile).get(entry_id) - link.download_file(download_dir, dl_tool) - session.commit() - file_name = link.file_name - return file_name - - def delete_entries(self): - for (table_name, table) in self.registry.items(): - # self.log.info("delete entries from table %s", table_name) - __session__ = sessionmaker(self.engine) - with __session__() as session: - items = session.query(table).all() - for item in items: - session.delete(item) - session.commit() - - def check_files(self): - pass diff --git a/kontor-api/src/schema/media.py b/kontor-api/src/schema/media.py deleted file mode 100644 index 774ad52..0000000 --- a/kontor-api/src/schema/media.py +++ /dev/null @@ -1,100 +0,0 @@ -import logging -import re -import subprocess -from datetime import datetime -from pathlib import Path - -import requests -from bs4 import BeautifulSoup -from sqlalchemy import Boolean, Column, String, ForeignKey -from sqlalchemy.orm import relationship - -from .base import Base, BaseMixin, BaseVideoMixin - - -class MediaFile(Base, BaseMixin, BaseVideoMixin): - __tablename__ = 'media_file' - media_actor_files = relationship("MediaActorFile") - - def __repr__(self): - return f'MediaFile({self.id} {self.title} {self.title})' - - def __str__(self): - return f'{self.title}({self.id})' - - def update_title(self) -> None: - logging.info(f"update title for {self.url}") - try: - r = requests.get(self.url) - soup = BeautifulSoup(r.content, "html.parser") - title = soup.title.string - self.title = title - self.review = False - except: - self.title = None - self.review = True - self.last_modified_date = datetime.now() - - def download_file(self, download_dir: str, dl_tool: str): - logging.info(f"download file for {self.url} to {download_dir}") - result = subprocess.run([dl_tool, self.url], cwd=download_dir, capture_output=True, text=True) - if result.returncode == 0: - output = result.stdout - output = re.sub(' +', ' ', output) - lines_list = output.splitlines() - file_name = self.__parse_output__(lines_list) - if file_name is None: - self.review = True - self.should_download = True - self.file_name = None - else: - download_file = Path(file_name) - self.should_download = False - self.file_name = download_file.name - self.cloud_link = str(download_file.absolute()) - self.last_modified_date = datetime.now() - - def __parse_output__(self, lines_list): - self.file_name = None - for line in lines_list: - if 'has already been downloaded' in line: - end_len = len(' has already been downloaded') - self.file_name = line[11:-end_len] - if 'Destination' in line: - line_len = len(line) - start_len = len('[download] Destination: ') - file_len = line_len - start_len - self.file_name = line[-file_len:] - return self.file_name - - -class MediaActor(Base, BaseMixin): - __tablename__ = 'media_actor' - name = Column(String(255)) - media_actor_files = relationship("MediaActorFile") - - -class MediaActorFile(Base, BaseMixin): - __tablename__ = 'media_actor_file' - media_actor_id = Column(String(255), ForeignKey("media_actor.id"), nullable=False) - media_actor = relationship("MediaActor", back_populates="media_actor_files") - media_file_id = Column(String(255), ForeignKey("media_file.id"), nullable=True) - media_file = relationship("MediaFile", back_populates="media_actor_files") - - -class MediaArticle(Base, BaseMixin): - __tablename__ = 'media_article' - review = Column(Boolean) - title = Column(String(255)) - url = Column(String(255), unique=True) - - -class MediaVideo(Base, BaseMixin): - __tablename__ = 'media_video' - cloud_link = Column(String(255)) - file_name = Column(String(255)) - path = Column(String(255)) - review = Column(Boolean) - title = Column(String(255)) - url = Column(String(255), unique=True) - should_download = Column(Boolean) diff --git a/kontor-api/src/schema/metadata.py b/kontor-api/src/schema/metadata.py deleted file mode 100644 index 9ac5aa4..0000000 --- a/kontor-api/src/schema/metadata.py +++ /dev/null @@ -1,41 +0,0 @@ -from sqlalchemy import Column, String, ForeignKey, Integer, Boolean -from sqlalchemy.orm import relationship - -from .base import Base, BaseMixin - - -class MetaDataTable(Base, BaseMixin): - __tablename__ = 'meta_data_table' - table_name = Column(String(255), unique=True) - table_columns = relationship("MetaDataColumn") - - def __repr__(self): - return f'MetaDataTable({self.id} {self.table_name})' - - def __str__(self): - return f'{self.table_name}({self.id})' - - -class MetaDataColumn(Base, BaseMixin): - __tablename__ = 'meta_data_column' - column_name = Column(String(255), nullable=False) - column_sync_name = Column(String(255)) - column_type = Column(String(255)) - column_modifier = Column(String(255), nullable=True) - column_order = Column(Integer) - table_id = Column(String, ForeignKey('meta_data_table.id')) - table = relationship("MetaDataTable", back_populates="table_columns") - column_label = Column(String(255)) - filter_label = Column(String(255)) - is_shown = Column(Boolean) - show_filter = Column(Boolean) - ref_column = Column(String, nullable=True) - - def __repr__(self): - if self.column_name is None: - return f'MetaDataColumn({self.id} {self.table.table_name}.__)' - else: - return f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})' - - def __str__(self): - return f'{self.column_name}({self.id})' diff --git a/kontor-api/src/schema/tysc.py b/kontor-api/src/schema/tysc.py deleted file mode 100644 index 3660f69..0000000 --- a/kontor-api/src/schema/tysc.py +++ /dev/null @@ -1,99 +0,0 @@ -from sqlalchemy import Boolean, Column, Integer, String, ForeignKey, UniqueConstraint -from sqlalchemy.orm import relationship - -from .base import Base, BaseMixin - - -class Sport(Base, BaseMixin): - __tablename__ = "sport" - __table_args__ = ( - UniqueConstraint("name"), - ) - name = Column(String(255), nullable=False, index=True, unique=True) - teams = relationship("Team") - positions = relationship("FieldPosition") - - -class Team(Base, BaseMixin): - __tablename__ = "team" - name = Column(String(255), nullable=False, index=True, unique=True) - short_name = Column(String(255), nullable=False, ) - sport_id = Column(String, ForeignKey("sport.id"), nullable=False) - sport = relationship("Sport", back_populates="teams") - roosters = relationship("Rooster") - - -class FieldPosition(Base, BaseMixin): - __tablename__ = "field_position" - __table_args__ = ( - UniqueConstraint("name", "sport_id"), - UniqueConstraint("short_name", "sport_id"), - ) - name = Column(String(255), nullable=False, index=True) - short_name = Column(String(255), nullable=False) - sport_id = Column(String, ForeignKey("sport.id"), nullable=False, index=True) - sport = relationship("Sport", back_populates="positions") - roosters = relationship("Rooster") - - -class Player(Base, BaseMixin): - __tablename__ = "player" - __table_args__ = ( - UniqueConstraint("first_name", "last_name"), - ) - first_name = Column(String(255), nullable=False, index=True) - last_name = Column(String(255), nullable=False, index=True) - roosters = relationship("Rooster") - - def get_full_name(self) -> str: - return f"{self.last_name}, {self.first_name}" - - -class Rooster(Base, BaseMixin): - __tablename__ = "rooster" - __table_args__ = ( - UniqueConstraint("year", "team_id", "player_id", "position_id"), - ) - year = Column(Integer) - team_id = Column(String, ForeignKey("team.id"), nullable=False, index=True) - team = relationship("Team", back_populates="roosters") - player_id = Column(String, ForeignKey("player.id"), nullable=False, index=True) - player = relationship("Player", back_populates="roosters") - position_id = Column(String, ForeignKey("field_position.id"), nullable=False, index=True) - position = relationship("FieldPosition", back_populates="roosters") - cards = relationship("Card") - - -class Vendor(Base, BaseMixin): - __tablename__ = "vendor" - name = Column(String(255), nullable=False, unique=True, index=True) - card_sets = relationship("CardSet") - cards = relationship("Card") - - -class CardSet(Base, BaseMixin): - __tablename__ = "card_set" - __table_args__ = ( - UniqueConstraint("name", "vendor_id"), - ) - name = Column(String(255), index=True) - parallel_set = Column(Boolean) - insert_set = Column(Boolean) - vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True) - vendor = relationship("Vendor", back_populates="card_sets") - cards = relationship("Card") - - -class Card(Base, BaseMixin): - __tablename__ = "card" - __table_args__ = ( - UniqueConstraint("card_number", "year", "vendor_id", "card_set_id"), - ) - card_number = Column(Integer, index=True) - year = Column(Integer, index=True) - card_set_id = Column(String, ForeignKey("card_set.id"), nullable=False) - card_set = relationship("CardSet", back_populates="cards") - rooster_id = Column(String, ForeignKey("rooster.id"), nullable=False) - rooster = relationship("Rooster", back_populates="cards") - vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False) - vendor = relationship("Vendor", back_populates="cards") diff --git a/kontor-api/uv.lock b/kontor-api/uv.lock index f156c50..a09337f 100644 --- a/kontor-api/uv.lock +++ b/kontor-api/uv.lock @@ -300,6 +300,7 @@ dependencies = [ { name = "beautifulsoup4" }, { name = "fastapi", extra = ["standard"] }, { name = "httpx" }, + { name = "kontor-schema" }, { name = "mariadb" }, { name = "pathlib" }, { name = "platformdirs" }, @@ -316,6 +317,7 @@ requires-dist = [ { name = "beautifulsoup4", specifier = ">=4.13.4" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" }, { name = "httpx", specifier = "==0.24.1" }, + { name = "kontor-schema", directory = "../kontor-schema" }, { name = "mariadb", specifier = ">=1.1.12" }, { name = "pathlib", specifier = ">=1.0.1" }, { name = "platformdirs", specifier = ">=4.3.7" }, @@ -327,6 +329,23 @@ requires-dist = [ { name = "sqlmodel", specifier = ">=0.0.24" }, ] +[[package]] +name = "kontor-schema" +version = "0.1.0" +source = { directory = "../kontor-schema" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "requests" }, + { name = "sqlalchemy" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", specifier = ">=4.13.4" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "sqlalchemy", specifier = ">=2.0.40" }, +] + [[package]] name = "mariadb" version = "1.1.12" diff --git a/kontor-schema/.gitignore b/kontor-schema/.gitignore new file mode 100644 index 0000000..849ddff --- /dev/null +++ b/kontor-schema/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/kontor-schema/pyproject.toml b/kontor-schema/pyproject.toml index 9699a36..2e572f9 100644 --- a/kontor-schema/pyproject.toml +++ b/kontor-schema/pyproject.toml @@ -7,7 +7,11 @@ authors = [ { name = "Thomas Peetz", email = "thomas.peetz@ingenieurbuero-peetz.de" } ] requires-python = ">=3.13" -dependencies = [] +dependencies = [ + "beautifulsoup4>=4.13.4", + "requests>=2.32.3", + "sqlalchemy>=2.0.40", +] [build-system] requires = ["hatchling"] diff --git a/kontor-schema/src/kontor_schema/admin.py b/kontor-schema/src/kontor_schema/admin.py index c5b3e99..2cbfff4 100644 --- a/kontor-schema/src/kontor_schema/admin.py +++ b/kontor-schema/src/kontor_schema/admin.py @@ -1,7 +1,6 @@ from datetime import datetime -from sqlalchemy import Column, ForeignKey, Integer, String -from sqlalchemy.dialects.mysql import BIT +from sqlalchemy import Column, ForeignKey, Integer, String, Boolean from sqlalchemy.orm import relationship, mapped_column, Mapped from .base import Base, BaseMixin @@ -14,7 +13,7 @@ class User(Base, BaseMixin): user_name = Column(String(255), nullable=False) email = Column(String(255)) password = Column(String(255)) - enabled = Column(BIT(1)) + enabled = Column(Boolean) matrix = relationship("AuthorizationMatrix") tokens = relationship("Token") @@ -34,7 +33,7 @@ class Token(Base, BaseMixin): token = Column(String(255), nullable=False, unique=True) name = Column(String(255)) last_used_date: Mapped[datetime] = mapped_column() - enabled = Column(BIT(1)) + enabled = Column(Boolean) user_id = Column(String(255), ForeignKey("user.id"), nullable=False) user = relationship("User", back_populates="tokens") @@ -56,7 +55,7 @@ class AuthorizationMatrix(Base, BaseMixin): class ModuleData(Base, BaseMixin): __tablename__ = "module_data" module_name = Column(String(255), nullable=False) - import_data = Column(BIT(1)) + import_data = Column(Boolean) class MailAccount(Base, BaseMixin): @@ -66,7 +65,7 @@ class MailAccount(Base, BaseMixin): protocol = Column(String(255)) user_name = Column(String(255)) password = Column(String(255)) - start_tls = Column(BIT(1)) + start_tls = Column(Boolean) class Mail(Base, BaseMixin): diff --git a/kontor-schema/src/kontor_schema/base.py b/kontor-schema/src/kontor_schema/base.py index 4a354e7..5ef8183 100644 --- a/kontor-schema/src/kontor_schema/base.py +++ b/kontor-schema/src/kontor_schema/base.py @@ -1,8 +1,7 @@ import uuid from datetime import datetime -from sqlalchemy import func, Column, String -from sqlalchemy.dialects.mysql import BIT +from sqlalchemy import func, Column, String, Boolean from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column @@ -25,7 +24,7 @@ class BaseVideoMixin: cloud_link = Column(String(255)) file_name = Column(String(255)) path = Column(String(255)) - review = Column(BIT(1)) + review = Column(Boolean) title = Column(String(255)) url = Column(String(255), unique=True) - should_download = Column(BIT(1)) + should_download = Column(Boolean) diff --git a/kontor-schema/src/kontor_schema/comic.py b/kontor-schema/src/kontor_schema/comic.py index 1052d79..45cb8c3 100644 --- a/kontor-schema/src/kontor_schema/comic.py +++ b/kontor-schema/src/kontor_schema/comic.py @@ -1,5 +1,4 @@ -from sqlalchemy import Column, ForeignKey, Integer, String -from sqlalchemy.dialects.mysql import BIT +from sqlalchemy import Column, ForeignKey, Integer, String, Boolean from sqlalchemy.orm import relationship from .base import Base, BaseMixin @@ -22,8 +21,8 @@ class Comic(Base, BaseMixin): title = Column(String(length=255), unique=True) publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False) publisher = relationship("Publisher", back_populates="comics") - current_order = Column(BIT(1)) - completed = Column(BIT(1)) + current_order = Column(Boolean) + completed = Column(Boolean) issues = relationship("Issue") story_arcs = relationship("StoryArc") trade_paperbacks = relationship("TradePaperback") @@ -64,8 +63,8 @@ class StoryArc(Base, BaseMixin): class Issue(Base, BaseMixin): __tablename__ = "issue" issue_number = Column(String(255)) - in_stock = Column(BIT(1)) - is_read = Column(BIT(1)) + in_stock = Column(Boolean) + is_read = Column(Boolean) comic_id = Column(String, ForeignKey("comic.id"), nullable=False) comic = relationship("Comic", back_populates="issues") volume_id = Column(String, ForeignKey("volume.id"), nullable=True) diff --git a/kontor-schema/src/kontor_schema/database.py b/kontor-schema/src/kontor_schema/database.py index 4627d87..54d3596 100644 --- a/kontor-schema/src/kontor_schema/database.py +++ b/kontor-schema/src/kontor_schema/database.py @@ -35,6 +35,12 @@ class StatusType(Enum): CLOUD_LINK_ID = auto() +class ExportType(Enum): + JSON = "JSON" + YAML = "YAML" + SQLITE = "SQLite" + + class KontorDB: def __init__(self, db_engine: Engine, log: Logger): @@ -125,7 +131,6 @@ class KontorDB: def get_columns(self, table_name: str) -> dict: columns = {} - order = 0 __session__ = sessionmaker(self.engine) table_info = self.get_table_by_name(table_name) _filters = {'table_id': table_info['id']} @@ -177,7 +182,7 @@ class KontorDB: # self.log.info("data: %s", data) return data - def export_db(self, export_type: str, export_file_name: str) -> dict: + def export_db(self, export_type: ExportType, export_file_name: str) -> dict: results = {} db = {} export_table_list = self.get_table_names() @@ -211,14 +216,14 @@ class KontorDB: db[table] = entries results[table] = len(entries) match export_type: - case "JSON": + case ExportType.JSON: json_dump = json.dumps(db, indent=4) with open(export_file_name, "w") as dump_file: dump_file.write(json_dump) - case "YAML": - export_file = Path(export_file_name) - case "SQLite": - export_file = Path(export_file_name) + case ExportType.YAML: + pass + case ExportType.SQLITE: + pass self.log.info(f"{len(results)} tables exported") return results diff --git a/kontor-schema/src/kontor_schema/media.py b/kontor-schema/src/kontor_schema/media.py index f17eb43..d3a9723 100644 --- a/kontor-schema/src/kontor_schema/media.py +++ b/kontor-schema/src/kontor_schema/media.py @@ -5,8 +5,7 @@ from pathlib import Path import requests from bs4 import BeautifulSoup -from sqlalchemy import Column, String, ForeignKey -from sqlalchemy.dialects.mysql import BIT +from sqlalchemy import Column, String, ForeignKey, Boolean from sqlalchemy.orm import relationship from .base import Base, BaseMixin, BaseVideoMixin @@ -44,12 +43,12 @@ class MediaFile(Base, BaseMixin, BaseVideoMixin): lines_list = output.splitlines() file_name = self.__parse_output__(lines_list) if file_name is None: - self.review = 1 - self.should_download = 1 + self.review = True + self.should_download = True self.file_name = None else: download_file = Path(file_name) - self.should_download = 0 + self.should_download = False self.file_name = download_file.name self.cloud_link = str(download_file.absolute()) self.last_modified_date = datetime.now() @@ -84,17 +83,10 @@ class MediaActorFile(Base, BaseMixin): class MediaArticle(Base, BaseMixin): __tablename__ = 'media_article' - review = Column(BIT(1)) + review = Column(Boolean) title = Column(String(255)) url = Column(String(255), unique=True) -class MediaVideo(Base, BaseMixin): +class MediaVideo(Base, BaseMixin, BaseVideoMixin): __tablename__ = 'media_video' - cloud_link = Column(String(255)) - file_name = Column(String(255)) - path = Column(String(255)) - review = Column(BIT(1)) - title = Column(String(255)) - url = Column(String(255), unique=True) - should_download = Column(BIT(1)) diff --git a/kontor-schema/src/kontor_schema/metadata.py b/kontor-schema/src/kontor_schema/metadata.py index 950cebe..9ac5aa4 100644 --- a/kontor-schema/src/kontor_schema/metadata.py +++ b/kontor-schema/src/kontor_schema/metadata.py @@ -1,5 +1,4 @@ -from sqlalchemy import Column, String, ForeignKey, Integer -from sqlalchemy.dialects.mysql import BIT +from sqlalchemy import Column, String, ForeignKey, Integer, Boolean from sqlalchemy.orm import relationship from .base import Base, BaseMixin @@ -28,8 +27,8 @@ class MetaDataColumn(Base, BaseMixin): table = relationship("MetaDataTable", back_populates="table_columns") column_label = Column(String(255)) filter_label = Column(String(255)) - is_shown = Column(BIT(1)) - show_filter = Column(BIT(1)) + is_shown = Column(Boolean) + show_filter = Column(Boolean) ref_column = Column(String, nullable=True) def __repr__(self): diff --git a/kontor-schema/src/kontor_schema/tysc.py b/kontor-schema/src/kontor_schema/tysc.py index 32c88f1..5ab4baa 100644 --- a/kontor-schema/src/kontor_schema/tysc.py +++ b/kontor-schema/src/kontor_schema/tysc.py @@ -1,5 +1,4 @@ -from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint -from sqlalchemy.dialects.mysql import BIT +from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, Boolean from sqlalchemy.orm import relationship from .base import Base, BaseMixin @@ -78,8 +77,8 @@ class CardSet(Base, BaseMixin): UniqueConstraint("name", "vendor_id"), ) name = Column(String(255), index=True) - parallel_set = Column(BIT(1)) - insert_set = Column(BIT(1)) + parallel_set = Column(Boolean) + insert_set = Column(Boolean) vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True) vendor = relationship("Vendor", back_populates="card_sets") cards = relationship("Card") diff --git a/kontor-schema/uv.lock b/kontor-schema/uv.lock index 5aefc1e..5323172 100644 --- a/kontor-schema/uv.lock +++ b/kontor-schema/uv.lock @@ -2,7 +2,160 @@ version = 1 revision = 1 requires-python = ">=3.13" +[[package]] +name = "beautifulsoup4" +version = "4.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "greenlet" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/9c/666d8c71b18d0189cf801c0e0b31c4bfc609ac823883286045b1f3ae8994/greenlet-3.2.0.tar.gz", hash = "sha256:1d2d43bd711a43db8d9b9187500e6432ddb4fafe112d082ffabca8660a9e01a7", size = 183685 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/43/c0b655d4d7eae19282b028bcec449e5c80626ad0d8d0ca3703f9b1c29258/greenlet-3.2.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b86a3ccc865ae601f446af042707b749eebc297928ea7bd0c5f60c56525850be", size = 269131 }, + { url = "https://files.pythonhosted.org/packages/7c/7d/c8f51c373c7f7ac0f73d04a6fd77ab34f6f643cb41a0d186d05ba96708e7/greenlet-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144283ad88ed77f3ebd74710dd419b55dd15d18704b0ae05935766a93f5671c5", size = 637323 }, + { url = "https://files.pythonhosted.org/packages/89/65/c3ee41b2e56586737d6e124b250583695628ffa6b324855b3a1267a8d1d9/greenlet-3.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5be69cd50994b8465c3ad1467f9e63001f76e53a89440ad4440d1b6d52591280", size = 651430 }, + { url = "https://files.pythonhosted.org/packages/f0/07/33bd7a3dcde1db7259371d026ce76be1eb653d2d892334fc79a500b3c5ee/greenlet-3.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47aeadd1e8fbdef8fdceb8fb4edc0cbb398a57568d56fd68f2bc00d0d809e6b6", size = 645798 }, + { url = "https://files.pythonhosted.org/packages/35/5b/33c221a6a867030b0b770513a1b78f6c30e04294131dafdc8da78906bbe6/greenlet-3.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18adc14ab154ca6e53eecc9dc50ff17aeb7ba70b7e14779b26e16d71efa90038", size = 648271 }, + { url = "https://files.pythonhosted.org/packages/4d/dd/d6452248fa6093504e3b7525dc2bdc4e55a4296ec6ee74ba241a51d852e2/greenlet-3.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8622b33d8694ec373ad55050c3d4e49818132b44852158442e1931bb02af336", size = 606779 }, + { url = "https://files.pythonhosted.org/packages/9d/24/160f04d2589bcb15b8661dcd1763437b22e01643626899a4139bf98f02af/greenlet-3.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:e8ac9a2c20fbff3d0b853e9ef705cdedb70d9276af977d1ec1cde86a87a4c821", size = 1117968 }, + { url = "https://files.pythonhosted.org/packages/6c/ff/c6e3f3a5168fef5209cfd9498b2b5dd77a0bf29dfc686a03dcc614cf4432/greenlet-3.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:cd37273dc7ca1d5da149b58c8b3ce0711181672ba1b09969663905a765affe21", size = 1145510 }, + { url = "https://files.pythonhosted.org/packages/dc/62/5215e374819052e542b5bde06bd7d4a171454b6938c96a2384f21cb94279/greenlet-3.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8a8940a8d301828acd8b9f3f85db23069a692ff2933358861b19936e29946b95", size = 296004 }, + { url = "https://files.pythonhosted.org/packages/62/6d/dc9c909cba5cbf4b0833fce69912927a8ca74791c23c47b9fd4f28092108/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee59db626760f1ca8da697a086454210d36a19f7abecc9922a2374c04b47735b", size = 629900 }, + { url = "https://files.pythonhosted.org/packages/5e/a9/f3f304fbbbd604858ff3df303d7fa1d8f7f9e45a6ef74481aaf03aaac021/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7154b13ef87a8b62fc05419f12d75532d7783586ad016c57b5de8a1c6feeb517", size = 635270 }, + { url = "https://files.pythonhosted.org/packages/34/92/4b7b4e2e23ecc723cceef9fe3898e78c8e14e106cc7ba2f276a66161da3e/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:199453d64b02d0c9d139e36d29681efd0e407ed8e2c0bf89d88878d6a787c28f", size = 632534 }, + { url = "https://files.pythonhosted.org/packages/da/7f/91f0ecbe72c9d789fb7f400b39da9d1e87fcc2cf8746a9636479ba79ab01/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0010e928e1901d36625f21d008618273f9dda26b516dbdecf873937d39c9dff0", size = 628826 }, + { url = "https://files.pythonhosted.org/packages/9f/59/e449a44ce52b13751f55376d85adc155dd311608f6d2aa5b6bd2c8d15486/greenlet-3.2.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6005f7a86de836a1dc4b8d824a2339cdd5a1ca7cb1af55ea92575401f9952f4c", size = 593697 }, + { url = "https://files.pythonhosted.org/packages/bb/09/cca3392927c5c990b7a8ede64ccd0712808438d6490d63ce6b8704d6df5f/greenlet-3.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:17fd241c0d50bacb7ce8ff77a30f94a2d0ca69434ba2e0187cf95a5414aeb7e1", size = 1105762 }, + { url = "https://files.pythonhosted.org/packages/4d/b9/3d201f819afc3b7a8cd7ebe645f1a17799603e2d62c968154518f79f4881/greenlet-3.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:7b17a26abc6a1890bf77d5d6b71c0999705386b00060d15c10b8182679ff2790", size = 1125173 }, + { url = "https://files.pythonhosted.org/packages/80/7b/773a30602234597fc2882091f8e1d1a38ea0b4419d99ca7ed82c827e2c3a/greenlet-3.2.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:397b6bbda06f8fe895893d96218cd6f6d855a6701dc45012ebe12262423cec8b", size = 269908 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + [[package]] name = "kontor-schema" version = "0.1.0" source = { editable = "." } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "requests" }, + { name = "sqlalchemy" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", specifier = ">=4.13.4" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "sqlalchemy", specifier = ">=2.0.40" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "soupsieve" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.40" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/c3/3f2bfa5e4dcd9938405fe2fab5b6ab94a9248a4f9536ea2fd497da20525f/sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00", size = 9664299 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/18/4e3a86cc0232377bc48c373a9ba6a1b3fb79ba32dbb4eda0b357f5a2c59d/sqlalchemy-2.0.40-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:915866fd50dd868fdcc18d61d8258db1bf9ed7fbd6dfec960ba43365952f3b01", size = 2107887 }, + { url = "https://files.pythonhosted.org/packages/cb/60/9fa692b1d2ffc4cbd5f47753731fd332afed30137115d862d6e9a1e962c7/sqlalchemy-2.0.40-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a4c5a2905a9ccdc67a8963e24abd2f7afcd4348829412483695c59e0af9a705", size = 2098367 }, + { url = "https://files.pythonhosted.org/packages/4c/9f/84b78357ca641714a439eb3fbbddb17297dacfa05d951dbf24f28d7b5c08/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55028d7a3ebdf7ace492fab9895cbc5270153f75442a0472d8516e03159ab364", size = 3184806 }, + { url = "https://files.pythonhosted.org/packages/4b/7d/e06164161b6bfce04c01bfa01518a20cccbd4100d5c951e5a7422189191a/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cfedff6878b0e0d1d0a50666a817ecd85051d12d56b43d9d425455e608b5ba0", size = 3198131 }, + { url = "https://files.pythonhosted.org/packages/6d/51/354af20da42d7ec7b5c9de99edafbb7663a1d75686d1999ceb2c15811302/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb19e30fdae77d357ce92192a3504579abe48a66877f476880238a962e5b96db", size = 3131364 }, + { url = "https://files.pythonhosted.org/packages/7a/2f/48a41ff4e6e10549d83fcc551ab85c268bde7c03cf77afb36303c6594d11/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:16d325ea898f74b26ffcd1cf8c593b0beed8714f0317df2bed0d8d1de05a8f26", size = 3159482 }, + { url = "https://files.pythonhosted.org/packages/33/ac/e5e0a807163652a35be878c0ad5cfd8b1d29605edcadfb5df3c512cdf9f3/sqlalchemy-2.0.40-cp313-cp313-win32.whl", hash = "sha256:a669cbe5be3c63f75bcbee0b266779706f1a54bcb1000f302685b87d1b8c1500", size = 2080704 }, + { url = "https://files.pythonhosted.org/packages/1c/cb/f38c61f7f2fd4d10494c1c135ff6a6ddb63508d0b47bccccd93670637309/sqlalchemy-2.0.40-cp313-cp313-win_amd64.whl", hash = "sha256:641ee2e0834812d657862f3a7de95e0048bdcb6c55496f39c6fa3d435f6ac6ad", size = 2104564 }, + { url = "https://files.pythonhosted.org/packages/d1/7c/5fc8e802e7506fe8b55a03a2e1dab156eae205c91bee46305755e086d2e2/sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a", size = 1903894 }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, +] diff --git a/kontor-scripts/.python-version b/kontor-scripts/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/kontor-scripts/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/kontor-scripts/Makefile b/kontor-scripts/Makefile new file mode 100644 index 0000000..638616c --- /dev/null +++ b/kontor-scripts/Makefile @@ -0,0 +1,19 @@ + +.PHONY: clean +clean: + find . -name '*.py[co]' -delete + +.PHONY: test +test: + python -m pytest \ + -v \ + --cov=kontor \ + --cov-report=term \ + --cov-report=html:coverage-report \ + tests/ + +.PHONY: build +build: + uv sync + uv build + diff --git a/kontor-scripts/README.md b/kontor-scripts/README.md new file mode 100644 index 0000000..3bb42eb --- /dev/null +++ b/kontor-scripts/README.md @@ -0,0 +1,3 @@ +# kontor-scripts + + diff --git a/scripts/check_kontor.py b/kontor-scripts/check_kontor.py similarity index 72% rename from scripts/check_kontor.py rename to kontor-scripts/check_kontor.py index e75a543..158e53c 100644 --- a/scripts/check_kontor.py +++ b/kontor-scripts/check_kontor.py @@ -36,7 +36,7 @@ class FileStatus: self.id = response['id'] -def get_status_of_file(found_file: Path, cursor, log) -> FileStatus: +def get_status_of_file(found_file: Path, cursor, logger) -> FileStatus: status = FileStatus() try: cursor.execute(f'SELECT id, cloud_link FROM media_file WHERE file_name="{found_file.name}"') @@ -45,7 +45,7 @@ def get_status_of_file(found_file: Path, cursor, log) -> FileStatus: status.status_type = StatusType.FILE_NAME status.id = rows[0][0] except mariadb.Error as error: - log.debug(f'select failed with {error}') + logger.debug(f'select failed with {error}') try: cursor.execute(f'SELECT id FROM media_file WHERE id="{found_file.stem}"') rows = cursor.fetchall() @@ -55,9 +55,9 @@ def get_status_of_file(found_file: Path, cursor, log) -> FileStatus: if len(rows) > 1: status.status_type = StatusType.DUPLICATE for row in rows: - log.info(f"found {row[0]} with {found_file}") + logger.info(f"found {row[0]} with {found_file}") except mariadb.Error as error: - log.debug(f'select failed with {error}') + logger.debug(f'select failed with {error}') try: cursor.execute(f'SELECT id FROM media_file WHERE cloud_link LIKE "%{found_file.stem}%"') rows = cursor.fetchall() @@ -68,75 +68,75 @@ def get_status_of_file(found_file: Path, cursor, log) -> FileStatus: else: status.status_type = StatusType.CLOUD_LINK except mariadb.Error as error: - log.debug(f'select failed with {error}') + logger.debug(f'select failed with {error}') response = requests.get(f"http://127.0.0.1:8800/media/files/{found_file.stem}") - log.debug(f"Status: {response.status_code}") + logger.debug(f"Status: {response.status_code}") if response.status_code == 200: status.status_type = StatusType.FILE_ID status.id = response.json()['id'] return status -def rename_files_to_id(media_dir, dry_run, conn, log): +def rename_files_to_id(media_dir, dry_run, conn, logger): media_path = Path(media_dir) cursor = conn.cursor() for file in media_path.iterdir(): - log.debug('found file: {}'.format(file.name)) - status: FileStatus = get_status_of_file(file, cursor, log) + logger.debug('found file: {}'.format(file.name)) + status: FileStatus = get_status_of_file(file, cursor, logger) file_id = status.id if not file_id: - log.info(f"ID of file {file.name} is unknown") + logger.info(f"ID of file {file.name} is unknown") continue new_file_path = file.with_name(f"{file_id}{file.suffix}") match status.status_type: case StatusType.FILE_NAME: - log.info(f'status of {file.name} is file_name') - rename_file(file, new_file_path, dry_run, log) - update_cloud_link(file_id, new_file_path, conn, dry_run, log) + logger.info(f'status of {file.name} is file_name') + rename_file(file, new_file_path, dry_run, logger) + update_cloud_link(file_id, new_file_path, conn, dry_run, logger) case StatusType.FILE_ID: - log.info(f'status of {file.name} is file_id') - update_cloud_link(file_id, new_file_path, conn, dry_run, log) + logger.info(f'status of {file.name} is file_id') + update_cloud_link(file_id, new_file_path, conn, dry_run, logger) case StatusType.CLOUD_LINK: - log.info(f'status of {file.name} is cloud_link') - rename_file(file, new_file_path, dry_run, log) - update_cloud_link(file_id, new_file_path, conn, dry_run, log) + logger.info(f'status of {file.name} is cloud_link') + rename_file(file, new_file_path, dry_run, logger) + update_cloud_link(file_id, new_file_path, conn, dry_run, logger) case StatusType.CLOUD_LINK_ID: - log.debug(f'status of {file.name} is cloud_link_id') - update_cloud_link(file_id, new_file_path, conn, dry_run, log) + logger.debug(f'status of {file.name} is cloud_link_id') + update_cloud_link(file_id, new_file_path, conn, dry_run, logger) case StatusType.DUPLICATE: - log.info(f'status of {file.name} is duplicate') + logger.info(f'status of {file.name} is duplicate') case StatusType.UNKNOWN: - log.info(f'status of {file.name} is unknown') + logger.info(f'status of {file.name} is unknown') -def rename_file(current_file, new_file_path, dry_run, log): +def rename_file(current_file, new_file_path, dry_run, logger): if dry_run: - log.info('rename file {} to {}'.format(current_file.name, new_file_path.name)) + logger.info('rename file {} to {}'.format(current_file.name, new_file_path.name)) else: current_file.rename(Path(new_file_path)) -def update_cloud_link(file_id, file_path, conn, dry_run, log): +def update_cloud_link(file_id, file_path, conn, dry_run, logger): cursor = conn.cursor() - log.debug(f'update entry {file_id} with {file_path.absolute()}') + logger.debug(f'update entry {file_id} with {file_path.absolute()}') if dry_run: - log.debug(f'UPDATE media_file: cloud_link={file_path.absolute()}') + logger.debug(f'UPDATE media_file: cloud_link={file_path.absolute()}') else: cursor.execute('UPDATE media_file SET cloud_link="{}" WHERE id="{}"'.format(file_path.absolute(), file_id)) conn.commit() -def reset_cloud_link(conn, dry_run, log): +def reset_cloud_link(conn, dry_run, logger): cursor = conn.cursor() if dry_run: - log.info('UPDATE media_file SET cloud_link=""') + logger.info('UPDATE media_file SET cloud_link=""') else: cursor.execute('UPDATE media_file SET cloud_link="" WHERE id is NOT NULL') conn.commit() -def check_file_with_db(data_file: Path, m_conn, log): - log.info(f"read json file: {data_file}") - cursor = m_conn.cursor() - with open(data_file, 'r') as json_file: +def check_file_with_db(json_file: Path, conn, logger): + logger.info(f"read json file: {json_file}") + cursor = conn.cursor() + with open(json_file, 'r') as json_file: json_load = json.load(json_file) for table in json_load: - log.info(f"{table}: {len(json_load[table])}") + logger.info(f"{table}: {len(json_load[table])}") items = json_load[table] for item in items: item_id = item['id'] @@ -144,11 +144,11 @@ def check_file_with_db(data_file: Path, m_conn, log): cursor.execute(select_statement) rows = cursor.fetchall() count = len(rows) - log.info(f"{count} entries found for {item_id}") + logger.info(f"{count} entries found for {item_id}") if count == 0: - log.info(f"entry for {item_id} not found") + logger.info(f"entry for {item_id} not found") if count == 1: - log.info(f"check entry {item_id}") + logger.info(f"check entry {item_id}") #log.info(f"entry {rows[0]}") columns = [] values = [] @@ -156,7 +156,7 @@ def check_file_with_db(data_file: Path, m_conn, log): columns.append(key) values.append(value) for index, _ in enumerate(columns): - log.info(f"compare {values[index]} with {rows[0][index]}") + logger.info(f"compare {values[index]} with {rows[0][index]}") diff --git a/scripts/config.py b/kontor-scripts/config.py similarity index 100% rename from scripts/config.py rename to kontor-scripts/config.py diff --git a/scripts/copy_to_mariadb.py b/kontor-scripts/copy_to_mariadb.py similarity index 100% rename from scripts/copy_to_mariadb.py rename to kontor-scripts/copy_to_mariadb.py diff --git a/scripts/copy_to_sqlite.py b/kontor-scripts/copy_to_sqlite.py similarity index 100% rename from scripts/copy_to_sqlite.py rename to kontor-scripts/copy_to_sqlite.py diff --git a/scripts/db_structure.py b/kontor-scripts/db_structure.py similarity index 100% rename from scripts/db_structure.py rename to kontor-scripts/db_structure.py diff --git a/scripts/download.py b/kontor-scripts/download.py similarity index 83% rename from scripts/download.py rename to kontor-scripts/download.py index 2faa422..084d2cc 100644 --- a/scripts/download.py +++ b/kontor-scripts/download.py @@ -7,6 +7,7 @@ from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter from datetime import datetime from enum import Enum, auto from pathlib import Path +from typing import Dict, Union from uuid import UUID import requests @@ -21,6 +22,8 @@ parser.add_argument('--tool', '-t', default='yt-dlp') parser.add_argument('--dry-run', '-m', action='store_true') args = parser.parse_args() +type FileInfo = Dict[str, Union[str, bool]] + class FileStatus(Enum): DOWNLOADED = auto() RENAMED = auto() @@ -61,37 +64,41 @@ def __parse_output__(lines_list: list[str]) -> str | None: return file_name -def is_file_downloaded(media_file: dict, dir: Path) -> FileStatus: +def is_file_downloaded(media_file: FileInfo, media_dir: Path) -> FileStatus: file_name_as_title = f"{media_file['file_name']}" - file_title = Path(dir, f"{file_name_as_title}.mp4") + file_title = Path(media_dir, f"{file_name_as_title}.mp4") if file_title.exists(): log.info(f"{file_name_as_title} has been downloaded") + media_file['review'] = False media_file['should_download'] = False return FileStatus.DOWNLOADED file_name_as_id = f"{media_file['id']}" - file_with_id_as_name = Path(dir, f"{file_name_as_id}.mp4") + file_with_id_as_name = Path(media_dir, f"{file_name_as_id}.mp4") if file_with_id_as_name.exists(): log.info(f"{file_with_id_as_name} has been downloaded and renamed") - media_file['cloud_link'] = file_with_id_as_name + media_file['cloud_link'] = file_with_id_as_name.as_posix() + media_file['review'] = False media_file['should_download'] = False return FileStatus.RENAMED log.info("could not find file - start download") return FileStatus.UNKNOWN -def update_status(item_id: UUID, file_info: dict): +def update_status(item_id: UUID, file_info: FileInfo): update = requests.put(f"http://127.0.0.1:8800/media/files/{item_id}", json=file_info) - log.info(f"update status: {update.status_code}") - log.info(f"update result: {update.json()}") + status = update.status_code + log.info(f"update status: {status}") + if status < 300: + log.info(f"update result: {update.json()}") -def rename_file(file_info: dict): +def rename_file(file_info: FileInfo): item_id = file_info['id'] file = Path(args.dir, file_info['file_name']) new_file_path = file.with_name(f"{item_id}{file.suffix}") log.info(f"rename {file} to {new_file_path}") file.rename(Path(new_file_path)) - file_info['cloud_link'] = str(new_file_path) + file_info['cloud_link'] = new_file_path.as_posix() if __name__ == '__main__': @@ -105,6 +112,9 @@ if __name__ == '__main__': link = item['url'] file_id = item['id'] log.info(f"{file_id} - {link}") + if link is None: + item['url'] = "" + log.info(f"set url for {file_id} to empty string") download_status: FileStatus = is_file_downloaded(item, args.dir) match download_status: case FileStatus.DOWNLOADED: @@ -119,4 +129,3 @@ if __name__ == '__main__': log.info(f'{item}') update_status(file_id, item) log.info('kontor.download finished') - diff --git a/scripts/export.py b/kontor-scripts/export.py similarity index 94% rename from scripts/export.py rename to kontor-scripts/export.py index 913ce71..34805a1 100644 --- a/scripts/export.py +++ b/kontor-scripts/export.py @@ -4,14 +4,14 @@ import data from json file to MariaDB from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter import yaml +from kontor_schema import Base, KontorDB +from kontor_schema.database import ExportType from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from platformdirs import PlatformDirs from pathlib import Path -from schema import Base, KontorDB from config import get_logger -from schema.database import ExportType parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) parser.add_argument('--verbose', '-v', action='count', default=0) diff --git a/scripts/import.py b/kontor-scripts/import.py similarity index 100% rename from scripts/import.py rename to kontor-scripts/import.py diff --git a/scripts/json_to_mariadb.py b/kontor-scripts/json_to_mariadb.py similarity index 100% rename from scripts/json_to_mariadb.py rename to kontor-scripts/json_to_mariadb.py diff --git a/scripts/kontor.py b/kontor-scripts/kontor.py similarity index 100% rename from scripts/kontor.py rename to kontor-scripts/kontor.py diff --git a/kontor-scripts/pyproject.toml b/kontor-scripts/pyproject.toml new file mode 100644 index 0000000..078dcf4 --- /dev/null +++ b/kontor-scripts/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "kontor-scripts" +version = "0.1.0" +description = "Scripts to execute Kontor actions from commandline" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "beautifulsoup4>=4.13.4", + "mariadb>=1.1.12", + "pathlib>=1.0.1", + "platformdirs>=4.3.7", + "pytest>=8.3.5", + "pytest-cov>=6.1.1", + "pyyaml>=6.0.2", + "requests>=2.32.3", + "kontor.schema>=0.1.0", + "sqlalchemy>=2.0.40", +] +[tool.uv.sources] +kontor-schema = { path = "../kontor-schema"} diff --git a/scripts/read_list.py b/kontor-scripts/read_list.py similarity index 100% rename from scripts/read_list.py rename to kontor-scripts/read_list.py diff --git a/scripts/update_title.py b/kontor-scripts/update_title.py similarity index 100% rename from scripts/update_title.py rename to kontor-scripts/update_title.py diff --git a/kontor-scripts/uv.lock b/kontor-scripts/uv.lock new file mode 100644 index 0000000..47f7209 --- /dev/null +++ b/kontor-scripts/uv.lock @@ -0,0 +1,333 @@ +version = 1 +revision = 1 +requires-python = ">=3.13" + +[[package]] +name = "beautifulsoup4" +version = "4.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708 }, + { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981 }, + { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495 }, + { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538 }, + { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561 }, + { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633 }, + { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712 }, + { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000 }, + { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195 }, + { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998 }, + { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541 }, + { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767 }, + { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997 }, + { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708 }, + { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046 }, + { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139 }, + { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307 }, + { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116 }, + { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909 }, + { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068 }, + { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435 }, +] + +[[package]] +name = "greenlet" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/9c/666d8c71b18d0189cf801c0e0b31c4bfc609ac823883286045b1f3ae8994/greenlet-3.2.0.tar.gz", hash = "sha256:1d2d43bd711a43db8d9b9187500e6432ddb4fafe112d082ffabca8660a9e01a7", size = 183685 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/43/c0b655d4d7eae19282b028bcec449e5c80626ad0d8d0ca3703f9b1c29258/greenlet-3.2.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b86a3ccc865ae601f446af042707b749eebc297928ea7bd0c5f60c56525850be", size = 269131 }, + { url = "https://files.pythonhosted.org/packages/7c/7d/c8f51c373c7f7ac0f73d04a6fd77ab34f6f643cb41a0d186d05ba96708e7/greenlet-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144283ad88ed77f3ebd74710dd419b55dd15d18704b0ae05935766a93f5671c5", size = 637323 }, + { url = "https://files.pythonhosted.org/packages/89/65/c3ee41b2e56586737d6e124b250583695628ffa6b324855b3a1267a8d1d9/greenlet-3.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5be69cd50994b8465c3ad1467f9e63001f76e53a89440ad4440d1b6d52591280", size = 651430 }, + { url = "https://files.pythonhosted.org/packages/f0/07/33bd7a3dcde1db7259371d026ce76be1eb653d2d892334fc79a500b3c5ee/greenlet-3.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47aeadd1e8fbdef8fdceb8fb4edc0cbb398a57568d56fd68f2bc00d0d809e6b6", size = 645798 }, + { url = "https://files.pythonhosted.org/packages/35/5b/33c221a6a867030b0b770513a1b78f6c30e04294131dafdc8da78906bbe6/greenlet-3.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18adc14ab154ca6e53eecc9dc50ff17aeb7ba70b7e14779b26e16d71efa90038", size = 648271 }, + { url = "https://files.pythonhosted.org/packages/4d/dd/d6452248fa6093504e3b7525dc2bdc4e55a4296ec6ee74ba241a51d852e2/greenlet-3.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8622b33d8694ec373ad55050c3d4e49818132b44852158442e1931bb02af336", size = 606779 }, + { url = "https://files.pythonhosted.org/packages/9d/24/160f04d2589bcb15b8661dcd1763437b22e01643626899a4139bf98f02af/greenlet-3.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:e8ac9a2c20fbff3d0b853e9ef705cdedb70d9276af977d1ec1cde86a87a4c821", size = 1117968 }, + { url = "https://files.pythonhosted.org/packages/6c/ff/c6e3f3a5168fef5209cfd9498b2b5dd77a0bf29dfc686a03dcc614cf4432/greenlet-3.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:cd37273dc7ca1d5da149b58c8b3ce0711181672ba1b09969663905a765affe21", size = 1145510 }, + { url = "https://files.pythonhosted.org/packages/dc/62/5215e374819052e542b5bde06bd7d4a171454b6938c96a2384f21cb94279/greenlet-3.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8a8940a8d301828acd8b9f3f85db23069a692ff2933358861b19936e29946b95", size = 296004 }, + { url = "https://files.pythonhosted.org/packages/62/6d/dc9c909cba5cbf4b0833fce69912927a8ca74791c23c47b9fd4f28092108/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee59db626760f1ca8da697a086454210d36a19f7abecc9922a2374c04b47735b", size = 629900 }, + { url = "https://files.pythonhosted.org/packages/5e/a9/f3f304fbbbd604858ff3df303d7fa1d8f7f9e45a6ef74481aaf03aaac021/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7154b13ef87a8b62fc05419f12d75532d7783586ad016c57b5de8a1c6feeb517", size = 635270 }, + { url = "https://files.pythonhosted.org/packages/34/92/4b7b4e2e23ecc723cceef9fe3898e78c8e14e106cc7ba2f276a66161da3e/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:199453d64b02d0c9d139e36d29681efd0e407ed8e2c0bf89d88878d6a787c28f", size = 632534 }, + { url = "https://files.pythonhosted.org/packages/da/7f/91f0ecbe72c9d789fb7f400b39da9d1e87fcc2cf8746a9636479ba79ab01/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0010e928e1901d36625f21d008618273f9dda26b516dbdecf873937d39c9dff0", size = 628826 }, + { url = "https://files.pythonhosted.org/packages/9f/59/e449a44ce52b13751f55376d85adc155dd311608f6d2aa5b6bd2c8d15486/greenlet-3.2.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6005f7a86de836a1dc4b8d824a2339cdd5a1ca7cb1af55ea92575401f9952f4c", size = 593697 }, + { url = "https://files.pythonhosted.org/packages/bb/09/cca3392927c5c990b7a8ede64ccd0712808438d6490d63ce6b8704d6df5f/greenlet-3.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:17fd241c0d50bacb7ce8ff77a30f94a2d0ca69434ba2e0187cf95a5414aeb7e1", size = 1105762 }, + { url = "https://files.pythonhosted.org/packages/4d/b9/3d201f819afc3b7a8cd7ebe645f1a17799603e2d62c968154518f79f4881/greenlet-3.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:7b17a26abc6a1890bf77d5d6b71c0999705386b00060d15c10b8182679ff2790", size = 1125173 }, + { url = "https://files.pythonhosted.org/packages/80/7b/773a30602234597fc2882091f8e1d1a38ea0b4419d99ca7ed82c827e2c3a/greenlet-3.2.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:397b6bbda06f8fe895893d96218cd6f6d855a6701dc45012ebe12262423cec8b", size = 269908 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + +[[package]] +name = "kontor-schema" +version = "0.1.0" +source = { directory = "../kontor-schema" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "requests" }, + { name = "sqlalchemy" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", specifier = ">=4.13.4" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "sqlalchemy", specifier = ">=2.0.40" }, +] + +[[package]] +name = "kontor-scripts" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "kontor-schema" }, + { name = "mariadb" }, + { name = "pathlib" }, + { name = "platformdirs" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sqlalchemy" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", specifier = ">=4.13.4" }, + { name = "kontor-schema", directory = "../kontor-schema" }, + { name = "mariadb", specifier = ">=1.1.12" }, + { name = "pathlib", specifier = ">=1.0.1" }, + { name = "platformdirs", specifier = ">=4.3.7" }, + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-cov", specifier = ">=6.1.1" }, + { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "sqlalchemy", specifier = ">=2.0.40" }, +] + +[[package]] +name = "mariadb" +version = "1.1.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/bb/4bbc803fbdafedbfba015f7cc1ab1e87a6d1de36725ba058c53e2f8a45ad/mariadb-1.1.12.tar.gz", hash = "sha256:50b02ff2c78b1b4f4628a054e3c8c7dd92972137727a5cc309a64c9ed20c878c", size = 85934 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/1b/b6eca3870ac1b5577a10d3b49ba42ac263c2e5718c9224cc1c8463940422/mariadb-1.1.12-cp313-cp313-win32.whl", hash = "sha256:ba43c42130d41352f32a5786c339cc931d05472ef7640fa3764d428dc294b88e", size = 184338 }, + { url = "https://files.pythonhosted.org/packages/fb/ff/c29a543ee1f9009755bc304138f61cd9b0ee1f14533e446513f84ccf143a/mariadb-1.1.12-cp313-cp313-win_amd64.whl", hash = "sha256:b69bc18418e72fcf359d17736cdc3f601a271203aff13ef7c57a415c8fd52ab0", size = 201272 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "pathlib" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/aa/9b065a76b9af472437a0059f77e8f962fe350438b927cb80184c32f075eb/pathlib-1.0.1.tar.gz", hash = "sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f", size = 49298 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/f9/690a8600b93c332de3ab4a344a4ac34f00c8f104917061f779db6a918ed6/pathlib-1.0.1-py3-none-any.whl", hash = "sha256:f35f95ab8b0f59e6d354090350b44a80a80635d22efdedfa84c7ad1cf0a74147", size = 14363 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, +] + +[[package]] +name = "pytest-cov" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "soupsieve" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.40" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/c3/3f2bfa5e4dcd9938405fe2fab5b6ab94a9248a4f9536ea2fd497da20525f/sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00", size = 9664299 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/18/4e3a86cc0232377bc48c373a9ba6a1b3fb79ba32dbb4eda0b357f5a2c59d/sqlalchemy-2.0.40-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:915866fd50dd868fdcc18d61d8258db1bf9ed7fbd6dfec960ba43365952f3b01", size = 2107887 }, + { url = "https://files.pythonhosted.org/packages/cb/60/9fa692b1d2ffc4cbd5f47753731fd332afed30137115d862d6e9a1e962c7/sqlalchemy-2.0.40-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a4c5a2905a9ccdc67a8963e24abd2f7afcd4348829412483695c59e0af9a705", size = 2098367 }, + { url = "https://files.pythonhosted.org/packages/4c/9f/84b78357ca641714a439eb3fbbddb17297dacfa05d951dbf24f28d7b5c08/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55028d7a3ebdf7ace492fab9895cbc5270153f75442a0472d8516e03159ab364", size = 3184806 }, + { url = "https://files.pythonhosted.org/packages/4b/7d/e06164161b6bfce04c01bfa01518a20cccbd4100d5c951e5a7422189191a/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cfedff6878b0e0d1d0a50666a817ecd85051d12d56b43d9d425455e608b5ba0", size = 3198131 }, + { url = "https://files.pythonhosted.org/packages/6d/51/354af20da42d7ec7b5c9de99edafbb7663a1d75686d1999ceb2c15811302/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb19e30fdae77d357ce92192a3504579abe48a66877f476880238a962e5b96db", size = 3131364 }, + { url = "https://files.pythonhosted.org/packages/7a/2f/48a41ff4e6e10549d83fcc551ab85c268bde7c03cf77afb36303c6594d11/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:16d325ea898f74b26ffcd1cf8c593b0beed8714f0317df2bed0d8d1de05a8f26", size = 3159482 }, + { url = "https://files.pythonhosted.org/packages/33/ac/e5e0a807163652a35be878c0ad5cfd8b1d29605edcadfb5df3c512cdf9f3/sqlalchemy-2.0.40-cp313-cp313-win32.whl", hash = "sha256:a669cbe5be3c63f75bcbee0b266779706f1a54bcb1000f302685b87d1b8c1500", size = 2080704 }, + { url = "https://files.pythonhosted.org/packages/1c/cb/f38c61f7f2fd4d10494c1c135ff6a6ddb63508d0b47bccccd93670637309/sqlalchemy-2.0.40-cp313-cp313-win_amd64.whl", hash = "sha256:641ee2e0834812d657862f3a7de95e0048bdcb6c55496f39c6fa3d435f6ac6ad", size = 2104564 }, + { url = "https://files.pythonhosted.org/packages/d1/7c/5fc8e802e7506fe8b55a03a2e1dab156eae205c91bee46305755e086d2e2/sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a", size = 1903894 }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, +] diff --git a/scripts/__init__.py b/scripts.bak/__init__.py similarity index 100% rename from scripts/__init__.py rename to scripts.bak/__init__.py diff --git a/scripts/pyproject.toml b/scripts.bak/pyproject.toml similarity index 100% rename from scripts/pyproject.toml rename to scripts.bak/pyproject.toml diff --git a/scripts/schema/__init__.py b/scripts.bak/schema/__init__.py similarity index 100% rename from scripts/schema/__init__.py rename to scripts.bak/schema/__init__.py diff --git a/scripts/schema/admin.py b/scripts.bak/schema/admin.py similarity index 100% rename from scripts/schema/admin.py rename to scripts.bak/schema/admin.py diff --git a/scripts/schema/base.py b/scripts.bak/schema/base.py similarity index 100% rename from scripts/schema/base.py rename to scripts.bak/schema/base.py diff --git a/kontor-api/src/schema/bookshelf.py b/scripts.bak/schema/bookshelf.py similarity index 100% rename from kontor-api/src/schema/bookshelf.py rename to scripts.bak/schema/bookshelf.py diff --git a/scripts/schema/comic.py b/scripts.bak/schema/comic.py similarity index 100% rename from scripts/schema/comic.py rename to scripts.bak/schema/comic.py diff --git a/scripts/schema/database.py b/scripts.bak/schema/database.py similarity index 100% rename from scripts/schema/database.py rename to scripts.bak/schema/database.py diff --git a/scripts/schema/media.py b/scripts.bak/schema/media.py similarity index 100% rename from scripts/schema/media.py rename to scripts.bak/schema/media.py diff --git a/scripts/schema/metadata.py b/scripts.bak/schema/metadata.py similarity index 100% rename from scripts/schema/metadata.py rename to scripts.bak/schema/metadata.py diff --git a/scripts/schema/tysc.py b/scripts.bak/schema/tysc.py similarity index 100% rename from scripts/schema/tysc.py rename to scripts.bak/schema/tysc.py diff --git a/scripts/setup.py b/scripts.bak/setup.py similarity index 100% rename from scripts/setup.py rename to scripts.bak/setup.py diff --git a/scripts/uv.lock b/scripts.bak/uv.lock similarity index 100% rename from scripts/uv.lock rename to scripts.bak/uv.lock diff --git a/scripts/Makefile b/scripts/Makefile deleted file mode 100644 index b016c3c..0000000 --- a/scripts/Makefile +++ /dev/null @@ -1,31 +0,0 @@ -.PHONY: clean virtualenv test docker dist dist-upload - -clean: - find . -name '*.py[co]' -delete - -virtualenv: - virtualenv --prompt '|> kontor <| ' env - env/bin/pip install -r requirements-dev.txt - env/bin/python setup.py develop - @echo - @echo "VirtualENV Setup Complete. Now run: source env/bin/activate" - @echo - -test: - python -m pytest \ - -v \ - --cov=kontor \ - --cov-report=term \ - --cov-report=html:coverage-report \ - tests/ - -docker: clean - docker build -t kontor:latest . - -dist: clean - rm -rf dist/* - python setup.py sdist - python setup.py bdist_wheel - -dist-upload: - twine upload dist/* diff --git a/scripts/schema/bookshelf.py b/scripts/schema/bookshelf.py deleted file mode 100644 index 91e0ae4..0000000 --- a/scripts/schema/bookshelf.py +++ /dev/null @@ -1,50 +0,0 @@ -from sqlalchemy import Column, ForeignKey, Integer, String -from sqlalchemy.orm import relationship - -from .base import Base, BaseMixin - - -class Article(Base, BaseMixin): - __tablename__ = 'article' - title = Column(String(length=255), unique=True) - article_authors = relationship("ArticleAuthor") - - -class Author(Base, BaseMixin): - __tablename__ = 'author' - first_name = Column(String(255)) - last_name = Column(String(255)) - article_authors = relationship("ArticleAuthor") - book_authors = relationship("BookAuthor") - - -class BookshelfPublisher(Base, BaseMixin): - __tablename__ = 'bookshelf_publisher' - name = Column(String(length=255), unique=True) - books = relationship("Book") - - -class Book(Base, BaseMixin): - __tablename__ = 'book' - isbn = Column(String(255), unique=True) - title = Column(String(255)) - year = Column(Integer, nullable=False) - publisher_id = Column(String, ForeignKey('bookshelf_publisher.id'), nullable=False) - publisher = relationship('BookshelfPublisher', back_populates="books") - book_authors = relationship("BookAuthor") - - -class ArticleAuthor(Base, BaseMixin): - __tablename__ = 'article_author' - article_id = Column(String, ForeignKey('article.id'), nullable=False) - article = relationship('Article', back_populates="article_authors") - author_id = Column(String, ForeignKey('author.id'), nullable=False) - author = relationship('Author', back_populates="article_authors") - - -class BookAuthor(Base, BaseMixin): - __tablename__ = 'book_author' - author_id = Column(String, ForeignKey('author.id'), nullable=False) - author = relationship('Author', back_populates="book_authors") - book_id = Column(String, ForeignKey('book.id'), nullable=False) - book = relationship('Book', back_populates="book_authors")