llms-py 2.0.20__py3-none-any.whl → 3.0.18__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 (207) hide show
  1. llms/__init__.py +3 -1
  2. llms/db.py +359 -0
  3. llms/{ui/Analytics.mjs → extensions/analytics/ui/index.mjs} +254 -327
  4. llms/extensions/app/README.md +20 -0
  5. llms/extensions/app/__init__.py +588 -0
  6. llms/extensions/app/db.py +540 -0
  7. llms/{ui → extensions/app/ui}/Recents.mjs +99 -73
  8. llms/{ui/Sidebar.mjs → extensions/app/ui/index.mjs} +139 -68
  9. llms/extensions/app/ui/threadStore.mjs +440 -0
  10. llms/extensions/computer/README.md +96 -0
  11. llms/extensions/computer/__init__.py +59 -0
  12. llms/extensions/computer/base.py +80 -0
  13. llms/extensions/computer/bash.py +185 -0
  14. llms/extensions/computer/computer.py +523 -0
  15. llms/extensions/computer/edit.py +299 -0
  16. llms/extensions/computer/filesystem.py +542 -0
  17. llms/extensions/computer/platform.py +461 -0
  18. llms/extensions/computer/run.py +37 -0
  19. llms/extensions/core_tools/CALCULATOR.md +32 -0
  20. llms/extensions/core_tools/__init__.py +599 -0
  21. llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
  22. llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
  23. llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
  24. llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
  25. llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
  26. llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
  27. llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
  28. llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
  29. llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
  30. llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
  31. llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
  32. llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
  33. llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
  34. llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
  35. llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
  36. llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
  37. llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
  38. llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
  39. llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
  40. llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
  41. llms/extensions/core_tools/ui/index.mjs +650 -0
  42. llms/extensions/gallery/README.md +61 -0
  43. llms/extensions/gallery/__init__.py +63 -0
  44. llms/extensions/gallery/db.py +243 -0
  45. llms/extensions/gallery/ui/index.mjs +482 -0
  46. llms/extensions/katex/README.md +39 -0
  47. llms/extensions/katex/__init__.py +6 -0
  48. llms/extensions/katex/ui/README.md +125 -0
  49. llms/extensions/katex/ui/contrib/auto-render.js +338 -0
  50. llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
  51. llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
  52. llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
  53. llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
  54. llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
  55. llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
  56. llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
  57. llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
  58. llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
  59. llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
  60. llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
  61. llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
  62. llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
  63. llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
  64. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
  65. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
  66. llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  67. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  68. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  69. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  70. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  71. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  72. llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  73. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  74. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  75. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  76. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  77. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  78. llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  79. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
  80. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
  81. llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
  82. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  83. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  84. llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  85. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
  86. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
  87. llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
  88. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
  89. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
  90. llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
  91. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  92. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  93. llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  94. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
  95. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
  96. llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
  97. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  98. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  99. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  100. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  101. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  102. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  103. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  104. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  105. llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  106. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
  107. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
  108. llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
  109. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
  110. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
  111. llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  112. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
  113. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
  114. llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  115. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
  116. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
  117. llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  118. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
  119. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
  120. llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  121. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  122. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  123. llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  124. llms/extensions/katex/ui/index.mjs +92 -0
  125. llms/extensions/katex/ui/katex-swap.css +1230 -0
  126. llms/extensions/katex/ui/katex-swap.min.css +1 -0
  127. llms/extensions/katex/ui/katex.css +1230 -0
  128. llms/extensions/katex/ui/katex.js +19080 -0
  129. llms/extensions/katex/ui/katex.min.css +1 -0
  130. llms/extensions/katex/ui/katex.min.js +1 -0
  131. llms/extensions/katex/ui/katex.min.mjs +1 -0
  132. llms/extensions/katex/ui/katex.mjs +18547 -0
  133. llms/extensions/providers/__init__.py +22 -0
  134. llms/extensions/providers/anthropic.py +260 -0
  135. llms/extensions/providers/cerebras.py +36 -0
  136. llms/extensions/providers/chutes.py +153 -0
  137. llms/extensions/providers/google.py +559 -0
  138. llms/extensions/providers/nvidia.py +103 -0
  139. llms/extensions/providers/openai.py +154 -0
  140. llms/extensions/providers/openrouter.py +74 -0
  141. llms/extensions/providers/zai.py +182 -0
  142. llms/extensions/skills/LICENSE +202 -0
  143. llms/extensions/skills/__init__.py +130 -0
  144. llms/extensions/skills/errors.py +25 -0
  145. llms/extensions/skills/models.py +39 -0
  146. llms/extensions/skills/parser.py +178 -0
  147. llms/extensions/skills/ui/index.mjs +376 -0
  148. llms/extensions/skills/ui/skills/create-plan/SKILL.md +74 -0
  149. llms/extensions/skills/validator.py +177 -0
  150. llms/extensions/system_prompts/README.md +22 -0
  151. llms/extensions/system_prompts/__init__.py +45 -0
  152. llms/extensions/system_prompts/ui/index.mjs +276 -0
  153. llms/extensions/system_prompts/ui/prompts.json +1067 -0
  154. llms/extensions/tools/__init__.py +67 -0
  155. llms/extensions/tools/ui/index.mjs +837 -0
  156. llms/index.html +36 -62
  157. llms/llms.json +180 -879
  158. llms/main.py +4009 -912
  159. llms/providers-extra.json +394 -0
  160. llms/providers.json +1 -0
  161. llms/ui/App.mjs +176 -8
  162. llms/ui/ai.mjs +156 -20
  163. llms/ui/app.css +3768 -321
  164. llms/ui/ctx.mjs +459 -0
  165. llms/ui/index.mjs +131 -0
  166. llms/ui/lib/chart.js +14 -0
  167. llms/ui/lib/charts.mjs +16 -0
  168. llms/ui/lib/color.js +14 -0
  169. llms/ui/lib/highlight.min.mjs +1243 -0
  170. llms/ui/lib/idb.min.mjs +8 -0
  171. llms/ui/lib/marked.min.mjs +8 -0
  172. llms/ui/lib/servicestack-client.mjs +1 -0
  173. llms/ui/lib/servicestack-vue.mjs +37 -0
  174. llms/ui/lib/vue-router.min.mjs +6 -0
  175. llms/ui/lib/vue.min.mjs +13 -0
  176. llms/ui/lib/vue.mjs +18530 -0
  177. llms/ui/markdown.mjs +25 -14
  178. llms/ui/modules/chat/ChatBody.mjs +1156 -0
  179. llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +74 -74
  180. llms/ui/modules/chat/index.mjs +995 -0
  181. llms/ui/modules/icons.mjs +46 -0
  182. llms/ui/modules/layout.mjs +271 -0
  183. llms/ui/modules/model-selector.mjs +811 -0
  184. llms/ui/tailwind.input.css +560 -78
  185. llms/ui/typography.css +54 -36
  186. llms/ui/utils.mjs +221 -92
  187. llms_py-3.0.18.dist-info/METADATA +49 -0
  188. llms_py-3.0.18.dist-info/RECORD +194 -0
  189. {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/WHEEL +1 -1
  190. {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/licenses/LICENSE +1 -2
  191. llms/ui/Avatar.mjs +0 -28
  192. llms/ui/Brand.mjs +0 -34
  193. llms/ui/ChatPrompt.mjs +0 -443
  194. llms/ui/Main.mjs +0 -740
  195. llms/ui/ModelSelector.mjs +0 -60
  196. llms/ui/ProviderIcon.mjs +0 -29
  197. llms/ui/ProviderStatus.mjs +0 -105
  198. llms/ui/SignIn.mjs +0 -64
  199. llms/ui/SystemPromptEditor.mjs +0 -31
  200. llms/ui/SystemPromptSelector.mjs +0 -36
  201. llms/ui/Welcome.mjs +0 -8
  202. llms/ui/threadStore.mjs +0 -524
  203. llms/ui.json +0 -1069
  204. llms_py-2.0.20.dist-info/METADATA +0 -931
  205. llms_py-2.0.20.dist-info/RECORD +0 -36
  206. {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/entry_points.txt +0 -0
  207. {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/top_level.txt +0 -0
llms/__init__.py CHANGED
@@ -1,2 +1,4 @@
1
1
  # Import the main module content
2
- from .main import *
2
+ from .main import main as main
3
+
4
+ __all__ = ["main"]
llms/db.py ADDED
@@ -0,0 +1,359 @@
1
+ import json
2
+ import os
3
+ import sqlite3
4
+ import threading
5
+ from queue import Empty, Queue
6
+ from threading import Event, Thread
7
+
8
+ POOL = os.getenv("LLMS_POOL", "0") == "1"
9
+
10
+
11
+ def create_reader_connection(db_path):
12
+ # isolation_level=None leaves the connection in autocommit mode
13
+ conn = sqlite3.connect(
14
+ db_path, timeout=1.0, check_same_thread=False, isolation_level=None
15
+ ) # Lower - reads should be fast
16
+ conn.execute("PRAGMA query_only=1") # Read-only optimization
17
+ return conn
18
+
19
+
20
+ def create_writer_connection(db_path):
21
+ conn = sqlite3.connect(db_path)
22
+ conn.execute("PRAGMA busy_timeout=5000") # Reasonable timeout for busy connections
23
+ conn.execute("PRAGMA journal_mode=WAL") # Enable WAL mode for better concurrency
24
+ conn.execute("PRAGMA cache_size=-128000") # Increase cache size for better performance
25
+ conn.execute("PRAGMA synchronous=NORMAL") # Reasonable durability/performance balance
26
+ return conn
27
+
28
+
29
+ def writer_thread(ctx, db_path, task_queue, stop_event):
30
+ conn = create_writer_connection(db_path)
31
+ try:
32
+ while not stop_event.is_set():
33
+ try:
34
+ # Use timeout to check stop_event periodically
35
+ task = task_queue.get(timeout=0.1)
36
+
37
+ if task is None: # Poison pill for clean shutdown
38
+ break
39
+
40
+ sql, args, callback = task # Optional callback for results
41
+
42
+ try:
43
+ ctx.dbg("SQL>" + ("\n" if "\n" in sql else " ") + sql + ("\n" if args else " ") + str(args))
44
+ cursor = conn.execute(sql, args)
45
+ conn.commit()
46
+ ctx.dbg(f"lastrowid {cursor.lastrowid}, rowcount {cursor.rowcount}")
47
+ if callback:
48
+ callback(cursor.lastrowid, cursor.rowcount)
49
+ except sqlite3.Error as e:
50
+ ctx.err("writer_thread", e)
51
+ if callback:
52
+ callback(None, None, error=e)
53
+ finally:
54
+ task_queue.task_done()
55
+
56
+ except Empty:
57
+ continue
58
+
59
+ finally:
60
+ conn.close()
61
+
62
+
63
+ def to_dto(ctx, row, json_columns):
64
+ # as=column -> [0,1,2]
65
+ if not isinstance(row, dict):
66
+ return row
67
+
68
+ to = {}
69
+ for k, v in row.items():
70
+ if k in json_columns and v is not None and isinstance(v, str):
71
+ try:
72
+ to[k] = json.loads(v)
73
+ except Exception as e:
74
+ print(f"Failed to parse JSON for {k}: {v} ({type(v)})", e)
75
+ to[k] = v
76
+ else:
77
+ to[k] = v
78
+ return to
79
+
80
+
81
+ def valid_columns(all_columns, fields):
82
+ if fields:
83
+ if not isinstance(fields, list):
84
+ fields = fields.split(",")
85
+ cols = []
86
+ for k in fields:
87
+ k = k.strip()
88
+ if k in all_columns:
89
+ cols.append(k)
90
+ return cols
91
+ return []
92
+
93
+
94
+ def table_columns(all_columns, fields):
95
+ cols = valid_columns(all_columns, fields)
96
+ return ", ".join(cols) if len(cols) > 0 else ", ".join(all_columns)
97
+
98
+
99
+ def select_columns(all_columns, fields, select=None):
100
+ columns = table_columns(all_columns, fields)
101
+ if select == "distinct":
102
+ return f"SELECT DISTINCT {columns}"
103
+ return f"SELECT {columns}"
104
+
105
+
106
+ def order_by(all_columns, sort):
107
+ cols = []
108
+ for k in sort.split(","):
109
+ k = k.strip()
110
+ by = ""
111
+ if k[0] == "-":
112
+ by = " DESC"
113
+ k = k[1:]
114
+ if k in all_columns:
115
+ cols.append(f"{k}{by}")
116
+ return f"ORDER BY {', '.join(cols)} " if len(cols) > 0 else ""
117
+
118
+
119
+ class DbManager:
120
+ def __init__(self, ctx, db_path, clone=None):
121
+ if db_path is None:
122
+ raise ValueError("db_path is required")
123
+ self.ctx = ctx
124
+ self.db_path = db_path
125
+ self.read_only_pool = Queue()
126
+ if not clone:
127
+ self.task_queue = Queue()
128
+ self.stop_event = Event()
129
+ self.writer_thread = Thread(target=writer_thread, args=(ctx, db_path, self.task_queue, self.stop_event))
130
+ self.writer_thread.start()
131
+ else:
132
+ # share singleton writer thread in clones
133
+ self.task_queue = clone.task_queue
134
+ self.stop_event = clone.stop_event
135
+ self.writer_thread = clone.writer_thread
136
+
137
+ def create_reader_connection(self):
138
+ return create_reader_connection(self.db_path)
139
+
140
+ def create_writer_connection(self):
141
+ return create_writer_connection(self.db_path)
142
+
143
+ def resolve_connection(self):
144
+ if POOL:
145
+ try:
146
+ return self.read_only_pool.get_nowait()
147
+ except Empty:
148
+ return self.create_reader_connection()
149
+ else:
150
+ return self.create_reader_connection()
151
+
152
+ def release_connection(self, conn):
153
+ if POOL:
154
+ conn.rollback()
155
+ self.read_only_pool.put(conn)
156
+ else:
157
+ conn.close()
158
+
159
+ def write(self, query, args=None, callback=None):
160
+ """
161
+ Execute a write operation asynchronously.
162
+
163
+ Args:
164
+ query (str): The SQL query to execute.
165
+ args (tuple, optional): Arguments for the query.
166
+ callback (callable, optional): A function called after execution with signature:
167
+ callback(lastrowid, rowcount, error=None)
168
+ - lastrowid (int): output of cursor.lastrowid
169
+ - rowcount (int): output of cursor.rowcount
170
+ - error (Exception): exception if operation failed, else None
171
+ """
172
+ self.task_queue.put((query, args, callback))
173
+
174
+ def log_sql(self, sql, parameters=None):
175
+ if self.ctx.debug:
176
+ self.ctx.dbg(
177
+ "SQL>" + ("\n" if "\n" in sql else " ") + sql + ("\n" if parameters else " ") + str(parameters)
178
+ )
179
+
180
+ def exec(self, connection, sql, parameters=None):
181
+ self.log_sql(sql, parameters)
182
+ return connection.execute(sql, parameters or ())
183
+
184
+ def all(self, sql, parameters=None, connection=None):
185
+ """
186
+ Execute a query and return all rows as a list of dictionaries.
187
+ """
188
+ conn = self.resolve_connection() if connection is None else connection
189
+
190
+ try:
191
+ self.log_sql(sql, parameters)
192
+ conn.row_factory = sqlite3.Row
193
+ cursor = conn.execute(sql, parameters or ())
194
+ rows = [dict(row) for row in cursor.fetchall()]
195
+ return rows
196
+ finally:
197
+ if connection is None:
198
+ conn.row_factory = None
199
+ self.release_connection(conn)
200
+
201
+ def one(self, sql, parameters=None, connection=None):
202
+ """
203
+ Execute a query and return the first row as a dictionary.
204
+ """
205
+ conn = self.resolve_connection() if connection is None else connection
206
+
207
+ try:
208
+ self.log_sql(sql, parameters)
209
+ conn.row_factory = sqlite3.Row
210
+ cursor = conn.execute(sql, parameters or ())
211
+ row = cursor.fetchone()
212
+ return dict(row) if row else None
213
+ finally:
214
+ if connection is None:
215
+ conn.row_factory = None
216
+ self.release_connection(conn)
217
+
218
+ def scalar(self, sql, parameters=None, connection=None):
219
+ """
220
+ Execute a scalar query and return the first column of the first row.
221
+ """
222
+ conn = self.resolve_connection() if connection is None else connection
223
+
224
+ try:
225
+ self.log_sql(sql, parameters)
226
+ conn.row_factory = sqlite3.Row
227
+ cursor = conn.execute(sql, parameters or ())
228
+ row = cursor.fetchone()
229
+ return row[0] if row else None
230
+ finally:
231
+ if connection is None:
232
+ conn.row_factory = None
233
+ self.release_connection(conn)
234
+
235
+ def column(self, sql, parameters=None, connection=None):
236
+ """
237
+ Execute a 1 column query and return the values as a list.
238
+ """
239
+ conn = self.resolve_connection() if connection is None else connection
240
+
241
+ try:
242
+ self.log_sql(sql, parameters)
243
+ cursor = conn.execute(sql, parameters or ())
244
+ return [row[0] for row in cursor.fetchall()]
245
+ finally:
246
+ if connection is None:
247
+ self.release_connection(conn)
248
+
249
+ def dict(self, sql, parameters=None, connection=None):
250
+ """
251
+ Execute a 2 column query and return the keys as the first column and the values as the second column.
252
+ """
253
+ conn = self.resolve_connection() if connection is None else connection
254
+
255
+ try:
256
+ self.log_sql(sql, parameters)
257
+ conn.row_factory = sqlite3.Row
258
+ cursor = conn.execute(sql, parameters or ())
259
+ rows = cursor.fetchall()
260
+ return {row[0]: row[1] for row in rows}
261
+ finally:
262
+ if connection is None:
263
+ conn.row_factory = None
264
+ self.release_connection(conn)
265
+
266
+ # Helper to safely dump JSON if value exists
267
+ def value(self, val):
268
+ if val is None or val == "":
269
+ return None
270
+ if isinstance(val, (dict, list)):
271
+ return json.dumps(val)
272
+ return val
273
+
274
+ def insert(self, table, columns, info, callback=None):
275
+ if not info:
276
+ raise Exception("info is required")
277
+
278
+ args = {}
279
+ known_columns = columns.keys()
280
+ for k, val in info.items():
281
+ if k in known_columns and k != "id":
282
+ args[k] = self.value(val)
283
+
284
+ insert_keys = list(args.keys())
285
+ insert_body = ", ".join(insert_keys)
286
+ insert_values = ", ".join(["?" for _ in insert_keys])
287
+
288
+ sql = f"INSERT INTO {table} ({insert_body}) VALUES ({insert_values})"
289
+
290
+ self.write(sql, tuple(args[k] for k in insert_keys), callback)
291
+
292
+ async def insert_async(self, table, columns, info):
293
+ event = threading.Event()
294
+
295
+ ret = [None, None]
296
+
297
+ def cb(lastrowid, rowcount, error=None):
298
+ nonlocal ret
299
+ if error:
300
+ ret[1] = error
301
+ else:
302
+ ret[0] = lastrowid
303
+ event.set()
304
+
305
+ self.insert(table, columns, info, cb)
306
+ event.wait()
307
+ if ret[1]:
308
+ raise ret[1]
309
+ return ret[0]
310
+
311
+ def update(self, table, columns, info, callback=None):
312
+ if not info:
313
+ raise Exception("info is required")
314
+
315
+ args = {}
316
+ known_columns = columns.keys()
317
+ for k, val in info.items():
318
+ if k in known_columns and k != "id":
319
+ args[k] = self.value(val)
320
+
321
+ update_keys = list(args.keys())
322
+ update_body = ", ".join([f"{k} = :{k}" for k in update_keys])
323
+
324
+ args["id"] = info["id"]
325
+ sql = f"UPDATE {table} SET {update_body} WHERE id = :id"
326
+
327
+ self.write(sql, args, callback)
328
+
329
+ async def update_async(self, table, columns, info):
330
+ event = threading.Event()
331
+
332
+ ret = [None, None]
333
+
334
+ def cb(lastrowid, rowcount, error=None):
335
+ nonlocal ret
336
+ if error:
337
+ ret[1] = error
338
+ else:
339
+ ret[0] = rowcount
340
+ event.set()
341
+
342
+ self.update(table, columns, info, cb)
343
+ event.wait()
344
+ if ret[1]:
345
+ raise ret[1]
346
+ return ret[0]
347
+
348
+ def close(self):
349
+ self.ctx.dbg("Closing database")
350
+ self.stop_event.set()
351
+ self.task_queue.put(None) # Poison pill to signal shutdown
352
+ self.writer_thread.join()
353
+
354
+ while not self.read_only_pool.empty():
355
+ try:
356
+ conn = self.read_only_pool.get_nowait()
357
+ conn.close()
358
+ except Empty:
359
+ break