Move talker settings menu generator to a separate file

This commit is contained in:
Mizaki 2023-02-14 01:32:56 +00:00
parent 83a8d5d5e1
commit 6a6a3320cb
4 changed files with 258 additions and 121 deletions

View File

@ -15,7 +15,6 @@
# limitations under the License.
from __future__ import annotations
import argparse
import html
import logging
import os
@ -26,6 +25,7 @@ from typing import Any
import settngs
from PyQt5 import QtCore, QtGui, QtWidgets, uic
import comictaggerlib.ui.talkeruigenerator
from comicapi import utils
from comicapi.genericmetadata import md_test
from comictaggerlib.ctversion import version
@ -190,6 +190,15 @@ class SettingsWindow(QtWidgets.QDialog):
self.settings_to_form()
self.rename_test()
self.dir_test()
self.sources: dict = {}
self.sources = comictaggerlib.ui.talkeruigenerator.generate_source_option_tabs(
parent, self.tTalkerTabs, self.config, self.talkers
)
# Select active source in dropdown
self.cobxInfoSource.setCurrentIndex(self.cobxInfoSource.findData(self.config[0].talker_source))
# Set General as start tab
self.tabWidget.setCurrentIndex(0)
def connect_signals(self) -> None:
self.btnBrowseRar.clicked.connect(self.select_rar)
@ -234,100 +243,6 @@ class SettingsWindow(QtWidgets.QDialog):
self.twLiteralReplacements.cellChanged.disconnect()
self.twValueReplacements.cellChanged.disconnect()
self.sources: dict = {}
self.generate_source_option_tabs()
def generate_source_option_tabs(self) -> None:
def format_internal_name(int_name: str = "") -> str:
# Presume talker_<name>_<nm>
int_name_split = int_name.split("_")
del int_name_split[0:3]
int_name_split[0] = int_name_split[0].capitalize()
new_name = " ".join(int_name_split)
return new_name
# Add source sub tabs to Comic Sources tab
for talker_id, talker_obj in self.talkers.items():
# Add source to general tab dropdown list
self.cobxInfoSource.addItem(talker_obj.name, talker_id)
# Use a dict to make a var name from var
source_info = {}
tab_name = talker_id
source_info[tab_name] = {"tab": QtWidgets.QWidget(), "widgets": {}}
layout_grid = QtWidgets.QGridLayout()
row = 0
full_talker_name = "talker_" + talker_id
for option in self.config[1][full_talker_name][1].values():
current_widget = None
if option.action is not None and isinstance(option.action, type(argparse.BooleanOptionalAction)):
# bool equals a checkbox (QCheckBox)
current_widget = QtWidgets.QCheckBox(format_internal_name(option.internal_name))
# Set widget status
current_widget.setChecked(getattr(self.config[0], option.internal_name))
# Add widget and span all columns
layout_grid.addWidget(current_widget, row, 0, 1, -1)
elif isinstance(option.type, type(int)):
# int equals a spinbox (QSpinBox)
lbl = QtWidgets.QLabel(option.internal_name)
# Create a label
layout_grid.addWidget(lbl, row, 0)
current_widget = QtWidgets.QSpinBox()
current_widget.setRange(0, 9999)
current_widget.setValue(getattr(self.config[0], option.internal_name))
layout_grid.addWidget(current_widget, row, 1, alignment=QtCore.Qt.AlignLeft)
elif isinstance(option.type, type(float)):
# float equals a spinbox (QDoubleSpinBox)
lbl = QtWidgets.QLabel(format_internal_name(option.internal_name))
# Create a label
layout_grid.addWidget(lbl, row, 0)
current_widget = QtWidgets.QDoubleSpinBox()
current_widget.setRange(0, 9999.99)
current_widget.setValue(getattr(self.config[0], option.internal_name))
layout_grid.addWidget(current_widget, row, 1, alignment=QtCore.Qt.AlignLeft)
# type of None should be string
elif option.type is None or isinstance(option.type, type(str)):
# str equals a text field (QLineEdit)
lbl = QtWidgets.QLabel(format_internal_name(option.internal_name))
# Create a label
layout_grid.addWidget(lbl, row, 0)
current_widget = QtWidgets.QLineEdit()
# Set widget status
current_widget.setText(getattr(self.config[0], option.internal_name))
layout_grid.addWidget(current_widget, row, 1)
# Special case for api_key, make a test button
if option.internal_name.endswith("api_key"):
btn = QtWidgets.QPushButton("Test Key")
layout_grid.addWidget(btn, row, 2)
btn.clicked.connect(lambda state, sn=talker_id: self.test_api_key(sn))
row += 1
if current_widget:
# Add tooltip text
current_widget.setToolTip(option.help)
source_info[tab_name]["widgets"][option.internal_name] = current_widget
else:
# An empty current_widget implies an unsupported type
logger.info(f"Unsupported talker option found. Name: {option.internal_name} Type: {option.type}")
# Add vertical spacer
vspacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
layout_grid.addItem(vspacer, row, 0)
# Display the new widgets
source_info[tab_name]["tab"].setLayout(layout_grid)
# Add new sub tab to Comic Source tab
self.tTalkerTabs.addTab(source_info[tab_name]["tab"], talker_obj.name)
self.sources.update(source_info)
# Select active source in dropdown
self.cobxInfoSource.setCurrentIndex(self.cobxInfoSource.findData(self.config[0].talker_source))
# Set General as start tab
self.tabWidget.setCurrentIndex(0)
def addLiteralReplacement(self) -> None:
self.insertRow(self.twLiteralReplacements, self.twLiteralReplacements.rowCount(), Replacement("", "", False))
@ -542,7 +457,7 @@ class SettingsWindow(QtWidgets.QDialog):
self.config[0].rename_strict = self.cbxRenameStrict.isChecked()
self.config[0].rename_replacements = self.get_replacements()
# Read settings from sources tabs and generate self.settings.config data
# Read settings from sources tabs and generate self.config data
for tab in self.sources.items():
for name, widget in tab[1]["widgets"].items():
widget_value = None
@ -574,21 +489,6 @@ class SettingsWindow(QtWidgets.QDialog):
ComicCacher(self.config[0].runtime_config.user_cache_dir, version).clear_cache()
QtWidgets.QMessageBox.information(self, self.name, "Cache has been cleared.")
def test_api_key(self, source_id) -> None:
# Find URL and API key
for tab in self.sources.items():
for name, widget in tab[1]["widgets"].items():
if tab[0] == source_id:
if name.endswith("api_key"):
key = widget.text().strip()
if name.endswith("url"):
url = widget.text().strip()
if self.talkers[source_id].check_api_key(key, url):
QtWidgets.QMessageBox.information(self, "API Key Test", "Key is valid!")
else:
QtWidgets.QMessageBox.warning(self, "API Key Test", "Key is NOT valid!")
def reset_settings(self) -> None:
self.config = settngs.get_namespace(settngs.defaults(self.config[1]))
self.settings_to_form()

