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.
@@ -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
@@ -2,7 +2,8 @@
2
2
  decpython - 跨平台、支持 Web 通信的 Python 终端工具
3
3
  """
4
4
 
5
+ from decpython.client import send
5
6
  from decpython.core import DecPython
6
7
 
7
- __version__ = '1.0.0'
8
- __all__ = ["DecPython"]
8
+ __version__ = "1.0.1"
9
+ __all__ = ["DecPython", "send"]
@@ -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
- - python: 终端中用于执行 Python 的命令,如 'python' 'python3.12'
207
- - gui=False:仅程序化调用 send(code) 获取 str 结果。
208
- - gui=True:启动类 Jupyter Web 界面,实时查看输入与输出(含通过 send 发送的内容与结果)。
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__(self, gui: bool = False, python: str = "python") -> None:
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
- if gui:
218
- from decpython.gui.server import start_gui_server
219
- self._gui_server = start_gui_server(self)
220
- threading.Timer(0.8, self._open_browser).start()
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 执行接口与 SSE 推送(使 send() 与页面同步)。
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
- from http.server import HTTPServer, BaseHTTPRequestHandler, ThreadingHTTPServer
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
- self._server = ThreadingHTTPServer(("127.0.0.1", port), _DecPythonHandler)
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 start_gui_server(decpython_instance: object) -> _GUIServer:
394
- port = _find_free_port()
395
- return _GUIServer(decpython_instance, port)
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "decpython"
7
- version = "0.1.0"
7
+ version = "1.0.1"
8
8
  description = "跨平台、支持 Web 通信的 Python 终端工具"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -15,7 +15,7 @@ URL = 'https://github.com/your-username/decpython_maskter'
15
15
  EMAIL = 'decrule@outlook.com'
16
16
  AUTHOR = 'DecRule'
17
17
  REQUIRES_PYTHON = '>=3.9.0'
18
- VERSION = '1.0.0'
18
+ VERSION = '1.0.1'
19
19
 
20
20
  REQUIRED = []
21
21
  EXTRAS = {'dev': ['pytest>=7', 'ruff>=0.1']}
@@ -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