m8flow 1.1.6 → 1.1.8

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.
@@ -2,6 +2,8 @@ from fastapi import APIRouter, HTTPException, Request, UploadFile, File
2
2
  from pydantic import BaseModel
3
3
  import os
4
4
  import shutil
5
+ import sys
6
+ import subprocess
5
7
 
6
8
  from core.parser import parse_node_code
7
9
  from templates import TEMPLATES
@@ -13,6 +15,42 @@ class ParseRequest(BaseModel):
13
15
  code: str
14
16
 
15
17
 
18
+ class InstallRequest(BaseModel):
19
+ package: str # e.g. "lightgbm" or "xgboost==2.0.3"
20
+
21
+
22
+ @router.post("/install")
23
+ def install_package(req: InstallRequest):
24
+ """
25
+ Install a Python package into the currently running venv using pip.
26
+ Returns stdout/stderr so the frontend can display real output.
27
+ """
28
+ package = req.package.strip()
29
+
30
+ # Basic safety: reject obviously dangerous strings
31
+ forbidden = (";", "&", "|", "`", "$", ">", "<", "\n", "\r")
32
+ if not package or any(c in package for c in forbidden):
33
+ raise HTTPException(status_code=422, detail="Invalid package name.")
34
+
35
+ result = subprocess.run(
36
+ [sys.executable, "-m", "pip", "install", package,
37
+ "--disable-pip-version-check"],
38
+ capture_output=True,
39
+ text=True,
40
+ timeout=180,
41
+ )
42
+
43
+ output = (result.stdout + result.stderr).strip()
44
+
45
+ if result.returncode != 0:
46
+ raise HTTPException(
47
+ status_code=400,
48
+ detail=output or f"pip install failed (exit {result.returncode})",
49
+ )
50
+
51
+ return {"ok": True, "output": output, "package": package}
52
+
53
+
16
54
  @router.post("/parse")
17
55
  def parse(req: ParseRequest):
18
56
  """Parse Python code and return the derived NodeSchema (inputs, outputs, errors)."""
