python-tty 0.1.0__py3-none-any.whl → 0.1.6__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.
- python_tty/__init__.py +13 -3
- python_tty/audit/__init__.py +7 -0
- python_tty/audit/sink.py +141 -0
- python_tty/{utils → audit}/ui_logger.py +3 -4
- python_tty/commands/examples/root_commands.py +2 -3
- python_tty/commands/mixins.py +4 -5
- python_tty/config/__init__.py +8 -1
- python_tty/config/config.py +60 -3
- python_tty/console_factory.py +52 -3
- python_tty/consoles/core.py +5 -5
- python_tty/consoles/manager.py +5 -5
- python_tty/consoles/registry.py +33 -0
- python_tty/executor/executor.py +115 -16
- python_tty/frontends/rpc/__init__.py +0 -0
- python_tty/frontends/web/__init__.py +0 -0
- python_tty/meta/__init__.py +96 -0
- python_tty/runtime/__init__.py +29 -0
- python_tty/runtime/events.py +147 -0
- python_tty/runtime/provider.py +62 -0
- python_tty/{ui/output.py → runtime/router.py} +53 -11
- python_tty/utils/__init__.py +1 -3
- {python_tty-0.1.0.dist-info → python_tty-0.1.6.dist-info}/METADATA +26 -13
- python_tty-0.1.6.dist-info/RECORD +47 -0
- {python_tty-0.1.0.dist-info → python_tty-0.1.6.dist-info}/WHEEL +1 -1
- python_tty-0.1.6.dist-info/licenses/LICENSE +176 -0
- python_tty-0.1.6.dist-info/licenses/NOTICE +3 -0
- {python_tty-0.1.0.dist-info → python_tty-0.1.6.dist-info}/top_level.txt +0 -0
- python_tty/ui/__init__.py +0 -13
- python_tty/ui/events.py +0 -55
- python_tty-0.1.0.dist-info/RECORD +0 -40
python_tty/__init__.py
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
from python_tty.config import Config
|
|
2
2
|
from python_tty.console_factory import ConsoleFactory
|
|
3
|
-
from python_tty.
|
|
4
|
-
|
|
3
|
+
from python_tty.runtime.events import (
|
|
4
|
+
EventBase,
|
|
5
|
+
RuntimeEvent,
|
|
6
|
+
RuntimeEventKind,
|
|
7
|
+
UIEvent,
|
|
8
|
+
UIEventLevel,
|
|
9
|
+
UIEventListener,
|
|
10
|
+
UIEventSpeaker,
|
|
11
|
+
)
|
|
12
|
+
from python_tty.runtime.router import proxy_print
|
|
5
13
|
|
|
6
14
|
__all__ = [
|
|
7
15
|
"UIEvent",
|
|
8
16
|
"UIEventLevel",
|
|
17
|
+
"EventBase",
|
|
18
|
+
"RuntimeEvent",
|
|
19
|
+
"RuntimeEventKind",
|
|
9
20
|
"UIEventListener",
|
|
10
21
|
"UIEventSpeaker",
|
|
11
22
|
"ConsoleFactory",
|
|
12
23
|
"Config",
|
|
13
24
|
"proxy_print",
|
|
14
25
|
]
|
|
15
|
-
|
python_tty/audit/sink.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import queue
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import asdict, is_dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Iterable, Optional, TextIO
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuditSink:
|
|
11
|
+
def __init__(self, file_path: Optional[str] = None, stream: Optional[TextIO] = None,
|
|
12
|
+
keep_in_memory: bool = False, async_mode: bool = False,
|
|
13
|
+
flush_interval: float = 1.0):
|
|
14
|
+
if file_path is not None and stream is not None:
|
|
15
|
+
raise ValueError("Only one of file_path or stream can be set")
|
|
16
|
+
self._path = file_path
|
|
17
|
+
self._stream = stream
|
|
18
|
+
self._owns_stream = False
|
|
19
|
+
if self._stream is None and self._path is not None:
|
|
20
|
+
self._stream = open(self._path, "a", encoding="utf-8")
|
|
21
|
+
self._owns_stream = True
|
|
22
|
+
self._buffer = [] if keep_in_memory else None
|
|
23
|
+
self._async_mode = async_mode and self._stream is not None
|
|
24
|
+
self._flush_interval = max(0.1, float(flush_interval))
|
|
25
|
+
self._lock = threading.Lock()
|
|
26
|
+
self._queue = None
|
|
27
|
+
self._stop_event = threading.Event()
|
|
28
|
+
self._worker = None
|
|
29
|
+
if self._async_mode:
|
|
30
|
+
self._queue = queue.Queue()
|
|
31
|
+
self._worker = threading.Thread(
|
|
32
|
+
target=self._worker_loop,
|
|
33
|
+
name="AuditSinkWriter",
|
|
34
|
+
daemon=True,
|
|
35
|
+
)
|
|
36
|
+
self._worker.start()
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def buffer(self):
|
|
40
|
+
return self._buffer
|
|
41
|
+
|
|
42
|
+
def record_invocation(self, invocation):
|
|
43
|
+
self._write("invocation", invocation)
|
|
44
|
+
|
|
45
|
+
def record_run_state(self, run_state):
|
|
46
|
+
self._write("run_state", run_state)
|
|
47
|
+
|
|
48
|
+
def record_event(self, event):
|
|
49
|
+
self._write("event", event)
|
|
50
|
+
|
|
51
|
+
def record_bundle(self, invocation=None, run_state=None, events: Optional[Iterable[Any]] = None):
|
|
52
|
+
if invocation is not None:
|
|
53
|
+
self.record_invocation(invocation)
|
|
54
|
+
if run_state is not None:
|
|
55
|
+
self.record_run_state(run_state)
|
|
56
|
+
if events:
|
|
57
|
+
for event in events:
|
|
58
|
+
self.record_event(event)
|
|
59
|
+
|
|
60
|
+
def close(self):
|
|
61
|
+
if self._async_mode and self._worker is not None:
|
|
62
|
+
self._stop_event.set()
|
|
63
|
+
self._worker.join()
|
|
64
|
+
if self._owns_stream and self._stream is not None:
|
|
65
|
+
self._stream.close()
|
|
66
|
+
self._stream = None
|
|
67
|
+
self._owns_stream = False
|
|
68
|
+
|
|
69
|
+
def _write(self, record_type: str, data):
|
|
70
|
+
record = {
|
|
71
|
+
"type": record_type,
|
|
72
|
+
"ts": time.time(),
|
|
73
|
+
"data": self._materialize(data),
|
|
74
|
+
}
|
|
75
|
+
if self._buffer is not None:
|
|
76
|
+
self._buffer.append(record)
|
|
77
|
+
if self._stream is None:
|
|
78
|
+
return
|
|
79
|
+
if self._async_mode and self._queue is not None:
|
|
80
|
+
self._queue.put(record)
|
|
81
|
+
return
|
|
82
|
+
self._write_record(record)
|
|
83
|
+
|
|
84
|
+
def _write_record(self, record):
|
|
85
|
+
payload = json.dumps(record, default=self._json_default)
|
|
86
|
+
with self._lock:
|
|
87
|
+
if self._stream is None:
|
|
88
|
+
return
|
|
89
|
+
self._stream.write(payload + "\n")
|
|
90
|
+
self._stream.flush()
|
|
91
|
+
|
|
92
|
+
def _write_batch(self, records):
|
|
93
|
+
payload = "\n".join(json.dumps(record, default=self._json_default) for record in records) + "\n"
|
|
94
|
+
with self._lock:
|
|
95
|
+
if self._stream is None:
|
|
96
|
+
return
|
|
97
|
+
self._stream.write(payload)
|
|
98
|
+
self._stream.flush()
|
|
99
|
+
|
|
100
|
+
def _worker_loop(self):
|
|
101
|
+
pending = []
|
|
102
|
+
while not self._stop_event.is_set() or (self._queue is not None and not self._queue.empty()):
|
|
103
|
+
try:
|
|
104
|
+
record = self._queue.get(timeout=self._flush_interval)
|
|
105
|
+
pending.append(record)
|
|
106
|
+
while True:
|
|
107
|
+
try:
|
|
108
|
+
pending.append(self._queue.get_nowait())
|
|
109
|
+
except queue.Empty:
|
|
110
|
+
break
|
|
111
|
+
except queue.Empty:
|
|
112
|
+
pass
|
|
113
|
+
if pending:
|
|
114
|
+
self._write_batch(pending)
|
|
115
|
+
pending.clear()
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _materialize(value):
|
|
119
|
+
if is_dataclass(value):
|
|
120
|
+
return asdict(value)
|
|
121
|
+
if isinstance(value, Enum):
|
|
122
|
+
return value.value
|
|
123
|
+
if isinstance(value, BaseException):
|
|
124
|
+
return str(value)
|
|
125
|
+
if hasattr(value, "__dict__"):
|
|
126
|
+
return dict(value.__dict__)
|
|
127
|
+
return value
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def _json_default(value):
|
|
131
|
+
if is_dataclass(value):
|
|
132
|
+
return asdict(value)
|
|
133
|
+
if isinstance(value, Enum):
|
|
134
|
+
return value.value
|
|
135
|
+
if isinstance(value, BaseException):
|
|
136
|
+
return str(value)
|
|
137
|
+
if isinstance(value, bytes):
|
|
138
|
+
return value.decode("utf-8", errors="replace")
|
|
139
|
+
if hasattr(value, "__dict__"):
|
|
140
|
+
return dict(value.__dict__)
|
|
141
|
+
return str(value)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
-
from python_tty.
|
|
4
|
-
from python_tty.
|
|
3
|
+
from python_tty.runtime.events import UIEventLevel
|
|
4
|
+
from python_tty.runtime.router import proxy_print
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class ConsoleHandler(logging.Handler):
|
|
@@ -11,7 +11,6 @@ class ConsoleHandler(logging.Handler):
|
|
|
11
11
|
def emit(self, record):
|
|
12
12
|
try:
|
|
13
13
|
log = self.format(record)
|
|
14
|
-
proxy_print(log, UIEventLevel.DEBUG)
|
|
14
|
+
proxy_print(log, UIEventLevel.DEBUG, source="tty")
|
|
15
15
|
except Exception:
|
|
16
16
|
self.handleError(record)
|
|
17
|
-
|
|
@@ -2,8 +2,8 @@ from python_tty.commands import BaseCommands
|
|
|
2
2
|
from python_tty.commands.decorators import register_command
|
|
3
3
|
from python_tty.commands.general import GeneralValidator
|
|
4
4
|
from python_tty.commands.mixins import HelpMixin, QuitMixin
|
|
5
|
-
from python_tty.
|
|
6
|
-
from python_tty.
|
|
5
|
+
from python_tty.runtime.events import UIEventLevel
|
|
6
|
+
from python_tty.runtime.router import proxy_print
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class RootCommands(BaseCommands, HelpMixin, QuitMixin):
|
|
@@ -30,4 +30,3 @@ class RootCommands(BaseCommands, HelpMixin, QuitMixin):
|
|
|
30
30
|
|
|
31
31
|
if __name__ == '__main__':
|
|
32
32
|
pass
|
|
33
|
-
|
python_tty/commands/mixins.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
|
|
3
|
-
from python_tty.
|
|
3
|
+
from python_tty.runtime.router import proxy_print
|
|
4
4
|
from python_tty.commands import BaseCommands
|
|
5
5
|
from python_tty.commands.decorators import register_command
|
|
6
6
|
from python_tty.commands.general import GeneralValidator
|
|
@@ -34,7 +34,7 @@ class HelpMixin(CommandMixin):
|
|
|
34
34
|
for cls in self.__class__.mro():
|
|
35
35
|
if cls is CommandMixin:
|
|
36
36
|
continue
|
|
37
|
-
if issubclass(cls, CommandMixin):
|
|
37
|
+
if issubclass(cls, CommandMixin) and not issubclass(cls, BaseCommands):
|
|
38
38
|
base_commands_funcs.extend([member[1] for member in inspect.getmembers(cls, inspect.isfunction)])
|
|
39
39
|
for name, func in self.command_funcs.items():
|
|
40
40
|
row = [name, func.info.func_description]
|
|
@@ -43,10 +43,9 @@ class HelpMixin(CommandMixin):
|
|
|
43
43
|
else:
|
|
44
44
|
custom_funcs.append(row)
|
|
45
45
|
if base_funcs:
|
|
46
|
-
proxy_print(Table(header, base_funcs, "Core Commands"))
|
|
46
|
+
proxy_print(Table(header, base_funcs, "Core Commands"), source="tty")
|
|
47
47
|
if custom_funcs:
|
|
48
|
-
proxy_print(Table(header, custom_funcs, "Custom Commands"))
|
|
48
|
+
proxy_print(Table(header, custom_funcs, "Custom Commands"), source="tty")
|
|
49
49
|
|
|
50
50
|
class DefaultCommands(BaseCommands, HelpMixin, QuitMixin):
|
|
51
51
|
pass
|
|
52
|
-
|
python_tty/config/__init__.py
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
from python_tty.config.config import
|
|
1
|
+
from python_tty.config.config import (
|
|
2
|
+
AuditConfig,
|
|
3
|
+
Config,
|
|
4
|
+
ConsoleFactoryConfig,
|
|
5
|
+
ConsoleManagerConfig,
|
|
6
|
+
ExecutorConfig,
|
|
7
|
+
)
|
|
2
8
|
|
|
3
9
|
__all__ = [
|
|
4
10
|
"Config",
|
|
11
|
+
"AuditConfig",
|
|
5
12
|
"ConsoleFactoryConfig",
|
|
6
13
|
"ConsoleManagerConfig",
|
|
7
14
|
"ExecutorConfig",
|
python_tty/config/config.py
CHANGED
|
@@ -1,35 +1,92 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
|
-
from typing import Optional, TYPE_CHECKING
|
|
2
|
+
from typing import Optional, TYPE_CHECKING, TextIO, Tuple, Type
|
|
3
3
|
|
|
4
4
|
if TYPE_CHECKING:
|
|
5
|
-
from python_tty.
|
|
5
|
+
from python_tty.audit.sink import AuditSink
|
|
6
|
+
from python_tty.runtime.router import OutputRouter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class AuditConfig:
|
|
11
|
+
"""Audit sink configuration.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
enabled: Toggle audit recording.
|
|
15
|
+
file_path: File path to append JSONL audit records.
|
|
16
|
+
stream: File-like stream to write audit records.
|
|
17
|
+
async_mode: Enable async background writer when stream is set.
|
|
18
|
+
flush_interval: Flush interval (seconds) for async writer.
|
|
19
|
+
keep_in_memory: Keep records in memory buffer for testing.
|
|
20
|
+
sink: Custom AuditSink instance to use instead of file/stream.
|
|
21
|
+
"""
|
|
22
|
+
enabled: bool = False
|
|
23
|
+
file_path: Optional[str] = None
|
|
24
|
+
stream: Optional[TextIO] = None
|
|
25
|
+
async_mode: bool = False
|
|
26
|
+
flush_interval: float = 1.0
|
|
27
|
+
keep_in_memory: bool = False
|
|
28
|
+
sink: Optional["AuditSink"] = None
|
|
6
29
|
|
|
7
30
|
|
|
8
31
|
@dataclass
|
|
9
32
|
class ExecutorConfig:
|
|
33
|
+
"""Executor runtime configuration.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
workers: Number of worker tasks to consume invocations.
|
|
37
|
+
retain_last_n: Keep only the last N completed runs in memory.
|
|
38
|
+
ttl_seconds: Time-to-live for completed runs.
|
|
39
|
+
pop_on_wait: Drop run state after wait_result completion.
|
|
40
|
+
exempt_exceptions: Exceptions treated as cancellations.
|
|
41
|
+
emit_run_events: Emit start/success/failure RuntimeEvent state.
|
|
42
|
+
audit: Audit sink configuration.
|
|
43
|
+
"""
|
|
10
44
|
workers: int = 1
|
|
11
45
|
retain_last_n: Optional[int] = None
|
|
12
46
|
ttl_seconds: Optional[float] = None
|
|
13
47
|
pop_on_wait: bool = False
|
|
48
|
+
exempt_exceptions: Optional[Tuple[Type[BaseException], ...]] = None
|
|
49
|
+
emit_run_events: bool = False
|
|
50
|
+
audit: AuditConfig = field(default_factory=AuditConfig)
|
|
14
51
|
|
|
15
52
|
|
|
16
53
|
@dataclass
|
|
17
54
|
class ConsoleManagerConfig:
|
|
55
|
+
"""Console manager configuration.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
use_patch_stdout: Patch stdout for prompt_toolkit rendering.
|
|
59
|
+
output_router: Output router instance for UI events.
|
|
60
|
+
"""
|
|
18
61
|
use_patch_stdout: bool = True
|
|
19
62
|
output_router: Optional["OutputRouter"] = None
|
|
20
63
|
|
|
21
64
|
|
|
22
65
|
@dataclass
|
|
23
66
|
class ConsoleFactoryConfig:
|
|
67
|
+
"""Console factory bootstrap configuration.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
run_mode: "tty" for single-thread TTY mode, "concurrent" for
|
|
71
|
+
main-thread asyncio loop with TTY in a background thread.
|
|
72
|
+
start_executor: Auto-start the executor when the factory starts.
|
|
73
|
+
executor_in_thread: Start executor in a background thread (tty mode).
|
|
74
|
+
executor_thread_name: Thread name for the executor loop thread.
|
|
75
|
+
tty_thread_name: Thread name for the TTY loop (concurrent mode).
|
|
76
|
+
shutdown_executor: Shutdown executor when the factory stops.
|
|
77
|
+
"""
|
|
78
|
+
run_mode: str = "tty"
|
|
24
79
|
start_executor: bool = True
|
|
25
80
|
executor_in_thread: bool = True
|
|
26
81
|
executor_thread_name: str = "ExecutorLoop"
|
|
82
|
+
tty_thread_name: str = "TTYLoop"
|
|
27
83
|
shutdown_executor: bool = True
|
|
28
84
|
|
|
29
85
|
|
|
30
86
|
@dataclass
|
|
31
87
|
class Config:
|
|
88
|
+
"""Top-level configuration for python-tty."""
|
|
32
89
|
console_manager: ConsoleManagerConfig = field(default_factory=ConsoleManagerConfig)
|
|
33
90
|
executor: ExecutorConfig = field(default_factory=ExecutorConfig)
|
|
34
91
|
console_factory: ConsoleFactoryConfig = field(default_factory=ConsoleFactoryConfig)
|
|
35
|
-
|
|
92
|
+
|
python_tty/console_factory.py
CHANGED
|
@@ -6,6 +6,8 @@ from python_tty.consoles.loader import load_consoles
|
|
|
6
6
|
from python_tty.consoles.manager import ConsoleManager
|
|
7
7
|
from python_tty.consoles.registry import REGISTRY
|
|
8
8
|
from python_tty.executor import CommandExecutor
|
|
9
|
+
from python_tty.runtime.provider import set_default_router
|
|
10
|
+
from python_tty.runtime.router import OutputRouter
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class ConsoleFactory:
|
|
@@ -27,6 +29,9 @@ class ConsoleFactory:
|
|
|
27
29
|
if config is None:
|
|
28
30
|
config = Config()
|
|
29
31
|
self.config = config
|
|
32
|
+
if self.config.console_manager.output_router is None:
|
|
33
|
+
self.config.console_manager.output_router = OutputRouter()
|
|
34
|
+
set_default_router(self.config.console_manager.output_router)
|
|
30
35
|
self.executor = CommandExecutor(config=config.executor)
|
|
31
36
|
self._executor_loop = None
|
|
32
37
|
self._executor_thread = None
|
|
@@ -36,13 +41,50 @@ class ConsoleFactory:
|
|
|
36
41
|
on_shutdown=self.shutdown,
|
|
37
42
|
config=config.console_manager,
|
|
38
43
|
)
|
|
44
|
+
self._attach_audit_sink()
|
|
39
45
|
load_consoles()
|
|
40
46
|
REGISTRY.register_all(self.manager)
|
|
41
47
|
|
|
42
48
|
def start(self):
|
|
43
49
|
"""Start the console loop with the registered root console."""
|
|
44
|
-
self.
|
|
45
|
-
|
|
50
|
+
run_mode = self.config.console_factory.run_mode
|
|
51
|
+
if run_mode == "tty":
|
|
52
|
+
self._start_executor_if_needed()
|
|
53
|
+
self.manager.run()
|
|
54
|
+
return
|
|
55
|
+
if run_mode == "concurrent":
|
|
56
|
+
self.start_concurrent()
|
|
57
|
+
return
|
|
58
|
+
raise ValueError(f"Unsupported run_mode: {run_mode}")
|
|
59
|
+
|
|
60
|
+
def start_concurrent(self):
|
|
61
|
+
"""Start executor on the main loop and run TTY in a background thread."""
|
|
62
|
+
loop = asyncio.new_event_loop()
|
|
63
|
+
self._executor_loop = loop
|
|
64
|
+
asyncio.set_event_loop(loop)
|
|
65
|
+
if self.config.console_factory.start_executor:
|
|
66
|
+
self.start_executor(loop=loop)
|
|
67
|
+
|
|
68
|
+
def _run_tty():
|
|
69
|
+
try:
|
|
70
|
+
self.manager.run()
|
|
71
|
+
finally:
|
|
72
|
+
if loop.is_running():
|
|
73
|
+
loop.call_soon_threadsafe(loop.stop)
|
|
74
|
+
|
|
75
|
+
tty_thread = threading.Thread(
|
|
76
|
+
target=_run_tty,
|
|
77
|
+
name=self.config.console_factory.tty_thread_name,
|
|
78
|
+
daemon=True,
|
|
79
|
+
)
|
|
80
|
+
tty_thread.start()
|
|
81
|
+
loop.run_forever()
|
|
82
|
+
pending = asyncio.all_tasks(loop)
|
|
83
|
+
if pending:
|
|
84
|
+
for task in pending:
|
|
85
|
+
task.cancel()
|
|
86
|
+
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
|
87
|
+
loop.close()
|
|
46
88
|
|
|
47
89
|
def start_executor(self, loop=None):
|
|
48
90
|
"""Start executor workers on the provided asyncio loop."""
|
|
@@ -97,4 +139,11 @@ class ConsoleFactory:
|
|
|
97
139
|
daemon=True,
|
|
98
140
|
)
|
|
99
141
|
self._executor_thread.start()
|
|
100
|
-
|
|
142
|
+
|
|
143
|
+
def _attach_audit_sink(self):
|
|
144
|
+
audit_sink = getattr(self.executor, "audit_sink", None)
|
|
145
|
+
if audit_sink is None:
|
|
146
|
+
return
|
|
147
|
+
default_router = self.config.console_manager.output_router
|
|
148
|
+
if default_router is not None:
|
|
149
|
+
default_router.attach_audit_sink(audit_sink)
|
python_tty/consoles/core.py
CHANGED
|
@@ -3,10 +3,10 @@ from abc import ABC
|
|
|
3
3
|
|
|
4
4
|
from prompt_toolkit import PromptSession
|
|
5
5
|
|
|
6
|
-
from python_tty.
|
|
6
|
+
from python_tty.runtime.events import UIEventListener, UIEventSpeaker
|
|
7
7
|
from python_tty.executor import Invocation
|
|
8
8
|
from python_tty.exceptions.console_exception import ConsoleExit, ConsoleInitException, SubConsoleExit
|
|
9
|
-
from python_tty.
|
|
9
|
+
from python_tty.runtime.router import proxy_print
|
|
10
10
|
from python_tty.utils import split_cmd
|
|
11
11
|
|
|
12
12
|
|
|
@@ -47,7 +47,7 @@ class BaseConsole(ABC, UIEventListener):
|
|
|
47
47
|
|
|
48
48
|
def handler_event(self, event):
|
|
49
49
|
if BaseConsole.forward_console is not None and BaseConsole.forward_console == self:
|
|
50
|
-
proxy_print(event.msg, event.level)
|
|
50
|
+
proxy_print(event.msg, event.level, source=event.source or "tty")
|
|
51
51
|
|
|
52
52
|
def run(self, invocation: Invocation):
|
|
53
53
|
command_def = self.commands.get_command_def_by_id(invocation.command_id)
|
|
@@ -70,7 +70,8 @@ class BaseConsole(ABC, UIEventListener):
|
|
|
70
70
|
if executor is None:
|
|
71
71
|
self.run(invocation)
|
|
72
72
|
return
|
|
73
|
-
executor.submit_threadsafe(invocation, handler=self.run)
|
|
73
|
+
run_id = executor.submit_threadsafe(invocation, handler=self.run)
|
|
74
|
+
executor.wait_result_sync(run_id)
|
|
74
75
|
except ValueError:
|
|
75
76
|
return
|
|
76
77
|
|
|
@@ -137,4 +138,3 @@ class SubConsole(BaseConsole):
|
|
|
137
138
|
if parent is None:
|
|
138
139
|
raise ConsoleInitException("SubConsole parent is None")
|
|
139
140
|
super().__init__(console_message, console_style, parent=parent, manager=manager)
|
|
140
|
-
|
python_tty/consoles/manager.py
CHANGED
|
@@ -2,8 +2,9 @@ from prompt_toolkit.patch_stdout import patch_stdout
|
|
|
2
2
|
|
|
3
3
|
from python_tty.config import ConsoleManagerConfig
|
|
4
4
|
from python_tty.exceptions.console_exception import ConsoleExit, ConsoleInitException, SubConsoleExit
|
|
5
|
-
from python_tty.
|
|
6
|
-
from python_tty.
|
|
5
|
+
from python_tty.runtime.events import UIEventLevel, UIEventSpeaker
|
|
6
|
+
from python_tty.runtime.provider import get_router
|
|
7
|
+
from python_tty.runtime.router import proxy_print
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class ConsoleEntry:
|
|
@@ -23,7 +24,7 @@ class ConsoleManager:
|
|
|
23
24
|
self._on_shutdown = on_shutdown
|
|
24
25
|
self._shutdown_called = False
|
|
25
26
|
self._config = config if config is not None else ConsoleManagerConfig()
|
|
26
|
-
self._output_router = self._config.output_router or
|
|
27
|
+
self._output_router = self._config.output_router or get_router()
|
|
27
28
|
self._use_patch_stdout = self._config.use_patch_stdout
|
|
28
29
|
self._warn_service_if_needed(service)
|
|
29
30
|
|
|
@@ -64,7 +65,7 @@ class ConsoleManager:
|
|
|
64
65
|
if service is not None and not isinstance(service, UIEventSpeaker):
|
|
65
66
|
msg = f"The Service core[{service.__class__}] doesn't extend the [UIEventSpeaker],"\
|
|
66
67
|
" which may affect the output of the Service core on the UI!"
|
|
67
|
-
proxy_print(msg, UIEventLevel.WARNING)
|
|
68
|
+
proxy_print(msg, UIEventLevel.WARNING, source="tty")
|
|
68
69
|
|
|
69
70
|
@property
|
|
70
71
|
def current(self):
|
|
@@ -143,4 +144,3 @@ class ConsoleManager:
|
|
|
143
144
|
self._output_router.clear_session()
|
|
144
145
|
return
|
|
145
146
|
self._output_router.bind_session(current.session)
|
|
146
|
-
|
python_tty/consoles/registry.py
CHANGED
|
@@ -58,6 +58,39 @@ class ConsoleRegistry:
|
|
|
58
58
|
raise ConsoleInitException(f"Console name duplicate [{resolved_name}]")
|
|
59
59
|
self._subs[resolved_name] = SubConsoleEntry(console_cls, parent_name, resolved_name)
|
|
60
60
|
|
|
61
|
+
def get_root(self):
|
|
62
|
+
if self._root_cls is None:
|
|
63
|
+
return None, None
|
|
64
|
+
root_name = self._root_name or _get_console_name(self._root_cls)
|
|
65
|
+
return self._root_cls, root_name
|
|
66
|
+
|
|
67
|
+
def get_subs(self):
|
|
68
|
+
return dict(self._subs)
|
|
69
|
+
|
|
70
|
+
def iter_consoles(self):
|
|
71
|
+
root_cls, root_name = self.get_root()
|
|
72
|
+
if root_cls is not None and root_name is not None:
|
|
73
|
+
yield root_name, root_cls, None
|
|
74
|
+
for name, entry in self._subs.items():
|
|
75
|
+
yield name, entry.console_cls, entry.parent_name
|
|
76
|
+
|
|
77
|
+
def get_console_tree(self):
|
|
78
|
+
_, root_name = self.get_root()
|
|
79
|
+
if not root_name:
|
|
80
|
+
return None
|
|
81
|
+
return _build_console_tree(root_name, self._subs)
|
|
82
|
+
|
|
83
|
+
def get_console_map(self):
|
|
84
|
+
console_map = {}
|
|
85
|
+
for name, console_cls, parent in self.iter_consoles():
|
|
86
|
+
console_map[name] = {
|
|
87
|
+
"name": name,
|
|
88
|
+
"parent": parent,
|
|
89
|
+
"type": console_cls.__name__,
|
|
90
|
+
"module": console_cls.__module__,
|
|
91
|
+
}
|
|
92
|
+
return console_map
|
|
93
|
+
|
|
61
94
|
def register_all(self, manager):
|
|
62
95
|
if self._root_cls is None:
|
|
63
96
|
raise ConsoleInitException("Root console not set")
|