View File

@ -0,0 +1,213 @@
from __future__ import annotations
import argparse
import logging
import settngs
from PyQt5 import QtCore, QtWidgets
from comictalker.comictalker import ComicTalker
logger = logging.getLogger(__name__)
def call_check_api_key(
talker_id: str,
sources_info: dict[str, QtWidgets.QWidget],
talkers: dict[str, ComicTalker],
parent: QtWidgets.QWidget,
):
key = ""
# Find the correct widget to get the API key
for name, widget in sources_info[talker_id]["widgets"].items():
if name.startswith("talker_" + talker_id) and name.endswith("api_key"):
key = widget.text().strip()
if talkers[talker_id].check_api_key(key):
QtWidgets.QMessageBox.information(parent, "API Key Test", "Key is valid!")
else:
QtWidgets.QMessageBox.warning(parent, "API Key Test", "Key is NOT valid!")
def call_check_api_url(
talker_id: str,
sources_info: dict[str, QtWidgets.QWidget],
talkers: dict[str, ComicTalker],
parent: QtWidgets.QWidget,
):
url = ""
# Find the correct widget to get the URL key
for name, widget in sources_info[talker_id]["widgets"].items():
if name.startswith("talker_" + talker_id) and name.endswith("url"):
url = widget.text().strip()
if talkers[talker_id].check_api_url(url):
QtWidgets.QMessageBox.information(parent, "API Key Test", "URL is valid!")
else:
QtWidgets.QMessageBox.warning(parent, "API Key Test", "URL is NOT valid!")
def test_api_key(
btn: QtWidgets.QPushButton,
talker_id: str,
sources_info: dict[str, QtWidgets.QWidget],
talkers: dict[str, ComicTalker],
parent: QtWidgets.QWidget,
) -> None:
btn.clicked.connect(lambda: call_check_api_key(talker_id, sources_info, talkers, parent))
def test_api_url(
btn: QtWidgets.QPushButton,
talker_id: str,
sources_info: dict[str, QtWidgets.QWidget],
talkers: dict[str, ComicTalker],
parent: QtWidgets.QWidget,
) -> None:
btn.clicked.connect(lambda: call_check_api_url(talker_id, sources_info, talkers, parent))
def format_internal_name(int_name: str = "") -> str:
# Presume talker_<name>_<nm>: talker_comicvine_cv_widget_name
int_name_split = int_name.split("_")
del int_name_split[0:3]
int_name_split[0] = int_name_split[0].capitalize()
new_name = " ".join(int_name_split)
return new_name
def generate_checkbox(
option: settngs.Setting, value: bool, layout: QtWidgets.QGridLayout
) -> tuple[QtWidgets.QGridLayout, QtWidgets.QCheckBox]:
# bool equals a checkbox (QCheckBox)
widget = QtWidgets.QCheckBox(format_internal_name(option.internal_name))
# Set widget status
widget.setChecked(value)
# Add tooltip text
widget.setToolTip(option.help)
# Add widget and span all columns
layout.addWidget(widget, layout.rowCount() + 1, 0, 1, -1)
return layout, widget
def generate_spinbox(
option: settngs.Setting, value: int | float, layout: QtWidgets.QGridLayout
) -> tuple[QtWidgets.QGridLayout, QtWidgets.QSpinBox | QtWidgets.QDoubleSpinBox]:
if isinstance(value, int):
# int equals a spinbox (QSpinBox)
lbl = QtWidgets.QLabel(option.internal_name)
# Create a label
layout.addWidget(lbl, layout.rowCount() + 1, 0)
widget = QtWidgets.QSpinBox()
widget.setRange(0, 9999)
widget.setValue(value)
widget.setToolTip(option.help)
layout.addWidget(widget, layout.rowCount() - 1, 1, alignment=QtCore.Qt.AlignLeft)
if isinstance(value, float):
# float equals a spinbox (QDoubleSpinBox)
lbl = QtWidgets.QLabel(format_internal_name(option.internal_name))
# Create a label
layout.addWidget(lbl, layout.rowCount() + 1, 0)
widget = QtWidgets.QDoubleSpinBox()
widget.setRange(0, 9999.99)
widget.setValue(value)
widget.setToolTip(option.help)
layout.addWidget(widget, layout.rowCount() - 1, 1, alignment=QtCore.Qt.AlignLeft)
return layout, widget
def generate_textbox(
option: settngs.Setting, value: str, layout: QtWidgets.QGridLayout
) -> tuple[QtWidgets.QGridLayout, QtWidgets.QLineEdit, QtWidgets.QPushButton]:
btn = None
# str equals a text field (QLineEdit)
lbl = QtWidgets.QLabel(format_internal_name(option.internal_name))
# Create a label
layout.addWidget(lbl, layout.rowCount() + 1, 0)
widget = QtWidgets.QLineEdit()
widget.setObjectName(option.internal_name)
# Set widget status
widget.setText(value)
widget.setToolTip(option.help)
layout.addWidget(widget, layout.rowCount() - 1, 1)
# Special case for api_key, make a test button
if option.internal_name.endswith("api_key"):
btn = QtWidgets.QPushButton("Test Key")
layout.addWidget(btn, layout.rowCount() - 1, 2)
if option.internal_name.endswith("url"):
btn = QtWidgets.QPushButton("Test URL")
layout.addWidget(btn, layout.rowCount() - 1, 2)
return layout, widget, btn
def generate_source_option_tabs(
parent: QtWidgets.QWidget,
tabs: QtWidgets.QTabWidget,
config: settngs.Config[settngs.Namespace],
talkers: dict[str, ComicTalker],
) -> dict[str, QtWidgets.QWidget]:
"""
Generate GUI tabs and settings for talkers
"""
sources: dict = {}
# Add source sub tabs to Comic Sources tab
for talker_id, talker_obj in talkers.items():
# Add source to general tab dropdown list
tabs.findChildren(QtWidgets.QComboBox, "cobxInfoSource")[0].addItem(talker_obj.name, talker_id)
# Use a dict to make a var name from var
source_info = {}
tab_name = talker_id
source_info[tab_name] = {"tab": QtWidgets.QWidget(), "widgets": {}}
layout_grid = QtWidgets.QGridLayout()
for option in config[1][f"talker_{talker_id}"][1].values():
current_widget = None
if option.action is not None and (
isinstance(option.action, type(argparse.BooleanOptionalAction))
or option.action == "store_true"
or option.action == "store_false"
):
layout_grid, current_widget = generate_checkbox(
option, getattr(config[0], option.internal_name), layout_grid
)
source_info[tab_name]["widgets"][option.internal_name] = current_widget
elif isinstance(option.type, type(int)) or isinstance(option.type, type(float)):
layout_grid, current_widget = generate_spinbox(
option, getattr(config[0], option.internal_name), layout_grid
)
source_info[tab_name]["widgets"][option.internal_name] = current_widget
# option.type of None should be string
elif option.type is None or isinstance(option.type, type(str)):
layout_grid, current_widget, btn = generate_textbox(
option, getattr(config[0], option.internal_name), layout_grid
)
source_info[tab_name]["widgets"][option.internal_name] = current_widget
if option.internal_name.endswith("key"):
# Attach test api function to button. A better way?
test_api_key(btn, talker_id, source_info, talkers, parent)
if option.internal_name.endswith("url"):
# Attach test api function to button. A better way?
test_api_url(btn, talker_id, source_info, talkers, parent)
else:
logger.debug(f"Unsupported talker option found. Name: {option.internal_name} Type: {option.type}")
# Add vertical spacer
vspacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
layout_grid.addItem(vspacer, layout_grid.rowCount() + 1, 0)
# Display the new widgets
source_info[tab_name]["tab"].setLayout(layout_grid)
# Add new sub tab to Comic Source tab
tabs.addTab(source_info[tab_name]["tab"], talker_obj.name)
sources.update(source_info)
return sources

