1# -*- coding: utf-8 -*- # 2# Copyright 2017 Google LLC. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""Classes to specify concept and resource specs. 17 18Concept specs hold information about concepts. "Concepts" are any entity that 19has multiple attributes, which can be specified via multiple flags on the 20command line. A single concept spec should be created and re-used for the same 21concept everywhere it appears. 22 23Resource specs (currently the only type of concept spec used in gcloud) hold 24information about a Cloud resource. "Resources" are types of concepts that 25correspond to Cloud resources specified by a collection path, such as 26'example.projects.shelves.books'. Their attributes correspond to the parameters 27of their collection path. As with concept specs, a single resource spec 28should be defined and re-used for each collection. 29 30For resources, attributes can be configured by ResourceParameterAttributeConfigs 31using kwargs. In many cases, users should also be able to reuse configs for the 32same attribute across several resources (for example, 33'example.projects.shelves.books.pages' could also use the shelf and project 34attribute configs). 35""" 36 37from __future__ import absolute_import 38from __future__ import division 39from __future__ import unicode_literals 40 41import copy 42import re 43 44from googlecloudsdk.calliope.concepts import deps as deps_lib 45from googlecloudsdk.command_lib.util.apis import registry 46from googlecloudsdk.command_lib.util.apis import yaml_command_schema_util as util 47from googlecloudsdk.core import exceptions 48from googlecloudsdk.core import properties 49from googlecloudsdk.core import resources 50import six 51 52 53IGNORED_FIELDS = { 54 'project': 'project', 55 'projectId': 'project', 56 'projectsId': 'project', 57} 58 59 60class Error(exceptions.Error): 61 """Base class for errors in this module.""" 62 63 64class InitializationError(Error): 65 """Raised if a spec fails to initialize.""" 66 67 68class ResourceConfigurationError(Error): 69 """Raised if a resource is improperly declared.""" 70 71 72class InvalidResourceArgumentLists(Error): 73 """Exception for missing, extra, or out of order arguments.""" 74 75 def __init__(self, expected, actual): 76 expected = ['[' + e + ']' if e in IGNORED_FIELDS else e for e in expected] 77 super(InvalidResourceArgumentLists, self).__init__( 78 'Invalid resource arguments: Expected [{}], Found [{}].'.format( 79 ', '.join(expected), ', '.join(actual))) 80 81 82class ConceptSpec(object): 83 """Base class for concept args.""" 84 85 @property 86 def attributes(self): 87 """A list of Attribute objects representing the attributes of the concept. 88 89 Must be defined in subclasses. 90 """ 91 raise NotImplementedError 92 93 @property 94 def name(self): 95 """The name of the overall concept. 96 97 Must be defined in subclasses. 98 """ 99 raise NotImplementedError 100 101 def Initialize(self, deps): 102 """Initializes the concept using information provided by a Deps object. 103 104 Must be defined in subclasses. 105 106 Args: 107 deps: googlecloudsdk.calliope.concepts.deps.Deps object representing the 108 fallthroughs for the concept's attributes. 109 110 Returns: 111 the initialized concept. 112 113 Raises: 114 InitializationError, if the concept cannot be initialized. 115 """ 116 raise NotImplementedError 117 118 def Parse(self, attribute_to_args_map, base_fallthroughs_map, 119 parsed_args=None, plural=False, allow_empty=False): 120 """Lazy parsing function for resource. 121 122 Must be overridden in subclasses. 123 124 Args: 125 attribute_to_args_map: {str: str}, A map of attribute names to the names 126 of their associated flags. 127 base_fallthroughs_map: {str: [deps.Fallthrough]} A map of attribute 128 names to non-argument fallthroughs, including command-level 129 fallthroughs. 130 parsed_args: the parsed Namespace. 131 plural: bool, True if multiple resources can be parsed, False otherwise. 132 allow_empty: bool, True if resource parsing is allowed to return no 133 resource, otherwise False. 134 135 Returns: 136 the initialized resource or a list of initialized resources if the 137 resource argument was pluralized. 138 """ 139 raise NotImplementedError 140 141 def __eq__(self, other): 142 if not isinstance(other, type(self)): 143 return False 144 return self.name == other.name and self.attributes == other.attributes 145 146 def __hash__(self): 147 return hash(self.name) + hash(self.attributes) 148 149 150class _Attribute(object): 151 """A base class for concept attributes. 152 153 Attributes: 154 name: The name of the attribute. Used primarily to control the arg or flag 155 name corresponding to the attribute. Must be in all lower case. 156 help_text: String describing the attribute's relationship to the concept, 157 used to generate help for an attribute flag. 158 required: True if the attribute is required. 159 fallthroughs: [googlecloudsdk.calliope.concepts.deps_lib.Fallthrough], the 160 list of sources of data, in priority order, that can provide a value for 161 the attribute if not given on the command line. These should only be 162 sources inherent to the attribute, such as associated properties, not 163 command-specific sources. 164 completer: core.cache.completion_cache.Completer, the completer associated 165 with the attribute. 166 value_type: the type to be accepted by the attribute arg. Defaults to str. 167 """ 168 169 def __init__(self, name, help_text=None, required=False, fallthroughs=None, 170 completer=None, value_type=None): 171 """Initializes.""" 172 # Check for attributes that mix lower- and uppercase. Camel case is not 173 # handled consistently among libraries. 174 if re.search(r'[A-Z]', name) and re.search('r[a-z]', name): 175 raise ValueError( 176 'Invalid attribute name [{}]: Attribute names should be in lower ' 177 'snake case (foo_bar) so they can be transformed to flag names.' 178 .format(name)) 179 self.name = name 180 self.help_text = help_text 181 self.required = required 182 self.fallthroughs = fallthroughs or [] 183 self.completer = completer 184 self.value_type = value_type or six.text_type 185 186 def __eq__(self, other): 187 """Overrides.""" 188 if not isinstance(other, type(self)): 189 return False 190 return (self.name == other.name and self.help_text == other.help_text 191 and self.required == other.required 192 and self.completer == other.completer 193 and self.fallthroughs == other.fallthroughs 194 and self.value_type == other.value_type) 195 196 def __hash__(self): 197 return sum(map(hash, [ 198 self.name, self.help_text, self.required, self.completer, 199 self.value_type])) + sum(map(hash, self.fallthroughs)) 200 201 202class Attribute(_Attribute): 203 """An attribute of a resource. 204 205 Has all attributes of the base class along with resource-specific attributes. 206 207 Attributes: 208 completion_request_params: {str: str}, a dict of field names to params to 209 use as static field values in any request to complete this resource. 210 completion_id_field: str, the ID field of the return value in the 211 response for completion requests. 212 """ 213 214 def __init__(self, name, completion_request_params=None, 215 completion_id_field=None, **kwargs): 216 """Initializes.""" 217 self.completion_request_params = completion_request_params or {} 218 self.completion_id_field = completion_id_field 219 super(Attribute, self).__init__(name, **kwargs) 220 221 def __eq__(self, other): 222 """Overrides.""" 223 return (super(Attribute, self).__eq__(other) 224 and self.completer == other.completer 225 and self.completion_request_params 226 == other.completion_request_params 227 and self.completion_id_field == other.completion_id_field) 228 229 def __hash__(self): 230 return super(Attribute, self).__hash__() + sum( 231 map(hash, [six.text_type(self.completer), 232 six.text_type(self.completion_request_params), 233 self.completion_id_field])) 234 235 236class ResourceSpec(ConceptSpec): 237 """Defines a Cloud resource as a set of attributes for argument creation. 238 """ 239 # TODO(b/67707644): Enable completers by default when confident enough. 240 disable_auto_complete = True 241 242 # TODO(b/78851830): update the documentation to use this method. 243 @classmethod 244 def FromYaml(cls, yaml_data, api_version=None): 245 """Constructs an instance of ResourceSpec from yaml data. 246 247 Args: 248 yaml_data: dict, the parsed data from a resources.yaml file under 249 command_lib/. 250 api_version: string, overrides the default version in the resource 251 registry if provided. 252 253 Returns: 254 A ResourceSpec object. 255 """ 256 if not yaml_data: 257 return None 258 collection = registry.GetAPICollection( 259 yaml_data['collection'], api_version=api_version) 260 attributes = ParseAttributesFromData( 261 yaml_data.get('attributes'), collection.detailed_params) 262 return cls( 263 resource_collection=collection.full_name, 264 resource_name=yaml_data['name'], 265 api_version=collection.api_version, 266 disable_auto_completers=yaml_data.get( 267 'disable_auto_completers', ResourceSpec.disable_auto_complete), 268 plural_name=yaml_data.get('plural_name'), 269 **{attribute.parameter_name: attribute for attribute in attributes}) 270 271 def __init__(self, resource_collection, resource_name='resource', 272 api_version=None, disable_auto_completers=disable_auto_complete, 273 plural_name=None, **kwargs): 274 """Initializes a ResourceSpec. 275 276 To use a ResourceSpec, give a collection path such as 277 'cloudiot.projects.locations.registries', and optionally an 278 API version. 279 280 For each parameter in the collection path, an attribute is added to the 281 resource spec. Names can be created by default or overridden in the 282 attribute_configs dict, which maps from the parameter name to a 283 ResourceParameterAttributeConfig object. ResourceParameterAttributeConfigs 284 also contain information about the help text that describes the attribute. 285 286 Attribute naming: By default, attributes are named after their collection 287 path param names, or "name" if they are the "anchor" attribute (the final 288 parameter in the path). 289 290 Args: 291 resource_collection: The collection path of the resource. 292 resource_name: The name of the resource, which will be used in attribute 293 help text. Defaults to 'resource'. 294 api_version: Overrides the default version in the resource 295 registry. 296 disable_auto_completers: bool, whether to add completers automatically 297 where possible. 298 plural_name: str, the pluralized name. Will be pluralized by default rules 299 if not given in cases where the resource is referred to in the plural. 300 **kwargs: Parameter names (such as 'projectsId') from the 301 collection path, mapped to ResourceParameterAttributeConfigs. 302 303 Raises: 304 ResourceConfigurationError: if the resource is given unknown params or the 305 collection has no params. 306 """ 307 self._name = resource_name 308 self.plural_name = plural_name 309 self.collection = resource_collection 310 self._resources = resources.REGISTRY.Clone() 311 self._collection_info = self._resources.GetCollectionInfo( 312 resource_collection, api_version=api_version) 313 self.disable_auto_completers = disable_auto_completers 314 collection_params = self._collection_info.GetParams('') 315 self._attributes = [] 316 self._param_names_map = {} 317 318 orig_kwargs = list(six.iterkeys(kwargs)) 319 # Add attributes. 320 anchor = False 321 for i, param_name in enumerate(collection_params): 322 if i == len(collection_params) - 1: 323 anchor = True 324 attribute_config = kwargs.pop(param_name, 325 ResourceParameterAttributeConfig()) 326 attribute_name = self._AttributeName(param_name, attribute_config, 327 anchor=anchor) 328 329 new_attribute = Attribute( 330 name=attribute_name, 331 help_text=attribute_config.help_text, 332 required=True, 333 fallthroughs=attribute_config.fallthroughs, 334 completer=attribute_config.completer, 335 value_type=attribute_config.value_type, 336 completion_request_params=attribute_config.completion_request_params, 337 completion_id_field=attribute_config.completion_id_field) 338 self._attributes.append(new_attribute) 339 # Keep a map from attribute names to param names. While attribute names 340 # are used for error messaging and arg creation/parsing, resource parsing 341 # during command runtime requires parameter names. 342 self._param_names_map[new_attribute.name] = param_name 343 if not self._attributes: 344 raise ResourceConfigurationError('Resource [{}] has no parameters; no ' 345 'arguments will be generated'.format( 346 self._name)) 347 if kwargs: 348 raise ResourceConfigurationError('Resource [{}] was given an attribute ' 349 'config for unknown attribute(s): ' 350 'Expected [{}], Found [{}]' 351 .format(self._name, 352 ', '.join(collection_params), 353 ', '.join(orig_kwargs))) 354 355 @property 356 def attributes(self): 357 return self._attributes 358 359 @property 360 def name(self): 361 return self._name 362 363 @property 364 def anchor(self): 365 """The "anchor" attribute of the resource.""" 366 # self.attributes cannot be empty; will cause an error on init. 367 return self.attributes[-1] 368 369 def IsAnchor(self, attribute): 370 """Convenience method.""" 371 return attribute == self.anchor 372 373 @property 374 def attribute_to_params_map(self): 375 """A map from all attribute names to param names.""" 376 return self._param_names_map 377 378 @property 379 def collection_info(self): 380 return self._collection_info 381 382 def _AttributeName(self, param_name, attribute_config, anchor=False): 383 """Chooses attribute name for a param name. 384 385 If attribute_config gives an attribute name, that is used. Otherwise, if the 386 param is an anchor attribute, 'name' is used, or if not, param_name is used. 387 388 Args: 389 param_name: str, the parameter name from the collection. 390 attribute_config: ResourceParameterAttributeConfig, the config for the 391 param_name. 392 anchor: bool, whether the parameter is the "anchor" or the last in the 393 collection path. 394 395 Returns: 396 (str) the attribute name. 397 """ 398 if attribute_config.attribute_name: 399 return attribute_config.attribute_name 400 if anchor: 401 return 'name' 402 return param_name.replace('Id', '_id').lower() 403 404 def ParamName(self, attribute_name): 405 """Given an attribute name, gets the param name for resource parsing.""" 406 if attribute_name not in self.attribute_to_params_map: 407 raise ValueError( 408 'No param name found for attribute [{}]. Existing attributes are ' 409 '[{}]'.format(attribute_name, 410 ', '.join(sorted(self.attribute_to_params_map.keys())))) 411 return self.attribute_to_params_map[attribute_name] 412 413 def AttributeName(self, param_name): 414 """Given a param name, gets the attribute name.""" 415 for attribute_name, p in six.iteritems(self.attribute_to_params_map): 416 if p == param_name: 417 return attribute_name 418 419 def Initialize(self, fallthroughs_map, parsed_args=None): 420 """Initializes a resource given its fallthroughs. 421 422 If the attributes have a property or arg fallthrough but the full 423 resource name is provided to the anchor attribute flag, the information 424 from the resource name is used over the properties and args. This 425 preserves typical resource parsing behavior in existing surfaces. 426 427 Args: 428 fallthroughs_map: {str: [deps_lib._FallthroughBase]}, a dict of finalized 429 fallthroughs for the resource. 430 parsed_args: the argparse namespace. 431 432 Returns: 433 (googlecloudsdk.core.resources.Resource) the fully initialized resource. 434 435 Raises: 436 googlecloudsdk.calliope.concepts.concepts.InitializationError, if the 437 concept can't be initialized. 438 """ 439 params = {} 440 441 # Returns a function that can be used to parse each attribute, which will be 442 # used only if the resource parser does not receive a fully qualified 443 # resource name. 444 def LazyGet(name): 445 f = lambda: deps_lib.Get(name, fallthroughs_map, parsed_args=parsed_args) 446 return f 447 448 for attribute in self.attributes: 449 params[self.ParamName(attribute.name)] = LazyGet(attribute.name) 450 self._resources.RegisterApiByName(self._collection_info.api_name, 451 self._collection_info.api_version) 452 try: 453 return self._resources.Parse( 454 deps_lib.Get( 455 self.anchor.name, fallthroughs_map, parsed_args=parsed_args), 456 collection=self.collection, 457 params=params) 458 except deps_lib.AttributeNotFoundError as e: 459 raise InitializationError( 460 'The [{}] resource is not properly specified.\n' 461 '{}'.format(self.name, six.text_type(e))) 462 except resources.UserError as e: 463 raise InitializationError(six.text_type(e)) 464 465 def Parse(self, attribute_to_args_map, base_fallthroughs_map, 466 parsed_args=None, plural=False, allow_empty=False): 467 """Lazy parsing function for resource. 468 469 Args: 470 attribute_to_args_map: {str: str}, A map of attribute names to the names 471 of their associated flags. 472 base_fallthroughs_map: {str: [deps_lib.Fallthrough]} A map of attribute 473 names to non-argument fallthroughs, including command-level 474 fallthroughs. 475 parsed_args: the parsed Namespace. 476 plural: bool, True if multiple resources can be parsed, False otherwise. 477 allow_empty: bool, True if resource parsing is allowed to return no 478 resource, otherwise False. 479 480 Returns: 481 the initialized resource or a list of initialized resources if the 482 resource argument was pluralized. 483 """ 484 if not plural: 485 fallthroughs_map = self.BuildFullFallthroughsMap( 486 attribute_to_args_map, base_fallthroughs_map, 487 with_anchor_fallthroughs=False) 488 try: 489 return self.Initialize( 490 fallthroughs_map, parsed_args=parsed_args) 491 except InitializationError: 492 if allow_empty: 493 return None 494 raise 495 496 results = self._ParseFromPluralValue(attribute_to_args_map, 497 base_fallthroughs_map, 498 self.anchor, 499 parsed_args) 500 if results: 501 return results 502 503 if allow_empty: 504 return [] 505 fallthroughs_map = self.BuildFullFallthroughsMap( 506 attribute_to_args_map, base_fallthroughs_map) 507 return self.Initialize( 508 base_fallthroughs_map, parsed_args=parsed_args) 509 510 def _ParseFromPluralValue(self, attribute_to_args_map, base_fallthroughs_map, 511 plural_attribute, parsed_args): 512 """Helper for parsing a list of results from a plural fallthrough.""" 513 attribute_name = plural_attribute.name 514 fallthroughs_map = self.BuildFullFallthroughsMap( 515 attribute_to_args_map, base_fallthroughs_map, plural=True, 516 with_anchor_fallthroughs=False) 517 current_fallthroughs = fallthroughs_map.get(attribute_name, []) 518 # Iterate through the values provided to the argument, creating for 519 # each a separate parsed resource. 520 parsed_resources = [] 521 for fallthrough in current_fallthroughs: 522 try: 523 values = fallthrough.GetValue(parsed_args) 524 except deps_lib.FallthroughNotFoundError: 525 continue 526 for value in values: 527 def F(return_value=value): 528 return return_value 529 530 new_fallthrough = deps_lib.Fallthrough( 531 F, fallthrough.hint, active=fallthrough.active) 532 fallthroughs_map[attribute_name] = [new_fallthrough] 533 # Add the anchor fallthroughs for this particular value, so that the 534 # error messages will contain the appropriate hints. 535 self._AddAnchorFallthroughs(plural_attribute, fallthroughs_map) 536 parsed_resources.append( 537 self.Initialize( 538 fallthroughs_map, parsed_args=parsed_args)) 539 return parsed_resources 540 541 def BuildFullFallthroughsMap(self, attribute_to_args_map, 542 base_fallthroughs_map, plural=False, 543 with_anchor_fallthroughs=True): 544 """Builds map of all fallthroughs including arg names. 545 546 Fallthroughs are a list of objects that, when called, try different ways of 547 getting values for attributes (see googlecloudsdk.calliope.concepts. 548 deps_lib._Fallthrough). This method builds a map from the name of each 549 attribute to its fallthroughs, including the "primary" fallthrough 550 representing its corresponding argument value in parsed_args if any, and any 551 fallthroughs that were configured for the attribute beyond that. 552 553 Args: 554 attribute_to_args_map: {str: str}, A map of attribute names to the names 555 of their associated flags. 556 base_fallthroughs_map: {str: [deps_lib._FallthroughBase]}, A map of 557 attribute names to non-argument fallthroughs, including command-level 558 fallthroughs. 559 plural: bool, True if multiple resources can be parsed, False otherwise. 560 with_anchor_fallthroughs: bool, whether to add fully specified anchor 561 fallthroughs. Used only for getting help text/error messages, 562 and for determining which attributes are specified -- not for parsing. 563 564 Returns: 565 {str: [deps_lib._Fallthrough]}, a map from attribute name to its 566 fallthroughs. 567 """ 568 fallthroughs_map = {} 569 for attribute in self.attributes: 570 fallthroughs_map[attribute.name] = ( 571 self.GetArgAndBaseFallthroughsForAttribute(attribute_to_args_map, 572 base_fallthroughs_map, 573 attribute, 574 plural=plural)) 575 if not with_anchor_fallthroughs: 576 return fallthroughs_map 577 for attribute in self.attributes: 578 if self.IsAnchor(attribute): 579 self._AddAnchorFallthroughs(attribute, fallthroughs_map) 580 return fallthroughs_map 581 582 def GetArgAndBaseFallthroughsForAttribute(self, 583 attribute_to_args_map, 584 base_fallthroughs_map, 585 attribute, 586 plural=False): 587 """Gets all fallthroughs for an attribute except anchor-dependent ones.""" 588 attribute_name = attribute.name 589 attribute_fallthroughs = [] 590 # The only args that should be lists are anchor args for plural 591 # resources. 592 attribute_is_plural = self.IsAnchor(attribute) and plural 593 594 # Start the fallthroughs list with the primary associated arg for the 595 # attribute. 596 arg_name = attribute_to_args_map.get(attribute_name) 597 if arg_name: 598 attribute_fallthroughs.append( 599 deps_lib.ArgFallthrough(arg_name, plural=attribute_is_plural)) 600 601 given_fallthroughs = base_fallthroughs_map.get(attribute_name, []) 602 for fallthrough in given_fallthroughs: 603 if attribute_is_plural: 604 fallthrough = copy.deepcopy(fallthrough) 605 fallthrough.plural = attribute_is_plural 606 attribute_fallthroughs.append(fallthrough) 607 return attribute_fallthroughs 608 609 def _GetAttributeAnchorFallthroughs(self, anchor_fallthroughs, attribute): 610 """Helper to get anchor-depednent fallthroughs for a specific attribute.""" 611 parameter_name = self.ParamName(attribute.name) 612 anchor_based_fallthroughs = [ 613 deps_lib.FullySpecifiedAnchorFallthrough( 614 anchor_fallthrough, self.collection_info, parameter_name) 615 for anchor_fallthrough in anchor_fallthroughs 616 ] 617 return anchor_based_fallthroughs 618 619 def _AddAnchorFallthroughs(self, anchor, fallthroughs_map): 620 """Helper for adding anchor fallthroughs to the fallthroughs map.""" 621 anchor_fallthroughs = fallthroughs_map.get(anchor.name, []) 622 for attribute in self.attributes: 623 if attribute == anchor: 624 continue 625 anchor_based_fallthroughs = self._GetAttributeAnchorFallthroughs( 626 anchor_fallthroughs, attribute) 627 fallthroughs_map[attribute.name] = ( 628 anchor_based_fallthroughs + fallthroughs_map[attribute.name]) 629 630 def __eq__(self, other): 631 return (super(ResourceSpec, self).__eq__(other) 632 and self.disable_auto_completers == other.disable_auto_completers 633 and self.attribute_to_params_map == other.attribute_to_params_map) 634 635 def __hash__(self): 636 return super(ResourceSpec, self).__hash__() + sum( 637 map(hash, [self.disable_auto_completers, self.attribute_to_params_map])) 638 639 640class ResourceParameterAttributeConfig(object): 641 """Configuration used to create attributes from resource parameters.""" 642 643 @classmethod 644 def FromData(cls, data): 645 """Constructs an attribute config from data defined in the yaml file. 646 647 Args: 648 data: {}, the dict of data from the YAML file for this single attribute. 649 650 Returns: 651 ResourceParameterAttributeConfig 652 """ 653 if not data: 654 return None 655 656 attribute_name = data['attribute_name'] 657 parameter_name = data['parameter_name'] 658 help_text = data['help'] 659 completer = util.Hook.FromData(data, 'completer') 660 completion_id_field = data.get('completion_id_field', None) 661 completion_request_params_list = data.get('completion_request_params', []) 662 completion_request_params = { 663 param.get('fieldName'): param.get('value') 664 for param in completion_request_params_list 665 } 666 667 # Add property fallthroughs. 668 fallthroughs = [] 669 prop = properties.FromString(data.get('property', '')) 670 if prop: 671 fallthroughs.append(deps_lib.PropertyFallthrough(prop)) 672 default_config = DEFAULT_RESOURCE_ATTRIBUTE_CONFIGS.get(attribute_name) 673 if default_config: 674 fallthroughs += [ 675 f for f in default_config.fallthroughs if f not in fallthroughs] 676 # Add fallthroughs from python hooks. 677 fallthrough_data = data.get('fallthroughs', []) 678 fallthroughs_from_hook = [ 679 deps_lib.Fallthrough(util.Hook.FromPath(f['hook']), hint=f['hint']) 680 for f in fallthrough_data 681 ] 682 fallthroughs += fallthroughs_from_hook 683 return cls( 684 name=attribute_name, 685 help_text=help_text, 686 fallthroughs=fallthroughs, 687 completer=completer, 688 completion_id_field=completion_id_field, 689 completion_request_params=completion_request_params, 690 parameter_name=parameter_name) 691 692 def __init__(self, 693 name=None, 694 help_text=None, 695 fallthroughs=None, 696 completer=None, 697 completion_request_params=None, 698 completion_id_field=None, 699 value_type=None, 700 parameter_name=None): 701 """Create a resource attribute. 702 703 Args: 704 name: str, the name of the attribute. This controls the naming of flags 705 based on the attribute. 706 help_text: str, generic help text for any flag based on the attribute. One 707 special expansion is available to convert "{resource}" to the name of 708 the resource. 709 fallthroughs: [deps_lib.Fallthrough], A list of fallthroughs to use to 710 resolve the attribute if it is not provided on the command line. 711 completer: core.cache.completion_cache.Completer, the completer 712 associated with the attribute. 713 completion_request_params: {str: value}, a dict of field names to static 714 values to fill in for the completion request. 715 completion_id_field: str, the ID field of the return value in the 716 response for completion commands. 717 value_type: the type to be accepted by the attribute arg. Defaults to str. 718 parameter_name: the API parameter name that this attribute maps to. 719 """ 720 self.attribute_name = name 721 self.help_text = help_text 722 self.fallthroughs = fallthroughs or [] 723 if completer and (completion_request_params or completion_id_field): 724 raise ValueError('Custom completer and auto-completer should not be ' 725 'specified at the same time') 726 self.completer = completer 727 self.completion_request_params = completion_request_params 728 self.completion_id_field = completion_id_field 729 self.value_type = value_type or six.text_type 730 self.parameter_name = parameter_name 731 732 733def ParseAttributesFromData(attributes_data, expected_param_names): 734 """Parses a list of ResourceParameterAttributeConfig from yaml data. 735 736 Args: 737 attributes_data: dict, the attributes data defined in 738 command_lib/resources.yaml file. 739 expected_param_names: [str], the names of the API parameters that the API 740 method accepts. Example, ['projectsId', 'instancesId']. 741 742 Returns: 743 [ResourceParameterAttributeConfig]. 744 745 Raises: 746 InvalidResourceArgumentLists: if the attributes defined in the yaml file 747 don't match the expected fields in the API method. 748 """ 749 raw_attributes = [ 750 ResourceParameterAttributeConfig.FromData(a) for a in attributes_data 751 ] 752 registered_param_names = [a.parameter_name for a in raw_attributes] 753 final_attributes = [] 754 755 # TODO(b/78851830): improve the time complexity here. 756 for expected_name in expected_param_names: 757 if raw_attributes and expected_name == raw_attributes[0].parameter_name: 758 # Attribute matches expected, add it and continue checking. 759 final_attributes.append(raw_attributes.pop(0)) 760 elif expected_name in IGNORED_FIELDS: 761 # Attribute doesn't match but is being ignored. Add an auto-generated 762 # attribute as a substitute. 763 # Currently, it would only be the project config. 764 attribute_name = IGNORED_FIELDS[expected_name] 765 ignored_attribute = DEFAULT_RESOURCE_ATTRIBUTE_CONFIGS.get(attribute_name) 766 # Manually add the parameter name, e.g. project, projectId or projectsId. 767 ignored_attribute.parameter_name = expected_name 768 final_attributes.append(ignored_attribute) 769 else: 770 # It doesn't match (or there are no more registered params) and the 771 # field is not being ignored, error. 772 raise InvalidResourceArgumentLists(expected_param_names, 773 registered_param_names) 774 775 if raw_attributes: 776 # All expected fields were processed but there are still registered 777 # attribute params remaining, they must be extra. 778 raise InvalidResourceArgumentLists(expected_param_names, 779 registered_param_names) 780 781 return final_attributes 782 783 784DEFAULT_PROJECT_ATTRIBUTE_CONFIG = ResourceParameterAttributeConfig( 785 name='project', 786 help_text='Project ID of the Google Cloud Platform project for ' 787 'the {resource}.', 788 fallthroughs=[ 789 # Typically argument fallthroughs should be configured at the command 790 # level, but the --project flag is currently available in every command. 791 deps_lib.ArgFallthrough('--project'), 792 deps_lib.PropertyFallthrough(properties.VALUES.core.project) 793 ]) 794DEFAULT_RESOURCE_ATTRIBUTE_CONFIGS = { 795 'project': DEFAULT_PROJECT_ATTRIBUTE_CONFIG} 796_DEFAULT_CONFIGS = {'project': DEFAULT_PROJECT_ATTRIBUTE_CONFIG} 797