py2max 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py2max/__init__.py +67 -0
- py2max/__main__.py +6 -0
- py2max/cli.py +1251 -0
- py2max/core/__init__.py +39 -0
- py2max/core/abstract.py +146 -0
- py2max/core/box.py +231 -0
- py2max/core/common.py +19 -0
- py2max/core/patcher.py +1658 -0
- py2max/core/patchline.py +68 -0
- py2max/exceptions.py +385 -0
- py2max/export/__init__.py +20 -0
- py2max/export/converters.py +345 -0
- py2max/export/svg.py +393 -0
- py2max/layout/__init__.py +26 -0
- py2max/layout/base.py +463 -0
- py2max/layout/flow.py +405 -0
- py2max/layout/grid.py +374 -0
- py2max/layout/matrix.py +628 -0
- py2max/log.py +338 -0
- py2max/maxref/__init__.py +78 -0
- py2max/maxref/category.py +163 -0
- py2max/maxref/db.py +1082 -0
- py2max/maxref/legacy.py +324 -0
- py2max/maxref/parser.py +703 -0
- py2max/py.typed +0 -0
- py2max/server/__init__.py +54 -0
- py2max/server/client.py +295 -0
- py2max/server/inline.py +312 -0
- py2max/server/repl.py +561 -0
- py2max/server/rpc.py +240 -0
- py2max/server/websocket.py +997 -0
- py2max/static/cola.min.js +4 -0
- py2max/static/d3.v7.min.js +2 -0
- py2max/static/dagre-bundle.js +328 -0
- py2max/static/elk.bundled.js +6663 -0
- py2max/static/index.html +168 -0
- py2max/static/interactive.html +589 -0
- py2max/static/interactive.js +2111 -0
- py2max/static/live-preview.js +324 -0
- py2max/static/svg.min.js +13 -0
- py2max/static/svg.min.js.map +1 -0
- py2max/transformers.py +168 -0
- py2max/utils.py +83 -0
- py2max-0.2.1.dist-info/METADATA +390 -0
- py2max-0.2.1.dist-info/RECORD +48 -0
- py2max-0.2.1.dist-info/WHEEL +4 -0
- py2max-0.2.1.dist-info/entry_points.txt +3 -0
- py2max-0.2.1.dist-info/licenses/LICENSE +19 -0
py2max/server/rpc.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Remote REPL server for py2max.
|
|
2
|
+
|
|
3
|
+
This module provides a WebSocket-based RPC server that allows remote
|
|
4
|
+
REPL clients to execute code against a running patcher instance.
|
|
5
|
+
|
|
6
|
+
The server listens on a separate port (default: 9000) and executes
|
|
7
|
+
code sent by REPL clients, returning results.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
import traceback as tb
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Dict
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ..core import Patcher
|
|
20
|
+
from .websocket import InteractivePatcherServer
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import websockets
|
|
24
|
+
from websockets.asyncio.server import ServerConnection, serve
|
|
25
|
+
except ImportError:
|
|
26
|
+
raise ImportError(
|
|
27
|
+
"websockets package required for REPL server. "
|
|
28
|
+
"Install with: pip install websockets"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ReplServer:
|
|
33
|
+
"""WebSocket-based REPL server for remote code execution."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, patcher: "Patcher", server: "InteractivePatcherServer"):
|
|
36
|
+
self.patcher = patcher
|
|
37
|
+
self.server = server
|
|
38
|
+
self.clients: set[ServerConnection] = set()
|
|
39
|
+
self._lock = asyncio.Lock()
|
|
40
|
+
|
|
41
|
+
# Build execution namespace with patcher and commands
|
|
42
|
+
from .repl import ReplCommands
|
|
43
|
+
|
|
44
|
+
self.commands = ReplCommands(patcher, server)
|
|
45
|
+
self.namespace: Dict[str, Any] = {
|
|
46
|
+
"p": patcher,
|
|
47
|
+
"patcher": patcher,
|
|
48
|
+
"server": server,
|
|
49
|
+
"asyncio": asyncio,
|
|
50
|
+
# Command functions
|
|
51
|
+
"save": self.commands.save,
|
|
52
|
+
"info": self.commands.info,
|
|
53
|
+
"layout": self.commands.layout,
|
|
54
|
+
"optimize": self.commands.optimize,
|
|
55
|
+
"clear": self.commands.clear,
|
|
56
|
+
"clients": self.commands.clients,
|
|
57
|
+
"help_obj": self.commands.help_obj,
|
|
58
|
+
"commands": self.commands.commands,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async def handle_client(self, websocket: ServerConnection):
|
|
62
|
+
"""Handle REPL client connection."""
|
|
63
|
+
async with self._lock:
|
|
64
|
+
self.clients.add(websocket)
|
|
65
|
+
print(f"REPL client connected (total: {len(self.clients)})")
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
async for message in websocket:
|
|
69
|
+
msg_str = (
|
|
70
|
+
message.decode("utf-8") if isinstance(message, bytes) else message
|
|
71
|
+
)
|
|
72
|
+
await self.handle_message(websocket, msg_str)
|
|
73
|
+
|
|
74
|
+
except websockets.exceptions.ConnectionClosed:
|
|
75
|
+
pass
|
|
76
|
+
finally:
|
|
77
|
+
async with self._lock:
|
|
78
|
+
self.clients.discard(websocket)
|
|
79
|
+
print(f"REPL client disconnected (total: {len(self.clients)})")
|
|
80
|
+
|
|
81
|
+
async def handle_message(self, websocket: ServerConnection, message: str):
|
|
82
|
+
"""Handle message from REPL client."""
|
|
83
|
+
try:
|
|
84
|
+
data = json.loads(message)
|
|
85
|
+
msg_type = data.get("type")
|
|
86
|
+
|
|
87
|
+
if msg_type == "init":
|
|
88
|
+
await self.handle_init(websocket)
|
|
89
|
+
elif msg_type == "eval":
|
|
90
|
+
await self.handle_eval(websocket, data)
|
|
91
|
+
else:
|
|
92
|
+
await websocket.send(
|
|
93
|
+
json.dumps(
|
|
94
|
+
{"type": "error", "error": f"Unknown message type: {msg_type}"}
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
except json.JSONDecodeError as e:
|
|
99
|
+
await websocket.send(
|
|
100
|
+
json.dumps({"type": "error", "error": f"Invalid JSON: {e}"})
|
|
101
|
+
)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
await websocket.send(
|
|
104
|
+
json.dumps({"type": "error", "error": f"Error handling message: {e}"})
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
async def handle_init(self, websocket: ServerConnection):
|
|
108
|
+
"""Handle initialization request from client."""
|
|
109
|
+
# Send patcher info
|
|
110
|
+
info = {
|
|
111
|
+
"patcher_path": str(self.patcher.filepath)
|
|
112
|
+
if self.patcher.filepath
|
|
113
|
+
else "Untitled",
|
|
114
|
+
"num_objects": len(self.patcher._boxes),
|
|
115
|
+
"num_connections": len(self.patcher._lines),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await websocket.send(json.dumps({"type": "init_response", "info": info}))
|
|
119
|
+
|
|
120
|
+
async def handle_eval(self, websocket: ServerConnection, data: dict):
|
|
121
|
+
"""Handle code evaluation request."""
|
|
122
|
+
code = data.get("code", "")
|
|
123
|
+
# mode = data.get("mode", "exec") # Reserved for future "exec" or "eval" mode
|
|
124
|
+
|
|
125
|
+
if not code.strip():
|
|
126
|
+
await websocket.send(
|
|
127
|
+
json.dumps({"type": "result", "result": None, "display": None})
|
|
128
|
+
)
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
# Capture output
|
|
132
|
+
output_lines = []
|
|
133
|
+
|
|
134
|
+
class OutputCapture:
|
|
135
|
+
def write(self, text):
|
|
136
|
+
output_lines.append(text)
|
|
137
|
+
|
|
138
|
+
def flush(self):
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
# Redirect stdout temporarily
|
|
142
|
+
old_stdout = sys.stdout
|
|
143
|
+
old_stderr = sys.stderr
|
|
144
|
+
sys.stdout = OutputCapture()
|
|
145
|
+
sys.stderr = OutputCapture()
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
# Try eval first (for expressions)
|
|
149
|
+
try:
|
|
150
|
+
result = eval(code, self.namespace)
|
|
151
|
+
|
|
152
|
+
# Handle async results
|
|
153
|
+
if asyncio.iscoroutine(result):
|
|
154
|
+
result = await result
|
|
155
|
+
|
|
156
|
+
# Get string representation
|
|
157
|
+
result_str = repr(result) if result is not None else None
|
|
158
|
+
|
|
159
|
+
# Get captured output
|
|
160
|
+
display = "".join(output_lines) if output_lines else None
|
|
161
|
+
|
|
162
|
+
await websocket.send(
|
|
163
|
+
json.dumps(
|
|
164
|
+
{
|
|
165
|
+
"type": "result",
|
|
166
|
+
"result": result_str,
|
|
167
|
+
"display": display,
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
except SyntaxError:
|
|
173
|
+
# Fall back to exec (for statements)
|
|
174
|
+
exec(code, self.namespace)
|
|
175
|
+
|
|
176
|
+
# Get captured output
|
|
177
|
+
display = "".join(output_lines) if output_lines else None
|
|
178
|
+
|
|
179
|
+
await websocket.send(
|
|
180
|
+
json.dumps(
|
|
181
|
+
{
|
|
182
|
+
"type": "result",
|
|
183
|
+
"result": None,
|
|
184
|
+
"display": display,
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
# Get traceback
|
|
191
|
+
traceback_str = tb.format_exc()
|
|
192
|
+
|
|
193
|
+
await websocket.send(
|
|
194
|
+
json.dumps(
|
|
195
|
+
{
|
|
196
|
+
"type": "error",
|
|
197
|
+
"error": str(e),
|
|
198
|
+
"traceback": traceback_str,
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
finally:
|
|
204
|
+
# Restore stdout/stderr
|
|
205
|
+
sys.stdout = old_stdout
|
|
206
|
+
sys.stderr = old_stderr
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
async def start_repl_server(
|
|
210
|
+
patcher: "Patcher",
|
|
211
|
+
server: "InteractivePatcherServer",
|
|
212
|
+
port: int = 9000,
|
|
213
|
+
) -> Any:
|
|
214
|
+
"""Start REPL server.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
patcher: Patcher instance
|
|
218
|
+
server: WebSocket server instance
|
|
219
|
+
port: REPL server port (default: 9000)
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Server instance
|
|
223
|
+
"""
|
|
224
|
+
repl_server = ReplServer(patcher, server)
|
|
225
|
+
|
|
226
|
+
# Start WebSocket server
|
|
227
|
+
ws_server = await serve(
|
|
228
|
+
repl_server.handle_client,
|
|
229
|
+
"localhost",
|
|
230
|
+
port,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
print(f"REPL server started on port {port}")
|
|
234
|
+
print(f"Connect with: py2max repl localhost:{port}")
|
|
235
|
+
print()
|
|
236
|
+
|
|
237
|
+
return ws_server
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
__all__ = ["ReplServer", "start_repl_server"]
|