syntaxmatrix 2.6.4.2__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.2 → syntaxmatrix-2.6.4.4}/PKG-INFO +3 -2
  2. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/SyntaxMatrix.egg-info/PKG-INFO +3 -2
  3. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/SyntaxMatrix.egg-info/SOURCES.txt +7 -0
  4. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/setup.py +7 -18
  5. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/core.py +32 -5
  6. {syntaxmatrix-2.6.4.2 → 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.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/routes.py +10 -11
  15. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/model_map.py +30 -30
  16. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/LICENSE.txt +0 -0
  17. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/README.md +0 -0
  18. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/SyntaxMatrix.egg-info/dependency_links.txt +0 -0
  19. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/SyntaxMatrix.egg-info/requires.txt +0 -0
  20. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/SyntaxMatrix.egg-info/top_level.txt +0 -0
  21. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/pyproject.toml +0 -0
  22. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/setup.cfg +0 -0
  23. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/__init__.py +0 -0
  24. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/__init__.py +0 -0
  25. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/agent_tools.py +0 -0
  26. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/agents.py +0 -0
  27. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/agents_orchestrer.py +0 -0
  28. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/code_tools_registry.py +0 -0
  29. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/model_templates.py +0 -0
  30. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/auth.py +0 -0
  31. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/bootstrap.py +0 -0
  32. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/commentary.py +0 -0
  33. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/dataset_preprocessing.py +0 -0
  34. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/display_html.py +0 -0
  35. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/emailer.py +0 -0
  36. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/file_processor.py +0 -0
  37. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/gpt_models_latest.py +0 -0
  38. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/history_store.py +0 -0
  39. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/kernel_manager.py +0 -0
  40. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/llm_store.py +0 -0
  41. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/media/__init__.py +0 -0
  42. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/media/media_pixabay.py +0 -0
  43. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/models.py +0 -0
  44. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/page_builder_defaults.py +0 -0
  45. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/page_builder_generation.py +0 -0
  46. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/page_layout_contract.py +0 -0
  47. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/page_patch_publish.py +0 -0
  48. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/plottings.py +0 -0
  49. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/preface.py +0 -0
  50. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/profiles.py +0 -0
  51. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/project_root.py +0 -0
  52. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/selftest_page_templates.py +0 -0
  53. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/session.py +0 -0
  54. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/__init__.py +0 -0
  55. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/client_items.py +0 -0
  56. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/default.yaml +0 -0
  57. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/logging.py +0 -0
  58. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/prompts.py +0 -0
  59. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/string_navbar.py +0 -0
  60. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/smiv.py +0 -0
  61. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/smpv.py +0 -0
  62. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/assets/hero-default.svg +0 -0
  63. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/css/style.css +0 -0
  64. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/docs.md +0 -0
  65. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/bot_icon.png +0 -0
  66. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/favicon.png +0 -0
  67. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/logo.png +0 -0
  68. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/logo3.png +0 -0
  69. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/svg_497526.svg +0 -0
  70. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/svg_497528.svg +0 -0
  71. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/js/chat.js +0 -0
  72. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/js/sidebar.js +0 -0
  73. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/js/widgets.js +0 -0
  74. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/admin_branding.html +0 -0
  75. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/admin_features.html +0 -0
  76. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/admin_secretes.html +0 -0
  77. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/change_password.html +0 -0
  78. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/code_cell.html +0 -0
  79. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/dashboard.html +0 -0
  80. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/dataset_resize.html +0 -0
  81. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/docs.html +0 -0
  82. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/edit_page.html +0 -0
  83. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/error.html +0 -0
  84. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/login.html +0 -0
  85. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/register.html +0 -0
  86. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/themes.py +0 -0
  87. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/ui_modes.py +0 -0
  88. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/utils.py +0 -0
  89. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vector_db.py +0 -0
  90. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/__init__.py +0 -0
  91. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/adapters/__init__.py +0 -0
  92. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/adapters/milvus_adapter.py +0 -0
  93. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/adapters/pgvector_adapter.py +0 -0
  94. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/adapters/sqlite_adapter.py +0 -0
  95. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/base.py +0 -0
  96. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/registry.py +0 -0
  97. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectorizer.py +0 -0
  98. {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/workspace_db.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syntaxmatrix
3
- Version: 2.6.4.2
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
7
+ License: MIT
7
8
  Classifier: Programming Language :: Python :: 3.9
8
- Classifier: License :: OSI Approved :: MIT License
9
9
  Classifier: Operating System :: OS Independent
10
10
  Requires-Python: >=3.9
11
11
  Description-Content-Type: text/markdown
@@ -48,6 +48,7 @@ Dynamic: author-email
48
48
  Dynamic: classifier
49
49
  Dynamic: description
50
50
  Dynamic: description-content-type
51
+ Dynamic: license
51
52
  Dynamic: license-file
52
53
  Dynamic: requires-dist
53
54
  Dynamic: requires-python
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syntaxmatrix
3
- Version: 2.6.4.2
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
7
+ License: MIT
7
8
  Classifier: Programming Language :: Python :: 3.9
8
- Classifier: License :: OSI Approved :: MIT License
9
9
  Classifier: Operating System :: OS Independent
10
10
  Requires-Python: >=3.9
11
11
  Description-Content-Type: text/markdown
@@ -48,6 +48,7 @@ Dynamic: author-email
48
48
  Dynamic: classifier
49
49
  Dynamic: description
50
50
  Dynamic: description-content-type
51
+ Dynamic: license
51
52
  Dynamic: license-file
52
53
  Dynamic: requires-dist
53
54
  Dynamic: requires-python
@@ -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,12 @@ 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.2",
11
+ version="2.6.4.4",
12
+ license="MIT",
13
+ classifiers=[
14
+ "Programming Language :: Python :: 3.9",
15
+ "Operating System :: OS Independent",
16
+ ],
12
17
  author="Bob Nti",
13
18
  author_email="bob.nti@syntaxmatrix.net",
14
19
  description="SyntaxMUI: A customizable framework for Python AI Assistant Projects.",
@@ -16,6 +21,7 @@ setup(
16
21
  long_description_content_type="text/markdown",
17
22
  packages=find_packages(),
18
23
  include_package_data=True,
24
+ python_requires='>=3.9',
19
25
  package_data={
20
26
  "syntaxmatrix": [
21
27
  "static/**/*",
@@ -61,21 +67,4 @@ setup(
61
67
  "shap>=0.42.0",
62
68
 
63
69
  ],
64
- # extras_require={
65
- # "mlearning": [
66
- # "pandas>=2.2.3",
67
- # "numpy>=2.0.2",
68
- # "matplotlib>=3.9.4",
69
- # ],
70
- # "auth": [
71
- # "sqlalchemy>=2.0.42",
72
- # "cryptography>=45.0.6",
73
- # ],
74
- # },
75
- classifiers=[
76
- "Programming Language :: Python :: 3.9",
77
- "License :: OSI Approved :: MIT License",
78
- "Operating System :: OS Independent",
79
- ],
80
- python_requires='>=3.9',
81
70
  )
@@ -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)
@@ -887,7 +903,12 @@ class SyntaxMUI:
887
903
  if not self.chat_profile:
888
904
  chat_profile = _prof.get_profile("chat") or _prof.get_profile("admin")
889
905
  if not chat_profile:
890
- yield """<p style='color:red;'>Error: Chat profile is not configured. Add a chat profile inside the admin panel or contact your administrator.</p>
906
+ yield """
907
+ <p style='color:red;'>
908
+ Error!<br>
909
+ Chat profile is not configured. Add a chat profile inside the admin panel.
910
+ To do that, you must login first or contact your administrator.
911
+ </p>
891
912
  """
892
913
  return None
893
914
  self.chat_profile = chat_profile
@@ -967,14 +988,20 @@ class SyntaxMUI:
967
988
  except Exception as e:
968
989
  yield f"Error during streaming: {type(e).__name__}: {e}"
969
990
 
991
+
970
992
  def process_query(self, query, context, history, stream=False):
971
993
 
972
994
  if not self.chat_profile:
973
995
  chat_profile = _prof.get_profile("chat") or _prof.get_profile("admin")
974
996
  if not chat_profile:
975
- return """<p style='color:red;'>Error: Chat profile is not configured. Add a chat profile inside the admin panel or contact your administrator.</p>
997
+ yield """
998
+ <p style='color:red;'>
999
+ Error!<br>
1000
+ Chat profile is not configured. Add a chat profile inside the admin panel.
1001
+ To do that, you must login first or contact your administrator.
1002
+ </p>
976
1003
  """
977
- return
1004
+ return None
978
1005
 
979
1006
  self.chat_profile = chat_profile
980
1007
  self.chat_profile['client'] = _prof.get_client(chat_profile)
@@ -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
+ )