separate cli and gui application in own python packages. provide database schema as python package.

This commit is contained in:
Thomas Peetz
2025-01-19 23:36:52 +01:00
parent f07c7b74ee
commit ada723dc48
113 changed files with 1224 additions and 1223 deletions
+105
View File
@@ -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/
@@ -1,4 +1,4 @@
# Kontor Change History
# Kontor CLI Change History
## 0.0.1
@@ -1,10 +1,6 @@
#FROM python:3.9-alpine
FROM python:3.11-bookworm
FROM python:3.9-alpine
LABEL MAINTAINER="Thomas Peetz <thomas.peetz@thpeetz.de>"
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
@@ -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:
```
@@ -1,4 +1,4 @@
### Kontor Configuration Settings
### Kontor CLI Configuration Settings
---
kontor:
@@ -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()
@@ -1,6 +1,5 @@
from cement import Controller, ex
from ..database import KontorDB
from kontor_schema import KontorDB
class Database(Controller):
@@ -0,0 +1,4 @@
class KontorCliError(Exception):
"""Generic errors."""
pass
@@ -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)
@@ -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:
@@ -0,0 +1,6 @@
cement==3.0.12
cement[jinja2]
cement[yaml]
cement[colorlog]
mariadb
sqlalchemy
@@ -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]
@@ -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()
@@ -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')
+105
View File
@@ -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/
+5
View File
@@ -0,0 +1,5 @@
# Kontor CLI Change History
## 0.0.1
Initial release.
+10
View File
@@ -0,0 +1,10 @@
FROM python:3.9-alpine
LABEL MAINTAINER="Thomas Peetz <thomas.peetz@thpeetz.de>"
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"]
+1
View File
@@ -0,0 +1 @@
+5
View File
@@ -0,0 +1,5 @@
recursive-include *.py
include setup.cfg
include README.md CHANGELOG.md LICENSE.md
include *.txt
recursive-include kontor/templates *
+31
View File
@@ -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/*
+69
View File
@@ -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
```
@@ -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
@@ -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')
@@ -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)
@@ -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()
+118
View File
@@ -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()
@@ -0,0 +1,4 @@
Example Template (templates/command1.jinja2)
Foo => {{ foo }}
+8
View File
@@ -0,0 +1,8 @@
-r requirements.txt
pytest
pytest-cov
coverage
twine>=1.11.0
setuptools>=38.6.0
wheel>=0.31.0
@@ -4,6 +4,4 @@ cement[yaml]
cement[colorlog]
mariadb
sqlalchemy
PySide6
beautifulsoup4
View File
+28
View File
@@ -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
""",
)
+16
View File
@@ -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()
+36
View File
@@ -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')
+7
View File
@@ -0,0 +1,7 @@
deployment/
kontor.bin
bin/
include/
lib/
lib64/
lib64
View File
@@ -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)
@@ -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')
+43
View File
@@ -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()
+5
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
-e /home/tpeetz/projects/kontor/python/kontor-schema
-e /home/tpeetz/projects/kontor/python/kontor-video
platformdirs
pyyaml
PySide6

Before

Width:  |  Height:  |  Size: 513 B

After

Width:  |  Height:  |  Size: 513 B

Before

Width:  |  Height:  |  Size: 524 B

After

Width:  |  Height:  |  Size: 524 B

Before

Width:  |  Height:  |  Size: 836 B

After

Width:  |  Height:  |  Size: 836 B

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 544 B

Before

Width:  |  Height:  |  Size: 634 B

After

Width:  |  Height:  |  Size: 634 B

+4
View File
@@ -0,0 +1,4 @@
# Schema for Kontor DB
This library contains the schema for the Kontor DB.
@@ -0,0 +1,164 @@
/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */
/* Greenlet object interface */
#ifndef Py_GREENLETOBJECT_H
#define Py_GREENLETOBJECT_H
#include <Python.h>
#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 */
@@ -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()
+5
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
mariadb
sqlalchemy
+23
View File
@@ -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(),
)
+3
View File
@@ -0,0 +1,3 @@
# Kontor Video
This project provides helper methods to handle video links, like Youtube or ZDF Mediathek.
@@ -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}")
+5
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
beautifulsoup4
requests
+23
View File
@@ -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(),
)