syntaxmatrix 2.6.4.2__py3-none-any.whl → 2.6.4.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
syntaxmatrix/core.py CHANGED
@@ -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)
syntaxmatrix/db.py CHANGED
@@ -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)
syntaxmatrix/routes.py CHANGED
@@ -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",
@@ -7964,7 +7964,6 @@ def setup_routes(smx):
7964
7964
  nav = _generate_nav()
7965
7965
  footer = footer_html()
7966
7966
 
7967
- # now use render_template_string so we can drop the same head/nav/footer
7968
7967
  return render_template_string(f"""
7969
7968
  {head}
7970
7969
  <body>
@@ -7982,17 +7981,17 @@ def setup_routes(smx):
7982
7981
  <pre style="background:#f4f4f4;padding:1rem;
7983
7982
  border-radius:4px;text-align:left;
7984
7983
  overflow-x:auto;max-height:200px;">
7985
- {{{{ error_message }}}}
7984
+ {{ error_message }}
7986
7985
  </pre>
7987
7986
  <p>
7988
- <a href="{{{{ url_for('home') }}}}"
7987
+ <a href="{{ url_for('home') }}"
7989
7988
  style="display:inline-block;
7990
- margin-top:2rem;
7991
- padding:0.75rem 1.25rem;
7992
- background:#007acc;
7993
- color:#fff;
7994
- text-decoration:none;
7995
- border-radius:4px;">
7989
+ margin-top:2rem;
7990
+ padding:0.75rem 1.25rem;
7991
+ background:#007acc;
7992
+ color:#fff;
7993
+ text-decoration:none;
7994
+ border-radius:4px;">
7996
7995
  ← Back to Home
7997
7996
  </a>
7998
7997
  </p>
@@ -5,23 +5,23 @@ import os
5
5
  PROVIDERS_MODELS = {
6
6
  #1
7
7
  "OpenAI": [
8
- "gpt-5.2", # $1.75 $0.175 $14.00
9
- "gpt-5.2-chat-latest", # $1.75 $0.175 $14.00
10
- "gpt-5.2-pro", # $21.00 - $168.00
11
- "gpt-5.1", # $1.25 $0.125 $10.00
12
- "gpt-5.1-chat-latest", # $1.25 $0.125 $10.00
8
+ "gpt-5.2",
9
+ "gpt-5.2-chat-latest",
10
+ "gpt-5.2-pro",
11
+ "gpt-5.1",
12
+ "gpt-5.1-chat-latest",
13
13
  "gpt-5.1-codex-mini",
14
- "gpt-5.1-codex-max", # $1.25 $0.125 $10.00
15
- "gpt-5", # $1.25 $0.125 $10.00
16
- "gpt-5-nano", # $0.05 $0.005 $0.40
17
- "gpt-5-mini", # $0.25 $0.025 $2.00
18
- "gpt-5-pro", # $15.00 - $120.00
19
- "gpt-4.1", # $2.00 $0.50 $8.00
20
- "gpt-4.1-nano", # $0.10 $0.025 $0.40
21
- "gpt-4.1-mini", # $0.40 $0.10 $1.60
22
- "gpt-4o", # $2.50 $1.25 $10.00
23
- # "gpt-4o-mini", # $0.15 $0.075 $0.60
24
- # "gpt-4o-mini-search-preview", # $0.15 - $0.60
14
+ "gpt-5.1-codex-max",
15
+ "gpt-5",
16
+ "gpt-5-nano",
17
+ "gpt-5-mini",
18
+ "gpt-5-pro",
19
+ "gpt-4.1",
20
+ "gpt-4.1-nano",
21
+ "gpt-4.1-mini",
22
+ "gpt-4o",
23
+ "gpt-4o-mini",
24
+ # "gpt-4o-mini-search-preview",
25
25
  ],
26
26
  #2
27
27
  "Google": [
@@ -175,7 +175,7 @@ MODEL_DESCRIPTIONS = {
175
175
  # - Summarizer
176
176
  # """,
177
177
 
178
- #1.4 OpenAI
178
+ #1.3 OpenAI
179
179
  "gpt-4.1":"""
180
180
  Model: GPT 4.1
181
181
  Cost:
@@ -199,7 +199,7 @@ MODEL_DESCRIPTIONS = {
199
199
  - Coder
200
200
  """,
201
201
 
202
- #1.5 OpenAI
202
+ #1.4 OpenAI
203
203
  "gpt-4.1-nano":"""
204
204
  Model: GPT 4.1 Nano
205
205
  Cost:
@@ -226,7 +226,7 @@ MODEL_DESCRIPTIONS = {
226
226
  - ImageTexter
227
227
  """,
228
228
 
229
- #1.6 OpenAI
229
+ #1.5 OpenAI
230
230
  "gpt-4.1-mini":"""
231
231
  Model: GPT 4.1 Mini
232
232
  Cost:
@@ -254,7 +254,7 @@ MODEL_DESCRIPTIONS = {
254
254
  - ImageTexter
255
255
  """,
256
256
 
257
- #1.7 OpenAI
257
+ #1.6 OpenAI
258
258
  "gpt-5":"""
259
259
  Model: GPT 5
260
260
  Cost:
@@ -279,7 +279,7 @@ MODEL_DESCRIPTIONS = {
279
279
  - Coder
280
280
  """,
281
281
 
282
- #1.8 OpenAI
282
+ #1.7 OpenAI
283
283
  "gpt-5-nano":"""
284
284
  Model: GPT 5 Nano
285
285
  Cost:
@@ -307,7 +307,7 @@ MODEL_DESCRIPTIONS = {
307
307
  - ImageTexter
308
308
  """,
309
309
 
310
- #1.9 OpenAI
310
+ #1.8 OpenAI
311
311
  "gpt-5-mini":"""
312
312
  Model: GPT 5 Mini
313
313
  Cost:
@@ -342,7 +342,7 @@ MODEL_DESCRIPTIONS = {
342
342
 
343
343
  #1.8 OpenAI
344
344
 
345
- #1.10 OpenAI
345
+ #1.9 OpenAI
346
346
  "gpt-5-pro":"""
347
347
  Model: GPT 5 Pro
348
348
  Cost:
@@ -367,7 +367,7 @@ MODEL_DESCRIPTIONS = {
367
367
  - Coder
368
368
  """,
369
369
 
370
- #1.11 OpenAI
370
+ #1.10 OpenAI
371
371
  "gpt-5.1":"""
372
372
  Model: GPT 5.1
373
373
  Cost:
@@ -393,7 +393,7 @@ MODEL_DESCRIPTIONS = {
393
393
  - Coder
394
394
  """,
395
395
 
396
- #1.12 OpenAI
396
+ #1.11 OpenAI
397
397
  "gpt-5.1-chat-latest":"""
398
398
  Model: GPT 5.1 Chat
399
399
  Cost:
@@ -419,7 +419,7 @@ MODEL_DESCRIPTIONS = {
419
419
  - Chat
420
420
  """,
421
421
 
422
- #1.13 OpenAI
422
+ #1.12 OpenAI
423
423
  "gpt-5.1-codex-mini":"""
424
424
  Model: GPT 5.1 Codex Mini
425
425
  Cost:
@@ -444,7 +444,7 @@ MODEL_DESCRIPTIONS = {
444
444
  - Coder
445
445
  """,
446
446
 
447
- #1.14 OpenAI
447
+ #1.13 OpenAI
448
448
  "gpt-5.1-codex-max":"""
449
449
  Model: GPT 5.1 Codex Max
450
450
  Cost:
@@ -469,7 +469,7 @@ MODEL_DESCRIPTIONS = {
469
469
  - Coder
470
470
  """,
471
471
 
472
- #1.15 OpenAI
472
+ #1.14 OpenAI
473
473
  "gpt-5.2":"""
474
474
  Model: GPT 5.2
475
475
  Cost:
@@ -494,7 +494,7 @@ MODEL_DESCRIPTIONS = {
494
494
  - Coder
495
495
  """,
496
496
 
497
- #1.16 OpenAI
497
+ #1.15 OpenAI
498
498
  "gpt-5.2-chat-latest":"""
499
499
  Model: GPT 5.2 Chat
500
500
  Cost:
@@ -519,7 +519,7 @@ MODEL_DESCRIPTIONS = {
519
519
  - Admin
520
520
  """,
521
521
 
522
- #1.17 OpenAI
522
+ #1.16 OpenAI
523
523
  "gpt-5.2-pro":"""
524
524
  Model: GPT 5.2 Pro
525
525
  Cost:
@@ -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
@@ -2,9 +2,10 @@ syntaxmatrix/__init__.py,sha256=_LnTrYAW2tbYA37Y233Vv4OMOk8NUnoJi-1yzFyHxEI,2573
2
2
  syntaxmatrix/auth.py,sha256=SCD6uWojXjj9yjUTKzgV5kBYe6ZkXASEG2VopLFkEtM,18140
3
3
  syntaxmatrix/bootstrap.py,sha256=Y7ZNg-Z3ecrr1iYem5EMzPmGstXnEKmO9kqKVoOoljo,817
4
4
  syntaxmatrix/commentary.py,sha256=3c8qBAKJI2IcYd9PBZrFEwmv-c4_tfa3ebEoPa5vW7U,12428
5
- syntaxmatrix/core.py,sha256=q1TbQx0gf_jBGDkQKdFGPEByuER0PEZnFqwaWSbOmco,66592
5
+ syntaxmatrix/core.py,sha256=IKcvPJMbTMwv0eqkeLx4se3AU_Qx7LNNbS5sz-VWVf4,67485
6
6
  syntaxmatrix/dataset_preprocessing.py,sha256=wtV4MWzkyfOsBHTsS0H1gqHho77ZQHGDI9skJryyZWA,8732
7
- syntaxmatrix/db.py,sha256=MLpNs-Ue3xjEwaOJOMgFuPEz_gP9Fr3b1__BA3A3RDg,20801
7
+ syntaxmatrix/db.py,sha256=Q3b_Dr8LifWpGQyBsA-MLc8pBQjFJbc0ox0t_sbdECE,23722
8
+ syntaxmatrix/db_contract.py,sha256=N7WvdTgRH87XX7K5cPSDXvNfHNsSZ4xxZO5DL5vrpVA,2141
8
9
  syntaxmatrix/display_html.py,sha256=tBeeHcRbmAOKqRTXY0hUehThFspCDsvjW4myi2zj0iU,3568
9
10
  syntaxmatrix/emailer.py,sha256=KazaSY0ieE5kC5nTVmh-O2N3gjfeG_oBnl4pl_UHEws,949
10
11
  syntaxmatrix/file_processor.py,sha256=9-TT20qfhZ7Q0eWCJpdxDA54jWM9g56A6VJNFme2azY,2863
@@ -18,10 +19,11 @@ syntaxmatrix/page_builder_generation.py,sha256=EuRVsHLxYDo0SQUo9apMY0ML0-OnaZ9kD
18
19
  syntaxmatrix/page_layout_contract.py,sha256=4i18ireN2wigWD3mGIA5A6RMw-0DCxnEXpKuu3UrHog,25175
19
20
  syntaxmatrix/page_patch_publish.py,sha256=pakIo8tA2y_2zulrU7aBqbqXkmgJLc2-YwghDvYKbgs,50473
20
21
  syntaxmatrix/plottings.py,sha256=MjHQ9T1_oC5oyr4_wkM2GJDrpjp0sbvudbs2lGaMyzk,6103
22
+ syntaxmatrix/plugin_manager.py,sha256=sRpm6xGU8AQSDU_bnh1aHHARwl4uqNYgYXgKlFGHYm4,3747
21
23
  syntaxmatrix/preface.py,sha256=tCm0C0BhY_SOntQT5I7cOJr6TB5mVDAeL9i8UmHLu5g,21237
22
24
  syntaxmatrix/profiles.py,sha256=hPg27IQjl8-Tpo3BanQQsByeAgcizqIA2I_IKKNZ0TI,2900
23
25
  syntaxmatrix/project_root.py,sha256=1ckvbFVV1szHtHsfSCoGcImHkRwbfszmPG1kGh9ZZlE,2227
24
- syntaxmatrix/routes.py,sha256=EwjA9Q3r1dHXkuzF_93DdOY2q3AikQmpxCLC3nATVTc,357004
26
+ syntaxmatrix/routes.py,sha256=CMBWRB7b1ju1odxWT6K7b8njkjz__f8XlX0biKSozKA,356892
25
27
  syntaxmatrix/selftest_page_templates.py,sha256=JY1i2xu7FBkN0TIPiAXhEk_iIjdOBmfc1g9aX98iqhw,14833
26
28
  syntaxmatrix/session.py,sha256=v0qgxnVM_LEaNvZQJSa-13Q2eiwc3RDnjd2SahNnHQk,599
27
29
  syntaxmatrix/smiv.py,sha256=1lSN3UYpXvYoVNd6VrkY5iZuF_nDxD6xxvLnTn9wcbQ,1405
@@ -38,13 +40,18 @@ syntaxmatrix/agentic/agents.py,sha256=IVqF2VLl-80nx75FW0-RK2-2nfhbQjVwyGgplgVOea
38
40
  syntaxmatrix/agentic/agents_orchestrer.py,sha256=NMC0Mr1zRxxWBr-KRZxu1iLMBJowqesNIkuNwY1AlQA,14681
39
41
  syntaxmatrix/agentic/code_tools_registry.py,sha256=rV0sA1qf_a9A4mmJXGuLnPD6qzAtTBjVgViYpwykfRU,1489
40
42
  syntaxmatrix/agentic/model_templates.py,sha256=A3ROE3BHkvnU9cxqSGjlCBIw9U15zRaTKgK-WxcZtUI,76033
43
+ syntaxmatrix/db_backends/__init__.py,sha256=ocEqHP5enZCCpurdITAucrrWbw2peihBC6AM3Ck9AAI,23
44
+ syntaxmatrix/db_backends/postgres_backend.py,sha256=vveUAz8Ss2KSFQRpKVxjBPpgqabw-Z5VVKHHdKxsuSU,519
45
+ syntaxmatrix/db_backends/sqlite_backend.py,sha256=DJ70rB7NCRpcYZ6heGnrunt9lxxFgAaidLcio-8y8U0,8384
41
46
  syntaxmatrix/media/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
47
  syntaxmatrix/media/media_pixabay.py,sha256=_gkntpbK7HzyLOtzoyWLjXXWtCXUFknD4hFHn_qHfRY,8195
48
+ syntaxmatrix/premium/__init__.py,sha256=ab4z0PWcMSbnIIqyRBHWTqoE1NyhhW5kzIHNcdhZ4J4,269
49
+ syntaxmatrix/premium/gate.py,sha256=-ko1x4iNZZxe6DYTZdW9Cni38NAbfdZoGJ5Co_813l0,3273
43
50
  syntaxmatrix/settings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
51
  syntaxmatrix/settings/client_items.py,sha256=jtJ2k5r_Roq42Pumor_VEsYVTIdbFVP2l61MOeMAPUc,868
45
52
  syntaxmatrix/settings/default.yaml,sha256=BznvF1D06VMPbT6UX3MQ4zUkXxTXLnAA53aUu8G4O38,569
46
53
  syntaxmatrix/settings/logging.py,sha256=U8iTDFv0H1ECdIzH9He2CtOVlK1x5KHCk126Zn5Vi7M,1362
47
- syntaxmatrix/settings/model_map.py,sha256=Sh84M4ZeKCSYNwgTS9u00-bWjD_8tMW_WVBEEyV8yLY,26712
54
+ syntaxmatrix/settings/model_map.py,sha256=P3RsBGkq36ozADjZOY_p8bjvLgaleiZynMgl4HP1qR8,26385
48
55
  syntaxmatrix/settings/prompts.py,sha256=Gni--SPxFfqbQXPJqkK0tdViBtAMtUBp185i5WdYux4,25888
49
56
  syntaxmatrix/settings/string_navbar.py,sha256=NqgTzo3J9rRI4c278VG6kpoViFfmi2FKmL6sO0R-bus,83
50
57
  syntaxmatrix/static/docs.md,sha256=rWlKjNcpS2cs5DElGNYuaA-XXdGZnRGMXx62nACvDwE,11105
@@ -78,8 +85,8 @@ syntaxmatrix/vectordb/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
78
85
  syntaxmatrix/vectordb/adapters/milvus_adapter.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
86
  syntaxmatrix/vectordb/adapters/pgvector_adapter.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
80
87
  syntaxmatrix/vectordb/adapters/sqlite_adapter.py,sha256=L8M2qHfwZRAFVxWeurUVdHaJXz6F5xTUSWh3uy6TSUs,6035
81
- syntaxmatrix-2.6.4.2.dist-info/licenses/LICENSE.txt,sha256=j1P8naTdy1JMxTC80XYQjbyAQnuOlpDusCUhncrvpy8,1083
82
- syntaxmatrix-2.6.4.2.dist-info/METADATA,sha256=Lfr4JXJYE44Ezkm909Yfp6RYLT7GziLSNk70_qSBcg8,18221
83
- syntaxmatrix-2.6.4.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
84
- syntaxmatrix-2.6.4.2.dist-info/top_level.txt,sha256=HKP_zkl4V_nt7osC15DlacoBZktHrbZYOqf_pPkF3T8,13
85
- syntaxmatrix-2.6.4.2.dist-info/RECORD,,
88
+ syntaxmatrix-2.6.4.4.dist-info/licenses/LICENSE.txt,sha256=j1P8naTdy1JMxTC80XYQjbyAQnuOlpDusCUhncrvpy8,1083
89
+ syntaxmatrix-2.6.4.4.dist-info/METADATA,sha256=UZK3_MnFpYk0wb3AUfEJx100W6v4ra2oHM_9Wt54DyE,18201
90
+ syntaxmatrix-2.6.4.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
91
+ syntaxmatrix-2.6.4.4.dist-info/top_level.txt,sha256=HKP_zkl4V_nt7osC15DlacoBZktHrbZYOqf_pPkF3T8,13
92
+ syntaxmatrix-2.6.4.4.dist-info/RECORD,,