Add cli app #53

Merged
tpeetz merged 4 commits from add-cli-app into develop/0.1.0 2025-01-14 12:22:54 +00:00
57 changed files with 1749 additions and 62 deletions
+108
View File
@@ -0,0 +1,108 @@
# 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
# PyCharm
.idea/
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
+5
View File
@@ -0,0 +1,5 @@
# Kontor 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 Tool
## Installation
```
$ pip install -r requirements.txt
$ python setup.py install
```
## Development
This project includes a number of helpers in the `Makefile` to streamline common development tasks.
### Environment Setup
The following demonstrates setting up and working with a development environment:
```
### create a virtualenv for development
$ make virtualenv
$ source env/bin/activate
### run kontor cli application
$ kontor --help
### run pytest / coverage
$ make test
```
### Releasing to PyPi
Before releasing to PyPi, you must configure your login credentials:
**~/.pypirc**:
```
[pypi]
username = YOUR_USERNAME
password = YOUR_PASSWORD
```
Then use the included helper function via the `Makefile`:
```
$ make dist
$ make dist-upload
```
## Deployments
### Docker
Included is a basic `Dockerfile` for building and distributing `Kontor`,
and can be built with the included `make` helper:
```
$ make docker
$ docker run -it kontor --help
```
+46
View File
@@ -0,0 +1,46 @@
### Kontor Configuration Settings
---
kontor:
### Toggle application level debug (does not toggle framework debugging)
# debug: false
### Where external (third-party) plugins are loaded from
# plugin_dir: /var/lib/kontor/plugins/
### Where all plugin configurations are loaded from
# plugin_config_dir: /etc/kontor/plugins.d/
### Where external templates are loaded from
# template_dir: /var/lib/kontor/templates/
### The log handler label
# log_handler: colorlog
### The output handler label
# output_handler: jinja2
### sample foo option
# foo: bar
log.colorlog:
### Where the log file lives (no log file by default)
# file: null
### The level for which to log. One of: info, warning, error, fatal, debug
# level: info
### Whether or not to log to console
# to_console: true
### Whether or not to rotate the log file when it reaches `max_bytes`
# rotate: false
### Max size in bytes that a log file can grow until it is rotated.
# max_bytes: 512000
### The maximum number of log files to maintain when rotating
# max_files: 4
View File
View File
+42
View File
@@ -0,0 +1,42 @@
from PySide6.QtWidgets import QApplication
from cement import Controller, ex
from cement.utils.version import get_version_banner
from ..core.version import get_version
from ..gui.main_window import MainWindow
VERSION_BANNER = """
Kontor CLI Tool %s
%s
""" % (get_version(), get_version_banner())
class CliBase(Controller):
class Meta:
label = 'base'
# text displayed at the top of --help output
description = 'Kontor CLI Tool'
# text displayed at the bottom of --help output
epilog = 'Usage: kontor gui|database'
# controller level arguments. ex: 'kontor --version'
arguments = [
### add a version banner
(['-v', '--version'],
{'action': 'version',
'version': VERSION_BANNER}),
]
def _default(self):
"""Default action if no sub-command is passed."""
self.gui()
@ex(
help='start GUI'
)
def gui(self):
application = QApplication([])
window = MainWindow(self.app.session, self.app.log)
window.show()
application.exec()
+32
View File
@@ -0,0 +1,32 @@
from cement import Controller, ex
from ..database import KontorDB
class Database(Controller):
class Meta:
label = 'database'
stacked_type = 'nested'
stacked_on = 'base'
@ex(
help='export database to given file',
arguments=[
(['-f', '--file'],
{'help': 'file to store database content',
'action': 'store',
'dest': 'db_file'})
],
)
def export(self):
data = {
'db_file': 'data.json',
'export_type': 'JSON',
}
if self.app.pargs.db_file is not None:
data['db_file'] = self.app.pargs.db_file
kontor_db = KontorDB(self.app.session, self.app.log)
table_list = kontor_db.get_table_names()
kontor_db.export_db(data['export_type'], data['db_file'], table_list)
self.app.render(data, 'command1.jinja2')
View File
+4
View File
@@ -0,0 +1,4 @@
class KontorError(Exception):
"""Generic errors."""
pass
+7
View File
@@ -0,0 +1,7 @@
from cement.utils.version import get_version as cement_get_version
VERSION = (0, 0, 1, 'alpha', 0)
def get_version(version=VERSION):
return cement_get_version(version)
+146
View File
@@ -0,0 +1,146 @@
import json
from datetime import datetime
from pathlib import Path
from .base import Base
from .comic import Comic, Artist, Publisher, Issue, StoryArc, TradePaperback, Volume, ComicWork, WorkType
from .metadata import MetaDataTable, MetaDataColumn
from .tysc import Card, CardSet, Sport, Team, FieldPosition, Rooster, Player, Vendor
from .media import MediaFile
class KontorDB:
def __init__(self, db_session, log):
self.session = db_session
self.log = log
self.registry = {}
self.init_registry()
def init_registry(self):
self.registry['card'] = Card
self.registry['card_set'] = CardSet
self.registry['sport'] = Sport
self.registry['team'] = Team
self.registry['field_position'] = FieldPosition
self.registry['rooster'] = Rooster
self.registry['player'] = Player
self.registry['vendor'] = Vendor
self.registry['artist'] = Artist
self.registry['publisher'] = Publisher
self.registry['comic'] = Comic
self.registry['issue'] = Issue
self.registry['story_arc'] = StoryArc
self.registry['trade_paperback'] = TradePaperback
self.registry['volume'] = Volume
self.registry['comic_work'] = ComicWork
self.registry['worktype'] = WorkType
self.registry['media_file'] = MediaFile
def get_table_names(self) -> list:
tables = self.session.query(MetaDataTable).all()
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}
self.log.info(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 export_db(self, export_type: str, export_file_name: str, export_table_list: list):
self.log.info(f"export DB to {export_file_name} as {export_type}")
db = {}
for table in export_table_list:
columns = self.get_column_meta_data(table, view_only=False)
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 = []
self.log.debug(f"found {len(rows)} entries")
self.log.debug(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:
self.log.debug("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 _:
self.log.debug("unknown export type")
if export_file.exists():
self.log.debug(f"{export_file} exists")
+5
View File
@@ -0,0 +1,5 @@
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
+130
View File
@@ -0,0 +1,130 @@
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship
from .base import Base
class Publisher(Base):
__tablename__ = "publisher"
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
name = Column(String(length=255), unique=True)
comics = relationship("Comic")
def __repr__(self):
return f'Publisher({self.id} {self.name})'
def __str__(self):
return self.__repr__()
class Comic(Base):
__tablename__ = 'comic'
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
title = Column(String(length=255), unique=True)
publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False)
publisher = relationship("Publisher", back_populates="comics")
current_order = Column(BIT(1))
completed = Column(BIT(1))
issues = relationship("Issue")
story_arcs = relationship("StoryArc")
trade_paperbacks = relationship("TradePaperback")
volumes = relationship("Volume")
comic_works = relationship("ComicWork")
def __repr__(self):
return f'Comic({self.id} {self.version} {self.title} {self.publisher.name})'
def __str__(self):
return f'{self.title}({self.id})'
class Volume(Base):
__tablename__ = "volume"
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
name = Column(String(length=255), nullable=False)
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
comic = relationship("Comic", back_populates="volumes")
issues = relationship("Issue")
class TradePaperback(Base):
__tablename__ = "trade_paperback"
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
name = Column(String(length=255), nullable=False)
issue_start = Column(Integer)
issue_end = Column(Integer)
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
comic = relationship("Comic", back_populates="trade_paperbacks")
class StoryArc(Base):
__tablename__ = "story_arc"
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
name = Column(String(length=255), nullable=False)
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
comic = relationship("Comic", back_populates="story_arcs")
class Issue(Base):
__tablename__ = "issue"
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
issue_number = Column(String(255))
in_stock = Column(BIT(1))
is_read = Column(BIT(1))
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
comic = relationship("Comic", back_populates="issues")
volume_id = Column(String, ForeignKey("volume.id"), nullable=True)
volume = relationship("Volume", back_populates="issues")
class Artist(Base):
__tablename__ = "artist"
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
name = Column(String(length=255), nullable=False)
comic_works = relationship("ComicWork")
class WorkType(Base):
__tablename__ = "worktype"
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
name = Column(String(length=255), nullable=False, unique=True)
comic_works = relationship("ComicWork")
class ComicWork(Base):
__tablename__ = "comic_work"
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
comic_id = Column(String, ForeignKey("comic.id"), nullable=False)
comic = relationship("Comic", back_populates="comic_works")
artist_id = Column(String, ForeignKey("artist.id"), nullable=False)
artist = relationship("Artist", back_populates="comic_works")
work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False)
work_type = relationship("WorkType", back_populates="comic_works")
+25
View File
@@ -0,0 +1,25 @@
from sqlalchemy import Column, DateTime, Integer, String
from sqlalchemy.dialects.mysql import BIT
from .base import Base
class MediaFile(Base):
__tablename__ = 'media_file'
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
cloud_link = Column(String(255))
file_name = Column(String(255))
path = Column(String(255))
review = Column(BIT(1))
title = Column(String(255))
url = Column(String(255))
should_download = Column(BIT(1))
def __repr__(self):
return f'MediaFile({self.id} {self.title} {self.title})'
def __str__(self):
return f'{self.title}({self.id})'
+50
View File
@@ -0,0 +1,50 @@
from sqlalchemy import Column, String, ForeignKey, DateTime, Integer, Boolean
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship
from .base import Base
class MetaDataTable(Base):
__tablename__ = 'meta_data_table'
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
table_name = Column(String(255), unique=True)
table_columns = relationship("MetaDataColumn")
def __repr__(self):
return f'MetaDataTable({self.id} {self.table_name})'
def __str__(self):
return f'{self.table_name}({self.id})'
class MetaDataColumn(Base):
__tablename__ = 'meta_data_column'
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
column_modifier = Column(String(255), nullable=True)
column_name = Column(String(255))
column_order = Column(Integer)
column_sync_name = Column(String(255))
column_type = Column(String(255))
table_id = Column(String, ForeignKey('meta_data_table.id'))
table = relationship("MetaDataTable", back_populates="table_columns")
column_label = Column(String(255))
filter_label = Column(String(255))
is_shown = Column(BIT(1))
show_filter = Column(BIT(1))
ref_column = Column(String, nullable=True)
def __repr__(self):
if self.column_name is None:
return f'MetaDataColumn({self.id} {self.table.table_name}.__)'
else:
return f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})'
def __str__(self):
return f'{self.column_name}({self.id})'
+131
View File
@@ -0,0 +1,131 @@
from sqlalchemy import Column, DateTime, Integer, String, ForeignKey, UniqueConstraint
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship
from .base import Base
class Sport(Base):
__tablename__ = "sport"
__table_args__ = (
UniqueConstraint("name"),
)
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
name = Column(String(255), nullable=False, index=True, unique=True)
teams = relationship("Team")
positions = relationship("FieldPosition")
class Team(Base):
__tablename__ = "team"
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
name = Column(String(255), nullable=False, index=True, unique=True)
short_name = Column(String(255), nullable=False, )
sport_id = Column(String, ForeignKey("sport.id"), nullable=False)
sport = relationship("Sport", back_populates="teams")
roosters = relationship("Rooster")
class FieldPosition(Base):
__tablename__ = "field_position"
__table_args__ = (
UniqueConstraint("name", "sport_id"),
UniqueConstraint("short_name", "sport_id"),
)
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
name = Column(String(255), nullable=False, index=True)
short_name = Column(String(255), nullable=False)
sport_id = Column(String, ForeignKey("sport.id"), nullable=False, index=True)
sport = relationship("Sport", back_populates="positions")
roosters = relationship("Rooster")
class Player(Base):
__tablename__ = "player"
__table_args__ = (
UniqueConstraint("first_name", "last_name"),
)
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
first_name = Column(String(255), nullable=False, index=True)
last_name = Column(String(255), nullable=False, index=True)
roosters = relationship("Rooster")
def get_full_name(self) -> str:
return f"{self.last_name}, {self.first_name}"
class Rooster(Base):
__tablename__ = "rooster"
__table_args__ = (
UniqueConstraint("year", "team_id", "player_id", "position_id"),
)
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
year = Column(Integer)
team_id = Column(String, ForeignKey("team.id"), nullable=False, index=True)
team = relationship("Team", back_populates="roosters")
player_id = Column(String, ForeignKey("player.id"), nullable=False, index=True)
player = relationship("Player", back_populates="roosters")
position_id = Column(String, ForeignKey("field_position.id"), nullable=False, index=True)
position = relationship("FieldPosition", back_populates="roosters")
cards = relationship("Card")
class Vendor(Base):
__tablename__ = "vendor"
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
name = Column(String(255), nullable=False, unique=True, index=True)
card_sets = relationship("CardSet")
cards = relationship("Card")
class CardSet(Base):
__tablename__ = "card_set"
__table_args__ = (
UniqueConstraint("name", "vendor_id"),
)
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
name = Column(String(255), index=True)
parallel_set = Column(BIT(1))
insert_set = Column(BIT(1))
vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True)
vendor = relationship("Vendor", back_populates="card_sets")
cards = relationship("Card")
class Card(Base):
__tablename__ = "card"
__table_args__ = (
UniqueConstraint("card_number", "year", "vendor_id", "card_set_id"),
)
id = Column(String, primary_key=True)
created_date = Column(DateTime)
last_modified_date = Column(DateTime)
version = Column(Integer)
card_number = Column(Integer, index=True)
year = Column(Integer, index=True)
card_set_id = Column(String, ForeignKey("card_set.id"), nullable=False)
card_set = relationship("CardSet", back_populates="cards")
rooster_id = Column(String, ForeignKey("rooster.id"), nullable=False)
rooster = relationship("Rooster", back_populates="cards")
vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False)
vendor = relationship("Vendor", back_populates="cards")
View File
View File
+12
View File
@@ -0,0 +1,12 @@
from abc import ABC, abstractmethod
class DataViewMeta(ABC):
@abstractmethod
def get_header(self):
pass
class ComicView(DataViewMeta):
def get_header(self):
pass
+32
View File
@@ -0,0 +1,32 @@
from typing import List
from PySide6.QtCore import QModelIndex, QAbstractTableModel
from PySide6.QtGui import Qt
from gui.data_view import DataViewMeta
class DataViewModel(QAbstractTableModel):
def __init__(self):
super().__init__()
self.main_window = None
self._config = None
self._data = List[DataViewMeta]
def rowCount(self, parent=QModelIndex()):
return len(self._data)
def columnCount(self, parent=QModelIndex()):
return 0
def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
return None
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
return None
def setData(self, index, value, role=Qt.ItemDataRole.EditRole):
return False
def flags(self, index):
return None
+106
View File
@@ -0,0 +1,106 @@
from pathlib import Path
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QFileDialog, \
QCheckBox, QComboBox
class ExportKontorDialog(QDialog):
def __init__(self, parent=None, kontor_db=None):
super().__init__(parent)
self.parent = parent
self.kontor_db = kontor_db
self.file_name = "data.json"
self.tables = []
self._table_options = {}
self.export_options = {"JSON": {"ext": ".json"}, "YAML": {"ext": ".yaml"}, "SQLite": {"ext": ".db"}}
self.current_export_type = "JSON"
buttons = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox = QDialogButtonBox(buttons)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
layout = QVBoxLayout()
self.label = QLabel()
self.label.setText("Export DB to data.json")
self.combo_box = QComboBox()
self.combo_box.addItems(["JSON", "YAML", "SQLite"])
self.combo_box.currentTextChanged.connect(self.change_export_type)
file_layout = QHBoxLayout()
file_layout.addWidget(self.label)
file_layout.addWidget(self.combo_box)
file_button = QPushButton("Select file")
file_button.clicked.connect(self.select_file)
file_layout.addWidget(file_button)
layout.addLayout(file_layout)
for table_name in self.kontor_db.get_table_names():
check_box = QCheckBox(table_name)
check_box.setChecked(True)
self.tables.append(table_name)
self._table_options[table_name] = check_box
check_box.stateChanged.connect(self.change_selection)
layout.addWidget(check_box)
layout.addWidget(self.buttonBox)
self.setLayout(layout)
def change_selection(self):
self.tables.clear()
for (name, box) in self._table_options.items():
if box.isChecked():
self.tables.append(name)
def change_export_type(self, text):
self.current_export_type = text
self.label.setText(f'Export DB to data.{self.export_options[text]["ext"]}')
def select_file(self):
file_dialog = QFileDialog()
file_dialog.setFileMode(QFileDialog.FileMode.AnyFile)
file_dialog.setDefaultSuffix(self.export_options[self.current_export_type]["ext"])
file_dialog.setNameFilter(f'*{self.export_options[self.current_export_type]["ext"]}')
if file_dialog.exec():
self.file_name = file_dialog.selectedFiles()[0]
export_file = Path(self.file_name)
self.file_name = export_file.with_suffix(self.export_options[self.current_export_type]["ext"])
self.label.setText(f"Export DB to {self.file_name}")
def get_tables_to_export(self) -> list:
return self.tables
class ImportKontorDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.file_name = None
QBtn = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.label = QLabel()
self.label.setText("Import DB from data.json")
layout = QVBoxLayout()
file_layout = QHBoxLayout()
file_layout.addWidget(self.label)
file_button = QPushButton("Select file")
file_button.clicked.connect(self.select_file)
file_layout.addWidget(file_button)
layout.addLayout(file_layout)
layout.addWidget(self.buttonBox)
self.setLayout(layout)
def select_file(self):
file_dialog = QFileDialog()
file_dialog.setFileMode(QFileDialog.FileMode.ExistingFile)
if file_dialog.exec():
self.file_name = file_dialog.selectedFiles()[0]
self.label.setText(f"Import DB from {self.file_name}")
+139
View File
@@ -0,0 +1,139 @@
from PySide6.QtGui import QAction, QIcon
from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView
from PySide6.QtWidgets import QLabel, QMainWindow
from ..database import KontorDB
from ..database.media import MediaFile
from ..database.comic import Comic
from .dialogs import ExportKontorDialog, ImportKontorDialog
from .model_config import KontorModelConfig
from .table_model import KontorTableModel
class MainWindow(QMainWindow):
def __init__(self, session, log):
super().__init__()
self.tick = QIcon('kontor/res/tick.png')
self.cross = QIcon('kontor/res/cross.png')
self.import_icon = QIcon("kontor/res/application-import.png")
self.export_icon = QIcon("kontor/res/application-export.png")
self.circle_icon = QIcon("kontor/res/arrow-circle-double.png")
self.setWindowTitle("Kontor")
self.setMinimumSize(800, 500)
self._create_actions()
self._create_menubar()
self._create_toolbars()
self._create_statusbar()
self.data = []
self.filter = {}
self.kontor_db = KontorDB(session, log)
self.log = log
self.central_widget = QWidget()
parent_layout = QVBoxLayout()
self.central_widget.setLayout(parent_layout)
self.tabs = QTabWidget()
self.tabs.addTab(self.generate_data_tab("comic", Comic), "Comics")
self.tabs.addTab(self.generate_data_tab("media_file", MediaFile), "MediaFile")
self.tabs.currentChanged.connect(self._tab_changed)
#label.setAlignment(Qt.AlignmentFlag.AlignCenter)
parent_layout.addWidget(self.tabs)
self.setCentralWidget(self.central_widget)
def _create_actions(self):
self.newAction = QAction("&New", self)
self.aboutAction = QAction("&Über...", self)
self.aboutAction.triggered.connect(self.about)
self.importAction = QAction(self.import_icon, "&Import", self)
self.importAction.triggered.connect(self.import_from_file)
self.exportAction = QAction(self.export_icon, "&Export", self)
self.exportAction.triggered.connect(self.export_to_file)
self.refreshAction = QAction(self.circle_icon, "&Refresh", self)
self.refreshAction.triggered.connect(self.refresh)
self.updateTitleAction = QAction("&Update Titles", self)
self.downloadAction = QAction("&Download Videos", self)
self.exitAction = QAction("&Beenden", self)
self.exitAction.setShortcut("Alt+F4")
self.exitAction.triggered.connect(self.close)
def _create_menubar(self):
menu_bar = self.menuBar()
# File menu
file_menu = QMenu("&Datei")
menu_bar.addMenu(file_menu)
file_menu.addAction(self.exitAction)
# Kontor menu
kontor_menu = QMenu("&Kontor")
menu_bar.addMenu(kontor_menu)
kontor_menu.addAction(self.importAction)
kontor_menu.addAction(self.exportAction)
comic_menu = QMenu("&Comic")
tysc_menu = QMenu("&TradeYourSportCards")
media_file_menu = QMenu("&MediaFile")
media_file_menu.addAction(self.updateTitleAction)
media_file_menu.addAction(self.downloadAction)
kontor_menu.addMenu(comic_menu)
kontor_menu.addMenu(tysc_menu)
kontor_menu.addMenu(media_file_menu)
# Help menu
help_menu = QMenu("&Hilfe")
menu_bar.addMenu(help_menu)
help_menu.addAction(self.aboutAction)
def _create_toolbars(self):
# Kontor toolbar
kontor_tool_bar = self.addToolBar("Kontor")
kontor_tool_bar.addAction(self.importAction)
kontor_tool_bar.addAction(self.exportAction)
kontor_tool_bar.addAction(self.refreshAction)
def _create_statusbar(self):
self.statusBar = self.statusBar()
self.statusBar.showMessage("Kontor ready", 6000)
self.status_label = QLabel("")
self.statusBar.addPermanentWidget(self.status_label)
def about(self):
QMessageBox.about(self.central_widget, "Über Kontor", f"Python: 3.11\nKontor: 0.1.0")
def import_from_file(self):
import_dlg = ImportKontorDialog(self)
if import_dlg.exec():
print(f"import DB from file {import_dlg.file_name}")
else:
print("no nothing for import")
pass
def export_to_file(self):
export_dlg = ExportKontorDialog(self, self.kontor_db)
if export_dlg.exec():
self.log.info(export_dlg.get_tables_to_export())
self.log.info(f"export DB to {export_dlg.file_name}")
self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000)
self.kontor_db.export_db(export_dlg.current_export_type, export_dlg.file_name, export_dlg.get_tables_to_export())
else:
self.statusBar.showMessage("Export cancelled", 3000)
def refresh(self):
self.data[self.tabs.currentIndex()].refresh()
def _tab_changed(self, tab_index):
self.data[tab_index].refresh()
def generate_data_tab(self, table_name, table):
data_tab = QWidget()
table_config = KontorModelConfig(self.kontor_db, self, table_name, table)
model = KontorTableModel(table_config)
layout = QVBoxLayout()
self.data.append(model)
data_tab.setLayout(layout)
table_view = QTableView()
table_view.setModel(model)
layout.addLayout(table_config.get_filter_layout())
layout.addWidget(table_view)
model.refresh()
return data_tab
+50
View File
@@ -0,0 +1,50 @@
import mariadb
from PySide6.QtWidgets import QHBoxLayout, QCheckBox
from ..database import KontorDB
class KontorModelConfig:
def __init__(self, kontor_db: KontorDB, main_window, table_name: str, table):
self.header = {}
self.filter = {}
self.main_window = main_window
self._table_name = table_name
self._table = table
self.kontor_db = kontor_db
self.get_table_config()
def get_table_config(self):
self.header = self.kontor_db.get_column_meta_data(self._table_name)
self.filter = self.kontor_db.get_filters(self._table_name)
def filters(self) -> dict:
_filters = {}
# print(self.filter["download"].isChecked())
for column, filter_info in self.filter.items():
# print(column, filter_info)
if filter_info['widget'].isChecked():
_filters[column] = True
# print(f"{filter_rule=}")
return _filters
def get_data(self) -> list:
# data = self.kontor_db.get_data(self._table_name, self.header, self.get_filter())
# data.clear()
data = self.kontor_db.data(self._table, self.header, self.filters())
# print(f"KontorModelConfig.get_data: {len(data)}")
# comics = self.kontor_db.session.query(Comic).all()
# print(f'{len(comics)} Comics loaded')
return data
def get_filter_layout(self) -> QHBoxLayout:
filter_layout = QHBoxLayout()
for column, filter_info in self.filter.items():
filter_checkbox = QCheckBox()
filter_checkbox.setText(filter_info['label'])
filter_checkbox.checkStateChanged.connect(self.main_window.refresh)
self.filter[column]['widget'] = filter_checkbox
filter_layout.addWidget(filter_checkbox)
filter_layout.addStretch()
return filter_layout
+108
View File
@@ -0,0 +1,108 @@
from datetime import datetime
from PySide6.QtCore import QAbstractTableModel, QModelIndex
from PySide6.QtGui import Qt
from .model_config import KontorModelConfig
class KontorTableModel(QAbstractTableModel):
def __init__(self, model_config: KontorModelConfig):
super().__init__()
self._main_window = model_config.main_window
self._config = model_config
self._data = []
def refresh(self):
data = self._config.get_data()
count = 0
# print(data)
if data is not None:
self.beginResetModel()
self._data.clear()
self._data = data
self.endResetModel()
count = len(data)
# print(data)
# print(self._data)
self.layoutChanged.emit()
self._main_window.statusBar.showMessage(f"{count} Einträge geladen", 3000)
def rowCount(self, parent=QModelIndex()):
# The length of the outer list.
if self._data is None:
return 0
return len(self._data)
def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole):
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
return self._config.header[col]['label']
if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole:
return str(col+1)
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
if self._data is None:
return None
value = self._data[index.row()][index.column()]
# print('{}:: {}:: {}: {}'.format(index, role, value, type(value)))
if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole:
if isinstance(value, datetime):
return value.strftime("%Y-%m-%d %M:%M:%S")
if isinstance(value, str):
return value
if isinstance(value, bytes):
if value == b'\x01':
return self._main_window.tick
else:
return self._main_window.cross
if isinstance(value, int):
# print('{}:: {}: {}'.format(index, value, type(value)))
if value == 1:
return self._main_window.tick
else:
return self._main_window.cross
if isinstance(value, bool):
if value:
return self._main_window.tick
else:
return self._main_window.cross
return str(value)
if role == Qt.ItemDataRole.DecorationRole:
if isinstance(value, bytes):
if value == b'\x01':
return self._main_window.tick
else:
return self._main_window.cross
if isinstance(value, int):
if value == 1:
return self._main_window.tick
else:
return self._main_window.cross
if isinstance(value, bool):
if value:
return self._main_window.tick
else:
return self._main_window.cross
def columnCount(self, index=QModelIndex()):
# The following takes the first sub-list, and returns
# the length (only works if all rows are an equal length)
# print(f"Header count: {len(self._config.get_header())}")
return len(self._config.header)
def setData(self, index, value, role: int) -> bool:
print(index, role)
if role == Qt.ItemDataRole.EditRole:
self._data[index.row()][index.column()] = value
print(self._data[index.row()][index.column()])
self.dataChanged.emit(index, index)
return True
if role == Qt.ItemDataRole.CheckStateRole:
print("role == Qt.ItemDataRole.CheckStateRole")
checked = value == Qt.CheckState.Checked
self._data[index.row()][index.column()] = checked
return False
def flags(self, index):
return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate
+119
View File
@@ -0,0 +1,119 @@
from cement import App, TestApp, init_defaults
from cement.core.exc import CaughtSignal
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from .core.exc import KontorError
from .database.base import Base
from .controllers.clibase import CliBase
from .controllers.database import Database
# configuration defaults
CONFIG = init_defaults('kontor', 'mariadb')
CONFIG['kontor']['foo'] = 'bar'
CONFIG['mariadb']['user'] = 'kontor'
CONFIG['mariadb']['password'] = 'kontor'
CONFIG['mariadb']['host'] = '127.0.0.1'
CONFIG['mariadb']['port'] = '3306'
CONFIG['mariadb']['database'] = 'kontor'
def extend_sqlalchemy(app):
app.log.info('extending kontor application with sqlalchemy')
connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format(
app.config.get('mariadb', 'user'),
app.config.get('mariadb', 'password'),
app.config.get('mariadb', 'host'),
app.config.get('mariadb', 'port'),
app.config.get('mariadb', 'database')
))
# engine = create_engine(connect_string, echo=True)
engine = create_engine(connect_string)
Base.metadata.create_all(bind=engine)
__session__ = sessionmaker(bind=engine)
app.extend('session', __session__())
def close_session(app):
app.log.info('close session')
app.session.close()
class Kontor(App):
"""Kontor primary application."""
class Meta:
label = 'kontor'
# configuration defaults
config_defaults = CONFIG
# call sys.exit() on close
exit_on_close = True
# load additional framework extensions
extensions = [
'yaml',
'colorlog',
'jinja2',
]
# configuration handler
config_handler = 'yaml'
# configuration file suffix
config_file_suffix = '.yml'
# set the log handler
log_handler = 'colorlog'
# set the output handler
output_handler = 'jinja2'
hooks = [
('post_setup', extend_sqlalchemy),
('pre_close', close_session),
]
# register handlers
handlers = [
CliBase,
Database,
]
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()
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

