syntaxmatrix 2.6.4.3__py3-none-any.whl → 3.0.0__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.
Files changed (45) hide show
  1. syntaxmatrix/__init__.py +6 -4
  2. syntaxmatrix/agentic/agents.py +195 -15
  3. syntaxmatrix/agentic/agents_orchestrer.py +16 -10
  4. syntaxmatrix/client_docs.py +237 -0
  5. syntaxmatrix/commentary.py +96 -25
  6. syntaxmatrix/core.py +156 -54
  7. syntaxmatrix/dataset_preprocessing.py +2 -2
  8. syntaxmatrix/db.py +60 -0
  9. syntaxmatrix/db_backends/__init__.py +1 -0
  10. syntaxmatrix/db_backends/postgres_backend.py +14 -0
  11. syntaxmatrix/db_backends/sqlite_backend.py +258 -0
  12. syntaxmatrix/db_contract.py +71 -0
  13. syntaxmatrix/kernel_manager.py +174 -150
  14. syntaxmatrix/page_builder_generation.py +654 -50
  15. syntaxmatrix/page_layout_contract.py +25 -3
  16. syntaxmatrix/page_patch_publish.py +368 -15
  17. syntaxmatrix/plugins/__init__.py +0 -0
  18. syntaxmatrix/plugins/plugin_manager.py +114 -0
  19. syntaxmatrix/premium/__init__.py +18 -0
  20. syntaxmatrix/premium/catalogue/__init__.py +121 -0
  21. syntaxmatrix/premium/gate.py +119 -0
  22. syntaxmatrix/premium/state.py +507 -0
  23. syntaxmatrix/premium/verify.py +222 -0
  24. syntaxmatrix/profiles.py +1 -1
  25. syntaxmatrix/routes.py +9782 -8004
  26. syntaxmatrix/settings/model_map.py +50 -65
  27. syntaxmatrix/settings/prompts.py +1435 -380
  28. syntaxmatrix/settings/string_navbar.py +4 -4
  29. syntaxmatrix/static/icons/bot_icon.png +0 -0
  30. syntaxmatrix/static/icons/bot_icon2.png +0 -0
  31. syntaxmatrix/templates/admin_billing.html +408 -0
  32. syntaxmatrix/templates/admin_branding.html +65 -2
  33. syntaxmatrix/templates/admin_features.html +54 -0
  34. syntaxmatrix/templates/dashboard.html +285 -8
  35. syntaxmatrix/templates/edit_page.html +199 -18
  36. syntaxmatrix/themes.py +17 -17
  37. syntaxmatrix/workspace_db.py +0 -23
  38. syntaxmatrix-3.0.0.dist-info/METADATA +219 -0
  39. {syntaxmatrix-2.6.4.3.dist-info → syntaxmatrix-3.0.0.dist-info}/RECORD +42 -30
  40. {syntaxmatrix-2.6.4.3.dist-info → syntaxmatrix-3.0.0.dist-info}/WHEEL +1 -1
  41. syntaxmatrix/settings/default.yaml +0 -13
  42. syntaxmatrix-2.6.4.3.dist-info/METADATA +0 -539
  43. syntaxmatrix-2.6.4.3.dist-info/licenses/LICENSE.txt +0 -21
  44. /syntaxmatrix/static/icons/{logo3.png → logo2.png} +0 -0
  45. {syntaxmatrix-2.6.4.3.dist-info → syntaxmatrix-3.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,507 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timedelta
7
+ from typing import Any, Dict, Optional
8
+
9
+ from .verify import instance_fingerprint, verify_licence_payload
10
+ from .catalogue import resolve_entitlements
11
+ from syntaxmatrix.project_root import detect_project_root
12
+
13
+
14
+ def _utcnow() -> datetime:
15
+ return datetime.utcnow()
16
+
17
+
18
+ def _iso_utc(dt: datetime) -> str:
19
+ return dt.replace(microsecond=0).isoformat() + "Z"
20
+
21
+
22
+ def _parse_iso_utc(s: str) -> Optional[datetime]:
23
+ if not s:
24
+ return None
25
+ s = str(s).strip()
26
+ if not s:
27
+ return None
28
+ s2 = s.replace(" ", "T")
29
+ if s2.endswith("Z"):
30
+ s2 = s2[:-1]
31
+ try:
32
+ return datetime.fromisoformat(s2)
33
+ except Exception:
34
+ return None
35
+
36
+
37
+ def _safe_json_loads(raw: str, default: Any) -> Any:
38
+ try:
39
+ return json.loads(raw)
40
+ except Exception:
41
+ return default
42
+
43
+
44
+ def _truthy(v: Any) -> bool:
45
+ return str(v).strip().lower() in ("1", "true", "yes", "y", "on")
46
+
47
+
48
+ @dataclass
49
+ class PremiumState:
50
+ trial_days: int
51
+ trial_started_at: Optional[str]
52
+ trial_active: bool
53
+ trial_days_left: int
54
+ plan: str
55
+ entitlements: Dict[str, Any]
56
+ source: str # trial | env | file | db | none
57
+ instance_id: Optional[str] = None
58
+ remote_status: Optional[str] = None
59
+ remote_grace_until: Optional[str] = None
60
+
61
+
62
+ def _fallback_entitlements(plan: str) -> Dict[str, Any]:
63
+ """Hard-coded fallbacks (only used if catalogue files are missing).
64
+
65
+ Normal path: entitlements are catalogue-driven so you can add/update values
66
+ without touching Python.
67
+ """
68
+ p = (plan or "").strip().lower()
69
+ if p == "pro":
70
+ return {
71
+ "plan": "pro",
72
+ "docs": True,
73
+ "ml_lab": True,
74
+ "registration": True,
75
+ "theme_toggle": True,
76
+ "branding_controls": True,
77
+ "plugins": True,
78
+ "premium_db_backends": False,
79
+ "audit_export": True,
80
+ "max_users": 10,
81
+ "max_pages": 50,
82
+ "max_upload_mb": 200,
83
+ "max_pdf_pages_per_doc": 300,
84
+ "max_vector_records": 50000,
85
+ }
86
+ if p == "business":
87
+ return {
88
+ "plan": "business",
89
+ "docs": True,
90
+ "ml_lab": True,
91
+ "registration": True,
92
+ "theme_toggle": True,
93
+ "branding_controls": True,
94
+ "plugins": True,
95
+ "premium_db_backends": True,
96
+ "audit_export": True,
97
+ "max_users": 25,
98
+ "max_pages": 150,
99
+ "max_upload_mb": 750,
100
+ "max_pdf_pages_per_doc": 750,
101
+ "max_vector_records": 200000,
102
+ }
103
+ if p == "enterprise":
104
+ return {
105
+ "plan": "enterprise",
106
+ "docs": True,
107
+ "ml_lab": True,
108
+ "registration": True,
109
+ "theme_toggle": True,
110
+ "branding_controls": True,
111
+ "plugins": True,
112
+ "premium_db_backends": True,
113
+ "audit_export": True,
114
+ "max_users": 200,
115
+ "max_pages": 500,
116
+ "max_upload_mb": 2000,
117
+ "max_pdf_pages_per_doc": 2000,
118
+ "max_vector_records": 500000,
119
+ }
120
+ if p == "trial":
121
+ return {
122
+ "plan": "trial",
123
+ "docs": True,
124
+ "ml_lab": True,
125
+ "registration": True,
126
+ "theme_toggle": True,
127
+ "branding_controls": True,
128
+ "plugins": True,
129
+ "premium_db_backends": True,
130
+ "audit_export": True,
131
+ "max_users": 9999,
132
+ "max_pages": 9999,
133
+ "max_upload_mb": 9999,
134
+ "max_pdf_pages_per_doc": 999999,
135
+ "max_vector_records": 999999999,
136
+ }
137
+ return {
138
+ "plan": "free",
139
+ "docs": False,
140
+ "ml_lab": False,
141
+ "registration": False,
142
+ "theme_toggle": False,
143
+ "branding_controls": False,
144
+ "plugins": False,
145
+ "premium_db_backends": False,
146
+ "audit_export": False,
147
+ "max_users": 1,
148
+ "max_pages": 3,
149
+ "max_upload_mb": 5,
150
+ "max_pdf_pages_per_doc": 20,
151
+ "max_vector_records": 1500,
152
+ }
153
+
154
+
155
+ def _catalogue_entitlements(plan: str, *, version: Optional[str] = None, addons: Optional[list] = None) -> Dict[str, Any]:
156
+ """Resolve entitlements from the catalogue (preferred)."""
157
+ try:
158
+ ent = resolve_entitlements(plan_id=plan, version=version, addons=addons)
159
+ # If catalogue is missing or empty, fall back.
160
+ if not isinstance(ent, dict) or len(ent.keys()) <= 2:
161
+ return _fallback_entitlements(plan)
162
+ return ent
163
+ except Exception:
164
+ return _fallback_entitlements(plan)
165
+
166
+
167
+ def free_entitlements() -> Dict[str, Any]:
168
+ return _catalogue_entitlements("free")
169
+
170
+
171
+ def pro_entitlements() -> Dict[str, Any]:
172
+ return _catalogue_entitlements("pro")
173
+
174
+
175
+ def enterprise_entitlements() -> Dict[str, Any]:
176
+ return _catalogue_entitlements("enterprise")
177
+
178
+
179
+ def trial_entitlements() -> Dict[str, Any]:
180
+ return _catalogue_entitlements("trial")
181
+
182
+
183
+ def _choose_preset_for_plan(plan: str) -> Dict[str, Any]:
184
+ p = (plan or "").strip().lower()
185
+ if p == "pro":
186
+ return pro_entitlements()
187
+ if p == "enterprise":
188
+ return enterprise_entitlements()
189
+ if p == "trial":
190
+ return trial_entitlements()
191
+ return free_entitlements()
192
+
193
+
194
+ def _plan_from_entitlements(ent: Dict[str, Any]) -> str:
195
+ plan = str(ent.get("plan") or "").strip().lower()
196
+ return plan or "free"
197
+
198
+
199
+ def _apply_entitlements_to_feature_flags(db: object, ent: Dict[str, Any]) -> None:
200
+ set_setting = getattr(db, "set_setting", None)
201
+ if not callable(set_setting):
202
+ return
203
+
204
+ def _as01(b: Any) -> str:
205
+ return "1" if bool(b) else "0"
206
+
207
+ set_setting("feature.site_documentation", _as01(ent.get("docs")))
208
+ set_setting("feature.ml_lab", _as01(ent.get("ml_lab")))
209
+ set_setting("feature.registration", _as01(ent.get("registration")))
210
+ set_setting("feature.theme_toggle", _as01(ent.get("theme_toggle")))
211
+
212
+
213
+ def _read_payload_from_env(env_key: str = "SMX_PREMIUM_ENTITLEMENTS") -> Optional[Dict[str, Any]]:
214
+ raw = os.environ.get(env_key)
215
+ if not raw:
216
+ return None
217
+ data = _safe_json_loads(raw, default=None)
218
+ return data if isinstance(data, dict) else None
219
+
220
+
221
+ def _read_payload_from_file(client_dir: str, relpath: str = os.path.join("premium", "licence.json")) -> Optional[Dict[str, Any]]:
222
+ p = os.path.join(client_dir, relpath)
223
+ if not os.path.exists(p):
224
+ return None
225
+ try:
226
+ with open(p, "r", encoding="utf-8") as f:
227
+ data = json.load(f)
228
+ return data if isinstance(data, dict) else None
229
+ except Exception:
230
+ return None
231
+
232
+
233
+ def ensure_premium_state(
234
+ *,
235
+ db: object,
236
+ client_dir: str,
237
+ trial_days: int = 7,
238
+ now: Optional[datetime] = None,
239
+ refresh_entitlements: bool = True,
240
+ ) -> PremiumState:
241
+ # Prefer the detected project root (your current design),
242
+ # but fall back to provided client_dir if detection fails.
243
+ try:
244
+ detected = str(detect_project_root())
245
+ if detected:
246
+ client_dir = detected
247
+ except Exception:
248
+ client_dir = str(client_dir or "")
249
+
250
+ get_setting = getattr(db, "get_setting", None)
251
+ set_setting = getattr(db, "set_setting", None)
252
+ if not callable(get_setting) or not callable(set_setting):
253
+ ent = free_entitlements()
254
+ return PremiumState(
255
+ trial_days=int(trial_days or 7),
256
+ trial_started_at=None,
257
+ trial_active=False,
258
+ trial_days_left=0,
259
+ plan=_plan_from_entitlements(ent),
260
+ entitlements=ent,
261
+ source="none",
262
+ instance_id=None,
263
+ remote_status=None,
264
+ remote_grace_until=None,
265
+ )
266
+
267
+ try:
268
+ set_setting("premium.client_dir", client_dir)
269
+ except Exception:
270
+ pass
271
+
272
+ licence_server = (os.environ.get("SMX_LICENCE_SERVER_URL") or "").strip()
273
+ if not licence_server:
274
+ licence_server = "https://licence.syntaxmatrix.com"
275
+
276
+ # How often to sync remote licence state (minutes).
277
+ try:
278
+ sync_minutes = int((os.environ.get("SMX_LICENCE_SYNC_MINUTES") or "10").strip() or "10")
279
+ except Exception:
280
+ sync_minutes = 10
281
+ if sync_minutes < 1:
282
+ sync_minutes = 1
283
+
284
+ now_dt = now or _utcnow()
285
+
286
+ def _now_iso() -> str:
287
+ return now_dt.replace(microsecond=0).isoformat() + "Z"
288
+
289
+ def _sync_due(last_iso: str) -> bool:
290
+ last = _parse_iso_utc(last_iso)
291
+ if not last:
292
+ return True
293
+ return (now_dt - last).total_seconds() >= sync_minutes * 60
294
+
295
+ def _post(url: str, payload: dict) -> dict:
296
+ import urllib.request
297
+
298
+ body = json.dumps(payload).encode("utf-8")
299
+ req = urllib.request.Request(
300
+ url,
301
+ data=body,
302
+ headers={"Content-Type": "application/json"},
303
+ method="POST",
304
+ )
305
+ try:
306
+ with urllib.request.urlopen(req, timeout=15) as resp:
307
+ raw = resp.read().decode("utf-8", errors="replace")
308
+ return json.loads(raw) if raw else {}
309
+ except Exception:
310
+ return {}
311
+
312
+ remote_status: Optional[str] = None
313
+ remote_grace_until: Optional[str] = None
314
+
315
+ try:
316
+ trial_days_int = int(trial_days)
317
+ except Exception:
318
+ trial_days_int = 14
319
+ if trial_days_int <= 0:
320
+ trial_days_int = 14
321
+
322
+ inst_fp = ""
323
+ try:
324
+ inst_fp = instance_fingerprint()
325
+ if inst_fp:
326
+ set_setting("premium.instance_id", inst_fp)
327
+ except Exception:
328
+ inst_fp = ""
329
+
330
+ remote_status = (get_setting("premium.remote_status", "") or "").strip().lower() or None
331
+ remote_grace_until = (get_setting("premium.remote_grace_until", "") or "").strip() or None
332
+
333
+ if licence_server and refresh_entitlements and inst_fp:
334
+ last_sync = get_setting("premium.last_sync_at", "") or ""
335
+ if _sync_due(last_sync):
336
+ resp = _post(f"{licence_server}/v1/licence", {"instance_fp": inst_fp})
337
+ if isinstance(resp, dict) and resp.get("ok"):
338
+ st = str(resp.get("status") or "").strip().lower() or ""
339
+ if st:
340
+ remote_status = st
341
+ try:
342
+ set_setting("premium.remote_status", st)
343
+ except Exception:
344
+ pass
345
+
346
+ gu_iso = None
347
+ if resp.get("grace_until_iso"):
348
+ gu_iso = str(resp.get("grace_until_iso") or "").strip() or None
349
+ elif resp.get("grace_until"):
350
+ try:
351
+ gu_ts = int(resp.get("grace_until"))
352
+ gu_iso = _iso_utc(datetime.utcfromtimestamp(gu_ts))
353
+ except Exception:
354
+ gu_iso = None
355
+
356
+ remote_grace_until = gu_iso
357
+ try:
358
+ set_setting("premium.remote_grace_until", gu_iso or "")
359
+ except Exception:
360
+ pass
361
+
362
+ lic_path = os.path.join(client_dir, "premium", "licence.json")
363
+ if st in ("active", "past_due") and isinstance(resp.get("licence"), dict):
364
+ try:
365
+ os.makedirs(os.path.dirname(lic_path), exist_ok=True)
366
+ with open(lic_path, "w", encoding="utf-8") as f:
367
+ json.dump(resp["licence"], f, ensure_ascii=False, indent=2)
368
+ except Exception:
369
+ pass
370
+ elif st == "revoked":
371
+ try:
372
+ if os.path.exists(lic_path):
373
+ os.remove(lic_path)
374
+ except Exception:
375
+ pass
376
+
377
+ try:
378
+ set_setting("premium.last_sync_at", _now_iso())
379
+ except Exception:
380
+ pass
381
+
382
+ remote_revoked = (str(remote_status or "").strip().lower() == "revoked")
383
+
384
+ # Trial tracking
385
+ started_raw = get_setting("premium.trial_started_at", "") or ""
386
+ started_dt = _parse_iso_utc(started_raw)
387
+ if started_dt is None:
388
+ started_dt = now_dt
389
+ started_raw = _iso_utc(started_dt)
390
+ try:
391
+ set_setting("premium.trial_started_at", started_raw)
392
+ except Exception:
393
+ pass
394
+
395
+ elapsed = now_dt - started_dt
396
+ trial_active = elapsed < timedelta(days=trial_days_int)
397
+ days_left = 0
398
+ if trial_active:
399
+ remaining = timedelta(days=trial_days_int) - elapsed
400
+ days_left = max(1, int((remaining.total_seconds() + 86399) // 86400))
401
+
402
+ if remote_revoked:
403
+ trial_active = False
404
+ days_left = 0
405
+
406
+ try:
407
+ set_setting("premium.trial_days", str(trial_days_int))
408
+ set_setting("premium.trial_active", "1" if trial_active else "0")
409
+ except Exception:
410
+ pass
411
+
412
+ source = "none"
413
+ ent: Dict[str, Any] = {}
414
+ last_err = ""
415
+
416
+ allow_unsigned = trial_active or _truthy(os.environ.get("SMX_PREMIUM_ALLOW_UNSIGNED", "0"))
417
+
418
+ def _accept_payload(payload: Optional[Dict[str, Any]], src: str) -> Optional[Dict[str, Any]]:
419
+ nonlocal last_err, source
420
+ if not payload:
421
+ return None
422
+ verified = verify_licence_payload(
423
+ payload,
424
+ now=now_dt,
425
+ allow_unsigned=allow_unsigned,
426
+ )
427
+ if not verified.ok:
428
+ last_err = verified.error or "Licence rejected"
429
+ return None
430
+ source = src
431
+ return verified.entitlements
432
+
433
+ def _resolve_from_claims(claims: Dict[str, Any]) -> Dict[str, Any]:
434
+ plan_claim = str(claims.get("plan_id") or claims.get("plan") or "free").strip().lower() or "free"
435
+ ver = str(claims.get("entitlement_version") or claims.get("entitlementVersion") or "").strip() or None
436
+ addons = claims.get("addons") if isinstance(claims.get("addons"), list) else None
437
+ overrides = claims.get("entitlements") if isinstance(claims.get("entitlements"), dict) else None
438
+
439
+ ent_resolved = _catalogue_entitlements(plan_claim, version=ver, addons=addons)
440
+
441
+ if overrides:
442
+ for k, v in overrides.items():
443
+ if k in ("sig", "instance_id", "issued_at", "expires_at"):
444
+ continue
445
+ ent_resolved[k] = v
446
+
447
+ return ent_resolved
448
+
449
+ ent = {}
450
+ last_err = ""
451
+
452
+ if remote_revoked:
453
+ ent = free_entitlements()
454
+ source = "remote"
455
+ last_err = ""
456
+ else:
457
+ if refresh_entitlements:
458
+ payload = _read_payload_from_file(client_dir)
459
+ claims = _accept_payload(payload, "file") or {}
460
+ if claims:
461
+ ent = _resolve_from_claims(claims)
462
+ source = "file"
463
+
464
+ if not ent:
465
+ if trial_active:
466
+ ent = trial_entitlements()
467
+ source = "trial"
468
+ else:
469
+ ent = free_entitlements()
470
+ source = "none"
471
+
472
+ try:
473
+ set_setting("premium.last_licence_error", last_err)
474
+ except Exception:
475
+ pass
476
+
477
+ plan = _plan_from_entitlements(ent)
478
+
479
+ trial_active_eff = trial_active
480
+ days_left_eff = days_left
481
+ if plan in ("pro", "business", "enterprise"):
482
+ trial_active_eff = False
483
+ days_left_eff = 0
484
+
485
+ try:
486
+ set_setting("premium.plan", plan)
487
+ set_setting("premium.entitlements", json.dumps(ent, ensure_ascii=False))
488
+ except Exception:
489
+ pass
490
+
491
+ try:
492
+ _apply_entitlements_to_feature_flags(db, ent)
493
+ except Exception:
494
+ pass
495
+
496
+ return PremiumState(
497
+ trial_days=trial_days_int,
498
+ trial_started_at=started_raw,
499
+ trial_active=trial_active_eff,
500
+ trial_days_left=days_left_eff,
501
+ plan=plan,
502
+ entitlements=ent,
503
+ source=source,
504
+ instance_id=(inst_fp or None),
505
+ remote_status=remote_status,
506
+ remote_grace_until=remote_grace_until,
507
+ )