decpython 0.1.0__tar.gz → 1.0.1__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.
- decpython-1.0.1/PKG-INFO +92 -0
- decpython-1.0.1/README.md +67 -0
- {decpython-0.1.0 → decpython-1.0.1}/decpython/__init__.py +3 -2
- decpython-1.0.1/decpython/client.py +56 -0
- {decpython-0.1.0 → decpython-1.0.1}/decpython/core.py +26 -8
- {decpython-0.1.0 → decpython-1.0.1}/decpython/gui/server.py +88 -9
- decpython-1.0.1/decpython.egg-info/PKG-INFO +92 -0
- {decpython-0.1.0 → decpython-1.0.1}/decpython.egg-info/SOURCES.txt +3 -1
- {decpython-0.1.0 → decpython-1.0.1}/pyproject.toml +1 -1
- {decpython-0.1.0 → decpython-1.0.1}/setup.py +1 -1
- decpython-1.0.1/tests/test_decpython.py +109 -0
- decpython-0.1.0/PKG-INFO +0 -55
- decpython-0.1.0/README.md +0 -30
- decpython-0.1.0/decpython.egg-info/PKG-INFO +0 -55
- {decpython-0.1.0 → decpython-1.0.1}/decpython/gui/__init__.py +0 -0
- {decpython-0.1.0 → decpython-1.0.1}/decpython.egg-info/dependency_links.txt +0 -0
- {decpython-0.1.0 → decpython-1.0.1}/decpython.egg-info/requires.txt +0 -0
- {decpython-0.1.0 → decpython-1.0.1}/decpython.egg-info/top_level.txt +0 -0
- {decpython-0.1.0 → decpython-1.0.1}/setup.cfg +0 -0
decpython-1.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: decpython
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: 跨平台、支持 Web 通信的 Python 终端工具
|
|
5
|
+
Home-page: https://github.com/your-username/decpython_maskter
|
|
6
|
+
Author: decpython
|
|
7
|
+
Author-email: decrule@outlook.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.9.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
21
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
22
|
+
Dynamic: author-email
|
|
23
|
+
Dynamic: home-page
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
|
|
26
|
+
# DecPython
|
|
27
|
+
|
|
28
|
+
Run Python in a subprocess from your code and get string results, or open a Jupyter-style web terminal. Optional HTTP API with host/port/secret for remote execution. Cross-platform, stdlib only.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install -e .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from decpython import DecPython
|
|
40
|
+
|
|
41
|
+
dp = DecPython(gui=False, python='python')
|
|
42
|
+
result = dp.send('a = 1\nb = 2\na + b') # -> '3'
|
|
43
|
+
dp.close()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
With a web UI: `gui=True`. Code and results from `send()` appear in the browser (Shift+Enter or Run).
|
|
47
|
+
|
|
48
|
+
## Host, port, secret (HTTP API)
|
|
49
|
+
|
|
50
|
+
When you pass `port` (or use `gui=True`), a web server is started. **`DecPython(...)` blocks until the server is bound and ready**, so you can call `send()` or the standalone `send()` immediately after.
|
|
51
|
+
|
|
52
|
+
- **host** — Bind address (default `'localhost'`). Use `'0.0.0.0'` to accept non-local requests.
|
|
53
|
+
- **port** — Port number, or `None` / `0` for auto.
|
|
54
|
+
- **secret** — If set, remote clients must send the same secret (timestamp + HMAC) to POST `/exec`. If `None`, no auth; client should use `send(..., secret=None)`.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# Server with no auth
|
|
58
|
+
dp = DecPython(gui=False, host='127.0.0.1', port=0, secret=None)
|
|
59
|
+
port = dp.port # ready after init
|
|
60
|
+
dp.close()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Standalone `send()` (remote)
|
|
64
|
+
|
|
65
|
+
Send a command to a running DecPython server without holding the instance:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from decpython import DecPython, send
|
|
69
|
+
|
|
70
|
+
dp = DecPython(gui=False, host='127.0.0.1', port=0, secret='key')
|
|
71
|
+
out = send('1 + 2', host='127.0.0.1', port=dp.port, secret='key')
|
|
72
|
+
# out == {'code': 200, 'data': '3', 'msg': ''}
|
|
73
|
+
dp.close()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- **Success:** `{'code': 200, 'data': '<result>', 'msg': ''}`
|
|
77
|
+
- **Error:** `{'code': -1, 'data': '', 'msg': '<error>'}`
|
|
78
|
+
Use `secret=None` when the server was started with `secret=None`.
|
|
79
|
+
|
|
80
|
+
See `examples/example_remote.py` for more.
|
|
81
|
+
|
|
82
|
+
## API
|
|
83
|
+
|
|
84
|
+
- **`DecPython(gui=..., python='python', host='localhost', port=None, secret=None)`** — Blocks until the web server (if any) is bound. `port` / `gui` control whether the server starts.
|
|
85
|
+
- **`dp.send(code)`** — Run code in the subprocess; returns the last expression as a string (or the error message).
|
|
86
|
+
- **`dp.port`** — Bound port (None if no server).
|
|
87
|
+
- **`dp.close()`** — Stop the subprocess and web server.
|
|
88
|
+
- **`send(command, host='localhost', port=..., secret=None)`** — POST to a running server; returns the dict above.
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# DecPython
|
|
2
|
+
|
|
3
|
+
Run Python in a subprocess from your code and get string results, or open a Jupyter-style web terminal. Optional HTTP API with host/port/secret for remote execution. Cross-platform, stdlib only.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e .
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from decpython import DecPython
|
|
15
|
+
|
|
16
|
+
dp = DecPython(gui=False, python='python')
|
|
17
|
+
result = dp.send('a = 1\nb = 2\na + b') # -> '3'
|
|
18
|
+
dp.close()
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
With a web UI: `gui=True`. Code and results from `send()` appear in the browser (Shift+Enter or Run).
|
|
22
|
+
|
|
23
|
+
## Host, port, secret (HTTP API)
|
|
24
|
+
|
|
25
|
+
When you pass `port` (or use `gui=True`), a web server is started. **`DecPython(...)` blocks until the server is bound and ready**, so you can call `send()` or the standalone `send()` immediately after.
|
|
26
|
+
|
|
27
|
+
- **host** — Bind address (default `'localhost'`). Use `'0.0.0.0'` to accept non-local requests.
|
|
28
|
+
- **port** — Port number, or `None` / `0` for auto.
|
|
29
|
+
- **secret** — If set, remote clients must send the same secret (timestamp + HMAC) to POST `/exec`. If `None`, no auth; client should use `send(..., secret=None)`.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
# Server with no auth
|
|
33
|
+
dp = DecPython(gui=False, host='127.0.0.1', port=0, secret=None)
|
|
34
|
+
port = dp.port # ready after init
|
|
35
|
+
dp.close()
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Standalone `send()` (remote)
|
|
39
|
+
|
|
40
|
+
Send a command to a running DecPython server without holding the instance:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from decpython import DecPython, send
|
|
44
|
+
|
|
45
|
+
dp = DecPython(gui=False, host='127.0.0.1', port=0, secret='key')
|
|
46
|
+
out = send('1 + 2', host='127.0.0.1', port=dp.port, secret='key')
|
|
47
|
+
# out == {'code': 200, 'data': '3', 'msg': ''}
|
|
48
|
+
dp.close()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- **Success:** `{'code': 200, 'data': '<result>', 'msg': ''}`
|
|
52
|
+
- **Error:** `{'code': -1, 'data': '', 'msg': '<error>'}`
|
|
53
|
+
Use `secret=None` when the server was started with `secret=None`.
|
|
54
|
+
|
|
55
|
+
See `examples/example_remote.py` for more.
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
- **`DecPython(gui=..., python='python', host='localhost', port=None, secret=None)`** — Blocks until the web server (if any) is bound. `port` / `gui` control whether the server starts.
|
|
60
|
+
- **`dp.send(code)`** — Run code in the subprocess; returns the last expression as a string (or the error message).
|
|
61
|
+
- **`dp.port`** — Bound port (None if no server).
|
|
62
|
+
- **`dp.close()`** — Stop the subprocess and web server.
|
|
63
|
+
- **`send(command, host='localhost', port=..., secret=None)`** — POST to a running server; returns the dict above.
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Standalone client: send(command, host, port, secret) to a running DecPython server.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import hmac
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.request
|
|
10
|
+
from typing import Any, Dict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def send(
|
|
14
|
+
command: str,
|
|
15
|
+
host: str = "localhost",
|
|
16
|
+
port: int | None = None,
|
|
17
|
+
secret: str | None = None,
|
|
18
|
+
) -> Dict[str, Any]:
|
|
19
|
+
"""
|
|
20
|
+
Send a command to a DecPython server via POST /exec.
|
|
21
|
+
When the server was started with secret=None, pass secret=None (no auth).
|
|
22
|
+
When the server was started with a secret, pass the same secret here.
|
|
23
|
+
|
|
24
|
+
Returns dict: {'code': 200, 'data': '<result>', 'msg': ''} on success,
|
|
25
|
+
or {'code': -1, 'data': '', 'msg': '<error>'} on failure.
|
|
26
|
+
"""
|
|
27
|
+
if port is None:
|
|
28
|
+
return {"code": -1, "data": "", "msg": "port is required"}
|
|
29
|
+
|
|
30
|
+
if secret is None:
|
|
31
|
+
body = json.dumps({"command": command}).encode("utf-8")
|
|
32
|
+
else:
|
|
33
|
+
ts = time.time()
|
|
34
|
+
sign = hmac.new(secret.encode("utf-8"), str(ts).encode("utf-8"), "sha256").hexdigest()
|
|
35
|
+
body = json.dumps({"command": command, "timestamp": ts, "sign": sign}).encode("utf-8")
|
|
36
|
+
|
|
37
|
+
url = f"http://{host}:{port}/exec"
|
|
38
|
+
req = urllib.request.Request(url, data=body, method="POST")
|
|
39
|
+
req.add_header("Content-Type", "application/json")
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
43
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
44
|
+
return {
|
|
45
|
+
"code": data.get("code", -1),
|
|
46
|
+
"data": data.get("data", ""),
|
|
47
|
+
"msg": data.get("msg", ""),
|
|
48
|
+
}
|
|
49
|
+
except urllib.error.HTTPError as e:
|
|
50
|
+
try:
|
|
51
|
+
data = json.loads(e.read().decode("utf-8"))
|
|
52
|
+
return {"code": data.get("code", -1), "data": data.get("data", ""), "msg": data.get("msg", str(e))}
|
|
53
|
+
except Exception:
|
|
54
|
+
return {"code": -1, "data": "", "msg": str(e)}
|
|
55
|
+
except Exception as e:
|
|
56
|
+
return {"code": -1, "data": "", "msg": str(e)}
|
|
@@ -203,26 +203,44 @@ class SubprocessExecutor:
|
|
|
203
203
|
class DecPython:
|
|
204
204
|
"""
|
|
205
205
|
跨平台、支持 Web 通信的 Python 终端。
|
|
206
|
-
-
|
|
207
|
-
-
|
|
208
|
-
-
|
|
206
|
+
- host: 绑定地址,默认 'localhost';仅当 host='0.0.0.0' 时可接受非本机请求。
|
|
207
|
+
- port: 绑定端口,默认 None 表示自动分配。
|
|
208
|
+
- secret: 密钥,默认 None 表示不启用;启用后可通过 POST /exec 配合时间戳签名远程执行命令。
|
|
209
|
+
- python: 终端中用于执行 Python 的命令。
|
|
210
|
+
- gui: 是否启动 Web 界面;与 port 任一非默认时会启动服务。
|
|
209
211
|
"""
|
|
210
212
|
|
|
211
|
-
def __init__(
|
|
213
|
+
def __init__(
|
|
214
|
+
self,
|
|
215
|
+
gui: bool = False,
|
|
216
|
+
python: str = "python",
|
|
217
|
+
host: str = "localhost",
|
|
218
|
+
port: Optional[int] = None,
|
|
219
|
+
secret: Optional[str] = None,
|
|
220
|
+
) -> None:
|
|
212
221
|
self._executor: SubprocessExecutor = SubprocessExecutor(python)
|
|
213
222
|
self._gui = gui
|
|
214
223
|
self._gui_server: Optional[object] = None
|
|
215
224
|
self._closed = False
|
|
216
225
|
self._last_result, self._last_error = "", ""
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
226
|
+
self._host = host
|
|
227
|
+
self._port = port
|
|
228
|
+
self._secret = secret
|
|
229
|
+
if gui or port is not None:
|
|
230
|
+
from decpython.gui.server import start_server
|
|
231
|
+
self._gui_server = start_server(self, host=host, port=port, secret=secret)
|
|
232
|
+
if gui:
|
|
233
|
+
threading.Timer(0.8, self._open_browser).start()
|
|
221
234
|
|
|
222
235
|
def _open_browser(self) -> None:
|
|
223
236
|
if self._gui_server and hasattr(self._gui_server, "url"):
|
|
224
237
|
webbrowser.open(self._gui_server.url)
|
|
225
238
|
|
|
239
|
+
@property
|
|
240
|
+
def port(self) -> Optional[int]:
|
|
241
|
+
"""Bound port (None if server not started)."""
|
|
242
|
+
return getattr(self._gui_server, "port", None)
|
|
243
|
+
|
|
226
244
|
def send(self, code: str) -> str:
|
|
227
245
|
"""执行代码,返回 str(执行结果);无表达式或仅语句时返回空字符串;出错时返回错误信息字符串。"""
|
|
228
246
|
if self._closed:
|
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
"""
|
|
2
|
-
本地 Web 服务:提供 Jupyter 风格的页面、/run
|
|
2
|
+
本地 Web 服务:提供 Jupyter 风格的页面、/run 执行接口、/exec(带 secret)与 SSE 推送。
|
|
3
3
|
仅使用标准库,跨平台。
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import hmac
|
|
6
7
|
import json
|
|
7
8
|
import queue
|
|
8
9
|
import socket
|
|
9
10
|
import threading
|
|
10
|
-
|
|
11
|
+
import time
|
|
12
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
13
|
+
|
|
14
|
+
# 时间戳有效期(秒),防止重放
|
|
15
|
+
_EXEC_TIMESTAMP_TOLERANCE = 300
|
|
16
|
+
# 等待 Web 绑定就绪的超时(秒)
|
|
17
|
+
_SERVER_READY_TIMEOUT = 10
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _ReadyHTTPServer(ThreadingHTTPServer):
|
|
21
|
+
"""在 server_activate() 完成后设置 ready_event,便于调用方阻塞直到绑定成功。"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, server_address, RequestHandlerClass, ready_event: threading.Event):
|
|
24
|
+
self._ready_event = ready_event
|
|
25
|
+
super().__init__(server_address, RequestHandlerClass)
|
|
26
|
+
|
|
27
|
+
def server_activate(self) -> None:
|
|
28
|
+
super().server_activate()
|
|
29
|
+
self._ready_event.set()
|
|
11
30
|
|
|
12
31
|
# 内嵌的 HTML 页面(科技感界面:深色终端风、霓虹高亮、玻璃拟态、流畅动效)
|
|
13
32
|
INDEX_HTML = """<!DOCTYPE html>
|
|
@@ -355,20 +374,74 @@ class _DecPythonHandler(BaseHTTPRequestHandler):
|
|
|
355
374
|
self.end_headers()
|
|
356
375
|
self.wfile.write(json.dumps({"result": result, "error": err}, ensure_ascii=False).encode("utf-8"))
|
|
357
376
|
return
|
|
377
|
+
if self.path == "/exec":
|
|
378
|
+
secret = getattr(self.server, "secret", None)
|
|
379
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
380
|
+
body = self.rfile.read(length).decode("utf-8") if length else "{}"
|
|
381
|
+
try:
|
|
382
|
+
data = json.loads(body)
|
|
383
|
+
command = data.get("command", "")
|
|
384
|
+
except Exception:
|
|
385
|
+
self._send_json(400, {"code": -1, "data": "", "msg": "Invalid JSON"})
|
|
386
|
+
return
|
|
387
|
+
if secret is None and ("timestamp" in data or "sign" in data):
|
|
388
|
+
self._send_json(400, {"code": -1, "data": "", "msg": "Server does not use secret; use send(..., secret=None)."})
|
|
389
|
+
return
|
|
390
|
+
if secret is not None:
|
|
391
|
+
ts = data.get("timestamp")
|
|
392
|
+
sign = data.get("sign", "")
|
|
393
|
+
try:
|
|
394
|
+
ts_val = float(ts)
|
|
395
|
+
except (TypeError, ValueError):
|
|
396
|
+
self._send_json(400, {"code": -1, "data": "", "msg": "Invalid timestamp"})
|
|
397
|
+
return
|
|
398
|
+
if abs(time.time() - ts_val) > _EXEC_TIMESTAMP_TOLERANCE:
|
|
399
|
+
self._send_json(400, {"code": -1, "data": "", "msg": "Timestamp expired"})
|
|
400
|
+
return
|
|
401
|
+
expected = hmac.new(secret.encode("utf-8"), str(ts_val).encode("utf-8"), "sha256").hexdigest()
|
|
402
|
+
if not hmac.compare_digest(expected, sign):
|
|
403
|
+
self._send_json(403, {"code": -1, "data": "", "msg": "Invalid signature"})
|
|
404
|
+
return
|
|
405
|
+
dp = _DecPythonHandler.decpython_instance
|
|
406
|
+
if not dp or getattr(dp, "_closed", True):
|
|
407
|
+
self._send_json(500, {"code": -1, "data": "", "msg": "Terminal closed"})
|
|
408
|
+
return
|
|
409
|
+
try:
|
|
410
|
+
result, err = dp._executor.run(command)
|
|
411
|
+
if dp._gui_server is not None and hasattr(dp._gui_server, "append_cell"):
|
|
412
|
+
dp._gui_server.append_cell(command, result, err)
|
|
413
|
+
if err:
|
|
414
|
+
self._send_json(200, {"code": -1, "data": "", "msg": err})
|
|
415
|
+
else:
|
|
416
|
+
self._send_json(200, {"code": 200, "data": result, "msg": ""})
|
|
417
|
+
except Exception as e:
|
|
418
|
+
self._send_json(200, {"code": -1, "data": "", "msg": str(e)})
|
|
419
|
+
return
|
|
358
420
|
self.send_response(404)
|
|
359
421
|
self.end_headers()
|
|
360
422
|
|
|
423
|
+
def _send_json(self, status: int, obj: dict) -> None:
|
|
424
|
+
self.send_response(status)
|
|
425
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
426
|
+
self.end_headers()
|
|
427
|
+
self.wfile.write(json.dumps(obj, ensure_ascii=False).encode("utf-8"))
|
|
428
|
+
|
|
361
429
|
|
|
362
430
|
class _GUIServer:
|
|
363
|
-
def __init__(self, decpython_instance: object, port: int) -> None:
|
|
364
|
-
|
|
431
|
+
def __init__(self, decpython_instance: object, host: str, port: int, secret: object) -> None:
|
|
432
|
+
ready = threading.Event()
|
|
433
|
+
self._server = _ReadyHTTPServer((host, port), _DecPythonHandler, ready)
|
|
365
434
|
self._server.state = {"cells": [], "queues": []}
|
|
435
|
+
self._server.secret = secret
|
|
366
436
|
_DecPythonHandler.decpython_instance = decpython_instance
|
|
367
437
|
self._decpython = decpython_instance
|
|
368
|
-
self.port = port
|
|
369
|
-
self.url = f"http://127.0.0.1:{port}/"
|
|
370
438
|
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
|
|
371
439
|
self._thread.start()
|
|
440
|
+
if not ready.wait(timeout=_SERVER_READY_TIMEOUT):
|
|
441
|
+
raise RuntimeError("DecPython web server failed to bind within timeout")
|
|
442
|
+
self.port = self._server.server_address[1]
|
|
443
|
+
self.host = host if host else "localhost"
|
|
444
|
+
self.url = f"http://127.0.0.1:{self.port}/" if self.host in ("localhost", "127.0.0.1") else f"http://{self.host}:{self.port}/"
|
|
372
445
|
|
|
373
446
|
def append_cell(self, code: str, result: str, error: str) -> None:
|
|
374
447
|
cell = {"code": code, "result": result or "", "error": error or ""}
|
|
@@ -390,6 +463,12 @@ class _GUIServer:
|
|
|
390
463
|
self._server = None
|
|
391
464
|
|
|
392
465
|
|
|
393
|
-
def
|
|
394
|
-
|
|
395
|
-
|
|
466
|
+
def start_server(
|
|
467
|
+
decpython_instance: object,
|
|
468
|
+
host: str = "localhost",
|
|
469
|
+
port: object = None,
|
|
470
|
+
secret: object = None,
|
|
471
|
+
) -> _GUIServer:
|
|
472
|
+
if port is None or port == 0:
|
|
473
|
+
port = _find_free_port()
|
|
474
|
+
return _GUIServer(decpython_instance, host, port, secret)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: decpython
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: 跨平台、支持 Web 通信的 Python 终端工具
|
|
5
|
+
Home-page: https://github.com/your-username/decpython_maskter
|
|
6
|
+
Author: decpython
|
|
7
|
+
Author-email: decrule@outlook.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.9.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
21
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
22
|
+
Dynamic: author-email
|
|
23
|
+
Dynamic: home-page
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
|
|
26
|
+
# DecPython
|
|
27
|
+
|
|
28
|
+
Run Python in a subprocess from your code and get string results, or open a Jupyter-style web terminal. Optional HTTP API with host/port/secret for remote execution. Cross-platform, stdlib only.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install -e .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from decpython import DecPython
|
|
40
|
+
|
|
41
|
+
dp = DecPython(gui=False, python='python')
|
|
42
|
+
result = dp.send('a = 1\nb = 2\na + b') # -> '3'
|
|
43
|
+
dp.close()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
With a web UI: `gui=True`. Code and results from `send()` appear in the browser (Shift+Enter or Run).
|
|
47
|
+
|
|
48
|
+
## Host, port, secret (HTTP API)
|
|
49
|
+
|
|
50
|
+
When you pass `port` (or use `gui=True`), a web server is started. **`DecPython(...)` blocks until the server is bound and ready**, so you can call `send()` or the standalone `send()` immediately after.
|
|
51
|
+
|
|
52
|
+
- **host** — Bind address (default `'localhost'`). Use `'0.0.0.0'` to accept non-local requests.
|
|
53
|
+
- **port** — Port number, or `None` / `0` for auto.
|
|
54
|
+
- **secret** — If set, remote clients must send the same secret (timestamp + HMAC) to POST `/exec`. If `None`, no auth; client should use `send(..., secret=None)`.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# Server with no auth
|
|
58
|
+
dp = DecPython(gui=False, host='127.0.0.1', port=0, secret=None)
|
|
59
|
+
port = dp.port # ready after init
|
|
60
|
+
dp.close()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Standalone `send()` (remote)
|
|
64
|
+
|
|
65
|
+
Send a command to a running DecPython server without holding the instance:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from decpython import DecPython, send
|
|
69
|
+
|
|
70
|
+
dp = DecPython(gui=False, host='127.0.0.1', port=0, secret='key')
|
|
71
|
+
out = send('1 + 2', host='127.0.0.1', port=dp.port, secret='key')
|
|
72
|
+
# out == {'code': 200, 'data': '3', 'msg': ''}
|
|
73
|
+
dp.close()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- **Success:** `{'code': 200, 'data': '<result>', 'msg': ''}`
|
|
77
|
+
- **Error:** `{'code': -1, 'data': '', 'msg': '<error>'}`
|
|
78
|
+
Use `secret=None` when the server was started with `secret=None`.
|
|
79
|
+
|
|
80
|
+
See `examples/example_remote.py` for more.
|
|
81
|
+
|
|
82
|
+
## API
|
|
83
|
+
|
|
84
|
+
- **`DecPython(gui=..., python='python', host='localhost', port=None, secret=None)`** — Blocks until the web server (if any) is bound. `port` / `gui` control whether the server starts.
|
|
85
|
+
- **`dp.send(code)`** — Run code in the subprocess; returns the last expression as a string (or the error message).
|
|
86
|
+
- **`dp.port`** — Bound port (None if no server).
|
|
87
|
+
- **`dp.close()`** — Stop the subprocess and web server.
|
|
88
|
+
- **`send(command, host='localhost', port=..., secret=None)`** — POST to a running server; returns the dict above.
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
|
@@ -2,6 +2,7 @@ README.md
|
|
|
2
2
|
pyproject.toml
|
|
3
3
|
setup.py
|
|
4
4
|
decpython/__init__.py
|
|
5
|
+
decpython/client.py
|
|
5
6
|
decpython/core.py
|
|
6
7
|
decpython.egg-info/PKG-INFO
|
|
7
8
|
decpython.egg-info/SOURCES.txt
|
|
@@ -9,4 +10,5 @@ decpython.egg-info/dependency_links.txt
|
|
|
9
10
|
decpython.egg-info/requires.txt
|
|
10
11
|
decpython.egg-info/top_level.txt
|
|
11
12
|
decpython/gui/__init__.py
|
|
12
|
-
decpython/gui/server.py
|
|
13
|
+
decpython/gui/server.py
|
|
14
|
+
tests/test_decpython.py
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stability tests for DecPython: instance send(), standalone send(), host/port/secret, /exec.
|
|
3
|
+
"""
|
|
4
|
+
import unittest
|
|
5
|
+
from decpython import DecPython, send
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestDecPythonProgrammatic(unittest.TestCase):
|
|
9
|
+
"""No server: gui=False, port=None."""
|
|
10
|
+
|
|
11
|
+
def test_no_server_when_gui_false_port_none(self):
|
|
12
|
+
dp = DecPython(gui=False, python="python")
|
|
13
|
+
self.assertIsNone(dp.port)
|
|
14
|
+
dp.close()
|
|
15
|
+
|
|
16
|
+
def test_send_returns_string(self):
|
|
17
|
+
dp = DecPython(gui=False, python="python")
|
|
18
|
+
try:
|
|
19
|
+
r = dp.send("a = 1\nb = 2\na + b")
|
|
20
|
+
self.assertEqual(r, "3")
|
|
21
|
+
finally:
|
|
22
|
+
dp.close()
|
|
23
|
+
|
|
24
|
+
def test_send_statement_returns_empty(self):
|
|
25
|
+
dp = DecPython(gui=False, python="python")
|
|
26
|
+
try:
|
|
27
|
+
r = dp.send("x = 10")
|
|
28
|
+
self.assertEqual(r, "")
|
|
29
|
+
finally:
|
|
30
|
+
dp.close()
|
|
31
|
+
|
|
32
|
+
def test_send_error_returns_msg(self):
|
|
33
|
+
dp = DecPython(gui=False, python="python")
|
|
34
|
+
try:
|
|
35
|
+
r = dp.send("1/0")
|
|
36
|
+
self.assertIn("ZeroDivisionError", r)
|
|
37
|
+
finally:
|
|
38
|
+
dp.close()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestDecPythonServerWithSecret(unittest.TestCase):
|
|
42
|
+
"""Server with port and secret; standalone send() to /exec."""
|
|
43
|
+
|
|
44
|
+
def test_exec_success(self):
|
|
45
|
+
dp = DecPython(gui=False, python="python", host="127.0.0.1", port=0, secret="test-secret")
|
|
46
|
+
try:
|
|
47
|
+
port = dp.port
|
|
48
|
+
self.assertIsNotNone(port)
|
|
49
|
+
out = send("1 + 2", host="127.0.0.1", port=port, secret="test-secret")
|
|
50
|
+
self.assertEqual(out["code"], 200)
|
|
51
|
+
self.assertEqual(out["data"], "3")
|
|
52
|
+
self.assertEqual(out["msg"], "")
|
|
53
|
+
finally:
|
|
54
|
+
dp.close()
|
|
55
|
+
|
|
56
|
+
def test_exec_error_returns_code_minus_one(self):
|
|
57
|
+
dp = DecPython(gui=False, python="python", host="127.0.0.1", port=0, secret="test-secret")
|
|
58
|
+
try:
|
|
59
|
+
port = dp.port
|
|
60
|
+
out = send("1/0", host="127.0.0.1", port=port, secret="test-secret")
|
|
61
|
+
self.assertEqual(out["code"], -1)
|
|
62
|
+
self.assertEqual(out["data"], "")
|
|
63
|
+
self.assertIn("ZeroDivisionError", out["msg"])
|
|
64
|
+
finally:
|
|
65
|
+
dp.close()
|
|
66
|
+
|
|
67
|
+
def test_exec_wrong_secret_rejected(self):
|
|
68
|
+
dp = DecPython(gui=False, python="python", host="127.0.0.1", port=0, secret="right-secret")
|
|
69
|
+
try:
|
|
70
|
+
port = dp.port
|
|
71
|
+
out = send("1+1", host="127.0.0.1", port=port, secret="wrong-secret")
|
|
72
|
+
self.assertEqual(out["code"], -1)
|
|
73
|
+
self.assertIn("Invalid signature", out["msg"])
|
|
74
|
+
finally:
|
|
75
|
+
dp.close()
|
|
76
|
+
|
|
77
|
+
def test_send_no_port_returns_error(self):
|
|
78
|
+
out = send("1+1", host="localhost", port=None, secret="x")
|
|
79
|
+
self.assertEqual(out["code"], -1)
|
|
80
|
+
self.assertIn("port", out["msg"])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class TestDecPythonServerNoSecret(unittest.TestCase):
|
|
84
|
+
"""Server with secret=None: send(..., secret=None) works; send(..., secret='x') returns error."""
|
|
85
|
+
|
|
86
|
+
def test_exec_no_secret_client_send_none(self):
|
|
87
|
+
dp = DecPython(gui=False, python="python", host="127.0.0.1", port=0, secret=None)
|
|
88
|
+
try:
|
|
89
|
+
port = dp.port
|
|
90
|
+
out = send("2 + 3", host="127.0.0.1", port=port, secret=None)
|
|
91
|
+
self.assertEqual(out["code"], 200)
|
|
92
|
+
self.assertEqual(out["data"], "5")
|
|
93
|
+
self.assertEqual(out["msg"], "")
|
|
94
|
+
finally:
|
|
95
|
+
dp.close()
|
|
96
|
+
|
|
97
|
+
def test_exec_no_secret_client_sends_secret_returns_error(self):
|
|
98
|
+
dp = DecPython(gui=False, python="python", host="127.0.0.1", port=0, secret=None)
|
|
99
|
+
try:
|
|
100
|
+
port = dp.port
|
|
101
|
+
out = send("1+1", host="127.0.0.1", port=port, secret="any")
|
|
102
|
+
self.assertEqual(out["code"], -1)
|
|
103
|
+
self.assertIn("does not use secret", out["msg"])
|
|
104
|
+
finally:
|
|
105
|
+
dp.close()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
unittest.main()
|
decpython-0.1.0/PKG-INFO
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.2
|
|
2
|
-
Name: decpython
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: 跨平台、支持 Web 通信的 Python 终端工具
|
|
5
|
-
Home-page: https://github.com/your-username/decpython_maskter
|
|
6
|
-
Author: decpython
|
|
7
|
-
Author-email: decrule@outlook.com
|
|
8
|
-
License: MIT
|
|
9
|
-
Classifier: Development Status :: 3 - Alpha
|
|
10
|
-
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
-
Requires-Python: >=3.9.0
|
|
18
|
-
Description-Content-Type: text/markdown
|
|
19
|
-
Provides-Extra: dev
|
|
20
|
-
Requires-Dist: pytest>=7; extra == "dev"
|
|
21
|
-
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
22
|
-
Dynamic: author-email
|
|
23
|
-
Dynamic: home-page
|
|
24
|
-
Dynamic: requires-python
|
|
25
|
-
|
|
26
|
-
# DecPython
|
|
27
|
-
|
|
28
|
-
Run Python in a subprocess from your code and get string results, or open a Jupyter-style web terminal. Cross-platform, stdlib only.
|
|
29
|
-
|
|
30
|
-
## Install
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
pip install -e .
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Quick start
|
|
37
|
-
|
|
38
|
-
```python
|
|
39
|
-
from decpython import DecPython
|
|
40
|
-
|
|
41
|
-
dp = DecPython(gui=False, python='python')
|
|
42
|
-
result = dp.send('a = 1\nb = 2\na + b') # -> '3'
|
|
43
|
-
dp.close()
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
With a web UI: use `gui=True` and optionally `python='python3.12'`. Code and results from `send()` appear in the browser. Press Shift+Enter or click Run in the page.
|
|
47
|
-
|
|
48
|
-
## API
|
|
49
|
-
|
|
50
|
-
- **`send(code)`** — Run code; returns the last expression as a string (or the error message).
|
|
51
|
-
- **`close()`** — Stop the subprocess and, if used, the web server.
|
|
52
|
-
|
|
53
|
-
## License
|
|
54
|
-
|
|
55
|
-
MIT
|
decpython-0.1.0/README.md
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# DecPython
|
|
2
|
-
|
|
3
|
-
Run Python in a subprocess from your code and get string results, or open a Jupyter-style web terminal. Cross-platform, stdlib only.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
pip install -e .
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Quick start
|
|
12
|
-
|
|
13
|
-
```python
|
|
14
|
-
from decpython import DecPython
|
|
15
|
-
|
|
16
|
-
dp = DecPython(gui=False, python='python')
|
|
17
|
-
result = dp.send('a = 1\nb = 2\na + b') # -> '3'
|
|
18
|
-
dp.close()
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
With a web UI: use `gui=True` and optionally `python='python3.12'`. Code and results from `send()` appear in the browser. Press Shift+Enter or click Run in the page.
|
|
22
|
-
|
|
23
|
-
## API
|
|
24
|
-
|
|
25
|
-
- **`send(code)`** — Run code; returns the last expression as a string (or the error message).
|
|
26
|
-
- **`close()`** — Stop the subprocess and, if used, the web server.
|
|
27
|
-
|
|
28
|
-
## License
|
|
29
|
-
|
|
30
|
-
MIT
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.2
|
|
2
|
-
Name: decpython
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: 跨平台、支持 Web 通信的 Python 终端工具
|
|
5
|
-
Home-page: https://github.com/your-username/decpython_maskter
|
|
6
|
-
Author: decpython
|
|
7
|
-
Author-email: decrule@outlook.com
|
|
8
|
-
License: MIT
|
|
9
|
-
Classifier: Development Status :: 3 - Alpha
|
|
10
|
-
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
-
Requires-Python: >=3.9.0
|
|
18
|
-
Description-Content-Type: text/markdown
|
|
19
|
-
Provides-Extra: dev
|
|
20
|
-
Requires-Dist: pytest>=7; extra == "dev"
|
|
21
|
-
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
22
|
-
Dynamic: author-email
|
|
23
|
-
Dynamic: home-page
|
|
24
|
-
Dynamic: requires-python
|
|
25
|
-
|
|
26
|
-
# DecPython
|
|
27
|
-
|
|
28
|
-
Run Python in a subprocess from your code and get string results, or open a Jupyter-style web terminal. Cross-platform, stdlib only.
|
|
29
|
-
|
|
30
|
-
## Install
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
pip install -e .
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Quick start
|
|
37
|
-
|
|
38
|
-
```python
|
|
39
|
-
from decpython import DecPython
|
|
40
|
-
|
|
41
|
-
dp = DecPython(gui=False, python='python')
|
|
42
|
-
result = dp.send('a = 1\nb = 2\na + b') # -> '3'
|
|
43
|
-
dp.close()
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
With a web UI: use `gui=True` and optionally `python='python3.12'`. Code and results from `send()` appear in the browser. Press Shift+Enter or click Run in the page.
|
|
47
|
-
|
|
48
|
-
## API
|
|
49
|
-
|
|
50
|
-
- **`send(code)`** — Run code; returns the last expression as a string (or the error message).
|
|
51
|
-
- **`close()`** — Stop the subprocess and, if used, the web server.
|
|
52
|
-
|
|
53
|
-
## License
|
|
54
|
-
|
|
55
|
-
MIT
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|