View File
+4
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
cement==3.0.12
cement[jinja2]
cement[yaml]
cement[colorlog]
mariadb
sqlalchemy
PySide6
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 Tool',
long_description=LONG_DESCRIPTION,
long_description_content_type='text/markdown',
author='Thomas Peetz',
author_email='thomas.peetz@thpeetz.de',
url='https://gitlab.com/tpeetz/kontor',
license='MIT',
packages=find_packages(exclude=['ez_setup', 'tests*']),
package_data={'kontor': ['templates/*']},
include_package_data=True,
entry_points="""
[console_scripts]
kontor = kontor.main:main
""",
)
+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')
+129 -22
View File
@@ -1,10 +1,16 @@
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 database.base import Base
from database.comic import Comic
from database.metadata import MetaDataTable, MetaDataColumn
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:
@@ -29,36 +35,91 @@ class KontorDB:
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_id(self, table_name):
result = self.session.execute(select(MetaDataTable.id).where(MetaDataTable.table_name == table_name)).scalar()
return result
def get_table_names(self) -> list:
tables = self.session.query(MetaDataTable).all()
result = [table.table_name for table in tables]
return result
def get_column_meta_data(self, table_id: str, table_name: str) -> dict:
def get_column_meta_data(self, table_name: str, view_only=True) -> dict:
meta_data = {}
order = 0
for (_, column) in self.session.query(MetaDataTable, MetaDataColumn).filter(MetaDataTable.id == MetaDataColumn.table_id).filter(MetaDataTable.table_name == table_name).filter(MetaDataColumn.is_shown == 1).all():
meta_data[order] = {'column': column.column_name, 'label': column.column_label, 'order': column.column_order, 'ref_column': column.ref_column}
order += 1
if view_only:
for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn).
filter(MetaDataTable.id == MetaDataColumn.table_id).
filter(MetaDataTable.table_name == table_name).
filter(MetaDataColumn.is_shown == 1).all()):
meta_data[order] = {'column': column.column_name, 'label': column.column_label,
'order': column.column_order, 'ref_column': column.ref_column}
order += 1
else:
for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn).
filter(MetaDataTable.id == MetaDataColumn.table_id).
filter(MetaDataTable.table_name == table_name).all()):
meta_data[order] = {
'column': column.column_name,
'order': column.column_order,
'ref_column': column.ref_column
}
order += 1
return meta_data
def get_filters(self, table_id):
cursor = self.db_conn.cursor()
filters = {}
cursor.execute(
"SELECT column_name, filter_label from meta_data_column WHERE table_id=? AND show_filter is true",
(table_id,))
rows = cursor.fetchall()
for row in rows:
filters[row[0]] = {'label': row[1], 'widget': None}
cursor.close()
# print(f"retrieved {len(rows)} filters: {filters}")
return filters
def get_filters(self, table_name):
_filter_map = {}
for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn).
filter(MetaDataTable.id == MetaDataColumn.table_id).
filter(MetaDataTable.table_name == table_name).
filter(MetaDataColumn.show_filter == 1).all()):
_filter_map[column.column_name] = {'label': column.filter_label, 'widget': None}
print(f"retrieved {len(_filter_map)} filters: {_filter_map}")
return _filter_map
def data(self, table, columns: dict, filters) -> list:
data = []
entries = []
if len(filters) == 0:
entries = self.session.query(table).all()
else:
entries = self.session.query(table).filter_by(**filters)
for entry in entries:
row = []
for order in columns.keys():
column_name = columns[order]['column']
if str(column_name).endswith("_id"):
ref_table = column_name[:-3]
# print(f"{ref_table=}")
ref = getattr(entry, ref_table)
value = getattr(ref, "name")
# print(f"{value=}")
row.append(value)
else:
row.append(getattr(entry, column_name))
# print(repr(row))
data.append(row)
return data
def get_data(self, table_name: str, columns: dict, where_clause: str) -> list:
data = []
@@ -103,3 +164,49 @@ class KontorDB:
statement = f"SELECT {columns} FROM {table} {where_clause}"
print(f"{statement=}")
return statement
def export_db(self, export_type: str, export_file_name: str, export_table_list: list):
print(f"export DB to {export_file_name} as {export_type}")
db = {}
for table in export_table_list:
columns = self.get_column_meta_data(table, view_only=False)
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")
+13 -2
View File
@@ -1,7 +1,18 @@
from sqlalchemy import Column, String, DateTime, Integer
from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker
from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker, declarative_base
class Base(DeclarativeBase):
pass
# class 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)
+5 -5
View File
@@ -1,8 +1,8 @@
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship
from database.base import Base
from .base import Base
class Publisher(Base):
@@ -106,7 +106,7 @@ class Artist(Base):
comic_works = relationship("ComicWork")
class Worktype(Base):
class WorkType(Base):
__tablename__ = "worktype"
id = Column(String, primary_key=True)
created_date = Column(DateTime)
@@ -126,5 +126,5 @@ class ComicWork(Base):
comic = relationship("Comic", back_populates="comic_works")
artist_id = Column(String, ForeignKey("artist.id"), nullable=False)
artist = relationship("Artist", back_populates="comic_works")
worktype_id = Column(String, ForeignKey("worktype.id"), nullable=False)
worktype = relationship("Worktype", back_populates="comic_works")
work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False)
work_type = relationship("WorkType", back_populates="comic_works")
+3 -3
View File
@@ -1,4 +1,4 @@
from sqlalchemy import Boolean, Column, DateTime, Integer, String
from sqlalchemy import Column, DateTime, Integer, String
from sqlalchemy.dialects.mysql import BIT
from database.base import Base
@@ -13,10 +13,10 @@ class MediaFile(Base):
cloud_link = Column(String(255))
file_name = Column(String(255))
path = Column(String(255))
review = Column(BIT(1), default=True)
review = Column(BIT(1))
title = Column(String(255))
url = Column(String(255))
should_download = Column(Boolean, default=True)
should_download = Column(BIT(1))
def __repr__(self):
return f'MediaFile({self.id} {self.title} {self.title})'
+6 -6
View File
@@ -1,8 +1,8 @@
from sqlalchemy import Boolean, Column, DateTime, Integer, String, ForeignKey, UniqueConstraint
from sqlalchemy import Column, DateTime, Integer, String, ForeignKey, UniqueConstraint
from sqlalchemy.dialects.mysql import BIT
from sqlalchemy.orm import relationship
from database.base import Base
from .base import Base
class Sport(Base):
@@ -28,7 +28,7 @@ class Team(Base):
name = Column(String(255), nullable=False, index=True, unique=True)
short_name = Column(String(255), nullable=False, )
sport_id = Column(String, ForeignKey("sport.id"), nullable=False)
sport = relationship("Sport", back_populates="positions")
sport = relationship("Sport", back_populates="teams")
roosters = relationship("Rooster")
@@ -81,7 +81,7 @@ class Rooster(Base):
player_id = Column(String, ForeignKey("player.id"), nullable=False, index=True)
player = relationship("Player", back_populates="roosters")
position_id = Column(String, ForeignKey("field_position.id"), nullable=False, index=True)
position = relationship("roosters")
position = relationship("FieldPosition", back_populates="roosters")
cards = relationship("Card")
class Vendor(Base):
@@ -124,8 +124,8 @@ class Card(Base):
card_number = Column(Integer, index=True)
year = Column(Integer, index=True)
card_set_id = Column(String, ForeignKey("card_set.id"), nullable=False)
card_set = relationship("cards")
card_set = relationship("CardSet", back_populates="cards")
rooster_id = Column(String, ForeignKey("rooster.id"), nullable=False)
rooster = relationship("cards")
rooster = relationship("Rooster", back_populates="cards")
vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False)
vendor = relationship("Vendor", back_populates="cards")
+12
View File
@@ -0,0 +1,12 @@
from abc import ABC, abstractmethod
class DataViewMeta(ABC):
@abstractmethod
def get_header(self):
pass
class ComicView(DataViewMeta):
def get_header(self):
pass
+32
View File
@@ -0,0 +1,32 @@
from typing import List
from PySide6.QtCore import QModelIndex, QAbstractTableModel
from PySide6.QtGui import Qt
from gui.data_view import DataViewMeta
class DataViewModel(QAbstractTableModel):
def __init__(self):
super().__init__()
self.main_window = None
self._config = None
self._data = List[DataViewMeta]
def rowCount(self, parent=QModelIndex()):
return len(self._data)
def columnCount(self, parent=QModelIndex()):
return 0
def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
return None
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
return None
def setData(self, index, value, role=Qt.ItemDataRole.EditRole):
return False
def flags(self, index):
return None
+2 -2
View File
@@ -1,7 +1,7 @@
from pathlib import Path
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QFileDialog, \
QGroupBox, QCheckBox, QComboBox
QCheckBox, QComboBox
class ExportKontorDialog(QDialog):
@@ -10,7 +10,7 @@ class ExportKontorDialog(QDialog):
self.parent = parent
self.kontor_db = kontor_db
self.file_name = None
self.file_name = "data.json"
self.tables = []
self._table_options = {}
+7 -4
View File
@@ -3,6 +3,8 @@ from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidg
from PySide6.QtWidgets import QLabel, QMainWindow
from database import KontorDB
from database.media import MediaFile
from database.comic import Comic
from gui.dialogs import ExportKontorDialog, ImportKontorDialog
from gui.model_config import KontorModelConfig
from gui.table_model import KontorTableModel
@@ -33,8 +35,8 @@ class MainWindow(QMainWindow):
parent_layout = QVBoxLayout()
self.central_widget.setLayout(parent_layout)
self.tabs = QTabWidget()
self.tabs.addTab(self.generate_data_tab("comic"), "Comics")
self.tabs.addTab(self.generate_data_tab("media_file"), "MediaFile")
self.tabs.addTab(self.generate_data_tab("comic", Comic), "Comics")
self.tabs.addTab(self.generate_data_tab("media_file", MediaFile), "MediaFile")
self.tabs.currentChanged.connect(self._tab_changed)
#label.setAlignment(Qt.AlignmentFlag.AlignCenter)
parent_layout.addWidget(self.tabs)
@@ -111,6 +113,7 @@ class MainWindow(QMainWindow):
print(export_dlg.get_tables_to_export())
print(f"export DB to {export_dlg.file_name}")
self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000)
self.kontor_db.export_db(export_dlg.current_export_type, export_dlg.file_name, export_dlg.get_tables_to_export())
else:
self.statusBar.showMessage("Export cancelled", 3000)
@@ -120,9 +123,9 @@ class MainWindow(QMainWindow):
def _tab_changed(self, tab_index):
self.data[tab_index].refresh()
def generate_data_tab(self, table_name):
def generate_data_tab(self, table_name, table):
data_tab = QWidget()
table_config = KontorModelConfig(self.kontor_db, self, table_name)
table_config = KontorModelConfig(self.kontor_db, self, table_name, table)
model = KontorTableModel(table_config)
layout = QVBoxLayout()
self.data.append(model)
+18 -13
View File
@@ -6,25 +6,18 @@ from database import KontorDB
class KontorModelConfig:
def __init__(self, kontor_db: KontorDB, main_window, table_name: str):
def __init__(self, kontor_db: KontorDB, main_window, table_name: str, table):
self.header = {}
self.filter = {}
self.main_window = main_window
self._table = table_name
self._table_id = None
self._table_name = table_name
self._table = table
self.kontor_db = kontor_db
self.get_table_config()
def get_table_id(self):
if self._table_id is not None:
return
self._table_id = self.kontor_db.get_table_id(self._table)
def get_table_config(self):
if self._table_id is None:
self.get_table_id()
self.header = self.kontor_db.get_column_meta_data(self._table_id, self._table)
self.filter = self.kontor_db.get_filters(self._table_id)
self.header = self.kontor_db.get_column_meta_data(self._table_name)
self.filter = self.kontor_db.get_filters(self._table_name)
def get_filter(self) -> str:
filter_rule = ""
@@ -41,8 +34,20 @@ class KontorModelConfig:
# print(f"{filter_rule=}")
return filter_rule
def filters(self) -> dict:
_filters = {}
# print(self.filter["download"].isChecked())
for column, filter_info in self.filter.items():
# print(column, filter_info)
if filter_info['widget'].isChecked():
_filters[column] = True
# print(f"{filter_rule=}")
return _filters
def get_data(self) -> list:
data = self.kontor_db.get_data(self._table, self.header, self.get_filter())
# data = self.kontor_db.get_data(self._table_name, self.header, self.get_filter())
# data.clear()
data = self.kontor_db.data(self._table, self.header, self.filters())
# print(f"KontorModelConfig.get_data: {len(data)}")
# comics = self.kontor_db.session.query(Comic).all()
# print(f'{len(comics)} Comics loaded')
+10 -5
View File
@@ -45,8 +45,8 @@ class KontorTableModel(QAbstractTableModel):
if self._data is None:
return None
value = self._data[index.row()][index.column()]
if role == Qt.ItemDataRole.DisplayRole:
# print('{}: {}'.format(value, type(value)))
# print('{}:: {}:: {}: {}'.format(index, role, value, type(value)))
if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole:
if isinstance(value, datetime):
return value.strftime("%Y-%m-%d %M:%M:%S")
if isinstance(value, str):
@@ -57,6 +57,7 @@ class KontorTableModel(QAbstractTableModel):
else:
return self._main_window.cross
if isinstance(value, int):
# print('{}:: {}: {}'.format(index, value, type(value)))
if value == 1:
return self._main_window.tick
else:
@@ -69,7 +70,6 @@ class KontorTableModel(QAbstractTableModel):
return str(value)
if role == Qt.ItemDataRole.DecorationRole:
if isinstance(value, bytes):
# print('{}: {}'.format(value, type(value)))
if value == b'\x01':
return self._main_window.tick
else:
@@ -91,13 +91,18 @@ class KontorTableModel(QAbstractTableModel):
# print(f"Header count: {len(self._config.get_header())}")
return len(self._config.header)
def setData(self, index, value, role=Qt.ItemDataRole.EditRole):
def setData(self, index, value, role: int) -> bool:
print(index, role)
if role == Qt.ItemDataRole.EditRole:
self._data[index.row()][index.column()] = value
print(self._data[index.row()][index.column()])
self.dataChanged.emit(index, index)
return True
if role == Qt.ItemDataRole.CheckStateRole:
print("role == Qt.ItemDataRole.CheckStateRole")
checked = value == Qt.CheckState.Checked
self._data[index.row()][index.column()] = checked
return True
return False
def flags(self, index):
return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate
Executable → Regular
View File