python-tty 0.2.0__tar.gz → 0.2.2__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.
- python_tty-0.2.2/PKG-INFO +290 -0
- python_tty-0.2.2/README.md +259 -0
- python_tty-0.2.2/README_zh.md +259 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/docs/LOG.md +51 -0
- python_tty-0.2.2/docs/Schema.md +155 -0
- python_tty-0.2.2/src/python_tty/__init__.py +69 -0
- python_tty-0.2.2/src/python_tty/executor/__init__.py +29 -0
- python_tty-0.2.2/src/python_tty/testing/__init__.py +12 -0
- python_tty-0.2.2/src/python_tty/testing/capture.py +72 -0
- python_tty-0.2.2/src/python_tty/testing/decorators.py +67 -0
- python_tty-0.2.2/src/python_tty/testing/discovery.py +263 -0
- python_tty-0.2.2/src/python_tty/testing/harness.py +295 -0
- python_tty-0.2.2/src/python_tty/testing/results.py +17 -0
- python_tty-0.2.2/src/python_tty.egg-info/PKG-INFO +290 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty.egg-info/SOURCES.txt +9 -1
- python_tty-0.2.2/tests/test_testing_api_mvp.py +278 -0
- python_tty-0.2.0/PKG-INFO +0 -215
- python_tty-0.2.0/README.md +0 -184
- python_tty-0.2.0/README_zh.md +0 -184
- python_tty-0.2.0/src/python_tty/__init__.py +0 -25
- python_tty-0.2.0/src/python_tty/executor/__init__.py +0 -10
- python_tty-0.2.0/src/python_tty.egg-info/PKG-INFO +0 -215
- {python_tty-0.2.0 → python_tty-0.2.2}/.github/workflows/python-publish.yml +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/.gitignore +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/LICENSE +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/MANIFEST.in +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/NOTICE +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/demos/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/demos/chat_room/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/demos/file_manager/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/demos/file_manager/commands/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/demos/file_manager/commands/root_commands.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/demos/file_manager/consoles/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/demos/file_manager/consoles/root.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/demos/file_manager/core/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/demos/file_manager/core/file_manager.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/demos/file_manager/exceptions/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/demos/file_manager/exceptions/console_exception.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/demos/file_manager/main.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/demos/file_manager/setup.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/demos/file_manager/utils/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/demos/file_manager/utils/table.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/docs/context.md +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/pyproject.toml +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/requirements.txt +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/setup.cfg +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/setup.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/audit/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/audit/sink.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/audit/ui_logger.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/commands/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/commands/core.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/commands/decorators.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/commands/examples/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/commands/examples/root_commands.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/commands/examples/sub_commands.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/commands/general.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/commands/mixins.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/commands/registry.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/config/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/config/config.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/console_factory.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/consoles/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/consoles/core.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/consoles/decorators.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/consoles/examples/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/consoles/examples/root_console.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/consoles/examples/sub_console.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/consoles/loader.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/consoles/manager.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/consoles/registry.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/exceptions/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/exceptions/console_exception.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/executor/execution.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/executor/executor.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/executor/models.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/frontends/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/frontends/rpc/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/frontends/rpc/core.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/frontends/rpc/proto/runtime.proto +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/frontends/rpc/server.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/frontends/web/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/frontends/web/core.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/frontends/web/server.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/meta/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/runtime/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/runtime/context.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/runtime/event_bus.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/runtime/events.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/runtime/jobs.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/runtime/provider.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/runtime/router.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/runtime/sinks.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/session/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/session/callbacks.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/session/manager.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/session/models.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/session/policy.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/session/store.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/utils/__init__.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/utils/table.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty/utils/tokenize.py +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty.egg-info/dependency_links.txt +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty.egg-info/requires.txt +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/src/python_tty.egg-info/top_level.txt +0 -0
- {python_tty-0.2.0 → python_tty-0.2.2}/tests/__init__.py +0 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-tty
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: A multi-console TTY framework for complex CLI/TTY apps
|
|
5
|
+
Home-page: https://github.com/ROOKIEMIE/python-tty
|
|
6
|
+
Author: ROOKIEMIE
|
|
7
|
+
License: Apache-2.0
|
|
8
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
License-File: NOTICE
|
|
15
|
+
Requires-Dist: fastapi>=0.110.0
|
|
16
|
+
Requires-Dist: grpcio>=1.60.0
|
|
17
|
+
Requires-Dist: prompt_toolkit>=3.0.32
|
|
18
|
+
Requires-Dist: protobuf>=4.25.0
|
|
19
|
+
Requires-Dist: tqdm
|
|
20
|
+
Requires-Dist: uvicorn>=0.27.0
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: classifier
|
|
23
|
+
Dynamic: description
|
|
24
|
+
Dynamic: description-content-type
|
|
25
|
+
Dynamic: home-page
|
|
26
|
+
Dynamic: license
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
Dynamic: requires-dist
|
|
29
|
+
Dynamic: requires-python
|
|
30
|
+
Dynamic: summary
|
|
31
|
+
|
|
32
|
+
# Command Line Framework (TTY + Executor + RPC/Web)
|
|
33
|
+
|
|
34
|
+
[中文](README_zh.md)
|
|
35
|
+
|
|
36
|
+
## Project Introduction
|
|
37
|
+
|
|
38
|
+
`python-tty` is a multi-console command framework centered on TTY interaction, with a shared runtime execution model (`Invocation -> Executor -> RuntimeEvent`) that can be reused by RPC/Web frontends.
|
|
39
|
+
|
|
40
|
+
This repository now includes an official lightweight testing API (`python_tty.testing`) so external projects can test individual command methods without booting the full TTY kernel.
|
|
41
|
+
|
|
42
|
+
Architecture and module-relationship details were moved out of README and are maintained in [docs/Schema.md](docs/Schema.md).
|
|
43
|
+
|
|
44
|
+
## Quick Start Example
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from python_tty.console_factory import ConsoleFactory
|
|
48
|
+
|
|
49
|
+
# service can be any business object that commands access via self.console.service
|
|
50
|
+
service = object()
|
|
51
|
+
|
|
52
|
+
factory = ConsoleFactory(service=service)
|
|
53
|
+
factory.start()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Typical setup flow:
|
|
57
|
+
1. Define command classes with `@register_command`.
|
|
58
|
+
2. Bind command classes to consoles with `@commands`.
|
|
59
|
+
3. Register console topology with `@root` / `@sub` / `@multi`.
|
|
60
|
+
4. Start `ConsoleFactory`.
|
|
61
|
+
|
|
62
|
+
Decorator registration example (`commands`, `root`, `sub`, `multi`):
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from prompt_toolkit.styles import Style
|
|
66
|
+
|
|
67
|
+
from python_tty.commands import BaseCommands
|
|
68
|
+
from python_tty.commands.decorators import commands, register_command
|
|
69
|
+
from python_tty.consoles import MainConsole, SubConsole, multi, root, sub
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class RootCommands(BaseCommands):
|
|
73
|
+
@register_command("ping", "health check")
|
|
74
|
+
def run_ping(self):
|
|
75
|
+
return "pong"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ModuleCommands(BaseCommands):
|
|
79
|
+
@register_command("status", "module status")
|
|
80
|
+
def run_status(self):
|
|
81
|
+
return "ok"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@root
|
|
85
|
+
@commands(RootCommands)
|
|
86
|
+
class RootConsole(MainConsole):
|
|
87
|
+
console_name = "root"
|
|
88
|
+
def __init__(self, parent=None, manager=None):
|
|
89
|
+
super().__init__([("class:prompt", "root> ")], Style.from_dict({"": ""}), parent=parent, manager=manager)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@sub("root")
|
|
93
|
+
@commands(ModuleCommands)
|
|
94
|
+
class ModuleConsole(SubConsole):
|
|
95
|
+
console_name = "module"
|
|
96
|
+
def __init__(self, parent=None, manager=None):
|
|
97
|
+
super().__init__([("class:prompt", "module> ")], Style.from_dict({"": ""}), parent=parent, manager=manager)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@multi({"root": "tools", "module": "tools"})
|
|
101
|
+
@commands(ModuleCommands)
|
|
102
|
+
class ToolsConsole(SubConsole):
|
|
103
|
+
# runtime names become root_tools / module_tools
|
|
104
|
+
def __init__(self, parent=None, manager=None):
|
|
105
|
+
super().__init__([("class:prompt", "tools> ")], Style.from_dict({"": ""}), parent=parent, manager=manager)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Detailed Configuration Explanation
|
|
109
|
+
|
|
110
|
+
Configuration entry: `python_tty/config/config.py`.
|
|
111
|
+
Top-level type: `Config`.
|
|
112
|
+
|
|
113
|
+
### ConsoleFactoryConfig
|
|
114
|
+
|
|
115
|
+
- `run_mode`: `"tty"` or `"concurrent"`.
|
|
116
|
+
- `start_executor`: auto start executor during factory startup.
|
|
117
|
+
- `executor_in_thread`: in tty mode, run executor loop in a background thread.
|
|
118
|
+
- `executor_thread_name`: executor thread name.
|
|
119
|
+
- `tty_thread_name`: tty thread name in concurrent mode.
|
|
120
|
+
- `shutdown_executor`: shutdown executor when factory stops.
|
|
121
|
+
|
|
122
|
+
### ExecutorConfig
|
|
123
|
+
|
|
124
|
+
- `workers`: worker task count.
|
|
125
|
+
- `retain_last_n`: keep only last N completed runs.
|
|
126
|
+
- `ttl_seconds`: TTL for completed runs.
|
|
127
|
+
- `pop_on_wait`: remove run state after wait result.
|
|
128
|
+
- `exempt_exceptions`: exceptions treated as cancellation.
|
|
129
|
+
- `emit_run_events`: emit state events (`start/success/failure/...`).
|
|
130
|
+
- `event_history_max`: max history events per run.
|
|
131
|
+
- `event_history_ttl`: history TTL per run.
|
|
132
|
+
- `sync_in_threadpool`: run sync handlers in threadpool.
|
|
133
|
+
- `threadpool_workers`: threadpool size.
|
|
134
|
+
- `audit`: nested `AuditConfig`.
|
|
135
|
+
|
|
136
|
+
### AuditConfig
|
|
137
|
+
|
|
138
|
+
- `enabled`: enable audit sink.
|
|
139
|
+
- `file_path`: jsonl output file.
|
|
140
|
+
- `stream`: output stream (mutually exclusive with `file_path`).
|
|
141
|
+
- `async_mode`: async sink writer.
|
|
142
|
+
- `flush_interval`: async flush interval.
|
|
143
|
+
- `keep_in_memory`: keep records in memory for tests.
|
|
144
|
+
- `sink`: custom sink instance.
|
|
145
|
+
|
|
146
|
+
### RPCConfig
|
|
147
|
+
|
|
148
|
+
- `enabled`: start gRPC server.
|
|
149
|
+
- `bind_host` / `port`: bind address.
|
|
150
|
+
- `max_message_bytes`: gRPC payload limit.
|
|
151
|
+
- `keepalive_time_ms` / `keepalive_timeout_ms` / `keepalive_permit_without_calls`: keepalive controls.
|
|
152
|
+
- `max_concurrent_rpcs`: concurrency limit.
|
|
153
|
+
- `max_streams_per_client`: stream fanout limit.
|
|
154
|
+
- `stream_backpressure_queue_size`: stream queue size.
|
|
155
|
+
- `default_deny`: deny by default when exposure is missing.
|
|
156
|
+
- `require_rpc_exposed`: require `exposure.rpc=True`.
|
|
157
|
+
- `allowed_principals`: principal allowlist.
|
|
158
|
+
- `admin_principals`: admin principal list (allowlist bypass only).
|
|
159
|
+
- `require_audit`: require audit for RPC invoke.
|
|
160
|
+
- `trust_client_principal`: trust request principal directly.
|
|
161
|
+
- `mtls`: nested `MTLSServerConfig`.
|
|
162
|
+
|
|
163
|
+
### MTLSServerConfig
|
|
164
|
+
|
|
165
|
+
- `enabled`: enable mTLS.
|
|
166
|
+
- `server_cert_file` / `server_key_file`: server cert/key.
|
|
167
|
+
- `client_ca_file`: CA bundle for client cert verification.
|
|
168
|
+
- `require_client_cert`: enforce client cert.
|
|
169
|
+
- `principal_keys`: auth_context keys for principal extraction.
|
|
170
|
+
|
|
171
|
+
### WebConfig
|
|
172
|
+
|
|
173
|
+
- `enabled`: start web server.
|
|
174
|
+
- `bind_host` / `port`: bind address.
|
|
175
|
+
- `root_path`: reverse proxy root path.
|
|
176
|
+
- `cors_allow_origins` / `cors_allow_credentials` / `cors_allow_methods` / `cors_allow_headers`: CORS controls.
|
|
177
|
+
- `meta_enabled`: enable `/meta` endpoint.
|
|
178
|
+
- `meta_cache_control_max_age`: `/meta` cache-control max-age.
|
|
179
|
+
- `ws_snapshot_enabled`: enable snapshot websocket.
|
|
180
|
+
- `ws_snapshot_include_jobs`: include running jobs in snapshots.
|
|
181
|
+
- `ws_max_connections`: websocket connection limit.
|
|
182
|
+
- `ws_heartbeat_interval`: heartbeat interval.
|
|
183
|
+
- `ws_send_queue_size`: websocket send queue size.
|
|
184
|
+
|
|
185
|
+
## Testing API Usage Examples
|
|
186
|
+
|
|
187
|
+
Public imports:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from python_tty.testing import (
|
|
191
|
+
tty_testable,
|
|
192
|
+
discover_tests,
|
|
193
|
+
CommandHarness,
|
|
194
|
+
InvocationHarness,
|
|
195
|
+
TestRunResult,
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### 1. Mark command class or command method
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from python_tty.commands import BaseCommands
|
|
203
|
+
from python_tty.commands.decorators import register_command
|
|
204
|
+
from python_tty.testing import tty_testable
|
|
205
|
+
|
|
206
|
+
@tty_testable(component="smoke")
|
|
207
|
+
class UserCommands(BaseCommands):
|
|
208
|
+
@register_command("ping", "health check")
|
|
209
|
+
def run_ping(self):
|
|
210
|
+
return "pong"
|
|
211
|
+
|
|
212
|
+
class PartialCommands(BaseCommands):
|
|
213
|
+
@tty_testable(component="critical")
|
|
214
|
+
@register_command("login", "login flow")
|
|
215
|
+
def run_login(self, username):
|
|
216
|
+
return username
|
|
217
|
+
|
|
218
|
+
@register_command("debug", "not included unless explicitly selected")
|
|
219
|
+
def run_debug(self):
|
|
220
|
+
return "debug"
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### 2. Explicit discovery (test-only, no startup auto-scan)
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
import my_project.commands as command_module
|
|
227
|
+
from python_tty.testing import discover_tests
|
|
228
|
+
|
|
229
|
+
suite = discover_tests(command_module)
|
|
230
|
+
all_cases = suite.list_cases()
|
|
231
|
+
|
|
232
|
+
case = suite.get_case("cmd:usercommands:ping")
|
|
233
|
+
|
|
234
|
+
# explicit selection still works without decorators
|
|
235
|
+
explicit_suite = discover_tests(
|
|
236
|
+
command_module,
|
|
237
|
+
command_ids=["cmd:partialcommands:debug"],
|
|
238
|
+
)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### 3. Run a command with CommandHarness
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from types import SimpleNamespace
|
|
245
|
+
from python_tty.testing import CommandHarness
|
|
246
|
+
|
|
247
|
+
harness = CommandHarness(service=SimpleNamespace(name="svc"), suite=suite)
|
|
248
|
+
result: TestRunResult = harness.run("cmd:usercommands:ping")
|
|
249
|
+
|
|
250
|
+
assert result.ok
|
|
251
|
+
assert result.return_value == "pong"
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### 4. Toggle validation on/off
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
# validator enabled
|
|
258
|
+
result_on = harness.run("cmd:partialcommands:login", argv=[], validate=True)
|
|
259
|
+
assert result_on.ok is False
|
|
260
|
+
assert result_on.validator_ran is True
|
|
261
|
+
|
|
262
|
+
# validator disabled
|
|
263
|
+
result_off = harness.run("cmd:partialcommands:login", argv=[], validate=False)
|
|
264
|
+
assert result_off.validator_ran is False
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### 5. Run through InvocationHarness runtime path
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
inv_harness = InvocationHarness(suite=suite)
|
|
271
|
+
|
|
272
|
+
# direct runtime binding execution
|
|
273
|
+
direct_result = inv_harness.run("cmd:usercommands:ping", through_executor=False)
|
|
274
|
+
|
|
275
|
+
# executor-backed execution (captures state/runtime events)
|
|
276
|
+
executor_result = inv_harness.run("cmd:usercommands:ping", through_executor=True)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 6. Inspect TestRunResult
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
result = inv_harness.run("cmd:usercommands:ping", through_executor=True)
|
|
283
|
+
|
|
284
|
+
print(result.ok)
|
|
285
|
+
print(result.return_value)
|
|
286
|
+
print(result.exception)
|
|
287
|
+
print(result.outputs) # normalized output records
|
|
288
|
+
print(result.runtime_events) # raw RuntimeEvent objects
|
|
289
|
+
print(result.run_id)
|
|
290
|
+
```
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Command Line Framework (TTY + Executor + RPC/Web)
|
|
2
|
+
|
|
3
|
+
[中文](README_zh.md)
|
|
4
|
+
|
|
5
|
+
## Project Introduction
|
|
6
|
+
|
|
7
|
+
`python-tty` is a multi-console command framework centered on TTY interaction, with a shared runtime execution model (`Invocation -> Executor -> RuntimeEvent`) that can be reused by RPC/Web frontends.
|
|
8
|
+
|
|
9
|
+
This repository now includes an official lightweight testing API (`python_tty.testing`) so external projects can test individual command methods without booting the full TTY kernel.
|
|
10
|
+
|
|
11
|
+
Architecture and module-relationship details were moved out of README and are maintained in [docs/Schema.md](docs/Schema.md).
|
|
12
|
+
|
|
13
|
+
## Quick Start Example
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from python_tty.console_factory import ConsoleFactory
|
|
17
|
+
|
|
18
|
+
# service can be any business object that commands access via self.console.service
|
|
19
|
+
service = object()
|
|
20
|
+
|
|
21
|
+
factory = ConsoleFactory(service=service)
|
|
22
|
+
factory.start()
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Typical setup flow:
|
|
26
|
+
1. Define command classes with `@register_command`.
|
|
27
|
+
2. Bind command classes to consoles with `@commands`.
|
|
28
|
+
3. Register console topology with `@root` / `@sub` / `@multi`.
|
|
29
|
+
4. Start `ConsoleFactory`.
|
|
30
|
+
|
|
31
|
+
Decorator registration example (`commands`, `root`, `sub`, `multi`):
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from prompt_toolkit.styles import Style
|
|
35
|
+
|
|
36
|
+
from python_tty.commands import BaseCommands
|
|
37
|
+
from python_tty.commands.decorators import commands, register_command
|
|
38
|
+
from python_tty.consoles import MainConsole, SubConsole, multi, root, sub
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RootCommands(BaseCommands):
|
|
42
|
+
@register_command("ping", "health check")
|
|
43
|
+
def run_ping(self):
|
|
44
|
+
return "pong"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ModuleCommands(BaseCommands):
|
|
48
|
+
@register_command("status", "module status")
|
|
49
|
+
def run_status(self):
|
|
50
|
+
return "ok"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@root
|
|
54
|
+
@commands(RootCommands)
|
|
55
|
+
class RootConsole(MainConsole):
|
|
56
|
+
console_name = "root"
|
|
57
|
+
def __init__(self, parent=None, manager=None):
|
|
58
|
+
super().__init__([("class:prompt", "root> ")], Style.from_dict({"": ""}), parent=parent, manager=manager)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@sub("root")
|
|
62
|
+
@commands(ModuleCommands)
|
|
63
|
+
class ModuleConsole(SubConsole):
|
|
64
|
+
console_name = "module"
|
|
65
|
+
def __init__(self, parent=None, manager=None):
|
|
66
|
+
super().__init__([("class:prompt", "module> ")], Style.from_dict({"": ""}), parent=parent, manager=manager)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@multi({"root": "tools", "module": "tools"})
|
|
70
|
+
@commands(ModuleCommands)
|
|
71
|
+
class ToolsConsole(SubConsole):
|
|
72
|
+
# runtime names become root_tools / module_tools
|
|
73
|
+
def __init__(self, parent=None, manager=None):
|
|
74
|
+
super().__init__([("class:prompt", "tools> ")], Style.from_dict({"": ""}), parent=parent, manager=manager)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Detailed Configuration Explanation
|
|
78
|
+
|
|
79
|
+
Configuration entry: `python_tty/config/config.py`.
|
|
80
|
+
Top-level type: `Config`.
|
|
81
|
+
|
|
82
|
+
### ConsoleFactoryConfig
|
|
83
|
+
|
|
84
|
+
- `run_mode`: `"tty"` or `"concurrent"`.
|
|
85
|
+
- `start_executor`: auto start executor during factory startup.
|
|
86
|
+
- `executor_in_thread`: in tty mode, run executor loop in a background thread.
|
|
87
|
+
- `executor_thread_name`: executor thread name.
|
|
88
|
+
- `tty_thread_name`: tty thread name in concurrent mode.
|
|
89
|
+
- `shutdown_executor`: shutdown executor when factory stops.
|
|
90
|
+
|
|
91
|
+
### ExecutorConfig
|
|
92
|
+
|
|
93
|
+
- `workers`: worker task count.
|
|
94
|
+
- `retain_last_n`: keep only last N completed runs.
|
|
95
|
+
- `ttl_seconds`: TTL for completed runs.
|
|
96
|
+
- `pop_on_wait`: remove run state after wait result.
|
|
97
|
+
- `exempt_exceptions`: exceptions treated as cancellation.
|
|
98
|
+
- `emit_run_events`: emit state events (`start/success/failure/...`).
|
|
99
|
+
- `event_history_max`: max history events per run.
|
|
100
|
+
- `event_history_ttl`: history TTL per run.
|
|
101
|
+
- `sync_in_threadpool`: run sync handlers in threadpool.
|
|
102
|
+
- `threadpool_workers`: threadpool size.
|
|
103
|
+
- `audit`: nested `AuditConfig`.
|
|
104
|
+
|
|
105
|
+
### AuditConfig
|
|
106
|
+
|
|
107
|
+
- `enabled`: enable audit sink.
|
|
108
|
+
- `file_path`: jsonl output file.
|
|
109
|
+
- `stream`: output stream (mutually exclusive with `file_path`).
|
|
110
|
+
- `async_mode`: async sink writer.
|
|
111
|
+
- `flush_interval`: async flush interval.
|
|
112
|
+
- `keep_in_memory`: keep records in memory for tests.
|
|
113
|
+
- `sink`: custom sink instance.
|
|
114
|
+
|
|
115
|
+
### RPCConfig
|
|
116
|
+
|
|
117
|
+
- `enabled`: start gRPC server.
|
|
118
|
+
- `bind_host` / `port`: bind address.
|
|
119
|
+
- `max_message_bytes`: gRPC payload limit.
|
|
120
|
+
- `keepalive_time_ms` / `keepalive_timeout_ms` / `keepalive_permit_without_calls`: keepalive controls.
|
|
121
|
+
- `max_concurrent_rpcs`: concurrency limit.
|
|
122
|
+
- `max_streams_per_client`: stream fanout limit.
|
|
123
|
+
- `stream_backpressure_queue_size`: stream queue size.
|
|
124
|
+
- `default_deny`: deny by default when exposure is missing.
|
|
125
|
+
- `require_rpc_exposed`: require `exposure.rpc=True`.
|
|
126
|
+
- `allowed_principals`: principal allowlist.
|
|
127
|
+
- `admin_principals`: admin principal list (allowlist bypass only).
|
|
128
|
+
- `require_audit`: require audit for RPC invoke.
|
|
129
|
+
- `trust_client_principal`: trust request principal directly.
|
|
130
|
+
- `mtls`: nested `MTLSServerConfig`.
|
|
131
|
+
|
|
132
|
+
### MTLSServerConfig
|
|
133
|
+
|
|
134
|
+
- `enabled`: enable mTLS.
|
|
135
|
+
- `server_cert_file` / `server_key_file`: server cert/key.
|
|
136
|
+
- `client_ca_file`: CA bundle for client cert verification.
|
|
137
|
+
- `require_client_cert`: enforce client cert.
|
|
138
|
+
- `principal_keys`: auth_context keys for principal extraction.
|
|
139
|
+
|
|
140
|
+
### WebConfig
|
|
141
|
+
|
|
142
|
+
- `enabled`: start web server.
|
|
143
|
+
- `bind_host` / `port`: bind address.
|
|
144
|
+
- `root_path`: reverse proxy root path.
|
|
145
|
+
- `cors_allow_origins` / `cors_allow_credentials` / `cors_allow_methods` / `cors_allow_headers`: CORS controls.
|
|
146
|
+
- `meta_enabled`: enable `/meta` endpoint.
|
|
147
|
+
- `meta_cache_control_max_age`: `/meta` cache-control max-age.
|
|
148
|
+
- `ws_snapshot_enabled`: enable snapshot websocket.
|
|
149
|
+
- `ws_snapshot_include_jobs`: include running jobs in snapshots.
|
|
150
|
+
- `ws_max_connections`: websocket connection limit.
|
|
151
|
+
- `ws_heartbeat_interval`: heartbeat interval.
|
|
152
|
+
- `ws_send_queue_size`: websocket send queue size.
|
|
153
|
+
|
|
154
|
+
## Testing API Usage Examples
|
|
155
|
+
|
|
156
|
+
Public imports:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from python_tty.testing import (
|
|
160
|
+
tty_testable,
|
|
161
|
+
discover_tests,
|
|
162
|
+
CommandHarness,
|
|
163
|
+
InvocationHarness,
|
|
164
|
+
TestRunResult,
|
|
165
|
+
)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 1. Mark command class or command method
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from python_tty.commands import BaseCommands
|
|
172
|
+
from python_tty.commands.decorators import register_command
|
|
173
|
+
from python_tty.testing import tty_testable
|
|
174
|
+
|
|
175
|
+
@tty_testable(component="smoke")
|
|
176
|
+
class UserCommands(BaseCommands):
|
|
177
|
+
@register_command("ping", "health check")
|
|
178
|
+
def run_ping(self):
|
|
179
|
+
return "pong"
|
|
180
|
+
|
|
181
|
+
class PartialCommands(BaseCommands):
|
|
182
|
+
@tty_testable(component="critical")
|
|
183
|
+
@register_command("login", "login flow")
|
|
184
|
+
def run_login(self, username):
|
|
185
|
+
return username
|
|
186
|
+
|
|
187
|
+
@register_command("debug", "not included unless explicitly selected")
|
|
188
|
+
def run_debug(self):
|
|
189
|
+
return "debug"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### 2. Explicit discovery (test-only, no startup auto-scan)
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
import my_project.commands as command_module
|
|
196
|
+
from python_tty.testing import discover_tests
|
|
197
|
+
|
|
198
|
+
suite = discover_tests(command_module)
|
|
199
|
+
all_cases = suite.list_cases()
|
|
200
|
+
|
|
201
|
+
case = suite.get_case("cmd:usercommands:ping")
|
|
202
|
+
|
|
203
|
+
# explicit selection still works without decorators
|
|
204
|
+
explicit_suite = discover_tests(
|
|
205
|
+
command_module,
|
|
206
|
+
command_ids=["cmd:partialcommands:debug"],
|
|
207
|
+
)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### 3. Run a command with CommandHarness
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from types import SimpleNamespace
|
|
214
|
+
from python_tty.testing import CommandHarness
|
|
215
|
+
|
|
216
|
+
harness = CommandHarness(service=SimpleNamespace(name="svc"), suite=suite)
|
|
217
|
+
result: TestRunResult = harness.run("cmd:usercommands:ping")
|
|
218
|
+
|
|
219
|
+
assert result.ok
|
|
220
|
+
assert result.return_value == "pong"
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### 4. Toggle validation on/off
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
# validator enabled
|
|
227
|
+
result_on = harness.run("cmd:partialcommands:login", argv=[], validate=True)
|
|
228
|
+
assert result_on.ok is False
|
|
229
|
+
assert result_on.validator_ran is True
|
|
230
|
+
|
|
231
|
+
# validator disabled
|
|
232
|
+
result_off = harness.run("cmd:partialcommands:login", argv=[], validate=False)
|
|
233
|
+
assert result_off.validator_ran is False
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### 5. Run through InvocationHarness runtime path
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
inv_harness = InvocationHarness(suite=suite)
|
|
240
|
+
|
|
241
|
+
# direct runtime binding execution
|
|
242
|
+
direct_result = inv_harness.run("cmd:usercommands:ping", through_executor=False)
|
|
243
|
+
|
|
244
|
+
# executor-backed execution (captures state/runtime events)
|
|
245
|
+
executor_result = inv_harness.run("cmd:usercommands:ping", through_executor=True)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### 6. Inspect TestRunResult
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
result = inv_harness.run("cmd:usercommands:ping", through_executor=True)
|
|
252
|
+
|
|
253
|
+
print(result.ok)
|
|
254
|
+
print(result.return_value)
|
|
255
|
+
print(result.exception)
|
|
256
|
+
print(result.outputs) # normalized output records
|
|
257
|
+
print(result.runtime_events) # raw RuntimeEvent objects
|
|
258
|
+
print(result.run_id)
|
|
259
|
+
```
|