pygpt-net 2.6.36__py3-none-any.whl → 2.6.38__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pygpt_net/CHANGELOG.txt +12 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/controller/chat/handler/anthropic_stream.py +164 -0
- pygpt_net/controller/chat/handler/google_stream.py +181 -0
- pygpt_net/controller/chat/handler/langchain_stream.py +24 -0
- pygpt_net/controller/chat/handler/llamaindex_stream.py +47 -0
- pygpt_net/controller/chat/handler/openai_stream.py +260 -0
- pygpt_net/controller/chat/handler/utils.py +210 -0
- pygpt_net/controller/chat/handler/worker.py +570 -0
- pygpt_net/controller/chat/handler/xai_stream.py +135 -0
- pygpt_net/controller/chat/stream.py +1 -1
- pygpt_net/controller/ctx/ctx.py +1 -1
- pygpt_net/controller/debug/debug.py +6 -6
- pygpt_net/controller/model/editor.py +3 -0
- pygpt_net/controller/model/importer.py +9 -2
- pygpt_net/controller/plugins/plugins.py +11 -3
- pygpt_net/controller/presets/presets.py +2 -2
- pygpt_net/core/bridge/context.py +35 -35
- pygpt_net/core/bridge/worker.py +40 -16
- pygpt_net/core/ctx/bag.py +7 -2
- pygpt_net/core/ctx/reply.py +17 -2
- pygpt_net/core/db/viewer.py +19 -34
- pygpt_net/core/render/plain/pid.py +12 -1
- pygpt_net/core/render/web/body.py +30 -39
- pygpt_net/core/tabs/tab.py +24 -1
- pygpt_net/data/config/config.json +10 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +105 -0
- pygpt_net/data/css/style.dark.css +2 -3
- pygpt_net/data/css/style.light.css +2 -3
- pygpt_net/data/locale/locale.de.ini +3 -1
- pygpt_net/data/locale/locale.en.ini +19 -1
- pygpt_net/data/locale/locale.es.ini +3 -1
- pygpt_net/data/locale/locale.fr.ini +3 -1
- pygpt_net/data/locale/locale.it.ini +3 -1
- pygpt_net/data/locale/locale.pl.ini +4 -2
- pygpt_net/data/locale/locale.uk.ini +3 -1
- pygpt_net/data/locale/locale.zh.ini +3 -1
- pygpt_net/item/assistant.py +51 -2
- pygpt_net/item/attachment.py +21 -20
- pygpt_net/item/calendar_note.py +19 -2
- pygpt_net/item/ctx.py +115 -2
- pygpt_net/item/index.py +9 -2
- pygpt_net/item/mode.py +9 -6
- pygpt_net/item/model.py +20 -3
- pygpt_net/item/notepad.py +14 -2
- pygpt_net/item/preset.py +42 -2
- pygpt_net/item/prompt.py +8 -2
- pygpt_net/plugin/cmd_files/plugin.py +2 -2
- pygpt_net/provider/api/__init__.py +5 -3
- pygpt_net/provider/api/anthropic/__init__.py +190 -29
- pygpt_net/provider/api/anthropic/audio.py +30 -0
- pygpt_net/provider/api/anthropic/chat.py +341 -0
- pygpt_net/provider/api/anthropic/image.py +25 -0
- pygpt_net/provider/api/anthropic/tools.py +266 -0
- pygpt_net/provider/api/anthropic/vision.py +142 -0
- pygpt_net/provider/api/google/chat.py +2 -2
- pygpt_net/provider/api/google/realtime/client.py +2 -2
- pygpt_net/provider/api/google/tools.py +58 -48
- pygpt_net/provider/api/google/vision.py +7 -1
- pygpt_net/provider/api/openai/chat.py +1 -0
- pygpt_net/provider/api/openai/vision.py +6 -0
- pygpt_net/provider/api/x_ai/__init__.py +247 -0
- pygpt_net/provider/api/x_ai/audio.py +32 -0
- pygpt_net/provider/api/x_ai/chat.py +968 -0
- pygpt_net/provider/api/x_ai/image.py +208 -0
- pygpt_net/provider/api/x_ai/remote.py +262 -0
- pygpt_net/provider/api/x_ai/tools.py +120 -0
- pygpt_net/provider/api/x_ai/vision.py +119 -0
- pygpt_net/provider/core/attachment/json_file.py +2 -2
- pygpt_net/provider/core/config/patch.py +28 -0
- pygpt_net/provider/llms/anthropic.py +4 -2
- pygpt_net/tools/text_editor/tool.py +4 -1
- pygpt_net/tools/text_editor/ui/dialogs.py +1 -1
- pygpt_net/ui/base/config_dialog.py +5 -11
- pygpt_net/ui/dialog/db.py +177 -59
- pygpt_net/ui/dialog/dictionary.py +57 -59
- pygpt_net/ui/dialog/editor.py +3 -2
- pygpt_net/ui/dialog/image.py +1 -1
- pygpt_net/ui/dialog/logger.py +3 -2
- pygpt_net/ui/dialog/models.py +16 -16
- pygpt_net/ui/dialog/plugins.py +63 -60
- pygpt_net/ui/layout/ctx/ctx_list.py +3 -4
- pygpt_net/ui/layout/toolbox/__init__.py +2 -2
- pygpt_net/ui/layout/toolbox/assistants.py +8 -9
- pygpt_net/ui/layout/toolbox/presets.py +2 -2
- pygpt_net/ui/main.py +9 -4
- pygpt_net/ui/widget/element/labels.py +20 -4
- pygpt_net/ui/widget/textarea/editor.py +0 -4
- pygpt_net/ui/widget/textarea/web.py +1 -1
- {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/METADATA +18 -6
- {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/RECORD +95 -76
- pygpt_net/controller/chat/handler/stream_worker.py +0 -1136
- {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# ================================================== #
|
|
4
|
+
# This file is a part of PYGPT package #
|
|
5
|
+
# Website: https://pygpt.net #
|
|
6
|
+
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
|
+
# MIT License #
|
|
8
|
+
# Created By : Marcin Szczygliński #
|
|
9
|
+
# Updated Date: 2025.09.05 01:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from typing import Optional, Dict, List
|
|
14
|
+
|
|
15
|
+
from pygpt_net.item.attachment import AttachmentItem
|
|
16
|
+
from pygpt_net.item.ctx import CtxItem
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Vision:
|
|
20
|
+
def __init__(self, window=None):
|
|
21
|
+
"""
|
|
22
|
+
Vision helpers for xAI (image inputs as data: URIs).
|
|
23
|
+
|
|
24
|
+
:param window: Window instance
|
|
25
|
+
"""
|
|
26
|
+
self.window = window
|
|
27
|
+
self.attachments: Dict[str, str] = {}
|
|
28
|
+
self.urls: List[str] = []
|
|
29
|
+
self.input_tokens = 0
|
|
30
|
+
self.allowed_mimes = {"image/jpeg", "image/png"}
|
|
31
|
+
|
|
32
|
+
def build_images_for_chat(self, attachments: Optional[Dict[str, AttachmentItem]]) -> List[str]:
|
|
33
|
+
"""
|
|
34
|
+
Build image inputs for xai_sdk.chat.image(...).
|
|
35
|
+
Returns list of image sources (URLs or data: URIs).
|
|
36
|
+
|
|
37
|
+
:param attachments: Attachments dict (id -> AttachmentItem)
|
|
38
|
+
:return: List of image sources
|
|
39
|
+
"""
|
|
40
|
+
import base64
|
|
41
|
+
|
|
42
|
+
images: List[str] = []
|
|
43
|
+
self.attachments = {}
|
|
44
|
+
self.urls = []
|
|
45
|
+
|
|
46
|
+
if not attachments:
|
|
47
|
+
return images
|
|
48
|
+
|
|
49
|
+
for id_, att in (attachments or {}).items():
|
|
50
|
+
try:
|
|
51
|
+
if att.path and self.window.core.api.xai.vision.is_image(att.path):
|
|
52
|
+
mime = self.window.core.api.xai.vision.guess_mime(att.path)
|
|
53
|
+
# Accept only JPEG/PNG for SDK too (for consistency)
|
|
54
|
+
#if mime not in self.allowed_mimes:
|
|
55
|
+
# continue
|
|
56
|
+
with open(att.path, "rb") as f:
|
|
57
|
+
b64 = base64.b64encode(f.read()).decode("utf-8")
|
|
58
|
+
images.append(f"data:{mime};base64,{b64}")
|
|
59
|
+
self.attachments[id_] = att.path
|
|
60
|
+
att.consumed = True
|
|
61
|
+
except Exception:
|
|
62
|
+
continue
|
|
63
|
+
return images
|
|
64
|
+
|
|
65
|
+
def is_image(self, path: str) -> bool:
|
|
66
|
+
"""
|
|
67
|
+
Return True if path looks like an image file.
|
|
68
|
+
|
|
69
|
+
:param path: File path
|
|
70
|
+
"""
|
|
71
|
+
return path.lower().endswith(('.png', '.jpg', '.jpeg', '.tiff', '.bmp', '.gif', '.webp'))
|
|
72
|
+
|
|
73
|
+
def guess_mime(self, path: str) -> str:
|
|
74
|
+
"""Guess MIME by extension.
|
|
75
|
+
|
|
76
|
+
:param path: File path
|
|
77
|
+
:return: MIME type string
|
|
78
|
+
"""
|
|
79
|
+
ext = os.path.splitext(path)[1].lower().lstrip(".")
|
|
80
|
+
if ext in ("jpg", "jpeg"):
|
|
81
|
+
return "image/jpeg"
|
|
82
|
+
if ext == "png":
|
|
83
|
+
return "image/png"
|
|
84
|
+
if ext == "gif":
|
|
85
|
+
return "image/gif"
|
|
86
|
+
if ext == "bmp":
|
|
87
|
+
return "image/bmp"
|
|
88
|
+
if ext == "webp":
|
|
89
|
+
return "image/webp"
|
|
90
|
+
if ext == "tiff":
|
|
91
|
+
return "image/tiff"
|
|
92
|
+
return "image/jpeg"
|
|
93
|
+
|
|
94
|
+
def append_images(self, ctx: CtxItem):
|
|
95
|
+
"""
|
|
96
|
+
Append sent images list to context for UI/history.
|
|
97
|
+
|
|
98
|
+
:param ctx: CtxItem
|
|
99
|
+
"""
|
|
100
|
+
images = self.get_attachments()
|
|
101
|
+
if len(images) > 0:
|
|
102
|
+
ctx.images = self.window.core.filesystem.make_local_list(list(images.values()))
|
|
103
|
+
|
|
104
|
+
def get_attachments(self) -> Dict[str, str]:
|
|
105
|
+
return self.attachments
|
|
106
|
+
|
|
107
|
+
def get_urls(self) -> List[str]:
|
|
108
|
+
return self.urls
|
|
109
|
+
|
|
110
|
+
def reset_tokens(self):
|
|
111
|
+
self.input_tokens = 0
|
|
112
|
+
|
|
113
|
+
def get_used_tokens(self) -> int:
|
|
114
|
+
return self.input_tokens
|
|
115
|
+
|
|
116
|
+
def reset(self):
|
|
117
|
+
self.attachments = {}
|
|
118
|
+
self.urls = []
|
|
119
|
+
self.input_tokens = 0
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date:
|
|
9
|
+
# Updated Date: 2025.09.05 18:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import json
|
|
@@ -162,7 +162,7 @@ class JsonFileProvider(BaseProvider):
|
|
|
162
162
|
attachment.name = data['name']
|
|
163
163
|
if 'path' in data:
|
|
164
164
|
attachment.path = data['path']
|
|
165
|
-
if '
|
|
165
|
+
if 'remote' in data:
|
|
166
166
|
attachment.remote = data['remote']
|
|
167
167
|
if 'send' in data:
|
|
168
168
|
attachment.send = data['send']
|
|
@@ -2447,6 +2447,34 @@ class Patch:
|
|
|
2447
2447
|
patch_css('web-blocks.css', True)
|
|
2448
2448
|
updated = True
|
|
2449
2449
|
|
|
2450
|
+
# < 2.6.37
|
|
2451
|
+
if old < parse_version("2.6.37"):
|
|
2452
|
+
print("Migrating config from < 2.6.37...")
|
|
2453
|
+
|
|
2454
|
+
# add: label-desc CSS
|
|
2455
|
+
patch_css('style.dark.css', True)
|
|
2456
|
+
patch_css('style.light.css', True)
|
|
2457
|
+
|
|
2458
|
+
# add: Anthropic SDK
|
|
2459
|
+
if "api_native_anthropic" not in data:
|
|
2460
|
+
data["api_native_anthropic"] = True
|
|
2461
|
+
if "remote_tools.anthropic.web_search" not in data:
|
|
2462
|
+
data["remote_tools.anthropic.web_search"] = True
|
|
2463
|
+
|
|
2464
|
+
# add: xAI SDK
|
|
2465
|
+
if "api_native_xai" not in data:
|
|
2466
|
+
data["api_native_xai"] = True
|
|
2467
|
+
if "remote_tools.xai.mode" not in data:
|
|
2468
|
+
data["remote_tools.xai.mode"] = "auto"
|
|
2469
|
+
if "remote_tools.xai.sources.web" not in data:
|
|
2470
|
+
data["remote_tools.xai.sources.web"] = True
|
|
2471
|
+
if "remote_tools.xai.sources.x" not in data:
|
|
2472
|
+
data["remote_tools.xai.sources.x"] = True
|
|
2473
|
+
if "remote_tools.xai.sources.news" not in data:
|
|
2474
|
+
data["remote_tools.xai.sources.news"] = False
|
|
2475
|
+
|
|
2476
|
+
updated = True
|
|
2477
|
+
|
|
2450
2478
|
# update file
|
|
2451
2479
|
migrated = False
|
|
2452
2480
|
if updated:
|
|
@@ -15,7 +15,7 @@ from llama_index.core.base.embeddings.base import BaseEmbedding
|
|
|
15
15
|
from llama_index.core.llms.llm import BaseLLM as LlamaBaseLLM
|
|
16
16
|
|
|
17
17
|
from pygpt_net.core.types import (
|
|
18
|
-
MODE_LLAMA_INDEX,
|
|
18
|
+
MODE_LLAMA_INDEX, MODE_CHAT,
|
|
19
19
|
)
|
|
20
20
|
from pygpt_net.provider.llms.base import BaseLLM
|
|
21
21
|
from pygpt_net.item.model import ModelItem
|
|
@@ -93,7 +93,9 @@ class AnthropicLLM(BaseLLM):
|
|
|
93
93
|
:param window: window instance
|
|
94
94
|
:return: list of models
|
|
95
95
|
"""
|
|
96
|
-
|
|
96
|
+
model = ModelItem()
|
|
97
|
+
model.provider = "anthropic"
|
|
98
|
+
client = window.core.api.anthropic.get_client(MODE_CHAT, model)
|
|
97
99
|
models_list = client.models.list()
|
|
98
100
|
items = []
|
|
99
101
|
if models_list.data:
|
|
@@ -183,16 +183,19 @@ class TextEditor(BaseTool):
|
|
|
183
183
|
self.window.core.filesystem.editor.destroy(id) # unregister from memory
|
|
184
184
|
self.window.ui.dialogs.close(id)
|
|
185
185
|
|
|
186
|
-
def save(self, id: str) -> str:
|
|
186
|
+
def save(self, id: str, close: bool = False) -> str:
|
|
187
187
|
"""
|
|
188
188
|
Save content to current file
|
|
189
189
|
|
|
190
190
|
:param id: editor id
|
|
191
|
+
:param close: close editor after save
|
|
191
192
|
:return: new editor id
|
|
192
193
|
"""
|
|
193
194
|
file = self.window.ui.dialog[id].file
|
|
194
195
|
if file:
|
|
195
196
|
self.window.core.filesystem.editor.save(id)
|
|
197
|
+
if close:
|
|
198
|
+
self.close(id)
|
|
196
199
|
else:
|
|
197
200
|
id = self.save_as_file(id)
|
|
198
201
|
return id
|
|
@@ -47,7 +47,7 @@ class DialogSpawner:
|
|
|
47
47
|
lambda: self.window.tools.get("editor").restore(id)
|
|
48
48
|
)
|
|
49
49
|
self.window.ui.nodes['editor.custom.btn.save'].clicked.connect(
|
|
50
|
-
lambda: self.window.tools.get("editor").save(id)
|
|
50
|
+
lambda: self.window.tools.get("editor").save(id, close=True)
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
bottom_layout = QHBoxLayout()
|
|
@@ -6,14 +6,13 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.
|
|
9
|
+
# Updated Date: 2025.09.04 00:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtCore import Qt
|
|
13
13
|
from PySide6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QSizePolicy, QWidget, QFrame
|
|
14
|
-
from PySide6.QtGui import QFont
|
|
15
14
|
|
|
16
|
-
from pygpt_net.ui.widget.element.labels import TitleLabel, UrlLabel
|
|
15
|
+
from pygpt_net.ui.widget.element.labels import TitleLabel, UrlLabel, DescLabel, BaseLabel
|
|
17
16
|
from pygpt_net.ui.widget.option.checkbox import OptionCheckbox
|
|
18
17
|
from pygpt_net.ui.widget.option.checkbox_list import OptionCheckboxList
|
|
19
18
|
from pygpt_net.ui.widget.option.combo import OptionCombo
|
|
@@ -123,7 +122,7 @@ class BaseConfigDialog:
|
|
|
123
122
|
if extra.get('bold'):
|
|
124
123
|
nodes[label_key] = TitleLabel(txt)
|
|
125
124
|
else:
|
|
126
|
-
nodes[label_key] =
|
|
125
|
+
nodes[label_key] = BaseLabel(txt)
|
|
127
126
|
nodes[label_key].setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
|
|
128
127
|
nodes[label_key].setMinimumWidth(120)
|
|
129
128
|
nodes[label_key].setWordWrap(True)
|
|
@@ -169,7 +168,7 @@ class BaseConfigDialog:
|
|
|
169
168
|
if extra.get('bold'):
|
|
170
169
|
nodes[label_key] = TitleLabel(txt)
|
|
171
170
|
else:
|
|
172
|
-
nodes[label_key] =
|
|
171
|
+
nodes[label_key] = BaseLabel(txt)
|
|
173
172
|
nodes[label_key].setMinimumHeight(30)
|
|
174
173
|
nodes[label_key].setWordWrap(True)
|
|
175
174
|
|
|
@@ -234,12 +233,7 @@ class BaseConfigDialog:
|
|
|
234
233
|
:param text: text (to translate)
|
|
235
234
|
:return: QLabel
|
|
236
235
|
"""
|
|
237
|
-
|
|
238
|
-
label = QLabel(desc)
|
|
239
|
-
label.setWordWrap(True)
|
|
240
|
-
label.setMaximumHeight(80)
|
|
241
|
-
label.setStyleSheet("font-size: 10px;")
|
|
242
|
-
return label
|
|
236
|
+
return DescLabel(trans(text))
|
|
243
237
|
|
|
244
238
|
def add_urls(self, urls, align=Qt.AlignLeft) -> QWidget:
|
|
245
239
|
"""
|
pygpt_net/ui/dialog/db.py
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.
|
|
9
|
+
# Updated Date: 2025.09.05 18:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
-
from PySide6.QtCore import Qt
|
|
13
|
-
from PySide6.QtGui import QAction, QIcon
|
|
12
|
+
from PySide6.QtCore import Qt, QTimer, QSignalBlocker
|
|
13
|
+
from PySide6.QtGui import QAction, QIcon, QIntValidator
|
|
14
14
|
from PySide6.QtWidgets import QGridLayout, QScrollArea, QSplitter, QComboBox, QLineEdit, QPushButton, \
|
|
15
|
-
QHBoxLayout, QVBoxLayout, QLabel, QWidget, QSizePolicy, QCheckBox, QMenuBar
|
|
15
|
+
QHBoxLayout, QVBoxLayout, QLabel, QWidget, QSizePolicy, QCheckBox, QMenuBar, QAbstractItemView, QHeaderView, QStyledItemDelegate
|
|
16
16
|
|
|
17
17
|
from pygpt_net.ui.widget.dialog.db import DatabaseDialog
|
|
18
18
|
from pygpt_net.ui.widget.lists.db import DatabaseList, DatabaseTableModel
|
|
@@ -20,6 +20,18 @@ from pygpt_net.ui.widget.textarea.editor import CodeEditor
|
|
|
20
20
|
from pygpt_net.utils import trans
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
class _FastTextDelegate(QStyledItemDelegate):
|
|
24
|
+
def __init__(self, max_chars=1200, parent=None):
|
|
25
|
+
super().__init__(parent)
|
|
26
|
+
self.max_chars = max_chars
|
|
27
|
+
|
|
28
|
+
def displayText(self, value, locale):
|
|
29
|
+
s = "" if value is None else str(value)
|
|
30
|
+
if len(s) > self.max_chars:
|
|
31
|
+
return s[:self.max_chars] + "…"
|
|
32
|
+
return s
|
|
33
|
+
|
|
34
|
+
|
|
23
35
|
class Database:
|
|
24
36
|
def __init__(self, window=None):
|
|
25
37
|
"""
|
|
@@ -29,6 +41,8 @@ class Database:
|
|
|
29
41
|
"""
|
|
30
42
|
self.window = window
|
|
31
43
|
self.viewer = None
|
|
44
|
+
self._splitter_timer = None
|
|
45
|
+
self._text_viewer = None
|
|
32
46
|
|
|
33
47
|
def setup(self, id: str = "db"):
|
|
34
48
|
"""
|
|
@@ -45,6 +59,7 @@ class Database:
|
|
|
45
59
|
# data viewer
|
|
46
60
|
text_viewer = CodeEditor(self.window)
|
|
47
61
|
text_viewer.setReadOnly(False)
|
|
62
|
+
self._text_viewer = text_viewer
|
|
48
63
|
|
|
49
64
|
self.window.ui.debug[id].browser = self.viewer
|
|
50
65
|
self.window.ui.debug[id].viewer = text_viewer
|
|
@@ -63,6 +78,12 @@ class Database:
|
|
|
63
78
|
splitter.addWidget(editor_widget) # Value viewer
|
|
64
79
|
splitter.setStretchFactor(0, 3)
|
|
65
80
|
splitter.setStretchFactor(1, 1)
|
|
81
|
+
splitter.setOpaqueResize(False)
|
|
82
|
+
|
|
83
|
+
self._splitter_timer = QTimer(self.window)
|
|
84
|
+
self._splitter_timer.setSingleShot(True)
|
|
85
|
+
self._splitter_timer.timeout.connect(self._finish_splitter_move)
|
|
86
|
+
splitter.splitterMoved.connect(self._on_splitter_moved)
|
|
66
87
|
|
|
67
88
|
self.menu_bar = QMenuBar()
|
|
68
89
|
self.batch_actions_menu = self.menu_bar.addMenu("Actions")
|
|
@@ -81,6 +102,18 @@ class Database:
|
|
|
81
102
|
self.window.ui.dialog['debug.db'].setLayout(layout)
|
|
82
103
|
self.window.ui.dialog['debug.db'].setWindowTitle("Debug: Database (SQLite)")
|
|
83
104
|
|
|
105
|
+
def _on_splitter_moved(self, pos, index):
|
|
106
|
+
if self._text_viewer is not None:
|
|
107
|
+
self._text_viewer.setUpdatesEnabled(False)
|
|
108
|
+
self._splitter_timer.start(120)
|
|
109
|
+
|
|
110
|
+
def _finish_splitter_move(self):
|
|
111
|
+
if self._text_viewer is not None:
|
|
112
|
+
self._text_viewer.setUpdatesEnabled(True)
|
|
113
|
+
if hasattr(self._text_viewer, 'viewport'):
|
|
114
|
+
self._text_viewer.viewport().update()
|
|
115
|
+
|
|
116
|
+
|
|
84
117
|
class DataBrowser(QWidget):
|
|
85
118
|
def __init__(self, window=None):
|
|
86
119
|
super().__init__()
|
|
@@ -111,7 +144,7 @@ class DataBrowser(QWidget):
|
|
|
111
144
|
|
|
112
145
|
# buttons
|
|
113
146
|
self.refresh_button = QPushButton(trans("db.refresh"))
|
|
114
|
-
self.refresh_button.clicked.connect(self.
|
|
147
|
+
self.refresh_button.clicked.connect(self.force_refresh)
|
|
115
148
|
self.prev_button = QPushButton(trans("db.prev"))
|
|
116
149
|
self.next_button = QPushButton(trans("db.next"))
|
|
117
150
|
self.limit_input = QLineEdit("100")
|
|
@@ -126,7 +159,7 @@ class DataBrowser(QWidget):
|
|
|
126
159
|
self.page_input_label = QLabel(trans("db.page") + ":")
|
|
127
160
|
self.page_input_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
128
161
|
self.page_input.editingFinished.connect(self.on_page_input_change)
|
|
129
|
-
self.page_info_label = QLabel(" / 1")
|
|
162
|
+
self.page_info_label = QLabel(" / 1")
|
|
130
163
|
self.page_info_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
131
164
|
|
|
132
165
|
# checkboxes
|
|
@@ -155,18 +188,30 @@ class DataBrowser(QWidget):
|
|
|
155
188
|
self.table_select.addItems(self.get_table_names())
|
|
156
189
|
self.sort_order_select.addItems(['ASC', 'DESC'])
|
|
157
190
|
|
|
191
|
+
self._search_timer = QTimer(self)
|
|
192
|
+
self._search_timer.setSingleShot(True)
|
|
193
|
+
self._search_timer.timeout.connect(self.update_table_view)
|
|
194
|
+
self._last_fetch_sig = None
|
|
195
|
+
self._last_count_sig = None
|
|
196
|
+
self._cached_total_rows = 0
|
|
197
|
+
self._last_columns_sig = None
|
|
198
|
+
|
|
199
|
+
v = QIntValidator(1, 1000000000, self)
|
|
200
|
+
self.limit_input.setValidator(v)
|
|
201
|
+
self.page_input.setValidator(v)
|
|
202
|
+
|
|
158
203
|
# signals
|
|
159
204
|
self.table_select.currentIndexChanged.connect(self.on_table_select_changed)
|
|
160
205
|
self.sort_by_select.currentIndexChanged.connect(self.update_table_view)
|
|
161
206
|
self.sort_order_select.currentIndexChanged.connect(self.update_table_view)
|
|
162
|
-
self.search_input.textChanged.connect(self.
|
|
207
|
+
self.search_input.textChanged.connect(self.on_search_text_changed)
|
|
163
208
|
self.search_column_select.currentIndexChanged.connect(self.update_table_view)
|
|
164
209
|
|
|
165
210
|
self.prev_button.clicked.connect(self.prev_page)
|
|
166
211
|
self.next_button.clicked.connect(self.next_page)
|
|
167
212
|
|
|
168
213
|
self.table_select.setCurrentText(self.get_default_table())
|
|
169
|
-
self.on_table_select_changed()
|
|
214
|
+
self.on_table_select_changed()
|
|
170
215
|
|
|
171
216
|
# setup layouts
|
|
172
217
|
if not self.is_inline():
|
|
@@ -221,6 +266,27 @@ class DataBrowser(QWidget):
|
|
|
221
266
|
|
|
222
267
|
self.setLayout(main_layout)
|
|
223
268
|
|
|
269
|
+
view = self.get_list_widget()
|
|
270
|
+
if hasattr(view, "setUniformRowHeights"):
|
|
271
|
+
view.setUniformRowHeights(True)
|
|
272
|
+
if hasattr(view, "setWordWrap"):
|
|
273
|
+
view.setWordWrap(False)
|
|
274
|
+
if hasattr(view, "setTextElideMode"):
|
|
275
|
+
view.setTextElideMode(Qt.ElideRight)
|
|
276
|
+
if hasattr(view, "setVerticalScrollMode"):
|
|
277
|
+
view.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
|
|
278
|
+
if hasattr(view, "setHorizontalScrollMode"):
|
|
279
|
+
view.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
|
|
280
|
+
if hasattr(view, "setViewportUpdateMode"):
|
|
281
|
+
view.setViewportUpdateMode(QAbstractItemView.MinimalViewportUpdate)
|
|
282
|
+
if hasattr(view, "horizontalHeader"):
|
|
283
|
+
hh = view.horizontalHeader()
|
|
284
|
+
if hh is not None:
|
|
285
|
+
hh.setSectionResizeMode(QHeaderView.Interactive)
|
|
286
|
+
self._fast_delegate = _FastTextDelegate(parent=view)
|
|
287
|
+
if hasattr(view, "setItemDelegate"):
|
|
288
|
+
view.setItemDelegate(self._fast_delegate)
|
|
289
|
+
|
|
224
290
|
def is_inline(self) -> bool:
|
|
225
291
|
return False
|
|
226
292
|
|
|
@@ -339,6 +405,8 @@ class DataBrowser(QWidget):
|
|
|
339
405
|
self.limit_input.setText(str(current_limit))
|
|
340
406
|
return
|
|
341
407
|
self.current_offset = 0
|
|
408
|
+
self._last_fetch_sig = None
|
|
409
|
+
self._last_count_sig = None
|
|
342
410
|
self.update_table_view()
|
|
343
411
|
|
|
344
412
|
def get_page_size(self) -> int:
|
|
@@ -358,27 +426,18 @@ class DataBrowser(QWidget):
|
|
|
358
426
|
|
|
359
427
|
:param update: Update table view
|
|
360
428
|
"""
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
current_table = self.get_table_name(self.table_select.currentText())
|
|
368
|
-
total_rows = self.get_viewer().count_rows(
|
|
369
|
-
current_table,
|
|
370
|
-
search_query=search_query,
|
|
371
|
-
search_column=search_column,
|
|
372
|
-
filters=self.get_filters(),
|
|
373
|
-
)
|
|
429
|
+
try:
|
|
430
|
+
page = int(self.page_input.text()) - 1
|
|
431
|
+
except ValueError:
|
|
432
|
+
page = 0
|
|
433
|
+
limit = self.get_page_size()
|
|
434
|
+
total_rows = self._get_total_rows()
|
|
374
435
|
|
|
375
|
-
# check if page is in range
|
|
376
436
|
if page * limit < total_rows and page >= 0:
|
|
377
437
|
if update:
|
|
378
438
|
self.current_offset = page * limit
|
|
379
439
|
self.update_table_view()
|
|
380
440
|
else:
|
|
381
|
-
# reset page
|
|
382
441
|
current_page = self.current_offset // limit + 1
|
|
383
442
|
self.page_input.setText(str(current_page))
|
|
384
443
|
|
|
@@ -395,38 +454,33 @@ class DataBrowser(QWidget):
|
|
|
395
454
|
"""Table select change event"""
|
|
396
455
|
tables = self.get_tables()
|
|
397
456
|
current_table = self.get_table_name(self.table_select.currentText())
|
|
398
|
-
self.sort_by_select.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
457
|
+
with QSignalBlocker(self.sort_by_select), QSignalBlocker(self.sort_order_select):
|
|
458
|
+
self.sort_by_select.clear()
|
|
459
|
+
if current_table in tables:
|
|
460
|
+
self.sort_by_select.addItems(tables[current_table]['sort_by'])
|
|
461
|
+
self.sort_by_select.setCurrentText(tables[current_table]['default_sort'])
|
|
462
|
+
self.sort_order_select.setCurrentText(tables[current_table]['default_order'])
|
|
463
|
+
self.page_input.setText("1")
|
|
464
|
+
self.current_offset = 0
|
|
465
|
+
self.update_search_columns()
|
|
466
|
+
self._last_fetch_sig = None
|
|
467
|
+
self._last_count_sig = None
|
|
468
|
+
self._last_columns_sig = None
|
|
469
|
+
self.update_table_view()
|
|
407
470
|
|
|
408
471
|
def update_search_columns(self):
|
|
409
472
|
"""Update search columns"""
|
|
410
473
|
tables = self.get_tables()
|
|
411
474
|
current_table = self.get_table_name(self.table_select.currentText())
|
|
412
|
-
self.search_column_select
|
|
413
|
-
|
|
414
|
-
|
|
475
|
+
with QSignalBlocker(self.search_column_select):
|
|
476
|
+
self.search_column_select.clear()
|
|
477
|
+
self.search_column_select.addItem("*", None)
|
|
478
|
+
self.search_column_select.addItems(tables[current_table]['columns'])
|
|
415
479
|
|
|
416
480
|
def update_pagination_info(self):
|
|
417
481
|
"""Update pagination info"""
|
|
418
|
-
limit =
|
|
419
|
-
|
|
420
|
-
search_column = self.search_column_select.currentText()
|
|
421
|
-
if search_column == "*":
|
|
422
|
-
search_column = None
|
|
423
|
-
current_table = self.get_table_name(self.table_select.currentText())
|
|
424
|
-
total_rows = self.get_viewer().count_rows(
|
|
425
|
-
current_table,
|
|
426
|
-
search_query=search_query,
|
|
427
|
-
search_column=search_column,
|
|
428
|
-
filters=self.get_filters(),
|
|
429
|
-
)
|
|
482
|
+
limit = self.get_page_size()
|
|
483
|
+
total_rows = self._get_total_rows()
|
|
430
484
|
total_pages = (total_rows - 1) // limit + 1
|
|
431
485
|
self.page_info_label.setText(f" / {total_pages} ({total_rows} rows)")
|
|
432
486
|
|
|
@@ -435,21 +489,24 @@ class DataBrowser(QWidget):
|
|
|
435
489
|
self.prev_button.setEnabled(self.current_offset > 0)
|
|
436
490
|
self.next_button.setEnabled(self.current_offset + limit < total_rows)
|
|
437
491
|
|
|
438
|
-
def update_table_view(self):
|
|
492
|
+
def update_table_view(self, *args, force: bool = False):
|
|
439
493
|
"""Update table view"""
|
|
440
494
|
tables = self.get_tables()
|
|
441
495
|
current_table = self.get_table_name(self.table_select.currentText())
|
|
442
496
|
sort_by = self.sort_by_select.currentText()
|
|
443
497
|
sort_order = self.sort_order_select.currentText()
|
|
444
498
|
search_query = self.search_input.text()
|
|
445
|
-
search_column = self.search_column_select.
|
|
446
|
-
|
|
447
|
-
search_column = None
|
|
448
|
-
limit = int(self.limit_input.text())
|
|
499
|
+
search_column = self.search_column_select.currentData()
|
|
500
|
+
limit = self.get_page_size()
|
|
449
501
|
|
|
450
502
|
if current_table not in tables or sort_by == '' or sort_order == '' or limit <= 0:
|
|
451
503
|
return
|
|
452
504
|
|
|
505
|
+
sig = self._params_signature_fetch()
|
|
506
|
+
if not force and self._last_fetch_sig == sig:
|
|
507
|
+
self.update_pagination_info()
|
|
508
|
+
return
|
|
509
|
+
|
|
453
510
|
data = self.get_viewer().fetch_data(
|
|
454
511
|
table=current_table,
|
|
455
512
|
columns=tables[current_table]['columns'],
|
|
@@ -462,11 +519,12 @@ class DataBrowser(QWidget):
|
|
|
462
519
|
filters=self.get_filters(),
|
|
463
520
|
)
|
|
464
521
|
self.load_data(data, tables[current_table])
|
|
522
|
+
self._last_fetch_sig = sig
|
|
465
523
|
self.update_pagination_info()
|
|
466
524
|
|
|
467
525
|
def prev_page(self):
|
|
468
526
|
"""Previous page event"""
|
|
469
|
-
limit =
|
|
527
|
+
limit = self.get_page_size()
|
|
470
528
|
if self.current_offset - limit >= 0:
|
|
471
529
|
self.current_offset -= limit
|
|
472
530
|
else:
|
|
@@ -475,10 +533,8 @@ class DataBrowser(QWidget):
|
|
|
475
533
|
|
|
476
534
|
def next_page(self):
|
|
477
535
|
"""Next page event"""
|
|
478
|
-
limit =
|
|
479
|
-
|
|
480
|
-
total_rows = self.get_viewer().count_rows(current_table)
|
|
481
|
-
|
|
536
|
+
limit = self.get_page_size()
|
|
537
|
+
total_rows = self._get_total_rows()
|
|
482
538
|
if self.current_offset + limit < total_rows:
|
|
483
539
|
self.current_offset += limit
|
|
484
540
|
self.update_table_view()
|
|
@@ -498,11 +554,73 @@ class DataBrowser(QWidget):
|
|
|
498
554
|
self.db_path_label.setText(self.window.core.db.db_path)
|
|
499
555
|
if not self.is_inline():
|
|
500
556
|
convert_timestamps = self.convert_timestamps_checkbox.isChecked()
|
|
557
|
+
view = self.get_list_widget()
|
|
558
|
+
if hasattr(view, 'setSortingEnabled'):
|
|
559
|
+
view.setSortingEnabled(False)
|
|
560
|
+
view.setUpdatesEnabled(False)
|
|
501
561
|
model = DatabaseTableModel(
|
|
502
562
|
data,
|
|
503
563
|
table['columns'],
|
|
504
564
|
timestamp_columns=table.get('timestamp_columns', []),
|
|
505
565
|
convert_timestamps=convert_timestamps,
|
|
506
566
|
)
|
|
507
|
-
|
|
508
|
-
|
|
567
|
+
if hasattr(model, 'setParent'):
|
|
568
|
+
model.setParent(view)
|
|
569
|
+
view.setModel(model)
|
|
570
|
+
if self._last_columns_sig != tuple(table['columns']):
|
|
571
|
+
if hasattr(view, 'adjustColumns'):
|
|
572
|
+
view.adjustColumns()
|
|
573
|
+
if hasattr(view, "horizontalHeader"):
|
|
574
|
+
hh = view.horizontalHeader()
|
|
575
|
+
if hh is not None:
|
|
576
|
+
hh.setSectionResizeMode(QHeaderView.Interactive)
|
|
577
|
+
self._last_columns_sig = tuple(table['columns'])
|
|
578
|
+
view.setUpdatesEnabled(True)
|
|
579
|
+
|
|
580
|
+
def on_search_text_changed(self, text):
|
|
581
|
+
self._search_timer.start(200)
|
|
582
|
+
|
|
583
|
+
def _params_signature_base(self):
|
|
584
|
+
table = self.get_table_name(self.table_select.currentText())
|
|
585
|
+
search_query = self.search_input.text()
|
|
586
|
+
search_column = self.search_column_select.currentData()
|
|
587
|
+
filters = self.get_filters()
|
|
588
|
+
if isinstance(filters, dict):
|
|
589
|
+
filters_key = tuple(sorted(filters.items()))
|
|
590
|
+
else:
|
|
591
|
+
filters_key = str(filters)
|
|
592
|
+
return table, search_query, search_column, filters_key
|
|
593
|
+
|
|
594
|
+
def _params_signature_fetch(self):
|
|
595
|
+
table, search_query, search_column, filters_key = self._params_signature_base()
|
|
596
|
+
tables = self.get_tables()
|
|
597
|
+
columns = tuple(tables[table]['columns']) if table in tables else tuple()
|
|
598
|
+
sort_by = self.sort_by_select.currentText()
|
|
599
|
+
sort_order = self.sort_order_select.currentText()
|
|
600
|
+
limit = self.get_page_size()
|
|
601
|
+
offset = self.current_offset
|
|
602
|
+
return table, columns, sort_by, sort_order, search_query, search_column, filters_key, limit, offset
|
|
603
|
+
|
|
604
|
+
def _params_signature_count(self):
|
|
605
|
+
return self._params_signature_base()
|
|
606
|
+
|
|
607
|
+
def _get_total_rows(self):
|
|
608
|
+
sig = self._params_signature_count()
|
|
609
|
+
if self._last_count_sig == sig:
|
|
610
|
+
return self._cached_total_rows
|
|
611
|
+
table, search_query, search_column, _ = sig
|
|
612
|
+
total_rows = self.get_viewer().count_rows(
|
|
613
|
+
table,
|
|
614
|
+
search_query=search_query,
|
|
615
|
+
search_column=search_column,
|
|
616
|
+
filters=self.get_filters(),
|
|
617
|
+
)
|
|
618
|
+
self._last_count_sig = sig
|
|
619
|
+
self._cached_total_rows = total_rows
|
|
620
|
+
return total_rows
|
|
621
|
+
|
|
622
|
+
def force_refresh(self):
|
|
623
|
+
self._last_fetch_sig = None
|
|
624
|
+
self._last_count_sig = None
|
|
625
|
+
self._last_columns_sig = None
|
|
626
|
+
self.update_table_view()
|