mirror of
https://github.com/chidiwilliams/buzz.git
synced 2024-06-28 04:30:06 +02:00
Add keyring store for API keys (#411)
This commit is contained in:
parent
49a3151117
commit
d68c2b9461
1
assets/visibility_FILL0_wght700_GRAD0_opsz48.svg
Normal file
1
assets/visibility_FILL0_wght700_GRAD0_opsz48.svg
Normal 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 |
1
assets/visibility_off_FILL0_wght700_GRAD0_opsz48.svg
Normal file
1
assets/visibility_off_FILL0_wght700_GRAD0_opsz48.svg
Normal 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 |
42
buzz/gui.py
42
buzz/gui.py
|
@ -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
0
buzz/store/__init__.py
Normal file
27
buzz/store/keyring_store.py
Normal file
27
buzz/store/keyring_store.py
Normal 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)
|
78
buzz/widgets/general_preferences_widget.py
Normal file
78
buzz/widgets/general_preferences_widget.py
Normal 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
11
buzz/widgets/line_edit.py
Normal 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 }')
|
39
buzz/widgets/openai_api_key_line_edit.py
Normal file
39
buzz/widgets/openai_api_key_line_edit.py
Normal 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)
|
|
@ -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'))
|
||||
|
||||
|
|
|
@ -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
157
poetry.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
43
tests/widgets/general_preferences_widget_test.py
Normal file
43
tests/widgets/general_preferences_widget_test.py
Normal 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)
|
24
tests/widgets/openai_api_key_line_edit_test.py
Normal file
24
tests/widgets/openai_api_key_line_edit_test.py
Normal 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
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue