pygpt-net 2.6.37__py3-none-any.whl → 2.6.39__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.
Files changed (54) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/chat/handler/anthropic_stream.py +0 -2
  4. pygpt_net/controller/chat/handler/worker.py +6 -2
  5. pygpt_net/controller/debug/debug.py +6 -6
  6. pygpt_net/controller/model/editor.py +20 -42
  7. pygpt_net/controller/model/importer.py +9 -2
  8. pygpt_net/controller/painter/common.py +0 -8
  9. pygpt_net/controller/plugins/plugins.py +11 -3
  10. pygpt_net/controller/presets/presets.py +2 -2
  11. pygpt_net/core/ctx/bag.py +7 -2
  12. pygpt_net/core/ctx/reply.py +17 -2
  13. pygpt_net/core/db/viewer.py +19 -34
  14. pygpt_net/core/render/plain/pid.py +12 -1
  15. pygpt_net/core/render/web/body.py +292 -445
  16. pygpt_net/core/tabs/tab.py +24 -1
  17. pygpt_net/data/config/config.json +3 -3
  18. pygpt_net/data/config/models.json +3 -3
  19. pygpt_net/item/assistant.py +51 -2
  20. pygpt_net/item/attachment.py +21 -20
  21. pygpt_net/item/calendar_note.py +19 -2
  22. pygpt_net/item/ctx.py +115 -2
  23. pygpt_net/item/index.py +9 -2
  24. pygpt_net/item/mode.py +9 -6
  25. pygpt_net/item/model.py +20 -3
  26. pygpt_net/item/notepad.py +14 -2
  27. pygpt_net/item/preset.py +42 -2
  28. pygpt_net/item/prompt.py +8 -2
  29. pygpt_net/plugin/cmd_files/plugin.py +2 -2
  30. pygpt_net/provider/api/anthropic/tools.py +1 -1
  31. pygpt_net/provider/api/google/realtime/client.py +2 -2
  32. pygpt_net/provider/core/attachment/json_file.py +2 -2
  33. pygpt_net/tools/text_editor/tool.py +4 -1
  34. pygpt_net/tools/text_editor/ui/dialogs.py +1 -1
  35. pygpt_net/ui/dialog/db.py +177 -59
  36. pygpt_net/ui/dialog/dictionary.py +57 -59
  37. pygpt_net/ui/dialog/editor.py +3 -2
  38. pygpt_net/ui/dialog/image.py +1 -1
  39. pygpt_net/ui/dialog/logger.py +3 -2
  40. pygpt_net/ui/dialog/models.py +171 -21
  41. pygpt_net/ui/dialog/plugins.py +26 -20
  42. pygpt_net/ui/layout/ctx/ctx_list.py +3 -4
  43. pygpt_net/ui/layout/toolbox/__init__.py +2 -2
  44. pygpt_net/ui/layout/toolbox/assistants.py +8 -9
  45. pygpt_net/ui/layout/toolbox/presets.py +2 -2
  46. pygpt_net/ui/main.py +9 -4
  47. pygpt_net/ui/widget/element/labels.py +2 -2
  48. pygpt_net/ui/widget/textarea/editor.py +0 -4
  49. pygpt_net/utils.py +12 -13
  50. {pygpt_net-2.6.37.dist-info → pygpt_net-2.6.39.dist-info}/METADATA +14 -2
  51. {pygpt_net-2.6.37.dist-info → pygpt_net-2.6.39.dist-info}/RECORD +54 -54
  52. {pygpt_net-2.6.37.dist-info → pygpt_net-2.6.39.dist-info}/LICENSE +0 -0
  53. {pygpt_net-2.6.37.dist-info → pygpt_net-2.6.39.dist-info}/WHEEL +0 -0
  54. {pygpt_net-2.6.37.dist-info → pygpt_net-2.6.39.dist-info}/entry_points.txt +0 -0
pygpt_net/CHANGELOG.txt CHANGED
@@ -1,3 +1,15 @@
1
+ 2.6.39 (2025-09-06)
2
+
3
+ - Added: Search input to the models editor.
4
+ - Fixed: Brush color switching when changing modes in the Painter.
5
+
6
+ 2.6.38 (2025-09-05)
7
+
8
+ - Fixed: Detection of chunk type in Ollama.
9
+ - Fixed: Import of models with existing IDs.
10
+ - Fixed: Updating Assistants UI list after create new Assistant.
11
+ - Refactor and optimization.
12
+
1
13
  2.6.37 (2025-09-05)
