atv 0.1.0__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.
- atv-0.1.0/LICENSE +21 -0
- atv-0.1.0/PKG-INFO +63 -0
- atv-0.1.0/README.md +52 -0
- atv-0.1.0/atv/__init__.py +7 -0
- atv-0.1.0/atv/__main__.py +9 -0
- atv-0.1.0/atv/app.py +249 -0
- atv-0.1.0/atv/cli.py +15 -0
- atv-0.1.0/atv/processes.py +82 -0
- atv-0.1.0/atv.egg-info/PKG-INFO +63 -0
- atv-0.1.0/atv.egg-info/SOURCES.txt +14 -0
- atv-0.1.0/atv.egg-info/dependency_links.txt +1 -0
- atv-0.1.0/atv.egg-info/entry_points.txt +2 -0
- atv-0.1.0/atv.egg-info/requires.txt +2 -0
- atv-0.1.0/atv.egg-info/top_level.txt +1 -0
- atv-0.1.0/pyproject.toml +16 -0
- atv-0.1.0/setup.cfg +4 -0
atv-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Junya Fukuda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
atv-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: atv
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TaskScope: a TUI tool for experimenting with Python 3.14 asyncio ps/pstree features
|
|
5
|
+
Requires-Python: >=3.14
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: textual<0.61,>=0.57
|
|
9
|
+
Requires-Dist: psutil>=5.9
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# atv 🌱
|
|
13
|
+
|
|
14
|
+
<img width="2850" height="1480" alt="image" src="https://github.com/user-attachments/assets/9b8916b7-4ca0-4715-b63b-280f2c1bf8fd" />
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
atv(async task viewer) is a CLI for exploring Python `asyncio` processes with a Textual UI. It starts by listing Python processes running locally, and we plan to integrate execution of `asyncio ps` / `pstree` and call-graph visualization.
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd atv
|
|
23
|
+
uv sync
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`uv sync` installs the specified dependencies (`textual`, `psutil`) into your local environment.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv run atv
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Shortcuts:
|
|
35
|
+
|
|
36
|
+
- `R` – Refresh the process list (the table shows PID / Process / File / Command Line)
|
|
37
|
+
- `Q` – Exit
|
|
38
|
+
- `Enter` / `P` – Run `python -m asyncio ps` for the selected PID and display the result in the right pane
|
|
39
|
+
|
|
40
|
+
### Using uvx
|
|
41
|
+
|
|
42
|
+
Once released, you'll be able to launch it with a one-liner.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uvx atv
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If you want to test uvx behavior during local development, run the following in the project root.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uvx --from . atv
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Roadmap
|
|
55
|
+
|
|
56
|
+
- View that periodically runs `python -m asyncio ps` for the selected process
|
|
57
|
+
- Visualization for `asyncio pstree` / `format_call_graph`
|
|
58
|
+
- Snapshot capture and diff comparison
|
|
59
|
+
- Integration with the FastAPI demo (retrieve task information via the API)
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
This project is licensed under the MIT License. See `LICENSE` for details.
|
atv-0.1.0/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# atv 🌱
|
|
2
|
+
|
|
3
|
+
<img width="2850" height="1480" alt="image" src="https://github.com/user-attachments/assets/9b8916b7-4ca0-4715-b63b-280f2c1bf8fd" />
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
atv(async task viewer) is a CLI for exploring Python `asyncio` processes with a Textual UI. It starts by listing Python processes running locally, and we plan to integrate execution of `asyncio ps` / `pstree` and call-graph visualization.
|
|
7
|
+
|
|
8
|
+
## Setup
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
cd atv
|
|
12
|
+
uv sync
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`uv sync` installs the specified dependencies (`textual`, `psutil`) into your local environment.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv run atv
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Shortcuts:
|
|
24
|
+
|
|
25
|
+
- `R` – Refresh the process list (the table shows PID / Process / File / Command Line)
|
|
26
|
+
- `Q` – Exit
|
|
27
|
+
- `Enter` / `P` – Run `python -m asyncio ps` for the selected PID and display the result in the right pane
|
|
28
|
+
|
|
29
|
+
### Using uvx
|
|
30
|
+
|
|
31
|
+
Once released, you'll be able to launch it with a one-liner.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uvx atv
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If you want to test uvx behavior during local development, run the following in the project root.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uvx --from . atv
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Roadmap
|
|
44
|
+
|
|
45
|
+
- View that periodically runs `python -m asyncio ps` for the selected process
|
|
46
|
+
- Visualization for `asyncio pstree` / `format_call_graph`
|
|
47
|
+
- Snapshot capture and diff comparison
|
|
48
|
+
- Integration with the FastAPI demo (retrieve task information via the API)
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
This project is licensed under the MIT License. See `LICENSE` for details.
|
atv-0.1.0/atv/app.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Textual application scaffolding for atv."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Iterable
|
|
8
|
+
|
|
9
|
+
from textual.app import App, ComposeResult
|
|
10
|
+
from textual.binding import Binding
|
|
11
|
+
from textual.containers import Container
|
|
12
|
+
from textual.coordinate import Coordinate
|
|
13
|
+
from textual.widgets import DataTable, Footer, Header, Log, Static
|
|
14
|
+
|
|
15
|
+
from .processes import ProcessInfo, list_python_processes
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProcessTable(DataTable):
|
|
19
|
+
"""Tabular view of detected Python processes."""
|
|
20
|
+
|
|
21
|
+
def on_mount(self) -> None: # noqa: D401 - Textual lifecycle hook
|
|
22
|
+
self.cursor_type = "row"
|
|
23
|
+
self.zebra_stripes = True
|
|
24
|
+
self.add_columns("PID", "Process", "File", "Command line")
|
|
25
|
+
|
|
26
|
+
def get_selected_pid(self) -> int | None:
|
|
27
|
+
"""Return the PID associated with the highlighted row, if any."""
|
|
28
|
+
|
|
29
|
+
if self.row_count == 0:
|
|
30
|
+
return None
|
|
31
|
+
coordinate = self.cursor_coordinate
|
|
32
|
+
if coordinate is None:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
# Always read PID from column 0 to avoid reliance on internal row keys.
|
|
36
|
+
pid_coordinate = Coordinate(coordinate.row, 0)
|
|
37
|
+
try:
|
|
38
|
+
pid_value = self.get_cell_at(pid_coordinate)
|
|
39
|
+
except Exception: # pragma: no cover - defensive safeguard
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
return int(pid_value)
|
|
44
|
+
except (TypeError, ValueError):
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
def update_processes(self, processes: Iterable[ProcessInfo]) -> None:
|
|
48
|
+
"""Replace the table content with the given process collection."""
|
|
49
|
+
|
|
50
|
+
self.clear(columns=False)
|
|
51
|
+
for info in processes:
|
|
52
|
+
display_name = info.name or "(unknown)"
|
|
53
|
+
file_display = info.file or "-"
|
|
54
|
+
self.add_row(
|
|
55
|
+
str(info.pid),
|
|
56
|
+
display_name,
|
|
57
|
+
file_display,
|
|
58
|
+
info.command_line,
|
|
59
|
+
key=info.pid,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if self.row_count:
|
|
63
|
+
self.cursor_coordinate = (0, 0)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AtvApp(App):
|
|
67
|
+
"""Primary Textual application for atv."""
|
|
68
|
+
|
|
69
|
+
CSS = """
|
|
70
|
+
#content {
|
|
71
|
+
layout: horizontal;
|
|
72
|
+
padding: 1 2;
|
|
73
|
+
height: 1fr;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#process-pane, #output-pane {
|
|
77
|
+
layout: vertical;
|
|
78
|
+
height: 1fr;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#process-pane {
|
|
82
|
+
width: 1fr;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#output-pane {
|
|
86
|
+
width: 2fr;
|
|
87
|
+
margin-left: 2;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#intro {
|
|
91
|
+
color: #b3b3b3;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
DataTable {
|
|
95
|
+
height: 1fr;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
Log {
|
|
99
|
+
height: 1fr;
|
|
100
|
+
border: round $accent;
|
|
101
|
+
padding: 0 1;
|
|
102
|
+
}
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
TITLE = "atv"
|
|
106
|
+
SUB_TITLE = "Asyncio process discovery (alpha)"
|
|
107
|
+
|
|
108
|
+
BINDINGS = [
|
|
109
|
+
Binding("r", "refresh_processes", "Refresh", priority=True),
|
|
110
|
+
Binding("enter", "inspect_selected", "Inspect"),
|
|
111
|
+
Binding("p", "inspect_selected", "Inspect"),
|
|
112
|
+
Binding("q", "quit", "Quit"),
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
def compose(self) -> ComposeResult:
|
|
116
|
+
yield Header(show_clock=True)
|
|
117
|
+
yield Container(
|
|
118
|
+
Container(
|
|
119
|
+
Static(
|
|
120
|
+
"Select a Python process to inspect. Press R to refresh. \n"
|
|
121
|
+
"Press Enter to capture `python -m asyncio ps <pid>`.",
|
|
122
|
+
id="intro",
|
|
123
|
+
),
|
|
124
|
+
ProcessTable(id="process-table"),
|
|
125
|
+
id="process-pane",
|
|
126
|
+
),
|
|
127
|
+
Container(
|
|
128
|
+
Static("Awaiting selection", id="ps-title"),
|
|
129
|
+
Log(id="ps-output"),
|
|
130
|
+
id="output-pane",
|
|
131
|
+
),
|
|
132
|
+
id="content",
|
|
133
|
+
)
|
|
134
|
+
yield Footer()
|
|
135
|
+
|
|
136
|
+
def on_mount(self) -> None: # noqa: D401 - Textual lifecycle hook
|
|
137
|
+
self.refresh_process_list()
|
|
138
|
+
|
|
139
|
+
def action_refresh_processes(self) -> None:
|
|
140
|
+
"""Binding target: refresh the process table."""
|
|
141
|
+
|
|
142
|
+
self.refresh_process_list()
|
|
143
|
+
|
|
144
|
+
def action_inspect_selected(self) -> None:
|
|
145
|
+
"""Run ``python -m asyncio ps`` for the highlighted process."""
|
|
146
|
+
|
|
147
|
+
table = self.query_one(ProcessTable)
|
|
148
|
+
pid = table.get_selected_pid()
|
|
149
|
+
if pid is None:
|
|
150
|
+
self.notify("Select a Python process first.", severity="warning", timeout=3)
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
header = self.query_one("#ps-title", Static)
|
|
154
|
+
header.update(f"Running asyncio ps for PID {pid}…")
|
|
155
|
+
log = self.query_one("#ps-output", Log)
|
|
156
|
+
log.clear()
|
|
157
|
+
log.write("Awaiting asyncio ps output…")
|
|
158
|
+
|
|
159
|
+
self.run_worker(
|
|
160
|
+
self.capture_asyncio_ps(pid),
|
|
161
|
+
exclusive=True,
|
|
162
|
+
description=f"Inspecting PID {pid}",
|
|
163
|
+
name=f"ps-{pid}",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def refresh_process_list(self) -> None:
|
|
167
|
+
"""Fetch Python processes and update UI state."""
|
|
168
|
+
|
|
169
|
+
processes = list_python_processes()
|
|
170
|
+
table = self.query_one(ProcessTable)
|
|
171
|
+
table.update_processes(processes)
|
|
172
|
+
|
|
173
|
+
count = len(processes)
|
|
174
|
+
noun = "process" if count == 1 else "processes"
|
|
175
|
+
self.sub_title = f"{count} Python {noun} visible"
|
|
176
|
+
|
|
177
|
+
if not count:
|
|
178
|
+
self.notify(
|
|
179
|
+
"No Python processes detected. Launch your target application and press R to refresh.",
|
|
180
|
+
severity="warning",
|
|
181
|
+
timeout=4,
|
|
182
|
+
)
|
|
183
|
+
output = self.query_one("#ps-output", Log)
|
|
184
|
+
output.clear()
|
|
185
|
+
self.query_one("#ps-title", Static).update("Awaiting selection")
|
|
186
|
+
|
|
187
|
+
async def capture_asyncio_ps(self, pid: int) -> None:
|
|
188
|
+
"""Execute ``python -m asyncio ps`` for *pid* and stream the results."""
|
|
189
|
+
|
|
190
|
+
cmd = [sys.executable, "-m", "asyncio", "ps", str(pid)]
|
|
191
|
+
try:
|
|
192
|
+
process = await asyncio.create_subprocess_exec(
|
|
193
|
+
*cmd,
|
|
194
|
+
stdout=asyncio.subprocess.PIPE,
|
|
195
|
+
stderr=asyncio.subprocess.PIPE,
|
|
196
|
+
)
|
|
197
|
+
except FileNotFoundError:
|
|
198
|
+
self._display_ps_failure(pid, "Failed to locate python executable to launch asyncio ps.")
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
stdout_bytes, stderr_bytes = await process.communicate()
|
|
202
|
+
stdout_text = stdout_bytes.decode("utf-8", errors="replace")
|
|
203
|
+
stderr_text = stderr_bytes.decode("utf-8", errors="replace")
|
|
204
|
+
|
|
205
|
+
self.log.debug(
|
|
206
|
+
"asyncio ps completed",
|
|
207
|
+
pid=pid,
|
|
208
|
+
returncode=process.returncode,
|
|
209
|
+
stdout_preview=stdout_text[:200],
|
|
210
|
+
stderr_preview=stderr_text[:200],
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if process.returncode != 0:
|
|
214
|
+
message = stderr_text or "asyncio ps exited with a non-zero status."
|
|
215
|
+
self._display_ps_failure(pid, message.strip())
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
self._display_ps_success(pid, stdout_text, stderr_text)
|
|
219
|
+
|
|
220
|
+
def _display_ps_success(self, pid: int, stdout_text: str, stderr_text: str) -> None:
|
|
221
|
+
header = self.query_one("#ps-title", Static)
|
|
222
|
+
header.update(f"asyncio ps output for PID {pid}")
|
|
223
|
+
log = self.query_one("#ps-output", Log)
|
|
224
|
+
log.clear()
|
|
225
|
+
lines: list[str] = [f"$ {sys.executable} -m asyncio ps {pid}"]
|
|
226
|
+
|
|
227
|
+
if stdout_text.strip():
|
|
228
|
+
lines.append("-- stdout --")
|
|
229
|
+
lines.extend(stdout_text.splitlines())
|
|
230
|
+
else:
|
|
231
|
+
lines.append("(stdout was empty)")
|
|
232
|
+
|
|
233
|
+
if stderr_text.strip():
|
|
234
|
+
lines.append("")
|
|
235
|
+
lines.append("-- stderr --")
|
|
236
|
+
lines.extend(stderr_text.splitlines())
|
|
237
|
+
|
|
238
|
+
log.write_lines(lines)
|
|
239
|
+
|
|
240
|
+
def _display_ps_failure(self, pid: int, message: str) -> None:
|
|
241
|
+
header = self.query_one("#ps-title", Static)
|
|
242
|
+
header.update(f"Failed to capture asyncio ps for PID {pid}")
|
|
243
|
+
log = self.query_one("#ps-output", Log)
|
|
244
|
+
log.clear()
|
|
245
|
+
log.write_lines([
|
|
246
|
+
f"$ {sys.executable} -m asyncio ps {pid}",
|
|
247
|
+
"-- error --",
|
|
248
|
+
message or "asyncio ps failed with an unknown error.",
|
|
249
|
+
])
|
atv-0.1.0/atv/cli.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Utilities for discovering asyncio-capable Python processes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Iterable, List, Sequence
|
|
8
|
+
|
|
9
|
+
import psutil
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class ProcessInfo:
|
|
14
|
+
"""Lightweight descriptor for a running Python process."""
|
|
15
|
+
|
|
16
|
+
pid: int
|
|
17
|
+
name: str
|
|
18
|
+
command_line: str
|
|
19
|
+
file: str | None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def list_python_processes() -> List[ProcessInfo]:
|
|
23
|
+
"""Return visible Python processes that are likely asyncio-capable.
|
|
24
|
+
|
|
25
|
+
We treat a process as "Python" if either its executable name or command
|
|
26
|
+
line contains the token ``python``. Processes we cannot introspect (e.g.
|
|
27
|
+
due to permissions) are skipped.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
candidates: List[ProcessInfo] = []
|
|
31
|
+
for proc in psutil.process_iter(attrs=["pid", "name", "cmdline"]):
|
|
32
|
+
try:
|
|
33
|
+
name = proc.info.get("name") or ""
|
|
34
|
+
cmdline: Iterable[str] = proc.info.get("cmdline") or []
|
|
35
|
+
cmdline_list: List[str] = list(cmdline)
|
|
36
|
+
mark = "python" in name.lower() or any(
|
|
37
|
+
"python" in part.lower() for part in cmdline_list
|
|
38
|
+
)
|
|
39
|
+
if not mark:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
command_line = " ".join(cmdline_list) if cmdline_list else name
|
|
43
|
+
file = _detect_entrypoint(cmdline_list)
|
|
44
|
+
candidates.append(
|
|
45
|
+
ProcessInfo(
|
|
46
|
+
pid=proc.pid, name=name, command_line=command_line, file=file
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
except (psutil.AccessDenied, psutil.NoSuchProcess):
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
candidates.sort(key=lambda info: info.pid)
|
|
53
|
+
return candidates
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _detect_entrypoint(cmdline: Sequence[str]) -> str | None:
|
|
57
|
+
"""Best-effort extraction of the target script/module name.
|
|
58
|
+
|
|
59
|
+
Returns a basename for file paths, the module string for ``-m`` invocations,
|
|
60
|
+
or ``None`` if nothing matches.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
if not cmdline:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
remainder = list(cmdline)
|
|
67
|
+
|
|
68
|
+
# If command begins with python executable, skip it.
|
|
69
|
+
if remainder and "python" in os.path.basename(remainder[0]).lower():
|
|
70
|
+
remainder = remainder[1:]
|
|
71
|
+
|
|
72
|
+
i = 0
|
|
73
|
+
while i < len(remainder):
|
|
74
|
+
token = remainder[i]
|
|
75
|
+
if token == "-m" and i + 1 < len(remainder):
|
|
76
|
+
return remainder[i + 1]
|
|
77
|
+
if token.startswith("-"):
|
|
78
|
+
i += 1
|
|
79
|
+
continue
|
|
80
|
+
# Treat first non-option token as a potential script path.
|
|
81
|
+
return os.path.basename(token)
|
|
82
|
+
return None
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: atv
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TaskScope: a TUI tool for experimenting with Python 3.14 asyncio ps/pstree features
|
|
5
|
+
Requires-Python: >=3.14
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: textual<0.61,>=0.57
|
|
9
|
+
Requires-Dist: psutil>=5.9
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# atv 🌱
|
|
13
|
+
|
|
14
|
+
<img width="2850" height="1480" alt="image" src="https://github.com/user-attachments/assets/9b8916b7-4ca0-4715-b63b-280f2c1bf8fd" />
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
atv(async task viewer) is a CLI for exploring Python `asyncio` processes with a Textual UI. It starts by listing Python processes running locally, and we plan to integrate execution of `asyncio ps` / `pstree` and call-graph visualization.
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd atv
|
|
23
|
+
uv sync
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`uv sync` installs the specified dependencies (`textual`, `psutil`) into your local environment.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv run atv
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Shortcuts:
|
|
35
|
+
|
|
36
|
+
- `R` – Refresh the process list (the table shows PID / Process / File / Command Line)
|
|
37
|
+
- `Q` – Exit
|
|
38
|
+
- `Enter` / `P` – Run `python -m asyncio ps` for the selected PID and display the result in the right pane
|
|
39
|
+
|
|
40
|
+
### Using uvx
|
|
41
|
+
|
|
42
|
+
Once released, you'll be able to launch it with a one-liner.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uvx atv
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If you want to test uvx behavior during local development, run the following in the project root.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uvx --from . atv
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Roadmap
|
|
55
|
+
|
|
56
|
+
- View that periodically runs `python -m asyncio ps` for the selected process
|
|
57
|
+
- Visualization for `asyncio pstree` / `format_call_graph`
|
|
58
|
+
- Snapshot capture and diff comparison
|
|
59
|
+
- Integration with the FastAPI demo (retrieve task information via the API)
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
This project is licensed under the MIT License. See `LICENSE` for details.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
atv/__init__.py
|
|
5
|
+
atv/__main__.py
|
|
6
|
+
atv/app.py
|
|
7
|
+
atv/cli.py
|
|
8
|
+
atv/processes.py
|
|
9
|
+
atv.egg-info/PKG-INFO
|
|
10
|
+
atv.egg-info/SOURCES.txt
|
|
11
|
+
atv.egg-info/dependency_links.txt
|
|
12
|
+
atv.egg-info/entry_points.txt
|
|
13
|
+
atv.egg-info/requires.txt
|
|
14
|
+
atv.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
atv
|
atv-0.1.0/pyproject.toml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "atv"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "TaskScope: a TUI tool for experimenting with Python 3.14 asyncio ps/pstree features"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"textual>=0.57,<0.61",
|
|
9
|
+
"psutil>=5.9",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
atv = "atv.cli:main"
|
|
14
|
+
|
|
15
|
+
[tool.uv]
|
|
16
|
+
package = true
|
atv-0.1.0/setup.cfg
ADDED