1
1
import inspect
2
- from typing import Callable , Optional
2
+ from typing import Callable , Generator , Optional
3
3
4
4
import typer
5
5
from fastapi import APIRouter , HTTPException
6
+ from fastapi .responses import StreamingResponse
6
7
from makefun import with_signature
7
8
from rb .api .models import CommandResult
8
9
from rb .lib .stdout import Capturing # type: ignore
10
+ from rb .lib .stdout import capture_stdout_as_generator
9
11
10
12
from rescuebox .main import app as rescuebox_app
11
13
12
14
cli_router = APIRouter ()
13
15
14
16
15
17
def static_endpoint (callback : Callable , * args , ** kwargs ) -> CommandResult :
18
+ """Execute a CLI command and return the result synchronously"""
16
19
with Capturing () as stdout :
17
20
try :
18
21
result = callback (* args , ** kwargs )
19
- success = True
20
- error = None
22
+ return CommandResult (result = result , stdout = stdout , success = True , error = None )
21
23
except Exception as e :
22
- result = None
23
- success = False
24
- error = f"Typer CLI aborted { e } "
25
- return CommandResult (result = result , stdout = stdout , success = success , error = error )
24
+ return CommandResult (
25
+ result = None ,
26
+ stdout = stdout ,
27
+ success = False ,
28
+ error = f"Typer CLI aborted { e } " ,
29
+ )
30
+
31
+
32
+ def streaming_endpoint (callback : Callable , * args , ** kwargs ) -> Generator :
33
+ """Execute a CLI command and stream the results"""
34
+ line_buffer = []
35
+ for line in capture_stdout_as_generator (callback , * args , ** kwargs ):
36
+ line_buffer .append (line )
37
+ result = CommandResult (
38
+ result = line , stdout = line_buffer , success = True , error = None
39
+ )
40
+ yield result .model_dump_json ()
26
41
27
42
28
43
def command_callback (command : typer .models .CommandInfo ):
44
+ """Create a FastAPI endpoint handler for a Typer CLI command"""
29
45
# Get the original callback signature
30
46
original_signature = inspect .signature (command .callback )
31
47
32
- # Modify the signature to include `streaming`
48
+ # Add streaming parameter to signature
33
49
new_params = list (original_signature .parameters .values ())
34
- new_params .append (
35
- inspect .Parameter (
36
- "streaming" ,
37
- inspect .Parameter .KEYWORD_ONLY ,
38
- default = False ,
39
- annotation = Optional [bool ],
40
- )
50
+ streaming_param = inspect .Parameter (
51
+ "streaming" ,
52
+ inspect .Parameter .KEYWORD_ONLY ,
53
+ default = False ,
54
+ annotation = Optional [bool ],
41
55
)
56
+ new_params .append (streaming_param )
42
57
new_signature = original_signature .replace (parameters = new_params )
43
58
44
- # Create a new function with the modified signature
45
59
@with_signature (new_signature )
46
60
def wrapper (* args , ** kwargs ):
47
- # Extract additional parameters
48
- # TODO(Jagath): Implement streaming
49
- streaming = kwargs .pop ("streaming" , False ) # noqa: F841
61
+ streaming = kwargs .pop ("streaming" , False )
62
+
63
+ if streaming :
64
+ return StreamingResponse (
65
+ streaming_endpoint (command .callback , * args , ** kwargs )
66
+ )
50
67
51
- # Call the static endpoint with the wrapped callback and arguments
52
68
result = static_endpoint (command .callback , * args , ** kwargs )
53
69
if not result .success :
54
- # Return the last 10 lines of stdout if there's an error
55
70
raise HTTPException (
56
71
status_code = 400 ,
57
- detail = {"error" : result .error , "stdout" : result .stdout [- 10 :]},
72
+ detail = {
73
+ "error" : result .error ,
74
+ "stdout" : result .stdout [- 10 :], # Last 10 lines of output
75
+ },
58
76
)
59
77
return result
60
78
79
+ # Preserve original function metadata
61
80
wrapper .__name__ = command .callback .__name__
62
81
wrapper .__doc__ = command .callback .__doc__
63
82
64
83
return wrapper
65
84
66
85
86
+ # Register routes for each plugin command
67
87
for plugin in rescuebox_app .registered_groups :
68
88
router = APIRouter ()
89
+
69
90
for command in plugin .typer_instance .registered_commands :
70
91
router .add_api_route (
71
92
f"/{ command .callback .__name__ } /" ,
@@ -74,4 +95,5 @@ def wrapper(*args, **kwargs):
74
95
name = command .callback .__name__ ,
75
96
response_model = CommandResult ,
76
97
)
98
+
77
99
cli_router .include_router (router , prefix = f"/{ plugin .name } " , tags = [plugin .name ])
0 commit comments