33
44import click
55from pydantic import TypeAdapter , ValidationError
6+ from pytimeparse2 import parse as parse_duration
67
78opt_selector = click .option (
89 "-l" ,
1516class DurationParamType (click .ParamType ):
1617 name = "duration"
1718
19+ def __init__ (self , minimum : timedelta | None = None ):
20+ super ().__init__ ()
21+ self .minimum = minimum
22+
1823 def convert (self , value , param , ctx ):
1924 if isinstance (value , timedelta ):
20- return value
25+ td = value
26+ elif isinstance (value , int ):
27+ # Integer as seconds (backward compatibility)
28+ td = timedelta (seconds = value )
29+ elif isinstance (value , str ):
30+ # Try parsing as plain integer first (backward compatibility)
31+ try :
32+ int_value = int (value )
33+ td = timedelta (seconds = int_value )
34+ except ValueError :
35+ # Parse with pytimeparse2 first (supports human-readable formats)
36+ td = None
37+ try :
38+ seconds = parse_duration (value )
39+ if seconds is not None :
40+ td = timedelta (seconds = seconds )
41+ except (ValueError , TypeError ):
42+ pass
43+
44+ # Fall back to pydantic/speedate for ISO 8601 and other formats
45+ if td is None :
46+ try :
47+ td = TypeAdapter (timedelta ).validate_python (value )
48+ except (ValueError , ValidationError ):
49+ self .fail (
50+ (
51+ f"{ value !r} is not a valid duration "
52+ "(e.g., '30m', '3h30m', '1d', '1d3h40m', 'PT1H30M', '01:30:00')"
53+ ),
54+ param ,
55+ ctx ,
56+ )
57+ else :
58+ self .fail (
59+ f"{ value !r} is not a valid duration (e.g., '30m', '3h30m', '1d', '1d3h40m')" ,
60+ param ,
61+ ctx ,
62+ )
2163
22- try :
23- return TypeAdapter (timedelta ).validate_python (value )
24- except (ValueError , ValidationError ):
25- self .fail (f"{ value !r} is not a valid duration" , param , ctx )
64+ # Validate minimum if specified
65+ if self .minimum is not None and td < self .minimum :
66+ min_seconds = int (self .minimum .total_seconds ())
67+ self .fail (
68+ f"{ value !r} must be at least { min_seconds } seconds" , param , ctx
69+ )
70+
71+ return td
2672
2773
2874DURATION = DurationParamType ()
@@ -36,12 +82,11 @@ def convert(self, value, param, ctx):
3682Accepted duration formats:
3783
3884\b
39- PnYnMnDTnHnMnS - ISO 8601 duration format
40- HH:MM:SS - time in hours, minutes, seconds
41- D days, HH:MM:SS - time prefixed by X days
42- D d, HH:MM:SS - time prefixed by X d
85+ Human-readable: 30m, 3h30m, 1d, 1d3h40m, etc.
86+ ISO 8601: PT1H30M, P1DT2H30M, etc.
87+ Time format: 01:30:00, 2 days, 01:30:00, etc.
4388
44- See https://docs.rs/speedate/latest/speedate/ for details
89+ See https://github.com/wroberts/pytimeparse2 for details
4590""" ,
4691)
4792
@@ -67,6 +112,21 @@ def convert(self, value, param, ctx):
67112
68113DATETIME = DateTimeParamType ()
69114
115+
116+ ACQUISITION_TIMEOUT = DurationParamType (minimum = timedelta (seconds = 5 ))
117+
118+ opt_acquisition_timeout = partial (
119+ click .option ,
120+ "--acquisition-timeout" ,
121+ "acquisition_timeout" ,
122+ type = ACQUISITION_TIMEOUT ,
123+ default = None ,
124+ help = (
125+ "Override acquisition timeout (e.g., '30m', '3h30m', '1d', '1d3h40m', "
126+ "or seconds as integer). Must be >= 5 seconds."
127+ ),
128+ )
129+
70130opt_begin_time = click .option (
71131 "--begin-time" ,
72132 "begin_time" ,
0 commit comments