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.
- syntaxmatrix/__init__.py +6 -4
- syntaxmatrix/agentic/agents.py +195 -15
- syntaxmatrix/agentic/agents_orchestrer.py +16 -10
- syntaxmatrix/client_docs.py +237 -0
- syntaxmatrix/commentary.py +96 -25
- syntaxmatrix/core.py +156 -54
- syntaxmatrix/dataset_preprocessing.py +2 -2
- syntaxmatrix/db.py +60 -0
- syntaxmatrix/db_backends/__init__.py +1 -0
- syntaxmatrix/db_backends/postgres_backend.py +14 -0
- syntaxmatrix/db_backends/sqlite_backend.py +258 -0
- syntaxmatrix/db_contract.py +71 -0
- syntaxmatrix/kernel_manager.py +174 -150
- syntaxmatrix/page_builder_generation.py +654 -50
- syntaxmatrix/page_layout_contract.py +25 -3
- syntaxmatrix/page_patch_publish.py +368 -15
- syntaxmatrix/plugins/__init__.py +0 -0
- syntaxmatrix/plugins/plugin_manager.py +114 -0
- syntaxmatrix/premium/__init__.py +18 -0
- syntaxmatrix/premium/catalogue/__init__.py +121 -0
- syntaxmatrix/premium/gate.py +119 -0
- syntaxmatrix/premium/state.py +507 -0
- syntaxmatrix/premium/verify.py +222 -0
- syntaxmatrix/profiles.py +1 -1
- syntaxmatrix/routes.py +9782 -8004
- syntaxmatrix/settings/model_map.py +50 -65
- syntaxmatrix/settings/prompts.py +1435 -380
- syntaxmatrix/settings/string_navbar.py +4 -4
- syntaxmatrix/static/icons/bot_icon.png +0 -0
- syntaxmatrix/static/icons/bot_icon2.png +0 -0
- syntaxmatrix/templates/admin_billing.html +408 -0
- syntaxmatrix/templates/admin_branding.html +65 -2
- syntaxmatrix/templates/admin_features.html +54 -0
- syntaxmatrix/templates/dashboard.html +285 -8
- syntaxmatrix/templates/edit_page.html +199 -18
- syntaxmatrix/themes.py +17 -17
- syntaxmatrix/workspace_db.py +0 -23
- syntaxmatrix-3.0.0.dist-info/METADATA +219 -0
- {syntaxmatrix-2.6.4.3.dist-info → syntaxmatrix-3.0.0.dist-info}/RECORD +42 -30
- {syntaxmatrix-2.6.4.3.dist-info → syntaxmatrix-3.0.0.dist-info}/WHEEL +1 -1
- syntaxmatrix/settings/default.yaml +0 -13
- syntaxmatrix-2.6.4.3.dist-info/METADATA +0 -539
- syntaxmatrix-2.6.4.3.dist-info/licenses/LICENSE.txt +0 -21
- /syntaxmatrix/static/icons/{logo3.png → logo2.png} +0 -0
- {syntaxmatrix-2.6.4.3.dist-info → syntaxmatrix-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
)
|
syntaxmatrix/kernel_manager.py
CHANGED
|
@@ -44,135 +44,46 @@ class SyntaxMatrixKernelManager:
|
|
|
44
44
|
def cleanup_all(cls):
|
|
45
45
|
for sid in list(cls._kernels):
|
|
46
46
|
cls.shutdown_kernel(sid)
|
|
47
|
+
|
|
48
|
+
def _prefer_display_over_to_html(code: str) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Convert common 'print(df.to_html())' patterns into IPython display(df).
|
|
51
|
+
Best-effort only; avoids breaking code.
|
|
52
|
+
"""
|
|
53
|
+
if not code or "to_html" not in code:
|
|
54
|
+
return code
|
|
55
|
+
|
|
56
|
+
# Replace print(X.to_html()) -> display(X)
|
|
57
|
+
code2 = _re.sub(
|
|
58
|
+
r"print\(\s*([A-Za-z_][A-Za-z0-9_\.]*)\s*\.to_html\(\s*\)\s*\)",
|
|
59
|
+
r"display(\1)",
|
|
60
|
+
code,
|
|
61
|
+
flags=_re.IGNORECASE,
|
|
62
|
+
)
|
|
47
63
|
|
|
64
|
+
# Replace print(pd.DataFrame(...).to_html()) -> display(pd.DataFrame(...))
|
|
65
|
+
code2 = _re.sub(
|
|
66
|
+
r"print\(\s*(pd\.DataFrame\([^\)]*\))\s*\.to_html\(\s*\)\s*\)",
|
|
67
|
+
r"display(\1)",
|
|
68
|
+
code2,
|
|
69
|
+
flags=_re.IGNORECASE,
|
|
70
|
+
)
|
|
48
71
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
_local_stdout = ""
|
|
54
|
-
_local_stderr = ""
|
|
72
|
+
# If we introduced display(), ensure import exists
|
|
73
|
+
if "display(" in code2 and "from IPython.display import display" not in code2:
|
|
74
|
+
code2 = "from IPython.display import display\n" + code2
|
|
55
75
|
|
|
56
|
-
|
|
57
|
-
exec_namespace = {}
|
|
76
|
+
return code2
|
|
58
77
|
|
|
59
|
-
if not hasattr(_pd.core.generic.NDFrame, "_patch_safe_reduce"):
|
|
60
|
-
_AGG_FUNCS = (
|
|
61
|
-
"sum", "mean", "median", "std", "var",
|
|
62
|
-
"min", "max", "prod",
|
|
63
|
-
)
|
|
64
78
|
|
|
65
|
-
|
|
66
|
-
orig = getattr(_pd.core.generic.NDFrame, name)
|
|
67
|
-
|
|
68
|
-
@wraps(orig)
|
|
69
|
-
def _wrapper(self, *args, **kwargs):
|
|
70
|
-
try:
|
|
71
|
-
return orig(self, *args, **kwargs)
|
|
72
|
-
|
|
73
|
-
except TypeError as exc:
|
|
74
|
-
msg = str(exc)
|
|
75
|
-
if (
|
|
76
|
-
"can only concatenate str" not in msg
|
|
77
|
-
and "could not convert" not in msg
|
|
78
|
-
):
|
|
79
|
-
raise # - not the error we’re guarding against
|
|
80
|
-
|
|
81
|
-
# Caller already supplied numeric_only (positional or kw) → re-raise
|
|
82
|
-
if len(args) >= 3 or "numeric_only" in kwargs:
|
|
83
|
-
raise
|
|
84
|
-
|
|
85
|
-
kwargs = dict(kwargs)
|
|
86
|
-
kwargs["numeric_only"] = True
|
|
87
|
-
return orig(self, *args, **kwargs)
|
|
88
|
-
|
|
89
|
-
return _wrapper
|
|
90
|
-
|
|
91
|
-
for _fn in _AGG_FUNCS:
|
|
92
|
-
setattr(
|
|
93
|
-
_pd.core.generic.NDFrame,
|
|
94
|
-
f"_orig_{_fn}",
|
|
95
|
-
getattr(_pd.core.generic.NDFrame, _fn),
|
|
96
|
-
)
|
|
97
|
-
setattr(
|
|
98
|
-
_pd.core.generic.NDFrame,
|
|
99
|
-
_fn,
|
|
100
|
-
_make_wrapper(_fn),
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
# marker so we don’t patch twice in the same kernel
|
|
104
|
-
_pd.core.generic.NDFrame._patch_safe_reduce = True
|
|
105
|
-
|
|
106
|
-
if "_pandas_sum_patched" not in exec_namespace:
|
|
107
|
-
|
|
108
|
-
if not hasattr(_pd.core.generic.NDFrame, "_orig_sum"):
|
|
109
|
-
_pd.core.generic.NDFrame._orig_sum = _pd.core.generic.NDFrame.sum
|
|
110
|
-
_pd.core.generic.NDFrame._orig_mean = _pd.core.generic.NDFrame.mean # ← NEW
|
|
111
|
-
|
|
112
|
-
def _safe_agg(orig_func):
|
|
113
|
-
def wrapper(self, *args, **kwargs):
|
|
114
|
-
try:
|
|
115
|
-
return orig_func(self, *args, **kwargs)
|
|
116
|
-
|
|
117
|
-
except TypeError as exc:
|
|
118
|
-
# Only rescue the classic mixed-dtype failure
|
|
119
|
-
if ("can only concatenate" not in str(exc) and
|
|
120
|
-
"could not convert" not in str(exc)):
|
|
121
|
-
raise
|
|
122
|
-
|
|
123
|
-
# Caller already gave numeric_only → we must not override
|
|
124
|
-
if "numeric_only" in kwargs or len(args) >= 3:
|
|
125
|
-
raise
|
|
126
|
-
|
|
127
|
-
kwargs = dict(kwargs)
|
|
128
|
-
kwargs["numeric_only"] = True
|
|
129
|
-
return orig_func(self, *args, **kwargs)
|
|
130
|
-
return wrapper
|
|
131
|
-
|
|
132
|
-
_pd.core.generic.NDFrame.sum = _safe_agg(_pd.core.generic.NDFrame._orig_sum)
|
|
133
|
-
_pd.core.generic.NDFrame.mean = _safe_agg(_pd.core.generic.NDFrame._orig_mean)
|
|
134
|
-
|
|
135
|
-
exec_namespace["_pandas_sum_patched"] = True
|
|
136
|
-
|
|
137
|
-
# inject cached df if we have one
|
|
138
|
-
if _df_cache is not None:
|
|
139
|
-
exec_namespace["df"] = _df_cache
|
|
140
|
-
|
|
141
|
-
try:
|
|
142
|
-
# Prevent any print()/stdout/stderr from hitting your server console
|
|
143
|
-
_buf_out, _buf_err = io.StringIO(), io.StringIO()
|
|
144
|
-
with contextlib.redirect_stdout(_buf_out), contextlib.redirect_stderr(_buf_err):
|
|
145
|
-
exec(code, exec_namespace, exec_namespace)
|
|
146
|
-
|
|
147
|
-
_local_stdout = _buf_out.getvalue()
|
|
148
|
-
_local_stderr = _buf_err.getvalue()
|
|
149
|
-
|
|
150
|
-
# ── show a friendly “missing package” hint ────
|
|
151
|
-
except (ModuleNotFoundError, ImportError) as e:
|
|
152
|
-
missing = getattr(e, "name", None) or str(e).split("'")[1]
|
|
153
|
-
hint = (
|
|
154
|
-
|
|
155
|
-
f"<div style='color:red; font-weight:bold;'>"
|
|
156
|
-
f"Missing package: <code>{missing}</code><br>"
|
|
157
|
-
f"Activate this virtual-env and run:<br>"
|
|
158
|
-
f"<code>pip install {missing}</code><br>"
|
|
159
|
-
f"then re-run your query.</div>"
|
|
160
|
-
)
|
|
161
|
-
return [hint], []
|
|
162
|
-
|
|
163
|
-
except Exception:
|
|
164
|
-
pass
|
|
79
|
+
_df_cache = None
|
|
165
80
|
|
|
166
|
-
|
|
167
|
-
if "df" in exec_namespace:
|
|
168
|
-
_df_cache = exec_namespace["df"]
|
|
81
|
+
def execute_code_in_kernel(kc, code, timeout=120):
|
|
169
82
|
|
|
170
|
-
|
|
83
|
+
code = SyntaxMatrixKernelManager._prefer_display_over_to_html(code)
|
|
171
84
|
if "display(" in code and "from IPython.display import display" not in code:
|
|
172
85
|
code = "from IPython.display import display\n" + code
|
|
173
86
|
|
|
174
|
-
# ------------------------------------------------------------------
|
|
175
|
-
# everything below is the original while-loop message collector
|
|
176
87
|
msg_id = kc.execute(
|
|
177
88
|
code,
|
|
178
89
|
user_expressions={"_last": "(_)",},
|
|
@@ -211,18 +122,6 @@ def execute_code_in_kernel(kc, code, timeout=120):
|
|
|
211
122
|
if txt:
|
|
212
123
|
output_blocks.append(f"<pre>{_html.escape(txt)}</pre>")
|
|
213
124
|
|
|
214
|
-
# elif mtype in ("execute_result", "display_data"):
|
|
215
|
-
# data = content["data"]
|
|
216
|
-
# if "text/html" in data:
|
|
217
|
-
# output_blocks.append(data["text/html"])
|
|
218
|
-
# elif "image/png" in data:
|
|
219
|
-
# output_blocks.append(
|
|
220
|
-
# f"<img src='data:image/png;base64,{data['image/png']}' "
|
|
221
|
-
# f"style='max-width:100%;'/>"
|
|
222
|
-
# )
|
|
223
|
-
# else:
|
|
224
|
-
# output_blocks.append(f"<pre>{data.get('text/plain','')}</pre>")
|
|
225
|
-
|
|
226
125
|
elif mtype in ("execute_result", "display_data"):
|
|
227
126
|
data = content.get("data", {})
|
|
228
127
|
if "text/html" in data:
|
|
@@ -246,38 +145,163 @@ def execute_code_in_kernel(kc, code, timeout=120):
|
|
|
246
145
|
# keep the traceback html-friendly
|
|
247
146
|
traceback_html = "<br>".join(content["traceback"])
|
|
248
147
|
errors.append(f"<pre style='color:red;'>{traceback_html}</pre>")
|
|
249
|
-
# --- surface the locally captured commentary (stdout/stderr) back to the UI ---
|
|
250
|
-
if _local_stdout.strip():
|
|
251
|
-
# Put commentary first so the user sees it above plots/tables
|
|
252
|
-
output_blocks.insert(0, f"<pre>{_html.escape(_local_stdout)}</pre>")
|
|
253
|
-
if _local_stderr.strip():
|
|
254
|
-
errors.insert(0, f"<pre style='color:#b00;'>{_html.escape(_local_stderr)}</pre>")
|
|
255
148
|
|
|
256
149
|
def _smx_strip_display_reprs(text: str) -> str:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
text =
|
|
263
|
-
|
|
264
|
-
|
|
150
|
+
"""Remove noisy Jupyter display reprs while keeping line breaks readable."""
|
|
151
|
+
if text is None:
|
|
152
|
+
return ""
|
|
153
|
+
# Strip common display wrappers that sometimes leak into stdout
|
|
154
|
+
text = text.replace("<IPython.core.display.HTML object>", "")
|
|
155
|
+
text = text.replace("<IPython.core.display.Markdown object>", "")
|
|
156
|
+
text = text.replace("<IPython.core.display.Image object>", "")
|
|
157
|
+
|
|
158
|
+
# Normalise line endings but KEEP newlines
|
|
159
|
+
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
160
|
+
|
|
161
|
+
# Collapse runs of spaces/tabs per line (do not flatten newlines)
|
|
162
|
+
lines = []
|
|
163
|
+
for ln in text.split("\n"):
|
|
164
|
+
ln = _re.sub(r"[\t ]{2,}", " ", ln).rstrip()
|
|
165
|
+
lines.append(ln)
|
|
166
|
+
|
|
167
|
+
text = "\n".join(lines).strip()
|
|
168
|
+
|
|
169
|
+
# Avoid huge blank sections
|
|
170
|
+
text = _re.sub(r"\n{3,}", "\n\n", text)
|
|
265
171
|
return text
|
|
266
172
|
|
|
173
|
+
def _sanitize_table_html(table_html: str) -> str:
|
|
174
|
+
"""Best-effort sanitiser for DataFrame-style HTML tables (blocks scripts/events)."""
|
|
175
|
+
if not table_html:
|
|
176
|
+
return ""
|
|
177
|
+
# Remove scripts and styles outright
|
|
178
|
+
table_html = _re.sub(
|
|
179
|
+
r"<\s*script[^>]*>.*?<\s*/\s*script\s*>",
|
|
180
|
+
"",
|
|
181
|
+
table_html,
|
|
182
|
+
flags=_re.IGNORECASE | _re.DOTALL,
|
|
183
|
+
)
|
|
184
|
+
table_html = _re.sub(
|
|
185
|
+
r"<\s*style[^>]*>.*?<\s*/\s*style\s*>",
|
|
186
|
+
"",
|
|
187
|
+
table_html,
|
|
188
|
+
flags=_re.IGNORECASE | _re.DOTALL,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Drop inline event handlers (onload=, onclick=, etc.)
|
|
192
|
+
table_html = _re.sub(r"\son\w+\s*=\s*\"[^\"]*\"", "", table_html, flags=_re.IGNORECASE)
|
|
193
|
+
table_html = _re.sub(r"\son\w+\s*=\s*\'[^\']*\'", "", table_html, flags=_re.IGNORECASE)
|
|
194
|
+
|
|
195
|
+
# Block javascript: URLs
|
|
196
|
+
table_html = _re.sub(r"javascript\s*:", "", table_html, flags=_re.IGNORECASE)
|
|
197
|
+
return table_html.strip()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _smx_ensure_table_class(table_html: str) -> str:
|
|
201
|
+
"""Ensure DataFrame tables pick up the dashboard's .smx-table styling."""
|
|
202
|
+
if not table_html:
|
|
203
|
+
return ""
|
|
204
|
+
|
|
205
|
+
def _inject_class(m):
|
|
206
|
+
tag = m.group(0)
|
|
207
|
+
|
|
208
|
+
# If there's already a class="", append smx-table
|
|
209
|
+
if _re.search(r"\bclass\s*=", tag, flags=_re.IGNORECASE):
|
|
210
|
+
tag = _re.sub(
|
|
211
|
+
r'(class\s*=\s*["\'])([^"\']*)(["\'])',
|
|
212
|
+
lambda mm: f"{mm.group(1)}{mm.group(2)} smx-table{mm.group(3)}",
|
|
213
|
+
tag,
|
|
214
|
+
count=1,
|
|
215
|
+
flags=_re.IGNORECASE,
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
# add class attr
|
|
219
|
+
tag = tag[:-1] + ' class="smx-table">'
|
|
220
|
+
|
|
221
|
+
# Remove noisy border attr if present (optional)
|
|
222
|
+
tag = _re.sub(r'\sborder\s*=\s*["\']?\d+["\']?', "", tag, flags=_re.IGNORECASE)
|
|
223
|
+
return tag
|
|
224
|
+
|
|
225
|
+
return _re.sub(r"<table\b[^>]*>", _inject_class, table_html, count=1, flags=_re.IGNORECASE)
|
|
226
|
+
|
|
227
|
+
def _smx_wrap_df_table(table_html: str, max_h: int = 460) -> str:
|
|
228
|
+
"""
|
|
229
|
+
Wrap table in a scroll box and apply the same table class the dashboard already styles.
|
|
230
|
+
"""
|
|
231
|
+
table_html = _smx_ensure_table_class(table_html)
|
|
232
|
+
if not table_html:
|
|
233
|
+
return ""
|
|
234
|
+
return (
|
|
235
|
+
f"<div class='smx-scroll' style='overflow:auto; max-height:{int(max_h)}px;'>"
|
|
236
|
+
f"{table_html}"
|
|
237
|
+
f"</div>"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def _split_text_and_tables(text: str):
|
|
241
|
+
"""Yield (kind, chunk) where kind is 'text' or 'table'."""
|
|
242
|
+
if not text:
|
|
243
|
+
return [("text", "")]
|
|
244
|
+
out = []
|
|
245
|
+
last = 0
|
|
246
|
+
for m in _TABLE_RE.finditer(text):
|
|
247
|
+
if m.start() > last:
|
|
248
|
+
out.append(("text", text[last:m.start()]))
|
|
249
|
+
out.append(("table", m.group(1)))
|
|
250
|
+
last = m.end()
|
|
251
|
+
if last < len(text):
|
|
252
|
+
out.append(("text", text[last:]))
|
|
253
|
+
return out
|
|
254
|
+
|
|
255
|
+
_TABLE_RE = _re.compile(r"(<table\b.*?</table>)", flags=_re.IGNORECASE | _re.DOTALL)
|
|
256
|
+
|
|
267
257
|
_cleaned_blocks = []
|
|
268
258
|
for blk in output_blocks:
|
|
269
259
|
# pre-wrapped plaintext
|
|
270
260
|
if blk.startswith("<pre>") and blk.endswith("</pre>"):
|
|
271
261
|
inner = blk[5:-6]
|
|
272
|
-
inner =
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
#
|
|
262
|
+
inner = _html.unescape(inner)
|
|
263
|
+
|
|
264
|
+
# If stdout contains HTML tables (e.g., DataFrame.to_html()), render them as tables
|
|
265
|
+
# rather than showing the raw <table> markup inside a <pre> block.
|
|
266
|
+
low = inner.lower()
|
|
267
|
+
if "<table" in low and "</table>" in low:
|
|
268
|
+
parts = _split_text_and_tables(inner)
|
|
269
|
+
else:
|
|
270
|
+
parts = [("text", inner)]
|
|
271
|
+
|
|
272
|
+
for kind, chunk in parts:
|
|
273
|
+
if not chunk:
|
|
274
|
+
continue
|
|
275
|
+
if kind == "table":
|
|
276
|
+
safe_tbl = _sanitize_table_html(chunk)
|
|
277
|
+
if safe_tbl:
|
|
278
|
+
_cleaned_blocks.append(_smx_wrap_df_table(safe_tbl))
|
|
279
|
+
else:
|
|
280
|
+
txt = _smx_strip_display_reprs(chunk)
|
|
281
|
+
if txt:
|
|
282
|
+
_cleaned_blocks.append(f"<pre>{_html.escape(txt)}</pre>")
|
|
283
|
+
|
|
276
284
|
continue
|
|
277
285
|
|
|
278
286
|
# html/img payloads: just remove stray repr tokens if they slipped in
|
|
279
287
|
cleaned = _re.sub(r"<IPython\.core\.display\.[A-Za-z]+\s+object>", "", blk)
|
|
280
|
-
|
|
288
|
+
low = cleaned.lower()
|
|
289
|
+
if "<table" in low and "</table>" in low:
|
|
290
|
+
parts = _split_text_and_tables(cleaned)
|
|
291
|
+
for kind, chunk in parts:
|
|
292
|
+
if not chunk:
|
|
293
|
+
continue
|
|
294
|
+
if kind == "table":
|
|
295
|
+
safe_tbl = _sanitize_table_html(chunk)
|
|
296
|
+
wrapped = _smx_wrap_df_table(safe_tbl)
|
|
297
|
+
if wrapped:
|
|
298
|
+
_cleaned_blocks.append(wrapped)
|
|
299
|
+
else:
|
|
300
|
+
# keep non-table html as-is
|
|
301
|
+
if chunk.strip():
|
|
302
|
+
_cleaned_blocks.append(chunk)
|
|
303
|
+
else:
|
|
304
|
+
_cleaned_blocks.append(cleaned)
|
|
281
305
|
output_blocks = _cleaned_blocks
|
|
282
306
|
|
|
283
307
|
return output_blocks, errors
|