From ada723dc48e7d967f8d9ef95dd2ab8f1995101e7 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Sun, 19 Jan 2025 23:36:52 +0100 Subject: [PATCH] separate cli and gui application in own python packages. provide database schema as python package. --- python/kontor-cli.backup/.gitignore | 105 +++++++++ python/{ => kontor-cli.backup}/CHANGELOG.md | 2 +- python/{ => kontor-cli.backup}/Dockerfile | 6 +- python/{ => kontor-cli.backup}/LICENSE.md | 0 python/{ => kontor-cli.backup}/MANIFEST.in | 0 python/{ => kontor-cli.backup}/Makefile | 0 python/{ => kontor-cli.backup}/README.md | 4 +- .../config/kontor.yml.example | 2 +- python/{ => kontor-cli.backup}/docs/.gitkeep | 0 .../kontor/__init__.py | 0 .../kontor/controllers/__init__.py | 0 .../kontor/controllers/base.py} | 18 +- .../kontor/controllers/database.py | 3 +- .../kontor/controllers/media.py | 0 .../kontor/core/__init__.py | 0 python/kontor-cli.backup/kontor/core/exc.py | 4 + .../kontor-cli.backup/kontor/core/version.py | 7 + .../kontor/ext/__init__.py | 0 python/{ => kontor-cli.backup}/kontor/main.py | 27 +-- .../kontor/plugins}/__init__.py | 0 .../kontor/templates}/__init__.py | 0 .../kontor/templates/command1.jinja2 | 0 .../kontor/templates/download.jinja2 | 0 .../kontor/templates/import.jinja2 | 0 .../kontor/templates/update.jinja2 | 0 .../requirements-dev.txt | 0 python/kontor-cli.backup/requirements.txt | 6 + python/{ => kontor-cli.backup}/setup.cfg | 0 python/{ => kontor-cli.backup}/setup.py | 6 +- python/kontor-cli.backup/tests/conftest.py | 16 ++ python/kontor-cli.backup/tests/test_kontor.py | 36 +++ python/kontor-cli/.gitignore | 105 +++++++++ python/kontor-cli/CHANGELOG.md | 5 + python/kontor-cli/Dockerfile | 10 + python/kontor-cli/LICENSE.md | 1 + python/kontor-cli/MANIFEST.in | 5 + python/kontor-cli/Makefile | 31 +++ python/kontor-cli/README.md | 69 ++++++ python/kontor-cli/config/kontor.yml.example | 46 ++++ .../__init__.py => kontor-cli/docs/.gitkeep} | 0 .../kontor-cli/kontor}/__init__.py | 0 .../kontor-cli/kontor/controllers/__init__.py | 0 python/kontor-cli/kontor/controllers/base.py | 60 +++++ .../kontor-cli/kontor/controllers/database.py | 51 +++++ python/kontor-cli/kontor/controllers/media.py | 81 +++++++ python/kontor-cli/kontor/core/__init__.py | 0 python/{ => kontor-cli}/kontor/core/exc.py | 0 .../{ => kontor-cli}/kontor/core/version.py | 0 python/kontor-cli/kontor/ext/__init__.py | 0 python/kontor-cli/kontor/main.py | 118 ++++++++++ python/kontor-cli/kontor/plugins/__init__.py | 0 .../kontor-cli/kontor/templates/__init__.py | 0 .../kontor/templates/command1.jinja2 | 4 + python/kontor-cli/requirements-dev.txt | 8 + python/{ => kontor-cli}/requirements.txt | 2 - python/kontor-cli/setup.cfg | 0 python/kontor-cli/setup.py | 28 +++ python/kontor-cli/tests/conftest.py | 16 ++ python/kontor-cli/tests/test_kontor.py | 36 +++ python/kontor-gui/.gitignore | 7 + python/kontor-gui/gui/__init__.py | 0 .../{kontor => kontor-gui}/gui/data_view.py | 0 .../gui/data_view_model.py | 0 python/{kontor => kontor-gui}/gui/dialogs.py | 0 .../{kontor => kontor-gui}/gui/main_window.py | 30 ++- .../gui/model_config.py | 9 +- python/{kontor => kontor-gui}/gui/progress.py | 0 .../{kontor => kontor-gui}/gui/table_model.py | 0 python/kontor-gui/kontor.py | 43 ++++ python/kontor-gui/pyvenv.cfg | 5 + python/kontor-gui/requirements.txt | 6 + .../res/application-export.png | Bin .../res/application-import.png | Bin .../res/arrow-circle-double.png | Bin python/{kontor => kontor-gui}/res/cross.png | Bin python/{kontor => kontor-gui}/res/tick.png | Bin python/kontor-schema/README.md | 4 + .../site/python3.11/greenlet/greenlet.h | 164 ++++++++++++++ .../kontor_schema}/__init__.py | 31 ++- .../kontor_schema}/base.py | 0 .../kontor_schema}/bookshelf.py | 0 .../kontor_schema}/comic.py | 0 .../kontor_schema}/media.py | 0 .../kontor_schema}/metadata.py | 0 .../kontor_schema}/tysc.py | 0 python/kontor-schema/pyvenv.cfg | 5 + python/kontor-schema/requirements.txt | 2 + python/kontor-schema/setup.py | 23 ++ python/kontor-video/README.md | 3 + python/kontor-video/kontor_video/__init__.py | 21 ++ python/kontor-video/pyvenv.cfg | 5 + python/kontor-video/requirements.txt | 2 + python/kontor-video/setup.py | 23 ++ qt/.gitignore | 2 - qt/database/__init__.py | 212 ------------------ qt/database/base.py | 18 -- qt/database/comic.py | 130 ----------- qt/database/media.py | 25 --- qt/database/metadata.py | 49 ---- qt/database/tysc.py | 131 ----------- qt/gui/data_view.py | 12 - qt/gui/data_view_model.py | 32 --- qt/gui/dialogs.py | 106 --------- qt/gui/main_window.py | 137 ----------- qt/gui/model_config.py | 65 ------ qt/gui/table_model.py | 108 --------- qt/kontor.py | 21 -- qt/pysidedeploy.spec | 98 -------- qt/res/application-export.png | Bin 513 -> 0 bytes qt/res/application-import.png | Bin 524 -> 0 bytes qt/res/arrow-circle-double.png | Bin 836 -> 0 bytes qt/res/cross.png | Bin 544 -> 0 bytes qt/res/tick.png | Bin 634 -> 0 bytes 113 files changed, 1224 insertions(+), 1223 deletions(-) create mode 100644 python/kontor-cli.backup/.gitignore rename python/{ => kontor-cli.backup}/CHANGELOG.md (50%) rename python/{ => kontor-cli.backup}/Dockerfile (58%) rename python/{ => kontor-cli.backup}/LICENSE.md (100%) rename python/{ => kontor-cli.backup}/MANIFEST.in (100%) rename python/{ => kontor-cli.backup}/Makefile (100%) rename python/{ => kontor-cli.backup}/README.md (97%) rename python/{ => kontor-cli.backup}/config/kontor.yml.example (96%) rename python/{ => kontor-cli.backup}/docs/.gitkeep (100%) rename python/{ => kontor-cli.backup}/kontor/__init__.py (100%) rename python/{ => kontor-cli.backup}/kontor/controllers/__init__.py (100%) rename python/{kontor/controllers/clibase.py => kontor-cli.backup/kontor/controllers/base.py} (66%) rename python/{ => kontor-cli.backup}/kontor/controllers/database.py (94%) rename python/{ => kontor-cli.backup}/kontor/controllers/media.py (100%) rename python/{ => kontor-cli.backup}/kontor/core/__init__.py (100%) create mode 100644 python/kontor-cli.backup/kontor/core/exc.py create mode 100644 python/kontor-cli.backup/kontor/core/version.py rename python/{ => kontor-cli.backup}/kontor/ext/__init__.py (100%) rename python/{ => kontor-cli.backup}/kontor/main.py (84%) rename python/{kontor/gui => kontor-cli.backup/kontor/plugins}/__init__.py (100%) rename python/{kontor/plugins => kontor-cli.backup/kontor/templates}/__init__.py (100%) rename python/{ => kontor-cli.backup}/kontor/templates/command1.jinja2 (100%) rename python/{ => kontor-cli.backup}/kontor/templates/download.jinja2 (100%) rename python/{ => kontor-cli.backup}/kontor/templates/import.jinja2 (100%) rename python/{ => kontor-cli.backup}/kontor/templates/update.jinja2 (100%) rename python/{ => kontor-cli.backup}/requirements-dev.txt (100%) create mode 100644 python/kontor-cli.backup/requirements.txt rename python/{ => kontor-cli.backup}/setup.cfg (100%) rename python/{ => kontor-cli.backup}/setup.py (81%) create mode 100644 python/kontor-cli.backup/tests/conftest.py create mode 100644 python/kontor-cli.backup/tests/test_kontor.py create mode 100644 python/kontor-cli/.gitignore create mode 100644 python/kontor-cli/CHANGELOG.md create mode 100644 python/kontor-cli/Dockerfile create mode 100644 python/kontor-cli/LICENSE.md create mode 100644 python/kontor-cli/MANIFEST.in create mode 100644 python/kontor-cli/Makefile create mode 100644 python/kontor-cli/README.md create mode 100644 python/kontor-cli/config/kontor.yml.example rename python/{kontor/templates/__init__.py => kontor-cli/docs/.gitkeep} (100%) rename {qt/gui => python/kontor-cli/kontor}/__init__.py (100%) create mode 100644 python/kontor-cli/kontor/controllers/__init__.py create mode 100644 python/kontor-cli/kontor/controllers/base.py create mode 100644 python/kontor-cli/kontor/controllers/database.py create mode 100644 python/kontor-cli/kontor/controllers/media.py create mode 100644 python/kontor-cli/kontor/core/__init__.py rename python/{ => kontor-cli}/kontor/core/exc.py (100%) rename python/{ => kontor-cli}/kontor/core/version.py (100%) create mode 100644 python/kontor-cli/kontor/ext/__init__.py create mode 100644 python/kontor-cli/kontor/main.py create mode 100644 python/kontor-cli/kontor/plugins/__init__.py create mode 100644 python/kontor-cli/kontor/templates/__init__.py create mode 100644 python/kontor-cli/kontor/templates/command1.jinja2 create mode 100644 python/kontor-cli/requirements-dev.txt rename python/{ => kontor-cli}/requirements.txt (77%) create mode 100644 python/kontor-cli/setup.cfg create mode 100644 python/kontor-cli/setup.py create mode 100644 python/kontor-cli/tests/conftest.py create mode 100644 python/kontor-cli/tests/test_kontor.py create mode 100644 python/kontor-gui/.gitignore create mode 100644 python/kontor-gui/gui/__init__.py rename python/{kontor => kontor-gui}/gui/data_view.py (100%) rename python/{kontor => kontor-gui}/gui/data_view_model.py (100%) rename python/{kontor => kontor-gui}/gui/dialogs.py (100%) rename python/{kontor => kontor-gui}/gui/main_window.py (88%) rename python/{kontor => kontor-gui}/gui/model_config.py (90%) rename python/{kontor => kontor-gui}/gui/progress.py (100%) rename python/{kontor => kontor-gui}/gui/table_model.py (100%) create mode 100644 python/kontor-gui/kontor.py create mode 100644 python/kontor-gui/pyvenv.cfg create mode 100644 python/kontor-gui/requirements.txt rename python/{kontor => kontor-gui}/res/application-export.png (100%) rename python/{kontor => kontor-gui}/res/application-import.png (100%) rename python/{kontor => kontor-gui}/res/arrow-circle-double.png (100%) rename python/{kontor => kontor-gui}/res/cross.png (100%) rename python/{kontor => kontor-gui}/res/tick.png (100%) create mode 100644 python/kontor-schema/README.md create mode 100644 python/kontor-schema/include/site/python3.11/greenlet/greenlet.h rename python/{kontor/database => kontor-schema/kontor_schema}/__init__.py (94%) rename python/{kontor/database => kontor-schema/kontor_schema}/base.py (100%) rename python/{kontor/database => kontor-schema/kontor_schema}/bookshelf.py (100%) rename python/{kontor/database => kontor-schema/kontor_schema}/comic.py (100%) rename python/{kontor/database => kontor-schema/kontor_schema}/media.py (100%) rename python/{kontor/database => kontor-schema/kontor_schema}/metadata.py (100%) rename python/{kontor/database => kontor-schema/kontor_schema}/tysc.py (100%) create mode 100644 python/kontor-schema/pyvenv.cfg create mode 100644 python/kontor-schema/requirements.txt create mode 100644 python/kontor-schema/setup.py create mode 100644 python/kontor-video/README.md create mode 100644 python/kontor-video/kontor_video/__init__.py create mode 100644 python/kontor-video/pyvenv.cfg create mode 100644 python/kontor-video/requirements.txt create mode 100644 python/kontor-video/setup.py delete mode 100644 qt/.gitignore delete mode 100644 qt/database/__init__.py delete mode 100644 qt/database/base.py delete mode 100644 qt/database/comic.py delete mode 100644 qt/database/media.py delete mode 100644 qt/database/metadata.py delete mode 100644 qt/database/tysc.py delete mode 100644 qt/gui/data_view.py delete mode 100644 qt/gui/data_view_model.py delete mode 100644 qt/gui/dialogs.py delete mode 100644 qt/gui/main_window.py delete mode 100644 qt/gui/model_config.py delete mode 100644 qt/gui/table_model.py delete mode 100644 qt/kontor.py delete mode 100644 qt/pysidedeploy.spec delete mode 100644 qt/res/application-export.png delete mode 100644 qt/res/application-import.png delete mode 100644 qt/res/arrow-circle-double.png delete mode 100644 qt/res/cross.png delete mode 100644 qt/res/tick.png diff --git a/python/kontor-cli.backup/.gitignore b/python/kontor-cli.backup/.gitignore new file mode 100644 index 0000000..a74b246 --- /dev/null +++ b/python/kontor-cli.backup/.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/CHANGELOG.md b/python/kontor-cli.backup/CHANGELOG.md similarity index 50% rename from python/CHANGELOG.md rename to python/kontor-cli.backup/CHANGELOG.md index 6c95d97..10e1f8a 100644 --- a/python/CHANGELOG.md +++ b/python/kontor-cli.backup/CHANGELOG.md @@ -1,4 +1,4 @@ -# Kontor Change History +# Kontor CLI Change History ## 0.0.1 diff --git a/python/Dockerfile b/python/kontor-cli.backup/Dockerfile similarity index 58% rename from python/Dockerfile rename to python/kontor-cli.backup/Dockerfile index a3e005d..d32cf5c 100644 --- a/python/Dockerfile +++ b/python/kontor-cli.backup/Dockerfile @@ -1,10 +1,6 @@ -#FROM python:3.9-alpine -FROM python:3.11-bookworm +FROM python:3.9-alpine LABEL MAINTAINER="Thomas Peetz " ENV PS1="\[\e[0;33m\]|> kontor <| \[\e[1;35m\]\W\[\e[0m\] \[\e[0m\]# " -ENV DEBIAN_FRONTEND noninteractive -RUN apt-get update && apt-get install -y apt-utils libmariadb3 libmariadb-dev -RUN rm -rf /var/lib/apt/lists/* WORKDIR /src COPY . /src diff --git a/python/LICENSE.md b/python/kontor-cli.backup/LICENSE.md similarity index 100% rename from python/LICENSE.md rename to python/kontor-cli.backup/LICENSE.md diff --git a/python/MANIFEST.in b/python/kontor-cli.backup/MANIFEST.in similarity index 100% rename from python/MANIFEST.in rename to python/kontor-cli.backup/MANIFEST.in diff --git a/python/Makefile b/python/kontor-cli.backup/Makefile similarity index 100% rename from python/Makefile rename to python/kontor-cli.backup/Makefile diff --git a/python/README.md b/python/kontor-cli.backup/README.md similarity index 97% rename from python/README.md rename to python/kontor-cli.backup/README.md index 6afe593..a95f4ea 100644 --- a/python/README.md +++ b/python/kontor-cli.backup/README.md @@ -1,4 +1,4 @@ -# Kontor CLI Tool +# Kontor CLI ## Installation @@ -59,7 +59,7 @@ $ make dist-upload ### Docker -Included is a basic `Dockerfile` for building and distributing `Kontor`, +Included is a basic `Dockerfile` for building and distributing `Kontor CLI`, and can be built with the included `make` helper: ``` diff --git a/python/config/kontor.yml.example b/python/kontor-cli.backup/config/kontor.yml.example similarity index 96% rename from python/config/kontor.yml.example rename to python/kontor-cli.backup/config/kontor.yml.example index ba6507c..6806151 100644 --- a/python/config/kontor.yml.example +++ b/python/kontor-cli.backup/config/kontor.yml.example @@ -1,4 +1,4 @@ -### Kontor Configuration Settings +### Kontor CLI Configuration Settings --- kontor: diff --git a/python/docs/.gitkeep b/python/kontor-cli.backup/docs/.gitkeep similarity index 100% rename from python/docs/.gitkeep rename to python/kontor-cli.backup/docs/.gitkeep diff --git a/python/kontor/__init__.py b/python/kontor-cli.backup/kontor/__init__.py similarity index 100% rename from python/kontor/__init__.py rename to python/kontor-cli.backup/kontor/__init__.py diff --git a/python/kontor/controllers/__init__.py b/python/kontor-cli.backup/kontor/controllers/__init__.py similarity index 100% rename from python/kontor/controllers/__init__.py rename to python/kontor-cli.backup/kontor/controllers/__init__.py diff --git a/python/kontor/controllers/clibase.py b/python/kontor-cli.backup/kontor/controllers/base.py similarity index 66% rename from python/kontor/controllers/clibase.py rename to python/kontor-cli.backup/kontor/controllers/base.py index 0491128..90153a3 100644 --- a/python/kontor/controllers/clibase.py +++ b/python/kontor-cli.backup/kontor/controllers/base.py @@ -1,16 +1,15 @@ -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 +Kontor CLI %s %s """ % (get_version(), get_version_banner()) -class CliBase(Controller): +class Base(Controller): class Meta: label = 'base' @@ -18,7 +17,7 @@ class CliBase(Controller): description = 'Kontor CLI Tool' # text displayed at the bottom of --help output - epilog = 'Usage: kontor (gui | database | media) [subcommands]' + epilog = 'Usage: kontor (database | media) [subcommands]' # controller level arguments. ex: 'kontor --version' arguments = [ @@ -34,12 +33,3 @@ class CliBase(Controller): def _default(self): """Default action if no sub-command is passed.""" self.app.args.print_help() - - @ex( - help='start GUI' - ) - def gui(self): - application = QApplication([]) - window = MainWindow(self.app.engine, self.app.config, self.app.log) - window.show() - application.exec() diff --git a/python/kontor/controllers/database.py b/python/kontor-cli.backup/kontor/controllers/database.py similarity index 94% rename from python/kontor/controllers/database.py rename to python/kontor-cli.backup/kontor/controllers/database.py index 565cbd8..5917d0a 100644 --- a/python/kontor/controllers/database.py +++ b/python/kontor-cli.backup/kontor/controllers/database.py @@ -1,6 +1,5 @@ from cement import Controller, ex - -from ..database import KontorDB +from kontor_schema import KontorDB class Database(Controller): diff --git a/python/kontor/controllers/media.py b/python/kontor-cli.backup/kontor/controllers/media.py similarity index 100% rename from python/kontor/controllers/media.py rename to python/kontor-cli.backup/kontor/controllers/media.py diff --git a/python/kontor/core/__init__.py b/python/kontor-cli.backup/kontor/core/__init__.py similarity index 100% rename from python/kontor/core/__init__.py rename to python/kontor-cli.backup/kontor/core/__init__.py diff --git a/python/kontor-cli.backup/kontor/core/exc.py b/python/kontor-cli.backup/kontor/core/exc.py new file mode 100644 index 0000000..8dd63e8 --- /dev/null +++ b/python/kontor-cli.backup/kontor/core/exc.py @@ -0,0 +1,4 @@ + +class KontorCliError(Exception): + """Generic errors.""" + pass diff --git a/python/kontor-cli.backup/kontor/core/version.py b/python/kontor-cli.backup/kontor/core/version.py new file mode 100644 index 0000000..846814d --- /dev/null +++ b/python/kontor-cli.backup/kontor/core/version.py @@ -0,0 +1,7 @@ + +from cement.utils.version import get_version as cement_get_version + +VERSION = (0, 1, 0, 'alpha', 0) + +def get_version(version=VERSION): + return cement_get_version(version) diff --git a/python/kontor/ext/__init__.py b/python/kontor-cli.backup/kontor/ext/__init__.py similarity index 100% rename from python/kontor/ext/__init__.py rename to python/kontor-cli.backup/kontor/ext/__init__.py diff --git a/python/kontor/main.py b/python/kontor-cli.backup/kontor/main.py similarity index 84% rename from python/kontor/main.py rename to python/kontor-cli.backup/kontor/main.py index 2f2e469..c8084b8 100644 --- a/python/kontor/main.py +++ b/python/kontor-cli.backup/kontor/main.py @@ -1,13 +1,13 @@ + from cement import App, TestApp, init_defaults from cement.core.exc import CaughtSignal from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -from kontor.controllers.media import Media -from kontor.core.exc import KontorError -from kontor.database.base import Base -from kontor.controllers.clibase import CliBase -from kontor.controllers.database import Database +from .controllers.database import Database +from .controllers.media import Media +from .core.exc import KontorCliError +from .controllers.base import Base # configuration defaults CONFIG = init_defaults('kontor', 'mariadb', 'media') @@ -40,8 +40,8 @@ def extend_sqlalchemy(app): app.extend('engine', engine) -class Kontor(App): - """Kontor primary application.""" +class KontorCli(App): + """Kontor CLI primary application.""" class Meta: label = 'kontor' @@ -77,23 +77,24 @@ class Kontor(App): hooks = [ ('post_setup', extend_sqlalchemy), ] + # register handlers handlers = [ - CliBase, + Base, Database, Media, ] -class KontorTest(TestApp, Kontor): - """A sub-class of Kontor that is better suited for testing.""" +class KontorCliTest(TestApp,KontorCli): + """A sub-class of KontorCli that is better suited for testing.""" class Meta: label = 'kontor' def main(): - with Kontor() as app: + with KontorCli() as app: try: app.run() @@ -105,8 +106,8 @@ def main(): import traceback traceback.print_exc() - except KontorError as e: - print('KontorError > %s' % e.args[0]) + except KontorCliError as e: + print('KontorCliError > %s' % e.args[0]) app.exit_code = 1 if app.debug is True: diff --git a/python/kontor/gui/__init__.py b/python/kontor-cli.backup/kontor/plugins/__init__.py similarity index 100% rename from python/kontor/gui/__init__.py rename to python/kontor-cli.backup/kontor/plugins/__init__.py diff --git a/python/kontor/plugins/__init__.py b/python/kontor-cli.backup/kontor/templates/__init__.py similarity index 100% rename from python/kontor/plugins/__init__.py rename to python/kontor-cli.backup/kontor/templates/__init__.py diff --git a/python/kontor/templates/command1.jinja2 b/python/kontor-cli.backup/kontor/templates/command1.jinja2 similarity index 100% rename from python/kontor/templates/command1.jinja2 rename to python/kontor-cli.backup/kontor/templates/command1.jinja2 diff --git a/python/kontor/templates/download.jinja2 b/python/kontor-cli.backup/kontor/templates/download.jinja2 similarity index 100% rename from python/kontor/templates/download.jinja2 rename to python/kontor-cli.backup/kontor/templates/download.jinja2 diff --git a/python/kontor/templates/import.jinja2 b/python/kontor-cli.backup/kontor/templates/import.jinja2 similarity index 100% rename from python/kontor/templates/import.jinja2 rename to python/kontor-cli.backup/kontor/templates/import.jinja2 diff --git a/python/kontor/templates/update.jinja2 b/python/kontor-cli.backup/kontor/templates/update.jinja2 similarity index 100% rename from python/kontor/templates/update.jinja2 rename to python/kontor-cli.backup/kontor/templates/update.jinja2 diff --git a/python/requirements-dev.txt b/python/kontor-cli.backup/requirements-dev.txt similarity index 100% rename from python/requirements-dev.txt rename to python/kontor-cli.backup/requirements-dev.txt diff --git a/python/kontor-cli.backup/requirements.txt b/python/kontor-cli.backup/requirements.txt new file mode 100644 index 0000000..bee2df1 --- /dev/null +++ b/python/kontor-cli.backup/requirements.txt @@ -0,0 +1,6 @@ +cement==3.0.12 +cement[jinja2] +cement[yaml] +cement[colorlog] +mariadb +sqlalchemy diff --git a/python/setup.cfg b/python/kontor-cli.backup/setup.cfg similarity index 100% rename from python/setup.cfg rename to python/kontor-cli.backup/setup.cfg diff --git a/python/setup.py b/python/kontor-cli.backup/setup.py similarity index 81% rename from python/setup.py rename to python/kontor-cli.backup/setup.py index 2f6b226..185d9fa 100644 --- a/python/setup.py +++ b/python/kontor-cli.backup/setup.py @@ -11,15 +11,15 @@ f.close() setup( name='kontor', version=VERSION, - description='Kontor CLI Tool', + description='Kontor CLI', 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', + url='https://gitlab.com/tpeetz/kontor/', license='MIT', packages=find_packages(exclude=['ez_setup', 'tests*']), - package_data={'kontor': ['templates/*', 'res/*',]}, + package_data={'kontor': ['templates/*']}, include_package_data=True, entry_points=""" [console_scripts] diff --git a/python/kontor-cli.backup/tests/conftest.py b/python/kontor-cli.backup/tests/conftest.py new file mode 100644 index 0000000..5124e2e --- /dev/null +++ b/python/kontor-cli.backup/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/kontor-cli.backup/tests/test_kontor.py b/python/kontor-cli.backup/tests/test_kontor.py new file mode 100644 index 0000000..e93dd44 --- /dev/null +++ b/python/kontor-cli.backup/tests/test_kontor.py @@ -0,0 +1,36 @@ + +from pytest import raises +from kontor.main import KontorCliTest + +def test_kontor(): + # test kontor without any subcommands or arguments + with KontorCliTest() as app: + app.run() + assert app.exit_code == 0 + + +def test_kontor_debug(): + # test that debug mode is functional + argv = ['--debug'] + with KontorCliTest(argv=argv) as app: + app.run() + assert app.debug is True + + +def test_command1(): + # test command1 without arguments + argv = ['command1'] + with KontorCliTest(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 KontorCliTest(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/kontor-cli/.gitignore b/python/kontor-cli/.gitignore new file mode 100644 index 0000000..a74b246 --- /dev/null +++ b/python/kontor-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/kontor-cli/CHANGELOG.md b/python/kontor-cli/CHANGELOG.md new file mode 100644 index 0000000..10e1f8a --- /dev/null +++ b/python/kontor-cli/CHANGELOG.md @@ -0,0 +1,5 @@ +# Kontor CLI Change History + +## 0.0.1 + +Initial release. diff --git a/python/kontor-cli/Dockerfile b/python/kontor-cli/Dockerfile new file mode 100644 index 0000000..d32cf5c --- /dev/null +++ b/python/kontor-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/kontor-cli/LICENSE.md b/python/kontor-cli/LICENSE.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/python/kontor-cli/LICENSE.md @@ -0,0 +1 @@ + diff --git a/python/kontor-cli/MANIFEST.in b/python/kontor-cli/MANIFEST.in new file mode 100644 index 0000000..1160952 --- /dev/null +++ b/python/kontor-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/kontor-cli/Makefile b/python/kontor-cli/Makefile new file mode 100644 index 0000000..b016c3c --- /dev/null +++ b/python/kontor-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/kontor-cli/README.md b/python/kontor-cli/README.md new file mode 100644 index 0000000..a95f4ea --- /dev/null +++ b/python/kontor-cli/README.md @@ -0,0 +1,69 @@ +# Kontor CLI + +## 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 CLI`, +and can be built with the included `make` helper: + +``` +$ make docker + +$ docker run -it kontor --help +``` diff --git a/python/kontor-cli/config/kontor.yml.example b/python/kontor-cli/config/kontor.yml.example new file mode 100644 index 0000000..6806151 --- /dev/null +++ b/python/kontor-cli/config/kontor.yml.example @@ -0,0 +1,46 @@ +### Kontor CLI 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/python/kontor/templates/__init__.py b/python/kontor-cli/docs/.gitkeep similarity index 100% rename from python/kontor/templates/__init__.py rename to python/kontor-cli/docs/.gitkeep diff --git a/qt/gui/__init__.py b/python/kontor-cli/kontor/__init__.py similarity index 100% rename from qt/gui/__init__.py rename to python/kontor-cli/kontor/__init__.py diff --git a/python/kontor-cli/kontor/controllers/__init__.py b/python/kontor-cli/kontor/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/kontor-cli/kontor/controllers/base.py b/python/kontor-cli/kontor/controllers/base.py new file mode 100644 index 0000000..0f7bdda --- /dev/null +++ b/python/kontor-cli/kontor/controllers/base.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 %s +%s +""" % (get_version(), get_version_banner()) + + +class CliBase(Controller): + class Meta: + label = 'clibase' + + # text displayed at the top of --help output + description = 'Kontor CLI' + + # 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/kontor-cli/kontor/controllers/database.py b/python/kontor-cli/kontor/controllers/database.py new file mode 100644 index 0000000..73732f4 --- /dev/null +++ b/python/kontor-cli/kontor/controllers/database.py @@ -0,0 +1,51 @@ +from cement import Controller, ex +from kontor_schema import KontorDB + + +class Database(Controller): + + class Meta: + label = 'database' + stacked_type = 'nested' + stacked_on = 'clibase' + + @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.engine, self.app.config, self.app.log) + kontor_db.export_db(data['export_type'], data['db_file']) + self.app.render(data, 'command1.jinja2') + + @ex( + label='import', + help='import data from file into database', + arguments=[ + (['-f', '--file'], + {'help': 'file to read data', + 'action': 'store', + 'dest': 'db_file'}) + ], + ) + def import_cmd(self): + data = { + 'db_file': 'data.json', + 'data_type': 'JSON', + } + if self.app.pargs.db_file is not None: + data['db_file'] = self.app.pargs.db_file + kontor_db = KontorDB(self.app.engine, self.app.config, self.app.log) + self.app.render(data, 'import.jinja2') + kontor_db.import_db(data['db_file'], self.app.pargs.dry_run) diff --git a/python/kontor-cli/kontor/controllers/media.py b/python/kontor-cli/kontor/controllers/media.py new file mode 100644 index 0000000..7979b25 --- /dev/null +++ b/python/kontor-cli/kontor/controllers/media.py @@ -0,0 +1,81 @@ +from cement import Controller, ex +from kontor_schema import KontorDB +from kontor_video import VideoLink + +class Media(Controller): + + class Meta: + label = 'media' + stacked_type = 'nested' + stacked_on = 'clibase' + + @ex( + label='update', + help='update title for mediafiles', + ) + def update_title(self): + kontor_db = KontorDB(self.app.engine, self.app.log) + kontor_db.update_title(self.app.pargs.dry_run) + + @ex( + label='download', + help='download all marked videos', + arguments=[ + (['-d', '--dir'], + {'help': 'directory to store videos', + 'action': 'store', + 'dest': 'media_dir'}) + ], + ) + def download(self): + data = { + 'media_dir': '/data/media', + } + if self.app.pargs.media_dir is not None: + data['media_dir'] = self.app.pargs.media_dir + kontor_db = KontorDB(self.app.engine, self.app.log) + downloads = kontor_db.get_download_list() + for download in downloads: + link = VideoLink(download, download_dir=data['media_dir']) + link.download() + + @ex( + help='add url to database', + arguments=[ + (['-u', '--url'], + {'help': 'link to downloadable video', + 'action': 'store', + 'dest': 'link'}) + ], + ) + def add(self): + data = { + 'link_url': None + } + if self.app.pargs.link is not None: + data['link_url'] = self.app.pargs.link + if self.app.pargs.dry_run: + print(f"add url {data['link_url']} to database") + kontor_db = KontorDB(self.app.engine, self.app.log) + kontor_db.add_link(self.app.pargs.link, self.app.pargs.dry_run) + + else: + print("no url was given.") + + @ex( + help='check files if existing', + arguments=[ + (['-d', '--dir'], + {'help': 'directory to store videos', + 'action': 'store', + 'dest': 'media_dir'}) + ], + ) + def check(self): + data = { + 'media_dir': '/data/media', + } + if self.app.pargs.media_dir is not None: + data['media_dir'] = self.app.pargs.media_dir + kontor_db = KontorDB(self.app.engine, self.app.log) + kontor_db.check_files() diff --git a/python/kontor-cli/kontor/core/__init__.py b/python/kontor-cli/kontor/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/kontor/core/exc.py b/python/kontor-cli/kontor/core/exc.py similarity index 100% rename from python/kontor/core/exc.py rename to python/kontor-cli/kontor/core/exc.py diff --git a/python/kontor/core/version.py b/python/kontor-cli/kontor/core/version.py similarity index 100% rename from python/kontor/core/version.py rename to python/kontor-cli/kontor/core/version.py diff --git a/python/kontor-cli/kontor/ext/__init__.py b/python/kontor-cli/kontor/ext/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/kontor-cli/kontor/main.py b/python/kontor-cli/kontor/main.py new file mode 100644 index 0000000..8884b87 --- /dev/null +++ b/python/kontor-cli/kontor/main.py @@ -0,0 +1,118 @@ + +from cement import App, TestApp, init_defaults +from cement.core.exc import CaughtSignal +from kontor_schema.base import Base +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from .core.exc import KontorError +from .controllers.base import CliBase +from .controllers.database import Database +from .controllers.media import Media + +# configuration defaults +CONFIG = init_defaults('kontor', 'mariadb', 'media') +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' +CONFIG['media']['yt-dlp'] = '/home/tpeetz/bin/yt-dlp' +CONFIG['media']['dir'] = '/data/media' + +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, checkfirst=True) + __session__ = sessionmaker(bind=engine) + app.extend('engine', engine) + + +class Kontor(App): + """Kontor CLI 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, + Database, + Media, + ] + + +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/kontor-cli/kontor/plugins/__init__.py b/python/kontor-cli/kontor/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/kontor-cli/kontor/templates/__init__.py b/python/kontor-cli/kontor/templates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/kontor-cli/kontor/templates/command1.jinja2 b/python/kontor-cli/kontor/templates/command1.jinja2 new file mode 100644 index 0000000..2435e4d --- /dev/null +++ b/python/kontor-cli/kontor/templates/command1.jinja2 @@ -0,0 +1,4 @@ + +Example Template (templates/command1.jinja2) + +Foo => {{ foo }} diff --git a/python/kontor-cli/requirements-dev.txt b/python/kontor-cli/requirements-dev.txt new file mode 100644 index 0000000..f20606e --- /dev/null +++ b/python/kontor-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/requirements.txt b/python/kontor-cli/requirements.txt similarity index 77% rename from python/requirements.txt rename to python/kontor-cli/requirements.txt index 76bba2b..5b4c054 100644 --- a/python/requirements.txt +++ b/python/kontor-cli/requirements.txt @@ -4,6 +4,4 @@ cement[yaml] cement[colorlog] mariadb sqlalchemy -PySide6 -beautifulsoup4 diff --git a/python/kontor-cli/setup.cfg b/python/kontor-cli/setup.cfg new file mode 100644 index 0000000..e69de29 diff --git a/python/kontor-cli/setup.py b/python/kontor-cli/setup.py new file mode 100644 index 0000000..18c2eba --- /dev/null +++ b/python/kontor-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', + 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='unlicensed', + 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/kontor-cli/tests/conftest.py b/python/kontor-cli/tests/conftest.py new file mode 100644 index 0000000..5124e2e --- /dev/null +++ b/python/kontor-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/kontor-cli/tests/test_kontor.py b/python/kontor-cli/tests/test_kontor.py new file mode 100644 index 0000000..3c1bd67 --- /dev/null +++ b/python/kontor-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/kontor-gui/.gitignore b/python/kontor-gui/.gitignore new file mode 100644 index 0000000..8d5ef37 --- /dev/null +++ b/python/kontor-gui/.gitignore @@ -0,0 +1,7 @@ +deployment/ +kontor.bin +bin/ +include/ +lib/ +lib64/ +lib64 diff --git a/python/kontor-gui/gui/__init__.py b/python/kontor-gui/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/kontor/gui/data_view.py b/python/kontor-gui/gui/data_view.py similarity index 100% rename from python/kontor/gui/data_view.py rename to python/kontor-gui/gui/data_view.py diff --git a/python/kontor/gui/data_view_model.py b/python/kontor-gui/gui/data_view_model.py similarity index 100% rename from python/kontor/gui/data_view_model.py rename to python/kontor-gui/gui/data_view_model.py diff --git a/python/kontor/gui/dialogs.py b/python/kontor-gui/gui/dialogs.py similarity index 100% rename from python/kontor/gui/dialogs.py rename to python/kontor-gui/gui/dialogs.py diff --git a/python/kontor/gui/main_window.py b/python/kontor-gui/gui/main_window.py similarity index 88% rename from python/kontor/gui/main_window.py rename to python/kontor-gui/gui/main_window.py index 0996d02..4f4ee91 100644 --- a/python/kontor/gui/main_window.py +++ b/python/kontor-gui/gui/main_window.py @@ -1,15 +1,10 @@ -from typing import Any - from PySide6.QtGui import QAction, QIcon from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView, QProgressBar from PySide6.QtWidgets import QLabel, QMainWindow -from cement.core.config import ConfigHandler from sqlalchemy import Engine +from kontor_schema import KontorDB from .progress import ProgressUpdate -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 @@ -17,14 +12,14 @@ from .table_model import KontorTableModel class MainWindow(QMainWindow): - def __init__(self, engine: Engine, config: ConfigHandler, log): + def __init__(self, engine: Engine, 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.tick = QIcon('res/tick.png') + self.cross = QIcon('res/cross.png') + self.import_icon = QIcon("res/application-import.png") + self.export_icon = QIcon("res/application-export.png") + self.circle_icon = QIcon("res/arrow-circle-double.png") self.setWindowTitle("Kontor") self.setMinimumSize(800, 500) @@ -37,14 +32,14 @@ class MainWindow(QMainWindow): self.data = [] self.filter = {} - self.kontor_db = KontorDB(engine, config, log) + self.kontor_db = KontorDB(engine, 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.addTab(self.generate_data_tab("comic"), "Comics") + self.tabs.addTab(self.generate_data_tab("media_file"), "MediaFile") self.tabs.currentChanged.connect(self._tab_changed) #label.setAlignment(Qt.AlignmentFlag.AlignCenter) parent_layout.addWidget(self.tabs) @@ -158,9 +153,10 @@ class MainWindow(QMainWindow): def _tab_changed(self, tab_index): self.data[tab_index].refresh() - def generate_data_tab(self, table_name, table): + def generate_data_tab(self, table_name): data_tab = QWidget() - table_config = KontorModelConfig(self.kontor_db, self, table_name, table) + + table_config = KontorModelConfig(self.kontor_db, self, table_name) model = KontorTableModel(table_config) layout = QVBoxLayout() self.data.append(model) diff --git a/python/kontor/gui/model_config.py b/python/kontor-gui/gui/model_config.py similarity index 90% rename from python/kontor/gui/model_config.py rename to python/kontor-gui/gui/model_config.py index ce61107..e791752 100644 --- a/python/kontor/gui/model_config.py +++ b/python/kontor-gui/gui/model_config.py @@ -1,17 +1,14 @@ -import mariadb from PySide6.QtWidgets import QHBoxLayout, QCheckBox - -from ..database import KontorDB +from kontor_schema import KontorDB class KontorModelConfig: - def __init__(self, kontor_db: KontorDB, main_window, table_name: str, table): + def __init__(self, kontor_db: KontorDB, main_window, table_name: str): 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() @@ -32,7 +29,7 @@ class KontorModelConfig: 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()) + data = self.kontor_db.data(self._table_name, 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/python/kontor/gui/progress.py b/python/kontor-gui/gui/progress.py similarity index 100% rename from python/kontor/gui/progress.py rename to python/kontor-gui/gui/progress.py diff --git a/python/kontor/gui/table_model.py b/python/kontor-gui/gui/table_model.py similarity index 100% rename from python/kontor/gui/table_model.py rename to python/kontor-gui/gui/table_model.py diff --git a/python/kontor-gui/kontor.py b/python/kontor-gui/kontor.py new file mode 100644 index 0000000..50e16cd --- /dev/null +++ b/python/kontor-gui/kontor.py @@ -0,0 +1,43 @@ +""" +PyQT6 GUI for Kontor +""" +import logging +import sys +import logging.config +from pathlib import Path +from platformdirs import PlatformDirs +from PySide6.QtWidgets import QApplication +import yaml +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from kontor_schema.base import Base + +from gui.main_window import MainWindow + + +if __name__ == '__main__': + app = QApplication(sys.argv) + dirs = PlatformDirs("kontor") + database_config = Path(dirs.user_config_dir, 'database-config.yaml') + with open(database_config, 'rt') as f: + db_config = yaml.safe_load(f.read()) + 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'] + )) + logging_config = Path(dirs.user_config_dir, 'logging-config.yaml') + with open(logging_config, 'rt') as f: + config = yaml.safe_load(f.read()) + logging.config.dictConfig(config) + logger = logging.getLogger('development') + # engine = create_engine(connect_string, echo=True) + engine = create_engine(connect_string) + Base.metadata.create_all(bind=engine, checkfirst=True) + __session__ = sessionmaker(bind=engine) + + window = MainWindow(engine, logger) + window.show() + app.exec() diff --git a/python/kontor-gui/pyvenv.cfg b/python/kontor-gui/pyvenv.cfg new file mode 100644 index 0000000..e6324f5 --- /dev/null +++ b/python/kontor-gui/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.11.2 +executable = /usr/bin/python3.11 +command = /usr/bin/python -m venv /home/tpeetz/projects/kontor/python/kontor-gui diff --git a/python/kontor-gui/requirements.txt b/python/kontor-gui/requirements.txt new file mode 100644 index 0000000..90b0945 --- /dev/null +++ b/python/kontor-gui/requirements.txt @@ -0,0 +1,6 @@ +-e /home/tpeetz/projects/kontor/python/kontor-schema +-e /home/tpeetz/projects/kontor/python/kontor-video + +platformdirs +pyyaml +PySide6 diff --git a/python/kontor/res/application-export.png b/python/kontor-gui/res/application-export.png similarity index 100% rename from python/kontor/res/application-export.png rename to python/kontor-gui/res/application-export.png diff --git a/python/kontor/res/application-import.png b/python/kontor-gui/res/application-import.png similarity index 100% rename from python/kontor/res/application-import.png rename to python/kontor-gui/res/application-import.png diff --git a/python/kontor/res/arrow-circle-double.png b/python/kontor-gui/res/arrow-circle-double.png similarity index 100% rename from python/kontor/res/arrow-circle-double.png rename to python/kontor-gui/res/arrow-circle-double.png diff --git a/python/kontor/res/cross.png b/python/kontor-gui/res/cross.png similarity index 100% rename from python/kontor/res/cross.png rename to python/kontor-gui/res/cross.png diff --git a/python/kontor/res/tick.png b/python/kontor-gui/res/tick.png similarity index 100% rename from python/kontor/res/tick.png rename to python/kontor-gui/res/tick.png diff --git a/python/kontor-schema/README.md b/python/kontor-schema/README.md new file mode 100644 index 0000000..eafef62 --- /dev/null +++ b/python/kontor-schema/README.md @@ -0,0 +1,4 @@ +# Schema for Kontor DB + +This library contains the schema for the Kontor DB. + diff --git a/python/kontor-schema/include/site/python3.11/greenlet/greenlet.h b/python/kontor-schema/include/site/python3.11/greenlet/greenlet.h new file mode 100644 index 0000000..d02a16e --- /dev/null +++ b/python/kontor-schema/include/site/python3.11/greenlet/greenlet.h @@ -0,0 +1,164 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ + +/* Greenlet object interface */ + +#ifndef Py_GREENLETOBJECT_H +#define Py_GREENLETOBJECT_H + + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* This is deprecated and undocumented. It does not change. */ +#define GREENLET_VERSION "1.0.0" + +#ifndef GREENLET_MODULE +#define implementation_ptr_t void* +#endif + +typedef struct _greenlet { + PyObject_HEAD + PyObject* weakreflist; + PyObject* dict; + implementation_ptr_t pimpl; +} PyGreenlet; + +#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type)) + + +/* C API functions */ + +/* Total number of symbols that are exported */ +#define PyGreenlet_API_pointers 12 + +#define PyGreenlet_Type_NUM 0 +#define PyExc_GreenletError_NUM 1 +#define PyExc_GreenletExit_NUM 2 + +#define PyGreenlet_New_NUM 3 +#define PyGreenlet_GetCurrent_NUM 4 +#define PyGreenlet_Throw_NUM 5 +#define PyGreenlet_Switch_NUM 6 +#define PyGreenlet_SetParent_NUM 7 + +#define PyGreenlet_MAIN_NUM 8 +#define PyGreenlet_STARTED_NUM 9 +#define PyGreenlet_ACTIVE_NUM 10 +#define PyGreenlet_GET_PARENT_NUM 11 + +#ifndef GREENLET_MODULE +/* This section is used by modules that uses the greenlet C API */ +static void** _PyGreenlet_API = NULL; + +# define PyGreenlet_Type \ + (*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM]) + +# define PyExc_GreenletError \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM]) + +# define PyExc_GreenletExit \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM]) + +/* + * PyGreenlet_New(PyObject *args) + * + * greenlet.greenlet(run, parent=None) + */ +# define PyGreenlet_New \ + (*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \ + _PyGreenlet_API[PyGreenlet_New_NUM]) + +/* + * PyGreenlet_GetCurrent(void) + * + * greenlet.getcurrent() + */ +# define PyGreenlet_GetCurrent \ + (*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM]) + +/* + * PyGreenlet_Throw( + * PyGreenlet *greenlet, + * PyObject *typ, + * PyObject *val, + * PyObject *tb) + * + * g.throw(...) + */ +# define PyGreenlet_Throw \ + (*(PyObject * (*)(PyGreenlet * self, \ + PyObject * typ, \ + PyObject * val, \ + PyObject * tb)) \ + _PyGreenlet_API[PyGreenlet_Throw_NUM]) + +/* + * PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args) + * + * g.switch(*args, **kwargs) + */ +# define PyGreenlet_Switch \ + (*(PyObject * \ + (*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \ + _PyGreenlet_API[PyGreenlet_Switch_NUM]) + +/* + * PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent) + * + * g.parent = new_parent + */ +# define PyGreenlet_SetParent \ + (*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \ + _PyGreenlet_API[PyGreenlet_SetParent_NUM]) + +/* + * PyGreenlet_GetParent(PyObject* greenlet) + * + * return greenlet.parent; + * + * This could return NULL even if there is no exception active. + * If it does not return NULL, you are responsible for decrementing the + * reference count. + */ +# define PyGreenlet_GetParent \ + (*(PyGreenlet* (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_GET_PARENT_NUM]) + +/* + * deprecated, undocumented alias. + */ +# define PyGreenlet_GET_PARENT PyGreenlet_GetParent + +# define PyGreenlet_MAIN \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_MAIN_NUM]) + +# define PyGreenlet_STARTED \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_STARTED_NUM]) + +# define PyGreenlet_ACTIVE \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_ACTIVE_NUM]) + + + + +/* Macro that imports greenlet and initializes C API */ +/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we + keep the older definition to be sure older code that might have a copy of + the header still works. */ +# define PyGreenlet_Import() \ + { \ + _PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \ + } + +#endif /* GREENLET_MODULE */ + +#ifdef __cplusplus +} +#endif +#endif /* !Py_GREENLETOBJECT_H */ diff --git a/python/kontor/database/__init__.py b/python/kontor-schema/kontor_schema/__init__.py similarity index 94% rename from python/kontor/database/__init__.py rename to python/kontor-schema/kontor_schema/__init__.py index 54c296b..3549955 100644 --- a/python/kontor/database/__init__.py +++ b/python/kontor-schema/kontor_schema/__init__.py @@ -4,29 +4,22 @@ import subprocess import uuid from datetime import datetime from pathlib import Path -from typing import Any -import requests -from bs4 import BeautifulSoup -from cement.core.config import ConfigHandler from sqlalchemy import Engine from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import sessionmaker -from .base import Base from .bookshelf import Article, Book, Author, BookshelfPublisher, ArticleAuthor, BookAuthor from .comic import Comic, Artist, Publisher, Issue, StoryArc, TradePaperback, Volume, ComicWork, WorkType from .metadata import MetaDataTable, MetaDataColumn from .tysc import Card, CardSet, Sport, Team, FieldPosition, Rooster, Player, Vendor from .media import MediaFile, MediaArticle, MediaVideo -from ..gui.progress import ProgressUpdate class KontorDB: - def __init__(self, db_engine: Engine, config: ConfigHandler, log): + def __init__(self, db_engine: Engine, log): self.engine = db_engine - self.config = config self.log = log self.registry = {} self.init_registry() @@ -106,9 +99,10 @@ class KontorDB: self.log.debug(f"retrieved {len(_filter_map)} filters: {_filter_map}") return _filter_map - def data(self, table, columns: dict, filters) -> list: + def data(self, table_name: str, columns: dict, filters: dict) -> list: data = [] __session__ = sessionmaker(self.engine) + table = self.registry[table_name] with __session__() as session: entries = [] if len(filters) == 0: @@ -306,8 +300,23 @@ class KontorDB: link.review = 0 session.commit() - def download_file(self, dry_run=False, update: ProgressUpdate=None): - self.log.info(f"download marked files of media_file") + def get_download_list(self) -> list[str]: + self.log.debug("get links marked as should_download") + download_list = [] + __session__ = sessionmaker(self.engine) + with __session__() as session: + links = session.query(MediaFile).filter(MediaFile.should_download == 1).all() + for link in links: + url = link.url + if url is None: + self.log.info(f"url has not been set for {link.id}") + continue + download_list.append(url) + self.log.debug(f"found {len(download_list)} urls for downloads") + return download_list + + def download_file(self, dry_run=False): + self.log.info(f"download marked urls of media_file") __session__ = sessionmaker(self.engine) with __session__() as session: links = session.query(MediaFile).filter(MediaFile.should_download == 1).all() diff --git a/python/kontor/database/base.py b/python/kontor-schema/kontor_schema/base.py similarity index 100% rename from python/kontor/database/base.py rename to python/kontor-schema/kontor_schema/base.py diff --git a/python/kontor/database/bookshelf.py b/python/kontor-schema/kontor_schema/bookshelf.py similarity index 100% rename from python/kontor/database/bookshelf.py rename to python/kontor-schema/kontor_schema/bookshelf.py diff --git a/python/kontor/database/comic.py b/python/kontor-schema/kontor_schema/comic.py similarity index 100% rename from python/kontor/database/comic.py rename to python/kontor-schema/kontor_schema/comic.py diff --git a/python/kontor/database/media.py b/python/kontor-schema/kontor_schema/media.py similarity index 100% rename from python/kontor/database/media.py rename to python/kontor-schema/kontor_schema/media.py diff --git a/python/kontor/database/metadata.py b/python/kontor-schema/kontor_schema/metadata.py similarity index 100% rename from python/kontor/database/metadata.py rename to python/kontor-schema/kontor_schema/metadata.py diff --git a/python/kontor/database/tysc.py b/python/kontor-schema/kontor_schema/tysc.py similarity index 100% rename from python/kontor/database/tysc.py rename to python/kontor-schema/kontor_schema/tysc.py diff --git a/python/kontor-schema/pyvenv.cfg b/python/kontor-schema/pyvenv.cfg new file mode 100644 index 0000000..794f3c8 --- /dev/null +++ b/python/kontor-schema/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.11.2 +executable = /usr/bin/python3.11 +command = /usr/bin/python -m venv /home/tpeetz/projects/kontor/python/kontor-schema diff --git a/python/kontor-schema/requirements.txt b/python/kontor-schema/requirements.txt new file mode 100644 index 0000000..d08ec81 --- /dev/null +++ b/python/kontor-schema/requirements.txt @@ -0,0 +1,2 @@ +mariadb +sqlalchemy diff --git a/python/kontor-schema/setup.py b/python/kontor-schema/setup.py new file mode 100644 index 0000000..7f76710 --- /dev/null +++ b/python/kontor-schema/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup, find_packages +import pathlib + +here = pathlib.Path(__file__).parent.resolve() + +long_description = ( here / "README.md").read_text(encoding="utf-8") + +setup( + name='kontor_schema', + version='0.1.0', + description='Schema for Konotor DB', + long_description=long_description, + long_description_content_type="text/markdown", + author='Thomas Peetz', + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.11", + ], + install_requires=["sqlalchemy", "mariadb"], + packages=find_packages(), +) diff --git a/python/kontor-video/README.md b/python/kontor-video/README.md new file mode 100644 index 0000000..c52965f --- /dev/null +++ b/python/kontor-video/README.md @@ -0,0 +1,3 @@ +# Kontor Video + +This project provides helper methods to handle video links, like Youtube or ZDF Mediathek. diff --git a/python/kontor-video/kontor_video/__init__.py b/python/kontor-video/kontor_video/__init__.py new file mode 100644 index 0000000..6fd7843 --- /dev/null +++ b/python/kontor-video/kontor_video/__init__.py @@ -0,0 +1,21 @@ +import requests +from bs4 import BeautifulSoup + + +class VideoLink: + + def __init__(self, url: str, log): + self.url = url + self.title = None + self.log = log + + def get_title(self): + try: + r = requests.get(self.url) + soup = BeautifulSoup(r.content, "html.parser") + title = soup.title.string + except: + self.log.info("Sorry, could not retrieve title") + + def download(self, download_dir=None): + self.log.info(f"download {self.url} to {download_dir}") diff --git a/python/kontor-video/pyvenv.cfg b/python/kontor-video/pyvenv.cfg new file mode 100644 index 0000000..e789070 --- /dev/null +++ b/python/kontor-video/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.11.2 +executable = /usr/bin/python3.11 +command = /usr/bin/python -m venv /home/tpeetz/projects/kontor/python/kontor-video diff --git a/python/kontor-video/requirements.txt b/python/kontor-video/requirements.txt new file mode 100644 index 0000000..1f3e778 --- /dev/null +++ b/python/kontor-video/requirements.txt @@ -0,0 +1,2 @@ +beautifulsoup4 +requests diff --git a/python/kontor-video/setup.py b/python/kontor-video/setup.py new file mode 100644 index 0000000..1362fdf --- /dev/null +++ b/python/kontor-video/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup, find_packages +import pathlib + +here = pathlib.Path(__file__).parent.resolve() + +long_description = ( here / "README.md").read_text(encoding="utf-8") + +setup( + name='kontor_video', + version='0.1.0', + description='Helper methods to download videos', + long_description=long_description, + long_description_content_type="text/markdown", + author='Thomas Peetz', + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.11", + ], + install_requires=["beautifulsoup4"], + packages=find_packages(), +) diff --git a/qt/.gitignore b/qt/.gitignore deleted file mode 100644 index 38b154f..0000000 --- a/qt/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -deployment/ -kontor.bin diff --git a/qt/database/__init__.py b/qt/database/__init__.py deleted file mode 100644 index 47f5706..0000000 --- a/qt/database/__init__.py +++ /dev/null @@ -1,212 +0,0 @@ -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/qt/database/base.py b/qt/database/base.py deleted file mode 100644 index 97d2990..0000000 --- a/qt/database/base.py +++ /dev/null @@ -1,18 +0,0 @@ -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/qt/database/comic.py b/qt/database/comic.py deleted file mode 100644 index 49d222f..0000000 --- a/qt/database/comic.py +++ /dev/null @@ -1,130 +0,0 @@ -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/qt/database/media.py deleted file mode 100644 index b5d793b..0000000 --- a/qt/database/media.py +++ /dev/null @@ -1,25 +0,0 @@ -from sqlalchemy import Column, DateTime, Integer, String -from sqlalchemy.dialects.mysql import BIT - -from database.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/qt/database/metadata.py b/qt/database/metadata.py deleted file mode 100644 index 46a6274..0000000 --- a/qt/database/metadata.py +++ /dev/null @@ -1,49 +0,0 @@ -from sqlalchemy import Column, String, ForeignKey, DateTime, Integer, Boolean -from sqlalchemy.dialects.mysql import BIT -from sqlalchemy.orm import relationship - -from database 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/qt/database/tysc.py deleted file mode 100644 index 733d7ed..0000000 --- a/qt/database/tysc.py +++ /dev/null @@ -1,131 +0,0 @@ -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/qt/gui/data_view.py b/qt/gui/data_view.py deleted file mode 100644 index 91c66d2..0000000 --- a/qt/gui/data_view.py +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 4ffdc8c..0000000 --- a/qt/gui/data_view_model.py +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 21dbe92..0000000 --- a/qt/gui/dialogs.py +++ /dev/null @@ -1,106 +0,0 @@ -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/qt/gui/main_window.py b/qt/gui/main_window.py deleted file mode 100644 index 66f2302..0000000 --- a/qt/gui/main_window.py +++ /dev/null @@ -1,137 +0,0 @@ -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 dialogs import ExportKontorDialog, ImportKontorDialog -from model_config import KontorModelConfig -from table_model import KontorTableModel -from media_file_model import MediaFileTableModel - - -class MainWindow(QMainWindow): - - def __init__(self, config): - super().__init__() - - self.tick = QIcon('res/tick.png') - self.cross = QIcon('res/cross.png') - self.import_icon = QIcon("res/application-import.png") - self.export_icon = QIcon("res/application-export.png") - self.circle_icon = QIcon("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(config) - 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(): - 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) - - 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/qt/gui/model_config.py b/qt/gui/model_config.py deleted file mode 100644 index a3f902d..0000000 --- a/qt/gui/model_config.py +++ /dev/null @@ -1,65 +0,0 @@ -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 get_filter(self) -> str: - filter_rule = "" - # print(self.filter["download"].isChecked()) - for column, filter_info in self.filter.items(): - # print(column, filter_info) - if filter_info['widget'].isChecked(): - # print(column, filter_info, filter_rule, len(filter_rule)) - if len(filter_rule) < 1: - filter_rule += "WHERE " - if len(filter_rule) > 8: - filter_rule += " AND " - filter_rule += f"{column} is true" - # 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_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/qt/gui/table_model.py b/qt/gui/table_model.py deleted file mode 100644 index e1ba6ae..0000000 --- a/qt/gui/table_model.py +++ /dev/null @@ -1,108 +0,0 @@ -from datetime import datetime - -from PySide6.QtCore import QAbstractTableModel, QModelIndex -from PySide6.QtGui import Qt - -from gui.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/qt/kontor.py b/qt/kontor.py deleted file mode 100644 index 2eed73b..0000000 --- a/qt/kontor.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -PyQT6 GUI for Kontor -""" -import sys -from pathlib import Path -from platformdirs import PlatformDirs -from PySide6.QtWidgets import QApplication -import yaml - -from gui.main_window import MainWindow - - -if __name__ == '__main__': - app = QApplication(sys.argv) - dirs = PlatformDirs("kontor") - database_config = Path(dirs.user_config_dir, 'database-config.yaml') - with open(database_config, 'rt') as f: - db_config = yaml.safe_load(f.read()) - window = MainWindow(db_config) - window.show() - app.exec() diff --git a/qt/pysidedeploy.spec b/qt/pysidedeploy.spec deleted file mode 100644 index 8e65745..0000000 --- a/qt/pysidedeploy.spec +++ /dev/null @@ -1,98 +0,0 @@ -[app] - -# title of your application -title = kontor - -# project directory. the general assumption is that project_dir is the parent directory -# of input_file -project_dir = /home/tpeetz/projects/kontor/qt - -# source file path -input_file = /home/tpeetz/projects/kontor/qt/kontor.py - -# directory where exec is stored -exec_directory = . - -# path to .pyproject project file -project_file = - -# application icon -icon = /usr/local/lib/python3.11/dist-packages/PySide6/scripts/deploy_lib/pyside_icon.jpg - -[python] - -# python path -python_path = /usr/bin/python3 - -# python packages to install -packages = Nuitka==2.4.8 - -# buildozer = for deploying Android application -android_packages = buildozer==1.5.0,cython==0.29.33 - -[qt] - -# comma separated path to qml files required -# normally all the qml files required by the project are added automatically -qml_files = - -# excluded qml plugin binaries -excluded_qml_plugins = - -# qt modules used. comma separated -modules = Gui,DBus,Core,Widgets - -# qt plugins used by the application -plugins = platformthemes,imageformats,platforms,generic,iconengines,egldeviceintegrations,styles,xcbglintegrations,platforms/darwin,platforminputcontexts,accessiblebridge - -[android] - -# path to pyside wheel -wheel_pyside = - -# path to shiboken wheel -wheel_shiboken = - -# plugins to be copied to libs folder of the packaged application. comma separated -plugins = - -[nuitka] - -# usage description for permissions requested by the app as found in the info.plist file -# of the app bundle -# eg = extra_args = --show-modules --follow-stdlib -macos.permissions = - -# mode of using nuitka. accepts standalone or onefile. default is onefile. -mode = onefile - -# (str) specify any extra nuitka arguments -extra_args = --quiet --noinclude-qt-translations - -[buildozer] - -# build mode -# possible options = [release, debug] -# release creates an aab, while debug creates an apk -mode = debug - -# contrains path to pyside6 and shiboken6 recipe dir -recipe_dir = - -# path to extra qt android jars to be loaded by the application -jars_dir = - -# if empty uses default ndk path downloaded by buildozer -ndk_path = - -# if empty uses default sdk path downloaded by buildozer -sdk_path = - -# other libraries to be loaded. comma separated. -# loaded at app startup -local_libs = - -# architecture of deployed platform -# possible values = ["aarch64", "armv7a", "i686", "x86_64"] -arch = - diff --git a/qt/res/application-export.png b/qt/res/application-export.png deleted file mode 100644 index 555887a28d64bc812c4dfa98a6ff1da1927b7792..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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 diff --git a/qt/res/cross.png b/qt/res/cross.png deleted file mode 100644 index 6b9fa6dd36ee8165272a13dd263f573507c78ca6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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