syntaxmatrix 2.6.4.3__tar.gz → 2.6.4.4__tar.gz

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 (98) hide show
  1. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/PKG-INFO +1 -1
  2. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/SyntaxMatrix.egg-info/PKG-INFO +1 -1
  3. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/SyntaxMatrix.egg-info/SOURCES.txt +7 -0
  4. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/setup.py +1 -1
  5. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/core.py +18 -2
  6. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/db.py +77 -0
  7. syntaxmatrix-2.6.4.4/syntaxmatrix/db_backends/__init__.py +1 -0
  8. syntaxmatrix-2.6.4.4/syntaxmatrix/db_backends/postgres_backend.py +14 -0
  9. syntaxmatrix-2.6.4.4/syntaxmatrix/db_backends/sqlite_backend.py +258 -0
  10. syntaxmatrix-2.6.4.4/syntaxmatrix/db_contract.py +71 -0
  11. syntaxmatrix-2.6.4.4/syntaxmatrix/plugin_manager.py +114 -0
  12. syntaxmatrix-2.6.4.4/syntaxmatrix/premium/__init__.py +10 -0
  13. syntaxmatrix-2.6.4.4/syntaxmatrix/premium/gate.py +107 -0
  14. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/routes.py +2 -2
  15. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/LICENSE.txt +0 -0
  16. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/README.md +0 -0
  17. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/SyntaxMatrix.egg-info/dependency_links.txt +0 -0
  18. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/SyntaxMatrix.egg-info/requires.txt +0 -0
  19. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/SyntaxMatrix.egg-info/top_level.txt +0 -0
  20. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/pyproject.toml +0 -0
  21. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/setup.cfg +0 -0
  22. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/__init__.py +0 -0
  23. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/__init__.py +0 -0
  24. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/agent_tools.py +0 -0
  25. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/agents.py +0 -0
  26. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/agents_orchestrer.py +0 -0
  27. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/code_tools_registry.py +0 -0
  28. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/model_templates.py +0 -0
  29. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/auth.py +0 -0
  30. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/bootstrap.py +0 -0
  31. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/commentary.py +0 -0
  32. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/dataset_preprocessing.py +0 -0
  33. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/display_html.py +0 -0
  34. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/emailer.py +0 -0
  35. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/file_processor.py +0 -0
  36. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/gpt_models_latest.py +0 -0
  37. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/history_store.py +0 -0
  38. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/kernel_manager.py +0 -0
  39. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/llm_store.py +0 -0
  40. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/media/__init__.py +0 -0
  41. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/media/media_pixabay.py +0 -0
  42. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/models.py +0 -0
  43. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/page_builder_defaults.py +0 -0
  44. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/page_builder_generation.py +0 -0
  45. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/page_layout_contract.py +0 -0
  46. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/page_patch_publish.py +0 -0
  47. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/plottings.py +0 -0
  48. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/preface.py +0 -0
  49. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/profiles.py +0 -0
  50. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/project_root.py +0 -0
  51. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/selftest_page_templates.py +0 -0
  52. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/session.py +0 -0
  53. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/__init__.py +0 -0
  54. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/client_items.py +0 -0
  55. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/default.yaml +0 -0
  56. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/logging.py +0 -0
  57. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/model_map.py +0 -0
  58. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/prompts.py +0 -0
  59. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/string_navbar.py +0 -0
  60. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/smiv.py +0 -0
  61. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/smpv.py +0 -0
  62. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/assets/hero-default.svg +0 -0
  63. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/css/style.css +0 -0
  64. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/docs.md +0 -0
  65. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/bot_icon.png +0 -0
  66. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/favicon.png +0 -0
  67. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/logo.png +0 -0
  68. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/logo3.png +0 -0
  69. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/svg_497526.svg +0 -0
  70. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/svg_497528.svg +0 -0
  71. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/js/chat.js +0 -0
  72. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/js/sidebar.js +0 -0
  73. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/js/widgets.js +0 -0
  74. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/admin_branding.html +0 -0
  75. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/admin_features.html +0 -0
  76. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/admin_secretes.html +0 -0
  77. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/change_password.html +0 -0
  78. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/code_cell.html +0 -0
  79. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/dashboard.html +0 -0
  80. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/dataset_resize.html +0 -0
  81. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/docs.html +0 -0
  82. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/edit_page.html +0 -0
  83. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/error.html +0 -0
  84. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/login.html +0 -0
  85. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/register.html +0 -0
  86. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/themes.py +0 -0
  87. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/ui_modes.py +0 -0
  88. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/utils.py +0 -0
  89. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vector_db.py +0 -0
  90. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/__init__.py +0 -0
  91. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/adapters/__init__.py +0 -0
  92. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/adapters/milvus_adapter.py +0 -0
  93. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/adapters/pgvector_adapter.py +0 -0
  94. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/adapters/sqlite_adapter.py +0 -0
  95. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/base.py +0 -0
  96. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/registry.py +0 -0
  97. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectorizer.py +0 -0
  98. {syntaxmatrix-2.6.4.3 → syntaxmatrix-2.6.4.4}/syntaxmatrix/workspace_db.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syntaxmatrix
