decpython 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.
- decpython-0.1.0/PKG-INFO +55 -0
- decpython-0.1.0/README.md +30 -0
- decpython-0.1.0/decpython/__init__.py +8 -0
- decpython-0.1.0/decpython/core.py +250 -0
- decpython-0.1.0/decpython/gui/__init__.py +1 -0
- decpython-0.1.0/decpython/gui/server.py +395 -0
- decpython-0.1.0/decpython.egg-info/PKG-INFO +55 -0
- decpython-0.1.0/decpython.egg-info/SOURCES.txt +12 -0
- decpython-0.1.0/decpython.egg-info/dependency_links.txt +1 -0
- decpython-0.1.0/decpython.egg-info/requires.txt +4 -0
- decpython-0.1.0/decpython.egg-info/top_level.txt +1 -0
- decpython-0.1.0/pyproject.toml +30 -0
- decpython-0.1.0/setup.cfg +4 -0
- decpython-0.1.0/setup.py +91 -0
decpython-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
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
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
核心执行器与 DecPython 终端类。
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import threading
|
|
10
|
+
import traceback
|
|
11
|
+
import webbrowser
|
|
12
|
+
from io import StringIO
|
|
13
|
+
from subprocess import PIPE, Popen
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
# 子进程运行器:从 stdin 读代码(直到 __DECPYTHON_END_INPUT__),执行后向 stdout 写 __DECPYTHON_RESULT__<json>__DECPYTHON_END_RESULT__
|
|
17
|
+
_RUNNER_SOURCE = r'''
|
|
18
|
+
import ast
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
import traceback
|
|
22
|
+
|
|
23
|
+
namespace = {"__name__": "__decpython__", "__builtins__": __import__("builtins").__dict__}
|
|
24
|
+
DELIM = "__DECPYTHON_END_INPUT__"
|
|
25
|
+
OUT_PREFIX = "__DECPYTHON_RESULT__"
|
|
26
|
+
OUT_SUFFIX = "__DECPYTHON_END_RESULT__"
|
|
27
|
+
|
|
28
|
+
def run(code):
|
|
29
|
+
code = code.strip()
|
|
30
|
+
if not code:
|
|
31
|
+
return "", ""
|
|
32
|
+
try:
|
|
33
|
+
tree = ast.parse(code)
|
|
34
|
+
except SyntaxError as e:
|
|
35
|
+
return "", "".join(traceback.format_exception_only(type(e), e))
|
|
36
|
+
if not tree.body:
|
|
37
|
+
return "", ""
|
|
38
|
+
try:
|
|
39
|
+
last = tree.body[-1]
|
|
40
|
+
if isinstance(last, ast.Expr):
|
|
41
|
+
if len(tree.body) == 1:
|
|
42
|
+
result = eval(compile(ast.Expression(body=last.value), "<decpython>", "eval"), namespace)
|
|
43
|
+
else:
|
|
44
|
+
exec(compile(ast.Module(body=tree.body[:-1], type_ignores=[]), "<decpython>", "exec"), namespace)
|
|
45
|
+
result = eval(compile(ast.Expression(body=last.value), "<decpython>", "eval"), namespace)
|
|
46
|
+
return (str(result) if result is not None else ""), ""
|
|
47
|
+
else:
|
|
48
|
+
exec(compile(tree, "<decpython>", "exec"), namespace)
|
|
49
|
+
return "", ""
|
|
50
|
+
except Exception as e:
|
|
51
|
+
return "", "".join(traceback.format_exception(type(e), e, e.__traceback__))
|
|
52
|
+
|
|
53
|
+
while True:
|
|
54
|
+
lines = []
|
|
55
|
+
while True:
|
|
56
|
+
line = sys.stdin.readline()
|
|
57
|
+
if not line:
|
|
58
|
+
sys.exit(0)
|
|
59
|
+
if line.rstrip() == DELIM:
|
|
60
|
+
break
|
|
61
|
+
lines.append(line)
|
|
62
|
+
code = "".join(lines)
|
|
63
|
+
result, err = run(code)
|
|
64
|
+
out = OUT_PREFIX + json.dumps({"result": result, "error": err}, ensure_ascii=False) + OUT_SUFFIX + "\n"
|
|
65
|
+
sys.stdout.write(out)
|
|
66
|
+
sys.stdout.flush()
|
|
67
|
+
'''
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Executor:
|
|
71
|
+
"""在独立命名空间中执行代码,支持多行与最后表达式求值。"""
|
|
72
|
+
|
|
73
|
+
def __init__(self) -> None:
|
|
74
|
+
self._namespace: dict = {}
|
|
75
|
+
self._init_builtins()
|
|
76
|
+
|
|
77
|
+
def _init_builtins(self) -> None:
|
|
78
|
+
import builtins
|
|
79
|
+
self._namespace = {
|
|
80
|
+
"__name__": "__decpython__",
|
|
81
|
+
"__builtins__": builtins.__dict__,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def run(self, code: str) -> tuple[str, str]:
|
|
85
|
+
"""
|
|
86
|
+
执行代码,返回 (result, error)。
|
|
87
|
+
result 为最后一次表达式结果的 str,无表达式或仅为语句时为空字符串;
|
|
88
|
+
error 为执行出错时的错误信息,否则为空字符串。
|
|
89
|
+
"""
|
|
90
|
+
code = code.strip()
|
|
91
|
+
if not code:
|
|
92
|
+
return "", ""
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
tree = ast.parse(code)
|
|
96
|
+
except SyntaxError as e:
|
|
97
|
+
return "", "".join(traceback.format_exception_only(type(e), e))
|
|
98
|
+
|
|
99
|
+
if not tree.body:
|
|
100
|
+
return "", ""
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
last = tree.body[-1]
|
|
104
|
+
if isinstance(last, ast.Expr):
|
|
105
|
+
if len(tree.body) == 1:
|
|
106
|
+
result = self._eval_expr(last.value)
|
|
107
|
+
else:
|
|
108
|
+
block = ast.Module(body=tree.body[:-1], type_ignores=[])
|
|
109
|
+
exec(compile(block, "<decpython>", "exec"), self._namespace)
|
|
110
|
+
result = self._eval_expr(last.value)
|
|
111
|
+
return self._repr_result(result), ""
|
|
112
|
+
else:
|
|
113
|
+
exec(compile(tree, "<decpython>", "exec"), self._namespace)
|
|
114
|
+
return "", ""
|
|
115
|
+
except Exception as e:
|
|
116
|
+
return "", "".join(traceback.format_exception(type(e), e, e.__traceback__))
|
|
117
|
+
|
|
118
|
+
def _eval_expr(self, node: ast.expr): # noqa: ANN001
|
|
119
|
+
"""在命名空间中求值单个表达式节点。"""
|
|
120
|
+
expr_ast = ast.Expression(body=node)
|
|
121
|
+
return eval(compile(expr_ast, "<decpython>", "eval"), self._namespace)
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _repr_result(value) -> str: # noqa: ANN001
|
|
125
|
+
"""将执行结果转为字符串返回。"""
|
|
126
|
+
if value is None:
|
|
127
|
+
return ""
|
|
128
|
+
return str(value)
|
|
129
|
+
|
|
130
|
+
def reset(self) -> None:
|
|
131
|
+
"""清空命名空间(保留 builtins)。"""
|
|
132
|
+
self._init_builtins()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class SubprocessExecutor:
|
|
136
|
+
"""使用指定终端命令(如 python / python3.12)在子进程中执行代码,保持独立命名空间。"""
|
|
137
|
+
|
|
138
|
+
_DELIM = "__DECPYTHON_END_INPUT__"
|
|
139
|
+
_OUT_PREFIX = "__DECPYTHON_RESULT__"
|
|
140
|
+
_OUT_SUFFIX = "__DECPYTHON_END_RESULT__"
|
|
141
|
+
|
|
142
|
+
def __init__(self, python_cmd: str) -> None:
|
|
143
|
+
self._python_cmd = python_cmd
|
|
144
|
+
self._process: Optional[Popen] = None
|
|
145
|
+
self._runner_path: Optional[str] = None
|
|
146
|
+
self._start()
|
|
147
|
+
|
|
148
|
+
def _start(self) -> None:
|
|
149
|
+
fd, self._runner_path = tempfile.mkstemp(suffix=".py", prefix="decpython_runner_")
|
|
150
|
+
try:
|
|
151
|
+
os.write(fd, _RUNNER_SOURCE.encode("utf-8"))
|
|
152
|
+
finally:
|
|
153
|
+
os.close(fd)
|
|
154
|
+
self._process = Popen(
|
|
155
|
+
[self._python_cmd, self._runner_path],
|
|
156
|
+
stdin=PIPE,
|
|
157
|
+
stdout=PIPE,
|
|
158
|
+
stderr=PIPE,
|
|
159
|
+
text=True,
|
|
160
|
+
bufsize=1,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def run(self, code: str) -> tuple[str, str]:
|
|
164
|
+
if self._process is None or self._process.poll() is not None:
|
|
165
|
+
return "", "子进程已退出"
|
|
166
|
+
try:
|
|
167
|
+
self._process.stdin.write(code)
|
|
168
|
+
self._process.stdin.write("\n" + self._DELIM + "\n")
|
|
169
|
+
self._process.stdin.flush()
|
|
170
|
+
except Exception as e:
|
|
171
|
+
return "", str(e)
|
|
172
|
+
while True:
|
|
173
|
+
line = self._process.stdout.readline()
|
|
174
|
+
if not line:
|
|
175
|
+
return "", "子进程意外结束"
|
|
176
|
+
if self._OUT_SUFFIX in line:
|
|
177
|
+
idx = line.find(self._OUT_PREFIX)
|
|
178
|
+
end = line.find(self._OUT_SUFFIX)
|
|
179
|
+
if idx != -1 and end != -1:
|
|
180
|
+
import json
|
|
181
|
+
raw = line[idx + len(self._OUT_PREFIX) : end]
|
|
182
|
+
try:
|
|
183
|
+
data = json.loads(raw)
|
|
184
|
+
return data.get("result", ""), data.get("error", "")
|
|
185
|
+
except Exception:
|
|
186
|
+
return raw, ""
|
|
187
|
+
break
|
|
188
|
+
return "", ""
|
|
189
|
+
|
|
190
|
+
def close(self) -> None:
|
|
191
|
+
if self._process and self._process.poll() is None:
|
|
192
|
+
self._process.terminate()
|
|
193
|
+
self._process.wait()
|
|
194
|
+
self._process = None
|
|
195
|
+
if self._runner_path and os.path.exists(self._runner_path):
|
|
196
|
+
try:
|
|
197
|
+
os.unlink(self._runner_path)
|
|
198
|
+
except OSError:
|
|
199
|
+
pass
|
|
200
|
+
self._runner_path = None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class DecPython:
|
|
204
|
+
"""
|
|
205
|
+
跨平台、支持 Web 通信的 Python 终端。
|
|
206
|
+
- python: 终端中用于执行 Python 的命令,如 'python' 或 'python3.12'。
|
|
207
|
+
- gui=False:仅程序化调用 send(code) 获取 str 结果。
|
|
208
|
+
- gui=True:启动类 Jupyter 的 Web 界面,实时查看输入与输出(含通过 send 发送的内容与结果)。
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def __init__(self, gui: bool = False, python: str = "python") -> None:
|
|
212
|
+
self._executor: SubprocessExecutor = SubprocessExecutor(python)
|
|
213
|
+
self._gui = gui
|
|
214
|
+
self._gui_server: Optional[object] = None
|
|
215
|
+
self._closed = False
|
|
216
|
+
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()
|
|
221
|
+
|
|
222
|
+
def _open_browser(self) -> None:
|
|
223
|
+
if self._gui_server and hasattr(self._gui_server, "url"):
|
|
224
|
+
webbrowser.open(self._gui_server.url)
|
|
225
|
+
|
|
226
|
+
def send(self, code: str) -> str:
|
|
227
|
+
"""执行代码,返回 str(执行结果);无表达式或仅语句时返回空字符串;出错时返回错误信息字符串。"""
|
|
228
|
+
if self._closed:
|
|
229
|
+
raise RuntimeError("DecPython terminal is already closed")
|
|
230
|
+
result, error = self._executor.run(code)
|
|
231
|
+
self._last_result, self._last_error = result, error
|
|
232
|
+
if self._gui_server is not None and hasattr(self._gui_server, "append_cell"):
|
|
233
|
+
self._gui_server.append_cell(code, result, error)
|
|
234
|
+
return result if not error else error
|
|
235
|
+
|
|
236
|
+
def close(self) -> None:
|
|
237
|
+
"""关闭终端;若曾启动 GUI 则停止 Web 服务,并结束子进程。"""
|
|
238
|
+
if self._closed:
|
|
239
|
+
return
|
|
240
|
+
self._closed = True
|
|
241
|
+
if self._gui_server is not None and hasattr(self._gui_server, "shutdown"):
|
|
242
|
+
self._gui_server.shutdown()
|
|
243
|
+
if hasattr(self._executor, "close"):
|
|
244
|
+
self._executor.close()
|
|
245
|
+
|
|
246
|
+
def __enter__(self) -> "DecPython":
|
|
247
|
+
return self
|
|
248
|
+
|
|
249
|
+
def __exit__(self, *args: object) -> None:
|
|
250
|
+
self.close()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# GUI 模块:Web 界面与本地服务
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""
|
|
2
|
+
本地 Web 服务:提供 Jupyter 风格的页面、/run 执行接口与 SSE 推送(使 send() 与页面同步)。
|
|
3
|
+
仅使用标准库,跨平台。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import queue
|
|
8
|
+
import socket
|
|
9
|
+
import threading
|
|
10
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
11
|
+
|
|
12
|
+
# 内嵌的 HTML 页面(科技感界面:深色终端风、霓虹高亮、玻璃拟态、流畅动效)
|
|
13
|
+
INDEX_HTML = """<!DOCTYPE html>
|
|
14
|
+
<html lang="zh-CN">
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="UTF-8">
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
18
|
+
<title>DecPython</title>
|
|
19
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
20
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
21
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
22
|
+
<style>
|
|
23
|
+
:root {
|
|
24
|
+
--bg: #0a0e14;
|
|
25
|
+
--bg-panel: rgba(12, 18, 28, 0.85);
|
|
26
|
+
--surface: rgba(18, 26, 38, 0.9);
|
|
27
|
+
--border: rgba(0, 212, 255, 0.25);
|
|
28
|
+
--text: #e6e6e6;
|
|
29
|
+
--text-dim: #7a8a99;
|
|
30
|
+
--accent: #00d4ff;
|
|
31
|
+
--accent-glow: rgba(0, 212, 255, 0.4);
|
|
32
|
+
--success: #00ff9f;
|
|
33
|
+
--success-glow: rgba(0, 255, 159, 0.25);
|
|
34
|
+
--error: #ff3366;
|
|
35
|
+
--error-glow: rgba(255, 51, 102, 0.25);
|
|
36
|
+
--radius: 10px;
|
|
37
|
+
--font: 'JetBrains Mono', 'Consolas', monospace;
|
|
38
|
+
}
|
|
39
|
+
* { box-sizing: border-box; }
|
|
40
|
+
body {
|
|
41
|
+
font-family: var(--font);
|
|
42
|
+
font-size: 14px;
|
|
43
|
+
background: var(--bg);
|
|
44
|
+
color: var(--text);
|
|
45
|
+
margin: 0;
|
|
46
|
+
min-height: 100vh;
|
|
47
|
+
overflow-x: hidden;
|
|
48
|
+
}
|
|
49
|
+
body::before {
|
|
50
|
+
content: '';
|
|
51
|
+
position: fixed;
|
|
52
|
+
inset: 0;
|
|
53
|
+
background-image:
|
|
54
|
+
linear-gradient(rgba(0, 212, 255, 0.03) 1px, transparent 1px),
|
|
55
|
+
linear-gradient(90deg, rgba(0, 212, 255, 0.03) 1px, transparent 1px);
|
|
56
|
+
background-size: 24px 24px;
|
|
57
|
+
pointer-events: none;
|
|
58
|
+
z-index: 0;
|
|
59
|
+
}
|
|
60
|
+
.app {
|
|
61
|
+
position: relative;
|
|
62
|
+
z-index: 1;
|
|
63
|
+
max-width: 900px;
|
|
64
|
+
margin: 0 auto;
|
|
65
|
+
padding: 24px 20px 40px;
|
|
66
|
+
}
|
|
67
|
+
.header {
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: 12px;
|
|
71
|
+
margin-bottom: 24px;
|
|
72
|
+
padding-bottom: 16px;
|
|
73
|
+
border-bottom: 1px solid var(--border);
|
|
74
|
+
}
|
|
75
|
+
.logo {
|
|
76
|
+
font-weight: 600;
|
|
77
|
+
font-size: 1.35rem;
|
|
78
|
+
letter-spacing: 0.08em;
|
|
79
|
+
color: var(--accent);
|
|
80
|
+
text-shadow: 0 0 20px var(--accent-glow);
|
|
81
|
+
}
|
|
82
|
+
.logo span { color: var(--text-dim); font-weight: 400; }
|
|
83
|
+
.live-dot {
|
|
84
|
+
width: 8px;
|
|
85
|
+
height: 8px;
|
|
86
|
+
border-radius: 50%;
|
|
87
|
+
background: var(--success);
|
|
88
|
+
box-shadow: 0 0 12px var(--success-glow);
|
|
89
|
+
animation: pulse 2s ease-in-out infinite;
|
|
90
|
+
}
|
|
91
|
+
.live-label { font-size: 0.75rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.1em; }
|
|
92
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
93
|
+
|
|
94
|
+
.input-panel {
|
|
95
|
+
background: var(--bg-panel);
|
|
96
|
+
border: 1px solid var(--border);
|
|
97
|
+
border-radius: var(--radius);
|
|
98
|
+
padding: 4px 4px 4px 16px;
|
|
99
|
+
margin-bottom: 24px;
|
|
100
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255,255,255,0.03);
|
|
101
|
+
transition: box-shadow 0.2s, border-color 0.2s;
|
|
102
|
+
}
|
|
103
|
+
.input-panel:focus-within {
|
|
104
|
+
border-color: rgba(0, 212, 255, 0.5);
|
|
105
|
+
box-shadow: 0 0 0 1px rgba(0, 212, 255, 0.15), 0 4px 24px rgba(0, 0, 0, 0.3);
|
|
106
|
+
}
|
|
107
|
+
.input-wrap {
|
|
108
|
+
display: flex;
|
|
109
|
+
gap: 12px;
|
|
110
|
+
align-items: flex-end;
|
|
111
|
+
}
|
|
112
|
+
.prompt {
|
|
113
|
+
color: var(--accent);
|
|
114
|
+
font-size: 0.9rem;
|
|
115
|
+
padding-bottom: 12px;
|
|
116
|
+
flex-shrink: 0;
|
|
117
|
+
text-shadow: 0 0 8px var(--accent-glow);
|
|
118
|
+
}
|
|
119
|
+
textarea {
|
|
120
|
+
flex: 1;
|
|
121
|
+
min-height: 100px;
|
|
122
|
+
background: transparent;
|
|
123
|
+
color: var(--text);
|
|
124
|
+
border: none;
|
|
125
|
+
padding: 10px 0 12px;
|
|
126
|
+
font-family: var(--font);
|
|
127
|
+
font-size: 13px;
|
|
128
|
+
line-height: 1.55;
|
|
129
|
+
resize: vertical;
|
|
130
|
+
outline: none;
|
|
131
|
+
}
|
|
132
|
+
textarea::placeholder { color: var(--text-dim); }
|
|
133
|
+
.run-wrap { flex-shrink: 0; }
|
|
134
|
+
.run-btn {
|
|
135
|
+
background: linear-gradient(135deg, rgba(0, 212, 255, 0.2), rgba(0, 212, 255, 0.08));
|
|
136
|
+
color: var(--accent);
|
|
137
|
+
border: 1px solid rgba(0, 212, 255, 0.4);
|
|
138
|
+
padding: 10px 20px;
|
|
139
|
+
border-radius: 8px;
|
|
140
|
+
font-family: var(--font);
|
|
141
|
+
font-size: 13px;
|
|
142
|
+
font-weight: 500;
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
transition: all 0.2s;
|
|
145
|
+
box-shadow: 0 0 16px rgba(0, 212, 255, 0.1);
|
|
146
|
+
}
|
|
147
|
+
.run-btn:hover {
|
|
148
|
+
background: linear-gradient(135deg, rgba(0, 212, 255, 0.3), rgba(0, 212, 255, 0.15));
|
|
149
|
+
box-shadow: 0 0 24px var(--accent-glow);
|
|
150
|
+
border-color: var(--accent);
|
|
151
|
+
}
|
|
152
|
+
.run-btn:active { transform: scale(0.98); }
|
|
153
|
+
|
|
154
|
+
.outputs-title {
|
|
155
|
+
font-size: 0.7rem;
|
|
156
|
+
text-transform: uppercase;
|
|
157
|
+
letter-spacing: 0.12em;
|
|
158
|
+
color: var(--text-dim);
|
|
159
|
+
margin-bottom: 12px;
|
|
160
|
+
}
|
|
161
|
+
#outputs {
|
|
162
|
+
display: flex;
|
|
163
|
+
flex-direction: column;
|
|
164
|
+
gap: 16px;
|
|
165
|
+
}
|
|
166
|
+
.cell {
|
|
167
|
+
animation: cellIn 0.35s ease-out;
|
|
168
|
+
border-radius: var(--radius);
|
|
169
|
+
overflow: hidden;
|
|
170
|
+
border: 1px solid var(--border);
|
|
171
|
+
background: var(--surface);
|
|
172
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
173
|
+
}
|
|
174
|
+
@keyframes cellIn {
|
|
175
|
+
from { opacity: 0; transform: translateY(-8px); }
|
|
176
|
+
to { opacity: 1; transform: translateY(0); }
|
|
177
|
+
}
|
|
178
|
+
.cell-in {
|
|
179
|
+
padding: 12px 16px;
|
|
180
|
+
border-left: 3px solid var(--accent);
|
|
181
|
+
background: rgba(0, 212, 255, 0.04);
|
|
182
|
+
box-shadow: inset 0 1px 0 rgba(255,255,255,0.02);
|
|
183
|
+
}
|
|
184
|
+
.cell-in pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-size: 13px; line-height: 1.5; }
|
|
185
|
+
.cell-out {
|
|
186
|
+
margin: 0;
|
|
187
|
+
padding: 12px 16px;
|
|
188
|
+
min-height: 24px;
|
|
189
|
+
border-left: 3px solid transparent;
|
|
190
|
+
}
|
|
191
|
+
.cell-out.result {
|
|
192
|
+
background: rgba(0, 255, 159, 0.06);
|
|
193
|
+
border-left-color: var(--success);
|
|
194
|
+
box-shadow: inset 0 0 20px var(--success-glow);
|
|
195
|
+
}
|
|
196
|
+
.cell-out.error {
|
|
197
|
+
background: rgba(255, 51, 102, 0.06);
|
|
198
|
+
border-left-color: var(--error);
|
|
199
|
+
box-shadow: inset 0 0 20px var(--error-glow);
|
|
200
|
+
}
|
|
201
|
+
.cell-out pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-size: 13px; line-height: 1.5; }
|
|
202
|
+
.cell-out.error pre { color: #ff6b8a; }
|
|
203
|
+
</style>
|
|
204
|
+
</head>
|
|
205
|
+
<body>
|
|
206
|
+
<div class="app">
|
|
207
|
+
<header class="header">
|
|
208
|
+
<div class="logo">Dec<span>Python</span></div>
|
|
209
|
+
<div class="live-dot" title="已连接"></div>
|
|
210
|
+
<span class="live-label">Live</span>
|
|
211
|
+
</header>
|
|
212
|
+
<div class="input-panel">
|
|
213
|
+
<div class="input-wrap">
|
|
214
|
+
<span class="prompt">>>></span>
|
|
215
|
+
<textarea id="code" placeholder="# 输入代码,Shift+Enter 运行" spellcheck="false"></textarea>
|
|
216
|
+
<div class="run-wrap"><button type="button" class="run-btn" id="run">Run</button></div>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
<div class="outputs-title">Output</div>
|
|
220
|
+
<div id="outputs"></div>
|
|
221
|
+
</div>
|
|
222
|
+
<script>
|
|
223
|
+
const codeEl = document.getElementById('code');
|
|
224
|
+
const runBtn = document.getElementById('run');
|
|
225
|
+
const outputsEl = document.getElementById('outputs');
|
|
226
|
+
|
|
227
|
+
function addCell(input, result, error) {
|
|
228
|
+
const cell = document.createElement('div');
|
|
229
|
+
cell.className = 'cell';
|
|
230
|
+
const text = error ? error : (result || '');
|
|
231
|
+
const isError = !!error;
|
|
232
|
+
cell.innerHTML = '<div class="cell-in"><pre>' + escapeHtml(input) + '</pre></div><div class="cell-out ' + (isError ? 'error' : 'result') + '"><pre>' + escapeHtml(text) + '</pre></div>';
|
|
233
|
+
outputsEl.appendChild(cell);
|
|
234
|
+
outputsEl.scrollTop = outputsEl.scrollHeight;
|
|
235
|
+
}
|
|
236
|
+
function escapeHtml(s) {
|
|
237
|
+
if (s == null) return '';
|
|
238
|
+
const div = document.createElement('div');
|
|
239
|
+
div.textContent = s;
|
|
240
|
+
return div.innerHTML;
|
|
241
|
+
}
|
|
242
|
+
// SSE:同步历史与后续通过 send() 或页面运行产生的新 cell
|
|
243
|
+
const evtSource = new EventSource('/stream');
|
|
244
|
+
evtSource.addEventListener('history', function(e) {
|
|
245
|
+
const cells = JSON.parse(e.data || '[]');
|
|
246
|
+
cells.forEach(function(c) { addCell(c.code, c.result, c.error); });
|
|
247
|
+
});
|
|
248
|
+
evtSource.addEventListener('cell', function(e) {
|
|
249
|
+
const c = JSON.parse(e.data || '{}');
|
|
250
|
+
addCell(c.code, c.result, c.error);
|
|
251
|
+
});
|
|
252
|
+
evtSource.onerror = function() { evtSource.close(); };
|
|
253
|
+
|
|
254
|
+
async function run() {
|
|
255
|
+
const code = codeEl.value.trim();
|
|
256
|
+
if (!code) return;
|
|
257
|
+
codeEl.value = '';
|
|
258
|
+
try {
|
|
259
|
+
await fetch('/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code }) });
|
|
260
|
+
} catch (e) {
|
|
261
|
+
addCell(code, null, String(e));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
runBtn.addEventListener('click', run);
|
|
265
|
+
codeEl.addEventListener('keydown', function(e) {
|
|
266
|
+
if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); run(); }
|
|
267
|
+
});
|
|
268
|
+
</script>
|
|
269
|
+
</body>
|
|
270
|
+
</html>
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _find_free_port() -> int:
|
|
275
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
276
|
+
s.bind(("", 0))
|
|
277
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
278
|
+
return s.getsockname()[1]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _sse_send(wfile, event: str, data: str) -> None:
|
|
282
|
+
wfile.write(f"event: {event}\ndata: {data}\n\n".encode("utf-8"))
|
|
283
|
+
wfile.flush()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class _DecPythonHandler(BaseHTTPRequestHandler):
|
|
287
|
+
decpython_instance = None # 由服务器注入
|
|
288
|
+
|
|
289
|
+
def log_message(self, format: str, *args: object) -> None:
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
def do_GET(self) -> None:
|
|
293
|
+
if self.path == "/" or self.path == "/index.html":
|
|
294
|
+
self.send_response(200)
|
|
295
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
296
|
+
self.end_headers()
|
|
297
|
+
self.wfile.write(INDEX_HTML.encode("utf-8"))
|
|
298
|
+
return
|
|
299
|
+
if self.path == "/stream":
|
|
300
|
+
state = getattr(self.server, "state", None)
|
|
301
|
+
if not state:
|
|
302
|
+
self.send_response(500)
|
|
303
|
+
self.end_headers()
|
|
304
|
+
return
|
|
305
|
+
self.send_response(200)
|
|
306
|
+
self.send_header("Content-Type", "text/event-stream")
|
|
307
|
+
self.send_header("Cache-Control", "no-cache")
|
|
308
|
+
self.send_header("Connection", "keep-alive")
|
|
309
|
+
self.end_headers()
|
|
310
|
+
client_queue = queue.Queue()
|
|
311
|
+
state["queues"].append(client_queue)
|
|
312
|
+
try:
|
|
313
|
+
history_json = json.dumps(state["cells"], ensure_ascii=False)
|
|
314
|
+
_sse_send(self.wfile, "history", history_json)
|
|
315
|
+
while True:
|
|
316
|
+
item = client_queue.get()
|
|
317
|
+
if item is None:
|
|
318
|
+
break
|
|
319
|
+
_sse_send(self.wfile, "cell", json.dumps(item, ensure_ascii=False))
|
|
320
|
+
except (BrokenPipeError, ConnectionResetError, OSError):
|
|
321
|
+
pass
|
|
322
|
+
finally:
|
|
323
|
+
try:
|
|
324
|
+
state["queues"].remove(client_queue)
|
|
325
|
+
except ValueError:
|
|
326
|
+
pass
|
|
327
|
+
return
|
|
328
|
+
self.send_response(404)
|
|
329
|
+
self.end_headers()
|
|
330
|
+
|
|
331
|
+
def do_POST(self) -> None:
|
|
332
|
+
if self.path == "/run":
|
|
333
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
334
|
+
body = self.rfile.read(length).decode("utf-8") if length else "{}"
|
|
335
|
+
try:
|
|
336
|
+
data = json.loads(body)
|
|
337
|
+
code = data.get("code", "")
|
|
338
|
+
except Exception:
|
|
339
|
+
code = ""
|
|
340
|
+
result = ""
|
|
341
|
+
err = ""
|
|
342
|
+
dp = _DecPythonHandler.decpython_instance
|
|
343
|
+
if dp and not getattr(dp, "_closed", True):
|
|
344
|
+
try:
|
|
345
|
+
dp.send(code)
|
|
346
|
+
result = getattr(dp, "_last_result", "")
|
|
347
|
+
err = getattr(dp, "_last_error", "")
|
|
348
|
+
except Exception as e:
|
|
349
|
+
err = str(e)
|
|
350
|
+
else:
|
|
351
|
+
err = "终端已关闭"
|
|
352
|
+
# dp.send() already calls append_cell(); do not append/push again here
|
|
353
|
+
self.send_response(200)
|
|
354
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
355
|
+
self.end_headers()
|
|
356
|
+
self.wfile.write(json.dumps({"result": result, "error": err}, ensure_ascii=False).encode("utf-8"))
|
|
357
|
+
return
|
|
358
|
+
self.send_response(404)
|
|
359
|
+
self.end_headers()
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class _GUIServer:
|
|
363
|
+
def __init__(self, decpython_instance: object, port: int) -> None:
|
|
364
|
+
self._server = ThreadingHTTPServer(("127.0.0.1", port), _DecPythonHandler)
|
|
365
|
+
self._server.state = {"cells": [], "queues": []}
|
|
366
|
+
_DecPythonHandler.decpython_instance = decpython_instance
|
|
367
|
+
self._decpython = decpython_instance
|
|
368
|
+
self.port = port
|
|
369
|
+
self.url = f"http://127.0.0.1:{port}/"
|
|
370
|
+
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
|
|
371
|
+
self._thread.start()
|
|
372
|
+
|
|
373
|
+
def append_cell(self, code: str, result: str, error: str) -> None:
|
|
374
|
+
cell = {"code": code, "result": result or "", "error": error or ""}
|
|
375
|
+
self._server.state["cells"].append(cell)
|
|
376
|
+
for q in self._server.state["queues"]:
|
|
377
|
+
try:
|
|
378
|
+
q.put(cell)
|
|
379
|
+
except Exception:
|
|
380
|
+
pass
|
|
381
|
+
|
|
382
|
+
def shutdown(self) -> None:
|
|
383
|
+
if self._server:
|
|
384
|
+
for q in self._server.state.get("queues", []):
|
|
385
|
+
try:
|
|
386
|
+
q.put(None)
|
|
387
|
+
except Exception:
|
|
388
|
+
pass
|
|
389
|
+
self._server.shutdown()
|
|
390
|
+
self._server = None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def start_gui_server(decpython_instance: object) -> _GUIServer:
|
|
394
|
+
port = _find_free_port()
|
|
395
|
+
return _GUIServer(decpython_instance, port)
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
decpython/__init__.py
|
|
5
|
+
decpython/core.py
|
|
6
|
+
decpython.egg-info/PKG-INFO
|
|
7
|
+
decpython.egg-info/SOURCES.txt
|
|
8
|
+
decpython.egg-info/dependency_links.txt
|
|
9
|
+
decpython.egg-info/requires.txt
|
|
10
|
+
decpython.egg-info/top_level.txt
|
|
11
|
+
decpython/gui/__init__.py
|
|
12
|
+
decpython/gui/server.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
decpython
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "decpython"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "跨平台、支持 Web 通信的 Python 终端工具"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "decpython" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = ["pytest>=7", "ruff>=0.1"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["."]
|
|
30
|
+
include = ["decpython*"]
|
decpython-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
# Publish to PyPI: pip install twine && python setup.py upload
|
|
5
|
+
|
|
6
|
+
import io
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from shutil import rmtree
|
|
10
|
+
from setuptools import find_packages, setup, Command
|
|
11
|
+
|
|
12
|
+
NAME = 'decpython'
|
|
13
|
+
DESCRIPTION = 'Run Python in a subprocess from your code and get string results, or open a Jupyter-style web terminal. Cross-platform, stdlib only.'
|
|
14
|
+
URL = 'https://github.com/your-username/decpython_maskter'
|
|
15
|
+
EMAIL = 'decrule@outlook.com'
|
|
16
|
+
AUTHOR = 'DecRule'
|
|
17
|
+
REQUIRES_PYTHON = '>=3.9.0'
|
|
18
|
+
VERSION = '1.0.0'
|
|
19
|
+
|
|
20
|
+
REQUIRED = []
|
|
21
|
+
EXTRAS = {'dev': ['pytest>=7', 'ruff>=0.1']}
|
|
22
|
+
|
|
23
|
+
here = os.path.abspath(os.path.dirname(__file__))
|
|
24
|
+
try:
|
|
25
|
+
with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f:
|
|
26
|
+
long_description = '\n' + f.read()
|
|
27
|
+
except FileNotFoundError:
|
|
28
|
+
long_description = DESCRIPTION
|
|
29
|
+
|
|
30
|
+
about = {'__version__': VERSION}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class UploadCommand(Command):
|
|
34
|
+
"""Support setup.py upload."""
|
|
35
|
+
|
|
36
|
+
description = 'Build and publish the package.'
|
|
37
|
+
user_options = []
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def status(s):
|
|
41
|
+
print('\033[1m{0}\033[0m'.format(s))
|
|
42
|
+
|
|
43
|
+
def initialize_options(self):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
def finalize_options(self):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
def run(self):
|
|
50
|
+
try:
|
|
51
|
+
self.status('Removing previous builds…')
|
|
52
|
+
rmtree(os.path.join(here, 'dist'))
|
|
53
|
+
except OSError:
|
|
54
|
+
pass
|
|
55
|
+
self.status('Building Source and Wheel distribution…')
|
|
56
|
+
os.system('{0} setup.py sdist bdist_wheel'.format(sys.executable))
|
|
57
|
+
self.status('Uploading the package to PyPI via Twine…')
|
|
58
|
+
os.system('twine upload dist/*')
|
|
59
|
+
self.status('Pushing git tags…')
|
|
60
|
+
os.system('git tag v{0}'.format(about['__version__']))
|
|
61
|
+
os.system('git push --tags')
|
|
62
|
+
sys.exit()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
setup(
|
|
66
|
+
name=NAME,
|
|
67
|
+
version=about['__version__'],
|
|
68
|
+
description=DESCRIPTION,
|
|
69
|
+
long_description=long_description,
|
|
70
|
+
long_description_content_type='text/markdown',
|
|
71
|
+
author=AUTHOR,
|
|
72
|
+
author_email=EMAIL,
|
|
73
|
+
python_requires=REQUIRES_PYTHON,
|
|
74
|
+
url=URL,
|
|
75
|
+
packages=find_packages(exclude=['tests', '*.tests', '*.tests.*', 'tests.*']),
|
|
76
|
+
install_requires=REQUIRED,
|
|
77
|
+
extras_require=EXTRAS,
|
|
78
|
+
include_package_data=True,
|
|
79
|
+
license='MIT',
|
|
80
|
+
classifiers=[
|
|
81
|
+
'Development Status :: 3 - Alpha',
|
|
82
|
+
'Intended Audience :: Developers',
|
|
83
|
+
'License :: OSI Approved :: MIT License',
|
|
84
|
+
'Programming Language :: Python :: 3',
|
|
85
|
+
'Programming Language :: Python :: 3.9',
|
|
86
|
+
'Programming Language :: Python :: 3.10',
|
|
87
|
+
'Programming Language :: Python :: 3.11',
|
|
88
|
+
'Programming Language :: Python :: 3.12',
|
|
89
|
+
],
|
|
90
|
+
cmdclass={'upload': UploadCommand},
|
|
91
|
+
)
|