syntaxmatrix 1.4.6__py3-none-any.whl → 2.5.5.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/__init__.py +13 -8
- syntaxmatrix/agentic/__init__.py +0 -0
- syntaxmatrix/agentic/agent_tools.py +24 -0
- syntaxmatrix/agentic/agents.py +810 -0
- syntaxmatrix/agentic/code_tools_registry.py +37 -0
- syntaxmatrix/agentic/model_templates.py +1790 -0
- syntaxmatrix/auth.py +308 -14
- syntaxmatrix/commentary.py +328 -0
- syntaxmatrix/core.py +993 -375
- syntaxmatrix/dataset_preprocessing.py +218 -0
- syntaxmatrix/db.py +92 -95
- syntaxmatrix/display.py +95 -121
- syntaxmatrix/generate_page.py +634 -0
- syntaxmatrix/gpt_models_latest.py +46 -0
- syntaxmatrix/history_store.py +26 -29
- syntaxmatrix/kernel_manager.py +96 -17
- syntaxmatrix/llm_store.py +1 -1
- syntaxmatrix/plottings.py +6 -0
- syntaxmatrix/profiles.py +64 -8
- syntaxmatrix/project_root.py +55 -43
- syntaxmatrix/routes.py +5072 -1398
- syntaxmatrix/session.py +19 -0
- syntaxmatrix/settings/logging.py +40 -0
- syntaxmatrix/settings/model_map.py +300 -33
- syntaxmatrix/settings/prompts.py +273 -62
- syntaxmatrix/settings/string_navbar.py +3 -3
- syntaxmatrix/static/docs.md +272 -0
- syntaxmatrix/static/icons/favicon.png +0 -0
- syntaxmatrix/static/icons/hero_bg.jpg +0 -0
- syntaxmatrix/templates/dashboard.html +608 -147
- syntaxmatrix/templates/docs.html +71 -0
- syntaxmatrix/templates/error.html +2 -3
- syntaxmatrix/templates/login.html +1 -0
- syntaxmatrix/templates/register.html +1 -0
- syntaxmatrix/ui_modes.py +14 -0
- syntaxmatrix/utils.py +2482 -159
- syntaxmatrix/vectorizer.py +16 -12
- {syntaxmatrix-1.4.6.dist-info → syntaxmatrix-2.5.5.4.dist-info}/METADATA +20 -17
- syntaxmatrix-2.5.5.4.dist-info/RECORD +68 -0
- syntaxmatrix/model_templates.py +0 -30
- syntaxmatrix/static/icons/favicon.ico +0 -0
- syntaxmatrix-1.4.6.dist-info/RECORD +0 -54
- {syntaxmatrix-1.4.6.dist-info → syntaxmatrix-2.5.5.4.dist-info}/WHEEL +0 -0
- {syntaxmatrix-1.4.6.dist-info → syntaxmatrix-2.5.5.4.dist-info}/licenses/LICENSE.txt +0 -0
- {syntaxmatrix-1.4.6.dist-info → syntaxmatrix-2.5.5.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# --- Helper: robustly extract text from Responses API objects ---
|
|
2
|
+
def extract_output_text(resp) -> str:
|
|
3
|
+
# Fast path
|
|
4
|
+
if hasattr(resp, "output_text") and resp.output_text:
|
|
5
|
+
return resp.output_text.strip()
|
|
6
|
+
# Fallback: parse .output for message->content blocks
|
|
7
|
+
text_parts = []
|
|
8
|
+
output = getattr(resp, "output", None) or []
|
|
9
|
+
for item in output:
|
|
10
|
+
if getattr(item, "type", "") != "message":
|
|
11
|
+
continue
|
|
12
|
+
for block in getattr(item, "content", []) or []:
|
|
13
|
+
btype = getattr(block, "type", "")
|
|
14
|
+
if btype in ("output_text", "text"):
|
|
15
|
+
t = (getattr(block, "text", "") or "").strip()
|
|
16
|
+
if t:
|
|
17
|
+
text_parts.append(t)
|
|
18
|
+
return "\n".join(text_parts).strip()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def set_args(
|
|
22
|
+
model,
|
|
23
|
+
instructions,
|
|
24
|
+
input,
|
|
25
|
+
previous_id=None,
|
|
26
|
+
store=False,
|
|
27
|
+
reasoning_effort="medium", # "minimal", "low", "medium", "high"
|
|
28
|
+
verbosity="medium", # "low", "medium", "high"
|
|
29
|
+
truncation="auto",
|
|
30
|
+
):
|
|
31
|
+
base_params = {
|
|
32
|
+
"model": model,
|
|
33
|
+
"instructions": instructions,
|
|
34
|
+
"input": input,
|
|
35
|
+
"previous_response_id": previous_id,
|
|
36
|
+
"store": store,
|
|
37
|
+
"truncation": truncation,
|
|
38
|
+
}
|
|
39
|
+
if model == "gpt-5.1-chat-latest":
|
|
40
|
+
args = base_params
|
|
41
|
+
else:
|
|
42
|
+
args = {**base_params,
|
|
43
|
+
"reasoning": {"effort": reasoning_effort},
|
|
44
|
+
"text": {"verbosity": verbosity}
|
|
45
|
+
}
|
|
46
|
+
return args
|
syntaxmatrix/history_store.py
CHANGED
|
@@ -11,7 +11,7 @@ from syntaxmatrix.project_root import detect_project_root
|
|
|
11
11
|
|
|
12
12
|
_CLIENT_DIR = detect_project_root()
|
|
13
13
|
# ——— Anonymous-user JSON fallback store ——————————————
|
|
14
|
-
_fallback_dir = os.path.join(_CLIENT_DIR, "
|
|
14
|
+
_fallback_dir = os.path.join(_CLIENT_DIR, "smx_history")
|
|
15
15
|
os.makedirs(_fallback_dir, exist_ok=True)
|
|
16
16
|
|
|
17
17
|
# Persist chats.db under dev app’s data/ directory
|
|
@@ -79,17 +79,14 @@ class SQLHistoryStore:
|
|
|
79
79
|
payload = json.dumps(history, ensure_ascii=False)
|
|
80
80
|
now = datetime.utcnow().isoformat()
|
|
81
81
|
# if caller didn’t supply a title, preserve existing or default
|
|
82
|
-
if title is
|
|
83
|
-
conn.execute(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
"UPDATE chats SET history = ?, updated_at = ? WHERE user_id = ? AND chat_id = ?",
|
|
91
|
-
(payload, now, user_id, chat_id)
|
|
92
|
-
)
|
|
82
|
+
if title is None:
|
|
83
|
+
cur = conn.execute("SELECT title FROM chats WHERE user_id=? AND chat_id=?", (user_id, chat_id))
|
|
84
|
+
row = cur.fetchone()
|
|
85
|
+
title = row[0] if row else "Current"
|
|
86
|
+
conn.execute(
|
|
87
|
+
"INSERT OR REPLACE INTO chats (user_id, chat_id, title, history, updated_at) VALUES (?, ?, ?, ?, ?)",
|
|
88
|
+
(user_id, chat_id, title, payload, now)
|
|
89
|
+
)
|
|
93
90
|
conn.commit()
|
|
94
91
|
conn.close()
|
|
95
92
|
|
|
@@ -173,20 +170,20 @@ class PersistentHistoryStore:
|
|
|
173
170
|
os.remove(file_path)
|
|
174
171
|
|
|
175
172
|
|
|
176
|
-
@atexit.register
|
|
177
|
-
def _clear_anonymous_history_on_exit() -> None:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
173
|
+
@atexit.register
|
|
174
|
+
def _clear_anonymous_history_on_exit() -> None:
|
|
175
|
+
"""
|
|
176
|
+
On clean shutdown, delete all JSON chat files for anonymous users
|
|
177
|
+
under data/smx_history/, leaving chats.db untouched.
|
|
178
|
+
"""
|
|
179
|
+
history_dir = Path(_fallback_dir)
|
|
180
|
+
if not history_dir.is_dir():
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
for file_path in history_dir.glob("*.json"):
|
|
184
|
+
try:
|
|
185
|
+
file_path.unlink()
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logging.warning(
|
|
188
|
+
f"syntaxmatrix: failed to delete anonymous history file {file_path}: {e}"
|
|
189
|
+
)
|
syntaxmatrix/kernel_manager.py
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import jupyter_client
|
|
4
4
|
import nest_asyncio
|
|
5
|
-
import
|
|
6
|
-
import time
|
|
7
|
-
import re
|
|
5
|
+
import io, contextlib
|
|
6
|
+
import time
|
|
8
7
|
from functools import wraps
|
|
9
8
|
import inspect, pandas as _pd
|
|
10
|
-
|
|
9
|
+
from functools import wraps
|
|
10
|
+
import inspect, pandas as _pd
|
|
11
|
+
import io, contextlib
|
|
12
|
+
import html as _html
|
|
13
|
+
import re as _re
|
|
11
14
|
|
|
12
15
|
nest_asyncio.apply()
|
|
13
16
|
|
|
@@ -42,11 +45,13 @@ class SyntaxMatrixKernelManager:
|
|
|
42
45
|
for sid in list(cls._kernels):
|
|
43
46
|
cls.shutdown_kernel(sid)
|
|
44
47
|
|
|
45
|
-
nest_asyncio.apply()
|
|
46
48
|
|
|
47
49
|
_df_cache = None
|
|
48
50
|
|
|
49
|
-
def execute_code_in_kernel(kc, code, timeout=
|
|
51
|
+
def execute_code_in_kernel(kc, code, timeout=120):
|
|
52
|
+
|
|
53
|
+
_local_stdout = ""
|
|
54
|
+
_local_stderr = ""
|
|
50
55
|
|
|
51
56
|
global _df_cache
|
|
52
57
|
exec_namespace = {}
|
|
@@ -134,7 +139,13 @@ def execute_code_in_kernel(kc, code, timeout=8):
|
|
|
134
139
|
exec_namespace["df"] = _df_cache
|
|
135
140
|
|
|
136
141
|
try:
|
|
137
|
-
|
|
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()
|
|
138
149
|
|
|
139
150
|
# ── show a friendly “missing package” hint ────
|
|
140
151
|
except (ModuleNotFoundError, ImportError) as e:
|
|
@@ -152,7 +163,7 @@ def execute_code_in_kernel(kc, code, timeout=8):
|
|
|
152
163
|
except Exception:
|
|
153
164
|
pass
|
|
154
165
|
|
|
155
|
-
|
|
166
|
+
# cache df for next call
|
|
156
167
|
if "df" in exec_namespace:
|
|
157
168
|
_df_cache = exec_namespace["df"]
|
|
158
169
|
|
|
@@ -171,34 +182,102 @@ def execute_code_in_kernel(kc, code, timeout=8):
|
|
|
171
182
|
start_time = time.time()
|
|
172
183
|
|
|
173
184
|
while True:
|
|
185
|
+
# Block until a message is available; if `timeout` is None this will block.
|
|
174
186
|
try:
|
|
175
187
|
msg = kc.get_iopub_msg(timeout=timeout)
|
|
176
188
|
except Exception:
|
|
177
|
-
break
|
|
189
|
+
break # only trips if a numeric timeout was provided # timeout reached
|
|
178
190
|
|
|
179
191
|
if msg["parent_header"].get("msg_id") != msg_id:
|
|
180
192
|
continue
|
|
181
193
|
|
|
182
194
|
mtype, content = msg["msg_type"], msg["content"]
|
|
195
|
+
# Stop cleanly when the kernel reports it is idle (execution finished)
|
|
196
|
+
if mtype == 'status' and content.get('execution_state') == 'idle':
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
# if mtype == "stream":
|
|
200
|
+
# output_blocks.append(f"<pre>{content['text']}</pre>")
|
|
183
201
|
|
|
184
202
|
if mtype == "stream":
|
|
185
|
-
|
|
203
|
+
raw = content.get("text", "")
|
|
204
|
+
# Remove noisy reprs from printed HTML/Markdown display objects
|
|
205
|
+
lines = [
|
|
206
|
+
ln for ln in raw.splitlines()
|
|
207
|
+
if ("IPython.core.display.HTML object" not in ln
|
|
208
|
+
and "IPython.core.display.Markdown object" not in ln)
|
|
209
|
+
]
|
|
210
|
+
txt = "\n".join(lines).strip()
|
|
211
|
+
if txt:
|
|
212
|
+
output_blocks.append(f"<pre>{_html.escape(txt)}</pre>")
|
|
213
|
+
|
|
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>")
|
|
186
225
|
|
|
187
226
|
elif mtype in ("execute_result", "display_data"):
|
|
188
|
-
data = content
|
|
227
|
+
data = content.get("data", {})
|
|
189
228
|
if "text/html" in data:
|
|
190
|
-
|
|
229
|
+
output_blocks.append(data["text/html"])
|
|
191
230
|
elif "image/png" in data:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
231
|
+
output_blocks.append(
|
|
232
|
+
f"<img src='data:image/png;base64,{data['image/png']}' "
|
|
233
|
+
f"style='max-width:100%;'/>"
|
|
234
|
+
)
|
|
235
|
+
|
|
196
236
|
else:
|
|
197
|
-
|
|
237
|
+
# Clean up plain-text reprs like "<IPython.core.display.HTML object>"
|
|
238
|
+
txt = data.get("text/plain", "") or ""
|
|
239
|
+
if ("IPython.core.display.HTML object" in txt
|
|
240
|
+
or "IPython.core.display.Markdown object" in txt):
|
|
241
|
+
# skip useless reprs entirely
|
|
242
|
+
continue
|
|
243
|
+
output_blocks.append(f"<pre>{_html.escape(txt)}</pre>")
|
|
198
244
|
|
|
199
245
|
elif mtype == "error":
|
|
200
246
|
# keep the traceback html-friendly
|
|
201
247
|
traceback_html = "<br>".join(content["traceback"])
|
|
202
248
|
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
|
+
|
|
256
|
+
def _smx_strip_display_reprs(text: str) -> str:
|
|
257
|
+
if not text:
|
|
258
|
+
return text
|
|
259
|
+
# remove tokens like "<IPython.core.display.HTML object>"
|
|
260
|
+
text = _re.sub(r"<IPython\.core\.display\.[A-Za-z]+\s+object>", "", text)
|
|
261
|
+
# if these were printed as lists, remove leftover brackets/commas
|
|
262
|
+
text = _re.sub(r"[\[\],]", " ", text)
|
|
263
|
+
# collapse whitespace
|
|
264
|
+
text = _re.sub(r"\s+", " ", text).strip()
|
|
265
|
+
return text
|
|
266
|
+
|
|
267
|
+
_cleaned_blocks = []
|
|
268
|
+
for blk in output_blocks:
|
|
269
|
+
# pre-wrapped plaintext
|
|
270
|
+
if blk.startswith("<pre>") and blk.endswith("</pre>"):
|
|
271
|
+
inner = blk[5:-6]
|
|
272
|
+
inner = _smx_strip_display_reprs(_html.unescape(inner))
|
|
273
|
+
if inner:
|
|
274
|
+
_cleaned_blocks.append(f"<pre>{_html.escape(inner)}</pre>")
|
|
275
|
+
# if empty after cleaning, drop it
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
# html/img payloads: just remove stray repr tokens if they slipped in
|
|
279
|
+
cleaned = _re.sub(r"<IPython\.core\.display\.[A-Za-z]+\s+object>", "", blk)
|
|
280
|
+
_cleaned_blocks.append(cleaned)
|
|
281
|
+
output_blocks = _cleaned_blocks
|
|
203
282
|
|
|
204
283
|
return output_blocks, errors
|
syntaxmatrix/llm_store.py
CHANGED
|
@@ -12,7 +12,7 @@ from syntaxmatrix.project_root import detect_project_root
|
|
|
12
12
|
# Ensures a stable encryption key, no env var needed.
|
|
13
13
|
# ------------------------------------------------------------------
|
|
14
14
|
_CLIENT_DIR = detect_project_root()
|
|
15
|
-
KEY_PATH = os.path.join(_CLIENT_DIR, "
|
|
15
|
+
KEY_PATH = os.path.join(_CLIENT_DIR, "fernet.key")
|
|
16
16
|
if os.path.exists(KEY_PATH):
|
|
17
17
|
__FERNET = Fernet(open(KEY_PATH, "rb").read())
|
|
18
18
|
else:
|
syntaxmatrix/plottings.py
CHANGED
|
@@ -14,6 +14,12 @@ warnings.filterwarnings(
|
|
|
14
14
|
message="FigureCanvasAgg is non-interactive"
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
+
def describe_matplotlib():
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
def describe_plotly():
|
|
21
|
+
pass
|
|
22
|
+
|
|
17
23
|
# ── Matplotlib Integration (static PNGs) ────────────────────────────────
|
|
18
24
|
|
|
19
25
|
def figure(*args, **kwargs):
|
syntaxmatrix/profiles.py
CHANGED
|
@@ -1,18 +1,74 @@
|
|
|
1
1
|
# syntaxmatrix/profiles.py
|
|
2
|
+
from openai import OpenAI
|
|
3
|
+
from google import genai
|
|
4
|
+
import anthropic
|
|
2
5
|
|
|
3
6
|
from syntaxmatrix.llm_store import list_profiles, load_profile
|
|
4
7
|
|
|
5
8
|
# Preload once at import-time
|
|
6
9
|
_profiles: dict[str, dict] = {}
|
|
7
|
-
for entry in list_profiles():
|
|
8
|
-
prof = load_profile(entry["name"])
|
|
9
|
-
if prof:
|
|
10
|
-
_profiles[entry["purpose"]] = prof
|
|
11
10
|
|
|
11
|
+
def _refresh_profiles() -> None:
|
|
12
|
+
_profiles.clear()
|
|
13
|
+
for p in list_profiles():
|
|
14
|
+
prof = load_profile(p["name"])
|
|
15
|
+
if prof:
|
|
16
|
+
_profiles[prof["purpose"]] = prof
|
|
17
|
+
|
|
18
|
+
def refresh_profiles_cache() -> None:
|
|
19
|
+
_refresh_profiles()
|
|
20
|
+
|
|
12
21
|
def get_profile(purpose: str) -> dict:
|
|
22
|
+
prof = _profiles.get(purpose)
|
|
23
|
+
if prof:
|
|
24
|
+
return prof
|
|
25
|
+
_refresh_profiles()
|
|
26
|
+
return _profiles.get(purpose)
|
|
27
|
+
|
|
28
|
+
def get_profiles():
|
|
29
|
+
return list_profiles()
|
|
30
|
+
|
|
31
|
+
def get_client(profile):
|
|
32
|
+
|
|
33
|
+
provider = profile["provider"].lower()
|
|
34
|
+
api_key = profile["api_key"]
|
|
35
|
+
|
|
36
|
+
#1 - Google - gemini series
|
|
37
|
+
if provider == "google":
|
|
38
|
+
return genai.Client(api_key=api_key)
|
|
39
|
+
|
|
40
|
+
#2 OpenAI gpt-5 series
|
|
41
|
+
if provider == "openai":
|
|
42
|
+
return OpenAI(api_key=api_key)
|
|
43
|
+
|
|
44
|
+
#3 - xAI - grok series
|
|
45
|
+
if provider == "xai":
|
|
46
|
+
return OpenAI(api_key=api_key, base_url="https://api.x.ai/v1")
|
|
47
|
+
|
|
48
|
+
#4 - DeepSeek chat model
|
|
49
|
+
if provider == "deepseek":
|
|
50
|
+
return OpenAI(api_key=api_key, base_url="https://api.deepseek.com")
|
|
51
|
+
|
|
52
|
+
#5 - Moonshot chat model
|
|
53
|
+
if provider == "moonshot": #5
|
|
54
|
+
return OpenAI(api_key=api_key, base_url="https://api.moonshot.ai/v1")
|
|
55
|
+
|
|
56
|
+
#6 - Alibaba qwen series
|
|
57
|
+
if provider == "alibaba": #6
|
|
58
|
+
return OpenAI(api_key=api_key, base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1",)
|
|
59
|
+
|
|
60
|
+
#7 - Anthropic claude series
|
|
61
|
+
if provider == "anthropic": #7
|
|
62
|
+
return anthropic.Anthropic(api_key=api_key)
|
|
63
|
+
|
|
64
|
+
def drop_cached_profile_by_name(profile_name: str) -> bool:
|
|
13
65
|
"""
|
|
14
|
-
|
|
15
|
-
Returns
|
|
66
|
+
Remove the cached profile with this name (if present) from the in-memory map.
|
|
67
|
+
Returns True if something was removed.
|
|
16
68
|
"""
|
|
17
|
-
|
|
18
|
-
|
|
69
|
+
removed = False
|
|
70
|
+
for purpose, prof in list(_profiles.items()):
|
|
71
|
+
if isinstance(prof, dict) and prof.get("name") == profile_name:
|
|
72
|
+
_profiles.pop(purpose, None)
|
|
73
|
+
removed = True
|
|
74
|
+
return removed
|
syntaxmatrix/project_root.py
CHANGED
|
@@ -1,61 +1,73 @@
|
|
|
1
|
-
# syntaxmatrix/project_root.py
|
|
2
|
-
|
|
1
|
+
# syntaxmatrix/project_root.py
|
|
3
2
|
import os
|
|
4
3
|
import inspect
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
import syntaxmatrix
|
|
7
6
|
|
|
8
|
-
|
|
9
7
|
def scandir() -> Path:
|
|
10
|
-
"""
|
|
11
|
-
Find the first stack frame outside of the syntaxmatrix package
|
|
12
|
-
whose filename is a real .py file on disk, and return its parent dir.
|
|
13
|
-
"""
|
|
14
8
|
framework_dir = Path(syntaxmatrix.__file__).resolve().parent
|
|
15
|
-
|
|
16
9
|
for frame in inspect.stack():
|
|
17
10
|
fname = frame.filename
|
|
18
|
-
|
|
19
|
-
# 1) skip internal frames (<frozen ...>) or empty names
|
|
20
|
-
if not fname or fname.startswith("<"):
|
|
11
|
+
if not fname or not isinstance(fname, str):
|
|
21
12
|
continue
|
|
22
|
-
|
|
23
|
-
candidate = Path(fname)
|
|
24
|
-
|
|
25
|
-
# 2) skip non-.py or non-existent paths
|
|
26
|
-
if candidate.suffix != ".py" or not candidate.exists():
|
|
27
|
-
continue
|
|
28
|
-
|
|
13
|
+
p = Path(fname)
|
|
29
14
|
try:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
15
|
+
if p.is_file() and framework_dir not in p.parents:
|
|
16
|
+
return p.parent
|
|
17
|
+
except Exception:
|
|
33
18
|
continue
|
|
19
|
+
return framework_dir
|
|
34
20
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
21
|
+
def _writable(p: Path) -> bool:
|
|
22
|
+
try:
|
|
23
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
test = p / ".smx_write_test"
|
|
25
|
+
with open(test, "w", encoding="utf-8") as f:
|
|
26
|
+
f.write("ok")
|
|
27
|
+
test.unlink(missing_ok=True)
|
|
28
|
+
return True
|
|
29
|
+
except Exception:
|
|
30
|
+
return False
|
|
45
31
|
|
|
46
32
|
def detect_project_root() -> Path:
|
|
47
33
|
"""
|
|
48
|
-
|
|
49
|
-
|
|
34
|
+
Return the consumer project's 'syntaxmatrixdir' folder, creating it if necessary.
|
|
35
|
+
Resolution order:
|
|
36
|
+
1) SMX_CLIENT_DIR (if set and writable)
|
|
37
|
+
2) ./syntaxmatrixdir under current working dir (if writable)
|
|
38
|
+
3) GCS Fuse standard mounts (if present & writable)
|
|
39
|
+
4) /tmp/syntaxmatrixdir (always writable on Cloud Run)
|
|
40
|
+
5) Fallback near the first non-framework caller (if writable)
|
|
50
41
|
"""
|
|
51
|
-
|
|
42
|
+
|
|
43
|
+
# 1) Explicit override (keeps local stable; handy in Cloud Run)
|
|
44
|
+
env = os.environ.get("SMX_CLIENT_DIR")
|
|
45
|
+
if env:
|
|
46
|
+
p = Path(env)
|
|
47
|
+
if _writable(p):
|
|
48
|
+
return p
|
|
49
|
+
|
|
50
|
+
# 2) CWD-based
|
|
52
51
|
cwd = Path.cwd()
|
|
53
|
-
|
|
54
|
-
if
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
p = cwd / "syntaxmatrixdir"
|
|
53
|
+
if _writable(p):
|
|
54
|
+
return p
|
|
55
|
+
|
|
56
|
+
# 3) Common GCS Fuse mount points (Gen2)
|
|
57
|
+
for candidate in [Path("/mnt/gcs/syntaxmatrixdir"),
|
|
58
|
+
Path("/mnt/disks/gcs/syntaxmatrixdir")]:
|
|
59
|
+
if _writable(candidate):
|
|
60
|
+
return candidate
|
|
61
|
+
|
|
62
|
+
# 4) Cloud Run safe default
|
|
63
|
+
tmp = Path("/tmp/syntaxmatrixdir")
|
|
64
|
+
if _writable(tmp):
|
|
65
|
+
return tmp
|
|
66
|
+
|
|
67
|
+
# 5) Fallback alongside caller
|
|
68
|
+
fallback = scandir() / "syntaxmatrixdir"
|
|
69
|
+
if _writable(fallback):
|
|
70
|
+
return fallback
|
|
71
|
+
|
|
72
|
+
# Last resort: return /tmp anyway (avoids import-time crashes)
|
|
73
|
+
return tmp
|