sf-m3-cli 0.2.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.
- m3_cli/__init__.py +5 -0
- m3_cli/__main__.py +5 -0
- m3_cli/branding.py +10 -0
- m3_cli/control_plane.py +278 -0
- m3_cli/doctor.py +376 -0
- m3_cli/errors.py +10 -0
- m3_cli/init.py +236 -0
- m3_cli/main.py +271 -0
- m3_cli/runtime.py +123 -0
- m3_cli/setup.py +353 -0
- m3_cli/supervisor.py +1336 -0
- m3_cli/ui/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- m3_cli/ui/assets/index-BB1JIVWj.css +1 -0
- m3_cli/ui/assets/index-DRJt176-.js +95 -0
- m3_cli/ui/assets/inter-latin-standard-normal-BwkfbSeq.woff2 +0 -0
- m3_cli/ui/index.html +34 -0
- m3_cli/web.py +166 -0
- sf_m3_cli-0.2.0.dist-info/METADATA +395 -0
- sf_m3_cli-0.2.0.dist-info/RECORD +22 -0
- sf_m3_cli-0.2.0.dist-info/WHEEL +4 -0
- sf_m3_cli-0.2.0.dist-info/entry_points.txt +2 -0
- sf_m3_cli-0.2.0.dist-info/licenses/LICENSE +197 -0
m3_cli/setup.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Project-local M3 SDK setup.
|
|
2
|
+
|
|
3
|
+
The standalone CLI lives in an isolated tool environment. This module is the
|
|
4
|
+
explicit bridge into a project's own Python environment; it never installs
|
|
5
|
+
the CLI or application package into that environment.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib.metadata
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, cast
|
|
20
|
+
|
|
21
|
+
_VALIDATE_SCRIPT = r"""
|
|
22
|
+
import importlib
|
|
23
|
+
import importlib.metadata
|
|
24
|
+
import json
|
|
25
|
+
|
|
26
|
+
checks = {}
|
|
27
|
+
for name in ("pytest", "m3", "m3.pytest_plugin", "openai"):
|
|
28
|
+
try:
|
|
29
|
+
importlib.import_module(name)
|
|
30
|
+
except Exception:
|
|
31
|
+
checks[name] = False
|
|
32
|
+
else:
|
|
33
|
+
checks[name] = True
|
|
34
|
+
try:
|
|
35
|
+
from m3.storage import SQLiteExecutionStore
|
|
36
|
+
except Exception:
|
|
37
|
+
checks["SQLiteExecutionStore"] = False
|
|
38
|
+
else:
|
|
39
|
+
checks["SQLiteExecutionStore"] = True
|
|
40
|
+
try:
|
|
41
|
+
version = importlib.metadata.version("sf-m3")
|
|
42
|
+
except Exception:
|
|
43
|
+
version = None
|
|
44
|
+
print(json.dumps({"checks": checks, "version": version}, sort_keys=True))
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SetupError(RuntimeError):
|
|
49
|
+
"""A safe, user-facing setup failure."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class EnvironmentTarget:
|
|
54
|
+
path: Path
|
|
55
|
+
python: Path
|
|
56
|
+
source: str
|
|
57
|
+
created: bool = False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _python_path(base: Path) -> Path:
|
|
61
|
+
candidate = base / ("Scripts" if os.name == "nt" else "bin") / "python"
|
|
62
|
+
if os.name == "nt" and not candidate.is_file():
|
|
63
|
+
candidate = candidate.with_suffix(".exe")
|
|
64
|
+
return candidate
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _environment_root(python: Path) -> Path:
|
|
68
|
+
return (
|
|
69
|
+
python.parent.parent
|
|
70
|
+
if python.parent.name in {"bin", "Scripts"}
|
|
71
|
+
else python.parent
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _absolute(value: str | os.PathLike[str]) -> Path:
|
|
76
|
+
return Path(value).expanduser().absolute()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _resolve_explicit(
|
|
80
|
+
value: str | os.PathLike[str], environment: Mapping[str, str]
|
|
81
|
+
) -> Path:
|
|
82
|
+
text = os.fspath(value)
|
|
83
|
+
if os.sep not in text and (os.altsep is None or os.altsep not in text):
|
|
84
|
+
found = shutil.which(text, path=environment.get("PATH"))
|
|
85
|
+
if found:
|
|
86
|
+
return _absolute(found)
|
|
87
|
+
return _absolute(text)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _environment_python(name: str, environment: Mapping[str, str]) -> Path | None:
|
|
91
|
+
raw = environment.get(name)
|
|
92
|
+
if not raw:
|
|
93
|
+
return None
|
|
94
|
+
base = _absolute(raw)
|
|
95
|
+
candidates = [_python_path(base)]
|
|
96
|
+
if name == "CONDA_PREFIX":
|
|
97
|
+
# Conda uses ``<prefix>/python.exe`` on Windows and
|
|
98
|
+
# ``<prefix>/bin/python`` on POSIX; accept either layout so a copied
|
|
99
|
+
# environment remains discoverable across platforms.
|
|
100
|
+
candidates = [base / "python.exe", base / "bin" / "python", *candidates]
|
|
101
|
+
for python in candidates:
|
|
102
|
+
if python.is_file():
|
|
103
|
+
return python
|
|
104
|
+
raise SetupError(f"active {name} environment is unavailable")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _probe_python(
|
|
108
|
+
python: Path,
|
|
109
|
+
*,
|
|
110
|
+
allow_conda: bool = False,
|
|
111
|
+
conda_prefix: Path | None = None,
|
|
112
|
+
) -> dict[str, Any]:
|
|
113
|
+
if not python.is_file():
|
|
114
|
+
raise SetupError("selected Python executable is unavailable")
|
|
115
|
+
try:
|
|
116
|
+
result = subprocess.run(
|
|
117
|
+
[
|
|
118
|
+
str(python),
|
|
119
|
+
"-c",
|
|
120
|
+
"import json,sys; print(json.dumps({'version': sys.version_info[:2], 'prefix': sys.prefix, 'base_prefix': sys.base_prefix}))",
|
|
121
|
+
],
|
|
122
|
+
capture_output=True,
|
|
123
|
+
text=True,
|
|
124
|
+
check=False,
|
|
125
|
+
timeout=10,
|
|
126
|
+
)
|
|
127
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
128
|
+
raise SetupError("selected Python executable could not be started") from None
|
|
129
|
+
if result.returncode != 0:
|
|
130
|
+
raise SetupError("selected Python executable could not be started")
|
|
131
|
+
try:
|
|
132
|
+
payload = json.loads(result.stdout.strip().splitlines()[-1])
|
|
133
|
+
version = payload["version"]
|
|
134
|
+
if tuple(version) < (3, 10):
|
|
135
|
+
raise SetupError("Python 3.10 or newer is required")
|
|
136
|
+
prefix = Path(str(payload["prefix"])).absolute()
|
|
137
|
+
base_prefix = Path(str(payload["base_prefix"])).absolute()
|
|
138
|
+
conda_root = conda_prefix.absolute() if conda_prefix is not None else None
|
|
139
|
+
# An arbitrary interpreter containing conda-meta is not enough to
|
|
140
|
+
# distinguish a project env from the global Conda base.
|
|
141
|
+
is_conda = bool(allow_conda and conda_root is not None and prefix == conda_root)
|
|
142
|
+
if prefix == base_prefix and not is_conda:
|
|
143
|
+
raise SetupError(
|
|
144
|
+
"refusing to install into a system or global Python; use an isolated environment"
|
|
145
|
+
)
|
|
146
|
+
return cast(dict[str, Any], payload)
|
|
147
|
+
except SetupError:
|
|
148
|
+
raise
|
|
149
|
+
except (IndexError, KeyError, TypeError, ValueError, json.JSONDecodeError):
|
|
150
|
+
raise SetupError(
|
|
151
|
+
"selected Python executable returned an invalid check result"
|
|
152
|
+
) from None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def resolve_target(
|
|
156
|
+
project_root: Path,
|
|
157
|
+
explicit: str | os.PathLike[str] | None = None,
|
|
158
|
+
*,
|
|
159
|
+
environment: Mapping[str, str] | None = None,
|
|
160
|
+
) -> EnvironmentTarget:
|
|
161
|
+
"""Select an existing isolated environment, or create project ``.venv``."""
|
|
162
|
+
|
|
163
|
+
env = os.environ if environment is None else environment
|
|
164
|
+
root = project_root.resolve()
|
|
165
|
+
if not root.is_dir():
|
|
166
|
+
raise SetupError("project root is unavailable")
|
|
167
|
+
if explicit is not None:
|
|
168
|
+
python = _resolve_explicit(explicit, env)
|
|
169
|
+
_probe_python(python)
|
|
170
|
+
return EnvironmentTarget(_environment_root(python), python, "--python")
|
|
171
|
+
|
|
172
|
+
for variable in ("VIRTUAL_ENV", "CONDA_PREFIX"):
|
|
173
|
+
if (
|
|
174
|
+
variable == "CONDA_PREFIX"
|
|
175
|
+
and env.get(variable)
|
|
176
|
+
and env.get("CONDA_DEFAULT_ENV", "").casefold() == "base"
|
|
177
|
+
):
|
|
178
|
+
raise SetupError(
|
|
179
|
+
"active Conda base is not a project environment; create or activate a project environment"
|
|
180
|
+
)
|
|
181
|
+
candidate = _environment_python(variable, env)
|
|
182
|
+
if candidate is not None:
|
|
183
|
+
python = candidate
|
|
184
|
+
_probe_python(
|
|
185
|
+
python,
|
|
186
|
+
allow_conda=variable == "CONDA_PREFIX",
|
|
187
|
+
conda_prefix=Path(raw).absolute()
|
|
188
|
+
if (raw := env.get(variable))
|
|
189
|
+
else None,
|
|
190
|
+
)
|
|
191
|
+
return EnvironmentTarget(_environment_root(python), python, variable)
|
|
192
|
+
|
|
193
|
+
venv = root / ".venv"
|
|
194
|
+
python = _python_path(venv)
|
|
195
|
+
if python.is_file():
|
|
196
|
+
_probe_python(python)
|
|
197
|
+
return EnvironmentTarget(venv, python, "project .venv")
|
|
198
|
+
if venv.exists():
|
|
199
|
+
raise SetupError(
|
|
200
|
+
"project .venv exists but its Python executable is unavailable"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# The caller creates this exact target, and records that fact for rollback.
|
|
204
|
+
return EnvironmentTarget(venv, python, "new project .venv", created=True)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _create_environment(target: EnvironmentTarget) -> EnvironmentTarget:
|
|
208
|
+
try:
|
|
209
|
+
target.path.parent.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
except OSError:
|
|
211
|
+
raise SetupError("could not create the project environment") from None
|
|
212
|
+
uv = shutil.which("uv")
|
|
213
|
+
try:
|
|
214
|
+
if uv:
|
|
215
|
+
result = subprocess.run(
|
|
216
|
+
[uv, "venv", str(target.path)],
|
|
217
|
+
capture_output=True,
|
|
218
|
+
text=True,
|
|
219
|
+
check=False,
|
|
220
|
+
timeout=120,
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
result = subprocess.run(
|
|
224
|
+
[sys.executable, "-m", "venv", str(target.path)],
|
|
225
|
+
capture_output=True,
|
|
226
|
+
text=True,
|
|
227
|
+
check=False,
|
|
228
|
+
timeout=120,
|
|
229
|
+
)
|
|
230
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
231
|
+
raise SetupError("could not create the project environment") from None
|
|
232
|
+
if result.returncode != 0:
|
|
233
|
+
raise SetupError("could not create the project environment")
|
|
234
|
+
python = _python_path(target.path)
|
|
235
|
+
_probe_python(python)
|
|
236
|
+
return EnvironmentTarget(target.path, python, target.source, created=True)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _cli_version() -> str:
|
|
240
|
+
try:
|
|
241
|
+
sdk = importlib.metadata.version("sf-m3")
|
|
242
|
+
cli = importlib.metadata.version("sf-m3-cli")
|
|
243
|
+
except importlib.metadata.PackageNotFoundError:
|
|
244
|
+
raise SetupError(
|
|
245
|
+
"the bundled M3 SDK version is unavailable; reinstall sf-m3-cli"
|
|
246
|
+
) from None
|
|
247
|
+
if sdk != cli:
|
|
248
|
+
raise SetupError(
|
|
249
|
+
"the bundled CLI and SDK versions do not match; reinstall sf-m3-cli"
|
|
250
|
+
)
|
|
251
|
+
return sdk
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _ready(python: Path, version: str, project_root: Path) -> bool:
|
|
255
|
+
try:
|
|
256
|
+
result = subprocess.run(
|
|
257
|
+
[str(python), "-c", _VALIDATE_SCRIPT],
|
|
258
|
+
cwd=str(project_root),
|
|
259
|
+
capture_output=True,
|
|
260
|
+
text=True,
|
|
261
|
+
check=False,
|
|
262
|
+
timeout=15,
|
|
263
|
+
)
|
|
264
|
+
payload = json.loads(result.stdout.strip().splitlines()[-1])
|
|
265
|
+
checks = payload.get("checks", {})
|
|
266
|
+
return (
|
|
267
|
+
result.returncode == 0
|
|
268
|
+
and payload.get("version") == version
|
|
269
|
+
and all(
|
|
270
|
+
checks.get(name)
|
|
271
|
+
for name in (
|
|
272
|
+
"pytest",
|
|
273
|
+
"m3",
|
|
274
|
+
"m3.pytest_plugin",
|
|
275
|
+
"openai",
|
|
276
|
+
"SQLiteExecutionStore",
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
except (
|
|
281
|
+
OSError,
|
|
282
|
+
subprocess.TimeoutExpired,
|
|
283
|
+
IndexError,
|
|
284
|
+
TypeError,
|
|
285
|
+
ValueError,
|
|
286
|
+
json.JSONDecodeError,
|
|
287
|
+
):
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _install_sdk(target: EnvironmentTarget, version: str) -> str:
|
|
292
|
+
requirement = f"sf-m3[pytest,storage,judge]=={version}"
|
|
293
|
+
uv = shutil.which("uv")
|
|
294
|
+
command = (
|
|
295
|
+
[uv, "pip", "install", "--python", str(target.python), requirement]
|
|
296
|
+
if uv
|
|
297
|
+
else [
|
|
298
|
+
str(target.python),
|
|
299
|
+
"-m",
|
|
300
|
+
"pip",
|
|
301
|
+
"install",
|
|
302
|
+
"--disable-pip-version-check",
|
|
303
|
+
requirement,
|
|
304
|
+
]
|
|
305
|
+
)
|
|
306
|
+
try:
|
|
307
|
+
result = subprocess.run(
|
|
308
|
+
command, capture_output=True, text=True, check=False, timeout=300
|
|
309
|
+
)
|
|
310
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
311
|
+
raise SetupError("could not install the matching M3 SDK from PyPI") from None
|
|
312
|
+
if result.returncode != 0:
|
|
313
|
+
raise SetupError("could not install the matching M3 SDK from PyPI")
|
|
314
|
+
return "uv" if uv else "venv/pip"
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def run(args: Any) -> int:
|
|
318
|
+
root = (args.project_root or Path.cwd()).resolve()
|
|
319
|
+
version = _cli_version()
|
|
320
|
+
target = resolve_target(root, args.python)
|
|
321
|
+
print(f"Installing M3 SDK {version}")
|
|
322
|
+
print(f"Project environment: {target.path} ({target.source})")
|
|
323
|
+
if not target.created and _ready(target.python, version, root):
|
|
324
|
+
print(f"Project environment ready: {target.path}")
|
|
325
|
+
print(f"M3 SDK: {version}")
|
|
326
|
+
print("Installer: skipped; environment is already ready")
|
|
327
|
+
print("Next:\n m3 doctor\n m3 test --ui -- -q")
|
|
328
|
+
return 0
|
|
329
|
+
|
|
330
|
+
created = target.created
|
|
331
|
+
try:
|
|
332
|
+
if created:
|
|
333
|
+
print("[1/3] Creating project environment")
|
|
334
|
+
target = _create_environment(target)
|
|
335
|
+
else:
|
|
336
|
+
print("[1/3] Selecting project environment")
|
|
337
|
+
print("[2/3] Installing matching SDK from PyPI")
|
|
338
|
+
installer = _install_sdk(target, version)
|
|
339
|
+
print("[3/3] Verifying project environment")
|
|
340
|
+
if not _ready(target.python, version, root):
|
|
341
|
+
raise SetupError("project environment did not pass the M3 SDK checks")
|
|
342
|
+
except SetupError:
|
|
343
|
+
if created and target.path.exists():
|
|
344
|
+
shutil.rmtree(target.path, ignore_errors=True)
|
|
345
|
+
raise
|
|
346
|
+
print(f"Project environment ready: {target.path}")
|
|
347
|
+
print(f"M3 SDK: {version}")
|
|
348
|
+
print(f"Installer: {installer}")
|
|
349
|
+
print("Next:\n m3 doctor\n m3 test --ui -- -q")
|
|
350
|
+
return 0
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
__all__ = ["EnvironmentTarget", "SetupError", "resolve_target", "run"]
|