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.
Files changed (48) hide show
  1. py2max/__init__.py +67 -0
  2. py2max/__main__.py +6 -0
  3. py2max/cli.py +1251 -0
  4. py2max/core/__init__.py +39 -0
  5. py2max/core/abstract.py +146 -0
  6. py2max/core/box.py +231 -0
  7. py2max/core/common.py +19 -0
  8. py2max/core/patcher.py +1658 -0
  9. py2max/core/patchline.py +68 -0
  10. py2max/exceptions.py +385 -0
  11. py2max/export/__init__.py +20 -0
  12. py2max/export/converters.py +345 -0
  13. py2max/export/svg.py +393 -0
  14. py2max/layout/__init__.py +26 -0
  15. py2max/layout/base.py +463 -0
  16. py2max/layout/flow.py +405 -0
  17. py2max/layout/grid.py +374 -0
  18. py2max/layout/matrix.py +628 -0
  19. py2max/log.py +338 -0
  20. py2max/maxref/__init__.py +78 -0
  21. py2max/maxref/category.py +163 -0
  22. py2max/maxref/db.py +1082 -0
  23. py2max/maxref/legacy.py +324 -0
  24. py2max/maxref/parser.py +703 -0
  25. py2max/py.typed +0 -0
  26. py2max/server/__init__.py +54 -0
  27. py2max/server/client.py +295 -0
  28. py2max/server/inline.py +312 -0
  29. py2max/server/repl.py +561 -0
  30. py2max/server/rpc.py +240 -0
  31. py2max/server/websocket.py +997 -0
  32. py2max/static/cola.min.js +4 -0
  33. py2max/static/d3.v7.min.js +2 -0
  34. py2max/static/dagre-bundle.js +328 -0
  35. py2max/static/elk.bundled.js +6663 -0
  36. py2max/static/index.html +168 -0
  37. py2max/static/interactive.html +589 -0
  38. py2max/static/interactive.js +2111 -0
  39. py2max/static/live-preview.js +324 -0
  40. py2max/static/svg.min.js +13 -0
  41. py2max/static/svg.min.js.map +1 -0
  42. py2max/transformers.py +168 -0
  43. py2max/utils.py +83 -0
  44. py2max-0.2.1.dist-info/METADATA +390 -0
  45. py2max-0.2.1.dist-info/RECORD +48 -0
  46. py2max-0.2.1.dist-info/WHEEL +4 -0
  47. py2max-0.2.1.dist-info/entry_points.txt +3 -0
  48. 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"]