3
- Version: 2.6.4.3
3
+ Version: 2.6.4.4
4
4
  Summary: SyntaxMUI: A customizable framework for Python AI Assistant Projects.
5
5
  Author: Bob Nti
6
6
  Author-email: bob.nti@syntaxmatrix.net
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syntaxmatrix
3
- Version: 2.6.4.3
3
+ Version: 2.6.4.4
4
4
  Summary: SyntaxMUI: A customizable framework for Python AI Assistant Projects.
5
5
  Author: Bob Nti
6
6
  Author-email: bob.nti@syntaxmatrix.net
@@ -14,6 +14,7 @@ syntaxmatrix/commentary.py
14
14
  syntaxmatrix/core.py
15
15
  syntaxmatrix/dataset_preprocessing.py
16
16
  syntaxmatrix/db.py
17
+ syntaxmatrix/db_contract.py
17
18
  syntaxmatrix/display_html.py
18
19
  syntaxmatrix/emailer.py
19
20
  syntaxmatrix/file_processor.py
@@ -27,6 +28,7 @@ syntaxmatrix/page_builder_generation.py
27
28
  syntaxmatrix/page_layout_contract.py
28
29
  syntaxmatrix/page_patch_publish.py
29
30
  syntaxmatrix/plottings.py
31
+ syntaxmatrix/plugin_manager.py
30
32
  syntaxmatrix/preface.py
31
33
  syntaxmatrix/profiles.py
32
34
  syntaxmatrix/project_root.py
@@ -52,8 +54,13 @@ syntaxmatrix/agentic/agents.py
52
54
  syntaxmatrix/agentic/agents_orchestrer.py
53
55
  syntaxmatrix/agentic/code_tools_registry.py
54
56
  syntaxmatrix/agentic/model_templates.py
57
+ syntaxmatrix/db_backends/__init__.py
58
+ syntaxmatrix/db_backends/postgres_backend.py
59
+ syntaxmatrix/db_backends/sqlite_backend.py
55
60
  syntaxmatrix/media/__init__.py
56
61
  syntaxmatrix/media/media_pixabay.py
62
+ syntaxmatrix/premium/__init__.py
63
+ syntaxmatrix/premium/gate.py
57
64
  syntaxmatrix/settings/__init__.py
58
65
  syntaxmatrix/settings/client_items.py
59
66
  syntaxmatrix/settings/default.yaml
@@ -8,7 +8,7 @@ with open(os.path.join(this_directory, "README.md"), encoding="utf-8") as f:
8
8
 
