add cli app and fix relationship typos
This commit is contained in:
committed by
Thomas Peetz
parent
89b7b87b8c
commit
54bc17ee7d
@@ -0,0 +1,2 @@
|
||||
deployment/
|
||||
kontor.bin
|
||||
@@ -0,0 +1,212 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import mariadb
|
||||
from sqlalchemy import create_engine, select, text, MetaData, join
|
||||
from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker
|
||||
|
||||
from .base import Base
|
||||
from .comic import Comic, Artist, Publisher, ComicWork, WorkType, StoryArc, Volume, Issue, TradePaperback
|
||||
from .tysc import Sport, Team, Card, CardSet, Vendor, Rooster, Player, FieldPosition
|
||||
from .media import MediaFile
|
||||
from .metadata import MetaDataTable, MetaDataColumn
|
||||
|
||||
|
||||
class KontorDB:
|
||||
|
||||
def __init__(self, db_config):
|
||||
self.db_conn = mariadb.connect(
|
||||
host=db_config['mariadb']['host'],
|
||||
port=db_config['mariadb']['port'],
|
||||
user=db_config['mariadb']['user'],
|
||||
password=db_config['mariadb']['password'],
|
||||
database=db_config['mariadb']['database']
|
||||
)
|
||||
connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'
|
||||
.format(
|
||||
db_config['mariadb']['user'],
|
||||
db_config['mariadb']['password'],
|
||||
db_config['mariadb']['host'],
|
||||
db_config['mariadb']['port'],
|
||||
db_config['mariadb']['database']))
|
||||
# engine = create_engine(connect_string, echo=True)
|
||||
engine = create_engine(connect_string)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
__session__ = sessionmaker(bind=engine)
|
||||
self.session = __session__()
|
||||
self.registry = {}
|
||||
self.init_registry()
|
||||
|
||||
def init_registry(self):
|
||||
self.registry['card'] = Card
|
||||
self.registry['card_set'] = CardSet
|
||||
self.registry['sport'] = Sport
|
||||
self.registry['team'] = Team
|
||||
self.registry['field_position'] = FieldPosition
|
||||
self.registry['rooster'] = Rooster
|
||||
self.registry['player'] = Player
|
||||
self.registry['vendor'] = Vendor
|
||||
self.registry['artist'] = Artist
|
||||
self.registry['publisher'] = Publisher
|
||||
self.registry['comic'] = Comic
|
||||
self.registry['issue'] = Issue
|
||||
self.registry['story_arc'] = StoryArc
|
||||
self.registry['trade_paperback'] = TradePaperback
|
||||
self.registry['volume'] = Volume
|
||||
self.registry['comic_work'] = ComicWork
|
||||
self.registry['worktype'] = WorkType
|
||||
self.registry['media_file'] = MediaFile
|
||||
|
||||
|
||||
def get_table_names(self) -> list:
|
||||
tables = self.session.query(MetaDataTable).all()
|
||||
result = [table.table_name for table in tables]
|
||||
return result
|
||||
|
||||
def get_column_meta_data(self, table_name: str, view_only=True) -> dict:
|
||||
meta_data = {}
|
||||
order = 0
|
||||
if view_only:
|
||||
for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn).
|
||||
filter(MetaDataTable.id == MetaDataColumn.table_id).
|
||||
filter(MetaDataTable.table_name == table_name).
|
||||
filter(MetaDataColumn.is_shown == 1).all()):
|
||||
meta_data[order] = {'column': column.column_name, 'label': column.column_label,
|
||||
'order': column.column_order, 'ref_column': column.ref_column}
|
||||
order += 1
|
||||
else:
|
||||
for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn).
|
||||
filter(MetaDataTable.id == MetaDataColumn.table_id).
|
||||
filter(MetaDataTable.table_name == table_name).all()):
|
||||
meta_data[order] = {
|
||||
'column': column.column_name,
|
||||
'order': column.column_order,
|
||||
'ref_column': column.ref_column
|
||||
}
|
||||
order += 1
|
||||
return meta_data
|
||||
|
||||
def get_filters(self, table_name):
|
||||
_filter_map = {}
|
||||
for (_, column) in (self.session.query(MetaDataTable, MetaDataColumn).
|
||||
filter(MetaDataTable.id == MetaDataColumn.table_id).
|
||||
filter(MetaDataTable.table_name == table_name).
|
||||
filter(MetaDataColumn.show_filter == 1).all()):
|
||||
_filter_map[column.column_name] = {'label': column.filter_label, 'widget': None}
|
||||
print(f"retrieved {len(_filter_map)} filters: {_filter_map}")
|
||||
return _filter_map
|
||||
|
||||
def data(self, table, columns: dict, filters) -> list:
|
||||
data = []
|
||||
entries = []
|
||||
if len(filters) == 0:
|
||||
entries = self.session.query(table).all()
|
||||
else:
|
||||
entries = self.session.query(table).filter_by(**filters)
|
||||
for entry in entries:
|
||||
row = []
|
||||
for order in columns.keys():
|
||||
column_name = columns[order]['column']
|
||||
if str(column_name).endswith("_id"):
|
||||
ref_table = column_name[:-3]
|
||||
# print(f"{ref_table=}")
|
||||
ref = getattr(entry, ref_table)
|
||||
value = getattr(ref, "name")
|
||||
# print(f"{value=}")
|
||||
row.append(value)
|
||||
else:
|
||||
row.append(getattr(entry, column_name))
|
||||
# print(repr(row))
|
||||
data.append(row)
|
||||
return data
|
||||
|
||||
def get_data(self, table_name: str, columns: dict, where_clause: str) -> list:
|
||||
data = []
|
||||
cursor = self.db_conn.cursor()
|
||||
cursor.execute(self.get_statement(table_name, columns, where_clause))
|
||||
rows = cursor.fetchall()
|
||||
print(len(rows))
|
||||
for row in rows:
|
||||
# print(f"KontorDB.get_data: {row}")
|
||||
data.append(list(row))
|
||||
cursor.close()
|
||||
# print(f"KontorDB.getData: return {len(data)}")
|
||||
if table_name == 'comic' and len(where_clause) == 0:
|
||||
data.clear()
|
||||
comics = self.session.query(Comic).all()
|
||||
for item in comics:
|
||||
# print(item)
|
||||
row = []
|
||||
for order in columns.keys():
|
||||
column_name = columns[order]['column']
|
||||
if str(column_name).endswith("_id"):
|
||||
ref_table = column_name[:-3]
|
||||
# print(f"{ref_table=}")
|
||||
ref = getattr(item, ref_table)
|
||||
value = getattr(ref, "name")
|
||||
# print(f"{value=}")
|
||||
row.append(value)
|
||||
else:
|
||||
row.append(getattr(item, column_name))
|
||||
# print(repr(row))
|
||||
data.append(row)
|
||||
return data
|
||||
|
||||
def get_statement(self, table: str, header: dict, where_clause):
|
||||
columns = ""
|
||||
for index, column in header.items():
|
||||
if index > 0:
|
||||
columns += ", "
|
||||
columns += column['column']
|
||||
if len(columns) == 0:
|
||||
columns = "*"
|
||||
statement = f"SELECT {columns} FROM {table} {where_clause}"
|
||||
print(f"{statement=}")
|
||||
return statement
|
||||
|
||||
def export_db(self, export_type: str, export_file_name: str, export_table_list: list):
|
||||
print(f"export DB to {export_file_name} as {export_type}")
|
||||
db = {}
|
||||
for table in export_table_list:
|
||||
columns = self.get_column_meta_data(table, view_only=False)
|
||||
if table in self.registry:
|
||||
model = self.registry[table]
|
||||
else:
|
||||
print(f"table {table} is not registered")
|
||||
continue
|
||||
rows = self.session.query(model).all()
|
||||
entries = []
|
||||
print(f"found {len(rows)} entries")
|
||||
print(f"found {len(columns)} columns")
|
||||
for row in rows:
|
||||
# print(row)
|
||||
entry = {}
|
||||
for order in columns:
|
||||
# print(columns[order])
|
||||
column_name = columns[order]['column']
|
||||
# print(f"get value {column_name} from {row} of table {table}")
|
||||
try:
|
||||
value = getattr(row, column_name)
|
||||
if isinstance(value, datetime):
|
||||
entry[column_name] = str(value)
|
||||
else:
|
||||
entry[column_name] = value
|
||||
except AttributeError as error:
|
||||
print("could not get value")
|
||||
entries.append(entry)
|
||||
db[table] = entries
|
||||
export_file = Path(export_file_name)
|
||||
match export_type:
|
||||
case "JSON":
|
||||
json_dump = json.dumps(db, indent=4)
|
||||
with open(export_file_name, "w") as dump_file:
|
||||
dump_file.write(json_dump)
|
||||
case "YAML":
|
||||
export_file = Path(export_file_name)
|
||||
case "SQLite":
|
||||
export_file = Path(export_file_name)
|
||||
case _:
|
||||
print("unknown export type")
|
||||
if export_file.exists():
|
||||
print(f"{export_file} exists")
|
||||
@@ -0,0 +1,18 @@
|
||||
from sqlalchemy.orm import DeclarativeBase, relationship, sessionmaker, declarative_base
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
# class BaseModel:
|
||||
#
|
||||
# @classmethod
|
||||
# def model_lookup_by_table_name(cls, table_name):
|
||||
# registry_instance = getattr(cls, "registry")
|
||||
# for mapper_ in registry_instance.mappers:
|
||||
# model = mapper_.class_
|
||||
# model_class_name = model.__tablename__
|
||||
# if model_class_name == table_name:
|
||||
# return model
|
||||
|
||||
# Base = declarative_base(cls=BaseModel)
|
||||
@@ -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")
|
||||
@@ -0,0 +1,25 @@
|
||||
from sqlalchemy import Column, DateTime, Integer, String
|
||||
from sqlalchemy.dialects.mysql import BIT
|
||||
|
||||
from database.base import Base
|
||||
|
||||
|
||||
class MediaFile(Base):
|
||||
__tablename__ = 'media_file'
|
||||
id = Column(String, primary_key=True)
|
||||
created_date = Column(DateTime)
|
||||
last_modified_date = Column(DateTime)
|
||||
version = Column(Integer)
|
||||
cloud_link = Column(String(255))
|
||||
file_name = Column(String(255))
|
||||
path = Column(String(255))
|
||||
review = Column(BIT(1))
|
||||
title = Column(String(255))
|
||||
url = Column(String(255))
|
||||
should_download = Column(BIT(1))
|
||||
|
||||
def __repr__(self):
|
||||
return f'MediaFile({self.id} {self.title} {self.title})'
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.title}({self.id})'
|
||||
@@ -0,0 +1,49 @@
|
||||
from sqlalchemy import Column, String, ForeignKey, DateTime, Integer, Boolean
|
||||
from sqlalchemy.dialects.mysql import BIT
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from database import Base
|
||||
|
||||
|
||||
class MetaDataTable(Base):
|
||||
__tablename__ = 'meta_data_table'
|
||||
id = Column(String, primary_key=True)
|
||||
created_date = Column(DateTime)
|
||||
last_modified_date = Column(DateTime)
|
||||
version = Column(Integer)
|
||||
table_name = Column(String(255), unique=True)
|
||||
table_columns = relationship("MetaDataColumn")
|
||||
|
||||
def __repr__(self):
|
||||
return f'MetaDataTable({self.id} {self.table_name})'
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.table_name}({self.id})'
|
||||
|
||||
class MetaDataColumn(Base):
|
||||
__tablename__ = 'meta_data_column'
|
||||
id = Column(String, primary_key=True)
|
||||
created_date = Column(DateTime)
|
||||
last_modified_date = Column(DateTime)
|
||||
version = Column(Integer)
|
||||
column_modifier = Column(String(255), nullable=True)
|
||||
column_name = Column(String(255))
|
||||
column_order = Column(Integer)
|
||||
column_sync_name = Column(String(255))
|
||||
column_type = Column(String(255))
|
||||
table_id = Column(String, ForeignKey('meta_data_table.id'))
|
||||
table = relationship("MetaDataTable", back_populates="table_columns")
|
||||
column_label = Column(String(255))
|
||||
filter_label = Column(String(255))
|
||||
is_shown = Column(BIT(1))
|
||||
show_filter = Column(BIT(1))
|
||||
ref_column = Column(String, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
if self.column_name is None:
|
||||
return f'MetaDataColumn({self.id} {self.table.table_name}.__)'
|
||||
else:
|
||||
return f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})'
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.column_name}({self.id})'
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}")
|
||||
@@ -0,0 +1,137 @@
|
||||
from PySide6.QtGui import QAction, QIcon
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QMenu, QMessageBox, QTabWidget, QTableView
|
||||
from PySide6.QtWidgets import QLabel, QMainWindow
|
||||
|
||||
from database import KontorDB
|
||||
from dialogs import ExportKontorDialog, ImportKontorDialog
|
||||
from model_config import KontorModelConfig
|
||||
from table_model import KontorTableModel
|
||||
from media_file_model import MediaFileTableModel
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
|
||||
def __init__(self, config):
|
||||
super().__init__()
|
||||
|
||||
self.tick = QIcon('res/tick.png')
|
||||
self.cross = QIcon('res/cross.png')
|
||||
self.import_icon = QIcon("res/application-import.png")
|
||||
self.export_icon = QIcon("res/application-export.png")
|
||||
self.circle_icon = QIcon("res/arrow-circle-double.png")
|
||||
|
||||
self.setWindowTitle("Kontor")
|
||||
self.setMinimumSize(800, 500)
|
||||
self._create_actions()
|
||||
self._create_menubar()
|
||||
self._create_toolbars()
|
||||
self._create_statusbar()
|
||||
|
||||
self.data = []
|
||||
self.filter = {}
|
||||
self.kontor_db = KontorDB(config)
|
||||
self.central_widget = QWidget()
|
||||
parent_layout = QVBoxLayout()
|
||||
self.central_widget.setLayout(parent_layout)
|
||||
self.tabs = QTabWidget()
|
||||
self.tabs.addTab(self.generate_data_tab("comic", Comic), "Comics")
|
||||
self.tabs.addTab(self.generate_data_tab("media_file", MediaFile), "MediaFile")
|
||||
self.tabs.currentChanged.connect(self._tab_changed)
|
||||
#label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
parent_layout.addWidget(self.tabs)
|
||||
|
||||
self.setCentralWidget(self.central_widget)
|
||||
|
||||
def _create_actions(self):
|
||||
self.newAction = QAction("&New", self)
|
||||
self.aboutAction = QAction("&Über...", self)
|
||||
self.aboutAction.triggered.connect(self.about)
|
||||
self.importAction = QAction(self.import_icon, "&Import", self)
|
||||
self.importAction.triggered.connect(self.import_from_file)
|
||||
self.exportAction = QAction(self.export_icon, "&Export", self)
|
||||
self.exportAction.triggered.connect(self.export_to_file)
|
||||
self.refreshAction = QAction(self.circle_icon, "&Refresh", self)
|
||||
self.refreshAction.triggered.connect(self.refresh)
|
||||
self.updateTitleAction = QAction("&Update Titles", self)
|
||||
self.downloadAction = QAction("&Download Videos", self)
|
||||
self.exitAction = QAction("&Beenden", self)
|
||||
self.exitAction.setShortcut("Alt+F4")
|
||||
self.exitAction.triggered.connect(self.close)
|
||||
|
||||
def _create_menubar(self):
|
||||
menu_bar = self.menuBar()
|
||||
# File menu
|
||||
file_menu = QMenu("&Datei")
|
||||
menu_bar.addMenu(file_menu)
|
||||
file_menu.addAction(self.exitAction)
|
||||
# Kontor menu
|
||||
kontor_menu = QMenu("&Kontor")
|
||||
menu_bar.addMenu(kontor_menu)
|
||||
kontor_menu.addAction(self.importAction)
|
||||
kontor_menu.addAction(self.exportAction)
|
||||
comic_menu = QMenu("&Comic")
|
||||
tysc_menu = QMenu("&TradeYourSportCards")
|
||||
media_file_menu = QMenu("&MediaFile")
|
||||
media_file_menu.addAction(self.updateTitleAction)
|
||||
media_file_menu.addAction(self.downloadAction)
|
||||
kontor_menu.addMenu(comic_menu)
|
||||
kontor_menu.addMenu(tysc_menu)
|
||||
kontor_menu.addMenu(media_file_menu)
|
||||
# Help menu
|
||||
help_menu = QMenu("&Hilfe")
|
||||
menu_bar.addMenu(help_menu)
|
||||
help_menu.addAction(self.aboutAction)
|
||||
|
||||
def _create_toolbars(self):
|
||||
# Kontor toolbar
|
||||
kontor_tool_bar = self.addToolBar("Kontor")
|
||||
kontor_tool_bar.addAction(self.importAction)
|
||||
kontor_tool_bar.addAction(self.exportAction)
|
||||
kontor_tool_bar.addAction(self.refreshAction)
|
||||
|
||||
def _create_statusbar(self):
|
||||
self.statusBar = self.statusBar()
|
||||
self.statusBar.showMessage("Kontor ready", 6000)
|
||||
self.status_label = QLabel("")
|
||||
self.statusBar.addPermanentWidget(self.status_label)
|
||||
|
||||
def about(self):
|
||||
QMessageBox.about(self.central_widget, "Über Kontor", f"Python: 3.11\nKontor: 0.1.0")
|
||||
|
||||
def import_from_file(self):
|
||||
import_dlg = ImportKontorDialog(self)
|
||||
if import_dlg.exec():
|
||||
print(f"import DB from file {import_dlg.file_name}")
|
||||
else:
|
||||
print("no nothing for import")
|
||||
pass
|
||||
|
||||
def export_to_file(self):
|
||||
export_dlg = ExportKontorDialog(self, self.kontor_db)
|
||||
if export_dlg.exec():
|
||||
print(export_dlg.get_tables_to_export())
|
||||
print(f"export DB to {export_dlg.file_name}")
|
||||
self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000)
|
||||
self.kontor_db.export_db(export_dlg.current_export_type, export_dlg.file_name, export_dlg.get_tables_to_export())
|
||||
else:
|
||||
self.statusBar.showMessage("Export cancelled", 3000)
|
||||
|
||||
def refresh(self):
|
||||
self.data[self.tabs.currentIndex()].refresh()
|
||||
|
||||
def _tab_changed(self, tab_index):
|
||||
self.data[tab_index].refresh()
|
||||
|
||||
def generate_data_tab(self, table_name, table):
|
||||
data_tab = QWidget()
|
||||
table_config = KontorModelConfig(self.kontor_db, self, table_name, table)
|
||||
model = KontorTableModel(table_config)
|
||||
layout = QVBoxLayout()
|
||||
self.data.append(model)
|
||||
data_tab.setLayout(layout)
|
||||
table_view = QTableView()
|
||||
table_view.setModel(model)
|
||||
layout.addLayout(table_config.get_filter_layout())
|
||||
layout.addWidget(table_view)
|
||||
model.refresh()
|
||||
return data_tab
|
||||
@@ -0,0 +1,65 @@
|
||||
import mariadb
|
||||
from PySide6.QtWidgets import QHBoxLayout, QCheckBox
|
||||
|
||||
from database import KontorDB
|
||||
|
||||
|
||||
class KontorModelConfig:
|
||||
|
||||
def __init__(self, kontor_db: KontorDB, main_window, table_name: str, table):
|
||||
self.header = {}
|
||||
self.filter = {}
|
||||
self.main_window = main_window
|
||||
self._table_name = table_name
|
||||
self._table = table
|
||||
self.kontor_db = kontor_db
|
||||
self.get_table_config()
|
||||
|
||||
def get_table_config(self):
|
||||
self.header = self.kontor_db.get_column_meta_data(self._table_name)
|
||||
self.filter = self.kontor_db.get_filters(self._table_name)
|
||||
|
||||
def get_filter(self) -> str:
|
||||
filter_rule = ""
|
||||
# print(self.filter["download"].isChecked())
|
||||
for column, filter_info in self.filter.items():
|
||||
# print(column, filter_info)
|
||||
if filter_info['widget'].isChecked():
|
||||
# print(column, filter_info, filter_rule, len(filter_rule))
|
||||
if len(filter_rule) < 1:
|
||||
filter_rule += "WHERE "
|
||||
if len(filter_rule) > 8:
|
||||
filter_rule += " AND "
|
||||
filter_rule += f"{column} is true"
|
||||
# print(f"{filter_rule=}")
|
||||
return filter_rule
|
||||
|
||||
def filters(self) -> dict:
|
||||
_filters = {}
|
||||
# print(self.filter["download"].isChecked())
|
||||
for column, filter_info in self.filter.items():
|
||||
# print(column, filter_info)
|
||||
if filter_info['widget'].isChecked():
|
||||
_filters[column] = True
|
||||
# print(f"{filter_rule=}")
|
||||
return _filters
|
||||
|
||||
def get_data(self) -> list:
|
||||
# data = self.kontor_db.get_data(self._table_name, self.header, self.get_filter())
|
||||
# data.clear()
|
||||
data = self.kontor_db.data(self._table, self.header, self.filters())
|
||||
# print(f"KontorModelConfig.get_data: {len(data)}")
|
||||
# comics = self.kontor_db.session.query(Comic).all()
|
||||
# print(f'{len(comics)} Comics loaded')
|
||||
return data
|
||||
|
||||
def get_filter_layout(self) -> QHBoxLayout:
|
||||
filter_layout = QHBoxLayout()
|
||||
for column, filter_info in self.filter.items():
|
||||
filter_checkbox = QCheckBox()
|
||||
filter_checkbox.setText(filter_info['label'])
|
||||
filter_checkbox.checkStateChanged.connect(self.main_window.refresh)
|
||||
self.filter[column]['widget'] = filter_checkbox
|
||||
filter_layout.addWidget(filter_checkbox)
|
||||
filter_layout.addStretch()
|
||||
return filter_layout
|
||||
@@ -0,0 +1,108 @@
|
||||
from datetime import datetime
|
||||
|
||||
from PySide6.QtCore import QAbstractTableModel, QModelIndex
|
||||
from PySide6.QtGui import Qt
|
||||
|
||||
from gui.model_config import KontorModelConfig
|
||||
|
||||
|
||||
class KontorTableModel(QAbstractTableModel):
|
||||
|
||||
def __init__(self, model_config: KontorModelConfig):
|
||||
super().__init__()
|
||||
self._main_window = model_config.main_window
|
||||
self._config = model_config
|
||||
self._data = []
|
||||
|
||||
def refresh(self):
|
||||
data = self._config.get_data()
|
||||
count = 0
|
||||
# print(data)
|
||||
if data is not None:
|
||||
self.beginResetModel()
|
||||
self._data.clear()
|
||||
self._data = data
|
||||
self.endResetModel()
|
||||
count = len(data)
|
||||
# print(data)
|
||||
# print(self._data)
|
||||
self.layoutChanged.emit()
|
||||
self._main_window.statusBar.showMessage(f"{count} Einträge geladen", 3000)
|
||||
|
||||
def rowCount(self, parent=QModelIndex()):
|
||||
# The length of the outer list.
|
||||
if self._data is None:
|
||||
return 0
|
||||
return len(self._data)
|
||||
|
||||
def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole):
|
||||
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
|
||||
return self._config.header[col]['label']
|
||||
if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole:
|
||||
return str(col+1)
|
||||
|
||||
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
|
||||
if self._data is None:
|
||||
return None
|
||||
value = self._data[index.row()][index.column()]
|
||||
# print('{}:: {}:: {}: {}'.format(index, role, value, type(value)))
|
||||
if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole:
|
||||
if isinstance(value, datetime):
|
||||
return value.strftime("%Y-%m-%d %M:%M:%S")
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if isinstance(value, bytes):
|
||||
if value == b'\x01':
|
||||
return self._main_window.tick
|
||||
else:
|
||||
return self._main_window.cross
|
||||
if isinstance(value, int):
|
||||
# print('{}:: {}: {}'.format(index, value, type(value)))
|
||||
if value == 1:
|
||||
return self._main_window.tick
|
||||
else:
|
||||
return self._main_window.cross
|
||||
if isinstance(value, bool):
|
||||
if value:
|
||||
return self._main_window.tick
|
||||
else:
|
||||
return self._main_window.cross
|
||||
return str(value)
|
||||
if role == Qt.ItemDataRole.DecorationRole:
|
||||
if isinstance(value, bytes):
|
||||
if value == b'\x01':
|
||||
return self._main_window.tick
|
||||
else:
|
||||
return self._main_window.cross
|
||||
if isinstance(value, int):
|
||||
if value == 1:
|
||||
return self._main_window.tick
|
||||
else:
|
||||
return self._main_window.cross
|
||||
if isinstance(value, bool):
|
||||
if value:
|
||||
return self._main_window.tick
|
||||
else:
|
||||
return self._main_window.cross
|
||||
|
||||
def columnCount(self, index=QModelIndex()):
|
||||
# The following takes the first sub-list, and returns
|
||||
# the length (only works if all rows are an equal length)
|
||||
# print(f"Header count: {len(self._config.get_header())}")
|
||||
return len(self._config.header)
|
||||
|
||||
def setData(self, index, value, role: int) -> bool:
|
||||
print(index, role)
|
||||
if role == Qt.ItemDataRole.EditRole:
|
||||
self._data[index.row()][index.column()] = value
|
||||
print(self._data[index.row()][index.column()])
|
||||
self.dataChanged.emit(index, index)
|
||||
return True
|
||||
if role == Qt.ItemDataRole.CheckStateRole:
|
||||
print("role == Qt.ItemDataRole.CheckStateRole")
|
||||
checked = value == Qt.CheckState.Checked
|
||||
self._data[index.row()][index.column()] = checked
|
||||
return False
|
||||
|
||||
def flags(self, index):
|
||||
return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate
|
||||
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
PyQT6 GUI for Kontor
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from platformdirs import PlatformDirs
|
||||
from PySide6.QtWidgets import QApplication
|
||||
import yaml
|
||||
|
||||
from gui.main_window import MainWindow
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QApplication(sys.argv)
|
||||
dirs = PlatformDirs("kontor")
|
||||
database_config = Path(dirs.user_config_dir, 'database-config.yaml')
|
||||
with open(database_config, 'rt') as f:
|
||||
db_config = yaml.safe_load(f.read())
|
||||
window = MainWindow(db_config)
|
||||
window.show()
|
||||
app.exec()
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
[app]
|
||||
|
||||
# title of your application
|
||||
title = kontor
|
||||
|
||||
# project directory. the general assumption is that project_dir is the parent directory
|
||||
# of input_file
|
||||
project_dir = /home/tpeetz/projects/kontor/qt
|
||||
|
||||
# source file path
|
||||
input_file = /home/tpeetz/projects/kontor/qt/kontor.py
|
||||
|
||||
# directory where exec is stored
|
||||
exec_directory = .
|
||||
|
||||
# path to .pyproject project file
|
||||
project_file =
|
||||
|
||||
# application icon
|
||||
icon = /usr/local/lib/python3.11/dist-packages/PySide6/scripts/deploy_lib/pyside_icon.jpg
|
||||
|
||||
[python]
|
||||
|
||||
# python path
|
||||
python_path = /usr/bin/python3
|
||||
|
||||
# python packages to install
|
||||
packages = Nuitka==2.4.8
|
||||
|
||||
# buildozer = for deploying Android application
|
||||
android_packages = buildozer==1.5.0,cython==0.29.33
|
||||
|
||||
[qt]
|
||||
|
||||
# comma separated path to qml files required
|
||||
# normally all the qml files required by the project are added automatically
|
||||
qml_files =
|
||||
|
||||
# excluded qml plugin binaries
|
||||
excluded_qml_plugins =
|
||||
|
||||
# qt modules used. comma separated
|
||||
modules = Gui,DBus,Core,Widgets
|
||||
|
||||
# qt plugins used by the application
|
||||
plugins = platformthemes,imageformats,platforms,generic,iconengines,egldeviceintegrations,styles,xcbglintegrations,platforms/darwin,platforminputcontexts,accessiblebridge
|
||||
|
||||
[android]
|
||||
|
||||
# path to pyside wheel
|
||||
wheel_pyside =
|
||||
|
||||
# path to shiboken wheel
|
||||
wheel_shiboken =
|
||||
|
||||
# plugins to be copied to libs folder of the packaged application. comma separated
|
||||
plugins =
|
||||
|
||||
[nuitka]
|
||||
|
||||
# usage description for permissions requested by the app as found in the info.plist file
|
||||
# of the app bundle
|
||||
# eg = extra_args = --show-modules --follow-stdlib
|
||||
macos.permissions =
|
||||
|
||||
# mode of using nuitka. accepts standalone or onefile. default is onefile.
|
||||
mode = onefile
|
||||
|
||||
# (str) specify any extra nuitka arguments
|
||||
extra_args = --quiet --noinclude-qt-translations
|
||||
|
||||
[buildozer]
|
||||
|
||||
# build mode
|
||||
# possible options = [release, debug]
|
||||
# release creates an aab, while debug creates an apk
|
||||
mode = debug
|
||||
|
||||
# contrains path to pyside6 and shiboken6 recipe dir
|
||||
recipe_dir =
|
||||
|
||||
# path to extra qt android jars to be loaded by the application
|
||||
jars_dir =
|
||||
|
||||
# if empty uses default ndk path downloaded by buildozer
|
||||
ndk_path =
|
||||
|
||||
# if empty uses default sdk path downloaded by buildozer
|
||||
sdk_path =
|
||||
|
||||
# other libraries to be loaded. comma separated.
|
||||
# loaded at app startup
|
||||
local_libs =
|
||||
|
||||
# architecture of deployed platform
|
||||
# possible values = ["aarch64", "armv7a", "i686", "x86_64"]
|
||||
arch =
|
||||
|
||||
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 |
Reference in New Issue
Block a user