5
5
import os
6
6
import datetime
7
7
from argparse import ArgumentParser , ArgumentDefaultsHelpFormatter , Namespace , Action , ArgumentError
8
+ import typing
8
9
from typing import Optional , List , Callable , Sequence , Dict , Any , Union , Type
9
10
from textwrap import indent
10
- import json
11
11
import yaml
12
12
from pydantic import BaseModel
13
- from pydantic .fields import ModelField
14
- from pydantic .main import ModelMetaclass
13
+ from pydantic .fields import FieldInfo
15
14
from dotenv import load_dotenv
16
15
from aiohttp import TCPConnector , ClientSession
17
16
from aiohttp .client_exceptions import ServerDisconnectedError
26
25
# pylint: disable=logging-fstring-interpolation
27
26
28
27
# constants
29
- log = logging .getLogger (__name__ ) # central logging channel
30
- json_ser_kwargs : Dict [ str , Any ] = dict (
31
- exclude_unset = True , indent = 2 ) # arguments to serialise the json
28
+ log = logging .getLogger (__name__ ) # central logging channel
29
+ # arguments to serialise the json
30
+ json_ser_kwargs : Dict [ str , Any ] = { ' exclude_unset' : True , ' indent' : 2 }
32
31
33
32
34
33
# text formatters
@@ -44,7 +43,7 @@ def table(d, h) -> str:
44
43
45
44
# simple list json for List[BaseModel] constructs
46
45
def model_list_to_json (it : Sequence [BaseModel ]) -> str :
47
- return f"[{ ',' .join ([i .json (** json_ser_kwargs ) for i in it ])} ]"
46
+ return f"[{ ',' .join ([i .model_dump_json (** json_ser_kwargs ) for i in it ])} ]"
48
47
49
48
50
49
# list methods dumping comprehensive output to stdout
@@ -166,7 +165,7 @@ async def do_status_get(args: Namespace, amplipi: AmpliPi, shell: bool, **kwargs
166
165
"""
167
166
log .debug ("status.get()" )
168
167
status : Status = await amplipi .get_status ()
169
- write_out (status .json (** json_ser_kwargs ), args .outfile )
168
+ write_out (status .model_dump_json (** json_ser_kwargs ), args .outfile )
170
169
171
170
172
171
async def do_config_load (args : Namespace , amplipi : AmpliPi , shell : bool , ** kwargs ):
@@ -227,7 +226,7 @@ async def do_info_get(args: Namespace, amplipi: AmpliPi, shell: bool, **kwargs):
227
226
"""
228
227
log .debug ("status.info()" )
229
228
info : Info = await amplipi .get_info ()
230
- write_out (info .json (** json_ser_kwargs ), args .outfile )
229
+ write_out (info .model_dump_json (** json_ser_kwargs ), args .outfile )
231
230
232
231
233
232
# -- source section
@@ -245,7 +244,7 @@ async def do_source_get(args: Namespace, amplipi: AmpliPi, shell: bool, **kwargs
245
244
log .debug (f"source.get({ args .sourceid } )" )
246
245
assert 0 <= args .sourceid <= 3 , "source id must be in range 0..3"
247
246
source : Source = await amplipi .get_source (args .sourceid )
248
- write_out (source .json (** json_ser_kwargs ), args .outfile )
247
+ write_out (source .model_dump_json (** json_ser_kwargs ), args .outfile )
249
248
250
249
251
250
async def do_source_getall (args : Namespace , amplipi : AmpliPi , shell : bool , ** kwargs ):
@@ -297,7 +296,7 @@ async def do_zone_get(args: Namespace, amplipi: AmpliPi, shell: bool, **kwargs):
297
296
log .debug (f"zone.get({ args .zoneid } )" )
298
297
assert 0 <= args .zoneid <= 35 , "zone id must be in range 0..35"
299
298
zone : Zone = await amplipi .get_zone (args .zoneid )
300
- write_out (zone .json (** json_ser_kwargs ), args .outfile )
299
+ write_out (zone .model_dump_json (** json_ser_kwargs ), args .outfile )
301
300
302
301
303
302
async def do_zone_getall (args : Namespace , amplipi : AmpliPi , shell : bool , ** kwargs ):
@@ -348,7 +347,7 @@ async def do_group_get(args: Namespace, amplipi: AmpliPi, shell: bool, **kwargs)
348
347
log .debug (f"group.get({ args .groupid } )" )
349
348
assert 0 <= args .groupid , "group id must be > 0"
350
349
group : Group = await amplipi .get_group (args .groupid )
351
- write_out (group .json (** json_ser_kwargs ), args .outfile )
350
+ write_out (group .model_dump_json (** json_ser_kwargs ), args .outfile )
352
351
353
352
354
353
async def do_group_getall (args : Namespace , amplipi : AmpliPi , shell : bool , ** kwargs ):
@@ -414,7 +413,7 @@ async def do_stream_get(args: Namespace, amplipi: AmpliPi, shell: bool, **kwargs
414
413
log .debug (f"stream.get({ args .streamid } )" )
415
414
assert 0 <= args .streamid , "stream id must be > 0"
416
415
stream : Stream = await amplipi .get_stream (args .streamid )
417
- write_out (stream .json (** json_ser_kwargs ), args .outfile )
416
+ write_out (stream .model_dump_json (** json_ser_kwargs ), args .outfile )
418
417
419
418
420
419
async def do_stream_getall (args : Namespace , amplipi : AmpliPi , shell : bool , ** kwargs ):
@@ -529,7 +528,7 @@ async def do_preset_get(args: Namespace, amplipi: AmpliPi, shell: bool, **kwargs
529
528
log .debug (f"preset.get({ args .presetid } )" )
530
529
assert 0 <= args .presetid , "preset id must be > 0"
531
530
preset : Preset = await amplipi .get_preset (args .presetid )
532
- write_out (preset .json (** json_ser_kwargs ), args .outfile )
531
+ write_out (preset .model_dump_json (** json_ser_kwargs ), args .outfile )
533
532
534
533
535
534
async def do_preset_getall (args : Namespace , amplipi : AmpliPi , shell : bool , ** kwargs ):
@@ -670,7 +669,7 @@ async def shell_cmd_exec(cmdline: str, amplipi: AmpliPi, argsparser: ArgumentPar
670
669
print (e )
671
670
672
671
673
- def instantiate_model (model_cls : ModelMetaclass , infile : str , _input : Optional [Dict [str , Any ]] = None ,
672
+ def instantiate_model (model_cls , infile : str , _input : Optional [Dict [str , Any ]] = None ,
674
673
validate : Optional [Callable ] = None ):
675
674
""" Instatiates the passed BaseModel based on:
676
675
(1) either the passed input dict (if not None) merged with env var defaults
@@ -688,10 +687,10 @@ def instantiate_model(model_cls: ModelMetaclass, infile: str, _input: Optional[D
688
687
validate (_input )
689
688
return model_cls (** _input )
690
689
# else read the object from stdin (json)
691
- return model_cls .parse_obj ( json . loads ( read_in (infile ) )) # type: ignore
690
+ return model_cls .model_validate_json ( read_in (infile )) # type: ignore
692
691
693
692
694
- def merge_model_kwargs (model_cls : ModelMetaclass , input : dict ) -> Dict [str , Any ]:
693
+ def merge_model_kwargs (model_cls , _input : dict ) -> Dict [str , Any ]:
695
694
""" Builds the kwargs needed to construct the passed BaseModel by merging the passed input dict
696
695
with possible available environment variables with key following this pattern:
697
696
"AMPLIPI_" + «name of BaseModel» + "_" + «name of field in BaseModel» (in all caps)
@@ -700,32 +699,40 @@ def envvar(name):
700
699
envkey = f"AMPLIPI_{ model_cls .__name__ } _{ name } " .upper ()
701
700
return os .getenv (envkey )
702
701
kwargs = dict ()
703
- for name , modelfield in model_cls .__fields__ .items (): # type: ignore
704
- value_str : str = input .get (name , envvar (name ))
702
+ for name , modelfield in model_cls .model_fields .items (): # type: ignore
703
+ value_str : str = _input .get (name , envvar (name ))
705
704
if value_str is not None and isinstance (value_str , str ) and len (value_str ) > 0 :
706
705
value = parse_valuestr (value_str , modelfield )
707
706
log .debug (
708
- f"converted { value_str } to { value } for { modelfield .type_ } " )
707
+ f"converted { value_str } to { value } for { modelfield .annotation } " )
709
708
kwargs [name ] = value
710
709
return kwargs
711
710
712
711
713
712
# helper functions for the arguments parsing
714
- def parse_valuestr (val_str : str , modelfield : ModelField ):
715
- """ Uses the pydantic defined Modelfield to correctly parse CLI passed string-values to typed values
716
- Supports simple types and lists of them
717
- """
718
- convertor = modelfield .type_
719
- if convertor == bool :
720
- def boolconvertor (s ):
713
+ def parse_valuestr (val_str : str , modelfield : FieldInfo ):
714
+ """ Uses the pydantic defined FieldInfo to correctly parse CLI passed string-values to typed values
715
+ Supports simple types and lists of them which can be wrapped in Optional.
716
+ TODO: This is fairly fragile. We should find a more robust solution.
717
+ """
718
+ converter : Union [Type , None , Callable ] = modelfield .annotation
719
+
720
+ if getattr (converter , '_name' , None ) == "Optional" :
721
+ # Optional needs to be manually unwrapped to the inner type
722
+ converter = typing .get_args (converter )[0 ] # unwrap Optional
723
+ if converter is bool :
724
+ def boolconverter (s ):
721
725
return len (s ) > 0 and s .lower () in ('y' , 'yes' , '1' , 'true' , 'on' )
722
- convertor = boolconvertor
723
- if modelfield . outer_type_ . __name__ == 'List' :
726
+ converter = boolconverter
727
+ if converter is list :
724
728
assert val_str [0 ] == '[' and val_str [- 1 ] == ']' , "expected array-value needs to be surrounded with []"
725
729
val_str = val_str [1 :- 1 ]
726
- return [convertor (v .strip ()) for v in val_str .split (',' )]
727
- # else
728
- return convertor (val_str )
730
+ return [converter (v .strip ()) for v in val_str .split (',' )]
731
+ if converter is None :
732
+ log .warning (
733
+ f"no converter for { modelfield .title } not converting { val_str } " )
734
+ return val_str
735
+ return converter (val_str )
729
736
730
737
731
738
class ParseDict (Action ):
@@ -756,7 +763,7 @@ def add_force_argument(ap: ArgumentParser):
756
763
help = "force the command to be executed without interaction." )
757
764
758
765
759
- def add_id_argument (ap : ArgumentParser , model_cls : ModelMetaclass ):
766
+ def add_id_argument (ap : ArgumentParser , model_cls ):
760
767
""" Adds the --input argument in a consistent way
761
768
"""
762
769
name = model_cls .__name__ .lower ()
@@ -766,7 +773,7 @@ def add_id_argument(ap: ArgumentParser, model_cls: ModelMetaclass):
766
773
help = "identifier of the {name} (integer)" )
767
774
768
775
769
- def add_input_arguments (ap : ArgumentParser , model_cls : ModelMetaclass , too_complex_for_cli_keyvals : bool = False ):
776
+ def add_input_arguments (ap : ArgumentParser , model_cls , too_complex_for_cli_keyvals : bool = False ):
770
777
""" Adds the --input -i and --infile -I argument in a consistent way
771
778
The -i argument takes key-value pairs to construct models rather then provide those in json via stdin (for simple models only)
772
779
The -I argument specifies an input file to use in stead of stdin
@@ -780,7 +787,7 @@ def add_input_arguments(ap: ArgumentParser, model_cls: ModelMetaclass, too_compl
780
787
if too_complex_for_cli_keyvals :
781
788
return
782
789
# else allow key-val --input
783
- fields = model_cls .__fields__ .keys () # type: ignore
790
+ fields = model_cls .model_fields .keys () # type: ignore
784
791
ap .add_argument (
785
792
'--input' , '-i' ,
786
793
action = ParseDict ,
@@ -1172,14 +1179,14 @@ def enable_logging(logconf=None):
1172
1179
1173
1180
1174
1181
# helper function to instantiate the client
1175
- def make_amplipi (args : Namespace ) -> AmpliPi :
1182
+ def make_amplipi (args : Namespace , loop ) -> AmpliPi :
1176
1183
""" Constructs the amplipi client
1177
1184
"""
1178
1185
endpoint : str = args .amplipi
1179
1186
timeout : int = args .timeout
1180
1187
# in shell modus we got frequent server-disconnected-errors - injecting this custom session avoids that
1181
- connector : TCPConnector = TCPConnector (force_close = True )
1182
- http_session : ClientSession = ClientSession (connector = connector )
1188
+ connector : TCPConnector = TCPConnector (force_close = True , loop = loop )
1189
+ http_session : ClientSession = ClientSession (connector = connector , loop = loop )
1183
1190
return AmpliPi (endpoint , timeout = timeout , http_session = http_session )
1184
1191
1185
1192
@@ -1198,10 +1205,10 @@ def main():
1198
1205
sys .exit (1 )
1199
1206
1200
1207
enable_logging (logconf = args .logconf )
1201
- amplipi = make_amplipi (args )
1202
1208
1203
1209
# setup async wait construct for main routines
1204
1210
loop = asyncio .get_event_loop_policy ().get_event_loop ()
1211
+ amplipi = make_amplipi (args , loop )
1205
1212
try :
1206
1213
# trigger the actual called action-function (async) and wait for it
1207
1214
loop .run_until_complete (
0 commit comments