syntaxmatrix 2.5.5.5__py3-none-any.whl → 2.6.2__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.
- syntaxmatrix/__init__.py +3 -2
- syntaxmatrix/agentic/agents.py +1220 -169
- syntaxmatrix/agentic/agents_orchestrer.py +326 -0
- syntaxmatrix/agentic/code_tools_registry.py +27 -32
- syntaxmatrix/auth.py +142 -5
- syntaxmatrix/commentary.py +16 -16
- syntaxmatrix/core.py +192 -84
- syntaxmatrix/db.py +460 -4
- syntaxmatrix/{display.py → display_html.py} +2 -6
- syntaxmatrix/gpt_models_latest.py +1 -1
- syntaxmatrix/media/__init__.py +0 -0
- syntaxmatrix/media/media_pixabay.py +277 -0
- syntaxmatrix/models.py +1 -1
- syntaxmatrix/page_builder_defaults.py +183 -0
- syntaxmatrix/page_builder_generation.py +1122 -0
- syntaxmatrix/page_layout_contract.py +644 -0
- syntaxmatrix/page_patch_publish.py +1471 -0
- syntaxmatrix/preface.py +670 -0
- syntaxmatrix/profiles.py +28 -10
- syntaxmatrix/routes.py +1941 -593
- syntaxmatrix/selftest_page_templates.py +360 -0
- syntaxmatrix/settings/client_items.py +28 -0
- syntaxmatrix/settings/model_map.py +1022 -207
- syntaxmatrix/settings/prompts.py +328 -130
- syntaxmatrix/static/assets/hero-default.svg +22 -0
- syntaxmatrix/static/icons/bot-icon.png +0 -0
- syntaxmatrix/static/icons/favicon.png +0 -0
- syntaxmatrix/static/icons/logo.png +0 -0
- syntaxmatrix/static/icons/logo3.png +0 -0
- syntaxmatrix/templates/admin_branding.html +104 -0
- syntaxmatrix/templates/admin_features.html +63 -0
- syntaxmatrix/templates/admin_secretes.html +108 -0
- syntaxmatrix/templates/change_password.html +124 -0
- syntaxmatrix/templates/dashboard.html +296 -131
- syntaxmatrix/templates/dataset_resize.html +535 -0
- syntaxmatrix/templates/edit_page.html +2535 -0
- syntaxmatrix/utils.py +2728 -2835
- {syntaxmatrix-2.5.5.5.dist-info → syntaxmatrix-2.6.2.dist-info}/METADATA +6 -2
- {syntaxmatrix-2.5.5.5.dist-info → syntaxmatrix-2.6.2.dist-info}/RECORD +42 -25
- syntaxmatrix/generate_page.py +0 -634
- syntaxmatrix/static/icons/hero_bg.jpg +0 -0
- {syntaxmatrix-2.5.5.5.dist-info → syntaxmatrix-2.6.2.dist-info}/WHEEL +0 -0
- {syntaxmatrix-2.5.5.5.dist-info → syntaxmatrix-2.6.2.dist-info}/licenses/LICENSE.txt +0 -0
- {syntaxmatrix-2.5.5.5.dist-info → syntaxmatrix-2.6.2.dist-info}/top_level.txt +0 -0
syntaxmatrix/db.py
CHANGED
|
@@ -14,9 +14,10 @@ TEMPLATES_DIR = os.path.join(_CLIENT_DIR, "templates")
|
|
|
14
14
|
os.makedirs(TEMPLATES_DIR, exist_ok=True)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
#
|
|
19
|
-
#
|
|
17
|
+
|
|
18
|
+
# ─────────────────────────────────────────────────────────
|
|
19
|
+
# Page
|
|
20
|
+
# ─────────────────────────────────────────────────────────
|
|
20
21
|
def init_db():
|
|
21
22
|
conn = sqlite3.connect(DB_PATH)
|
|
22
23
|
|
|
@@ -38,10 +39,50 @@ def init_db():
|
|
|
38
39
|
)
|
|
39
40
|
""")
|
|
40
41
|
|
|
42
|
+
conn.execute("""
|
|
43
|
+
CREATE TABLE IF NOT EXISTS secretes (
|
|
44
|
+
name TEXT PRIMARY KEY,
|
|
45
|
+
value TEXT NOT NULL,
|
|
46
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
47
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
48
|
+
)
|
|
49
|
+
""")
|
|
50
|
+
|
|
51
|
+
conn.execute("""
|
|
52
|
+
CREATE TABLE IF NOT EXISTS media_assets (
|
|
53
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
54
|
+
kind TEXT NOT NULL DEFAULT 'image',
|
|
55
|
+
rel_path TEXT NOT NULL UNIQUE,
|
|
56
|
+
thumb_path TEXT,
|
|
57
|
+
sha256 TEXT,
|
|
58
|
+
dhash TEXT,
|
|
59
|
+
width INTEGER,
|
|
60
|
+
height INTEGER,
|
|
61
|
+
mime TEXT,
|
|
62
|
+
source TEXT,
|
|
63
|
+
source_url TEXT,
|
|
64
|
+
author TEXT,
|
|
65
|
+
licence TEXT,
|
|
66
|
+
tags TEXT,
|
|
67
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
68
|
+
)
|
|
69
|
+
""")
|
|
70
|
+
|
|
71
|
+
conn.execute("""
|
|
72
|
+
CREATE TABLE IF NOT EXISTS app_settings (
|
|
73
|
+
key TEXT PRIMARY KEY,
|
|
74
|
+
value TEXT NOT NULL,
|
|
75
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
76
|
+
)
|
|
77
|
+
""")
|
|
78
|
+
|
|
41
79
|
conn.commit()
|
|
42
80
|
conn.close()
|
|
43
81
|
|
|
44
82
|
|
|
83
|
+
# ***************************************
|
|
84
|
+
# Pages Helpers
|
|
85
|
+
# ***************************************
|
|
45
86
|
def get_pages():
|
|
46
87
|
"""Return {page_name: html} resolving relative paths under syntaxmatrixdir/templates."""
|
|
47
88
|
import sqlite3
|
|
@@ -62,6 +103,44 @@ def get_pages():
|
|
|
62
103
|
return pages
|
|
63
104
|
|
|
64
105
|
|
|
106
|
+
def get_page_html(name: str):
|
|
107
|
+
"""
|
|
108
|
+
Return the latest HTML for a single page by reading the file path stored in DB.
|
|
109
|
+
This avoids stale in-memory cache (smx.pages) across Gunicorn workers.
|
|
110
|
+
"""
|
|
111
|
+
if not name:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
conn = sqlite3.connect(DB_PATH)
|
|
115
|
+
try:
|
|
116
|
+
row = conn.execute(
|
|
117
|
+
"SELECT content FROM pages WHERE lower(name) = lower(?)",
|
|
118
|
+
(name.strip(),)
|
|
119
|
+
).fetchone()
|
|
120
|
+
finally:
|
|
121
|
+
conn.close()
|
|
122
|
+
|
|
123
|
+
if not row:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
stored = (row[0] or "").strip()
|
|
127
|
+
if not stored:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
# Backwards compatible: if DB ever stored raw HTML instead of a file path
|
|
131
|
+
if stored.lstrip().startswith("<"):
|
|
132
|
+
return stored
|
|
133
|
+
|
|
134
|
+
# Resolve path (relative under syntaxmatrixdir)
|
|
135
|
+
abs_path = stored if os.path.isabs(stored) else os.path.join(_CLIENT_DIR, stored)
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
with open(abs_path, "r", encoding="utf-8") as f:
|
|
139
|
+
return f.read()
|
|
140
|
+
except Exception:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
65
144
|
def add_page(name, html):
|
|
66
145
|
"""Create templates/<slug>.html and store a relative path in the DB."""
|
|
67
146
|
filename = secure_filename(name.lower()) + ".html"
|
|
@@ -151,4 +230,381 @@ def delete_page(name):
|
|
|
151
230
|
|
|
152
231
|
cur.execute("DELETE FROM pages WHERE name = ?", (name,))
|
|
153
232
|
conn.commit()
|
|
154
|
-
conn.close()
|
|
233
|
+
conn.close()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ***************************************
|
|
237
|
+
# Secrete Helpers
|
|
238
|
+
# ***************************************
|
|
239
|
+
def set_secret(name: str, value: str) -> None:
|
|
240
|
+
"""Create/update a secret in the local SyntaxMatrix DB."""
|
|
241
|
+
if not name:
|
|
242
|
+
return
|
|
243
|
+
name = name.strip().upper()
|
|
244
|
+
conn = sqlite3.connect(DB_PATH)
|
|
245
|
+
try:
|
|
246
|
+
with conn:
|
|
247
|
+
conn.execute(
|
|
248
|
+
"""
|
|
249
|
+
INSERT INTO secretes (name, value, updated_at)
|
|
250
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
251
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
252
|
+
value = excluded.value,
|
|
253
|
+
updated_at = CURRENT_TIMESTAMP
|
|
254
|
+
""",
|
|
255
|
+
(name, value),
|
|
256
|
+
)
|
|
257
|
+
finally:
|
|
258
|
+
conn.close()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def get_secret(name: str) -> str | None:
|
|
262
|
+
"""Get a secret value, or None if missing."""
|
|
263
|
+
if not name:
|
|
264
|
+
return None
|
|
265
|
+
name = name.strip().upper()
|
|
266
|
+
conn = sqlite3.connect(DB_PATH)
|
|
267
|
+
try:
|
|
268
|
+
row = conn.execute("SELECT value FROM secretes WHERE name = ?", (name,)).fetchone()
|
|
269
|
+
return row[0] if row else None
|
|
270
|
+
finally:
|
|
271
|
+
conn.close()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def delete_secret(name: str) -> None:
|
|
275
|
+
if not name:
|
|
276
|
+
return
|
|
277
|
+
name = name.strip().upper()
|
|
278
|
+
conn = sqlite3.connect(DB_PATH)
|
|
279
|
+
try:
|
|
280
|
+
with conn:
|
|
281
|
+
conn.execute("DELETE FROM secretes WHERE name = ?", (name,))
|
|
282
|
+
finally:
|
|
283
|
+
conn.close()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def list_secret_names() -> list[str]:
|
|
287
|
+
"""Return secret names only (never values)."""
|
|
288
|
+
conn = sqlite3.connect(DB_PATH)
|
|
289
|
+
try:
|
|
290
|
+
rows = conn.execute("SELECT name FROM secretes ORDER BY name").fetchall()
|
|
291
|
+
return [r[0] for r in rows]
|
|
292
|
+
finally:
|
|
293
|
+
conn.close()
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ─────────────────────────────────────────────────────────
|
|
297
|
+
# Page navigation metadata (show_in_nav / nav_label)
|
|
298
|
+
# ─────────────────────────────────────────────────────────
|
|
299
|
+
def _init_page_nav_table():
|
|
300
|
+
"""Ensure the page_nav table exists and has all expected columns."""
|
|
301
|
+
import sqlite3 # safe if already imported at top
|
|
302
|
+
conn = sqlite3.connect(DB_PATH)
|
|
303
|
+
try:
|
|
304
|
+
with conn:
|
|
305
|
+
# Create table if missing (with nav_order already defined)
|
|
306
|
+
conn.execute("""
|
|
307
|
+
CREATE TABLE IF NOT EXISTS page_nav (
|
|
308
|
+
page_name TEXT PRIMARY KEY,
|
|
309
|
+
show_in_nav INTEGER NOT NULL DEFAULT 1,
|
|
310
|
+
nav_label TEXT,
|
|
311
|
+
nav_order INTEGER
|
|
312
|
+
)
|
|
313
|
+
""")
|
|
314
|
+
|
|
315
|
+
# If table existed before, make sure nav_order column is present
|
|
316
|
+
cur = conn.execute("PRAGMA table_info(page_nav)")
|
|
317
|
+
cols = [row[1] for row in cur.fetchall()]
|
|
318
|
+
if "nav_order" not in cols:
|
|
319
|
+
conn.execute("ALTER TABLE page_nav ADD COLUMN nav_order INTEGER")
|
|
320
|
+
finally:
|
|
321
|
+
conn.close()
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def set_page_nav(
|
|
325
|
+
page_name: str,
|
|
326
|
+
show_in_nav: bool = True,
|
|
327
|
+
nav_label: str | None = None,
|
|
328
|
+
nav_order: int | None = None,
|
|
329
|
+
) -> None:
|
|
330
|
+
"""
|
|
331
|
+
Upsert navigation preferences for a page.
|
|
332
|
+
"""
|
|
333
|
+
if not page_name:
|
|
334
|
+
return
|
|
335
|
+
_init_page_nav_table()
|
|
336
|
+
import sqlite3
|
|
337
|
+
conn = sqlite3.connect(DB_PATH)
|
|
338
|
+
try:
|
|
339
|
+
with conn:
|
|
340
|
+
conn.execute("""
|
|
341
|
+
INSERT INTO page_nav (page_name, show_in_nav, nav_label, nav_order)
|
|
342
|
+
VALUES (?, ?, ?, ?)
|
|
343
|
+
ON CONFLICT(page_name) DO UPDATE SET
|
|
344
|
+
show_in_nav = excluded.show_in_nav,
|
|
345
|
+
nav_label = excluded.nav_label,
|
|
346
|
+
nav_order = excluded.nav_order
|
|
347
|
+
""", (page_name.lower(), 1 if show_in_nav else 0, nav_label, nav_order),
|
|
348
|
+
)
|
|
349
|
+
finally:
|
|
350
|
+
conn.close()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def get_page_nav_map() -> dict:
|
|
354
|
+
"""
|
|
355
|
+
Return a dict mapping page_name -> {"show_in_nav": bool, "nav_label": str|None, "nav_order": int|None}.
|
|
356
|
+
"""
|
|
357
|
+
_init_page_nav_table()
|
|
358
|
+
import sqlite3
|
|
359
|
+
conn = sqlite3.connect(DB_PATH)
|
|
360
|
+
try:
|
|
361
|
+
cur = conn.cursor()
|
|
362
|
+
cur.execute("SELECT page_name, show_in_nav, nav_label, nav_order FROM page_nav")
|
|
363
|
+
rows = cur.fetchall()
|
|
364
|
+
finally:
|
|
365
|
+
conn.close()
|
|
366
|
+
|
|
367
|
+
result = {}
|
|
368
|
+
for name, show, label, order_val in rows:
|
|
369
|
+
if not name:
|
|
370
|
+
continue
|
|
371
|
+
result[name.lower()] = {
|
|
372
|
+
"show_in_nav": bool(show),
|
|
373
|
+
"nav_label": label,
|
|
374
|
+
"nav_order": order_val,
|
|
375
|
+
}
|
|
376
|
+
return result
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# ─────────────────────────────────────────────────────────
|
|
380
|
+
# Page layouts (builder JSON) kept separate from final HTML
|
|
381
|
+
# ─────────────────────────────────────────────────────────
|
|
382
|
+
def _init_page_layouts_table():
|
|
383
|
+
import sqlite3
|
|
384
|
+
conn = sqlite3.connect(DB_PATH)
|
|
385
|
+
try:
|
|
386
|
+
with conn:
|
|
387
|
+
conn.execute("""
|
|
388
|
+
CREATE TABLE IF NOT EXISTS page_layouts (
|
|
389
|
+
page_name TEXT PRIMARY KEY,
|
|
390
|
+
layout_json TEXT NOT NULL,
|
|
391
|
+
is_detached INTEGER NOT NULL DEFAULT 0,
|
|
392
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
393
|
+
)
|
|
394
|
+
""")
|
|
395
|
+
finally:
|
|
396
|
+
conn.close()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def upsert_page_layout(page_name: str, layout_json: str, is_detached: bool | None = None) -> None:
|
|
400
|
+
if not page_name:
|
|
401
|
+
return
|
|
402
|
+
_init_page_layouts_table()
|
|
403
|
+
import sqlite3
|
|
404
|
+
conn = sqlite3.connect(DB_PATH)
|
|
405
|
+
try:
|
|
406
|
+
with conn:
|
|
407
|
+
if is_detached is None:
|
|
408
|
+
conn.execute(
|
|
409
|
+
"""
|
|
410
|
+
INSERT INTO page_layouts (page_name, layout_json)
|
|
411
|
+
VALUES (?, ?)
|
|
412
|
+
ON CONFLICT(page_name) DO UPDATE SET
|
|
413
|
+
layout_json = excluded.layout_json,
|
|
414
|
+
updated_at = CURRENT_TIMESTAMP
|
|
415
|
+
""",
|
|
416
|
+
(page_name.lower(), layout_json),
|
|
417
|
+
)
|
|
418
|
+
else:
|
|
419
|
+
conn.execute(
|
|
420
|
+
"""
|
|
421
|
+
INSERT INTO page_layouts (page_name, layout_json, is_detached)
|
|
422
|
+
VALUES (?, ?, ?)
|
|
423
|
+
ON CONFLICT(page_name) DO UPDATE SET
|
|
424
|
+
layout_json = excluded.layout_json,
|
|
425
|
+
is_detached = excluded.is_detached,
|
|
426
|
+
updated_at = CURRENT_TIMESTAMP
|
|
427
|
+
""",
|
|
428
|
+
(page_name.lower(), layout_json, 1 if is_detached else 0),
|
|
429
|
+
)
|
|
430
|
+
finally:
|
|
431
|
+
conn.close()
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def get_page_layout(page_name: str) -> dict | None:
|
|
435
|
+
if not page_name:
|
|
436
|
+
return None
|
|
437
|
+
_init_page_layouts_table()
|
|
438
|
+
import sqlite3
|
|
439
|
+
conn = sqlite3.connect(DB_PATH)
|
|
440
|
+
try:
|
|
441
|
+
row = conn.execute(
|
|
442
|
+
"SELECT page_name, layout_json, is_detached, updated_at FROM page_layouts WHERE page_name = ?",
|
|
443
|
+
(page_name.lower(),),
|
|
444
|
+
).fetchone()
|
|
445
|
+
if not row:
|
|
446
|
+
return None
|
|
447
|
+
return {
|
|
448
|
+
"page_name": row[0],
|
|
449
|
+
"layout_json": row[1],
|
|
450
|
+
"is_detached": bool(row[2]),
|
|
451
|
+
"updated_at": row[3],
|
|
452
|
+
}
|
|
453
|
+
finally:
|
|
454
|
+
conn.close()
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _init_media_assets_table() -> None:
|
|
458
|
+
|
|
459
|
+
conn = sqlite3.connect(DB_PATH)
|
|
460
|
+
try:
|
|
461
|
+
with conn:
|
|
462
|
+
conn.execute(
|
|
463
|
+
"""
|
|
464
|
+
CREATE TABLE IF NOT EXISTS media_assets (
|
|
465
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
466
|
+
kind TEXT NOT NULL DEFAULT 'image',
|
|
467
|
+
rel_path TEXT NOT NULL UNIQUE,
|
|
468
|
+
thumb_path TEXT,
|
|
469
|
+
sha256 TEXT,
|
|
470
|
+
dhash TEXT,
|
|
471
|
+
width INTEGER,
|
|
472
|
+
height INTEGER,
|
|
473
|
+
mime TEXT,
|
|
474
|
+
source TEXT,
|
|
475
|
+
source_url TEXT,
|
|
476
|
+
author TEXT,
|
|
477
|
+
licence TEXT,
|
|
478
|
+
tags TEXT,
|
|
479
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
480
|
+
)
|
|
481
|
+
"""
|
|
482
|
+
)
|
|
483
|
+
finally:
|
|
484
|
+
conn.close()
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def upsert_media_asset(
|
|
488
|
+
*,
|
|
489
|
+
rel_path: str,
|
|
490
|
+
kind: str = "image",
|
|
491
|
+
thumb_path: str | None = None,
|
|
492
|
+
sha256: str | None = None,
|
|
493
|
+
dhash: str | None = None,
|
|
494
|
+
width: int | None = None,
|
|
495
|
+
height: int | None = None,
|
|
496
|
+
mime: str | None = None,
|
|
497
|
+
source: str | None = None,
|
|
498
|
+
source_url: str | None = None,
|
|
499
|
+
author: str | None = None,
|
|
500
|
+
licence: str | None = None,
|
|
501
|
+
tags: str | None = None,
|
|
502
|
+
) -> None:
|
|
503
|
+
if not rel_path:
|
|
504
|
+
return
|
|
505
|
+
_init_media_assets_table()
|
|
506
|
+
|
|
507
|
+
conn = sqlite3.connect(DB_PATH)
|
|
508
|
+
try:
|
|
509
|
+
with conn:
|
|
510
|
+
conn.execute(
|
|
511
|
+
"""
|
|
512
|
+
INSERT INTO media_assets (
|
|
513
|
+
kind, rel_path, thumb_path, sha256, dhash, width, height, mime,
|
|
514
|
+
source, source_url, author, licence, tags
|
|
515
|
+
)
|
|
516
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
517
|
+
ON CONFLICT(rel_path) DO UPDATE SET
|
|
518
|
+
kind = excluded.kind,
|
|
519
|
+
thumb_path = excluded.thumb_path,
|
|
520
|
+
sha256 = excluded.sha256,
|
|
521
|
+
dhash = excluded.dhash,
|
|
522
|
+
width = excluded.width,
|
|
523
|
+
height = excluded.height,
|
|
524
|
+
mime = excluded.mime,
|
|
525
|
+
source = excluded.source,
|
|
526
|
+
source_url = excluded.source_url,
|
|
527
|
+
author = excluded.author,
|
|
528
|
+
licence = excluded.licence,
|
|
529
|
+
tags = excluded.tags
|
|
530
|
+
""",
|
|
531
|
+
(
|
|
532
|
+
kind, rel_path, thumb_path, sha256, dhash, width, height, mime,
|
|
533
|
+
source, source_url, author, licence, tags,
|
|
534
|
+
),
|
|
535
|
+
)
|
|
536
|
+
finally:
|
|
537
|
+
conn.close()
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def get_media_asset_by_rel_path(rel_path: str) -> dict | None:
|
|
541
|
+
if not rel_path:
|
|
542
|
+
return None
|
|
543
|
+
_init_media_assets_table()
|
|
544
|
+
import sqlite3
|
|
545
|
+
conn = sqlite3.connect(DB_PATH)
|
|
546
|
+
try:
|
|
547
|
+
row = conn.execute(
|
|
548
|
+
"""
|
|
549
|
+
SELECT kind, rel_path, thumb_path, sha256, dhash, width, height, mime,
|
|
550
|
+
source, source_url, author, licence, tags, created_at
|
|
551
|
+
FROM media_assets WHERE rel_path = ?
|
|
552
|
+
""",
|
|
553
|
+
(rel_path,),
|
|
554
|
+
).fetchone()
|
|
555
|
+
if not row:
|
|
556
|
+
return None
|
|
557
|
+
return {
|
|
558
|
+
"kind": row[0],
|
|
559
|
+
"rel_path": row[1],
|
|
560
|
+
"thumb_path": row[2],
|
|
561
|
+
"sha256": row[3],
|
|
562
|
+
"dhash": row[4],
|
|
563
|
+
"width": row[5],
|
|
564
|
+
"height": row[6],
|
|
565
|
+
"mime": row[7],
|
|
566
|
+
"source": row[8],
|
|
567
|
+
"source_url": row[9],
|
|
568
|
+
"author": row[10],
|
|
569
|
+
"licence": row[11],
|
|
570
|
+
"tags": row[12],
|
|
571
|
+
"created_at": row[13],
|
|
572
|
+
}
|
|
573
|
+
finally:
|
|
574
|
+
conn.close()
|
|
575
|
+
|
|
576
|
+
# ***************************************
|
|
577
|
+
# App Settings Helpers (feature toggles)
|
|
578
|
+
# ***************************************
|
|
579
|
+
def set_setting(key: str, value: str) -> None:
|
|
580
|
+
if not key:
|
|
581
|
+
return
|
|
582
|
+
key = key.strip()
|
|
583
|
+
value = "" if value is None else str(value)
|
|
584
|
+
conn = sqlite3.connect(DB_PATH)
|
|
585
|
+
try:
|
|
586
|
+
with conn:
|
|
587
|
+
conn.execute(
|
|
588
|
+
"""
|
|
589
|
+
INSERT INTO app_settings (key, value, updated_at)
|
|
590
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
591
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
592
|
+
value = excluded.value,
|
|
593
|
+
updated_at = CURRENT_TIMESTAMP
|
|
594
|
+
""",
|
|
595
|
+
(key, value),
|
|
596
|
+
)
|
|
597
|
+
finally:
|
|
598
|
+
conn.close()
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def get_setting(key: str, default: str | None = None) -> str | None:
|
|
602
|
+
if not key:
|
|
603
|
+
return default
|
|
604
|
+
key = key.strip()
|
|
605
|
+
conn = sqlite3.connect(DB_PATH)
|
|
606
|
+
try:
|
|
607
|
+
row = conn.execute("SELECT value FROM app_settings WHERE key = ?", (key,)).fetchone()
|
|
608
|
+
return row[0] if row else default
|
|
609
|
+
finally:
|
|
610
|
+
conn.close()
|
|
@@ -28,8 +28,6 @@ except Exception: # pragma: no cover
|
|
|
28
28
|
__all__ = ["show"]
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
# ---- internal helpers -------------------------------------------------------
|
|
32
|
-
|
|
33
31
|
|
|
34
32
|
def _wrap_html_table(html: str) -> str:
|
|
35
33
|
"""Apply consistent UI styling and horizontal scrolling."""
|
|
@@ -47,10 +45,7 @@ def _wrap_html_table(html: str) -> str:
|
|
|
47
45
|
)
|
|
48
46
|
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def show(obj: Any) -> None:
|
|
48
|
+
def show_table(obj: Any) -> None:
|
|
54
49
|
"""
|
|
55
50
|
Render common objects so the Dashboard (or chat) always shows output.
|
|
56
51
|
|
|
@@ -104,3 +99,4 @@ def show(obj: Any) -> None:
|
|
|
104
99
|
# 6) Fallback: show as preformatted text (safe and predictable)
|
|
105
100
|
display(HTML(f"<pre>{obj}</pre>"))
|
|
106
101
|
return None
|
|
102
|
+
|
|
File without changes
|