2
14
 
3
15
  - Fixed: Function parameters sanitization in the Google Gen AI SDK.
pygpt_net/__init__.py CHANGED
@@ -6,15 +6,15 @@
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.09.05 00:00:00 #
9
+ # Updated Date: 2025.09.06 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  __author__ = "Marcin Szczygliński"
13
13
  __copyright__ = "Copyright 2025, Marcin Szczygliński"
14
14
  __credits__ = ["Marcin Szczygliński"]
15
15
  __license__ = "MIT"
16
- __version__ = "2.6.37"
17
- __build__ = "2025-09-05"
16
+ __version__ = "2.6.39"
17
+ __build__ = "2025-09-06"
18
18
  __maintainer__ = "Marcin Szczygliński"
19
19
  __github__ = "https://github.com/szczyglis-dev/py-gpt"
20
20
  __report__ = "https://github.com/szczyglis-dev/py-gpt/issues"
@@ -154,8 +154,6 @@ def process_anthropic_chunk(ctx, core, state, chunk) -> Optional[str]:
154
154
  state.usage_payload["out"] = out_tok
155
155
  delta = getattr(chunk, "delta", None)
156
156
  stop_reason = getattr(delta, "stop_reason", None) if delta else None
157
- if stop_reason == "tool_use":
158
- state.force_func_call = True
159
157
  except Exception:
160
158
  pass
161
159
  return None
@@ -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: 2025.09.04 00:00:00 #
9
+ # Updated Date: 2025.09.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import io
@@ -16,6 +16,7 @@ from typing import Optional, Literal, Any
16
16
  from enum import Enum
17
17
 
18
18
  from PySide6.QtCore import QObject, Signal, Slot, QRunnable
19
+ from openai.types.chat import ChatCompletionChunk
19
20
 
20
21
  from pygpt_net.core.events import RenderEvent
21
22
  from pygpt_net.core.text.utils import has_unclosed_code_tag
@@ -262,6 +263,10 @@ class StreamWorker(QRunnable):
262
263
  if not hasattr(chunk, "type") and not hasattr(chunk, "candidates"):
263
264
  return ChunkType.LLAMA_CHAT
264
265
 
266
+ # fallback: OpenAI ChatCompletionChunk not caught above
267
+ if isinstance(chunk, ChatCompletionChunk):
268
+ return ChunkType.API_CHAT
269
+
265
270
  return ChunkType.RAW
266
271
 
