Add keyring store for API keys (#411)

This commit is contained in:
Chidi Williams 2023-04-23 21:37:50 +01:00 committed by GitHub
parent 49a3151117
commit d68c2b9461
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 419 additions and 30 deletions

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48"><path d="M480.118 726Q551 726 600.5 676.382q49.5-49.617 49.5-120.5Q650 485 600.382 435.5q-49.617-49.5-120.5-49.5Q409 386 359.5 435.618q-49.5 49.617-49.5 120.5Q310 627 359.618 676.5q49.617 49.5 120.5 49.5ZM480 652q-40 0-68-28t-28-68q0-40 28-68t68-28q40 0 68 28t28 68q0 40-28 68t-68 28Zm0 227q-154 0-278-90T17 556q61-143 185-233t278-90q154 0 278 90t185 233q-61 143-185 233t-278 90Zm0-323Zm-.08 240q120.454 0 221.267-65.5T855 556q-53-109-153.733-174.5Q600.533 316 480.08 316q-120.454 0-221.267 65.5T104 556q54 109 154.733 174.5Q359.467 796 479.92 796Z"/></svg>

After

Width:  |  Height:  |  Size: 643 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48"><path d="m634 624-77-76q21-44-12.5-69t-62.5-6l-70-71q15-8 33-12t35-4q71 0 120.5 49.5T650 556q0 16-4 36t-12 32Zm148 150-55-56q44-35 76.5-77t52.5-85q-52-110-150.5-175T490 316q-42 0-82 7t-59 16l-66-66q35-16 92.5-28T485 233q147 0 272 85t186 238q-25 66-67.5 121.5T782 774Zm25 227L653 849q-35 14-80 22t-93 8q-151 0-276.5-85.5T17 556q18-51 55-103t86-100L35 232l50-52 769 769-47 52ZM216 410q-36 29-66 68.5T104 556q52 111 153 175.5T488 796q27 0 55.5-3t44.5-11l-64-64q-8 4-20.5 6t-23.5 2q-70 0-120-49t-50-121q0-11 1.5-23t4.5-21L216 410Zm323 128Zm-137 69Z"/></svg>

After

Width:  |  Height:  |  Size: 639 B

View file

@ -2,7 +2,6 @@ import enum
import json
import logging
import os
import platform
import sys
from datetime import datetime
from enum import auto
@ -37,11 +36,14 @@ from .recording import RecordingAmplitudeListener
from .settings.settings import Settings, APP_NAME
from .settings.shortcut import Shortcut
from .settings.shortcut_settings import ShortcutSettings
from .store.keyring_store import KeyringStore
from .transcriber import (SUPPORTED_OUTPUT_FORMATS, FileTranscriptionOptions, OutputFormat,
Task,
TranscriptionOptions,
FileTranscriberQueueWorker, FileTranscriptionTask, RecordingTranscriber, LOADED_WHISPER_DLL,
DEFAULT_WHISPER_TEMPERATURE)
from .widgets.line_edit import LineEdit
from .widgets.openai_api_key_line_edit import OpenAIAPIKeyLineEdit
from .widgets.preferences_dialog import PreferencesDialog
from .widgets.toolbar import ToolBar
from .widgets.transcription_viewer_widget import TranscriptionViewerWidget
@ -893,7 +895,7 @@ class MainWindow(QMainWindow):
table_widget: TranscriptionTasksTableWidget
tasks: Dict[int, 'FileTranscriptionTask']
tasks_changed = pyqtSignal()
openai_access_token: Optional[str] = None
openai_access_token: Optional[str]
def __init__(self, tasks_cache=TasksCache()):
super().__init__(flags=Qt.WindowType.Window)
@ -906,6 +908,8 @@ class MainWindow(QMainWindow):
self.tasks_cache = tasks_cache
self.openai_access_token = KeyringStore.get_password(KeyringStore.Key.OPENAI_API_KEY)
self.settings = Settings()
self.shortcut_settings = ShortcutSettings(settings=self.settings)
@ -922,10 +926,11 @@ class MainWindow(QMainWindow):
self.addToolBar(self.toolbar)
self.setUnifiedTitleAndToolBarOnMac(True)
self.menu_bar = MenuBar(shortcuts=self.shortcuts, parent=self)
self.menu_bar = MenuBar(shortcuts=self.shortcuts, openai_api_key=self.openai_access_token, parent=self)
self.menu_bar.import_action_triggered.connect(
self.on_new_transcription_action_triggered)
self.menu_bar.shortcuts_changed.connect(self.on_shortcuts_changed)
self.menu_bar.openai_api_key_changed.connect(self.on_openai_access_token_changed)
self.setMenuBar(self.menu_bar)
self.table_widget = TranscriptionTasksTableWidget(self)
@ -947,8 +952,6 @@ class MainWindow(QMainWindow):
self.transcriber_worker.completed.connect(self.transcriber_thread.quit)
self.transcriber_thread.started.connect(self.transcriber_worker.run)
self.transcriber_thread.finished.connect(
self.transcriber_thread.deleteLater)
self.transcriber_thread.start()
@ -1024,10 +1027,10 @@ class MainWindow(QMainWindow):
file_transcriber_window.openai_access_token_changed.connect(self.on_openai_access_token_changed)
file_transcriber_window.show()
# Save the access token on the main window so the user doesn't need to re-enter (at least, not while the app is
# still open)
def on_openai_access_token_changed(self, access_token: str):
self.openai_access_token = access_token
self.menu_bar.set_openai_api_key(self.openai_access_token)
KeyringStore.set_password(KeyringStore.Key.OPENAI_API_KEY, access_token)
def open_transcript_viewer(self):
selected_rows = self.table_widget.selectionModel().selectedRows()
@ -1107,13 +1110,6 @@ class MainWindow(QMainWindow):
super().closeEvent(event)
class LineEdit(QLineEdit):
def __init__(self, default_text: str = '', parent: Optional[QWidget] = None):
super().__init__(default_text, parent)
if platform.system() == 'Darwin':
self.setStyleSheet('QLineEdit { padding: 4px }')
# Adapted from https://github.com/ismailsunni/scripts/blob/master/autocomplete_from_url.py
class HuggingFaceSearchLineEdit(LineEdit):
model_selected = pyqtSignal(str)
@ -1286,10 +1282,9 @@ class TranscriptionOptionsGroupBox(QGroupBox):
default_transcription_options.model.whisper_model_size.value.title())
self.whisper_model_size_combo_box.currentTextChanged.connect(self.on_whisper_model_size_changed)
self.openai_access_token_edit = QLineEdit(self)
self.openai_access_token_edit.setText(default_transcription_options.openai_access_token)
self.openai_access_token_edit.setEchoMode(QLineEdit.EchoMode.Password)
self.openai_access_token_edit.textChanged.connect(self.on_openai_access_token_edit_changed)
self.openai_access_token_edit = OpenAIAPIKeyLineEdit(key=default_transcription_options.openai_access_token,
parent=self)
self.openai_access_token_edit.key_changed.connect(self.on_openai_access_token_edit_changed)
self.form_layout.addRow(_('Model:'), self.model_type_combo_box)
self.form_layout.addRow('', self.whisper_model_size_combo_box)
@ -1362,11 +1357,13 @@ class TranscriptionOptionsGroupBox(QGroupBox):
class MenuBar(QMenuBar):
import_action_triggered = pyqtSignal()
shortcuts_changed = pyqtSignal(dict)
openai_api_key_changed = pyqtSignal(str)
def __init__(self, shortcuts: Dict[str, str], parent: QWidget):
def __init__(self, shortcuts: Dict[str, str], openai_api_key: str, parent: QWidget):
super().__init__(parent)
self.shortcuts = shortcuts
self.openai_api_key = openai_api_key
self.import_action = QAction(_("Import Media File..."), self)
self.import_action.triggered.connect(
@ -1395,8 +1392,10 @@ class MenuBar(QMenuBar):
about_dialog.open()
def on_preferences_action_triggered(self):
preferences_dialog = PreferencesDialog(shortcuts=self.shortcuts, parent=self)
preferences_dialog = PreferencesDialog(shortcuts=self.shortcuts, openai_api_key=self.openai_api_key,
parent=self)
preferences_dialog.shortcuts_changed.connect(self.shortcuts_changed)
preferences_dialog.openai_api_key_changed.connect(self.openai_api_key_changed)
preferences_dialog.open()
def set_shortcuts(self, shortcuts: Dict[str, str]):
@ -1405,6 +1404,9 @@ class MenuBar(QMenuBar):
self.import_action.setShortcut(QKeySequence.fromString(shortcuts[Shortcut.OPEN_IMPORT_WINDOW.name]))
self.preferences_action.setShortcut(QKeySequence.fromString(shortcuts[Shortcut.OPEN_PREFERENCES_WINDOW.name]))
def set_openai_api_key(self, key: str):
self.openai_api_key = key
class Application(QApplication):
window: MainWindow

0
buzz/store/__init__.py Normal file
View file

View file

@ -0,0 +1,27 @@
import enum
import logging
import keyring
from keyring.errors import KeyringLocked, KeyringError, PasswordSetError
from buzz.settings.settings import APP_NAME
class KeyringStore:
class Key(enum.Enum):
OPENAI_API_KEY = 'OpenAI API key'
@staticmethod
def get_password(username: Key) -> str:
try:
return keyring.get_password(APP_NAME, username=username.value)
except (KeyringLocked, KeyringError) as exc:
logging.error('Unable to read from keyring: %s', exc)
return ''
@staticmethod
def set_password(username: Key, password: str) -> None:
try:
keyring.set_password(APP_NAME, username.value, password)
except (KeyringLocked, PasswordSetError) as exc:
logging.error('Unable to write to keyring: %s', exc)

View file

@ -0,0 +1,78 @@
import logging
from typing import Optional
import openai
from PyQt6.QtCore import QRunnable, QObject, pyqtSignal, QThreadPool
from PyQt6.QtWidgets import QWidget, QFormLayout, QLineEdit, QPushButton, QMessageBox
from openai.error import AuthenticationError
from buzz.widgets.openai_api_key_line_edit import OpenAIAPIKeyLineEdit
class GeneralPreferencesWidget(QWidget):
openai_api_key_changed = pyqtSignal(str)
def __init__(self, openai_api_key: str, parent: Optional[QWidget] = None):
super().__init__(parent)
self.openai_api_key = openai_api_key
layout = QFormLayout(self)
self.openai_api_key_line_edit = OpenAIAPIKeyLineEdit(openai_api_key, self)
self.openai_api_key_line_edit.key_changed.connect(self.on_openai_api_key_changed)
self.test_openai_api_key_button = QPushButton('Test')
self.test_openai_api_key_button.clicked.connect(self.on_click_test_openai_api_key_button)
self.update_test_openai_api_key_button()
layout.addRow('OpenAI API Key', self.openai_api_key_line_edit)
layout.addRow('', self.test_openai_api_key_button)
self.setLayout(layout)
def update_test_openai_api_key_button(self):
self.test_openai_api_key_button.setEnabled(len(self.openai_api_key) > 0)
def on_click_test_openai_api_key_button(self):
self.test_openai_api_key_button.setEnabled(False)
job = TestOpenAIApiKeyJob(api_key=self.openai_api_key)
job.signals.success.connect(self.on_test_openai_api_key_success)
job.signals.failed.connect(self.on_test_openai_api_key_failure)
job.setAutoDelete(True)
thread_pool = QThreadPool.globalInstance()
thread_pool.start(job)
def on_test_openai_api_key_success(self):
self.test_openai_api_key_button.setEnabled(True)
QMessageBox.information(self, 'OpenAI API Key Test',
'Your API key is valid. Buzz will use this key to perform Whisper API transcriptions.')
def on_test_openai_api_key_failure(self, error: str):
self.test_openai_api_key_button.setEnabled(True)
QMessageBox.warning(self, 'OpenAI API Key Test', error)
def on_openai_api_key_changed(self, key: str):
self.openai_api_key = key
self.update_test_openai_api_key_button()
self.openai_api_key_changed.emit(key)
class TestOpenAIApiKeyJob(QRunnable):
class Signals(QObject):
success = pyqtSignal()
failed = pyqtSignal(str)
def __init__(self, api_key: str):
super().__init__()
self.api_key = api_key
self.signals = self.Signals()
def run(self):
try:
openai.Model.list(api_key=self.api_key)
self.signals.success.emit()
except AuthenticationError as exc:
logging.error(exc)
self.signals.failed.emit(str(exc))

11
buzz/widgets/line_edit.py Normal file
View file

@ -0,0 +1,11 @@
import platform
from typing import Optional
from PyQt6.QtWidgets import QLineEdit, QWidget
class LineEdit(QLineEdit):
def __init__(self, default_text: str = '', parent: Optional[QWidget] = None):
super().__init__(default_text, parent)
if platform.system() == 'Darwin':
self.setStyleSheet('QLineEdit { padding: 4px }')

View file

@ -0,0 +1,39 @@
from typing import Optional
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtWidgets import QWidget, QLineEdit
from buzz.assets import get_asset_path
from buzz.icon import Icon
from buzz.widgets.line_edit import LineEdit
class OpenAIAPIKeyLineEdit(LineEdit):
key_changed = pyqtSignal(str)
def __init__(self, key: str, parent: Optional[QWidget] = None):
super().__init__(key, parent)
self.key = key
self.visible_on_icon = Icon(get_asset_path('assets/visibility_FILL0_wght700_GRAD0_opsz48.svg'), self)
self.visible_off_icon = Icon(get_asset_path('assets/visibility_off_FILL0_wght700_GRAD0_opsz48.svg'), self)
self.setPlaceholderText('sk-...')
self.setEchoMode(QLineEdit.EchoMode.Password)
self.textChanged.connect(self.on_openai_api_key_changed)
self.toggle_show_openai_api_key_action = self.addAction(self.visible_on_icon,
QLineEdit.ActionPosition.TrailingPosition)
self.toggle_show_openai_api_key_action.triggered.connect(self.on_toggle_show_action_triggered)
def on_toggle_show_action_triggered(self):
if self.echoMode() == QLineEdit.EchoMode.Password:
self.setEchoMode(QLineEdit.EchoMode.Normal)
self.toggle_show_openai_api_key_action.setIcon(self.visible_off_icon)
else:
self.setEchoMode(QLineEdit.EchoMode.Password)
self.toggle_show_openai_api_key_action.setIcon(self.visible_on_icon)
def on_openai_api_key_changed(self, key: str):
self.key = key
self.key_changed.emit(key)

View file

@ -4,13 +4,15 @@ from PyQt6.QtCore import pyqtSignal
from PyQt6.QtWidgets import QDialog, QWidget, QVBoxLayout, QTabWidget, QDialogButtonBox
from buzz.locale import _
from buzz.widgets.shortcuts_editor_widget import ShortcutsEditorWidget
from buzz.widgets.general_preferences_widget import GeneralPreferencesWidget
from buzz.widgets.shortcuts_editor_preferences_widget import ShortcutsEditorPreferencesWidget
class PreferencesDialog(QDialog):
shortcuts_changed = pyqtSignal(dict)
openai_api_key_changed = pyqtSignal(str)
def __init__(self, shortcuts: Dict[str, str], parent: Optional[QWidget] = None) -> None:
def __init__(self, shortcuts: Dict[str, str], openai_api_key: str, parent: Optional[QWidget] = None) -> None:
super().__init__(parent)
self.setWindowTitle('Preferences')
@ -18,7 +20,12 @@ class PreferencesDialog(QDialog):
layout = QVBoxLayout(self)
tab_widget = QTabWidget(self)
shortcuts_table_widget = ShortcutsEditorWidget(shortcuts, self)
general_tab_widget = GeneralPreferencesWidget(openai_api_key=openai_api_key,
parent=self)
general_tab_widget.openai_api_key_changed.connect(self.openai_api_key_changed)
tab_widget.addTab(general_tab_widget, _('General'))
shortcuts_table_widget = ShortcutsEditorPreferencesWidget(shortcuts, self)
shortcuts_table_widget.shortcuts_changed.connect(self.shortcuts_changed)
tab_widget.addTab(shortcuts_table_widget, _('Shortcuts'))

View file

@ -9,7 +9,7 @@ from PyQt6.QtWidgets import QKeySequenceEdit, QWidget, QFormLayout, QPushButton
from buzz.settings.shortcut import Shortcut
class ShortcutsEditorWidget(QWidget):
class ShortcutsEditorPreferencesWidget(QWidget):
shortcuts_changed = pyqtSignal(dict)
def __init__(self, shortcuts: Dict[str, str], parent: Optional[QWidget] = None):

157
poetry.lock generated
View file

@ -477,6 +477,48 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1
[package.extras]
toml = ["tomli"]
[[package]]
name = "cryptography"
version = "40.0.2"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "cryptography-40.0.2-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:8f79b5ff5ad9d3218afb1e7e20ea74da5f76943ee5edb7f76e56ec5161ec782b"},
{file = "cryptography-40.0.2-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:05dc219433b14046c476f6f09d7636b92a1c3e5808b9a6536adf4932b3b2c440"},
{file = "cryptography-40.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4df2af28d7bedc84fe45bd49bc35d710aede676e2a4cb7fc6d103a2adc8afe4d"},
{file = "cryptography-40.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dcca15d3a19a66e63662dc8d30f8036b07be851a8680eda92d079868f106288"},
{file = "cryptography-40.0.2-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a04386fb7bc85fab9cd51b6308633a3c271e3d0d3eae917eebab2fac6219b6d2"},
{file = "cryptography-40.0.2-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:adc0d980fd2760c9e5de537c28935cc32b9353baaf28e0814df417619c6c8c3b"},
{file = "cryptography-40.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d5a1bd0e9e2031465761dfa920c16b0065ad77321d8a8c1f5ee331021fda65e9"},
{file = "cryptography-40.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a95f4802d49faa6a674242e25bfeea6fc2acd915b5e5e29ac90a32b1139cae1c"},
{file = "cryptography-40.0.2-cp36-abi3-win32.whl", hash = "sha256:aecbb1592b0188e030cb01f82d12556cf72e218280f621deed7d806afd2113f9"},
{file = "cryptography-40.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:b12794f01d4cacfbd3177b9042198f3af1c856eedd0a98f10f141385c809a14b"},
{file = "cryptography-40.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:142bae539ef28a1c76794cca7f49729e7c54423f615cfd9b0b1fa90ebe53244b"},
{file = "cryptography-40.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:956ba8701b4ffe91ba59665ed170a2ebbdc6fc0e40de5f6059195d9f2b33ca0e"},
{file = "cryptography-40.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f01c9863da784558165f5d4d916093737a75203a5c5286fde60e503e4276c7a"},
{file = "cryptography-40.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3daf9b114213f8ba460b829a02896789751626a2a4e7a43a28ee77c04b5e4958"},
{file = "cryptography-40.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48f388d0d153350f378c7f7b41497a54ff1513c816bcbbcafe5b829e59b9ce5b"},
{file = "cryptography-40.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c0764e72b36a3dc065c155e5b22f93df465da9c39af65516fe04ed3c68c92636"},
{file = "cryptography-40.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cbaba590180cba88cb99a5f76f90808a624f18b169b90a4abb40c1fd8c19420e"},
{file = "cryptography-40.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7a38250f433cd41df7fcb763caa3ee9362777fdb4dc642b9a349721d2bf47404"},
{file = "cryptography-40.0.2.tar.gz", hash = "sha256:c33c0d32b8594fa647d2e01dbccc303478e16fdd7cf98652d5b3ed11aa5e5c99"},
]
[package.dependencies]
cffi = ">=1.12"
[package.extras]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
pep8test = ["black", "check-manifest", "mypy", "ruff"]
sdist = ["setuptools-rust (>=0.11.4)"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist"]
test-randomorder = ["pytest-randomly"]
tox = ["tox"]
[[package]]
name = "ctranslate2"
version = "3.11.0"
@ -821,6 +863,26 @@ files = [
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
]
[[package]]
name = "importlib-metadata"
version = "6.6.0"
description = "Read metadata from Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "importlib_metadata-6.6.0-py3-none-any.whl", hash = "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed"},
{file = "importlib_metadata-6.6.0.tar.gz", hash = "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705"},
]
[package.dependencies]
zipp = ">=0.5"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
perf = ["ipython"]
testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
[[package]]
name = "iniconfig"
version = "1.1.1"
@ -851,6 +913,65 @@ pipfile-deprecated-finder = ["pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "jaraco-classes"
version = "3.2.3"
description = "Utility functions for Python class constructs"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "jaraco.classes-3.2.3-py3-none-any.whl", hash = "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158"},
{file = "jaraco.classes-3.2.3.tar.gz", hash = "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a"},
]
[package.dependencies]
more-itertools = "*"
[package.extras]
docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
[[package]]
name = "jeepney"
version = "0.8.0"
description = "Low-level, pure Python DBus protocol wrapper."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"},
{file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"},
]
[package.extras]
test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"]
trio = ["async_generator", "trio"]
[[package]]
name = "keyring"
version = "23.13.1"
description = "Store and access your passwords safely."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "keyring-23.13.1-py3-none-any.whl", hash = "sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd"},
{file = "keyring-23.13.1.tar.gz", hash = "sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678"},
]
[package.dependencies]
importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""}
"jaraco.classes" = "*"
jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""}
pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""}
SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
[package.extras]
completion = ["shtab"]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
[[package]]
name = "lazy-object-proxy"
version = "1.8.0"
@ -1507,7 +1628,7 @@ files = [
name = "pywin32-ctypes"
version = "0.2.0"
description = ""
category = "dev"
category = "main"
optional = false
python-versions = "*"
files = [
@ -1685,6 +1806,22 @@ urllib3 = ">=1.21.1,<1.27"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "secretstorage"
version = "3.3.3"
description = "Python bindings to FreeDesktop.org Secret Service API"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"},
{file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"},
]
[package.dependencies]
cryptography = ">=2.0"
jeepney = ">=0.6"
[[package]]
name = "setuptools"
version = "65.6.3"
@ -2213,7 +2350,23 @@ files = [
idna = ">=2.0"
multidict = ">=4.0"
[[package]]
name = "zipp"
version = "3.15.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
{file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.9.13,<3.11"
content-hash = "1221c97d2440f0de33521f1c1f97f89d52d43ec18984629f591421c11012ebfc"
content-hash = "10dd56bee87a09ccb6c7abf32fcda50d7379a6c5d5e87c59ca4aed48cd648d86"

View file

@ -19,6 +19,7 @@ PyQt6 = "^6.4.0"
stable-ts = "^1.0.1"
openai = "^0.27.1"
faster-whisper = "^0.4.1"
keyring = "^23.13.1"
[tool.poetry.group.dev.dependencies]
autopep8 = "^1.7.0"

View file

@ -0,0 +1,43 @@
from unittest.mock import Mock
from PyQt6.QtWidgets import QPushButton, QMessageBox, QLineEdit
from buzz.widgets.general_preferences_widget import GeneralPreferencesWidget
class TestGeneralPreferencesWidget:
def test_should_disable_test_button_if_no_api_key(self, qtbot):
widget = GeneralPreferencesWidget(openai_api_key='')
qtbot.add_widget(widget)
test_button = widget.findChild(QPushButton)
assert isinstance(test_button, QPushButton)
assert test_button.text() == 'Test'
assert not test_button.isEnabled()
line_edit = widget.findChild(QLineEdit)
assert isinstance(line_edit, QLineEdit)
line_edit.setText('123')
assert test_button.isEnabled()
def test_should_test_openai_api_key(self, qtbot):
widget = GeneralPreferencesWidget(openai_api_key='wrong-api-key')
qtbot.add_widget(widget)
test_button = widget.findChild(QPushButton)
assert isinstance(test_button, QPushButton)
test_button.click()
mock = Mock()
QMessageBox.warning = mock
def mock_called():
mock.assert_called()
assert mock.call_args[0][1] == 'OpenAI API Key Test'
assert mock.call_args[0][
2] == 'Incorrect API key provided: wrong-ap*-key. You can find your API key at https://platform.openai.com/account/api-keys.'
qtbot.waitUntil(mock_called)

View file

@ -0,0 +1,24 @@
from buzz.widgets.openai_api_key_line_edit import OpenAIAPIKeyLineEdit
class TestOpenAIAPIKeyLineEdit:
def test_should_emit_key_changed(self, qtbot):
line_edit = OpenAIAPIKeyLineEdit(key='')
qtbot.add_widget(line_edit)
with qtbot.wait_signal(line_edit.key_changed):
line_edit.setText('abcdefg')
def test_should_toggle_visibility(self, qtbot):
line_edit = OpenAIAPIKeyLineEdit(key='')
qtbot.add_widget(line_edit)
assert line_edit.echoMode() == OpenAIAPIKeyLineEdit.EchoMode.Password
toggle_action = line_edit.actions()[0]
toggle_action.trigger()
assert line_edit.echoMode() == OpenAIAPIKeyLineEdit.EchoMode.Normal
toggle_action.trigger()
assert line_edit.echoMode() == OpenAIAPIKeyLineEdit.EchoMode.Password

View file

@ -6,11 +6,13 @@ from buzz.widgets.preferences_dialog import PreferencesDialog
class TestPreferencesDialog:
def test_create(self, qtbot: QtBot):
dialog = PreferencesDialog(shortcuts={})
dialog = PreferencesDialog(shortcuts={}, openai_api_key='')
qtbot.add_widget(dialog)
assert dialog.windowTitle() == 'Preferences'
tab_widget = dialog.findChild(QTabWidget)
assert isinstance(tab_widget, QTabWidget)
assert tab_widget.tabText(0) == 'Shortcuts'
assert tab_widget.count() == 2
assert tab_widget.tabText(0) == 'General'
assert tab_widget.tabText(1) == 'Shortcuts'

View file

@ -1,12 +1,12 @@
from PyQt6.QtWidgets import QPushButton, QLabel
from buzz.settings.shortcut import Shortcut
from buzz.widgets.shortcuts_editor_widget import ShortcutsEditorWidget, SequenceEdit
from buzz.widgets.shortcuts_editor_preferences_widget import ShortcutsEditorPreferencesWidget, SequenceEdit
class TestShortcutsEditorWidget:
def test_should_reset_to_defaults(self, qtbot):
widget = ShortcutsEditorWidget(shortcuts=Shortcut.get_default_shortcuts())
widget = ShortcutsEditorPreferencesWidget(shortcuts=Shortcut.get_default_shortcuts())
qtbot.add_widget(widget)
reset_button = widget.findChild(QPushButton)