9
9
  setup(
10
10
  name="syntaxmatrix",
11
- version="2.6.4.3",
11
+ version="2.6.4.4",
12
12
  license="MIT",
13
13
  classifiers=[
14
14
  "Programming Language :: Python :: 3.9",
@@ -34,6 +34,10 @@ from syntaxmatrix.settings.prompts import(
34
34
  )
35
35
  from syntaxmatrix.settings.client_items import read_client_file, getenv_api_key
36
36
 
37
+ from .premium import FeatureGate
38
+ from .plugin_manager import PluginManager
39
+
40
+
37
41
  # ──────── framework‐local storage paths ────────
38
42
  # this ensures the key & data always live under the package dir,
39
43
  _CLIENT_DIR = detect_project_root()
@@ -127,6 +131,18 @@ class SyntaxMUI:
127
131
  self.is_streaming = False
128
132
  self.stream_args = {}
129
133
  self._apply_feature_flags_from_db()
134
+ # Premium (entitlements + plugins). Safe no-op unless configured.
135
+ try:
136
+ self.feature_gate = FeatureGate(client_dir=_CLIENT_DIR, db=db)
137
+ except Exception:
138
+ self.feature_gate = FeatureGate(client_dir=_CLIENT_DIR)
139
+
140
+ try:
141
+ self.plugins = PluginManager(self, gate=self.feature_gate, db=db)
142
+ self.plugins.load_all()
143
+ except Exception:
144
+ # Never break app boot because of premium plumbing
145
+ self.plugins = PluginManager(self)
130
146
 
131
147
  self._recent_visual_summaries = []
132
148
 
@@ -345,8 +361,8 @@ class SyntaxMUI:
345
361
  return str(v or "").strip().lower() in ("1", "true", "yes", "on")
346
362
 
347
363
  try:
348
- stream_v = db.get_setting("feature.stream_mode", "0")
349
- user_files_v = db.get_setting("feature.user_files", "0")
364
+ stream_v = db.get_setting("feature.stream_mode", "1")
365
+ user_files_v = db.get_setting("feature.user_files", "1")
350
366
 
351
367
  self.is_streaming = _truthy(stream_v)
352
368
  self.user_files_enabled = _truthy(user_files_v)
@@ -6,6 +6,8 @@ from werkzeug.utils import secure_filename
6
6
  from syntaxmatrix.project_root import detect_project_root
7
7
 
8
8
 
9
+ _SMX_DB_PROVIDER = (os.environ.get("SMX_DB_PROVIDER") or "sqlite").strip().lower()
10
+
9
11
  _CLIENT_DIR = detect_project_root()
10
12
  DB_PATH = os.path.join(_CLIENT_DIR, "data", "syntaxmatrix.db")
11
13
  os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
@@ -624,3 +626,78 @@ def get_setting(key: str, default: str | None = None) -> str | None:
624
626
  return row[0] if row else default
625
627
  finally:
626
628
  conn.close()
629
+
630
+
631
+ # ============================================================================
632
+ # Optional DB backend override (Premium hook)
633
+ #
634
+ # Default behaviour (no env vars): SQLite stays in use exactly as before.
635
+ #
636
+ # To enable a premium backend (e.g. Postgres), set:
637
+ # SMX_DB_PROVIDER=postgres
638
+ #
639
+ # Optional:
640
+ # SMX_DB_BACKEND_MODULE=syntaxmatrix_premium.db_backends.postgres_backend
641
+ #
642
+ # The backend module should either expose:
643
+ # - install(target_globals: dict) -> None (preferred)
644
+ # OR export a compatible surface (functions/constants) that will be copied
645
+ # into this module namespace.
646
+ # ============================================================================
647
+
648
+ def _smx_apply_optional_backend_override() -> None:
649
+ import os
650
+ import importlib
651
+
652
+ provider = (os.getenv("SMX_DB_PROVIDER") or "sqlite").strip().lower()
653
+ if provider in ("", "sqlite", "sqlite3"):
654
+ return
655
+
656
+ # Pick backend module (allow override for custom installations)
657
+ mod_name = (os.getenv("SMX_DB_BACKEND_MODULE") or "").strip()
658
+ if not mod_name:
659
+ if provider in ("postgres", "postgresql", "pg"):
660
+ mod_name = "syntaxmatrix.db_backends.postgres_backend"
661
+ else:
662
+ # Convention: syntaxmatrix.db_backends.<provider>_backend
663
+ mod_name = f"syntaxmatrix.db_backends.{provider}_backend"
664
+
665
+ try:
666
+ mod = importlib.import_module(mod_name)
667
+ except Exception as e:
668
+ raise RuntimeError(
669
+ f"SMX_DB_PROVIDER='{provider}' requested, but backend module '{mod_name}' "
670
+ f"could not be imported. Install the premium backend package (or set "
671
+ f"SMX_DB_BACKEND_MODULE) and try again. Underlying error: {e}"
672
+ ) from e
673
+
674
+ installer = getattr(mod, "install", None)
675
+ if callable(installer):
676
+ installer(globals())
677
+ else:
678
+ names = getattr(mod, "__all__", None)
679
+ if not names:
680
+ names = [n for n in dir(mod) if not n.startswith("_")]
681
+ for n in names:
682
+ globals()[n] = getattr(mod, n)
683
+
684
+ # Helpful introspection
685
+ globals()["_SMX_DB_PROVIDER"] = provider
686
+ globals()["_SMX_DB_BACKEND_MODULE"] = mod_name
687
+
688
+
689
+ # Apply on import
690
+ _smx_apply_optional_backend_override()
691
+
692
+ # If a non-SQLite backend was requested, validate the required API surface now.
693
+ try:
694
+ import os as _os
695
+ _provider = (globals().get("_SMX_DB_PROVIDER") or _os.getenv("SMX_DB_PROVIDER") or "sqlite").strip().lower()
696
+
697
+ if _provider not in ("", "sqlite", "sqlite3"):
698
+ from .db_contract import assert_backend_implements_core_api as _assert_backend_implements_core_api
699
+ _assert_backend_implements_core_api(globals(), provider=_provider)
700
+ except Exception:
701
+ # Fail fast with a clear error; this is intentional for premium backends.
702
+ raise
703
+
@@ -0,0 +1 @@
1
+ # DB backends package
@@ -0,0 +1,14 @@
1
+ # syntaxmatrix/db_backends/postgres_backend.py
2
+ """
3
+ PostgreSQL backend
4
+ """
5
+
6
+ def install(ns: dict) -> None:
7
+ raise RuntimeError(
8
+ "Postgres backend is not available in the free tier.\n\n"
9
+ "You set SMX_DB_PROVIDER=postgres, but the premium Postgres backend package "
10
+ "is not installed.\n\n"
11
+ "Fix:\n"
12
+ "- Install the SyntaxMatrix premium Postgres backend package, then restart.\n"
13
+ "- Or set SMX_DB_PROVIDER=sqlite to use the built-in SQLite backend.\n"
14
+ )
@@ -0,0 +1,258 @@
1
+ from __future__ import annotations
2
+ import sqlite3
3
+ import os
4
+ import json
5
+ from werkzeug.utils import secure_filename
6
+ from syntaxmatrix.project_root import detect_project_root
7
+
8
+
9
+ _CLIENT_DIR = detect_project_root()
10
+ DB_PATH = os.path.join(_CLIENT_DIR, "data", "syntaxmatrix.db")
11
+ os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
12
+
13
+ TEMPLATES_DIR = os.path.join(_CLIENT_DIR, "templates")
14
+ os.makedirs(TEMPLATES_DIR, exist_ok=True)
15
+
16
+
17
+ # ------------ Utils ------------
18
+ def connect_db():
19
+ conn = sqlite3.connect(DB_PATH)
20
+ conn.row_factory = sqlite3.Row
21
+ return conn
22
+
23
+ def _col_exists(conn, table: str, col: str) -> bool:
24
+ cur = conn.execute(f"PRAGMA table_info({table})")
25
+ cols = [r["name"] for r in cur.fetchall()]
26
+ return col in cols
27
+
28
+ def _ensure_column(conn, table: str, col: str, col_sql: str):
29
+ if not _col_exists(conn, table, col):
30
+ conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {col_sql}")
31
+
32
+ def _ensure_index(conn, idx_sql: str):
33
+ try:
34
+ conn.execute(idx_sql)
35
+ except Exception:
36
+ pass
37
+
38
+
39
+ # ------------ Schema init ------------
40
+ def init_db():
41
+ conn = connect_db()
42
+ conn.execute("""
43
+ CREATE TABLE IF NOT EXISTS pages (
44
+ name TEXT PRIMARY KEY,
45
+ content TEXT NOT NULL,
46
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
47
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
48
+ )
49
+ """)
50
+ conn.execute("""
51
+ CREATE TABLE IF NOT EXISTS page_layouts (
52
+ name TEXT PRIMARY KEY,
53
+ layout_json TEXT NOT NULL,
54
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
55
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
56
+ )
57
+ """)
58
+ conn.execute("""
59
+ CREATE TABLE IF NOT EXISTS settings (
60
+ key TEXT PRIMARY KEY,
61
+ value TEXT NOT NULL,
62
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
63
+ )
64
+ """)
65
+ conn.execute("""
66
+ CREATE TABLE IF NOT EXISTS audit_log (
67
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
68
+ action TEXT NOT NULL,
69
+ subject TEXT,
70
+ meta TEXT,
71
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
72
+ )
73
+ """)
74
+
75
+ _init_media_assets_table(conn)
76
+
77
+ conn.commit()
78
+ conn.close()
79
+
80
+
81
+ # ------------ Pages ------------
82
+ def get_pages():
83
+ conn = connect_db()
84
+ cur = conn.execute("SELECT name FROM pages ORDER BY name")
85
+ rows = [r["name"] for r in cur.fetchall()]
86
+ conn.close()
87
+ return rows
88
+
89
+ def get_page(name: str):
90
+ conn = connect_db()
91
+ cur = conn.execute("SELECT name, content FROM pages WHERE name = ?", (name,))
92
+ row = cur.fetchone()
93
+ conn.close()
94
+ return dict(row) if row else None
95
+
96
+ def add_page(name: str, content: str):
97
+ conn = connect_db()
98
+ conn.execute("INSERT OR REPLACE INTO pages (name, content) VALUES (?, ?)", (name, content))
99
+ conn.commit()
100
+ conn.close()
101
+
102
+ def update_page(name: str, content: str):
103
+ conn = connect_db()
104
+ conn.execute("UPDATE pages SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?", (content, name))
105
+ conn.commit()
106
+ conn.close()
107
+
108
+ def delete_page(name: str):
109
+ conn = connect_db()
110
+ conn.execute("DELETE FROM pages WHERE name = ?", (name,))
111
+ conn.execute("DELETE FROM page_layouts WHERE name = ?", (name,))
112
+ conn.commit()
113
+ conn.close()
114
+
115
+ def rename_page(old_name: str, new_name: str):
116
+ conn = connect_db()
117
+ conn.execute("UPDATE pages SET name = ? WHERE name = ?", (new_name, old_name))
118
+ conn.execute("UPDATE page_layouts SET name = ? WHERE name = ?", (new_name, old_name))
119
+ conn.commit()
120
+ conn.close()
121
+
122
+
123
+ # ------------ Layouts ------------
124
+ def upsert_page_layout(name: str, layout_json: str):
125
+ conn = connect_db()
126
+ conn.execute(
127
+ "INSERT OR REPLACE INTO page_layouts (name, layout_json) VALUES (?, ?)",
128
+ (name, layout_json)
129
+ )
130
+ conn.commit()
131
+ conn.close()
132
+
133
+ def get_page_layout(name: str):
134
+ conn = connect_db()
135
+ cur = conn.execute("SELECT name, layout_json FROM page_layouts WHERE name = ?", (name,))
136
+ row = cur.fetchone()
137
+ conn.close()
138
+ return dict(row) if row else None
139
+
140
+ def get_all_page_layouts():
141
+ conn = connect_db()
142
+ cur = conn.execute("SELECT name, layout_json FROM page_layouts ORDER BY name")
143
+ rows = [dict(r) for r in cur.fetchall()]
144
+ conn.close()
145
+ return rows
146
+
147
+
148
+ # ------------ Settings ------------
149
+ def get_setting(key: str, default=None):
150
+ conn = connect_db()
151
+ cur = conn.execute("SELECT value FROM settings WHERE key = ?", (key,))
152
+ row = cur.fetchone()
153
+ conn.close()
154
+ if not row:
155
+ return default
156
+ try:
157
+ return json.loads(row["value"])
158
+ except Exception:
159
+ return row["value"]
160
+
161
+ def set_setting(key: str, value):
162
+ conn = connect_db()
163
+ conn.execute(
164
+ "INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)",
165
+ (key, json.dumps(value))
166
+ )
167
+ conn.commit()
168
+ conn.close()
169
+
170
+ def delete_setting(key: str):
171
+ conn = connect_db()
172
+ conn.execute("DELETE FROM settings WHERE key = ?", (key,))
173
+ conn.commit()
174
+ conn.close()
175
+
176
+
177
+ # ------------ Audit log ------------
178
+ def audit(action: str, subject: str = "", meta: dict | None = None):
179
+ conn = connect_db()
180
+ conn.execute(
181
+ "INSERT INTO audit_log (action, subject, meta) VALUES (?, ?, ?)",
182
+ (action, subject, json.dumps(meta or {}))
183
+ )
184
+ conn.commit()
185
+ conn.close()
186
+
187
+
188
+ # ------------ Media assets (dedupe + metadata) ------------
189
+ MEDIA_DIR = os.path.join(_CLIENT_DIR, "uploads", "media")
190
+ os.makedirs(MEDIA_DIR, exist_ok=True)
191
+
192
+ MEDIA_IMAGES_DIR = os.path.join(MEDIA_DIR, "images")
193
+ os.makedirs(MEDIA_IMAGES_DIR, exist_ok=True)
194
+
195
+ MEDIA_THUMBS_DIR = os.path.join(MEDIA_DIR, "thumbs")
196
+ os.makedirs(MEDIA_THUMBS_DIR, exist_ok=True)
197
+
198
+ def _init_media_assets_table(conn):
199
+ conn.execute("""
200
+ CREATE TABLE IF NOT EXISTS media_assets (
201
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
202
+ kind TEXT NOT NULL DEFAULT 'image',
203
+ rel_path TEXT NOT NULL UNIQUE,
204
+ thumb_path TEXT,
205
+ sha256 TEXT,
206
+ dhash TEXT,
207
+ width INTEGER,
208
+ height INTEGER,
209
+ bytes INTEGER,
210
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
211
+ )
212
+ """)
213
+ _ensure_index(conn, "CREATE INDEX IF NOT EXISTS idx_media_assets_kind ON media_assets(kind)")
214
+ _ensure_index(conn, "CREATE INDEX IF NOT EXISTS idx_media_assets_sha256 ON media_assets(sha256)")
215
+ _ensure_index(conn, "CREATE INDEX IF NOT EXISTS idx_media_assets_dhash ON media_assets(dhash)")
216
+
217
+ def upsert_media_asset(kind: str, rel_path: str, thumb_path: str | None = None,
218
+ sha256: str | None = None, dhash: str | None = None,
219
+ width: int | None = None, height: int | None = None, bytes_: int | None = None):
220
+ conn = connect_db()
221
+ _init_media_assets_table(conn)
222
+ conn.execute(
223
+ """
224
+ INSERT INTO media_assets (kind, rel_path, thumb_path, sha256, dhash, width, height, bytes)
225
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
226
+ ON CONFLICT(rel_path) DO UPDATE SET
227
+ kind=excluded.kind,
228
+ thumb_path=excluded.thumb_path,
229
+ sha256=excluded.sha256,
230
+ dhash=excluded.dhash,
231
+ width=excluded.width,
232
+ height=excluded.height,
233
+ bytes=excluded.bytes
234
+ """,
235
+ (kind, rel_path, thumb_path, sha256, dhash, width, height, bytes_)
236
+ )
237
+ conn.commit()
238
+ conn.close()
239
+
240
+ def get_media_asset_by_rel_path(rel_path: str):
241
+ conn = connect_db()
242
+ cur = conn.execute("SELECT * FROM media_assets WHERE rel_path = ?", (rel_path,))
243
+ row = cur.fetchone()
244
+ conn.close()
245
+ return dict(row) if row else None
246
+
247
+ def list_media_assets(kind: str = "image"):
248
+ conn = connect_db()
249
+ cur = conn.execute("SELECT * FROM media_assets WHERE kind = ? ORDER BY id DESC", (kind,))
250
+ rows = [dict(r) for r in cur.fetchall()]
251
+ conn.close()
252
+ return rows
253
+
254
+ def normalise_media_filename(filename: str) -> str:
255
+ filename = secure_filename(filename or "file")
256
+ if not filename:
257
+ filename = "file"
258
+ return filename
@@ -0,0 +1,71 @@
1
+ # syntaxmatrix/db_contract.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Dict, Iterable, Optional
5
+
6
+
7
+ # Keep this list tight: only functions that the framework truly depends on.
8
+ # If we add more later, we do it deliberately.
9
+ CORE_REQUIRED_FUNCTIONS = (
10
+ # Pages
11
+ "get_pages",
12
+ "get_page_html",
13
+ "add_page",
14
+ "update_page",
15
+ "delete_page",
16
+
17
+ # Secrets
18
+ "get_secrets",
19
+ "set_secret",
20
+ "delete_secret",
21
+
22
+ # Nav
23
+ "get_nav_links",
24
+ "set_nav_links",
25
+
26
+ # Page layouts (builder)
27
+ "get_page_layout",
28
+ "upsert_page_layout",
29
+ "delete_page_layout",
30
+
31
+ # Media library
32
+ "add_media_file",
33
+ "list_media_files",
34
+ "delete_media_file",
35
+
36
+ # Generic settings (used by branding + profiles + other admin toggles)
37
+ "get_setting",
38
+ "set_setting",
39
+
40
+ # Optional: some backends may want init, but we do not force it here.
41
+ )
42
+
43
+
44
+ def assert_backend_implements_core_api(ns: Dict[str, object], *, provider: str = "") -> None:
45
+ """
46
+ Validate that the loaded backend provides the minimum required surface.
47
+
48
+ `ns` is usually `globals()` from syntaxmatrix.db (the facade module).
49
+
50
+ We keep this strict for non-SQLite providers, because:
51
+ - SQLite is the built-in reference backend.
52
+ - Premium/Cloud backends must be complete, or we fail fast with a clear error.
53
+ """
54
+ missing = []
55
+ for fn in CORE_REQUIRED_FUNCTIONS:
56
+ obj = ns.get(fn)
57
+ if not callable(obj):
58
+ missing.append(fn)
59
+
60
+ if missing:
61
+ prov = provider or "unknown"
62
+ raise RuntimeError(
63
+ "SyntaxMatrix DB backend validation failed.\n"
64
+ f"Provider: {prov}\n"
65
+ "Missing required functions:\n"
66
+ f" - " + "\n - ".join(missing) + "\n\n"
67
+ "Fix:\n"
68
+ "- If you are using the premium Postgres backend, ensure the premium package is installed\n"
69
+ " and that your backend module's install(ns) correctly injects these functions.\n"
70
+ "- If you are writing your own backend, implement the missing functions.\n"
71
+ )
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import json
5
+ import os
6
+ from dataclasses import dataclass
7
+ from typing import Any, Dict, List, Optional, Tuple
8
+
9
+
10
+ def _safe_json_loads(raw: str, *, default: Any) -> Any:
11
+ try:
12
+ return json.loads(raw)
13
+ except Exception:
14
+ return default
15
+
16
+
17
+ @dataclass
18
+ class PluginSpec:
19
+ """A single plugin to load.
20
+
21
+ module: python module path (e.g. 'syntaxmatrix_premium_cloud_db')
22
+ name: entitlement key (e.g. 'cloud_db')
23
+ """
24
+
25
+ name: str
26
+ module: str
27
+
28
+
29
+ class PluginManager:
30
+ """Loads optional plugins (typically premium) in a controlled, safe way."""
31
+
32
+ ENV_PLUGINS = "SMX_PREMIUM_PLUGINS" # JSON list of {name,module}
33
+ DB_PLUGINS_KEY = "premium.plugins" # JSON list of {name,module}
34
+
35
+ def __init__(self, smx: object, *, gate: Optional[object] = None, db: Optional[object] = None):
36
+ self._smx = smx
37
+ self._gate = gate
38
+ self._db = db
39
+ self.loaded: Dict[str, str] = {} # name -> module
40
+ self.errors: List[str] = []
41
+
42
+ def _specs_from_env(self) -> List[PluginSpec]:
43
+ raw = os.environ.get(self.ENV_PLUGINS, "").strip()
44
+ if not raw:
45
+ return []
46
+ data = _safe_json_loads(raw, default=[])
47
+ return self._coerce_specs(data)
48
+
49
+ def _specs_from_db(self) -> List[PluginSpec]:
50
+ if not self._db:
51
+ return []
52
+ get_setting = getattr(self._db, "get_setting", None)
53
+ if not callable(get_setting):
54
+ return []
55
+ raw = get_setting(self.DB_PLUGINS_KEY, "[]")
56
+ data = _safe_json_loads(str(raw or "[]"), default=[])
57
+ return self._coerce_specs(data)
58
+
59
+ def _coerce_specs(self, data: Any) -> List[PluginSpec]:
60
+ out: List[PluginSpec] = []
61
+ if isinstance(data, list):
62
+ for row in data:
63
+ if not isinstance(row, dict):
64
+ continue
65
+ name = str(row.get("name") or "").strip()
66
+ module = str(row.get("module") or "").strip()
67
+ if name and module:
68
+ out.append(PluginSpec(name=name, module=module))
69
+ return out
70
+
71
+ def _entitled(self, name: str) -> bool:
72
+ if not self._gate:
73
+ return True # if no gate configured, don't block
74
+ enabled = getattr(self._gate, "enabled", None)
75
+ if not callable(enabled):
76
+ return True
77
+ try:
78
+ return bool(enabled(name))
79
+ except Exception:
80
+ return False
81
+
82
+ def load_all(self) -> Tuple[Dict[str, str], List[str]]:
83
+ """Load all configured plugins. Returns (loaded, errors)."""
84
+ specs = self._specs_from_env()
85
+ if not specs:
86
+ specs = self._specs_from_db()
87
+
88
+ for spec in specs:
89
+ if not self._entitled(spec.name):
90
+ continue
91
+ if spec.name in self.loaded:
92
+ continue
93
+ self._load_one(spec)
94
+
95
+ return self.loaded, self.errors
96
+
97
+ def _load_one(self, spec: PluginSpec) -> None:
98
+ try:
99
+ mod = importlib.import_module(spec.module)
100
+ except Exception as e:
101
+ self.errors.append(f"{spec.name}: import failed: {e}")
102
+ return
103
+
104
+ # Plugin contract: module exposes register(smx) -> None
105
+ reg = getattr(mod, "register", None)
106
+ if not callable(reg):
107
+ self.errors.append(f"{spec.name}: missing register(smx) function")
108
+ return
109
+
110
+ try:
111
+ reg(self._smx)
112
+ self.loaded[spec.name] = spec.module
113
+ except Exception as e:
114
+ self.errors.append(f"{spec.name}: register() failed: {e}")
@@ -0,0 +1,10 @@
1
+ """Premium support.
2
+
3
+ This package contains runtime plumbing for premium features (entitlements +
4
+ plugin loading). The actual premium implementations should live in separate,
5
+ private distributions.
6
+ """
7
+
8
+ from .gate import FeatureGate
9
+
10
+ __all__ = ["FeatureGate"]
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass
6
+ from typing import Any, Dict, Optional
7
+
8
+
9
+ def _safe_json_loads(raw: str, *, default: Any) -> Any:
10
+ try:
11
+ return json.loads(raw)
12
+ except Exception:
13
+ return default
14
+
15
+
16
+ @dataclass
17
+ class GateSources:
18
+ """Where the gate reads entitlements from.
19
+
20
+ Precedence (highest first):
21
+ 1) env_json
22
+ 2) db_setting_key
23
+ 3) licence_file
24
+ """
25
+
26
+ env_json: str = "SMX_PREMIUM_ENTITLEMENTS"
27
+ db_setting_key: str = "premium.entitlements"
28
+ licence_file_relpath: str = os.path.join("premium", "licence.json")
29
+
30
+
31
+ class FeatureGate:
32
+ """Runtime entitlement checks for premium features.
33
+
34
+ This is intentionally small, dependency-free, and safe:
35
+ - If anything fails, it returns 'not entitled' rather than crashing the app.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ *,
41
+ client_dir: str,
42
+ db: Optional[object] = None,
43
+ sources: Optional[GateSources] = None,
44
+ ):
45
+ self._client_dir = client_dir
46
+ self._db = db
47
+ self._sources = sources or GateSources()
48
+ self._cache: Optional[Dict[str, Any]] = None
49
+
50
+ def _load_from_env(self) -> Optional[Dict[str, Any]]:
51
+ raw = os.environ.get(self._sources.env_json)
52
+ if not raw:
53
+ return None
54
+ data = _safe_json_loads(raw, default=None)
55
+ return data if isinstance(data, dict) else None
56
+
57
+ def _load_from_db(self) -> Optional[Dict[str, Any]]:
58
+ if not self._db:
59
+ return None
60
+ get_setting = getattr(self._db, "get_setting", None)
61
+ if not callable(get_setting):
62
+ return None
63
+
64
+ raw = get_setting(self._sources.db_setting_key, "{}")
65
+ data = _safe_json_loads(str(raw or "{}"), default={})
66
+ return data if isinstance(data, dict) else None
67
+
68
+ def _load_from_file(self) -> Optional[Dict[str, Any]]:
69
+ p = os.path.join(self._client_dir, self._sources.licence_file_relpath)
70
+ if not os.path.exists(p):
71
+ return None
72
+ try:
73
+ with open(p, "r", encoding="utf-8") as f:
74
+ data = json.load(f)
75
+ return data if isinstance(data, dict) else None
76
+ except Exception:
77
+ return None
78
+
79
+ def entitlements(self, *, refresh: bool = False) -> Dict[str, Any]:
80
+ """Returns entitlement dict (possibly empty)."""
81
+ if self._cache is not None and not refresh:
82
+ return self._cache
83
+
84
+ ent = self._load_from_env()
85
+ if ent is None:
86
+ ent = self._load_from_db()
87
+ if ent is None:
88
+ ent = self._load_from_file()
89
+ if ent is None:
90
+ ent = {}
91
+
92
+ self._cache = ent
93
+ return ent
94
+
95
+ def enabled(self, key: str) -> bool:
96
+ """True if entitlement exists and is truthy."""
97
+ key = (key or "").strip()
98
+ if not key:
99
+ return False
100
+ ent = self.entitlements()
101
+ return bool(ent.get(key))
102
+
103
+ def get(self, key: str, default: Any = None) -> Any:
104
+ key = (key or "").strip()
105
+ if not key:
106
+ return default
107
+ return self.entitlements().get(key, default)
@@ -5715,8 +5715,8 @@ def setup_routes(smx):
5715
5715
  flash("Settings updated ✓")
5716
5716
  return redirect(url_for("admin_features"))
5717
5717
 
5718
- stream_mode = _truthy(db.get_setting("feature.stream_mode", "0"))
5719
- user_files = _truthy(db.get_setting("feature.user_files", "0"))
5718
+ stream_mode = _truthy(db.get_setting("feature.stream_mode", "1"))
5719
+ user_files = _truthy(db.get_setting("feature.user_files", "1"))
5720
5720
 
5721
5721
  return render_template(
5722
5722
  "admin_features.html",
File without changes
File without changes