55
66import asyncio
77import os
8+ from datetime import datetime , timezone
89from pprint import pformat
910from typing import Any , List
1011
1617from prompt_toolkit .patch_stdout import patch_stdout
1718from prompt_toolkit .shortcuts import CompleteStyle
1819
19- from frequenz .client .dispatch .recurrence import (
20- EndCriteria ,
21- Frequency ,
22- RecurrenceRule ,
23- Weekday ,
24- )
25-
2620from ._cli_types import (
2721 FuzzyDateTime ,
2822 FuzzyIntRange ,
3125 TargetComponentParamType ,
3226)
3327from ._client import Client
28+ from .recurrence import EndCriteria , Frequency , RecurrenceRule , Weekday
29+ from .types import Dispatch
3430
3531DEFAULT_DISPATCH_API_URL = "grpc://fz-0004.frequenz.io:50051"
3632
3733
34+ def format_datetime (dt : datetime | None ) -> str :
35+ """Format datetime object to a readable string, or return 'N/A' if None."""
36+ return dt .strftime ("%Y-%m-%d %H:%M:%S %Z" ) if dt else "N/A"
37+
38+
39+ def format_recurrence (recurrence : RecurrenceRule ) -> str :
40+ """Format the recurrence rule, omitting empty or unspecified fields."""
41+ parts : List [str ] = []
42+ # Since frequency is not UNSPECIFIED here (we check before calling this function)
43+ parts .append (f"Frequency: { recurrence .frequency .name } " )
44+ if recurrence .interval :
45+ parts .append (f"Interval: { recurrence .interval } " )
46+ if recurrence .end_criteria :
47+ parts .append (f"End Criteria: { recurrence .end_criteria } " )
48+ # Include only non-empty lists
49+ if recurrence .byminutes :
50+ parts .append (f"Minutes: { ', ' .join (map (str , recurrence .byminutes ))} " )
51+ if recurrence .byhours :
52+ parts .append (f"Hours: { ', ' .join (map (str , recurrence .byhours ))} " )
53+ if recurrence .byweekdays :
54+ weekdays = ", " .join (day .name for day in recurrence .byweekdays )
55+ parts .append (f"Weekdays: { weekdays } " )
56+ if recurrence .bymonthdays :
57+ parts .append (f"Month Days: { ', ' .join (map (str , recurrence .bymonthdays ))} " )
58+ if recurrence .bymonths :
59+ months = ", " .join (map (month_name , recurrence .bymonths ))
60+ parts .append (f"Months: { months } " )
61+ return "\n " .join (parts )
62+
63+
64+ def month_name (month : int ) -> str :
65+ """Return the name of the month."""
66+ return datetime (2000 , month , 1 ).strftime ("%B" )
67+
68+
69+ # pylint: disable=too-many-statements, too-many-locals
70+ def print_dispatch (dispatch : Dispatch ) -> None :
71+ """Print the dispatch details in a nicely formatted way with colors."""
72+ # Determine the status and color
73+ status : str = "running" if dispatch .started else "not running"
74+ status_color : str = "green" if dispatch .started else "red"
75+ status_str : str = click .style (status , fg = status_color , bold = True )
76+
77+ # Format the next start time with color
78+ next_start_time_str : str = format_datetime (dispatch .next_run )
79+ next_start_time_colored : str = click .style (next_start_time_str , fg = "cyan" )
80+
81+ start_in_timedelta = (
82+ dispatch .next_run - datetime .now (timezone .utc ) if dispatch .next_run else None
83+ )
84+ start_in_timedelta_str = str (start_in_timedelta ) if start_in_timedelta else "N/A"
85+ start_in_timedelta_colored = click .style (start_in_timedelta_str , fg = "yellow" )
86+
87+ # Format the target
88+ if dispatch .target :
89+ if len (dispatch .target ) == 1 :
90+ target_str : str = str (dispatch .target [0 ])
91+ else :
92+ target_str = ", " .join (str (s ) for s in dispatch .target )
93+ else :
94+ target_str = "None"
95+
96+ # Prepare the dispatch details
97+ lines : List [str ] = []
98+ # Define the keys for alignment
99+ keys = [
100+ "ID" ,
101+ "Type" ,
102+ "Start Time" ,
103+ "Duration" ,
104+ "Target" ,
105+ "Active" ,
106+ "Dry Run" ,
107+ "Payload" ,
108+ "Recurrence" ,
109+ "Create Time" ,
110+ "Update Time" ,
111+ ]
112+ max_key_length : int = max (len (k ) for k in keys )
113+
114+ # Helper function to format each line
115+ def format_line (key : str , value : str , color : str = "cyan" ) -> str :
116+ key_str = click .style (f"{ key } :" , fg = color )
117+ val_color = "white"
118+
119+ if value in ("None" , "False" ):
120+ val_color = "red"
121+ elif value == "True" :
122+ val_color = "green"
123+
124+ val_str : str = click .style (value , fg = val_color )
125+ return f"{ key_str :<{max_key_length + 2 }} { val_str } "
126+
127+ lines .append (click .style ("Dispatch Details:" , bold = True , underline = True ))
128+ lines .append (format_line ("ID" , str (dispatch .id )))
129+ lines .append (format_line ("Type" , str (dispatch .type )))
130+ lines .append (format_line ("Start Time" , format_datetime (dispatch .start_time )))
131+ if dispatch .duration :
132+ lines .append (format_line ("Duration" , str (dispatch .duration )))
133+ else :
134+ lines .append (format_line ("Duration" , "Infinite" ))
135+ lines .append (format_line ("Target" , target_str ))
136+ lines .append (format_line ("Active" , str (dispatch .active )))
137+ lines .append (format_line ("Dry Run" , str (dispatch .dry_run )))
138+ if dispatch .payload :
139+ lines .append (format_line ("Payload" , str (dispatch .payload )))
140+ # Only include recurrence if frequency is not UNSPECIFIED
141+ if dispatch .recurrence and dispatch .recurrence .frequency != Frequency .UNSPECIFIED :
142+ recurrence_str = format_recurrence (dispatch .recurrence )
143+ # Indent recurrence details for better readability
144+ indented_recurrence = "\n " + recurrence_str .replace ("\n " , "\n " )
145+ lines .append (format_line ("Recurrence" , indented_recurrence , "green" ))
146+ else :
147+ lines .append (format_line ("Recurrence" , "None" ))
148+ lines .append (format_line ("Create Time" , format_datetime (dispatch .create_time )))
149+ lines .append (format_line ("Update Time" , format_datetime (dispatch .update_time )))
150+
151+ # Combine all lines
152+ dispatch_info : str = "\n " .join (lines )
153+
154+ # Output the formatted dispatch details
155+ click .echo (f"{ dispatch_info } \n " )
156+ click .echo (f"Dispatch is currently { status_str } " )
157+ click .echo (
158+ f"Next start in: { start_in_timedelta_colored } ({ next_start_time_colored } )\n "
159+ )
160+
161+
38162# Click command groups
39163@click .group (invoke_without_command = True )
40164@click .option (
52176 show_envvar = True ,
53177 required = True ,
54178)
179+ @click .option (
180+ "--raw" ,
181+ is_flag = True ,
182+ help = "Print output raw instead of formatted and colored" ,
183+ required = False ,
184+ default = False ,
185+ )
55186@click .pass_context
56- async def cli (ctx : click .Context , url : str , key : str ) -> None :
187+ async def cli (ctx : click .Context , url : str , key : str , raw : bool ) -> None :
57188 """Dispatch Service CLI."""
58189 if ctx .obj is None :
59190 ctx .obj = {}
@@ -71,6 +202,8 @@ async def cli(ctx: click.Context, url: str, key: str) -> None:
71202 "key" : key ,
72203 }
73204
205+ ctx .obj ["raw" ] = raw
206+
74207 # Check if a subcommand was given
75208 if ctx .invoked_subcommand is None :
76209 await interactive_mode (url , key )
@@ -102,7 +235,10 @@ async def list_(ctx: click.Context, /, **filters: Any) -> None:
102235 num_dispatches = 0
103236 async for page in ctx .obj ["client" ].list (** filters ):
104237 for dispatch in page :
105- click .echo (pformat (dispatch , compact = True ))
238+ if ctx .obj ["raw" ]:
239+ click .echo (pformat (dispatch , compact = True ))
240+ else :
241+ print_dispatch (dispatch )
106242 num_dispatches += 1
107243
108244 click .echo (f"{ num_dispatches } dispatches total." )
@@ -114,7 +250,10 @@ async def list_(ctx: click.Context, /, **filters: Any) -> None:
114250async def stream (ctx : click .Context , microgrid_id : int ) -> None :
115251 """Stream dispatches."""
116252 async for message in ctx .obj ["client" ].stream (microgrid_id = microgrid_id ):
117- click .echo (pformat (message , compact = True ))
253+ if ctx .obj ["raw" ]:
254+ click .echo (pformat (message , compact = True ))
255+ else :
256+ print_dispatch (message )
118257
119258
120259def parse_recurrence (kwargs : dict [str , Any ]) -> RecurrenceRule | None :
@@ -275,7 +414,10 @@ async def create(
275414 recurrence = parse_recurrence (kwargs ),
276415 ** kwargs ,
277416 )
278- click .echo (pformat (dispatch , compact = True ))
417+ if ctx .obj ["raw" ]:
418+ click .echo (pformat (dispatch , compact = True ))
419+ else :
420+ print_dispatch (dispatch )
279421 click .echo ("Dispatch created." )
280422
281423
@@ -330,7 +472,10 @@ def skip_field(value: Any) -> bool:
330472 microgrid_id = microgrid_id , dispatch_id = dispatch_id , new_fields = new_fields
331473 )
332474 click .echo ("Dispatch updated:" )
333- click .echo (pformat (changed_dispatch , compact = True ))
475+ if ctx .obj ["raw" ]:
476+ click .echo (pformat (changed_dispatch , compact = True ))
477+ else :
478+ print_dispatch (changed_dispatch )
334479 except grpc .RpcError as e :
335480 raise click .ClickException (f"Update failed: { e } " )
336481
@@ -348,7 +493,10 @@ async def get(ctx: click.Context, microgrid_id: int, dispatch_ids: List[int]) ->
348493 dispatch = await ctx .obj ["client" ].get (
349494 microgrid_id = microgrid_id , dispatch_id = dispatch_id
350495 )
351- click .echo (pformat (dispatch , compact = True ))
496+ if ctx .obj ["raw" ]:
497+ click .echo (pformat (dispatch , compact = True ))
498+ else :
499+ print_dispatch (dispatch )
352500 except grpc .RpcError as e :
353501 click .echo (f"Error getting dispatch { dispatch_id } : { e } " , err = True )
354502 num_failed += 1
0 commit comments