Skip to content

Commit 6d3727e

Browse files
Merge pull request #1 from UMass-Rescue/jagath/ui
UI
2 parents e44e67a + 4ca43c8 commit 6d3727e

File tree

15 files changed

+786
-161
lines changed

15 files changed

+786
-161
lines changed

docs

Submodule docs updated from 82ff4f4 to 2458047

src/rb-api/rb/api/models.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55

66
class CommandResult(BaseModel):
77
result: Any
8-
stdout: list[str]
8+
stdout: str | list[str]
99
success: bool
1010
error: str | None

src/rb-api/rb/api/routes/cli.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import inspect
6+
import time
67
from typing import Callable, Generator, Optional
78

89
import typer
@@ -35,20 +36,23 @@ def static_endpoint(callback: Callable, *args, **kwargs) -> CommandResult:
3536

3637
def streaming_endpoint(callback: Callable, *args, **kwargs) -> Generator:
3738
"""Execute a CLI command and stream the results"""
38-
line_buffer = []
39+
line_buffer = ""
3940
for line in capture_stdout_as_generator(callback, *args, **kwargs):
40-
line_buffer.append(line)
41+
line_buffer += line
4142
result = CommandResult(
4243
result=line, stdout=line_buffer, success=True, error=None
4344
)
4445
yield result.model_dump_json()
46+
time.sleep(0.01) # Sleep to prevent flooding the stream
4547

4648

4749
def command_callback(command: typer.models.CommandInfo):
4850
"""Create a FastAPI endpoint handler for a Typer CLI command"""
51+
# TODO: this works, but it expects all the inputs as query parameters
52+
# We should be able to handle the inputs as a JSON body
53+
4954
# Get the original callback signature
5055
original_signature = inspect.signature(command.callback)
51-
5256
# Add streaming parameter to signature
5357
new_params = list(original_signature.parameters.values())
5458
streaming_param = inspect.Parameter(
@@ -63,7 +67,7 @@ def command_callback(command: typer.models.CommandInfo):
6367
@with_signature(new_signature)
6468
def wrapper(*args, **kwargs):
6569
streaming = kwargs.pop("streaming", False)
66-
70+
print(streaming)
6771
if streaming:
6872
return StreamingResponse(
6973
streaming_endpoint(command.callback, *args, **kwargs)
@@ -93,7 +97,7 @@ def wrapper(*args, **kwargs):
9397

9498
for command in plugin.typer_instance.registered_commands:
9599
router.add_api_route(
96-
f"/{command.callback.__name__}/",
100+
f"/{command.callback.__name__}",
97101
endpoint=command_callback(command),
98102
methods=["POST"],
99103
name=command.callback.__name__,

src/rb-api/rb/api/static/index/main.css

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/rb-api/rb/api/static/index/main.js

+16-12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/rb-api/rb/api/templates/index.html.j2

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta charset="UTF-8">
66
<meta name="viewport" content="width=device-width, initial-scale=1.0">
77
<link rel="stylesheet" href="{{ url_for('static', path='index/main.css') }}"/>
8-
<title>FastAPI + React</title>
8+
<title>Rescuebox CLI</title>
99
</head>
1010
<body>
1111
<!-- Embed JSON data in a script tag -->
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import requests
2+
3+
response = requests.post("http://localhost:8000/manage/list_plugins?streaming=True")
4+
5+
for chunk in response.iter_content(chunk_size=8192):
6+
print(chunk)

src/rb-file-utils/rb_file_utils/main.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
from typing import Annotated
23

34
import typer
45

@@ -26,7 +27,7 @@ def ls(
2627

2728

2829
@app.command()
29-
def open(path: str = typer.Argument(..., help="The path to open")) -> str:
30+
def op(path: str = typer.Argument(..., help="The path to open")) -> str:
3031
"""
3132
Open a file
3233
"""
@@ -35,3 +36,18 @@ def open(path: str = typer.Argument(..., help="The path to open")) -> str:
3536
raise typer.Abort()
3637
typer.launch(path)
3738
return path
39+
40+
41+
@app.command()
42+
def head(
43+
path: str = typer.Argument(..., help="The path to the file to cat"),
44+
n: Annotated[int, typer.Option("-n", help="The number of lines to print")] = 10,
45+
) -> str:
46+
"""
47+
Print the first n lines of a file
48+
"""
49+
with open(path, "r") as f:
50+
for _ in range(n):
51+
print(f.readline())
52+
53+
return path

src/rb-lib/rb/lib/typer.py

+33-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,33 @@
1+
import inspect
2+
import typing
3+
14
import typer
2-
from anytree import Node, RenderTree
3-
from loguru import logger
5+
from anytree import Node
6+
7+
8+
def get_inputs_from_signature(signature: inspect.Signature) -> list[dict]:
9+
result = []
10+
for param in signature.parameters.values():
11+
data = {
12+
"name": param.name,
13+
}
14+
if typing.get_origin(param.annotation) == typing.Annotated:
15+
data["type"] = typing.get_args(param.annotation)[0].__name__
16+
data["help"] = typing.get_args(param.annotation)[1].help
17+
else:
18+
data["type"] = param.annotation.__name__
19+
20+
if isinstance(param.default, typer.models.OptionInfo):
21+
data["default"] = param.default.default
22+
data["help"] = param.default.help
23+
elif isinstance(param.default, typer.models.ArgumentInfo):
24+
data["help"] = param.default.help
25+
elif param.default is not inspect.Parameter.empty:
26+
data["default"] = param.default
27+
else:
28+
data["default"] = None
29+
result.append(data)
30+
return result
431

532

633
def typer_app_to_tree(app: typer.Typer) -> dict:
@@ -28,20 +55,19 @@ def add_commands_to_node(typer_app: typer.Typer, parent_node: Node):
2855
parent=parent_node,
2956
command=command,
3057
is_group=False,
58+
signature=inspect.signature(command.callback),
3159
)
3260

3361
# Build the full tree structure
3462
add_commands_to_node(app, root)
3563

36-
# Debug print the tree
37-
for pre, fill, node in RenderTree(root):
38-
logger.debug("%s%s" % (pre, node.name))
39-
4064
def node_to_dict(node: Node) -> dict:
41-
result = {"name": node.name, "is_group": node.is_group}
65+
result = {"name": node.name, "is_group": node.is_group, "help": None}
4266
if not node.is_group:
4367
# the endpoint is the path without the rescuebox root
4468
result["endpoint"] = "/" + "/".join([_.name for _ in node.path][1:])
69+
result["inputs"] = get_inputs_from_signature(node.signature)
70+
result["help"] = node.command.callback.__doc__
4571
if node.children:
4672
result["children"] = [node_to_dict(child) for child in node.children]
4773
return result

0 commit comments

Comments
 (0)