@@ -92,6 +130,22 @@ async def generate_node_code_route(http_request: Request, req: GenerateCodeReque
92
130
  raise HTTPException(status_code=500, detail=f"{type(exc).__name__}: {exc}")
93
131
 
94
132
 
133
+ @router.get("/packages")
134
+ def list_packages():
135
+ """Return all packages currently installed in the running Python environment."""
136
+ result = subprocess.run(
137
+ [sys.executable, "-m", "pip", "list", "--format=json", "--disable-pip-version-check"],
138
+ capture_output=True,
139
+ text=True,
140
+ timeout=30,
141
+ )
142
+ if result.returncode != 0:
143
+ raise HTTPException(status_code=500, detail="Failed to list packages.")
144
+ import json as _json
145
+ packages = _json.loads(result.stdout or "[]")
146
+ return {"packages": packages}
147
+
148
+
95
149
  @router.get("/templates")
96
150
  def list_templates():
97
151
  """Return every prebuilt template with its pre-parsed schema attached."""
@@ -17,14 +17,14 @@ from typing import NamedTuple
17
17
  # ── Blocklists ────────────────────────────────────────────────────────────────
18
18
 
19
19
  BLOCKED_IMPORTS = frozenset({
20
- # System / process execution
21
- "os", "sys", "subprocess", "shutil", "signal",
20
+ # System / process execution (os is allowed with restrictions below)
21
+ "sys", "subprocess", "shutil", "signal",
22
22
  # Networking
23
23
  "socket", "http", "urllib", "requests", "httpx", "ftplib", "smtplib",
24
24
  # Code injection
25
25
  "importlib", "builtins", "ctypes", "cffi",
26
- # Filesystem I/O (allow pandas read_csv via allowlist only)
27
- "pathlib", "glob", "tempfile",
26
+ # Filesystem wildcards
27
+ "glob",
28
28
  # Threading / multiprocessing
29
29
  "threading", "multiprocessing", "concurrent",
30
30
  # Package management
@@ -32,19 +32,25 @@ BLOCKED_IMPORTS = frozenset({
32
32
  })
33
33
 
34
34
  BLOCKED_BUILTINS = frozenset({
35
- "eval", "exec", "compile", "__import__", "open", "input",
35
+ "eval", "exec", "compile", "__import__", "input",
36
36
  "breakpoint", "vars", "dir",
37
+ # os-level execution even if os is imported
38
+ "system", "popen", "execv", "execve", "execvp",
37
39
  })
38
40
 
39
41
  ALLOWED_IMPORTS = frozenset({
40
42
  # Scientific computing
41
43
  "numpy", "pandas", "scipy", "sklearn", "xgboost", "lightgbm",
42
44
  "statsmodels", "imblearn",
45
+ # Model persistence
46
+ "joblib", "pickle",
43
47
  # Plotting
44
48
  "matplotlib", "seaborn", "plotly", "mpl_toolkits",
45
49
  # Standard safe libs
46
50
  "math", "statistics", "itertools", "functools", "collections",
47
51
  "json", "re", "datetime", "typing",
52
+ # Filesystem (path operations, needed for model saving)
53
+ "os", "io", "pathlib", "tempfile",
48
54
  })
49
55
 
50
56
 
@@ -2769,6 +2769,56 @@ def run(
2769
2769
  '''
2770
2770
 
2771
2771
 
2772
+ MODEL_SAVER = '''
2773
+ def run(model, file_path: str = "saved_model.pkl") -> dict:
2774
+ """Save a trained model to disk using joblib (faster + handles numpy arrays better than pickle).
2775
+
2776
+ file_path : path where the model file will be written.
2777
+ Supports any extension (.pkl / .joblib / .model).
2778
+ Returns the absolute path so downstream nodes can reference it.
2779
+ """
2780
+ import joblib
2781
+ import os
2782
+
2783
+ abs_path = os.path.abspath(file_path)
2784
+ parent = os.path.dirname(abs_path)
2785
+ if parent:
2786
+ os.makedirs(parent, exist_ok=True)
2787
+
2788
+ joblib.dump(model, abs_path)
2789
+ size_kb = round(os.path.getsize(abs_path) / 1024, 1)
2790
+
2791
+ return {
2792
+ "path": abs_path,
2793
+ "size_kb": size_kb,
2794
+ "status": "saved",
2795
+ }
2796
+ '''
2797
+
2798
+ MODEL_LOADER = '''
2799
+ def run(file_path: str = "saved_model.pkl") -> dict:
2800
+ """Load a previously saved model from disk using joblib.
2801
+
2802
+ file_path : path to the model file produced by Model Saver.
2803
+ Returns the model object ready for inference or further training.
2804
+ """
2805
+ import joblib
2806
+ import os
2807
+
2808
+ abs_path = os.path.abspath(file_path)
2809
+ if not os.path.exists(abs_path):
2810
+ raise FileNotFoundError(f"Model file not found: {abs_path}")
2811
+
2812
+ model = joblib.load(abs_path)
2813
+
2814
+ return {
2815
+ "model": model,
2816
+ "path": abs_path,
2817
+ "status": "loaded",
2818
+ }
2819
+ '''
2820
+
2821
+
2772
2822
  TEMPLATES: list[dict] = [
2773
2823
  # Data
2774
2824
  {"id": "csv_loader", "label": "CSV Loader", "category": "Data", "code": CSV_LOADER},
@@ -2892,6 +2942,10 @@ TEMPLATES: list[dict] = [
2892
2942
  {"id": "calibration_curve_data", "label": "Calibration Curve", "category": "Evaluation", "code": CALIBRATION_CURVE_DATA},
2893
2943
  {"id": "learning_curve_analyzer", "label": "Learning Curve Analyzer", "category": "Evaluation", "code": LEARNING_CURVE_ANALYZER},
2894
2944
  {"id": "cost_benefit_matrix", "label": "Cost-Benefit Matrix", "category": "Evaluation", "code": COST_BENEFIT_MATRIX},
2945
+
2946
+ # ── Model Persistence ────────────────────────────────────────────────────
2947
+ {"id": "model_saver", "label": "Model Saver", "category": "Data", "code": MODEL_SAVER},
2948
+ {"id": "model_loader", "label": "Model Loader", "category": "Data", "code": MODEL_LOADER},
2895
2949
  ]
2896
2950
 
2897
2951