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.
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/PKG-INFO +3 -2
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/SyntaxMatrix.egg-info/PKG-INFO +3 -2
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/SyntaxMatrix.egg-info/SOURCES.txt +7 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/setup.py +7 -18
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/core.py +32 -5
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/db.py +77 -0
- syntaxmatrix-2.6.4.4/syntaxmatrix/db_backends/__init__.py +1 -0
- syntaxmatrix-2.6.4.4/syntaxmatrix/db_backends/postgres_backend.py +14 -0
- syntaxmatrix-2.6.4.4/syntaxmatrix/db_backends/sqlite_backend.py +258 -0
- syntaxmatrix-2.6.4.4/syntaxmatrix/db_contract.py +71 -0
- syntaxmatrix-2.6.4.4/syntaxmatrix/plugin_manager.py +114 -0
- syntaxmatrix-2.6.4.4/syntaxmatrix/premium/__init__.py +10 -0
- syntaxmatrix-2.6.4.4/syntaxmatrix/premium/gate.py +107 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/routes.py +10 -11
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/model_map.py +30 -30
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/LICENSE.txt +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/README.md +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/SyntaxMatrix.egg-info/dependency_links.txt +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/SyntaxMatrix.egg-info/requires.txt +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/SyntaxMatrix.egg-info/top_level.txt +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/pyproject.toml +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/setup.cfg +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/__init__.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/__init__.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/agent_tools.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/agents.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/agents_orchestrer.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/code_tools_registry.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/agentic/model_templates.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/auth.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/bootstrap.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/commentary.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/dataset_preprocessing.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/display_html.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/emailer.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/file_processor.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/gpt_models_latest.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/history_store.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/kernel_manager.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/llm_store.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/media/__init__.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/media/media_pixabay.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/models.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/page_builder_defaults.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/page_builder_generation.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/page_layout_contract.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/page_patch_publish.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/plottings.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/preface.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/profiles.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/project_root.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/selftest_page_templates.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/session.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/__init__.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/client_items.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/default.yaml +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/logging.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/prompts.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/settings/string_navbar.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/smiv.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/smpv.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/assets/hero-default.svg +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/css/style.css +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/docs.md +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/bot_icon.png +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/favicon.png +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/logo.png +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/logo3.png +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/svg_497526.svg +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/icons/svg_497528.svg +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/js/chat.js +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/js/sidebar.js +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/static/js/widgets.js +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/admin_branding.html +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/admin_features.html +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/admin_secretes.html +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/change_password.html +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/code_cell.html +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/dashboard.html +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/dataset_resize.html +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/docs.html +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/edit_page.html +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/error.html +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/login.html +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/templates/register.html +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/themes.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/ui_modes.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/utils.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vector_db.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/__init__.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/adapters/__init__.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/adapters/milvus_adapter.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/adapters/pgvector_adapter.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/adapters/sqlite_adapter.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/base.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectordb/registry.py +0 -0
- {syntaxmatrix-2.6.4.2 → syntaxmatrix-2.6.4.4}/syntaxmatrix/vectorizer.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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", "
|
|
349
|
-
user_files_v = db.get_setting("feature.user_files", "
|
|
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 """
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
)
|