View File

@ -118,7 +118,10 @@ class ComicTalker:
self.api_url: str = ""
def register_settings(self, parser: settngs.Manager) -> None:
"""Allows registering settings using the settngs package with an argparse like interface"""
"""
Allows registering settings using the settngs package with an argparse like interface
NOTE: The order used will be reflected within the settings menu
"""
return None
def parse_settings(self, settings: dict[str, Any]) -> dict[str, Any]:
@ -128,10 +131,15 @@ class ComicTalker:
"""
return settings
def check_api_key(self, key: str, url: str) -> bool:
def check_api_key(self, key: str) -> bool:
"""
This function should return true if the given api key and url are valid.
If the Talker does not use an api key it should validate that the url works.
This function should return true if the given api key is valid.
"""
raise NotImplementedError
def check_api_url(self, url: str) -> bool:
"""
This function should return true if the given url is valid.
"""
raise NotImplementedError

View File

@ -181,8 +181,6 @@ class ComicVineTalker(ComicTalker):
self.wait_on_ratelimit_time: int = 20
def register_settings(self, parser: settngs.Manager) -> None:
parser.add_setting("--cv-api-key", help="Use the given Comic Vine API Key.")
parser.add_setting("--cv-url", help="Use the given Comic Vine URL.")
parser.add_setting("--cv-use-series-start-as-volume", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--cv-wait-on-ratelimit", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting(
@ -191,6 +189,8 @@ class ComicVineTalker(ComicTalker):
action=argparse.BooleanOptionalAction,
help="Removes html tables instead of converting them to text.",
)
parser.add_setting("--cv-api-key", help="Use the given Comic Vine API Key.")
parser.add_setting("--cv-url", help="Use the given Comic Vine URL.")
def parse_settings(self, settings: dict[str, Any]) -> dict[str, Any]:
if settings["cv_api_key"]:
@ -208,7 +208,23 @@ class ComicVineTalker(ComicTalker):
self.remove_html_tables = settings["cv_remove_html_tables"]
return settngs
def check_api_key(self, key: str, url: str) -> bool:
def check_api_key(self, key: str) -> bool:
url = self.api_url
try:
test_url = urljoin(url, "issue/1/")
cv_response: CVResult = requests.get(
test_url,
headers={"user-agent": "comictagger/" + self.version},
params={"api_key": key, "format": "json", "field_list": "name"},
).json()
# Bogus request, but if the key is wrong, you get error 100: "Invalid API Key"
return cv_response["status_code"] != 100
except Exception:
return False
def check_api_url(self, url: str) -> bool:
if not url:
url = self.api_url
try:
@ -221,11 +237,11 @@ class ComicVineTalker(ComicTalker):
cv_response: CVResult = requests.get(
test_url,
headers={"user-agent": "comictagger/" + self.version},
params={"api_key": key, "format": "json", "field_list": "name"},
params={"api_key": self.api_key, "format": "json", "field_list": "name"},
).json()
# Bogus request, but if the key is wrong, you get error 100: "Invalid API Key"
return cv_response["status_code"] != 100
# Bogus request, but if the url is correct, you get error 102: "Error in URL Format"
return cv_response["status_code"] == 102
except Exception:
return False