From 820ae3d3747708842f57f27c3c3b14b764e0ebf7 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 13 Jan 2025 00:26:42 +0100 Subject: [PATCH 1/4] create files with abstract model class --- qt/database/media.py | 6 +++--- qt/database/tysc.py | 2 +- qt/gui/data_view.py | 12 ++++++++++++ qt/gui/data_view_model.py | 33 +++++++++++++++++++++++++++++++++ qt/gui/table_model.py | 15 ++++++++++----- 5 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 qt/gui/data_view.py create mode 100644 qt/gui/data_view_model.py diff --git a/qt/database/media.py b/qt/database/media.py index 621fdce..b5d793b 100644 --- a/qt/database/media.py +++ b/qt/database/media.py @@ -1,4 +1,4 @@ -from sqlalchemy import Boolean, Column, DateTime, Integer, String +from sqlalchemy import Column, DateTime, Integer, String from sqlalchemy.dialects.mysql import BIT from database.base import Base @@ -13,10 +13,10 @@ class MediaFile(Base): cloud_link = Column(String(255)) file_name = Column(String(255)) path = Column(String(255)) - review = Column(BIT(1), default=True) + review = Column(BIT(1)) title = Column(String(255)) url = Column(String(255)) - should_download = Column(Boolean, default=True) + should_download = Column(BIT(1)) def __repr__(self): return f'MediaFile({self.id} {self.title} {self.title})' diff --git a/qt/database/tysc.py b/qt/database/tysc.py index 9c94177..0e77165 100644 --- a/qt/database/tysc.py +++ b/qt/database/tysc.py @@ -1,4 +1,4 @@ -from sqlalchemy import Boolean, Column, DateTime, Integer, String, ForeignKey, UniqueConstraint +from sqlalchemy import Column, DateTime, Integer, String, ForeignKey, UniqueConstraint from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship diff --git a/qt/gui/data_view.py b/qt/gui/data_view.py new file mode 100644 index 0000000..91c66d2 --- /dev/null +++ b/qt/gui/data_view.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod + + +class DataViewMeta(ABC): + @abstractmethod + def get_header(self): + pass + + +class ComicView(DataViewMeta): + def get_header(self): + pass diff --git a/qt/gui/data_view_model.py b/qt/gui/data_view_model.py new file mode 100644 index 0000000..e4e1bab --- /dev/null +++ b/qt/gui/data_view_model.py @@ -0,0 +1,33 @@ +from typing import List + +from PyQt5.QtCore import QAbstractTableModel +from PySide6.QtCore import QModelIndex +from PySide6.QtGui import Qt + +from gui.data_view import DataViewMeta + + +class DataViewModel(QAbstractTableModel): + def __init__(self): + super().__init__() + self.main_window = None + self._config = None + self._data = List[DataViewMeta] + + def rowCount(self, parent = QModelIndex()): + return len(self._data) + + def columnCount(self, parent = QModelIndex()): + return 0 + + def headerData(self, section, orientation, role = Qt.ItemDataRole.DisplayRole): + return None + + def data(self, index, role = Qt.ItemDataRole.DisplayRole): + return None + + def setData(self, index, value, role = Qt.ItemDataRole.EditRole): + return False + + def flags(self, index): + return None diff --git a/qt/gui/table_model.py b/qt/gui/table_model.py index 5e80c3b..e11dacc 100644 --- a/qt/gui/table_model.py +++ b/qt/gui/table_model.py @@ -45,8 +45,8 @@ class KontorTableModel(QAbstractTableModel): if self._data is None: return None value = self._data[index.row()][index.column()] - if role == Qt.ItemDataRole.DisplayRole: - # print('{}: {}'.format(value, type(value))) + # print('{}:: {}:: {}: {}'.format(index, role, value, type(value))) + if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole: if isinstance(value, datetime): return value.strftime("%Y-%m-%d %M:%M:%S") if isinstance(value, str): @@ -57,6 +57,7 @@ class KontorTableModel(QAbstractTableModel): else: return self._main_window.cross if isinstance(value, int): + print('{}:: {}: {}'.format(index, value, type(value))) if value == 1: return self._main_window.tick else: @@ -69,7 +70,6 @@ class KontorTableModel(QAbstractTableModel): return str(value) if role == Qt.ItemDataRole.DecorationRole: if isinstance(value, bytes): - # print('{}: {}'.format(value, type(value))) if value == b'\x01': return self._main_window.tick else: @@ -91,13 +91,18 @@ class KontorTableModel(QAbstractTableModel): # print(f"Header count: {len(self._config.get_header())}") return len(self._config.header) - def setData(self, index, value, role=Qt.ItemDataRole.EditRole): + def setData(self, index, value, role: int) -> bool: + print(index, role) if role == Qt.ItemDataRole.EditRole: self._data[index.row()][index.column()] = value + print(self._data[index.row()][index.column()]) + self.dataChanged.emit(index, index) + return True if role == Qt.ItemDataRole.CheckStateRole: + print("role == Qt.ItemDataRole.CheckStateRole") checked = value == Qt.CheckState.Checked self._data[index.row()][index.column()] = checked - return True + return False def flags(self, index): return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate From d0eae1980a2af1846d346f5d20b2bd3ce4243530 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 13 Jan 2025 16:18:13 +0100 Subject: [PATCH 2/4] add export to json --- qt/database/__init__.py | 118 +++++++++++++++++++++++++++++++------- qt/database/base.py | 19 +++++- qt/database/comic.py | 8 +-- qt/gui/data_view_model.py | 13 ++--- qt/gui/dialogs.py | 4 +- qt/gui/main_window.py | 11 ++-- qt/gui/model_config.py | 31 +++++----- qt/gui/table_model.py | 2 +- 8 files changed, 152 insertions(+), 54 deletions(-) diff --git a/qt/database/__init__.py b/qt/database/__init__.py index c0191cc..d74e927 100644 --- a/qt/database/__init__.py +++ b/qt/database/__init__.py @@ -1,3 +1,7 @@ +import json +from datetime import datetime +from pathlib import Path + import mariadb from sqlalchemy import create_engine, select, text, MetaData, join from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker @@ -30,35 +34,67 @@ class KontorDB: __session__ = sessionmaker(bind=engine) self.session = __session__() - def get_table_id(self, table_name): - result = self.session.execute(select(MetaDataTable.id).where(MetaDataTable.table_name == table_name)).scalar() - return result - def get_table_names(self) -> list: tables = self.session.query(MetaDataTable).all() result = [table.table_name for table in tables] return result - def get_column_meta_data(self, table_id: str, table_name: str) -> dict: + def get_column_meta_data(self, table_name: str, view_only=True) -> dict: meta_data = {} order = 0 - for (_, column) in self.session.query(MetaDataTable, MetaDataColumn).filter(MetaDataTable.id == MetaDataColumn.table_id).filter(MetaDataTable.table_name == table_name).filter(MetaDataColumn.is_shown == 1).all(): - meta_data[order] = {'column': column.column_name, 'label': column.column_label, 'order': column.column_order, 'ref_column': column.ref_column} - order += 1 + if view_only: + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.is_shown == 1).all()): + meta_data[order] = {'column': column.column_name, 'label': column.column_label, + 'order': column.column_order, 'ref_column': column.ref_column} + order += 1 + else: + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name).all()): + meta_data[order] = { + 'column': column.column_name, + 'order': column.column_order, + 'ref_column': column.ref_column + } + order += 1 return meta_data - def get_filters(self, table_id): - cursor = self.db_conn.cursor() - filters = {} - cursor.execute( - "SELECT column_name, filter_label from meta_data_column WHERE table_id=? AND show_filter is true", - (table_id,)) - rows = cursor.fetchall() - for row in rows: - filters[row[0]] = {'label': row[1], 'widget': None} - cursor.close() - # print(f"retrieved {len(rows)} filters: {filters}") - return filters + def get_filters(self, table_name): + _filter_map = {} + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.show_filter == 1).all()): + _filter_map[column.column_name] = {'label': column.filter_label, 'widget': None} + print(f"retrieved {len(_filter_map)} filters: {_filter_map}") + return _filter_map + + def data(self, table, columns: dict, filters) -> list: + data = [] + entries = [] + if len(filters) == 0: + entries = self.session.query(table).all() + else: + entries = self.session.query(table).filter_by(**filters) + for entry in entries: + row = [] + for order in columns.keys(): + column_name = columns[order]['column'] + if str(column_name).endswith("_id"): + ref_table = column_name[:-3] + # print(f"{ref_table=}") + ref = getattr(entry, ref_table) + value = getattr(ref, "name") + # print(f"{value=}") + row.append(value) + else: + row.append(getattr(entry, column_name)) + # print(repr(row)) + data.append(row) + return data def get_data(self, table_name: str, columns: dict, where_clause: str) -> list: data = [] @@ -103,3 +139,45 @@ class KontorDB: statement = f"SELECT {columns} FROM {table} {where_clause}" print(f"{statement=}") return statement + + def export_db(self, export_type: str, export_file_name: str, export_table_list: list): + print(f"export DB to {export_file_name} as {export_type}") + db = {} + for table in export_table_list: + columns = self.get_column_meta_data(table, view_only=False) + model = Base.model_lookup_by_table_name(table) + rows = self.session.query(model).all() + entries = [] + print(f"found {len(rows)} entries") + print(f"found {len(columns)} columns") + for row in rows: + print(row) + entry = {} + for order in columns: + print(columns[order]) + column_name = columns[order]['column'] + 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 as error: + print("could not get value") + entries.append(entry) + db[table] = entries + export_file = Path(export_file_name) + 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": + export_file = Path(export_file_name) + case "SQLite": + export_file = Path(export_file_name) + case _: + print("unknown export type") + if export_file.exists(): + print(f"{export_file} exists") diff --git a/qt/database/base.py b/qt/database/base.py index 9339e51..f97d724 100644 --- a/qt/database/base.py +++ b/qt/database/base.py @@ -1,7 +1,20 @@ from sqlalchemy import Column, String, DateTime, Integer -from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker +from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker, declarative_base -class Base(DeclarativeBase): - pass +# class Base(DeclarativeBase): +# pass +class BaseModel: + + @classmethod + def model_lookup_by_table_name(cls, table_name): + registry_instance = getattr(cls, "registry") + for mapper_ in registry_instance.mappers: + model = mapper_.class_ + model_class_name = model.__tablename__ + if model_class_name == table_name: + return model + + +Base = declarative_base(cls=BaseModel) \ No newline at end of file diff --git a/qt/database/comic.py b/qt/database/comic.py index dc4204f..1dc31b4 100644 --- a/qt/database/comic.py +++ b/qt/database/comic.py @@ -1,4 +1,4 @@ -from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship @@ -106,7 +106,7 @@ class Artist(Base): comic_works = relationship("ComicWork") -class Worktype(Base): +class WorkType(Base): __tablename__ = "worktype" id = Column(String, primary_key=True) created_date = Column(DateTime) @@ -126,5 +126,5 @@ class ComicWork(Base): comic = relationship("Comic", back_populates="comic_works") artist_id = Column(String, ForeignKey("artist.id"), nullable=False) artist = relationship("Artist", back_populates="comic_works") - worktype_id = Column(String, ForeignKey("worktype.id"), nullable=False) - worktype = relationship("Worktype", 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/qt/gui/data_view_model.py b/qt/gui/data_view_model.py index e4e1bab..4ffdc8c 100644 --- a/qt/gui/data_view_model.py +++ b/qt/gui/data_view_model.py @@ -1,7 +1,6 @@ from typing import List -from PyQt5.QtCore import QAbstractTableModel -from PySide6.QtCore import QModelIndex +from PySide6.QtCore import QModelIndex, QAbstractTableModel from PySide6.QtGui import Qt from gui.data_view import DataViewMeta @@ -14,19 +13,19 @@ class DataViewModel(QAbstractTableModel): self._config = None self._data = List[DataViewMeta] - def rowCount(self, parent = QModelIndex()): + def rowCount(self, parent=QModelIndex()): return len(self._data) - def columnCount(self, parent = QModelIndex()): + def columnCount(self, parent=QModelIndex()): return 0 - def headerData(self, section, orientation, role = Qt.ItemDataRole.DisplayRole): + def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): return None - def data(self, index, role = Qt.ItemDataRole.DisplayRole): + def data(self, index, role=Qt.ItemDataRole.DisplayRole): return None - def setData(self, index, value, role = Qt.ItemDataRole.EditRole): + def setData(self, index, value, role=Qt.ItemDataRole.EditRole): return False def flags(self, index): diff --git a/qt/gui/dialogs.py b/qt/gui/dialogs.py index 703baeb..21dbe92 100644 --- a/qt/gui/dialogs.py +++ b/qt/gui/dialogs.py @@ -1,7 +1,7 @@ from pathlib import Path from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QFileDialog, \ - QGroupBox, QCheckBox, QComboBox + QCheckBox, QComboBox class ExportKontorDialog(QDialog): @@ -10,7 +10,7 @@ class ExportKontorDialog(QDialog): self.parent = parent self.kontor_db = kontor_db - self.file_name = None + self.file_name = "data.json" self.tables = [] self._table_options = {} diff --git a/qt/gui/main_window.py b/qt/gui/main_window.py index 42b4383..f47abd5 100644 --- a/qt/gui/main_window.py +++ b/qt/gui/main_window.py @@ -3,6 +3,8 @@ from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidg from PySide6.QtWidgets import QLabel, QMainWindow from database import KontorDB +from database.media import MediaFile +from database.comic import Comic from gui.dialogs import ExportKontorDialog, ImportKontorDialog from gui.model_config import KontorModelConfig from gui.table_model import KontorTableModel @@ -33,8 +35,8 @@ class MainWindow(QMainWindow): parent_layout = QVBoxLayout() self.central_widget.setLayout(parent_layout) self.tabs = QTabWidget() - self.tabs.addTab(self.generate_data_tab("comic"), "Comics") - self.tabs.addTab(self.generate_data_tab("media_file"), "MediaFile") + self.tabs.addTab(self.generate_data_tab("comic", Comic), "Comics") + self.tabs.addTab(self.generate_data_tab("media_file", MediaFile), "MediaFile") self.tabs.currentChanged.connect(self._tab_changed) #label.setAlignment(Qt.AlignmentFlag.AlignCenter) parent_layout.addWidget(self.tabs) @@ -111,6 +113,7 @@ class MainWindow(QMainWindow): print(export_dlg.get_tables_to_export()) print(f"export DB to {export_dlg.file_name}") self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000) + self.kontor_db.export_db(export_dlg.current_export_type, export_dlg.file_name, export_dlg.get_tables_to_export()) else: self.statusBar.showMessage("Export cancelled", 3000) @@ -120,9 +123,9 @@ class MainWindow(QMainWindow): def _tab_changed(self, tab_index): self.data[tab_index].refresh() - def generate_data_tab(self, table_name): + def generate_data_tab(self, table_name, table): data_tab = QWidget() - table_config = KontorModelConfig(self.kontor_db, self, table_name) + table_config = KontorModelConfig(self.kontor_db, self, table_name, table) model = KontorTableModel(table_config) layout = QVBoxLayout() self.data.append(model) diff --git a/qt/gui/model_config.py b/qt/gui/model_config.py index c2644e9..a3f902d 100644 --- a/qt/gui/model_config.py +++ b/qt/gui/model_config.py @@ -6,25 +6,18 @@ from database import KontorDB class KontorModelConfig: - def __init__(self, kontor_db: KontorDB, main_window, table_name: str): + def __init__(self, kontor_db: KontorDB, main_window, table_name: str, table): self.header = {} self.filter = {} self.main_window = main_window - self._table = table_name - self._table_id = None + self._table_name = table_name + self._table = table self.kontor_db = kontor_db self.get_table_config() - def get_table_id(self): - if self._table_id is not None: - return - self._table_id = self.kontor_db.get_table_id(self._table) - def get_table_config(self): - if self._table_id is None: - self.get_table_id() - self.header = self.kontor_db.get_column_meta_data(self._table_id, self._table) - self.filter = self.kontor_db.get_filters(self._table_id) + self.header = self.kontor_db.get_column_meta_data(self._table_name) + self.filter = self.kontor_db.get_filters(self._table_name) def get_filter(self) -> str: filter_rule = "" @@ -41,8 +34,20 @@ class KontorModelConfig: # print(f"{filter_rule=}") return filter_rule + def filters(self) -> dict: + _filters = {} + # print(self.filter["download"].isChecked()) + for column, filter_info in self.filter.items(): + # print(column, filter_info) + if filter_info['widget'].isChecked(): + _filters[column] = True + # print(f"{filter_rule=}") + return _filters + def get_data(self) -> list: - data = self.kontor_db.get_data(self._table, self.header, self.get_filter()) + # data = self.kontor_db.get_data(self._table_name, self.header, self.get_filter()) + # data.clear() + data = self.kontor_db.data(self._table, self.header, self.filters()) # print(f"KontorModelConfig.get_data: {len(data)}") # comics = self.kontor_db.session.query(Comic).all() # print(f'{len(comics)} Comics loaded') diff --git a/qt/gui/table_model.py b/qt/gui/table_model.py index e11dacc..e1ba6ae 100644 --- a/qt/gui/table_model.py +++ b/qt/gui/table_model.py @@ -57,7 +57,7 @@ class KontorTableModel(QAbstractTableModel): else: return self._main_window.cross if isinstance(value, int): - print('{}:: {}: {}'.format(index, value, type(value))) + # print('{}:: {}: {}'.format(index, value, type(value))) if value == 1: return self._main_window.tick else: From f74c07af9aec29840fb9a635cd43da710f78f02c Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Mon, 13 Jan 2025 22:54:25 +0100 Subject: [PATCH 3/4] add cli app and fix relationship typos --- python/cli/.gitignore | 105 +++++++++ python/cli/CHANGELOG.md | 5 + python/cli/Dockerfile | 10 + python/cli/LICENSE.md | 1 + python/cli/MANIFEST.in | 5 + python/cli/Makefile | 31 +++ python/cli/README.md | 69 ++++++ python/cli/config/kontor.yml.example | 46 ++++ .../__init__.py => python/cli/docs/.gitkeep | 0 python/cli/kontor/__init__.py | 0 python/cli/kontor/controllers/__init__.py | 0 python/cli/kontor/controllers/clibase.py | 60 +++++ python/cli/kontor/core/__init__.py | 0 python/cli/kontor/core/exc.py | 4 + python/cli/kontor/core/version.py | 7 + .../cli/kontor}/database/__init__.py | 6 +- {qt => python/cli/kontor}/database/base.py | 0 {qt => python/cli/kontor}/database/comic.py | 2 +- python/cli/kontor/database/media.py | 25 +++ python/cli/kontor/database/metadata.py | 49 ++++ {qt => python/cli/kontor}/database/tysc.py | 2 +- python/cli/kontor/ext/__init__.py | 0 python/cli/kontor/main.py | 111 +++++++++ python/cli/kontor/plugins/__init__.py | 0 python/cli/kontor/templates/__init__.py | 0 python/cli/kontor/templates/command1.jinja2 | 4 + python/cli/requirements-dev.txt | 8 + python/cli/requirements.txt | 6 + python/cli/setup.cfg | 0 python/cli/setup.py | 28 +++ python/cli/tests/conftest.py | 16 ++ python/cli/tests/test_kontor.py | 36 +++ python/main.py | 16 ++ {qt => python/qt}/.gitignore | 0 python/qt/database/__init__.py | 212 ++++++++++++++++++ python/qt/database/base.py | 18 ++ python/qt/database/comic.py | 130 +++++++++++ {qt => python/qt}/database/media.py | 0 {qt => python/qt}/database/metadata.py | 0 python/qt/database/tysc.py | 131 +++++++++++ python/qt/gui/__init__.py | 0 {qt => python/qt}/gui/data_view.py | 0 {qt => python/qt}/gui/data_view_model.py | 0 {qt => python/qt}/gui/dialogs.py | 0 {qt => python/qt}/gui/main_window.py | 0 {qt => python/qt}/gui/model_config.py | 0 {qt => python/qt}/gui/table_model.py | 0 {qt => python/qt}/kontor.py | 0 {qt => python/qt}/pysidedeploy.spec | 0 {qt => python/qt}/res/application-export.png | Bin {qt => python/qt}/res/application-import.png | Bin {qt => python/qt}/res/arrow-circle-double.png | Bin {qt => python/qt}/res/cross.png | Bin {qt => python/qt}/res/tick.png | Bin 54 files changed, 1138 insertions(+), 5 deletions(-) create mode 100644 python/cli/.gitignore create mode 100644 python/cli/CHANGELOG.md create mode 100644 python/cli/Dockerfile create mode 100644 python/cli/LICENSE.md create mode 100644 python/cli/MANIFEST.in create mode 100644 python/cli/Makefile create mode 100644 python/cli/README.md create mode 100644 python/cli/config/kontor.yml.example rename qt/gui/__init__.py => python/cli/docs/.gitkeep (100%) create mode 100644 python/cli/kontor/__init__.py create mode 100644 python/cli/kontor/controllers/__init__.py create mode 100644 python/cli/kontor/controllers/clibase.py create mode 100644 python/cli/kontor/core/__init__.py create mode 100644 python/cli/kontor/core/exc.py create mode 100644 python/cli/kontor/core/version.py rename {qt => python/cli/kontor}/database/__init__.py (98%) rename {qt => python/cli/kontor}/database/base.py (100%) rename {qt => python/cli/kontor}/database/comic.py (99%) create mode 100644 python/cli/kontor/database/media.py create mode 100644 python/cli/kontor/database/metadata.py rename {qt => python/cli/kontor}/database/tysc.py (99%) create mode 100644 python/cli/kontor/ext/__init__.py create mode 100644 python/cli/kontor/main.py create mode 100644 python/cli/kontor/plugins/__init__.py create mode 100644 python/cli/kontor/templates/__init__.py create mode 100644 python/cli/kontor/templates/command1.jinja2 create mode 100644 python/cli/requirements-dev.txt create mode 100644 python/cli/requirements.txt create mode 100644 python/cli/setup.cfg create mode 100644 python/cli/setup.py create mode 100644 python/cli/tests/conftest.py create mode 100644 python/cli/tests/test_kontor.py create mode 100644 python/main.py rename {qt => python/qt}/.gitignore (100%) create mode 100644 python/qt/database/__init__.py create mode 100644 python/qt/database/base.py create mode 100644 python/qt/database/comic.py rename {qt => python/qt}/database/media.py (100%) rename {qt => python/qt}/database/metadata.py (100%) create mode 100644 python/qt/database/tysc.py create mode 100644 python/qt/gui/__init__.py rename {qt => python/qt}/gui/data_view.py (100%) rename {qt => python/qt}/gui/data_view_model.py (100%) rename {qt => python/qt}/gui/dialogs.py (100%) rename {qt => python/qt}/gui/main_window.py (100%) rename {qt => python/qt}/gui/model_config.py (100%) rename {qt => python/qt}/gui/table_model.py (100%) rename {qt => python/qt}/kontor.py (100%) rename {qt => python/qt}/pysidedeploy.spec (100%) rename {qt => python/qt}/res/application-export.png (100%) rename {qt => python/qt}/res/application-import.png (100%) rename {qt => python/qt}/res/arrow-circle-double.png (100%) rename {qt => python/qt}/res/cross.png (100%) rename {qt => python/qt}/res/tick.png (100%) diff --git a/python/cli/.gitignore b/python/cli/.gitignore new file mode 100644 index 0000000..a74b246 --- /dev/null +++ b/python/cli/.gitignore @@ -0,0 +1,105 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +coverage-report/ +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/python/cli/CHANGELOG.md b/python/cli/CHANGELOG.md new file mode 100644 index 0000000..6c95d97 --- /dev/null +++ b/python/cli/CHANGELOG.md @@ -0,0 +1,5 @@ +# Kontor Change History + +## 0.0.1 + +Initial release. diff --git a/python/cli/Dockerfile b/python/cli/Dockerfile new file mode 100644 index 0000000..d32cf5c --- /dev/null +++ b/python/cli/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.9-alpine +LABEL MAINTAINER="Thomas Peetz " +ENV PS1="\[\e[0;33m\]|> kontor <| \[\e[1;35m\]\W\[\e[0m\] \[\e[0m\]# " + +WORKDIR /src +COPY . /src +RUN pip install --no-cache-dir -r requirements.txt \ + && python setup.py install +WORKDIR / +ENTRYPOINT ["kontor"] diff --git a/python/cli/LICENSE.md b/python/cli/LICENSE.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/python/cli/LICENSE.md @@ -0,0 +1 @@ + diff --git a/python/cli/MANIFEST.in b/python/cli/MANIFEST.in new file mode 100644 index 0000000..1160952 --- /dev/null +++ b/python/cli/MANIFEST.in @@ -0,0 +1,5 @@ +recursive-include *.py +include setup.cfg +include README.md CHANGELOG.md LICENSE.md +include *.txt +recursive-include kontor/templates * diff --git a/python/cli/Makefile b/python/cli/Makefile new file mode 100644 index 0000000..b016c3c --- /dev/null +++ b/python/cli/Makefile @@ -0,0 +1,31 @@ +.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/python/cli/README.md b/python/cli/README.md new file mode 100644 index 0000000..6afe593 --- /dev/null +++ b/python/cli/README.md @@ -0,0 +1,69 @@ +# Kontor CLI Tool + +## Installation + +``` +$ pip install -r requirements.txt + +$ python setup.py install +``` + +## Development + +This project includes a number of helpers in the `Makefile` to streamline common development tasks. + +### Environment Setup + +The following demonstrates setting up and working with a development environment: + +``` +### create a virtualenv for development + +$ make virtualenv + +$ source env/bin/activate + + +### run kontor cli application + +$ kontor --help + + +### run pytest / coverage + +$ make test +``` + + +### Releasing to PyPi + +Before releasing to PyPi, you must configure your login credentials: + +**~/.pypirc**: + +``` +[pypi] +username = YOUR_USERNAME +password = YOUR_PASSWORD +``` + +Then use the included helper function via the `Makefile`: + +``` +$ make dist + +$ make dist-upload +``` + +## Deployments + +### Docker + +Included is a basic `Dockerfile` for building and distributing `Kontor`, +and can be built with the included `make` helper: + +``` +$ make docker + +$ docker run -it kontor --help +``` diff --git a/python/cli/config/kontor.yml.example b/python/cli/config/kontor.yml.example new file mode 100644 index 0000000..ba6507c --- /dev/null +++ b/python/cli/config/kontor.yml.example @@ -0,0 +1,46 @@ +### Kontor Configuration Settings +--- + +kontor: + +### Toggle application level debug (does not toggle framework debugging) +# debug: false + +### Where external (third-party) plugins are loaded from +# plugin_dir: /var/lib/kontor/plugins/ + +### Where all plugin configurations are loaded from +# plugin_config_dir: /etc/kontor/plugins.d/ + +### Where external templates are loaded from +# template_dir: /var/lib/kontor/templates/ + +### The log handler label +# log_handler: colorlog + +### The output handler label +# output_handler: jinja2 + +### sample foo option +# foo: bar + + +log.colorlog: + +### Where the log file lives (no log file by default) +# file: null + +### The level for which to log. One of: info, warning, error, fatal, debug +# level: info + +### Whether or not to log to console +# to_console: true + +### Whether or not to rotate the log file when it reaches `max_bytes` +# rotate: false + +### Max size in bytes that a log file can grow until it is rotated. +# max_bytes: 512000 + +### The maximum number of log files to maintain when rotating +# max_files: 4 diff --git a/qt/gui/__init__.py b/python/cli/docs/.gitkeep similarity index 100% rename from qt/gui/__init__.py rename to python/cli/docs/.gitkeep diff --git a/python/cli/kontor/__init__.py b/python/cli/kontor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/controllers/__init__.py b/python/cli/kontor/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/controllers/clibase.py b/python/cli/kontor/controllers/clibase.py new file mode 100644 index 0000000..8d69fb2 --- /dev/null +++ b/python/cli/kontor/controllers/clibase.py @@ -0,0 +1,60 @@ + +from cement import Controller, ex +from cement.utils.version import get_version_banner +from ..core.version import get_version + +VERSION_BANNER = """ +Kontor CLI Tool %s +%s +""" % (get_version(), get_version_banner()) + + +class CliBase(Controller): + class Meta: + label = 'base' + + # text displayed at the top of --help output + description = 'Kontor CLI Tool' + + # text displayed at the bottom of --help output + epilog = 'Usage: kontor command1 --foo bar' + + # controller level arguments. ex: 'kontor --version' + arguments = [ + ### add a version banner + ( [ '-v', '--version' ], + { 'action' : 'version', + 'version' : VERSION_BANNER } ), + ] + + + def _default(self): + """Default action if no sub-command is passed.""" + + self.app.args.print_help() + + + @ex( + help='example sub command1', + + # sub-command level arguments. ex: 'kontor command1 --foo bar' + arguments=[ + ### add a sample foo option under subcommand namespace + ( [ '-f', '--foo' ], + { 'help' : 'notorious foo option', + 'action' : 'store', + 'dest' : 'foo' } ), + ], + ) + def command1(self): + """Example sub-command.""" + + data = { + 'foo' : 'bar', + } + + ### do something with arguments + if self.app.pargs.foo is not None: + data['foo'] = self.app.pargs.foo + + self.app.render(data, 'command1.jinja2') diff --git a/python/cli/kontor/core/__init__.py b/python/cli/kontor/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/core/exc.py b/python/cli/kontor/core/exc.py new file mode 100644 index 0000000..aaeb159 --- /dev/null +++ b/python/cli/kontor/core/exc.py @@ -0,0 +1,4 @@ + +class KontorError(Exception): + """Generic errors.""" + pass diff --git a/python/cli/kontor/core/version.py b/python/cli/kontor/core/version.py new file mode 100644 index 0000000..d130f85 --- /dev/null +++ b/python/cli/kontor/core/version.py @@ -0,0 +1,7 @@ + +from cement.utils.version import get_version as cement_get_version + +VERSION = (0, 0, 1, 'alpha', 0) + +def get_version(version=VERSION): + return cement_get_version(version) diff --git a/qt/database/__init__.py b/python/cli/kontor/database/__init__.py similarity index 98% rename from qt/database/__init__.py rename to python/cli/kontor/database/__init__.py index d74e927..95ce1d8 100644 --- a/qt/database/__init__.py +++ b/python/cli/kontor/database/__init__.py @@ -6,9 +6,9 @@ import mariadb from sqlalchemy import create_engine, select, text, MetaData, join from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker -from database.base import Base -from database.comic import Comic -from database.metadata import MetaDataTable, MetaDataColumn +from .base import Base +from .comic import Comic +from .metadata import MetaDataTable, MetaDataColumn class KontorDB: diff --git a/qt/database/base.py b/python/cli/kontor/database/base.py similarity index 100% rename from qt/database/base.py rename to python/cli/kontor/database/base.py diff --git a/qt/database/comic.py b/python/cli/kontor/database/comic.py similarity index 99% rename from qt/database/comic.py rename to python/cli/kontor/database/comic.py index 1dc31b4..49d222f 100644 --- a/qt/database/comic.py +++ b/python/cli/kontor/database/comic.py @@ -2,7 +2,7 @@ from sqlalchemy import Column, DateTime, ForeignKey, Integer, String from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship -from database.base import Base +from .base import Base class Publisher(Base): diff --git a/python/cli/kontor/database/media.py b/python/cli/kontor/database/media.py new file mode 100644 index 0000000..0129c03 --- /dev/null +++ b/python/cli/kontor/database/media.py @@ -0,0 +1,25 @@ +from sqlalchemy import Column, DateTime, Integer, String +from sqlalchemy.dialects.mysql import BIT + +from .base import Base + + +class MediaFile(Base): + __tablename__ = 'media_file' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + 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)) + should_download = Column(BIT(1)) + + def __repr__(self): + return f'MediaFile({self.id} {self.title} {self.title})' + + def __str__(self): + return f'{self.title}({self.id})' diff --git a/python/cli/kontor/database/metadata.py b/python/cli/kontor/database/metadata.py new file mode 100644 index 0000000..2975e10 --- /dev/null +++ b/python/cli/kontor/database/metadata.py @@ -0,0 +1,49 @@ +from sqlalchemy import Column, String, ForeignKey, DateTime, Integer, Boolean +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from .base import Base + + +class MetaDataTable(Base): + __tablename__ = 'meta_data_table' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + 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): + __tablename__ = 'meta_data_column' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + column_modifier = Column(String(255), nullable=True) + column_name = Column(String(255)) + column_order = Column(Integer) + column_sync_name = Column(String(255)) + column_type = Column(String(255)) + 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(BIT(1)) + show_filter = Column(BIT(1)) + 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/qt/database/tysc.py b/python/cli/kontor/database/tysc.py similarity index 99% rename from qt/database/tysc.py rename to python/cli/kontor/database/tysc.py index 0e77165..54cacea 100644 --- a/qt/database/tysc.py +++ b/python/cli/kontor/database/tysc.py @@ -2,7 +2,7 @@ from sqlalchemy import Column, DateTime, Integer, String, ForeignKey, UniqueCons from sqlalchemy.dialects.mysql import BIT from sqlalchemy.orm import relationship -from database.base import Base +from .base import Base class Sport(Base): diff --git a/python/cli/kontor/ext/__init__.py b/python/cli/kontor/ext/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/main.py b/python/cli/kontor/main.py new file mode 100644 index 0000000..4c71aab --- /dev/null +++ b/python/cli/kontor/main.py @@ -0,0 +1,111 @@ + +from cement import App, TestApp, init_defaults +from cement.core.exc import CaughtSignal +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from .core.exc import KontorError +from .database.base import Base +from .controllers.clibase import CliBase + +# configuration defaults +CONFIG = init_defaults('kontor', 'mariadb') +CONFIG['kontor']['foo'] = 'bar' +CONFIG['mariadb']['user'] = 'kontor' +CONFIG['mariadb']['password'] = 'kontor' +CONFIG['mariadb']['host'] = '127.0.0.1' +CONFIG['mariadb']['port'] = '3306' +CONFIG['mariadb']['database'] = 'kontor' + +def extend_sqlalchemy(app): + app.log.info('extending kontor application with sqlalchemy') + connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format( + app.config.get('mariadb', 'user'), + app.config.get('mariadb', 'password'), + app.config.get('mariadb', 'host'), + app.config.get('mariadb', 'port'), + app.config.get('mariadb', 'database') + )) + # engine = create_engine(connect_string, echo=True) + engine = create_engine(connect_string) + Base.metadata.create_all(bind=engine) + __session__ = sessionmaker(bind=engine) + app.extend('session', __session__()) + + +class Kontor(App): + """Kontor primary application.""" + + class Meta: + label = 'kontor' + + # configuration defaults + config_defaults = CONFIG + + # call sys.exit() on close + exit_on_close = True + + # load additional framework extensions + extensions = [ + 'yaml', + 'colorlog', + 'jinja2', + ] + + # configuration handler + config_handler = 'yaml' + + # configuration file suffix + config_file_suffix = '.yml' + + # set the log handler + log_handler = 'colorlog' + + # set the output handler + output_handler = 'jinja2' + + hooks = [ + ('post_setup', extend_sqlalchemy), + ] + # register handlers + handlers = [ + CliBase + ] + + +class KontorTest(TestApp,Kontor): + """A sub-class of Kontor that is better suited for testing.""" + + class Meta: + label = 'kontor' + + +def main(): + with Kontor() as app: + try: + app.run() + + except AssertionError as e: + print('AssertionError > %s' % e.args[0]) + app.exit_code = 1 + + if app.debug is True: + import traceback + traceback.print_exc() + + except KontorError as e: + print('KontorError > %s' % e.args[0]) + app.exit_code = 1 + + if app.debug is True: + import traceback + traceback.print_exc() + + except CaughtSignal as e: + # Default Cement signals are SIGINT and SIGTERM, exit 0 (non-error) + print('\n%s' % e) + app.exit_code = 0 + + +if __name__ == '__main__': + main() diff --git a/python/cli/kontor/plugins/__init__.py b/python/cli/kontor/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/templates/__init__.py b/python/cli/kontor/templates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/kontor/templates/command1.jinja2 b/python/cli/kontor/templates/command1.jinja2 new file mode 100644 index 0000000..2435e4d --- /dev/null +++ b/python/cli/kontor/templates/command1.jinja2 @@ -0,0 +1,4 @@ + +Example Template (templates/command1.jinja2) + +Foo => {{ foo }} diff --git a/python/cli/requirements-dev.txt b/python/cli/requirements-dev.txt new file mode 100644 index 0000000..f20606e --- /dev/null +++ b/python/cli/requirements-dev.txt @@ -0,0 +1,8 @@ +-r requirements.txt + +pytest +pytest-cov +coverage +twine>=1.11.0 +setuptools>=38.6.0 +wheel>=0.31.0 diff --git a/python/cli/requirements.txt b/python/cli/requirements.txt new file mode 100644 index 0000000..bee2df1 --- /dev/null +++ b/python/cli/requirements.txt @@ -0,0 +1,6 @@ +cement==3.0.12 +cement[jinja2] +cement[yaml] +cement[colorlog] +mariadb +sqlalchemy diff --git a/python/cli/setup.cfg b/python/cli/setup.cfg new file mode 100644 index 0000000..e69de29 diff --git a/python/cli/setup.py b/python/cli/setup.py new file mode 100644 index 0000000..0b92a43 --- /dev/null +++ b/python/cli/setup.py @@ -0,0 +1,28 @@ + +from setuptools import setup, find_packages +from kontor.core.version import get_version + +VERSION = get_version() + +f = open('README.md', 'r') +LONG_DESCRIPTION = f.read() +f.close() + +setup( + name='kontor', + version=VERSION, + description='Kontor CLI Tool', + long_description=LONG_DESCRIPTION, + long_description_content_type='text/markdown', + author='Thomas Peetz', + author_email='thomas.peetz@thpeetz.de', + url='https://gitlab.com/tpeetz/kontor', + license='MIT', + packages=find_packages(exclude=['ez_setup', 'tests*']), + package_data={'kontor': ['templates/*']}, + include_package_data=True, + entry_points=""" + [console_scripts] + kontor = kontor.main:main + """, +) diff --git a/python/cli/tests/conftest.py b/python/cli/tests/conftest.py new file mode 100644 index 0000000..5124e2e --- /dev/null +++ b/python/cli/tests/conftest.py @@ -0,0 +1,16 @@ +""" +PyTest Fixtures. +""" + +import pytest +from cement import fs + +@pytest.fixture(scope="function") +def tmp(request): + """ + Create a `tmp` object that geneates a unique temporary directory, and file + for each test function that requires it. + """ + t = fs.Tmp() + yield t + t.remove() diff --git a/python/cli/tests/test_kontor.py b/python/cli/tests/test_kontor.py new file mode 100644 index 0000000..3c1bd67 --- /dev/null +++ b/python/cli/tests/test_kontor.py @@ -0,0 +1,36 @@ + +from pytest import raises +from kontor.main import KontorTest + +def test_kontor(): + # test kontor without any subcommands or arguments + with KontorTest() as app: + app.run() + assert app.exit_code == 0 + + +def test_kontor_debug(): + # test that debug mode is functional + argv = ['--debug'] + with KontorTest(argv=argv) as app: + app.run() + assert app.debug is True + + +def test_command1(): + # test command1 without arguments + argv = ['command1'] + with KontorTest(argv=argv) as app: + app.run() + data,output = app.last_rendered + assert data['foo'] == 'bar' + assert output.find('Foo => bar') + + + # test command1 with arguments + argv = ['command1', '--foo', 'not-bar'] + with KontorTest(argv=argv) as app: + app.run() + data,output = app.last_rendered + assert data['foo'] == 'not-bar' + assert output.find('Foo => not-bar') diff --git a/python/main.py b/python/main.py new file mode 100644 index 0000000..76d0e8c --- /dev/null +++ b/python/main.py @@ -0,0 +1,16 @@ +# This is a sample Python script. + +# Press Umschalt+F10 to execute it or replace it with your code. +# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings. + + +def print_hi(name): + # Use a breakpoint in the code line below to debug your script. + print(f'Hi, {name}') # Press Strg+F8 to toggle the breakpoint. + + +# Press the green button in the gutter to run the script. +if __name__ == '__main__': + print_hi('PyCharm') + +# See PyCharm help at https://www.jetbrains.com/help/pycharm/ diff --git a/qt/.gitignore b/python/qt/.gitignore similarity index 100% rename from qt/.gitignore rename to python/qt/.gitignore diff --git a/python/qt/database/__init__.py b/python/qt/database/__init__.py new file mode 100644 index 0000000..47f5706 --- /dev/null +++ b/python/qt/database/__init__.py @@ -0,0 +1,212 @@ +import json +from datetime import datetime +from pathlib import Path + +import mariadb +from sqlalchemy import create_engine, select, text, MetaData, join +from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker + +from .base import Base +from .comic import Comic, Artist, Publisher, ComicWork, WorkType, StoryArc, Volume, Issue, TradePaperback +from .tysc import Sport, Team, Card, CardSet, Vendor, Rooster, Player, FieldPosition +from .media import MediaFile +from .metadata import MetaDataTable, MetaDataColumn + + +class KontorDB: + + def __init__(self, db_config): + self.db_conn = mariadb.connect( + host=db_config['mariadb']['host'], + port=db_config['mariadb']['port'], + user=db_config['mariadb']['user'], + password=db_config['mariadb']['password'], + database=db_config['mariadb']['database'] + ) + connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}' + .format( + db_config['mariadb']['user'], + db_config['mariadb']['password'], + db_config['mariadb']['host'], + db_config['mariadb']['port'], + db_config['mariadb']['database'])) + # engine = create_engine(connect_string, echo=True) + engine = create_engine(connect_string) + Base.metadata.create_all(bind=engine) + __session__ = sessionmaker(bind=engine) + self.session = __session__() + self.registry = {} + self.init_registry() + + def init_registry(self): + self.registry['card'] = Card + self.registry['card_set'] = CardSet + self.registry['sport'] = Sport + self.registry['team'] = Team + self.registry['field_position'] = FieldPosition + self.registry['rooster'] = Rooster + self.registry['player'] = Player + self.registry['vendor'] = Vendor + self.registry['artist'] = Artist + self.registry['publisher'] = Publisher + self.registry['comic'] = Comic + self.registry['issue'] = Issue + self.registry['story_arc'] = StoryArc + self.registry['trade_paperback'] = TradePaperback + self.registry['volume'] = Volume + self.registry['comic_work'] = ComicWork + self.registry['worktype'] = WorkType + self.registry['media_file'] = MediaFile + + + def get_table_names(self) -> list: + tables = self.session.query(MetaDataTable).all() + result = [table.table_name for table in tables] + return result + + def get_column_meta_data(self, table_name: str, view_only=True) -> dict: + meta_data = {} + order = 0 + if view_only: + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.is_shown == 1).all()): + meta_data[order] = {'column': column.column_name, 'label': column.column_label, + 'order': column.column_order, 'ref_column': column.ref_column} + order += 1 + else: + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name).all()): + meta_data[order] = { + 'column': column.column_name, + 'order': column.column_order, + 'ref_column': column.ref_column + } + order += 1 + return meta_data + + def get_filters(self, table_name): + _filter_map = {} + for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn). + filter(MetaDataTable.id == MetaDataColumn.table_id). + filter(MetaDataTable.table_name == table_name). + filter(MetaDataColumn.show_filter == 1).all()): + _filter_map[column.column_name] = {'label': column.filter_label, 'widget': None} + print(f"retrieved {len(_filter_map)} filters: {_filter_map}") + return _filter_map + + def data(self, table, columns: dict, filters) -> list: + data = [] + entries = [] + if len(filters) == 0: + entries = self.session.query(table).all() + else: + entries = self.session.query(table).filter_by(**filters) + for entry in entries: + row = [] + for order in columns.keys(): + column_name = columns[order]['column'] + if str(column_name).endswith("_id"): + ref_table = column_name[:-3] + # print(f"{ref_table=}") + ref = getattr(entry, ref_table) + value = getattr(ref, "name") + # print(f"{value=}") + row.append(value) + else: + row.append(getattr(entry, column_name)) + # print(repr(row)) + data.append(row) + return data + + def get_data(self, table_name: str, columns: dict, where_clause: str) -> list: + data = [] + cursor = self.db_conn.cursor() + cursor.execute(self.get_statement(table_name, columns, where_clause)) + rows = cursor.fetchall() + print(len(rows)) + for row in rows: + # print(f"KontorDB.get_data: {row}") + data.append(list(row)) + cursor.close() + # print(f"KontorDB.getData: return {len(data)}") + if table_name == 'comic' and len(where_clause) == 0: + data.clear() + comics = self.session.query(Comic).all() + for item in comics: + # print(item) + row = [] + for order in columns.keys(): + column_name = columns[order]['column'] + if str(column_name).endswith("_id"): + ref_table = column_name[:-3] + # print(f"{ref_table=}") + ref = getattr(item, ref_table) + value = getattr(ref, "name") + # print(f"{value=}") + row.append(value) + else: + row.append(getattr(item, column_name)) + # print(repr(row)) + data.append(row) + return data + + def get_statement(self, table: str, header: dict, where_clause): + columns = "" + for index, column in header.items(): + if index > 0: + columns += ", " + columns += column['column'] + if len(columns) == 0: + columns = "*" + statement = f"SELECT {columns} FROM {table} {where_clause}" + print(f"{statement=}") + return statement + + def export_db(self, export_type: str, export_file_name: str, export_table_list: list): + print(f"export DB to {export_file_name} as {export_type}") + db = {} + 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: + print(f"table {table} is not registered") + continue + rows = self.session.query(model).all() + entries = [] + print(f"found {len(rows)} entries") + print(f"found {len(columns)} columns") + for row in rows: + # print(row) + entry = {} + for order in columns: + # print(columns[order]) + column_name = columns[order]['column'] + # 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 as error: + print("could not get value") + entries.append(entry) + db[table] = entries + export_file = Path(export_file_name) + 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": + export_file = Path(export_file_name) + case "SQLite": + export_file = Path(export_file_name) + case _: + print("unknown export type") + if export_file.exists(): + print(f"{export_file} exists") diff --git a/python/qt/database/base.py b/python/qt/database/base.py new file mode 100644 index 0000000..97d2990 --- /dev/null +++ b/python/qt/database/base.py @@ -0,0 +1,18 @@ +from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker, declarative_base + + +class Base(DeclarativeBase): + pass + +# class BaseModel: +# +# @classmethod +# def model_lookup_by_table_name(cls, table_name): +# registry_instance = getattr(cls, "registry") +# for mapper_ in registry_instance.mappers: +# model = mapper_.class_ +# model_class_name = model.__tablename__ +# if model_class_name == table_name: +# return model + +# Base = declarative_base(cls=BaseModel) \ No newline at end of file diff --git a/python/qt/database/comic.py b/python/qt/database/comic.py new file mode 100644 index 0000000..49d222f --- /dev/null +++ b/python/qt/database/comic.py @@ -0,0 +1,130 @@ +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from .base import Base + + +class Publisher(Base): + __tablename__ = "publisher" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + 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): + __tablename__ = 'comic' + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + 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)) + 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): + __tablename__ = "volume" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + 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): + __tablename__ = "trade_paperback" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + 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): + __tablename__ = "story_arc" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + 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): + __tablename__ = "issue" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + issue_number = Column(String(255)) + in_stock = Column(BIT(1)) + is_read = Column(BIT(1)) + 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): + __tablename__ = "artist" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False) + comic_works = relationship("ComicWork") + + +class WorkType(Base): + __tablename__ = "worktype" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(length=255), nullable=False, unique=True) + comic_works = relationship("ComicWork") + + +class ComicWork(Base): + __tablename__ = "comic_work" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + 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/qt/database/media.py b/python/qt/database/media.py similarity index 100% rename from qt/database/media.py rename to python/qt/database/media.py diff --git a/qt/database/metadata.py b/python/qt/database/metadata.py similarity index 100% rename from qt/database/metadata.py rename to python/qt/database/metadata.py diff --git a/python/qt/database/tysc.py b/python/qt/database/tysc.py new file mode 100644 index 0000000..733d7ed --- /dev/null +++ b/python/qt/database/tysc.py @@ -0,0 +1,131 @@ +from sqlalchemy import Column, DateTime, Integer, String, ForeignKey, UniqueConstraint +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from .base import Base + + +class Sport(Base): + __tablename__ = "sport" + __table_args__ = ( + UniqueConstraint("name"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255), nullable=False, index=True, unique=True) + teams = relationship("Team") + positions = relationship("FieldPosition") + + +class Team(Base): + __tablename__ = "team" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + 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): + __tablename__ = "field_position" + __table_args__ = ( + UniqueConstraint("name", "sport_id"), + UniqueConstraint("short_name", "sport_id"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + 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): + __tablename__ = "player" + __table_args__ = ( + UniqueConstraint("first_name", "last_name"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + 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): + __tablename__ = "rooster" + __table_args__ = ( + UniqueConstraint("year", "team_id", "player_id", "position_id"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + 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): + __tablename__ = "vendor" + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255), nullable=False, unique=True, index=True) + card_sets = relationship("CardSet") + cards = relationship("Card") + + +class CardSet(Base): + __tablename__ = "card_set" + __table_args__ = ( + UniqueConstraint("name", "vendor_id"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + name = Column(String(255), index=True) + parallel_set = Column(BIT(1)) + insert_set = Column(BIT(1)) + vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True) + vendor = relationship("Vendor", back_populates="card_sets") + cards = relationship("Card") + + +class Card(Base): + __tablename__ = "card" + __table_args__ = ( + UniqueConstraint("card_number", "year", "vendor_id", "card_set_id"), + ) + id = Column(String, primary_key=True) + created_date = Column(DateTime) + last_modified_date = Column(DateTime) + version = Column(Integer) + 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/python/qt/gui/__init__.py b/python/qt/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qt/gui/data_view.py b/python/qt/gui/data_view.py similarity index 100% rename from qt/gui/data_view.py rename to python/qt/gui/data_view.py diff --git a/qt/gui/data_view_model.py b/python/qt/gui/data_view_model.py similarity index 100% rename from qt/gui/data_view_model.py rename to python/qt/gui/data_view_model.py diff --git a/qt/gui/dialogs.py b/python/qt/gui/dialogs.py similarity index 100% rename from qt/gui/dialogs.py rename to python/qt/gui/dialogs.py diff --git a/qt/gui/main_window.py b/python/qt/gui/main_window.py similarity index 100% rename from qt/gui/main_window.py rename to python/qt/gui/main_window.py diff --git a/qt/gui/model_config.py b/python/qt/gui/model_config.py similarity index 100% rename from qt/gui/model_config.py rename to python/qt/gui/model_config.py diff --git a/qt/gui/table_model.py b/python/qt/gui/table_model.py similarity index 100% rename from qt/gui/table_model.py rename to python/qt/gui/table_model.py diff --git a/qt/kontor.py b/python/qt/kontor.py similarity index 100% rename from qt/kontor.py rename to python/qt/kontor.py diff --git a/qt/pysidedeploy.spec b/python/qt/pysidedeploy.spec similarity index 100% rename from qt/pysidedeploy.spec rename to python/qt/pysidedeploy.spec diff --git a/qt/res/application-export.png b/python/qt/res/application-export.png similarity index 100% rename from qt/res/application-export.png rename to python/qt/res/application-export.png diff --git a/qt/res/application-import.png b/python/qt/res/application-import.png similarity index 100% rename from qt/res/application-import.png rename to python/qt/res/application-import.png diff --git a/qt/res/arrow-circle-double.png b/python/qt/res/arrow-circle-double.png similarity index 100% rename from qt/res/arrow-circle-double.png rename to python/qt/res/arrow-circle-double.png diff --git a/qt/res/cross.png b/python/qt/res/cross.png similarity index 100% rename from qt/res/cross.png rename to python/qt/res/cross.png diff --git a/qt/res/tick.png b/python/qt/res/tick.png similarity index 100% rename from qt/res/tick.png rename to python/qt/res/tick.png From 276302570f38e525a6a813232dc246cc400e95ba Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Tue, 14 Jan 2025 13:10:24 +0100 Subject: [PATCH 4/4] merged command line and gui app in one command --- python/{cli => }/.gitignore | 3 + python/{cli => }/CHANGELOG.md | 0 python/{cli => }/Dockerfile | 0 python/{cli => }/LICENSE.md | 0 python/{cli => }/MANIFEST.in | 0 python/{cli => }/Makefile | 0 python/{cli => }/README.md | 0 python/cli/kontor/controllers/clibase.py | 60 -------- python/cli/kontor/database/base.py | 20 --- python/{cli => }/config/kontor.yml.example | 0 python/{cli => }/docs/.gitkeep | 0 python/{cli => }/kontor/__init__.py | 0 .../{cli => }/kontor/controllers/__init__.py | 0 python/kontor/controllers/clibase.py | 42 ++++++ python/kontor/controllers/database.py | 32 ++++ python/{cli => }/kontor/core/__init__.py | 0 python/{cli => }/kontor/core/exc.py | 0 python/{cli => }/kontor/core/version.py | 0 python/{cli => }/kontor/database/__init__.py | 123 ++++++---------- python/kontor/database/base.py | 5 + python/{cli => }/kontor/database/comic.py | 0 python/{cli => }/kontor/database/media.py | 0 python/{cli => }/kontor/database/metadata.py | 1 + python/{qt => kontor}/database/tysc.py | 0 python/{cli => }/kontor/ext/__init__.py | 0 .../kontor/plugins => kontor/gui}/__init__.py | 0 python/{qt => kontor}/gui/data_view.py | 0 python/{qt => kontor}/gui/data_view_model.py | 0 python/{qt => kontor}/gui/dialogs.py | 0 python/kontor/gui/main_window.py | 139 ++++++++++++++++++ python/kontor/gui/model_config.py | 50 +++++++ python/kontor/gui/table_model.py | 108 ++++++++++++++ python/{cli => }/kontor/main.py | 14 +- .../templates => kontor/plugins}/__init__.py | 0 .../{qt => kontor}/res/application-export.png | Bin .../{qt => kontor}/res/application-import.png | Bin .../res/arrow-circle-double.png | Bin python/{qt => kontor}/res/cross.png | Bin python/{qt => kontor}/res/tick.png | Bin .../{qt/gui => kontor/templates}/__init__.py | 0 .../kontor/templates/command1.jinja2 | 0 python/main.py | 16 -- python/{cli => }/requirements-dev.txt | 0 python/{cli => }/requirements.txt | 1 + python/{cli => }/setup.cfg | 0 python/{cli => }/setup.py | 0 python/{cli => }/tests/conftest.py | 0 python/{cli => }/tests/test_kontor.py | 0 {python/qt => qt}/.gitignore | 0 {python/qt => qt}/database/__init__.py | 0 {python/qt => qt}/database/base.py | 0 {python/qt => qt}/database/comic.py | 0 {python/qt => qt}/database/media.py | 0 {python/qt => qt}/database/metadata.py | 0 {python/cli/kontor => qt}/database/tysc.py | 8 +- qt/gui/__init__.py | 0 qt/gui/data_view.py | 12 ++ qt/gui/data_view_model.py | 32 ++++ qt/gui/dialogs.py | 106 +++++++++++++ {python/qt => qt}/gui/main_window.py | 0 {python/qt => qt}/gui/model_config.py | 0 {python/qt => qt}/gui/table_model.py | 0 {python/qt => qt}/kontor.py | 0 {python/qt => qt}/pysidedeploy.spec | 0 qt/res/application-export.png | Bin 0 -> 513 bytes qt/res/application-import.png | Bin 0 -> 524 bytes qt/res/arrow-circle-double.png | Bin 0 -> 836 bytes qt/res/cross.png | Bin 0 -> 544 bytes qt/res/tick.png | Bin 0 -> 634 bytes 69 files changed, 589 insertions(+), 183 deletions(-) rename python/{cli => }/.gitignore (98%) rename python/{cli => }/CHANGELOG.md (100%) rename python/{cli => }/Dockerfile (100%) rename python/{cli => }/LICENSE.md (100%) rename python/{cli => }/MANIFEST.in (100%) rename python/{cli => }/Makefile (100%) rename python/{cli => }/README.md (100%) delete mode 100644 python/cli/kontor/controllers/clibase.py delete mode 100644 python/cli/kontor/database/base.py rename python/{cli => }/config/kontor.yml.example (100%) rename python/{cli => }/docs/.gitkeep (100%) rename python/{cli => }/kontor/__init__.py (100%) rename python/{cli => }/kontor/controllers/__init__.py (100%) create mode 100644 python/kontor/controllers/clibase.py create mode 100644 python/kontor/controllers/database.py rename python/{cli => }/kontor/core/__init__.py (100%) rename python/{cli => }/kontor/core/exc.py (100%) rename python/{cli => }/kontor/core/version.py (100%) rename python/{cli => }/kontor/database/__init__.py (55%) create mode 100644 python/kontor/database/base.py rename python/{cli => }/kontor/database/comic.py (100%) rename python/{cli => }/kontor/database/media.py (100%) rename python/{cli => }/kontor/database/metadata.py (99%) rename python/{qt => kontor}/database/tysc.py (100%) rename python/{cli => }/kontor/ext/__init__.py (100%) rename python/{cli/kontor/plugins => kontor/gui}/__init__.py (100%) rename python/{qt => kontor}/gui/data_view.py (100%) rename python/{qt => kontor}/gui/data_view_model.py (100%) rename python/{qt => kontor}/gui/dialogs.py (100%) create mode 100644 python/kontor/gui/main_window.py create mode 100644 python/kontor/gui/model_config.py create mode 100644 python/kontor/gui/table_model.py rename python/{cli => }/kontor/main.py (91%) rename python/{cli/kontor/templates => kontor/plugins}/__init__.py (100%) rename python/{qt => kontor}/res/application-export.png (100%) rename python/{qt => kontor}/res/application-import.png (100%) rename python/{qt => kontor}/res/arrow-circle-double.png (100%) rename python/{qt => kontor}/res/cross.png (100%) rename python/{qt => kontor}/res/tick.png (100%) rename python/{qt/gui => kontor/templates}/__init__.py (100%) rename python/{cli => }/kontor/templates/command1.jinja2 (100%) delete mode 100644 python/main.py rename python/{cli => }/requirements-dev.txt (100%) rename python/{cli => }/requirements.txt (90%) rename python/{cli => }/setup.cfg (100%) rename python/{cli => }/setup.py (100%) rename python/{cli => }/tests/conftest.py (100%) rename python/{cli => }/tests/test_kontor.py (100%) rename {python/qt => qt}/.gitignore (100%) rename {python/qt => qt}/database/__init__.py (100%) rename {python/qt => qt}/database/base.py (100%) rename {python/qt => qt}/database/comic.py (100%) rename {python/qt => qt}/database/media.py (100%) rename {python/qt => qt}/database/metadata.py (100%) rename {python/cli/kontor => qt}/database/tysc.py (94%) create mode 100644 qt/gui/__init__.py create mode 100644 qt/gui/data_view.py create mode 100644 qt/gui/data_view_model.py create mode 100644 qt/gui/dialogs.py rename {python/qt => qt}/gui/main_window.py (100%) rename {python/qt => qt}/gui/model_config.py (100%) rename {python/qt => qt}/gui/table_model.py (100%) rename {python/qt => qt}/kontor.py (100%) rename {python/qt => qt}/pysidedeploy.spec (100%) mode change 100755 => 100644 create mode 100644 qt/res/application-export.png create mode 100644 qt/res/application-import.png create mode 100644 qt/res/arrow-circle-double.png create mode 100644 qt/res/cross.png create mode 100644 qt/res/tick.png diff --git a/python/cli/.gitignore b/python/.gitignore similarity index 98% rename from python/cli/.gitignore rename to python/.gitignore index a74b246..0f47561 100644 --- a/python/cli/.gitignore +++ b/python/.gitignore @@ -95,6 +95,9 @@ venv.bak/ .spyderproject .spyproject +# PyCharm +.idea/ + # Rope project settings .ropeproject diff --git a/python/cli/CHANGELOG.md b/python/CHANGELOG.md similarity index 100% rename from python/cli/CHANGELOG.md rename to python/CHANGELOG.md diff --git a/python/cli/Dockerfile b/python/Dockerfile similarity index 100% rename from python/cli/Dockerfile rename to python/Dockerfile diff --git a/python/cli/LICENSE.md b/python/LICENSE.md similarity index 100% rename from python/cli/LICENSE.md rename to python/LICENSE.md diff --git a/python/cli/MANIFEST.in b/python/MANIFEST.in similarity index 100% rename from python/cli/MANIFEST.in rename to python/MANIFEST.in diff --git a/python/cli/Makefile b/python/Makefile similarity index 100% rename from python/cli/Makefile rename to python/Makefile diff --git a/python/cli/README.md b/python/README.md similarity index 100% rename from python/cli/README.md rename to python/README.md diff --git a/python/cli/kontor/controllers/clibase.py b/python/cli/kontor/controllers/clibase.py deleted file mode 100644 index 8d69fb2..0000000 --- a/python/cli/kontor/controllers/clibase.py +++ /dev/null @@ -1,60 +0,0 @@ - -from cement import Controller, ex -from cement.utils.version import get_version_banner -from ..core.version import get_version - -VERSION_BANNER = """ -Kontor CLI Tool %s -%s -""" % (get_version(), get_version_banner()) - - -class CliBase(Controller): - class Meta: - label = 'base' - - # text displayed at the top of --help output - description = 'Kontor CLI Tool' - - # text displayed at the bottom of --help output - epilog = 'Usage: kontor command1 --foo bar' - - # controller level arguments. ex: 'kontor --version' - arguments = [ - ### add a version banner - ( [ '-v', '--version' ], - { 'action' : 'version', - 'version' : VERSION_BANNER } ), - ] - - - def _default(self): - """Default action if no sub-command is passed.""" - - self.app.args.print_help() - - - @ex( - help='example sub command1', - - # sub-command level arguments. ex: 'kontor command1 --foo bar' - arguments=[ - ### add a sample foo option under subcommand namespace - ( [ '-f', '--foo' ], - { 'help' : 'notorious foo option', - 'action' : 'store', - 'dest' : 'foo' } ), - ], - ) - def command1(self): - """Example sub-command.""" - - data = { - 'foo' : 'bar', - } - - ### do something with arguments - if self.app.pargs.foo is not None: - data['foo'] = self.app.pargs.foo - - self.app.render(data, 'command1.jinja2') diff --git a/python/cli/kontor/database/base.py b/python/cli/kontor/database/base.py deleted file mode 100644 index f97d724..0000000 --- a/python/cli/kontor/database/base.py +++ /dev/null @@ -1,20 +0,0 @@ -from sqlalchemy import Column, String, DateTime, Integer -from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker, declarative_base - - -# class Base(DeclarativeBase): -# pass - -class BaseModel: - - @classmethod - def model_lookup_by_table_name(cls, table_name): - registry_instance = getattr(cls, "registry") - for mapper_ in registry_instance.mappers: - model = mapper_.class_ - model_class_name = model.__tablename__ - if model_class_name == table_name: - return model - - -Base = declarative_base(cls=BaseModel) \ No newline at end of file diff --git a/python/cli/config/kontor.yml.example b/python/config/kontor.yml.example similarity index 100% rename from python/cli/config/kontor.yml.example rename to python/config/kontor.yml.example diff --git a/python/cli/docs/.gitkeep b/python/docs/.gitkeep similarity index 100% rename from python/cli/docs/.gitkeep rename to python/docs/.gitkeep diff --git a/python/cli/kontor/__init__.py b/python/kontor/__init__.py similarity index 100% rename from python/cli/kontor/__init__.py rename to python/kontor/__init__.py diff --git a/python/cli/kontor/controllers/__init__.py b/python/kontor/controllers/__init__.py similarity index 100% rename from python/cli/kontor/controllers/__init__.py rename to python/kontor/controllers/__init__.py diff --git a/python/kontor/controllers/clibase.py b/python/kontor/controllers/clibase.py new file mode 100644 index 0000000..568a546 --- /dev/null +++ b/python/kontor/controllers/clibase.py @@ -0,0 +1,42 @@ +from PySide6.QtWidgets import QApplication +from cement import Controller, ex +from cement.utils.version import get_version_banner +from ..core.version import get_version +from ..gui.main_window import MainWindow + +VERSION_BANNER = """ +Kontor CLI Tool %s +%s +""" % (get_version(), get_version_banner()) + + +class CliBase(Controller): + class Meta: + label = 'base' + + # text displayed at the top of --help output + description = 'Kontor CLI Tool' + + # text displayed at the bottom of --help output + epilog = 'Usage: kontor gui|database' + + # controller level arguments. ex: 'kontor --version' + arguments = [ + ### add a version banner + (['-v', '--version'], + {'action': 'version', + 'version': VERSION_BANNER}), + ] + + def _default(self): + """Default action if no sub-command is passed.""" + self.gui() + + @ex( + help='start GUI' + ) + def gui(self): + application = QApplication([]) + window = MainWindow(self.app.session, self.app.log) + window.show() + application.exec() diff --git a/python/kontor/controllers/database.py b/python/kontor/controllers/database.py new file mode 100644 index 0000000..246d4f8 --- /dev/null +++ b/python/kontor/controllers/database.py @@ -0,0 +1,32 @@ +from cement import Controller, ex + +from ..database import KontorDB + + +class Database(Controller): + + class Meta: + label = 'database' + stacked_type = 'nested' + stacked_on = 'base' + + @ex( + help='export database to given file', + arguments=[ + (['-f', '--file'], + {'help': 'file to store database content', + 'action': 'store', + 'dest': 'db_file'}) + ], + ) + def export(self): + data = { + 'db_file': 'data.json', + 'export_type': 'JSON', + } + if self.app.pargs.db_file is not None: + data['db_file'] = self.app.pargs.db_file + kontor_db = KontorDB(self.app.session, self.app.log) + table_list = kontor_db.get_table_names() + kontor_db.export_db(data['export_type'], data['db_file'], table_list) + self.app.render(data, 'command1.jinja2') diff --git a/python/cli/kontor/core/__init__.py b/python/kontor/core/__init__.py similarity index 100% rename from python/cli/kontor/core/__init__.py rename to python/kontor/core/__init__.py diff --git a/python/cli/kontor/core/exc.py b/python/kontor/core/exc.py similarity index 100% rename from python/cli/kontor/core/exc.py rename to python/kontor/core/exc.py diff --git a/python/cli/kontor/core/version.py b/python/kontor/core/version.py similarity index 100% rename from python/cli/kontor/core/version.py rename to python/kontor/core/version.py diff --git a/python/cli/kontor/database/__init__.py b/python/kontor/database/__init__.py similarity index 55% rename from python/cli/kontor/database/__init__.py rename to python/kontor/database/__init__.py index 95ce1d8..c7e6cb5 100644 --- a/python/cli/kontor/database/__init__.py +++ b/python/kontor/database/__init__.py @@ -2,37 +2,40 @@ import json from datetime import datetime from pathlib import Path -import mariadb -from sqlalchemy import create_engine, select, text, MetaData, join -from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker - from .base import Base -from .comic import Comic +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 class KontorDB: - def __init__(self, db_config): - self.db_conn = mariadb.connect( - host=db_config['mariadb']['host'], - port=db_config['mariadb']['port'], - user=db_config['mariadb']['user'], - password=db_config['mariadb']['password'], - database=db_config['mariadb']['database'] - ) - connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}' - .format( - db_config['mariadb']['user'], - db_config['mariadb']['password'], - db_config['mariadb']['host'], - db_config['mariadb']['port'], - db_config['mariadb']['database'])) - # engine = create_engine(connect_string, echo=True) - engine = create_engine(connect_string) - Base.metadata.create_all(bind=engine) - __session__ = sessionmaker(bind=engine) - self.session = __session__() + def __init__(self, db_session, log): + self.session = db_session + self.log = log + self.registry = {} + self.init_registry() + + def init_registry(self): + self.registry['card'] = Card + self.registry['card_set'] = CardSet + self.registry['sport'] = Sport + self.registry['team'] = Team + self.registry['field_position'] = FieldPosition + self.registry['rooster'] = Rooster + self.registry['player'] = Player + self.registry['vendor'] = Vendor + self.registry['artist'] = Artist + self.registry['publisher'] = Publisher + self.registry['comic'] = Comic + self.registry['issue'] = Issue + self.registry['story_arc'] = StoryArc + self.registry['trade_paperback'] = TradePaperback + self.registry['volume'] = Volume + self.registry['comic_work'] = ComicWork + self.registry['worktype'] = WorkType + self.registry['media_file'] = MediaFile def get_table_names(self) -> list: tables = self.session.query(MetaDataTable).all() @@ -69,7 +72,7 @@ class KontorDB: filter(MetaDataTable.table_name == table_name). filter(MetaDataColumn.show_filter == 1).all()): _filter_map[column.column_name] = {'label': column.filter_label, 'widget': None} - print(f"retrieved {len(_filter_map)} filters: {_filter_map}") + self.log.info(f"retrieved {len(_filter_map)} filters: {_filter_map}") return _filter_map def data(self, table, columns: dict, filters) -> list: @@ -96,67 +99,27 @@ class KontorDB: data.append(row) return data - def get_data(self, table_name: str, columns: dict, where_clause: str) -> list: - data = [] - cursor = self.db_conn.cursor() - cursor.execute(self.get_statement(table_name, columns, where_clause)) - rows = cursor.fetchall() - print(len(rows)) - for row in rows: - # print(f"KontorDB.get_data: {row}") - data.append(list(row)) - cursor.close() - # print(f"KontorDB.getData: return {len(data)}") - if table_name == 'comic' and len(where_clause) == 0: - data.clear() - comics = self.session.query(Comic).all() - for item in comics: - # print(item) - row = [] - for order in columns.keys(): - column_name = columns[order]['column'] - if str(column_name).endswith("_id"): - ref_table = column_name[:-3] - # print(f"{ref_table=}") - ref = getattr(item, ref_table) - value = getattr(ref, "name") - # print(f"{value=}") - row.append(value) - else: - row.append(getattr(item, column_name)) - # print(repr(row)) - data.append(row) - return data - - def get_statement(self, table: str, header: dict, where_clause): - columns = "" - for index, column in header.items(): - if index > 0: - columns += ", " - columns += column['column'] - if len(columns) == 0: - columns = "*" - statement = f"SELECT {columns} FROM {table} {where_clause}" - print(f"{statement=}") - return statement - def export_db(self, export_type: str, export_file_name: str, export_table_list: list): - print(f"export DB to {export_file_name} as {export_type}") + self.log.info(f"export DB to {export_file_name} as {export_type}") db = {} for table in export_table_list: columns = self.get_column_meta_data(table, view_only=False) - model = Base.model_lookup_by_table_name(table) + if table in self.registry: + model = self.registry[table] + else: + print(f"table {table} is not registered") + continue rows = self.session.query(model).all() entries = [] - print(f"found {len(rows)} entries") - print(f"found {len(columns)} columns") + self.log.debug(f"found {len(rows)} entries") + self.log.debug(f"found {len(columns)} columns") for row in rows: - print(row) + # print(row) entry = {} for order in columns: - print(columns[order]) + # print(columns[order]) column_name = columns[order]['column'] - print(f"get value {column_name} from {row} of table {table}") + # print(f"get value {column_name} from {row} of table {table}") try: value = getattr(row, column_name) if isinstance(value, datetime): @@ -164,7 +127,7 @@ class KontorDB: else: entry[column_name] = value except AttributeError as error: - print("could not get value") + self.log.debug("could not get value") entries.append(entry) db[table] = entries export_file = Path(export_file_name) @@ -178,6 +141,6 @@ class KontorDB: case "SQLite": export_file = Path(export_file_name) case _: - print("unknown export type") + self.log.debug("unknown export type") if export_file.exists(): - print(f"{export_file} exists") + self.log.debug(f"{export_file} exists") diff --git a/python/kontor/database/base.py b/python/kontor/database/base.py new file mode 100644 index 0000000..fa2b68a --- /dev/null +++ b/python/kontor/database/base.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import DeclarativeBase + + +class Base(DeclarativeBase): + pass diff --git a/python/cli/kontor/database/comic.py b/python/kontor/database/comic.py similarity index 100% rename from python/cli/kontor/database/comic.py rename to python/kontor/database/comic.py diff --git a/python/cli/kontor/database/media.py b/python/kontor/database/media.py similarity index 100% rename from python/cli/kontor/database/media.py rename to python/kontor/database/media.py diff --git a/python/cli/kontor/database/metadata.py b/python/kontor/database/metadata.py similarity index 99% rename from python/cli/kontor/database/metadata.py rename to python/kontor/database/metadata.py index 2975e10..d7dd4c0 100644 --- a/python/cli/kontor/database/metadata.py +++ b/python/kontor/database/metadata.py @@ -20,6 +20,7 @@ class MetaDataTable(Base): def __str__(self): return f'{self.table_name}({self.id})' + class MetaDataColumn(Base): __tablename__ = 'meta_data_column' id = Column(String, primary_key=True) diff --git a/python/qt/database/tysc.py b/python/kontor/database/tysc.py similarity index 100% rename from python/qt/database/tysc.py rename to python/kontor/database/tysc.py diff --git a/python/cli/kontor/ext/__init__.py b/python/kontor/ext/__init__.py similarity index 100% rename from python/cli/kontor/ext/__init__.py rename to python/kontor/ext/__init__.py diff --git a/python/cli/kontor/plugins/__init__.py b/python/kontor/gui/__init__.py similarity index 100% rename from python/cli/kontor/plugins/__init__.py rename to python/kontor/gui/__init__.py diff --git a/python/qt/gui/data_view.py b/python/kontor/gui/data_view.py similarity index 100% rename from python/qt/gui/data_view.py rename to python/kontor/gui/data_view.py diff --git a/python/qt/gui/data_view_model.py b/python/kontor/gui/data_view_model.py similarity index 100% rename from python/qt/gui/data_view_model.py rename to python/kontor/gui/data_view_model.py diff --git a/python/qt/gui/dialogs.py b/python/kontor/gui/dialogs.py similarity index 100% rename from python/qt/gui/dialogs.py rename to python/kontor/gui/dialogs.py diff --git a/python/kontor/gui/main_window.py b/python/kontor/gui/main_window.py new file mode 100644 index 0000000..24a84a6 --- /dev/null +++ b/python/kontor/gui/main_window.py @@ -0,0 +1,139 @@ +from PySide6.QtGui import QAction, QIcon +from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView +from PySide6.QtWidgets import QLabel, QMainWindow + +from ..database import KontorDB +from ..database.media import MediaFile +from ..database.comic import Comic +from .dialogs import ExportKontorDialog, ImportKontorDialog +from .model_config import KontorModelConfig +from .table_model import KontorTableModel + + +class MainWindow(QMainWindow): + + def __init__(self, session, log): + super().__init__() + + self.tick = QIcon('kontor/res/tick.png') + self.cross = QIcon('kontor/res/cross.png') + self.import_icon = QIcon("kontor/res/application-import.png") + self.export_icon = QIcon("kontor/res/application-export.png") + self.circle_icon = QIcon("kontor/res/arrow-circle-double.png") + + self.setWindowTitle("Kontor") + self.setMinimumSize(800, 500) + self._create_actions() + self._create_menubar() + self._create_toolbars() + self._create_statusbar() + + self.data = [] + self.filter = {} + self.kontor_db = KontorDB(session, log) + self.log = log + self.central_widget = QWidget() + parent_layout = QVBoxLayout() + self.central_widget.setLayout(parent_layout) + self.tabs = QTabWidget() + self.tabs.addTab(self.generate_data_tab("comic", Comic), "Comics") + self.tabs.addTab(self.generate_data_tab("media_file", MediaFile), "MediaFile") + self.tabs.currentChanged.connect(self._tab_changed) + #label.setAlignment(Qt.AlignmentFlag.AlignCenter) + parent_layout.addWidget(self.tabs) + + self.setCentralWidget(self.central_widget) + + def _create_actions(self): + self.newAction = QAction("&New", self) + self.aboutAction = QAction("&Über...", self) + self.aboutAction.triggered.connect(self.about) + self.importAction = QAction(self.import_icon, "&Import", self) + self.importAction.triggered.connect(self.import_from_file) + self.exportAction = QAction(self.export_icon, "&Export", self) + self.exportAction.triggered.connect(self.export_to_file) + self.refreshAction = QAction(self.circle_icon, "&Refresh", self) + self.refreshAction.triggered.connect(self.refresh) + self.updateTitleAction = QAction("&Update Titles", self) + self.downloadAction = QAction("&Download Videos", self) + self.exitAction = QAction("&Beenden", self) + self.exitAction.setShortcut("Alt+F4") + self.exitAction.triggered.connect(self.close) + + def _create_menubar(self): + menu_bar = self.menuBar() + # File menu + file_menu = QMenu("&Datei") + menu_bar.addMenu(file_menu) + file_menu.addAction(self.exitAction) + # Kontor menu + kontor_menu = QMenu("&Kontor") + menu_bar.addMenu(kontor_menu) + kontor_menu.addAction(self.importAction) + kontor_menu.addAction(self.exportAction) + comic_menu = QMenu("&Comic") + tysc_menu = QMenu("&TradeYourSportCards") + media_file_menu = QMenu("&MediaFile") + media_file_menu.addAction(self.updateTitleAction) + media_file_menu.addAction(self.downloadAction) + kontor_menu.addMenu(comic_menu) + kontor_menu.addMenu(tysc_menu) + kontor_menu.addMenu(media_file_menu) + # Help menu + help_menu = QMenu("&Hilfe") + menu_bar.addMenu(help_menu) + help_menu.addAction(self.aboutAction) + + def _create_toolbars(self): + # Kontor toolbar + kontor_tool_bar = self.addToolBar("Kontor") + kontor_tool_bar.addAction(self.importAction) + kontor_tool_bar.addAction(self.exportAction) + kontor_tool_bar.addAction(self.refreshAction) + + def _create_statusbar(self): + self.statusBar = self.statusBar() + self.statusBar.showMessage("Kontor ready", 6000) + self.status_label = QLabel("") + self.statusBar.addPermanentWidget(self.status_label) + + def about(self): + QMessageBox.about(self.central_widget, "Über Kontor", f"Python: 3.11\nKontor: 0.1.0") + + def import_from_file(self): + import_dlg = ImportKontorDialog(self) + if import_dlg.exec(): + print(f"import DB from file {import_dlg.file_name}") + else: + print("no nothing for import") + pass + + def export_to_file(self): + export_dlg = ExportKontorDialog(self, self.kontor_db) + if export_dlg.exec(): + self.log.info(export_dlg.get_tables_to_export()) + self.log.info(f"export DB to {export_dlg.file_name}") + self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000) + self.kontor_db.export_db(export_dlg.current_export_type, export_dlg.file_name, export_dlg.get_tables_to_export()) + else: + self.statusBar.showMessage("Export cancelled", 3000) + + def refresh(self): + self.data[self.tabs.currentIndex()].refresh() + + def _tab_changed(self, tab_index): + self.data[tab_index].refresh() + + def generate_data_tab(self, table_name, table): + data_tab = QWidget() + table_config = KontorModelConfig(self.kontor_db, self, table_name, table) + model = KontorTableModel(table_config) + layout = QVBoxLayout() + self.data.append(model) + data_tab.setLayout(layout) + table_view = QTableView() + table_view.setModel(model) + layout.addLayout(table_config.get_filter_layout()) + layout.addWidget(table_view) + model.refresh() + return data_tab diff --git a/python/kontor/gui/model_config.py b/python/kontor/gui/model_config.py new file mode 100644 index 0000000..ce61107 --- /dev/null +++ b/python/kontor/gui/model_config.py @@ -0,0 +1,50 @@ +import mariadb +from PySide6.QtWidgets import QHBoxLayout, QCheckBox + +from ..database import KontorDB + + +class KontorModelConfig: + + def __init__(self, kontor_db: KontorDB, main_window, table_name: str, table): + self.header = {} + self.filter = {} + self.main_window = main_window + self._table_name = table_name + self._table = table + self.kontor_db = kontor_db + self.get_table_config() + + def get_table_config(self): + self.header = self.kontor_db.get_column_meta_data(self._table_name) + self.filter = self.kontor_db.get_filters(self._table_name) + + def filters(self) -> dict: + _filters = {} + # print(self.filter["download"].isChecked()) + for column, filter_info in self.filter.items(): + # print(column, filter_info) + if filter_info['widget'].isChecked(): + _filters[column] = True + # print(f"{filter_rule=}") + return _filters + + def get_data(self) -> list: + # data = self.kontor_db.get_data(self._table_name, self.header, self.get_filter()) + # data.clear() + data = self.kontor_db.data(self._table, self.header, self.filters()) + # print(f"KontorModelConfig.get_data: {len(data)}") + # comics = self.kontor_db.session.query(Comic).all() + # print(f'{len(comics)} Comics loaded') + return data + + def get_filter_layout(self) -> QHBoxLayout: + filter_layout = QHBoxLayout() + for column, filter_info in self.filter.items(): + filter_checkbox = QCheckBox() + filter_checkbox.setText(filter_info['label']) + filter_checkbox.checkStateChanged.connect(self.main_window.refresh) + self.filter[column]['widget'] = filter_checkbox + filter_layout.addWidget(filter_checkbox) + filter_layout.addStretch() + return filter_layout diff --git a/python/kontor/gui/table_model.py b/python/kontor/gui/table_model.py new file mode 100644 index 0000000..cd08ce1 --- /dev/null +++ b/python/kontor/gui/table_model.py @@ -0,0 +1,108 @@ +from datetime import datetime + +from PySide6.QtCore import QAbstractTableModel, QModelIndex +from PySide6.QtGui import Qt + +from .model_config import KontorModelConfig + + +class KontorTableModel(QAbstractTableModel): + + def __init__(self, model_config: KontorModelConfig): + super().__init__() + self._main_window = model_config.main_window + self._config = model_config + self._data = [] + + def refresh(self): + data = self._config.get_data() + count = 0 + # print(data) + if data is not None: + self.beginResetModel() + self._data.clear() + self._data = data + self.endResetModel() + count = len(data) + # print(data) + # print(self._data) + self.layoutChanged.emit() + self._main_window.statusBar.showMessage(f"{count} Einträge geladen", 3000) + + def rowCount(self, parent=QModelIndex()): + # The length of the outer list. + if self._data is None: + return 0 + return len(self._data) + + def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): + if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: + return self._config.header[col]['label'] + if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: + return str(col+1) + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + if self._data is None: + return None + value = self._data[index.row()][index.column()] + # print('{}:: {}:: {}: {}'.format(index, role, value, type(value))) + if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole: + if isinstance(value, datetime): + return value.strftime("%Y-%m-%d %M:%M:%S") + if isinstance(value, str): + return value + if isinstance(value, bytes): + if value == b'\x01': + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, int): + # print('{}:: {}: {}'.format(index, value, type(value))) + if value == 1: + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, bool): + if value: + return self._main_window.tick + else: + return self._main_window.cross + return str(value) + if role == Qt.ItemDataRole.DecorationRole: + if isinstance(value, bytes): + if value == b'\x01': + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, int): + if value == 1: + return self._main_window.tick + else: + return self._main_window.cross + if isinstance(value, bool): + if value: + return self._main_window.tick + else: + return self._main_window.cross + + def columnCount(self, index=QModelIndex()): + # The following takes the first sub-list, and returns + # the length (only works if all rows are an equal length) + # print(f"Header count: {len(self._config.get_header())}") + return len(self._config.header) + + def setData(self, index, value, role: int) -> bool: + print(index, role) + if role == Qt.ItemDataRole.EditRole: + self._data[index.row()][index.column()] = value + print(self._data[index.row()][index.column()]) + self.dataChanged.emit(index, index) + return True + if role == Qt.ItemDataRole.CheckStateRole: + print("role == Qt.ItemDataRole.CheckStateRole") + checked = value == Qt.CheckState.Checked + self._data[index.row()][index.column()] = checked + return False + + def flags(self, index): + return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate diff --git a/python/cli/kontor/main.py b/python/kontor/main.py similarity index 91% rename from python/cli/kontor/main.py rename to python/kontor/main.py index 4c71aab..aa71e06 100644 --- a/python/cli/kontor/main.py +++ b/python/kontor/main.py @@ -1,4 +1,3 @@ - from cement import App, TestApp, init_defaults from cement.core.exc import CaughtSignal from sqlalchemy import create_engine @@ -7,6 +6,7 @@ from sqlalchemy.orm import sessionmaker from .core.exc import KontorError from .database.base import Base from .controllers.clibase import CliBase +from .controllers.database import Database # configuration defaults CONFIG = init_defaults('kontor', 'mariadb') @@ -17,6 +17,7 @@ CONFIG['mariadb']['host'] = '127.0.0.1' CONFIG['mariadb']['port'] = '3306' CONFIG['mariadb']['database'] = 'kontor' + def extend_sqlalchemy(app): app.log.info('extending kontor application with sqlalchemy') connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format( @@ -33,6 +34,11 @@ def extend_sqlalchemy(app): app.extend('session', __session__()) +def close_session(app): + app.log.info('close session') + app.session.close() + + class Kontor(App): """Kontor primary application.""" @@ -66,14 +72,16 @@ class Kontor(App): hooks = [ ('post_setup', extend_sqlalchemy), + ('pre_close', close_session), ] # register handlers handlers = [ - CliBase + CliBase, + Database, ] -class KontorTest(TestApp,Kontor): +class KontorTest(TestApp, Kontor): """A sub-class of Kontor that is better suited for testing.""" class Meta: diff --git a/python/cli/kontor/templates/__init__.py b/python/kontor/plugins/__init__.py similarity index 100% rename from python/cli/kontor/templates/__init__.py rename to python/kontor/plugins/__init__.py diff --git a/python/qt/res/application-export.png b/python/kontor/res/application-export.png similarity index 100% rename from python/qt/res/application-export.png rename to python/kontor/res/application-export.png diff --git a/python/qt/res/application-import.png b/python/kontor/res/application-import.png similarity index 100% rename from python/qt/res/application-import.png rename to python/kontor/res/application-import.png diff --git a/python/qt/res/arrow-circle-double.png b/python/kontor/res/arrow-circle-double.png similarity index 100% rename from python/qt/res/arrow-circle-double.png rename to python/kontor/res/arrow-circle-double.png diff --git a/python/qt/res/cross.png b/python/kontor/res/cross.png similarity index 100% rename from python/qt/res/cross.png rename to python/kontor/res/cross.png diff --git a/python/qt/res/tick.png b/python/kontor/res/tick.png similarity index 100% rename from python/qt/res/tick.png rename to python/kontor/res/tick.png diff --git a/python/qt/gui/__init__.py b/python/kontor/templates/__init__.py similarity index 100% rename from python/qt/gui/__init__.py rename to python/kontor/templates/__init__.py diff --git a/python/cli/kontor/templates/command1.jinja2 b/python/kontor/templates/command1.jinja2 similarity index 100% rename from python/cli/kontor/templates/command1.jinja2 rename to python/kontor/templates/command1.jinja2 diff --git a/python/main.py b/python/main.py deleted file mode 100644 index 76d0e8c..0000000 --- a/python/main.py +++ /dev/null @@ -1,16 +0,0 @@ -# This is a sample Python script. - -# Press Umschalt+F10 to execute it or replace it with your code. -# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings. - - -def print_hi(name): - # Use a breakpoint in the code line below to debug your script. - print(f'Hi, {name}') # Press Strg+F8 to toggle the breakpoint. - - -# Press the green button in the gutter to run the script. -if __name__ == '__main__': - print_hi('PyCharm') - -# See PyCharm help at https://www.jetbrains.com/help/pycharm/ diff --git a/python/cli/requirements-dev.txt b/python/requirements-dev.txt similarity index 100% rename from python/cli/requirements-dev.txt rename to python/requirements-dev.txt diff --git a/python/cli/requirements.txt b/python/requirements.txt similarity index 90% rename from python/cli/requirements.txt rename to python/requirements.txt index bee2df1..06a15f9 100644 --- a/python/cli/requirements.txt +++ b/python/requirements.txt @@ -4,3 +4,4 @@ cement[yaml] cement[colorlog] mariadb sqlalchemy +PySide6 diff --git a/python/cli/setup.cfg b/python/setup.cfg similarity index 100% rename from python/cli/setup.cfg rename to python/setup.cfg diff --git a/python/cli/setup.py b/python/setup.py similarity index 100% rename from python/cli/setup.py rename to python/setup.py diff --git a/python/cli/tests/conftest.py b/python/tests/conftest.py similarity index 100% rename from python/cli/tests/conftest.py rename to python/tests/conftest.py diff --git a/python/cli/tests/test_kontor.py b/python/tests/test_kontor.py similarity index 100% rename from python/cli/tests/test_kontor.py rename to python/tests/test_kontor.py diff --git a/python/qt/.gitignore b/qt/.gitignore similarity index 100% rename from python/qt/.gitignore rename to qt/.gitignore diff --git a/python/qt/database/__init__.py b/qt/database/__init__.py similarity index 100% rename from python/qt/database/__init__.py rename to qt/database/__init__.py diff --git a/python/qt/database/base.py b/qt/database/base.py similarity index 100% rename from python/qt/database/base.py rename to qt/database/base.py diff --git a/python/qt/database/comic.py b/qt/database/comic.py similarity index 100% rename from python/qt/database/comic.py rename to qt/database/comic.py diff --git a/python/qt/database/media.py b/qt/database/media.py similarity index 100% rename from python/qt/database/media.py rename to qt/database/media.py diff --git a/python/qt/database/metadata.py b/qt/database/metadata.py similarity index 100% rename from python/qt/database/metadata.py rename to qt/database/metadata.py diff --git a/python/cli/kontor/database/tysc.py b/qt/database/tysc.py similarity index 94% rename from python/cli/kontor/database/tysc.py rename to qt/database/tysc.py index 54cacea..733d7ed 100644 --- a/python/cli/kontor/database/tysc.py +++ b/qt/database/tysc.py @@ -28,7 +28,7 @@ class Team(Base): 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="positions") + sport = relationship("Sport", back_populates="teams") roosters = relationship("Rooster") @@ -81,7 +81,7 @@ class Rooster(Base): 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("roosters") + position = relationship("FieldPosition", back_populates="roosters") cards = relationship("Card") class Vendor(Base): @@ -124,8 +124,8 @@ class Card(Base): 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("cards") + card_set = relationship("CardSet", back_populates="cards") rooster_id = Column(String, ForeignKey("rooster.id"), nullable=False) - rooster = relationship("cards") + rooster = relationship("Rooster", back_populates="cards") vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False) vendor = relationship("Vendor", back_populates="cards") diff --git a/qt/gui/__init__.py b/qt/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qt/gui/data_view.py b/qt/gui/data_view.py new file mode 100644 index 0000000..91c66d2 --- /dev/null +++ b/qt/gui/data_view.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod + + +class DataViewMeta(ABC): + @abstractmethod + def get_header(self): + pass + + +class ComicView(DataViewMeta): + def get_header(self): + pass diff --git a/qt/gui/data_view_model.py b/qt/gui/data_view_model.py new file mode 100644 index 0000000..4ffdc8c --- /dev/null +++ b/qt/gui/data_view_model.py @@ -0,0 +1,32 @@ +from typing import List + +from PySide6.QtCore import QModelIndex, QAbstractTableModel +from PySide6.QtGui import Qt + +from gui.data_view import DataViewMeta + + +class DataViewModel(QAbstractTableModel): + def __init__(self): + super().__init__() + self.main_window = None + self._config = None + self._data = List[DataViewMeta] + + def rowCount(self, parent=QModelIndex()): + return len(self._data) + + def columnCount(self, parent=QModelIndex()): + return 0 + + def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): + return None + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + return None + + def setData(self, index, value, role=Qt.ItemDataRole.EditRole): + return False + + def flags(self, index): + return None diff --git a/qt/gui/dialogs.py b/qt/gui/dialogs.py new file mode 100644 index 0000000..21dbe92 --- /dev/null +++ b/qt/gui/dialogs.py @@ -0,0 +1,106 @@ +from pathlib import Path + +from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QFileDialog, \ + QCheckBox, QComboBox + + +class ExportKontorDialog(QDialog): + def __init__(self, parent=None, kontor_db=None): + super().__init__(parent) + + self.parent = parent + self.kontor_db = kontor_db + self.file_name = "data.json" + self.tables = [] + self._table_options = {} + + self.export_options = {"JSON": {"ext": ".json"}, "YAML": {"ext": ".yaml"}, "SQLite": {"ext": ".db"}} + self.current_export_type = "JSON" + + buttons = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + self.buttonBox = QDialogButtonBox(buttons) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + layout = QVBoxLayout() + + self.label = QLabel() + self.label.setText("Export DB to data.json") + + self.combo_box = QComboBox() + self.combo_box.addItems(["JSON", "YAML", "SQLite"]) + self.combo_box.currentTextChanged.connect(self.change_export_type) + file_layout = QHBoxLayout() + file_layout.addWidget(self.label) + file_layout.addWidget(self.combo_box) + file_button = QPushButton("Select file") + file_button.clicked.connect(self.select_file) + file_layout.addWidget(file_button) + layout.addLayout(file_layout) + + for table_name in self.kontor_db.get_table_names(): + check_box = QCheckBox(table_name) + check_box.setChecked(True) + self.tables.append(table_name) + self._table_options[table_name] = check_box + check_box.stateChanged.connect(self.change_selection) + layout.addWidget(check_box) + layout.addWidget(self.buttonBox) + self.setLayout(layout) + + def change_selection(self): + self.tables.clear() + for (name, box) in self._table_options.items(): + if box.isChecked(): + self.tables.append(name) + + def change_export_type(self, text): + self.current_export_type = text + self.label.setText(f'Export DB to data.{self.export_options[text]["ext"]}') + + def select_file(self): + file_dialog = QFileDialog() + file_dialog.setFileMode(QFileDialog.FileMode.AnyFile) + file_dialog.setDefaultSuffix(self.export_options[self.current_export_type]["ext"]) + file_dialog.setNameFilter(f'*{self.export_options[self.current_export_type]["ext"]}') + if file_dialog.exec(): + self.file_name = file_dialog.selectedFiles()[0] + export_file = Path(self.file_name) + self.file_name = export_file.with_suffix(self.export_options[self.current_export_type]["ext"]) + self.label.setText(f"Export DB to {self.file_name}") + + def get_tables_to_export(self) -> list: + return self.tables + + +class ImportKontorDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + + self.file_name = None + + QBtn = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + self.buttonBox = QDialogButtonBox(QBtn) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + self.label = QLabel() + self.label.setText("Import DB from data.json") + layout = QVBoxLayout() + file_layout = QHBoxLayout() + file_layout.addWidget(self.label) + file_button = QPushButton("Select file") + file_button.clicked.connect(self.select_file) + file_layout.addWidget(file_button) + layout.addLayout(file_layout) + layout.addWidget(self.buttonBox) + self.setLayout(layout) + + def select_file(self): + file_dialog = QFileDialog() + file_dialog.setFileMode(QFileDialog.FileMode.ExistingFile) + if file_dialog.exec(): + self.file_name = file_dialog.selectedFiles()[0] + self.label.setText(f"Import DB from {self.file_name}") diff --git a/python/qt/gui/main_window.py b/qt/gui/main_window.py similarity index 100% rename from python/qt/gui/main_window.py rename to qt/gui/main_window.py diff --git a/python/qt/gui/model_config.py b/qt/gui/model_config.py similarity index 100% rename from python/qt/gui/model_config.py rename to qt/gui/model_config.py diff --git a/python/qt/gui/table_model.py b/qt/gui/table_model.py similarity index 100% rename from python/qt/gui/table_model.py rename to qt/gui/table_model.py diff --git a/python/qt/kontor.py b/qt/kontor.py similarity index 100% rename from python/qt/kontor.py rename to qt/kontor.py diff --git a/python/qt/pysidedeploy.spec b/qt/pysidedeploy.spec old mode 100755 new mode 100644 similarity index 100% rename from python/qt/pysidedeploy.spec rename to qt/pysidedeploy.spec diff --git a/qt/res/application-export.png b/qt/res/application-export.png new file mode 100644 index 0000000000000000000000000000000000000000..555887a28d64bc812c4dfa98a6ff1da1927b7792 GIT binary patch literal 513 zcmV+c0{;DpP)A0|fCeqz@KJVX8nnt2D9k2UrOajFq_SwR4OUbbUJ;HgeDQF66oE777mBiWdd2F(NKOO zk$Ax17hTui{#6jPXfz7dY87I!*bgRr4TVA=afmd=7~nkYBq$b(su#^>Q?2y;%rJ~= zA;j4sgM^B|5YOb(M4cc`5q!^h_wPg5is0Dq{42l!6{r`}{QE*r00000NkvXXu0mjf Djqud07&@TR)18#v5Hqj8@|Bnpn>z;V`CueWd< z7oI05i2^A#D9Qz9v#hs`;>*D7{eB7>0ppH2SLstJO-8BwR~M`A7kF ztyTlI66tgrtyT-MSPa~yU>3AuKR54&OXJih@;oco-=1sDLQa}`@LC7Iy> O0000dz zNLw#yfl;S$OS+NxP!T0`q27a_KcR=-dJ5{Xm+WEf!J>k|BqE5#C}ImGwQ$$<(wdh& z>AIJ5zVFx9lo8J{@aWY%oIP6CAZKYHol)uRf|554m_YTnpAlGl zL9{5M4)qVt#u>|mLQVrh46q|<)MP|~t8EE-8crSv>~io{4+abW2A~lV0z>OM)mbBD zmWtqV;+?9YGlhpQQ@M9zi{*KRG_T5PQ_nvC9tMo_mT6XEIU5;J+S?d(HD_0f!1ES7 z7@Uj?4!kD>&?r8e&c1jan^9Cs!}3z0d7Izafj)-}rbYZR}EdihS6`M#Fcy= zaw{u#mV;BNWe3{Sqiq;{_M>cOP*8MhP1CLQf3t|C2tpM55ycYh7RL%}Yf+Bp#e!~C z2(WG^ZnlxC>igv!sW2GM8SslVJ(oFwkp~5*6E^0rT$Q zya}^2lB>ITrX0zGEa!lc90$HC4~#^g#%ZB2j3vzHhi;TO$n{?Wwx(^0>^QA_x~H^z zTf=2SxY0rCf|fl@rb7TXYr%pE(0=u@G literal 0 HcmV?d00001 diff --git a/qt/res/cross.png b/qt/res/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..6b9fa6dd36ee8165272a13dd263f573507c78ca6 GIT binary patch literal 544 zcmV+*0^j|KP)L-ku(! z6_?D-?!0+#=VtbVZQGdTnZt~apI_HPK#=zVadHM((E`e*C+RoFb)Qo8-U{Lb7{`Tz zw4B8FZ|o?Wox+p=1wp47C;7ar*Xu~;a?<=sjPv>+otDjJ6FaGt!YnNyxQQhp)G1V! zk;r6ZtyV)c8pTbiRAnHRNXTxti%=+p$4aG2*+mMM&xrdiU^`VPk^N*+HX98^7!HT% zwA%;A{T)GxeQ|RcxyzV%! z$AbYD+)i1R64zB?q}NmTz}5|04~J#YG_goA*Eq(QJvp7pG14hUj1pZ^t>3S*xqHUO ze~r;}zTY_XkZ*}d@gm!;N90h8nBQenBUZ_ukZKNicn*hc_Pmc!Jn|2=s<~}>!Sb>Qm3!QP46b_JFw5YU70Tu$X}=T}ez@@t%j iG9vD)nDux55?}x$+|UyQVK_bj0000tYd4K$mX5uyr2F@fdffp{&DSHtl4|Bn9=ukpCx`#%PTUr-D(@b7;C zhJXJjrnw{;1KBM=6&|E`fsNrGL!Y6%zUh}QUl`(@V)PmQFtotEKmafTHP0uP}7&%m4pcKQ#X)BiAJ2y+PrtBI>9eEIt2-_c7)?*Lsf z5vX5*Af@m-wBJRV%#Fiz=BdPr0!2^a2d-yv7gSU);Z6{`~Oy?E5qSnHUln4*Y%!){F!Y1~5WXoC44g zb7l_)X~q^V6MmWR7e3wj|L|Wb!|^}Y86N$^h+kv_Kw-fP!~#If$6(B4$)LlK#&G6; zC&ShMSAb$5tA9Xg5dOsg@-z^@3;;QS9f&!gG&3|;{Da~@Q2ZB({s%XJ5&#fj0J|In U+D>(nEdT%j07*qoM6N<$g0@&8X#fBK literal 0 HcmV?d00001