inis 0.1.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.
inis/__init__.py ADDED
@@ -0,0 +1,63 @@
1
+ """inis.run Python SDK — thin wrapper over the session HTTP API."""
2
+
3
+ from inis.async_client import (
4
+ AsyncClient,
5
+ AsyncInisClient,
6
+ AsyncSession,
7
+ )
8
+ from inis.client import (
9
+ ArtifactFile,
10
+ ArtifactInfo,
11
+ BatchExecResult,
12
+ Capacity,
13
+ CapacityLimits,
14
+ CheckpointInfo,
15
+ Client,
16
+ ConnectorInfo,
17
+ ExposeResult,
18
+ ExecResult,
19
+ ExecStreamEvent,
20
+ ForkResult,
21
+ InisClient,
22
+ InisError,
23
+ ProcessInfo,
24
+ ProcessLogEvent,
25
+ ProcessLogs,
26
+ RegistryCredentialInfo,
27
+ Session,
28
+ SessionInfo,
29
+ TemplateInfo,
30
+ )
31
+ from inis.interpreter_common import InterpreterResult
32
+ from inis.pty import AsyncPTY, PTY, PTYExitInfo
33
+
34
+ __all__ = [
35
+ "ArtifactFile",
36
+ "ArtifactInfo",
37
+ "AsyncClient",
38
+ "AsyncInisClient",
39
+ "AsyncPTY",
40
+ "AsyncSession",
41
+ "BatchExecResult",
42
+ "Capacity",
43
+ "CapacityLimits",
44
+ "CheckpointInfo",
45
+ "Client",
46
+ "ConnectorInfo",
47
+ "ExposeResult",
48
+ "ExecResult",
49
+ "ExecStreamEvent",
50
+ "ForkResult",
51
+ "InisClient",
52
+ "InisError",
53
+ "InterpreterResult",
54
+ "ProcessInfo",
55
+ "ProcessLogEvent",
56
+ "ProcessLogs",
57
+ "PTY",
58
+ "PTYExitInfo",
59
+ "RegistryCredentialInfo",
60
+ "Session",
61
+ "SessionInfo",
62
+ "TemplateInfo",
63
+ ]
inis/_kernel_script.py ADDED
@@ -0,0 +1,279 @@
1
+ # inis.run code-interpreter kernel.
2
+ #
3
+ # Ships inside the inis SDK packages (Python + TypeScript — see
4
+ # sdk/typescript/src/kernel_script.ts, generated from this file by
5
+ # scripts/gen-kernel-ts.py, which must be re-run after any edit here) and is
6
+ # uploaded into a session's filesystem on the first run_code() call, then
7
+ # started as a long-lived background process via start_process(). It has no
8
+ # dependency on the inis SDK itself and no guest-agent changes are needed:
9
+ # stdlib only for the core loop. matplotlib/pandas are used opportunistically
10
+ # if already importable in the target environment (e.g. the official
11
+ # "data-science" template) to produce richer results, but their absence
12
+ # never breaks plain code execution.
13
+ #
14
+ # Protocol: polls a requests directory for atomically renamed *.json request
15
+ # files ({"id", "code"}) and executes each against a persistent globals()
16
+ # dict shared across calls (the "context"), then atomically writes a *.json
17
+ # result envelope ({"id", "results": [...]}) to a results directory. The SDK
18
+ # writes request files via write-then-rename (write a temp file, then a
19
+ # separate exec() renames it into place) so this loop never observes a
20
+ # partially-written request; it does the same on the way out (write a .tmp
21
+ # file, then os.rename) so the SDK's shell-side poll loop never observes a
22
+ # partially-written result.
23
+ #
24
+ # Each result item has a "type" of "text", "image", "table", "json", or
25
+ # "error". restart_context() in the SDK kills this process outright (the
26
+ # simplest way to guarantee a fully fresh context) rather than messaging it,
27
+ # so this script has no reset protocol of its own.
28
+ import ast
29
+ import contextlib
30
+ import io
31
+ import json
32
+ import os
33
+ import sys
34
+ import time
35
+ import traceback
36
+
37
+ ROOT = sys.argv[1] if len(sys.argv) > 1 else "/workspace/.inis_kernel"
38
+ REQ_DIR = os.path.join(ROOT, "requests")
39
+ RES_DIR = os.path.join(ROOT, "results")
40
+ ART_DIR = os.path.join(ROOT, "artifacts")
41
+
42
+ # These bound how much of a rich result (a DataFrame's rows, or
43
+ # a value's repr()) the kernel puts in one result envelope, distinct from
44
+ # the transport's own limits. The envelope is read back over the same
45
+ # buffered-exec channel as any other run_code() stdout, so it inherits that
46
+ # path's cap (protocol.MaxStdoutBytes, 4 MiB — see internal/protocol/
47
+ # protocol.go) as the real ceiling; these were previously 200/20000 with no
48
+ # stated relationship to that budget. Raised 5x — still comfortably inside
49
+ # the 4 MiB budget for realistic tables/reprs, while an agent asking for the
50
+ # last value no longer needs a second call just to see past row 200.
51
+ # truncated/row_count are still reported so a caller that needs more than
52
+ # this can tell and can re-query with an explicit .head()/slice instead of
53
+ # silently getting a partial view.
54
+ MAX_TABLE_ROWS = 1000
55
+ MAX_REPR_LEN = 100000
56
+
57
+ _globals = {"__name__": "__main__"}
58
+
59
+
60
+ def _ensure_dirs():
61
+ for d in (REQ_DIR, RES_DIR, ART_DIR):
62
+ os.makedirs(d, exist_ok=True)
63
+
64
+
65
+ def _try_matplotlib():
66
+ try:
67
+ import matplotlib
68
+
69
+ matplotlib.use("Agg")
70
+ import matplotlib.pyplot as plt
71
+
72
+ return plt
73
+ except Exception:
74
+ return None
75
+
76
+
77
+ def _try_pandas():
78
+ try:
79
+ import pandas as pd
80
+
81
+ return pd
82
+ except Exception:
83
+ return None
84
+
85
+
86
+ def _json_safe(value):
87
+ try:
88
+ json.dumps(value)
89
+ return value
90
+ except Exception:
91
+ return repr(value)
92
+
93
+
94
+ def _table_result(df):
95
+ try:
96
+ truncated = len(df) > MAX_TABLE_ROWS
97
+ view = df.head(MAX_TABLE_ROWS)
98
+ rows = json.loads(view.to_json(orient="records", date_format="iso"))
99
+ return {
100
+ "type": "table",
101
+ "columns": [str(c) for c in view.columns],
102
+ "rows": rows,
103
+ "row_count": int(len(df)),
104
+ "truncated": truncated,
105
+ }
106
+ except Exception:
107
+ return None
108
+
109
+
110
+ def capture_figures(req_id, plt):
111
+ """Save every open matplotlib figure with at least one axes to ART_DIR
112
+ as a PNG, close it, and return one "image" result dict per figure."""
113
+ results = []
114
+ if plt is None:
115
+ return results
116
+ for i, num in enumerate(list(plt.get_fignums())):
117
+ fig = plt.figure(num)
118
+ try:
119
+ if not fig.axes:
120
+ continue
121
+ path = os.path.join(ART_DIR, "%s_%d.png" % (req_id, i))
122
+ fig.savefig(path, format="png", bbox_inches="tight")
123
+ results.append(
124
+ {
125
+ "type": "image",
126
+ "format": "png",
127
+ "path": path,
128
+ "size": os.path.getsize(path),
129
+ }
130
+ )
131
+ finally:
132
+ plt.close(fig)
133
+ return results
134
+
135
+
136
+ def run_cell(code, req_id, globals_dict=None):
137
+ """Execute one cell of code against globals_dict (defaults to the
138
+ persistent module-level context), returning a list of result dicts.
139
+ Pure function of (code, globals_dict) plus the optional-dependency
140
+ probes above — no filesystem polling, so it's directly unit-testable."""
141
+ g = _globals if globals_dict is None else globals_dict
142
+ results = []
143
+ stdout_buf, stderr_buf = io.StringIO(), io.StringIO()
144
+ plt = _try_matplotlib()
145
+ pd = _try_pandas()
146
+ last_value = None
147
+ has_last_value = False
148
+
149
+ try:
150
+ parsed = ast.parse(code, mode="exec")
151
+ last_expr = None
152
+ if parsed.body and isinstance(parsed.body[-1], ast.Expr):
153
+ last_expr = parsed.body.pop()
154
+ with contextlib.redirect_stdout(stdout_buf), contextlib.redirect_stderr(stderr_buf):
155
+ if parsed.body:
156
+ exec(compile(parsed, "<inis-run_code>", "exec"), g)
157
+ if last_expr is not None:
158
+ value = eval(
159
+ compile(ast.Expression(last_expr.value), "<inis-run_code>", "eval"),
160
+ g,
161
+ )
162
+ if value is not None:
163
+ last_value = value
164
+ has_last_value = True
165
+ except Exception as exc:
166
+ # Deliberately not BaseException: SystemExit/KeyboardInterrupt (e.g.
167
+ # user code calling exit()) should kill this process rather than be
168
+ # reported as a catchable "error" result — the same call-exit-to-
169
+ # restart-the-kernel convention Jupyter uses. main()'s caller lets
170
+ # SystemExit propagate; the next run_code() transparently restarts
171
+ # the (now-dead) kernel with a fresh context, same as any other
172
+ # kernel death.
173
+ tb = traceback.format_exc()
174
+ out, err = stdout_buf.getvalue(), stderr_buf.getvalue()
175
+ if out:
176
+ results.append({"type": "text", "stream": "stdout", "text": out})
177
+ if err:
178
+ results.append({"type": "text", "stream": "stderr", "text": err})
179
+ results.append(
180
+ {
181
+ "type": "error",
182
+ "ename": type(exc).__name__,
183
+ "evalue": str(exc),
184
+ "traceback": tb,
185
+ }
186
+ )
187
+ results.extend(capture_figures(req_id, plt))
188
+ return results
189
+
190
+ out, err = stdout_buf.getvalue(), stderr_buf.getvalue()
191
+ if out:
192
+ results.append({"type": "text", "stream": "stdout", "text": out})
193
+ if err:
194
+ results.append({"type": "text", "stream": "stderr", "text": err})
195
+
196
+ results.extend(capture_figures(req_id, plt))
197
+
198
+ if has_last_value:
199
+ handled = False
200
+ if pd is not None:
201
+ try:
202
+ if isinstance(last_value, pd.DataFrame):
203
+ t = _table_result(last_value)
204
+ if t is not None:
205
+ results.append(t)
206
+ handled = True
207
+ elif isinstance(last_value, pd.Series):
208
+ t = _table_result(last_value.to_frame())
209
+ if t is not None:
210
+ results.append(t)
211
+ handled = True
212
+ except Exception:
213
+ pass
214
+ if not handled:
215
+ if isinstance(last_value, (dict, list, tuple)):
216
+ results.append({"type": "json", "json": _json_safe(last_value)})
217
+ else:
218
+ r = repr(last_value)
219
+ if len(r) > MAX_REPR_LEN:
220
+ r = r[:MAX_REPR_LEN] + "... (truncated)"
221
+ results.append({"type": "text", "stream": "result", "text": r})
222
+
223
+ return results
224
+
225
+
226
+ def _write_result(req_id, results):
227
+ payload = {"id": req_id, "results": results}
228
+ tmp_path = os.path.join(RES_DIR, ".%s.tmp" % req_id)
229
+ final_path = os.path.join(RES_DIR, "%s.json" % req_id)
230
+ with open(tmp_path, "w") as f:
231
+ json.dump(payload, f)
232
+ os.rename(tmp_path, final_path)
233
+
234
+
235
+ def main():
236
+ _ensure_dirs()
237
+ seen = set()
238
+ while True:
239
+ try:
240
+ names = sorted(os.listdir(REQ_DIR))
241
+ except FileNotFoundError:
242
+ names = []
243
+ for name in names:
244
+ if not name.endswith(".json") or name.startswith(".") or name in seen:
245
+ continue
246
+ seen.add(name)
247
+ path = os.path.join(REQ_DIR, name)
248
+ try:
249
+ with open(path, "r") as f:
250
+ req = json.load(f)
251
+ except Exception:
252
+ continue
253
+ finally:
254
+ try:
255
+ os.remove(path)
256
+ except OSError:
257
+ pass
258
+
259
+ req_id = req.get("id") or name[:-5]
260
+ code = req.get("code", "")
261
+ try:
262
+ results = run_cell(code, req_id)
263
+ except SystemExit:
264
+ raise
265
+ except Exception:
266
+ results = [
267
+ {
268
+ "type": "error",
269
+ "ename": "KernelError",
270
+ "evalue": "kernel failed to execute request",
271
+ "traceback": traceback.format_exc(),
272
+ }
273
+ ]
274
+ _write_result(req_id, results)
275
+ time.sleep(0.05)
276
+
277
+
278
+ if __name__ == "__main__":
279
+ main()