arvados.commands.arvcli
Main executable for Arvados CLI SDK, the arv command.
This script implements the arv command’s argument parser. The arv command
is meant to be invoked in the following manner:
$ arv [–flags] subcommand|resource […options]
where --flags are common CLI options for the various subcommands.
The ArvCLIArgumentParser class, specializing the standard Python
argparse.ArgumentParser, provides the support for this CLI usage.
1# Copyright (C) The Arvados Authors. All rights reserved. 2# 3# SPDX-License-Identifier: Apache-2.0 4 5"""Main executable for Arvados CLI SDK, the `arv` command. 6 7This script implements the `arv` command's argument parser. The `arv` command 8is meant to be invoked in the following manner: 9 10$ arv [--flags] subcommand|resource [...options] 11 12where `--flags` are common CLI options for the various subcommands. 13 14The `ArvCLIArgumentParser` class, specializing the standard Python 15`argparse.ArgumentParser`, provides the support for this CLI usage. 16""" 17 18 19import sys 20import argparse 21import functools 22import json 23import arvados 24import arvados.commands._util as cmd_util 25 26 27class _ArgTypes: 28 """Private namespace class for JSON-related CLI argument types.""" 29 @staticmethod 30 def _validate_type(obj_type, obj): 31 if isinstance(obj, obj_type): 32 return obj 33 raise ValueError(f"{obj!r} is not of type {obj_type!s}.") 34 35 json_array = cmd_util.JSONStringArgument( 36 validator=functools.partial(_validate_type, list), 37 pretty_name="JSON array" 38 ) 39 40 json_object = cmd_util.JSONStringArgument( 41 validator=functools.partial(_validate_type, dict), 42 pretty_name="JSON object" 43 ) 44 45 json_body = cmd_util.JSONArgument( 46 validator=json_object.post_validator, 47 pretty_name="JSON request body object" 48 ) 49 50 51class _ArgUtil: 52 """Private namespace class for helpful functions (static methods) that 53 processes the discovery document for the purpose of CLI parser generation. 54 """ 55 @staticmethod 56 def singularize_resource(plural: str) -> str: 57 """Returns the singular form of a resource term in the original 58 plural. 59 """ 60 match plural: 61 case "vocabularies": 62 return "vocabulary" 63 case "sys": 64 return "sys" 65 case _: 66 return plural.removesuffix("s") 67 68 @staticmethod 69 def parameter_key_to_argument_name(parameter_key: str) -> str: 70 """Convert a parameter key in the discovery document to CLI parameter 71 form, for example, `--foo-bar`. 72 73 Arguments: 74 75 * parameter_key: str -- Parameter key in the form as they appear in the 76 discovery document, typically like `foo_bar`. 77 """ 78 return "--" + parameter_key.replace("_", "-") 79 80 @staticmethod 81 def get_method_options(method_schema): 82 """Generate command-line options, in the form of "-f/--foo", from the 83 parameters as defined by the API method schema in the discovery 84 document. 85 86 For each key "foo_bar" in the "parameters" field of the method schema, 87 command-line options are created according to its definition as 88 follows. 89 90 If the parameter type is "boolean", a pair of options "--no-foo-bar" 91 and "--foo-bar" are created, with opposite meaning. 92 93 If the parameter type is "integer", the CLI input will be interpreted 94 as a Python int. 95 96 All other parameter types are parsed as Python str. 97 98 The short form of each option will also be created, by taking the first 99 letter of the long form, except when that letter is already used, in 100 which case the second letter will be used, and so on. For example, 101 "--foo-bar" will have short form "-f", unless "-f" is already used for 102 another option, in which case "-o" will be used, etc. 103 104 The "negative" form of boolean options ("--no-foo-bar") will not have 105 separate short forms of their own. 106 107 This generator yields tuples in the form of `(names, kwargs)`, where 108 `names` is a one- or two-element tuple and `kwargs` is a dict, suitable 109 to be passed as 110 `argparse.ArgumentParser.add_argument(*names, **kwargs)`. 111 112 Arguments: 113 114 * method_schema: dict --- Dict object from the parsed discover document 115 that defines a method. 116 """ 117 parameters_schema = method_schema.get("parameters", {}).copy() 118 # If the method comes with the "request" field, add another parameter 119 # based on the sole key in the "properties" dict of that field 120 request_schema = method_schema.get("request") 121 if request_schema is not None and request_schema.get("properties"): 122 for parameter_key in request_schema["properties"].keys(): 123 parameters_schema[parameter_key] = { 124 "type": "request", # special value for request parameter 125 "required": request_schema.get("required"), 126 "description": ( 127 f"Either a string representing {parameter_key} as JSON" 128 f" or a filename from which to read {parameter_key}" 129 " JSON (use '-' to read from stdin)." 130 ) 131 } 132 argument_key_abbrevs = set("h") # prevent conflict with "help" 133 for parameter_key, parameter_dict in parameters_schema.items(): 134 parameter_kwargs = { 135 "required": parameter_dict.get("required", False) 136 } 137 parameter_kwargs["help"] = parameter_dict.get("description", "") 138 if parameter_kwargs["required"]: 139 parameter_kwargs["help"] += " This option must be specified." 140 # The "type" member refers to one of the JSON values types, out of 141 # string/integer/array/object/boolean. 142 # NOTE: Currently, enum-like value choices are not implemented, as 143 # the enum values cannot be directly inferred from the discover 144 # doc. 145 argument_key = _ArgUtil.parameter_key_to_argument_name( 146 parameter_key 147 ) 148 for argument_short_key in argument_key: 149 if ( 150 argument_short_key.isalpha() 151 and argument_short_key not in argument_key_abbrevs 152 ): 153 argument_key_abbrevs.add(argument_short_key) 154 break 155 else: 156 # If the letters of the full argument name are exhausted, fall 157 # back to not using a short argument, indicated by the special 158 # value None: 159 argument_short_key = None 160 default = parameter_dict.get("default") 161 if default is not None: 162 parameter_kwargs["default"] = default 163 if parameter_dict.get("type") != "boolean": 164 parameter_kwargs["help"] += f" Default: {default!s}." 165 match parameter_dict.get("type"): 166 case "boolean": 167 # Using the 'action="store_true" (or "store_false")' 168 # mechanism results in flag-like action rather than an 169 # option that takes a true or false value. For each bool 170 # flag "--foo", also generate an additional "negative" 171 # version "--no-foo". 172 neg_argument_key = _ArgUtil.parameter_key_to_argument_name( 173 f"no_{parameter_key}" 174 ) 175 neg_parameter_kwargs = {} 176 neg_parameter_kwargs["action"] = "store_false" 177 neg_parameter_kwargs["required"] = False 178 neg_parameter_kwargs["dest"] = parameter_key 179 neg_parameter_kwargs["default"] = json.loads( 180 parameter_dict.get("default", "null") 181 ) 182 yield (neg_argument_key,), neg_parameter_kwargs 183 184 parameter_kwargs["action"] = "store_true" 185 parameter_kwargs["dest"] = parameter_key 186 parameter_kwargs["default"] = ( 187 neg_parameter_kwargs["default"] 188 ) 189 case "integer": 190 parameter_kwargs["type"] = int 191 parameter_kwargs["metavar"] = "N" 192 case "array": 193 parameter_kwargs["type"] = _ArgTypes.json_array 194 parameter_kwargs["metavar"] = "JSON_ARRAY" 195 case "object": 196 parameter_kwargs["type"] = _ArgTypes.json_object 197 parameter_kwargs["metavar"] = "JSON_OBJECT" 198 case "request": 199 parameter_kwargs["type"] = _ArgTypes.json_body 200 parameter_kwargs["metavar"] = "{JSON,FILE,-}" 201 case _: 202 parameter_kwargs["type"] = str 203 parameter_kwargs["metavar"] = "STR" 204 if argument_short_key is None: 205 yield (argument_key,), parameter_kwargs 206 else: 207 yield ( 208 (f"-{argument_short_key}", argument_key), parameter_kwargs 209 ) 210 211 212class ArvCLIArgumentParser(argparse.ArgumentParser): 213 """Argument parser for `arv` commands. 214 """ 215 def __init__(self, resource_dictionary, **kwargs): 216 """Arguments: 217 218 * resource dictionary: dict --- Dict containing the resources defined 219 in the discovery document; can be obtained as the 220 `_resourceDesc["resources"]` attribute of an Arvados API client 221 object. 222 """ 223 super().__init__(description="Arvados command line client", **kwargs) 224 # Common flags to the main command. 225 self.add_argument("-n", "--dry-run", action="store_true", 226 help="Don't actually do anything") 227 self.add_argument("-v", "--verbose", action="store_true", 228 help="Print some things on stderr") 229 # Default output format is JSON, while "-s" or "--short" can be 230 # used as a shorthand for "--format=uuid". If both are specified, the 231 # last one takes effect. 232 self.add_argument( 233 "-f", "--format", 234 choices=["json", "yaml", "uuid"], 235 default="json", 236 help="Set output format" 237 ) 238 self.add_argument( 239 "-s", "--short", 240 dest="format", 241 action="store_const", const="uuid", 242 help="Return only UUIDs (equivalent to --format=uuid)" 243 ) 244 245 subparsers = self.add_subparsers( 246 dest="subcommand", 247 help="Subcommands", 248 required=True, 249 parser_class=functools.partial( 250 argparse.ArgumentParser, 251 add_help=False 252 ) 253 ) 254 255 keep_parser = subparsers.add_parser("keep") 256 keep_parser.add_argument( 257 "method", 258 choices=["ls", "get", "put", "docker"] 259 ) 260 261 ws_parser = subparsers.add_parser("ws") 262 copy_parser = subparsers.add_parser("copy") 263 264 self.subparsers = subparsers 265 self.resource_dictionary = resource_dictionary 266 self._subparser_index = {} 267 268 self.add_resource_subcommands() 269 270 def add_resource_subcommands(self): 271 """Add resources as subcommands, their associated methods as 272 sub-subcommands, and the parameters associated with each method. 273 """ 274 for resource, resource_schema in self.resource_dictionary.items(): 275 subcommand = _ArgUtil.singularize_resource(resource) 276 resource_subparser = self.subparsers.add_parser( 277 subcommand, 278 # For backward compatibility with legacy Ruby CLI client. 279 aliases=["sy"] if subcommand == "sys" else [] 280 ) 281 self._subparser_index[subcommand] = resource_subparser 282 if subcommand == "sys": 283 self._subparser_index["sy"] = resource_subparser 284 methods_dict = resource_schema.get("methods") 285 if methods_dict: 286 # Create a collection of "sub-subparsers" under the resource 287 # subparser for the methods. 288 method_subparsers = resource_subparser.add_subparsers( 289 title="Methods", 290 dest="method", 291 parser_class=argparse.ArgumentParser, 292 help="Methods for subcommand {}".format(subcommand) 293 ) 294 for method, method_schema in methods_dict.items(): 295 # Add each specific method as a (sub-)subparser with its 296 # associated parameters. 297 method_parser = method_subparsers.add_parser( 298 method, 299 help=method_schema.get("description") 300 ) 301 for parameter_names, kwargs in _ArgUtil.get_method_options( 302 method_schema 303 ): 304 method_parser.add_argument(*parameter_names, **kwargs) 305 306 307def dispatch(arguments=None): 308 api_client = arvados.api("v1") 309 cmd_parser = ArvCLIArgumentParser(api_client._resourceDesc["resources"]) 310 args, remaining_args = cmd_parser.parse_known_args(arguments) 311 312 match args.subcommand: 313 case "keep": 314 match args.method: 315 case "ls": 316 from arvados.commands.ls import main 317 case "get": 318 from arvados.commands.get import main 319 case "put": 320 from arvados.commands.put import main 321 case "docker": 322 from arvados.commands.keepdocker import main 323 case "ws": 324 from arvados.commands.ws import main 325 case "copy": 326 from arvados.commands.arv_copy import main 327 case _: 328 # FIXME 329 print("Called API resource {!r}, method {!r}".format( 330 args.subcommand, args.method 331 )) 332 for k, v in vars(args).items(): 333 print("{!r}={!r}".format(k, v)) 334 help_wanted = "-h" in remaining_args or "--help" in remaining_args 335 if args.method is None or help_wanted: 336 subparser = cmd_parser._subparser_index.get(args.subcommand) 337 if subparser: 338 subparser.print_help() 339 sys.exit(0 if help_wanted else 2) 340 sys.exit(0) 341 status = main(remaining_args) 342 sys.exit(status) 343 344 345if __name__ == "__main__": 346 dispatch()
class
ArvCLIArgumentParser(argparse.ArgumentParser):
213class ArvCLIArgumentParser(argparse.ArgumentParser): 214 """Argument parser for `arv` commands. 215 """ 216 def __init__(self, resource_dictionary, **kwargs): 217 """Arguments: 218 219 * resource dictionary: dict --- Dict containing the resources defined 220 in the discovery document; can be obtained as the 221 `_resourceDesc["resources"]` attribute of an Arvados API client 222 object. 223 """ 224 super().__init__(description="Arvados command line client", **kwargs) 225 # Common flags to the main command. 226 self.add_argument("-n", "--dry-run", action="store_true", 227 help="Don't actually do anything") 228 self.add_argument("-v", "--verbose", action="store_true", 229 help="Print some things on stderr") 230 # Default output format is JSON, while "-s" or "--short" can be 231 # used as a shorthand for "--format=uuid". If both are specified, the 232 # last one takes effect. 233 self.add_argument( 234 "-f", "--format", 235 choices=["json", "yaml", "uuid"], 236 default="json", 237 help="Set output format" 238 ) 239 self.add_argument( 240 "-s", "--short", 241 dest="format", 242 action="store_const", const="uuid", 243 help="Return only UUIDs (equivalent to --format=uuid)" 244 ) 245 246 subparsers = self.add_subparsers( 247 dest="subcommand", 248 help="Subcommands", 249 required=True, 250 parser_class=functools.partial( 251 argparse.ArgumentParser, 252 add_help=False 253 ) 254 ) 255 256 keep_parser = subparsers.add_parser("keep") 257 keep_parser.add_argument( 258 "method", 259 choices=["ls", "get", "put", "docker"] 260 ) 261 262 ws_parser = subparsers.add_parser("ws") 263 copy_parser = subparsers.add_parser("copy") 264 265 self.subparsers = subparsers 266 self.resource_dictionary = resource_dictionary 267 self._subparser_index = {} 268 269 self.add_resource_subcommands() 270 271 def add_resource_subcommands(self): 272 """Add resources as subcommands, their associated methods as 273 sub-subcommands, and the parameters associated with each method. 274 """ 275 for resource, resource_schema in self.resource_dictionary.items(): 276 subcommand = _ArgUtil.singularize_resource(resource) 277 resource_subparser = self.subparsers.add_parser( 278 subcommand, 279 # For backward compatibility with legacy Ruby CLI client. 280 aliases=["sy"] if subcommand == "sys" else [] 281 ) 282 self._subparser_index[subcommand] = resource_subparser 283 if subcommand == "sys": 284 self._subparser_index["sy"] = resource_subparser 285 methods_dict = resource_schema.get("methods") 286 if methods_dict: 287 # Create a collection of "sub-subparsers" under the resource 288 # subparser for the methods. 289 method_subparsers = resource_subparser.add_subparsers( 290 title="Methods", 291 dest="method", 292 parser_class=argparse.ArgumentParser, 293 help="Methods for subcommand {}".format(subcommand) 294 ) 295 for method, method_schema in methods_dict.items(): 296 # Add each specific method as a (sub-)subparser with its 297 # associated parameters. 298 method_parser = method_subparsers.add_parser( 299 method, 300 help=method_schema.get("description") 301 ) 302 for parameter_names, kwargs in _ArgUtil.get_method_options( 303 method_schema 304 ): 305 method_parser.add_argument(*parameter_names, **kwargs)
Argument parser for arv commands.
ArvCLIArgumentParser(resource_dictionary, **kwargs)
216 def __init__(self, resource_dictionary, **kwargs): 217 """Arguments: 218 219 * resource dictionary: dict --- Dict containing the resources defined 220 in the discovery document; can be obtained as the 221 `_resourceDesc["resources"]` attribute of an Arvados API client 222 object. 223 """ 224 super().__init__(description="Arvados command line client", **kwargs) 225 # Common flags to the main command. 226 self.add_argument("-n", "--dry-run", action="store_true", 227 help="Don't actually do anything") 228 self.add_argument("-v", "--verbose", action="store_true", 229 help="Print some things on stderr") 230 # Default output format is JSON, while "-s" or "--short" can be 231 # used as a shorthand for "--format=uuid". If both are specified, the 232 # last one takes effect. 233 self.add_argument( 234 "-f", "--format", 235 choices=["json", "yaml", "uuid"], 236 default="json", 237 help="Set output format" 238 ) 239 self.add_argument( 240 "-s", "--short", 241 dest="format", 242 action="store_const", const="uuid", 243 help="Return only UUIDs (equivalent to --format=uuid)" 244 ) 245 246 subparsers = self.add_subparsers( 247 dest="subcommand", 248 help="Subcommands", 249 required=True, 250 parser_class=functools.partial( 251 argparse.ArgumentParser, 252 add_help=False 253 ) 254 ) 255 256 keep_parser = subparsers.add_parser("keep") 257 keep_parser.add_argument( 258 "method", 259 choices=["ls", "get", "put", "docker"] 260 ) 261 262 ws_parser = subparsers.add_parser("ws") 263 copy_parser = subparsers.add_parser("copy") 264 265 self.subparsers = subparsers 266 self.resource_dictionary = resource_dictionary 267 self._subparser_index = {} 268 269 self.add_resource_subcommands()
Arguments:
- resource dictionary: dict — Dict containing the resources defined
in the discovery document; can be obtained as the
_resourceDesc["resources"]attribute of an Arvados API client object.
def
add_resource_subcommands(self):
271 def add_resource_subcommands(self): 272 """Add resources as subcommands, their associated methods as 273 sub-subcommands, and the parameters associated with each method. 274 """ 275 for resource, resource_schema in self.resource_dictionary.items(): 276 subcommand = _ArgUtil.singularize_resource(resource) 277 resource_subparser = self.subparsers.add_parser( 278 subcommand, 279 # For backward compatibility with legacy Ruby CLI client. 280 aliases=["sy"] if subcommand == "sys" else [] 281 ) 282 self._subparser_index[subcommand] = resource_subparser 283 if subcommand == "sys": 284 self._subparser_index["sy"] = resource_subparser 285 methods_dict = resource_schema.get("methods") 286 if methods_dict: 287 # Create a collection of "sub-subparsers" under the resource 288 # subparser for the methods. 289 method_subparsers = resource_subparser.add_subparsers( 290 title="Methods", 291 dest="method", 292 parser_class=argparse.ArgumentParser, 293 help="Methods for subcommand {}".format(subcommand) 294 ) 295 for method, method_schema in methods_dict.items(): 296 # Add each specific method as a (sub-)subparser with its 297 # associated parameters. 298 method_parser = method_subparsers.add_parser( 299 method, 300 help=method_schema.get("description") 301 ) 302 for parameter_names, kwargs in _ArgUtil.get_method_options( 303 method_schema 304 ): 305 method_parser.add_argument(*parameter_names, **kwargs)
Add resources as subcommands, their associated methods as sub-subcommands, and the parameters associated with each method.
def
dispatch(arguments=None):
308def dispatch(arguments=None): 309 api_client = arvados.api("v1") 310 cmd_parser = ArvCLIArgumentParser(api_client._resourceDesc["resources"]) 311 args, remaining_args = cmd_parser.parse_known_args(arguments) 312 313 match args.subcommand: 314 case "keep": 315 match args.method: 316 case "ls": 317 from arvados.commands.ls import main 318 case "get": 319 from arvados.commands.get import main 320 case "put": 321 from arvados.commands.put import main 322 case "docker": 323 from arvados.commands.keepdocker import main 324 case "ws": 325 from arvados.commands.ws import main 326 case "copy": 327 from arvados.commands.arv_copy import main 328 case _: 329 # FIXME 330 print("Called API resource {!r}, method {!r}".format( 331 args.subcommand, args.method 332 )) 333 for k, v in vars(args).items(): 334 print("{!r}={!r}".format(k, v)) 335 help_wanted = "-h" in remaining_args or "--help" in remaining_args 336 if args.method is None or help_wanted: 337 subparser = cmd_parser._subparser_index.get(args.subcommand) 338 if subparser: 339 subparser.print_help() 340 sys.exit(0 if help_wanted else 2) 341 sys.exit(0) 342 status = main(remaining_args) 343 sys.exit(status)