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/py.typed ADDED
File without changes
@@ -0,0 +1,54 @@
1
+ """Interactive server and REPL for py2max.
2
+
3
+ This subpackage provides WebSocket-based server and REPL functionality
4
+ for real-time interactive editing of Max patches.
5
+
6
+ Modules:
7
+ - websocket: WebSocket server for bidirectional communication
8
+ - repl: Interactive REPL using ptpython
9
+ - client: Remote REPL client
10
+ - inline: Inline REPL with background server
11
+ - rpc: Remote procedure call server for REPL
12
+ """
13
+
14
+ from .websocket import (
15
+ InteractivePatcherServer,
16
+ InteractiveWebSocketHandler,
17
+ get_patcher_state_json,
18
+ serve_interactive,
19
+ )
20
+ from .repl import AutoSyncWrapper, ReplCommands, start_repl, start_repl_with_refresh
21
+ from .client import ReplClient, start_repl_client
22
+ from .inline import (
23
+ BackgroundServerREPL,
24
+ restore_console_logging,
25
+ setup_file_logging,
26
+ start_background_server_repl,
27
+ start_inline_repl,
28
+ )
29
+ from .rpc import ReplServer, start_repl_server
30
+
31
+ __all__ = [
32
+ # WebSocket server
33
+ "InteractivePatcherServer",
34
+ "InteractiveWebSocketHandler",
35
+ "get_patcher_state_json",
36
+ "serve_interactive",
37
+ # REPL
38
+ "AutoSyncWrapper",
39
+ "ReplCommands",
40
+ "start_repl",
41
+ "start_repl_with_refresh",
42
+ # Client
43
+ "ReplClient",
44
+ "start_repl_client",
45
+ # Inline
46
+ "BackgroundServerREPL",
47
+ "start_inline_repl",
48
+ "start_background_server_repl",
49
+ "setup_file_logging",
50
+ "restore_console_logging",
51
+ # RPC
52
+ "ReplServer",
53
+ "start_repl_server",
54
+ ]
@@ -0,0 +1,295 @@
1
+ """Remote REPL client for py2max.
2
+
3
+ This module provides a client that connects to a running py2max server
4
+ and provides an interactive REPL in a separate terminal, keeping server
5
+ logs separate from the REPL interface.
6
+
7
+ Usage:
8
+ # Terminal 1: Start server
9
+ $ py2max serve my-patch.maxpat
10
+
11
+ # Terminal 2: Connect REPL
12
+ $ py2max repl localhost:9000
13
+
14
+ Architecture:
15
+ - Server runs HTTP (8000), WebSocket (8001), and REPL (9000)
16
+ - REPL client connects to port 9000 via WebSocket
17
+ - Commands sent to server for execution
18
+ - Results returned and displayed in client
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import sys
25
+ from typing import Any, Dict, Optional
26
+
27
+ try:
28
+ from websockets.asyncio.client import ClientConnection, connect
29
+ except ImportError:
30
+ raise ImportError(
31
+ "websockets package required for REPL client. "
32
+ "Install with: pip install websockets"
33
+ )
34
+
35
+
36
+ class ReplClient:
37
+ """Remote REPL client that connects to py2max server."""
38
+
39
+ def __init__(self, host: str = "localhost", port: int = 9000):
40
+ self.host = host
41
+ self.port = port
42
+ self.ws: Optional[ClientConnection] = None
43
+ self.namespace: Dict[str, Any] = {}
44
+ self.connected = False
45
+
46
+ async def connect(self):
47
+ """Connect to remote REPL server."""
48
+ uri = f"ws://{self.host}:{self.port}"
49
+ print(f"Connecting to py2max server at {uri}...")
50
+
51
+ try:
52
+ self.ws = await connect(uri)
53
+ self.connected = True
54
+
55
+ # Get initial state from server
56
+ await self.ws.send(json.dumps({"type": "init"}))
57
+ response = await self.ws.recv()
58
+ data = json.loads(response)
59
+
60
+ if data.get("type") == "init_response":
61
+ info = data.get("info", {})
62
+ print()
63
+ print("=" * 70)
64
+ print("py2max Remote REPL")
65
+ print("=" * 70)
66
+ print()
67
+ print(f"Connected to: {self.host}:{self.port}")
68
+ print(f"Patcher: {info.get('patcher_path', 'Unknown')}")
69
+ print(f"Objects: {info.get('num_objects', 0)} boxes")
70
+ print(f"Connections: {info.get('num_connections', 0)} lines")
71
+ print()
72
+ print("Available commands:")
73
+ print(" save() - Save patcher")
74
+ print(" info() - Show patcher info")
75
+ print(" layout(type) - Change layout")
76
+ print(" optimize() - Optimize layout")
77
+ print(" clear() - Clear all objects")
78
+ print(" help_obj(name) - Show Max object help")
79
+ print()
80
+ print("Use Ctrl+D to exit")
81
+ print("=" * 70)
82
+ print()
83
+
84
+ except Exception as e:
85
+ print(f"Error connecting to server: {e}", file=sys.stderr)
86
+ print()
87
+ print("Make sure the server is running with:")
88
+ print(" py2max serve <patch.maxpat>")
89
+ print()
90
+ raise
91
+
92
+ async def disconnect(self):
93
+ """Disconnect from server."""
94
+ if self.ws:
95
+ await self.ws.close()
96
+ self.connected = False
97
+
98
+ async def execute(self, code: str) -> Any:
99
+ """Execute code on remote server.
100
+
101
+ Args:
102
+ code: Python code to execute
103
+
104
+ Returns:
105
+ Result from server
106
+ """
107
+ if not self.connected or self.ws is None:
108
+ print("Not connected to server", file=sys.stderr)
109
+ return None
110
+
111
+ try:
112
+ # Send code to server
113
+ await self.ws.send(
114
+ json.dumps({"type": "eval", "code": code, "mode": "exec"})
115
+ )
116
+
117
+ # Get response
118
+ response = await self.ws.recv()
119
+ data = json.loads(response)
120
+
121
+ if data.get("type") == "error":
122
+ error = data.get("error", "Unknown error")
123
+ traceback = data.get("traceback", "")
124
+ print(f"Error: {error}", file=sys.stderr)
125
+ if traceback:
126
+ print(traceback, file=sys.stderr)
127
+ return None
128
+
129
+ elif data.get("type") == "result":
130
+ result = data.get("result")
131
+ display = data.get("display")
132
+
133
+ # Print display if available
134
+ if display:
135
+ print(display)
136
+
137
+ return result
138
+
139
+ else:
140
+ print(f"Unknown response type: {data.get('type')}", file=sys.stderr)
141
+ return None
142
+
143
+ except Exception as e:
144
+ print(f"Error executing code: {e}", file=sys.stderr)
145
+ return None
146
+
147
+ async def run_repl(self):
148
+ """Run interactive REPL using ptpython."""
149
+
150
+ # Create proxy functions that execute remotely
151
+ async def remote_eval(code: str) -> Any:
152
+ """Evaluate code on remote server."""
153
+ return await self.execute(code)
154
+
155
+ # Build namespace with remote execution (reserved for future use)
156
+ _namespace = { # noqa: F841
157
+ "__name__": "__main__",
158
+ "__doc__": None,
159
+ "_remote_eval": remote_eval,
160
+ }
161
+
162
+ def configure(repl):
163
+ """Configure ptpython."""
164
+ repl.show_signature = True
165
+ repl.enable_auto_suggest = True
166
+ repl.show_docstring = True
167
+ repl.complete_while_typing = False # Disable - can't complete remote
168
+ repl.enable_history_search = True
169
+ repl.vi_mode = False
170
+ repl.enable_syntax_highlighting = True
171
+ repl.highlight_matching_parenthesis = True
172
+ repl.enable_mouse_support = True
173
+
174
+ # Custom input handler that sends to server
175
+ # Note: This is a simplified version - full implementation would need
176
+ # to intercept ptpython's execution and route through websocket
177
+ print("Starting REPL...")
178
+ print()
179
+
180
+ try:
181
+ # Use ptpython with syntax highlighting and history
182
+ await self._ptpython_repl()
183
+
184
+ except (EOFError, KeyboardInterrupt):
185
+ print()
186
+ print("Exiting REPL...")
187
+
188
+ async def _ptpython_repl(self) -> None:
189
+ """Full-featured ptpython REPL with remote execution.
190
+
191
+ Uses ptpython for editing features (syntax highlighting, completion)
192
+ but executes code on the remote server.
193
+ """
194
+ # Create a remote execution namespace (reserved for future use)
195
+ _namespace = { # noqa: F841
196
+ "_client": self,
197
+ "_execute": self.execute,
198
+ }
199
+
200
+ # Custom evaluator that executes remotely (reserved for future use)
201
+ class RemoteEvaluator:
202
+ def __init__(self, client: "ReplClient") -> None:
203
+ self.client = client
204
+
205
+ async def __call__(self, code: str) -> Optional[str]:
206
+ """Execute code on remote server."""
207
+ if not code.strip():
208
+ return None
209
+
210
+ # Execute on server
211
+ result = await self.client.execute(code)
212
+ return result
213
+
214
+ _evaluator = RemoteEvaluator(self) # noqa: F841
215
+
216
+ # Configure ptpython
217
+ def configure(repl: Any) -> None:
218
+ """Configure ptpython REPL."""
219
+ # Enable auto-completion
220
+ repl.enable_auto_suggest = True
221
+ repl.complete_while_typing = False # Disable since completion is local
222
+
223
+ # Enable syntax highlighting
224
+ repl.enable_syntax_highlighting = True
225
+
226
+ # Show signature
227
+ repl.show_signature = True
228
+
229
+ # Vi mode (optional - can be toggled with F4)
230
+ repl.vi_mode = False
231
+
232
+ # Run ptpython REPL with remote execution
233
+ # Note: We use a custom event loop integration
234
+ while True:
235
+ try:
236
+ # Get input using ptpython
237
+ from prompt_toolkit import PromptSession
238
+ from prompt_toolkit.lexers import PygmentsLexer
239
+ from pygments.lexers.python import PythonLexer # type: ignore[import-untyped]
240
+
241
+ session: PromptSession[str] = PromptSession(
242
+ message="py2max[remote]>>> ",
243
+ lexer=PygmentsLexer(PythonLexer),
244
+ enable_history_search=True,
245
+ complete_while_typing=False,
246
+ )
247
+
248
+ code = await session.prompt_async()
249
+
250
+ if not code.strip():
251
+ continue
252
+
253
+ # Handle exit
254
+ if code.strip() in ("exit", "exit()", "quit", "quit()"):
255
+ break
256
+
257
+ # Execute on server
258
+ result = await self.execute(code)
259
+
260
+ # Display result if not None
261
+ if result is not None:
262
+ print(result)
263
+
264
+ except EOFError:
265
+ break
266
+ except KeyboardInterrupt:
267
+ print()
268
+ continue
269
+
270
+
271
+ async def start_repl_client(host: str = "localhost", port: int = 9000):
272
+ """Start remote REPL client.
273
+
274
+ Args:
275
+ host: Server hostname
276
+ port: REPL server port (default: 9000)
277
+
278
+ Example:
279
+ >>> await start_repl_client("localhost", 9000)
280
+ """
281
+ client = ReplClient(host, port)
282
+
283
+ try:
284
+ await client.connect()
285
+ await client.run_repl()
286
+ except Exception as e:
287
+ print(f"REPL client error: {e}", file=sys.stderr)
288
+ return 1
289
+ finally:
290
+ await client.disconnect()
291
+
292
+ return 0
293
+
294
+
295
+ __all__ = ["ReplClient", "start_repl_client"]
@@ -0,0 +1,312 @@
1
+ """Inline REPL with background server and log redirection.
2
+
3
+ This module provides an inline REPL mode where the server runs in the
4
+ background with logs redirected to a file, allowing a clean single-terminal
5
+ experience.
6
+
7
+ Usage:
8
+ $ py2max serve my-patch.maxpat --repl --log-file server.log
9
+
10
+ Features:
11
+ - Single terminal (no need for separate client terminal)
12
+ - Server logs redirected to file
13
+ - Clean REPL interface
14
+ - Can tail log file in separate terminal if needed
15
+
16
+ Architecture:
17
+ ┌─────────────────────────────────────┐
18
+ │ Single Terminal │
19
+ │ │
20
+ │ ┌─────────────────────────────────┐ │
21
+ │ │ Server (background thread) │ │
22
+ │ │ - HTTP: 8000 │ │
23
+ │ │ - WebSocket: 8001 │ │
24
+ │ │ - Logs → server.log │ │
25
+ │ └─────────────────────────────────┘ │
26
+ │ │
27
+ │ ┌─────────────────────────────────┐ │
28
+ │ │ REPL (foreground, ptpython) │ │
29
+ │ │ py2max[demo.maxpat]>>> │ │
30
+ │ │ [clean, no logs] │ │
31
+ │ └─────────────────────────────────┘ │
32
+ └─────────────────────────────────────┘
33
+
34
+ Optional: $ tail -f server.log (in another terminal)
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import asyncio
40
+ import logging
41
+ import sys
42
+ import threading
43
+ from pathlib import Path
44
+ from typing import TYPE_CHECKING, Optional
45
+
46
+ if TYPE_CHECKING:
47
+ from ..core import Patcher
48
+ from .websocket import InteractivePatcherServer
49
+
50
+
51
+ def setup_file_logging(log_file: Path, level: int = logging.INFO) -> logging.Handler:
52
+ """Setup file logging for server.
53
+
54
+ Args:
55
+ log_file: Path to log file
56
+ level: Logging level (default: INFO)
57
+
58
+ Returns:
59
+ File handler that was added
60
+ """
61
+ # Get root logger
62
+ root_logger = logging.getLogger()
63
+
64
+ # Remove existing handlers (console output)
65
+ for handler in root_logger.handlers[:]:
66
+ root_logger.removeHandler(handler)
67
+
68
+ # Add file handler
69
+ file_handler = logging.FileHandler(log_file, mode="a")
70
+ file_handler.setLevel(level)
71
+
72
+ # Format with timestamp
73
+ formatter = logging.Formatter(
74
+ "[%(asctime)s] - %(levelname)s - %(name)s - %(message)s",
75
+ datefmt="%H:%M:%S",
76
+ )
77
+ file_handler.setFormatter(formatter)
78
+
79
+ root_logger.addHandler(file_handler)
80
+ root_logger.setLevel(level)
81
+
82
+ return file_handler
83
+
84
+
85
+ def restore_console_logging():
86
+ """Restore console logging (remove file handler)."""
87
+ root_logger = logging.getLogger()
88
+
89
+ # Remove file handlers
90
+ for handler in root_logger.handlers[:]:
91
+ if isinstance(handler, logging.FileHandler):
92
+ handler.close()
93
+ root_logger.removeHandler(handler)
94
+
95
+ # Add console handler back
96
+ console_handler = logging.StreamHandler(sys.stderr)
97
+ console_handler.setLevel(logging.INFO)
98
+ formatter = logging.Formatter(
99
+ "\x1b[97;20m%(asctime)s\x1b[0m - "
100
+ "\x1b[38;20m%(levelname)s\x1b[0m - "
101
+ "\x1b[97;20m%(name)s\x1b[0m - "
102
+ "\x1b[38;20m%(message)s\x1b[0m",
103
+ datefmt="%H:%M:%S",
104
+ )
105
+ console_handler.setFormatter(formatter)
106
+ root_logger.addHandler(console_handler)
107
+
108
+
109
+ async def start_inline_repl(
110
+ patcher: "Patcher",
111
+ server: "InteractivePatcherServer",
112
+ log_file: Optional[Path] = None,
113
+ ) -> None:
114
+ """Start inline REPL with server running in background.
115
+
116
+ Args:
117
+ patcher: Patcher instance
118
+ server: Server instance (already started)
119
+ log_file: Optional log file path (default: outputs/py2max_server.log)
120
+
121
+ Example:
122
+ >>> p = Patcher('demo.maxpat')
123
+ >>> server = await p.serve(port=8000)
124
+ >>> await start_inline_repl(p, server, Path('server.log'))
125
+ """
126
+ from .repl import start_repl
127
+
128
+ # Setup log file
129
+ if log_file is None:
130
+ log_file = Path("outputs/py2max_server.log")
131
+
132
+ log_file.parent.mkdir(parents=True, exist_ok=True)
133
+
134
+ # Redirect logging to file
135
+ file_handler = setup_file_logging(log_file)
136
+
137
+ # Print info about logging
138
+ print()
139
+ print("=" * 70)
140
+ print("py2max Inline REPL (Single Terminal Mode)")
141
+ print("=" * 70)
142
+ print()
143
+ print(f"Server logs redirected to: {log_file}")
144
+ print(f"To monitor logs: tail -f {log_file}")
145
+ print()
146
+ print("Starting REPL...")
147
+ print()
148
+
149
+ try:
150
+ # Start REPL in foreground
151
+ await start_repl(patcher, server)
152
+
153
+ finally:
154
+ # Restore console logging
155
+ file_handler.close()
156
+ restore_console_logging()
157
+
158
+
159
+ class BackgroundServerREPL:
160
+ """REPL with server running in background thread.
161
+
162
+ This class manages the server lifecycle in a background thread
163
+ while the REPL runs in the foreground.
164
+ """
165
+
166
+ def __init__(
167
+ self,
168
+ patcher: "Patcher",
169
+ port: int = 8000,
170
+ log_file: Optional[Path] = None,
171
+ ):
172
+ self.patcher = patcher
173
+ self.port = port
174
+ self.log_file = log_file or Path("outputs/py2max_server.log")
175
+ self.server: Optional[InteractivePatcherServer] = None
176
+ self.server_thread: Optional[threading.Thread] = None
177
+ self.loop: Optional[asyncio.AbstractEventLoop] = None
178
+ self.ready_event = threading.Event()
179
+
180
+ def notify_update_sync(self):
181
+ """Synchronously notify server (running in background thread) of updates.
182
+
183
+ This method is called from the REPL (main thread) and schedules
184
+ the notify_update coroutine in the server's event loop.
185
+ """
186
+ if self.loop and self.server:
187
+ # Schedule coroutine in the server's event loop
188
+ future = asyncio.run_coroutine_threadsafe(
189
+ self.server.notify_update(), self.loop
190
+ )
191
+ # Wait for completion (with timeout to avoid hanging)
192
+ try:
193
+ future.result(timeout=2.0)
194
+ except Exception as e:
195
+ print(f"Warning: Failed to notify server: {e}", file=sys.stderr)
196
+
197
+ def _run_server(self):
198
+ """Run server in background thread."""
199
+ # Create new event loop for this thread
200
+ self.loop = asyncio.new_event_loop()
201
+ asyncio.set_event_loop(self.loop)
202
+
203
+ async def run():
204
+ # Start server
205
+ self.server = await self.patcher.serve(
206
+ port=self.port,
207
+ auto_open=True,
208
+ )
209
+
210
+ # Signal that server is ready
211
+ self.ready_event.set()
212
+
213
+ # Keep running
214
+ try:
215
+ while True:
216
+ await asyncio.sleep(1)
217
+ except asyncio.CancelledError:
218
+ # Shutdown requested
219
+ await self.server.stop()
220
+
221
+ # Run server
222
+ try:
223
+ self.loop.run_until_complete(run())
224
+ except Exception as e:
225
+ print(f"Server error: {e}", file=sys.stderr)
226
+ finally:
227
+ self.loop.close()
228
+
229
+ async def start(self):
230
+ """Start server in background and REPL in foreground."""
231
+ # Setup logging
232
+ self.log_file.parent.mkdir(parents=True, exist_ok=True)
233
+ file_handler = setup_file_logging(self.log_file)
234
+
235
+ # Start server in background thread
236
+ self.server_thread = threading.Thread(
237
+ target=self._run_server,
238
+ daemon=True,
239
+ )
240
+ self.server_thread.start()
241
+
242
+ # Wait for server to be ready
243
+ self.ready_event.wait(timeout=10)
244
+
245
+ if not self.ready_event.is_set():
246
+ raise RuntimeError("Server failed to start")
247
+
248
+ print()
249
+ print("=" * 70)
250
+ print("py2max Inline REPL (Single Terminal Mode)")
251
+ print("=" * 70)
252
+ print()
253
+ print("Server running:")
254
+ print(f" HTTP: http://localhost:{self.port}")
255
+ print(f" WebSocket: ws://localhost:{self.port + 1}")
256
+ print()
257
+ print(f"Server logs: {self.log_file}")
258
+ print(f"To monitor: tail -f {self.log_file}")
259
+ print()
260
+ print("Starting REPL...")
261
+ print()
262
+
263
+ try:
264
+ # Start REPL in foreground with refresh() function
265
+ from .repl import start_repl_with_refresh
266
+
267
+ assert self.server is not None, "Server failed to start"
268
+ await start_repl_with_refresh(
269
+ self.patcher, self.server, self.notify_update_sync
270
+ )
271
+
272
+ finally:
273
+ # Cleanup
274
+ self.stop()
275
+ file_handler.close()
276
+ restore_console_logging()
277
+
278
+ def stop(self):
279
+ """Stop background server."""
280
+ if self.loop and self.server:
281
+ # Cancel the server loop
282
+ for task in asyncio.all_tasks(self.loop):
283
+ task.cancel()
284
+
285
+
286
+ async def start_background_server_repl(
287
+ patcher: "Patcher",
288
+ port: int = 8000,
289
+ log_file: Optional[Path] = None,
290
+ ) -> None:
291
+ """Start REPL with server in background (single terminal mode).
292
+
293
+ Args:
294
+ patcher: Patcher instance
295
+ port: Server port (default: 8000)
296
+ log_file: Log file path (default: outputs/py2max_server.log)
297
+
298
+ Example:
299
+ >>> p = Patcher('demo.maxpat')
300
+ >>> await start_background_server_repl(p, port=8000)
301
+ """
302
+ repl = BackgroundServerREPL(patcher, port, log_file)
303
+ await repl.start()
304
+
305
+
306
+ __all__ = [
307
+ "start_inline_repl",
308
+ "start_background_server_repl",
309
+ "BackgroundServerREPL",
310
+ "setup_file_logging",
311
+ "restore_console_logging",
312
+ ]