267
272
  def _append_response(
@@ -344,7 +349,6 @@ class StreamWorker(QRunnable):
344
349
  calls = xai_stream.xai_extract_tool_calls(state.xai_last_response)
345
350
  if calls:
346
351
  state.tool_calls = calls
347
- state.force_func_call = True
348
352
  except Exception:
349
353
  pass
350
354
 
@@ -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: 2025.08.18 01:00:00 #
9
+ # Updated Date: 2025.09.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from datetime import datetime
@@ -31,6 +31,7 @@ class Debug(QObject):
31
31
  self.is_logger = False # logger window opened
32
32
  self.is_app_log = False # app log window opened
33
33
  self.allow_level_change = False # allow changing log level
34
+ self._ids = None
34
35
 
35
36
  def update(self):
36
37
  """Update debug"""
@@ -100,12 +101,11 @@ class Debug(QObject):
100
101
 
101
102
  :param all: update all debug windows
102
103
  """
103
- # not_realtime = ['context']
104
- not_realtime = []
105
- for id in self.window.controller.dialogs.debug.get_ids():
104
+ if self._ids is None:
105
+ self._ids = self.window.controller.dialogs.debug.get_ids()
106
+ for id in self._ids:
106
107
  if self.window.controller.dialogs.debug.is_active(id):
107
- if all or id not in not_realtime:
108
- self.window.controller.dialogs.debug.update_worker(id)
108
+ self.window.controller.dialogs.debug.update_worker(id)
109
109
 
110
110
  def post_setup(self):
111
111
  """Post setup debug"""
@@ -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: 2025.08.14 01:00:00 #
9
+ # Updated Date: 2025.09.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
@@ -69,7 +69,6 @@ class Editor:
69
69
  "type": "bool",
70
70
  "label": "model.tool_calls",
71
71
  "description": "model.tool_calls.desc",
72
- "advanced": True,
73
72
  },
74
73
  "input": {
75
74
  "type": "bool_list", # list of comma separated values
@@ -424,15 +423,14 @@ class Editor:
424
423
 
425
424
  :param idx: tab index
426
425
  """
427
- model_idx = 0
428
- self.window.core.models.sort_items()
429
- for id in self.window.core.models.get_ids():
430
- if model_idx == idx:
431
- self.current = id
432
- break
433
- model_idx += 1
434
- current = self.window.ui.models['models.list'].index(idx, 0)
435
- self.window.ui.nodes['models.list'].setCurrentIndex(current)
426
+ if idx is None:
427
+ return
428
+ # Resolve model id using the filtered view mapping
429
+ model_id = self.window.model_settings.get_model_id_by_row(idx)
430
+ if model_id is not None:
431
+ self.current = model_id
432
+ current = self.window.ui.models['models.list'].index(idx, 0)
433
+ self.window.ui.nodes['models.list'].setCurrentIndex(current)
436
434
 
437
435
  def set_tab_by_id(self, model_id: str):
438
436
  """
@@ -441,57 +439,37 @@ class Editor:
441
439
  :param model_id: model id
442
440
  """
443
441
  idx = self.get_tab_idx(model_id)
442
+ if idx is None:
443
+ return
444
444
  current = self.window.ui.models['models.list'].index(idx, 0)
445
445
  self.window.ui.nodes['models.list'].setCurrentIndex(current)
446
446
 
447
- def get_tab_idx(self, model_id: str) -> int:
447
+ def get_tab_idx(self, model_id: str) -> Optional[int]:
448
448
  """
449
- Get model list index
449
+ Get model list index (in the current filtered view)
450
450
 
451
451
  :param model_id: model id
452
452
  :return: list index
453
453
  """
454
- model_idx = None
455
- i = 0
456
- self.window.core.models.sort_items()
457
- for id in self.window.core.models.get_ids():
458
- if id == model_id:
459
- model_idx = i
460
- break
461
- i += 1
462
- return model_idx
454
+ return self.window.model_settings.get_row_by_model_id(model_id)
463
455
 
464
- def get_tab_by_id(self, model_id: str) -> int:
456
+ def get_tab_by_id(self, model_id: str) -> Optional[int]:
465
457
  """
466
- Get model list index
458
+ Get model list index (alias to get_tab_idx for compatibility)
467
459
 
468
460
  :param model_id: model id
469
461
  :return: list index
470
462
  """
471
- model_idx = None
472
- i = 0
473
- self.window.core.models.sort_items()
474
- for id in self.window.core.models.get_ids():
475
- if id == model_id:
476
- model_idx = i
477
- break
478
- i += 1
479
- return model_idx
463
+ return self.get_tab_idx(model_id)
480
464
 
481
465
  def get_model_by_tab_idx(self, idx: int) -> Optional[str]:
482
466
  """
483
- Get model key by list index
467
+ Get model key by list index (in the current filtered view)
484
468
 
485
469
  :param idx: list index
486
470
  :return: model key
487
471
  """
488
- model_idx = 0
489
- self.window.core.models.sort_items()
490
- for id in self.window.core.models.get_ids():
491
- if model_idx == idx:
492
- return id
493
- model_idx += 1
494
- return None
472
+ return self.window.model_settings.get_model_id_by_row(idx)
495
473
 
496
474
  def open_by_idx(self, idx: int):
497
475
  """
@@ -534,4 +512,4 @@ class Editor:
534
512
 
535
513
  def clear_selected(self):
536
514
  """Clear selected list"""
537
- self.selected = []
515
+ self.selected = []
@@ -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: 2025.08.26 23:00:00 #
9
+ # Updated Date: 2025.09.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
@@ -503,7 +503,7 @@ class Importer:
503
503
  return models
504
504
  else:
505
505
  for model in ollama_models:
506
- name = model.get('name').replace(":latest", "")
506
+ name = model.get('name')
507
507
  m = self.window.core.models.create_empty(append=False)
508
508
  m.id = name
509
509
  m.name = name
@@ -541,6 +541,13 @@ class Importer:
541
541
  base_models[key] = copy.deepcopy(self.pending[key])
542
542
  base_models[key].imported = True
543
543
  added = True
544
+ else:
545
+ # add provider suffix - to key
546
+ new_key = f"{key}-{self.provider}"
547
+ if new_key not in base_models:
548
+ base_models[new_key] = copy.deepcopy(self.pending[key])
549
+ base_models[new_key].imported = True
550
+ added = True
544
551
  for key in list(self.removed.keys()):
545
552
  if key in base_models:
546
553
  del base_models[key]
@@ -56,10 +56,6 @@ class Common:
56
56
  :param enabled: bool
57
57
  """
58
58
  if enabled:
59
- # keep UI color for compatibility
60
- self.window.ui.nodes['painter.select.brush.color'].setCurrentText("Black")
61
- self.window.ui.painter.set_brush_color(Qt.black)
62
- # switch widget to brush mode (layered painting)
63
59
  self.window.ui.painter.set_mode("brush")
64
60
  self.window.core.config.set('painter.brush.mode', "brush")
65
61
  self.window.core.config.save()
@@ -71,10 +67,6 @@ class Common:
71
67
  :param enabled: bool
72
68
  """
73
69
  if enabled:
74
- # keep UI color for compatibility
75
- self.window.ui.nodes['painter.select.brush.color'].setCurrentText("White")
76
- self.window.ui.painter.set_brush_color(Qt.white)
77
- # switch widget to erase mode (layered erasing)
78
70
  self.window.ui.painter.set_mode("erase")
79
71
  self.window.core.config.set('painter.brush.mode', "erase")
80
72
  self.window.core.config.save()
@@ -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: 2025.08.15 23:00:00 #
9
+ # Updated Date: 2025.09.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import List, Dict, Any, Optional
@@ -34,6 +34,8 @@ class Plugins:
34
34
  self.settings = Settings(window)
35
35
  self.presets = Presets(window)
36
36
  self.enabled = {}
37
+ self._ids = None
38
+ self._ids_with_update = None
37
39
  self._suspend_updates = 0
38
40
 
39
41
  def _begin_batch(self):
@@ -316,9 +318,15 @@ class Plugins:
316
318
  def on_post_update(self):
317
319
  """Called on post update"""
318
320
  pm = self.window.core.plugins
319
- for pid in pm.get_ids():
321
+ if self._ids is None:
322
+ self._ids = pm.get_ids()
323
+ if self._ids_with_update is None:
324
+ self._ids_with_update = [pid for pid in self._ids if hasattr(self.window.core.plugins.get(pid), "on_post_update")]
325
+ if len(self._ids_with_update) == 0:
326
+ return
327
+ for pid in self._ids_with_update:
320
328
  if self.is_enabled(pid):
321
- fn = getattr(pm.get(pid), "on_post_update", None)
329
+ fn = pm.get(pid).on_post_update
322
330
  if callable(fn):
323
331
  fn()
324
332
 
@@ -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: 2025.08.15 03:00:00 #
9
+ # Updated Date: 2025.09.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import re
@@ -440,7 +440,7 @@ class Presets:
440
440
  if preset_id and preset_id in w.core.presets.items:
441
441
  preset = w.core.presets.items[preset_id]
442
442
  preset.prompt = w.core.config.get('prompt')
443
- w.core.presets.save(preset)
443
+ w.core.presets.save(preset_id)
444
444
 
445
445
  def select_model(self):
446
446
  """Select model by current preset"""
pygpt_net/core/ctx/bag.py CHANGED
@@ -6,16 +6,21 @@
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.08.19 07:00:00 #
9
+ # Updated Date: 2025.09.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import List
13
+ from dataclasses import dataclass, field
13
14
 
14
15
  from pygpt_net.item.ctx import CtxItem
15
16
 
16
17
 
18
+ @dataclass(slots=True)
17
19
  class Bag:
18
- __slots__ = ('window', 'meta', 'tab_id', 'items')
20
+ window: object = None
21
+ meta: object = None
22
+ tab_id: int = 0
23
+ items: List[CtxItem] = field(default_factory=list)
19
24
 
20
25
  def __init__(self, window=None):
21
26
  """
@@ -6,12 +6,14 @@
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.07.26 18:00:00 #
9
+ # Updated Date: 2025.09.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from typing import Dict, Any
12
+ from typing import Dict, Any, Optional
13
+ from dataclasses import dataclass, field
13
14
 
14
15
 
16
+ @dataclass(slots=True)
15
17
  class ReplyContext:
16
18
 
17
19
  AGENT_CONTINUE = "agent.continue"
@@ -21,6 +23,15 @@ class ReplyContext:
21
23
  EXPERT_CALL = "expert.call"
22
24
  EXPERT_RESPONSE = "expert.response"
23
25
 
26
+ type: Optional[object] = None
27
+ bridge_context: Optional[object] = None
28
+ ctx: Optional[object] = None
29
+ prev_ctx: Optional[object] = None
30
+ parent_id: Optional[object] = None
31
+ input: str = ""
32
+ internal: bool = False
33
+ cmds: list = field(default_factory=list)
34
+
24
35
  def __init__(self):
25
36
  """Reply context"""
26
37
  self.type = None
@@ -55,7 +66,11 @@ class ReplyContext:
55
66
  data["prev_ctx"] = self.prev_ctx.to_dict()
56
67
  return data
57
68
 
69
+
70
+ @dataclass(slots=True)
58
71
  class Reply:
72
+ window: Optional[object] = None
73
+
59
74
  def __init__(self, window=None):
60
75
  """
61
76
  Reply core
@@ -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: 2024.12.14 08:00:00 #
9
+ # Updated Date: 2025.09.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
@@ -65,16 +65,13 @@ class Viewer:
65
65
  limit_clause = f" LIMIT {limit} OFFSET {offset}"
66
66
 
67
67
  params = {}
68
- if search_query:
69
- search_clauses = [f"{column} LIKE :search_query" for column in search_fields]
70
- where_clause = f" WHERE ({' OR '.join(search_clauses)})"
71
- params['search_query'] = f"%{search_query}%"
72
-
73
- # apply filters
74
- # filters = {
75
- # "column1": "value1",
76
- # "column2": "value2" # AND condition
77
- # }
68
+ if search_query is not None:
69
+ sq = search_query.strip()
70
+ if sq:
71
+ search_clauses = [f"{column} LIKE :search_query" for column in search_fields]
72
+ where_clause = f" WHERE ({' OR '.join(search_clauses)})"
73
+ params['search_query'] = f"%{sq}%"
74
+
78
75
  if filters:
79
76
  filter_clauses = [f"{column} = :filter_{column}" for column in filters.keys()]
80
77
  if where_clause == "":
@@ -82,13 +79,13 @@ class Viewer:
82
79
  else:
83
80
  where_clause += f" AND ({' AND '.join(filter_clauses)})"
84
81
  for column, value in filters.items():
85
- params[f"filter_{column}"] = value # filter placeholder prefixed with 'filter_'
82
+ params[f"filter_{column}"] = value
86
83
 
87
84
  query = f"{base_query}{where_clause}{order_clause}{limit_clause}"
88
85
  stmt = text(query).bindparams(**params)
89
86
  with self.database.get_db().connect() as conn:
90
87
  result = conn.execute(stmt).fetchall()
91
- return result
88
+ return [tuple(r) for r in result]
92
89
 
93
90
  def count_rows(
94
91
  self,
@@ -116,15 +113,12 @@ class Viewer:
116
113
  else:
117
114
  search_fields = tables[table]['search_fields']
118
115
 
119
- if search_query:
120
- where_clause = f" WHERE {' OR '.join([f'{column} LIKE :search_query' for column in search_fields])}"
121
- params['search_query'] = f"%{search_query}%"
116
+ if search_query is not None:
117
+ sq = search_query.strip()
118
+ if sq:
119
+ where_clause = f" WHERE {' OR '.join([f'{column} LIKE :search_query' for column in search_fields])}"
120
+ params['search_query'] = f"%{sq}%"
122
121
 
123
- # apply filters
124
- # filters = {
125
- # "column1": "value1",
126
- # "column2": "value2" # AND condition
127
- # }
128
122
  if filters:
129
123
  filter_clauses = [f"{column} = :filter_{column}" for column in filters.keys()]
130
124
  if where_clause == "":
@@ -132,13 +126,13 @@ class Viewer:
132
126
  else:
133
127
  where_clause += f" AND ({' AND '.join(filter_clauses)})"
134
128
  for column, value in filters.items():
135
- params[f"filter_{column}"] = value # filter placeholder prefixed with 'filter_'
129
+ params[f"filter_{column}"] = value
136
130
 
137
131
  query = f"{base_query}{where_clause}"
138
132
  stmt = text(query).bindparams(**params)
139
133
  with self.database.get_db().connect() as conn:
140
134
  count = conn.execute(stmt).scalar()
141
- return count
135
+ return int(count) if count is not None else 0
142
136
 
143
137
  def is_auto_backup(self) -> bool:
144
138
  """
@@ -154,14 +148,12 @@ class Viewer:
154
148
 
155
149
  :param data: Dictionary with table and row_id keys
156
150
  """
157
- # create backup
158
151
  if self.is_auto_backup():
159
152
  backup_path = self.database.make_backup()
160
153
  if backup_path:
161
154
  msg = f"[DB] Created DB backup: {backup_path}"
162
155
  self.log(msg)
163
156
 
164
- # delete row
165
157
  with self.database.get_db().begin() as conn:
166
158
  conn.execute(
167
159
  text(f"DELETE FROM {data['table']} WHERE id = :row_id")
@@ -184,35 +176,30 @@ class Viewer:
184
176
  timestamp_columns = tables[data['table']]['timestamp_columns']
185
177
  primary_key = tables[data['table']]['primary_key']
186
178
 
187
- # check JSON
188
179
  if field in json_columns or field.endswith("_json"):
189
180
  try:
190
- value = json.dumps(json.loads(value)) # validate and pack JSON
181
+ value = json.dumps(json.loads(value))
191
182
  except:
192
183
  raise ValueError(f"Invalid JSON value for column {field}")
193
184
 
194
- # check timestamp
195
185
  if field in timestamp_columns or field.endswith("_ts"):
196
186
  try:
197
187
  value = int(value)
198
188
  except:
199
189
  raise ValueError(f"Invalid timestamp value for column {field}")
200
190
 
201
- # check foreign id field
202
191
  if field.endswith("_id"):
203
192
  try:
204
193
  value = int(value)
205
194
  except:
206
195
  raise ValueError(f"Invalid _id value for column {field}")
207
196
 
208
- # create backup
209
197
  if self.is_auto_backup():
210
198
  backup_path = self.database.make_backup()
211
199
  if backup_path:
212
200
  msg = f"[DB] Created DB backup: {backup_path}"
213
201
  self.log(msg)
214
202
 
215
- # update row
216
203
  with self.database.get_db().begin() as conn:
217
204
  conn.execute(
218
205
  text(f"UPDATE {data['table']} SET {data['field']} = :value WHERE {primary_key} = :id")
@@ -229,17 +216,15 @@ class Viewer:
229
216
  :param data: Dictionary with table key
230
217
  :param reset: Reset table sequence
231
218
  """
232
- # create backup
233
219
  if self.is_auto_backup():
234
220
  backup_path = self.database.make_backup()
235
221
  if backup_path:
236
222
  msg = f"[DB] Created DB backup: {backup_path}"
237
223
  self.log(msg)
238
224
 
239
- # truncate table
240
225
  with self.database.get_db().begin() as conn:
241
226
  conn.execute(text(f"DELETE FROM {data['table']}"))
242
- if reset: # reset table sequence (autoincrement)
227
+ if reset:
243
228
  conn.execute(text(f"DELETE FROM sqlite_sequence WHERE name='{data['table']}'"))
244
229
  msg = f"[DB] Truncated table {data['table']}"
245
230
  else:
@@ -6,13 +6,24 @@
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.08.12 19:00:00 #
9
+ # Updated Date: 2025.09.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import io
13
+ from dataclasses import dataclass, field
13
14
 
15
+
16
+ @dataclass(slots=True)
14
17
  class PidData:
15
18
 
19
+ pid: object = None
20
+ meta: object = None
21
+ images_appended: list = field(default_factory=list)
22
+ urls_appended: list = field(default_factory=list)
23
+ files_appended: list = field(default_factory=list)
24
+ _buffer: io.StringIO = field(default_factory=io.StringIO)
25
+ is_cmd: bool = False
26
+
16
27
  def __init__(self, pid, meta=None):
17
28
  """Pid Data"""
18
29
  self.pid = pid