inis 0.1.0__tar.gz
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-0.1.0/PKG-INFO +80 -0
- inis-0.1.0/README.md +60 -0
- inis-0.1.0/inis/__init__.py +63 -0
- inis-0.1.0/inis/_kernel_script.py +279 -0
- inis-0.1.0/inis/async_client.py +1751 -0
- inis-0.1.0/inis/client.py +2672 -0
- inis-0.1.0/inis/client_stub.py +1837 -0
- inis-0.1.0/inis/interpreter_common.py +177 -0
- inis-0.1.0/inis/pty.py +386 -0
- inis-0.1.0/inis.egg-info/PKG-INFO +80 -0
- inis-0.1.0/inis.egg-info/SOURCES.txt +24 -0
- inis-0.1.0/inis.egg-info/dependency_links.txt +1 -0
- inis-0.1.0/inis.egg-info/requires.txt +7 -0
- inis-0.1.0/inis.egg-info/top_level.txt +1 -0
- inis-0.1.0/pyproject.toml +41 -0
- inis-0.1.0/setup.cfg +4 -0
- inis-0.1.0/tests/test_async_client.py +455 -0
- inis-0.1.0/tests/test_async_session.py +611 -0
- inis-0.1.0/tests/test_async_session_interpreter.py +139 -0
- inis-0.1.0/tests/test_client.py +645 -0
- inis-0.1.0/tests/test_errors.py +179 -0
- inis-0.1.0/tests/test_kernel_script.py +175 -0
- inis-0.1.0/tests/test_openapi_sync.py +227 -0
- inis-0.1.0/tests/test_pty.py +272 -0
- inis-0.1.0/tests/test_session.py +881 -0
- inis-0.1.0/tests/test_session_interpreter.py +150 -0
inis-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: inis
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for inis.run agent sandboxes
|
|
5
|
+
Author-email: "inis.run" <ops@inis.run>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://inis.run
|
|
8
|
+
Project-URL: Documentation, https://docs.inis.run
|
|
9
|
+
Project-URL: Repository, https://github.com/inis-run/sdk/tree/main/python
|
|
10
|
+
Project-URL: Issues, https://github.com/inis-run/sdk/issues
|
|
11
|
+
Keywords: inis,sandbox,agents,llm
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: httpx>=0.27
|
|
15
|
+
Requires-Dist: websockets>=13
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
19
|
+
Requires-Dist: pyyaml>=6; extra == "dev"
|
|
20
|
+
|
|
21
|
+
# inis.run Python SDK
|
|
22
|
+
|
|
23
|
+
Run untrusted or AI-generated code in its own isolated VM.
|
|
24
|
+
|
|
25
|
+
[](https://pypi.org/project/inis/)
|
|
26
|
+
[](https://github.com/inis-run/sdk/blob/main/LICENSE)
|
|
27
|
+
[](https://pypi.org/project/inis/)
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install inis
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from inis import Client
|
|
39
|
+
|
|
40
|
+
client = Client() # reads INIS_API_KEY (and INIS_BASE_URL) from the environment
|
|
41
|
+
|
|
42
|
+
with client.session() as session:
|
|
43
|
+
session.exec(["pip", "install", "httpx", "-q"])
|
|
44
|
+
session.files.write("/workspace/out.txt", "hello")
|
|
45
|
+
print(session.files.read("/workspace/out.txt"))
|
|
46
|
+
|
|
47
|
+
preview = session.expose(8765)
|
|
48
|
+
print(preview.preview_url)
|
|
49
|
+
|
|
50
|
+
# One-shot execution (throwaway session, no cleanup needed)
|
|
51
|
+
result = client.execute(language="python", code="print(42)")
|
|
52
|
+
print(result.stdout)
|
|
53
|
+
|
|
54
|
+
# Re-attach to a session created elsewhere, by ID
|
|
55
|
+
session = client.sessions.attach("ses_123")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`client.session()` returns a context manager that destroys the session on
|
|
59
|
+
exit. Use `client.sessions.create(...)` directly if you want to manage its
|
|
60
|
+
lifetime yourself, and `client.sessions.attach(id)` to reconnect to a
|
|
61
|
+
session you don't own — that call never destroys it.
|
|
62
|
+
|
|
63
|
+
## Timeouts
|
|
64
|
+
|
|
65
|
+
`Client(timeout=...)` and `Session(timeout=...)` are **seconds** (httpx
|
|
66
|
+
convention). The TypeScript SDK's `timeoutMs` is milliseconds (JS
|
|
67
|
+
convention) — a deliberate per-language idiom, not drift.
|
|
68
|
+
|
|
69
|
+
## Links
|
|
70
|
+
|
|
71
|
+
- Docs: [docs.inis.run](https://docs.inis.run)
|
|
72
|
+
- Quickstart: [docs.inis.run/docs/quickstart](https://docs.inis.run/docs/quickstart)
|
|
73
|
+
- Source: [github.com/inis-run/sdk/tree/main/python](https://github.com/inis-run/sdk/tree/main/python)
|
|
74
|
+
- Homepage: [inis.run](https://inis.run)
|
|
75
|
+
|
|
76
|
+
Built and run in the EU. Your code and data never leave Europe.
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT — see [LICENSE](https://github.com/inis-run/sdk/blob/main/LICENSE).
|
inis-0.1.0/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# inis.run Python SDK
|
|
2
|
+
|
|
3
|
+
Run untrusted or AI-generated code in its own isolated VM.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/inis/)
|
|
6
|
+
[](https://github.com/inis-run/sdk/blob/main/LICENSE)
|
|
7
|
+
[](https://pypi.org/project/inis/)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install inis
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from inis import Client
|
|
19
|
+
|
|
20
|
+
client = Client() # reads INIS_API_KEY (and INIS_BASE_URL) from the environment
|
|
21
|
+
|
|
22
|
+
with client.session() as session:
|
|
23
|
+
session.exec(["pip", "install", "httpx", "-q"])
|
|
24
|
+
session.files.write("/workspace/out.txt", "hello")
|
|
25
|
+
print(session.files.read("/workspace/out.txt"))
|
|
26
|
+
|
|
27
|
+
preview = session.expose(8765)
|
|
28
|
+
print(preview.preview_url)
|
|
29
|
+
|
|
30
|
+
# One-shot execution (throwaway session, no cleanup needed)
|
|
31
|
+
result = client.execute(language="python", code="print(42)")
|
|
32
|
+
print(result.stdout)
|
|
33
|
+
|
|
34
|
+
# Re-attach to a session created elsewhere, by ID
|
|
35
|
+
session = client.sessions.attach("ses_123")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`client.session()` returns a context manager that destroys the session on
|
|
39
|
+
exit. Use `client.sessions.create(...)` directly if you want to manage its
|
|
40
|
+
lifetime yourself, and `client.sessions.attach(id)` to reconnect to a
|
|
41
|
+
session you don't own — that call never destroys it.
|
|
42
|
+
|
|
43
|
+
## Timeouts
|
|
44
|
+
|
|
45
|
+
`Client(timeout=...)` and `Session(timeout=...)` are **seconds** (httpx
|
|
46
|
+
convention). The TypeScript SDK's `timeoutMs` is milliseconds (JS
|
|
47
|
+
convention) — a deliberate per-language idiom, not drift.
|
|
48
|
+
|
|
49
|
+
## Links
|
|
50
|
+
|
|
51
|
+
- Docs: [docs.inis.run](https://docs.inis.run)
|
|
52
|
+
- Quickstart: [docs.inis.run/docs/quickstart](https://docs.inis.run/docs/quickstart)
|
|
53
|
+
- Source: [github.com/inis-run/sdk/tree/main/python](https://github.com/inis-run/sdk/tree/main/python)
|
|
54
|
+
- Homepage: [inis.run](https://inis.run)
|
|
55
|
+
|
|
56
|
+
Built and run in the EU. Your code and data never leave Europe.
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT — see [LICENSE](https://github.com/inis-run/sdk/blob/main/LICENSE).
|
|
@@ -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
|
+
]
|
|
@@ -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()
|