779 lines
31 KiB
Python
779 lines
31 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2017 Google LLC. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Classes to specify concept and resource specs.
|
|
|
|
Concept specs hold information about concepts. "Concepts" are any entity that
|
|
has multiple attributes, which can be specified via multiple flags on the
|
|
command line. A single concept spec should be created and re-used for the same
|
|
concept everywhere it appears.
|
|
|
|
Resource specs (currently the only type of concept spec used in gcloud) hold
|
|
information about a Cloud resource. "Resources" are types of concepts that
|
|
correspond to Cloud resources specified by a collection path, such as
|
|
'example.projects.shelves.books'. Their attributes correspond to the parameters
|
|
of their collection path. As with concept specs, a single resource spec
|
|
should be defined and re-used for each collection.
|
|
|
|
For resources, attributes can be configured by ResourceParameterAttributeConfigs
|
|
using kwargs. In many cases, users should also be able to reuse configs for the
|
|
same attribute across several resources (for example,
|
|
'example.projects.shelves.books.pages' could also use the shelf and project
|
|
attribute configs).
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import abc
|
|
import re
|
|
|
|
from googlecloudsdk.calliope.concepts import deps as deps_lib
|
|
from googlecloudsdk.calliope.concepts import deps_map_util
|
|
from googlecloudsdk.calliope.concepts import util as format_util
|
|
from googlecloudsdk.command_lib.util.apis import yaml_command_schema_util as util
|
|
from googlecloudsdk.core import exceptions
|
|
from googlecloudsdk.core import properties
|
|
from googlecloudsdk.core import resources
|
|
|
|
|
|
IGNORED_FIELDS = {
|
|
'project': 'project',
|
|
'projectId': 'project',
|
|
'projectsId': 'project',
|
|
}
|
|
|
|
|
|
class Error(exceptions.Error):
|
|
"""Base class for errors in this module."""
|
|
|
|
|
|
class InitializationError(Error):
|
|
"""Raised if a spec fails to initialize."""
|
|
|
|
|
|
class ResourceConfigurationError(Error):
|
|
"""Raised if a resource is improperly declared."""
|
|
|
|
|
|
class InvalidResourceArgumentLists(Error):
|
|
"""Exception for missing, extra, or out of order arguments."""
|
|
|
|
def __init__(self, expected, actual):
|
|
expected = ['[' + e + ']' if e in IGNORED_FIELDS else e for e in expected]
|
|
super(InvalidResourceArgumentLists, self).__init__(
|
|
'Invalid resource arguments: Expected [{}], Found [{}].'.format(
|
|
', '.join(expected), ', '.join(actual)))
|
|
|
|
|
|
class ConceptSpec(object, metaclass=abc.ABCMeta):
|
|
"""Base class for concept args."""
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def attributes(self):
|
|
"""A list of Attribute objects representing the attributes of the concept.
|
|
"""
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def name(self):
|
|
"""The name of the overall concept."""
|
|
|
|
@abc.abstractmethod
|
|
def IsAnchor(self, attribute):
|
|
"""Returns True if attribute is an anchor."""
|
|
|
|
@abc.abstractmethod
|
|
def IsLeafAnchor(self, attribute):
|
|
"""Returns True if attribute is a leaf anchor."""
|
|
|
|
@abc.abstractmethod
|
|
def Initialize(self, fallthroughs_map, parsed_args=None):
|
|
"""Initializes the concept using fallthroughs and parsed args."""
|
|
|
|
@abc.abstractmethod
|
|
def Parse(self, attribute_to_args_map, base_fallthroughs_map,
|
|
parsed_args=None, plural=False, allow_empty=False):
|
|
"""Lazy parsing function for resource."""
|
|
|
|
@abc.abstractmethod
|
|
def BuildFullFallthroughsMap(
|
|
self, attribute_to_args_map, base_fallthroughs_map):
|
|
"""Builds list of fallthroughs for each attribute."""
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, type(self)):
|
|
return False
|
|
else:
|
|
return self.name == other.name and self.attributes == other.attributes
|
|
|
|
def __hash__(self):
|
|
return hash(self.name) + hash(self.attributes)
|
|
|
|
|
|
class _Attribute(object):
|
|
"""A base class for concept attributes.
|
|
|
|
Attributes:
|
|
name: The name of the attribute. Used primarily to control the arg or flag
|
|
name corresponding to the attribute. Must be in all lower case.
|
|
param_name: corresponds to where the attribute is mapped in the resource
|
|
help_text: String describing the attribute's relationship to the concept,
|
|
used to generate help for an attribute flag.
|
|
required: True if the attribute is required.
|
|
fallthroughs: [googlecloudsdk.calliope.concepts.deps_lib.Fallthrough], the
|
|
list of sources of data, in priority order, that can provide a value for
|
|
the attribute if not given on the command line. These should only be
|
|
sources inherent to the attribute, such as associated properties, not
|
|
command-specific sources.
|
|
completer: core.cache.completion_cache.Completer, the completer associated
|
|
with the attribute.
|
|
value_type: the type to be accepted by the attribute arg. Defaults to str.
|
|
"""
|
|
|
|
def __init__(self, name, param_name, help_text=None, required=False,
|
|
fallthroughs=None, completer=None, value_type=None):
|
|
"""Initializes."""
|
|
# Check for attributes that mix lower- and uppercase. Camel case is not
|
|
# handled consistently among libraries.
|
|
if re.search(r'[A-Z]', name) and re.search('r[a-z]', name):
|
|
raise ValueError(
|
|
'Invalid attribute name [{}]: Attribute names should be in lower '
|
|
'snake case (foo_bar) so they can be transformed to flag names.'
|
|
.format(name))
|
|
self.name = name
|
|
self.param_name = param_name or name
|
|
self.help_text = help_text
|
|
self.required = required
|
|
self.fallthroughs = fallthroughs or []
|
|
self.completer = completer
|
|
self.value_type = value_type or str
|
|
|
|
def __eq__(self, other):
|
|
"""Overrides."""
|
|
if not isinstance(other, type(self)):
|
|
return False
|
|
return (self.name == other.name and self.param_name == other.param_name
|
|
and self.help_text == other.help_text
|
|
and self.required == other.required
|
|
and self.completer == other.completer
|
|
and self.fallthroughs == other.fallthroughs
|
|
and self.value_type == other.value_type)
|
|
|
|
def __hash__(self):
|
|
return sum(map(hash, [
|
|
self.name, self.param_name, self.help_text, self.required,
|
|
self.completer, self.value_type])) + sum(map(hash, self.fallthroughs))
|
|
|
|
|
|
class Attribute(_Attribute):
|
|
"""An attribute of a resource.
|
|
|
|
Has all attributes of the base class along with resource-specific attributes.
|
|
|
|
Attributes:
|
|
completion_request_params: {str: str}, a dict of field names to params to
|
|
use as static field values in any request to complete this resource.
|
|
completion_id_field: str, the ID field of the return value in the
|
|
response for completion requests.
|
|
"""
|
|
|
|
def __init__(self, name, completion_request_params=None,
|
|
completion_id_field=None, **kwargs):
|
|
"""Initializes."""
|
|
self.completion_request_params = completion_request_params or {}
|
|
self.completion_id_field = completion_id_field
|
|
super(Attribute, self).__init__(name, **kwargs)
|
|
|
|
def __eq__(self, other):
|
|
"""Overrides."""
|
|
return (super(Attribute, self).__eq__(other)
|
|
and self.completer == other.completer
|
|
and self.completion_request_params
|
|
== other.completion_request_params
|
|
and self.completion_id_field == other.completion_id_field)
|
|
|
|
def __hash__(self):
|
|
return super(Attribute, self).__hash__() + sum(
|
|
map(hash, [str(self.completer),
|
|
str(self.completion_request_params),
|
|
self.completion_id_field]))
|
|
|
|
|
|
class ResourceSpec(ConceptSpec):
|
|
"""Defines a Cloud resource as a set of attributes for argument creation.
|
|
"""
|
|
disable_auto_complete = True
|
|
|
|
@classmethod
|
|
def FromYaml(cls, yaml_data, is_positional=None, api_version=None):
|
|
"""Constructs an instance of ResourceSpec from yaml data.
|
|
|
|
Args:
|
|
yaml_data: dict, the parsed data from a resources.yaml file under
|
|
command_lib/.
|
|
is_positional: bool, optional value that determines if anchor argument is
|
|
a positional and reformats anchor attribute name accordingly.
|
|
api_version: string, overrides the default version in the resource
|
|
registry if provided.
|
|
|
|
Returns:
|
|
A ResourceSpec object.
|
|
"""
|
|
# pylint: disable=g-import-not-at-top
|
|
from googlecloudsdk.command_lib.util.apis import registry
|
|
# pylint: enable=g-import-not-at-top
|
|
collection = registry.GetAPICollection(
|
|
yaml_data['collection'], api_version=api_version)
|
|
attributes = ParseAttributesFromData(
|
|
yaml_data.get('attributes'), collection.detailed_params)
|
|
return cls(
|
|
resource_collection=collection.full_name,
|
|
resource_name=yaml_data['name'],
|
|
api_version=collection.api_version,
|
|
disable_auto_completers=yaml_data.get(
|
|
'disable_auto_completers', ResourceSpec.disable_auto_complete),
|
|
plural_name=yaml_data.get('plural_name'),
|
|
is_positional=is_positional,
|
|
**{attribute.parameter_name: attribute for attribute in attributes})
|
|
|
|
def __init__(self, resource_collection, resource_name='resource',
|
|
api_version=None, disable_auto_completers=disable_auto_complete,
|
|
plural_name=None, is_positional=None, **kwargs):
|
|
"""Initializes a ResourceSpec.
|
|
|
|
To use a ResourceSpec, give a collection path such as
|
|
'cloudiot.projects.locations.registries', and optionally an
|
|
API version.
|
|
|
|
For each parameter in the collection path, an attribute is added to the
|
|
resource spec. Names can be created by default or overridden in the
|
|
attribute_configs dict, which maps from the parameter name to a
|
|
ResourceParameterAttributeConfig object. ResourceParameterAttributeConfigs
|
|
also contain information about the help text that describes the attribute.
|
|
|
|
Attribute naming: By default, attributes are named after their collection
|
|
path param names, or "name" if they are the "anchor" attribute (the final
|
|
parameter in the path).
|
|
|
|
Args:
|
|
resource_collection: The collection path of the resource.
|
|
resource_name: The name of the resource, which will be used in attribute
|
|
help text. Defaults to 'resource'.
|
|
api_version: Overrides the default version in the resource
|
|
registry.
|
|
disable_auto_completers: bool, whether to add completers automatically
|
|
where possible.
|
|
plural_name: str, the pluralized name. Will be pluralized by default rules
|
|
if not given in cases where the resource is referred to in the plural.
|
|
is_positional: bool, optional value that determines if anchor argument is
|
|
a positional and reformats anchor attribute name accordingly.
|
|
**kwargs: Parameter names (such as 'projectsId') from the
|
|
collection path, mapped to ResourceParameterAttributeConfigs.
|
|
|
|
Raises:
|
|
ResourceConfigurationError: if the resource is given unknown params or the
|
|
collection has no params.
|
|
"""
|
|
self._name = resource_name
|
|
self.plural_name = plural_name
|
|
self.collection = resource_collection
|
|
self._resources = resources.REGISTRY.Clone()
|
|
self._collection_info = self._resources.GetCollectionInfo(
|
|
resource_collection, api_version=api_version)
|
|
self.disable_auto_completers = disable_auto_completers
|
|
collection_params = self._collection_info.GetParams('')
|
|
self._attributes = []
|
|
self._param_names_map = {}
|
|
|
|
orig_kwargs = list(kwargs.keys())
|
|
# Add attributes.
|
|
anchor = False
|
|
for i, param_name in enumerate(collection_params):
|
|
if i == len(collection_params) - 1:
|
|
anchor = True
|
|
attribute_config = kwargs.pop(param_name,
|
|
ResourceParameterAttributeConfig())
|
|
attribute_name = self._AttributeName(param_name, attribute_config,
|
|
anchor=anchor,
|
|
is_positional=is_positional)
|
|
|
|
new_attribute = Attribute(
|
|
name=attribute_name,
|
|
param_name=param_name,
|
|
help_text=attribute_config.help_text,
|
|
required=True,
|
|
fallthroughs=attribute_config.fallthroughs,
|
|
completer=attribute_config.completer,
|
|
value_type=attribute_config.value_type,
|
|
completion_request_params=attribute_config.completion_request_params,
|
|
completion_id_field=attribute_config.completion_id_field)
|
|
self._attributes.append(new_attribute)
|
|
# Keep a map from attribute names to param names. While attribute names
|
|
# are used for error messaging and arg creation/parsing, resource parsing
|
|
# during command runtime requires parameter names.
|
|
self._param_names_map[new_attribute.name] = param_name
|
|
if not self._attributes:
|
|
raise ResourceConfigurationError('Resource [{}] has no parameters; no '
|
|
'arguments will be generated'.format(
|
|
self._name))
|
|
if kwargs:
|
|
raise ResourceConfigurationError('Resource [{}] was given an attribute '
|
|
'config for unknown attribute(s): '
|
|
'Expected [{}], Found [{}]'
|
|
.format(self._name,
|
|
', '.join(collection_params),
|
|
', '.join(orig_kwargs)))
|
|
|
|
@property
|
|
def attributes(self):
|
|
return self._attributes
|
|
|
|
@property
|
|
def name(self):
|
|
return self._name
|
|
|
|
@property
|
|
def anchor(self):
|
|
"""The "anchor" attribute of the resource."""
|
|
# self.attributes cannot be empty; will cause an error on init.
|
|
return self.attributes[-1]
|
|
|
|
def IsAnchor(self, attribute):
|
|
"""Convenience method."""
|
|
return attribute == self.anchor
|
|
|
|
def IsLeafAnchor(self, attribute):
|
|
"""Convenience method."""
|
|
return self.IsAnchor(attribute)
|
|
|
|
@property
|
|
def attribute_to_params_map(self):
|
|
"""A map from all attribute names to param names."""
|
|
return self._param_names_map
|
|
|
|
@property
|
|
def collection_info(self):
|
|
return self._collection_info
|
|
|
|
# TODO(b/314193603): Add ParamName, AttributeName, and
|
|
# attribute_to_params_map to multitype to enable resource completers.
|
|
# Then add items to the meta class.
|
|
def ParamName(self, attribute_name):
|
|
"""Gets the param name from attribute. Used for autocompleters."""
|
|
if attribute_name not in self.attribute_to_params_map:
|
|
raise ValueError(
|
|
'No param name found for attribute [{}]. Existing attributes are '
|
|
'[{}]'.format(attribute_name,
|
|
', '.join(sorted(self.attribute_to_params_map.keys()))))
|
|
return self.attribute_to_params_map[attribute_name]
|
|
|
|
def AttributeName(self, param_name):
|
|
"""Gets the attribute name from param name. Used for autocompleters."""
|
|
for attribute_name, p in self.attribute_to_params_map.items():
|
|
if p == param_name:
|
|
return attribute_name
|
|
else:
|
|
return None
|
|
|
|
def Initialize(self, fallthroughs_map, parsed_args=None):
|
|
"""Initializes a resource given its fallthroughs.
|
|
|
|
The fallthrough map is used to derive each resource attribute (including
|
|
the anchor). Returns a fully parsed resource object.
|
|
|
|
Args:
|
|
fallthroughs_map: {str: [deps_lib._FallthroughBase]}, a dict of finalized
|
|
fallthroughs for the resource.
|
|
parsed_args: the argparse namespace.
|
|
|
|
Returns:
|
|
(googlecloudsdk.core.resources.Resource) the fully initialized resource.
|
|
|
|
Raises:
|
|
googlecloudsdk.calliope.concepts.concepts.InitializationError, if the
|
|
concept can't be initialized.
|
|
"""
|
|
params = {}
|
|
|
|
# Returns a function that can be used to parse each attribute, which will be
|
|
# used only if the resource parser does not receive a fully qualified
|
|
# resource name.
|
|
def LazyGet(name):
|
|
return lambda: deps_lib.Get(name, fallthroughs_map, parsed_args)
|
|
|
|
for attribute in self.attributes:
|
|
params[attribute.param_name] = LazyGet(attribute.name)
|
|
self._resources.RegisterApiByName(self._collection_info.api_name,
|
|
self._collection_info.api_version)
|
|
try:
|
|
return self._resources.Parse(
|
|
deps_lib.Get(
|
|
self.anchor.name, fallthroughs_map, parsed_args=parsed_args),
|
|
collection=self.collection,
|
|
params=params,
|
|
api_version=self._collection_info.api_version)
|
|
except deps_lib.AttributeNotFoundError as e:
|
|
raise InitializationError(
|
|
'The [{}] resource is not properly specified.\n'
|
|
'{}'.format(self.name, str(e)))
|
|
except resources.UserError as e:
|
|
raise InitializationError(str(e))
|
|
|
|
def Parse(self, attribute_to_args_map, base_fallthroughs_map,
|
|
parsed_args=None, plural=False, allow_empty=False):
|
|
"""Lazy parsing function for resource.
|
|
|
|
Generates resource based off of the parsed_args (user provided
|
|
arguments) and specified fallthrough behavior.
|
|
|
|
Args:
|
|
attribute_to_args_map: {str: str}, A map of attribute names to the names
|
|
of their associated flags.
|
|
base_fallthroughs_map: {str: [deps.Fallthrough]}, A map of attribute
|
|
names to non-argument fallthroughs, including command-level
|
|
fallthroughs.
|
|
parsed_args: the parsed Namespace.
|
|
plural: bool, True if multiple resources can be parsed, False otherwise.
|
|
allow_empty: bool, True if resource parsing is allowed to return no
|
|
resource, otherwise False.
|
|
|
|
Returns:
|
|
the initialized resources.Resource or a list of resources.Resource if the
|
|
resource argument is plural.
|
|
"""
|
|
if plural:
|
|
return self._ParseFromPluralValue(
|
|
attribute_to_args_map, base_fallthroughs_map, parsed_args,
|
|
allow_empty=allow_empty)
|
|
else:
|
|
return self._ParseFromValue(
|
|
attribute_to_args_map, base_fallthroughs_map, parsed_args,
|
|
allow_empty=allow_empty)
|
|
|
|
def BuildFullFallthroughsMap(
|
|
self, attribute_to_args_map, base_fallthroughs_map, parsed_args=None):
|
|
"""Generate fallthrough map that is used to resolve resource params.
|
|
|
|
Used as source of truth for how each attribute is resolved. It is also used
|
|
to generate help text for both plural and singular resources.
|
|
Fallthroughs are a list of objects that, when called, try different ways of
|
|
resolving a resource attribute (see googlecloudsdk.calliope.concepts.
|
|
deps_lib._Fallthrough). This method builds a map from the name of each
|
|
attribute to its list of fallthroughs.
|
|
|
|
For each attribute, adds default flag fallthroughs and fully specified
|
|
anchor fallthroughs.
|
|
|
|
Args:
|
|
attribute_to_args_map: {str: str}, A map of attribute names to the names
|
|
of their associated flags.
|
|
base_fallthroughs_map: {str: [deps.Fallthrough]}, A map of attribute
|
|
names to non-argument fallthroughs, including command-level
|
|
fallthroughs.
|
|
parsed_args: Namespace | None, user's CLI input
|
|
|
|
Returns:
|
|
{str: [deps.Fallthrough]}, a map from attribute name to all its
|
|
fallthroughs.
|
|
"""
|
|
fallthroughs_map = {**base_fallthroughs_map}
|
|
deps_map_util.AddFlagFallthroughs(
|
|
fallthroughs_map, self.attributes, attribute_to_args_map)
|
|
deps_map_util.UpdateWithValueFallthrough(
|
|
fallthroughs_map, self.anchor.name, parsed_args)
|
|
deps_map_util.AddAnchorFallthroughs(
|
|
fallthroughs_map, self.attributes, self.anchor, self.collection_info,
|
|
fallthroughs_map.get(self.anchor.name, []))
|
|
return fallthroughs_map
|
|
|
|
def _BuildFullFallthroughsMapList(
|
|
self, attribute_to_args_map, base_fallthroughs_map, parsed_args=None):
|
|
"""Builds fallthrough map for each anchor value specified in a list.
|
|
|
|
For each anchor value, create a falthrough map to derive the rest
|
|
of the resource params. For each attribute, adds flag fallthroughs
|
|
and fully specified anchor fallthroughs. For each attribute,
|
|
adds default flag fallthroughs and fully specified anchor fallthroughs.
|
|
|
|
Args:
|
|
attribute_to_args_map: {str: str}, A map of attribute names to the names
|
|
of their associated flags.
|
|
base_fallthroughs_map: FallthroughsMap, A map of attribute names to
|
|
non-argument fallthroughs, including command-level fallthroughs.
|
|
parsed_args: Namespace, used to parse the anchor value and derive
|
|
fully specified fallthroughs.
|
|
|
|
Returns:
|
|
list[FallthroughsMap], fallthrough map for each anchor value
|
|
"""
|
|
fallthroughs_map = {**base_fallthroughs_map}
|
|
deps_map_util.AddFlagFallthroughs(
|
|
fallthroughs_map, self.attributes, attribute_to_args_map)
|
|
deps_map_util.PluralizeFallthroughs(fallthroughs_map, self.anchor.name)
|
|
|
|
map_list = deps_map_util.CreateValueFallthroughMapList(
|
|
fallthroughs_map, self.anchor.name, parsed_args)
|
|
for full_map in map_list:
|
|
deps_map_util.AddAnchorFallthroughs(
|
|
full_map, self.attributes, self.anchor, self.collection_info,
|
|
full_map.get(self.anchor.name, []))
|
|
|
|
return map_list
|
|
|
|
def _ParseFromValue(
|
|
self, attribute_to_args_map, base_fallthroughs_map,
|
|
parsed_args, allow_empty=False):
|
|
"""Helper for parsing a singular resource from user input."""
|
|
fallthroughs_map = self.BuildFullFallthroughsMap(
|
|
attribute_to_args_map, base_fallthroughs_map, parsed_args)
|
|
try:
|
|
return self.Initialize(
|
|
fallthroughs_map, parsed_args=parsed_args)
|
|
except InitializationError:
|
|
if allow_empty:
|
|
return None
|
|
raise
|
|
|
|
def _ParseFromPluralValue(
|
|
self, attribute_to_args_map, base_fallthroughs_map,
|
|
parsed_args, allow_empty=False):
|
|
"""Helper for parsing a list of resources from user input."""
|
|
map_list = self._BuildFullFallthroughsMapList(
|
|
attribute_to_args_map, base_fallthroughs_map,
|
|
parsed_args=parsed_args)
|
|
parsed_resources = []
|
|
for fallthroughs_map in map_list:
|
|
resource = self.Initialize(fallthroughs_map, parsed_args=parsed_args)
|
|
parsed_resources.append(resource)
|
|
|
|
if parsed_resources:
|
|
return parsed_resources
|
|
elif allow_empty:
|
|
return []
|
|
else:
|
|
return self.Initialize(base_fallthroughs_map, parsed_args=parsed_args)
|
|
|
|
def _AttributeName(self, param_name, attribute_config, anchor=False,
|
|
is_positional=None):
|
|
"""Chooses attribute name for a param name.
|
|
|
|
If attribute_config gives an attribute name, that is used. Otherwise, if the
|
|
param is an anchor attribute, 'name' is used, or if not, param_name is used.
|
|
|
|
Args:
|
|
param_name: str, the parameter name from the collection.
|
|
attribute_config: ResourceParameterAttributeConfig, the config for the
|
|
param_name.
|
|
anchor: bool, whether the parameter is the "anchor" or the last in the
|
|
collection path.
|
|
is_positional: bool, optional value that determines if anchor argument is
|
|
a positional and reformats anchor attribute name accordingly.
|
|
|
|
Returns:
|
|
(str) the attribute name.
|
|
"""
|
|
attribute_name = attribute_config.attribute_name
|
|
if attribute_name:
|
|
# TODO(b/246766107) We need to investigate if we can reformat the
|
|
# attribute names automatically all the time. Currently, the attribute is
|
|
# only auto-formatted when positional is specified in resource spec.
|
|
# Currently, only resource specs generated by yaml files are passing in
|
|
# the is_positional value in order to avoid breaking changes.
|
|
if is_positional is None:
|
|
return attribute_name
|
|
return (format_util.SnakeCase(attribute_name) if is_positional and anchor
|
|
else format_util.KebabCase(attribute_name))
|
|
if anchor:
|
|
return 'name'
|
|
return param_name.replace('Id', '_id').lower()
|
|
|
|
def __eq__(self, other):
|
|
return (super(ResourceSpec, self).__eq__(other)
|
|
and self.disable_auto_completers == other.disable_auto_completers
|
|
and self.attribute_to_params_map == other.attribute_to_params_map)
|
|
|
|
def __hash__(self):
|
|
return super(ResourceSpec, self).__hash__() + sum(
|
|
map(hash, [self.disable_auto_completers, self.attribute_to_params_map]))
|
|
|
|
|
|
class ResourceParameterAttributeConfig(object):
|
|
"""Configuration used to create attributes from resource parameters."""
|
|
|
|
@classmethod
|
|
def FromData(cls, data):
|
|
"""Constructs an attribute config from data defined in the yaml file.
|
|
|
|
Args:
|
|
data: {}, the dict of data from the YAML file for this single attribute.
|
|
|
|
Returns:
|
|
ResourceParameterAttributeConfig
|
|
"""
|
|
attribute_name = data['attribute_name']
|
|
parameter_name = data['parameter_name']
|
|
help_text = data['help']
|
|
completer = util.Hook.FromData(data, 'completer')
|
|
completion_id_field = data.get('completion_id_field', None)
|
|
completion_request_params_list = data.get('completion_request_params', [])
|
|
completion_request_params = {
|
|
param.get('fieldName'): param.get('value')
|
|
for param in completion_request_params_list
|
|
}
|
|
|
|
if default_config := DEFAULT_RESOURCE_ATTRIBUTE_CONFIGS.get(attribute_name):
|
|
fallthroughs = default_config.fallthroughs.copy()
|
|
else:
|
|
fallthroughs = []
|
|
|
|
# Add property fallthroughs.
|
|
prop = properties.FromString(data.get('property', ''))
|
|
prop_fallthrough = prop and deps_lib.PropertyFallthrough(prop)
|
|
if prop_fallthrough and prop_fallthrough not in fallthroughs:
|
|
fallthroughs.append(prop_fallthrough)
|
|
|
|
# Add fallthroughs from python hooks.
|
|
fallthrough_data = data.get('fallthroughs', [])
|
|
fallthroughs_from_hook = []
|
|
for f in fallthrough_data:
|
|
if 'value' in f:
|
|
fallthroughs_from_hook.append(
|
|
deps_lib.ValueFallthrough(
|
|
f['value'], f['hint'] if 'hint' in f else None
|
|
)
|
|
)
|
|
elif 'hook' in f:
|
|
fallthroughs_from_hook.append(
|
|
deps_lib.Fallthrough(util.Hook.FromPath(f['hook']), hint=f['hint'])
|
|
)
|
|
|
|
fallthroughs += fallthroughs_from_hook
|
|
return cls(
|
|
name=attribute_name,
|
|
help_text=help_text,
|
|
fallthroughs=fallthroughs,
|
|
completer=completer,
|
|
completion_id_field=completion_id_field,
|
|
completion_request_params=completion_request_params,
|
|
parameter_name=parameter_name)
|
|
|
|
def __init__(self,
|
|
name=None,
|
|
help_text=None,
|
|
fallthroughs=None,
|
|
completer=None,
|
|
completion_request_params=None,
|
|
completion_id_field=None,
|
|
value_type=None,
|
|
parameter_name=None):
|
|
"""Create a resource attribute.
|
|
|
|
Args:
|
|
name: str, the name of the attribute. This controls the naming of flags
|
|
based on the attribute.
|
|
help_text: str, generic help text for any flag based on the attribute. One
|
|
special expansion is available to convert "{resource}" to the name of
|
|
the resource.
|
|
fallthroughs: [deps_lib.Fallthrough], A list of fallthroughs to use to
|
|
resolve the attribute if it is not provided on the command line.
|
|
completer: core.cache.completion_cache.Completer, the completer
|
|
associated with the attribute.
|
|
completion_request_params: {str: value}, a dict of field names to static
|
|
values to fill in for the completion request.
|
|
completion_id_field: str, the ID field of the return value in the
|
|
response for completion commands.
|
|
value_type: the type to be accepted by the attribute arg. Defaults to str.
|
|
parameter_name: the API parameter name that this attribute maps to.
|
|
"""
|
|
self.attribute_name = name
|
|
self.help_text = help_text
|
|
self.fallthroughs = fallthroughs or []
|
|
if completer and (completion_request_params or completion_id_field):
|
|
raise ValueError('Custom completer and auto-completer should not be '
|
|
'specified at the same time')
|
|
self.completer = completer
|
|
self.completion_request_params = completion_request_params
|
|
self.completion_id_field = completion_id_field
|
|
self.value_type = value_type or str
|
|
self.parameter_name = parameter_name
|
|
|
|
|
|
def ParseAttributesFromData(attributes_data, expected_param_names):
|
|
"""Parses a list of ResourceParameterAttributeConfig from yaml data.
|
|
|
|
Args:
|
|
attributes_data: dict, the attributes data defined in
|
|
command_lib/resources.yaml file.
|
|
expected_param_names: [str], the names of the API parameters that the API
|
|
method accepts. Example, ['projectsId', 'instancesId'].
|
|
|
|
Returns:
|
|
[ResourceParameterAttributeConfig].
|
|
|
|
Raises:
|
|
InvalidResourceArgumentLists: if the attributes defined in the yaml file
|
|
don't match the expected fields in the API method.
|
|
"""
|
|
raw_attributes = [
|
|
ResourceParameterAttributeConfig.FromData(a) for a in attributes_data
|
|
]
|
|
registered_param_names = [a.parameter_name for a in raw_attributes]
|
|
final_attributes = []
|
|
|
|
# TODO(b/78851830): improve the time complexity here.
|
|
for expected_name in expected_param_names:
|
|
if raw_attributes and expected_name == raw_attributes[0].parameter_name:
|
|
# Attribute matches expected, add it and continue checking.
|
|
final_attributes.append(raw_attributes.pop(0))
|
|
elif expected_name in IGNORED_FIELDS:
|
|
# Attribute doesn't match but is being ignored. Add an auto-generated
|
|
# attribute as a substitute.
|
|
# Currently, it would only be the project config.
|
|
attribute_name = IGNORED_FIELDS[expected_name]
|
|
ignored_attribute = DEFAULT_RESOURCE_ATTRIBUTE_CONFIGS.get(attribute_name)
|
|
# Manually add the parameter name, e.g. project, projectId or projectsId.
|
|
ignored_attribute.parameter_name = expected_name
|
|
final_attributes.append(ignored_attribute)
|
|
else:
|
|
# It doesn't match (or there are no more registered params) and the
|
|
# field is not being ignored, error.
|
|
raise InvalidResourceArgumentLists(expected_param_names,
|
|
registered_param_names)
|
|
|
|
if raw_attributes:
|
|
# All expected fields were processed but there are still registered
|
|
# attribute params remaining, they must be extra.
|
|
raise InvalidResourceArgumentLists(expected_param_names,
|
|
registered_param_names)
|
|
|
|
return final_attributes
|
|
|
|
|
|
DEFAULT_PROJECT_ATTRIBUTE_CONFIG = ResourceParameterAttributeConfig(
|
|
name='project',
|
|
help_text='Project ID of the Google Cloud project for the {resource}.',
|
|
fallthroughs=[
|
|
# Typically argument fallthroughs should be configured at the command
|
|
# level, but the --project flag is currently available in every command.
|
|
deps_lib.ArgFallthrough('--project'),
|
|
deps_lib.PropertyFallthrough(properties.VALUES.core.project)
|
|
])
|
|
DEFAULT_RESOURCE_ATTRIBUTE_CONFIGS = {
|
|
'project': DEFAULT_PROJECT_ATTRIBUTE_CONFIG}
|
|
_DEFAULT_CONFIGS = {'project': DEFAULT_PROJECT_ATTRIBUTE_CONFIG}
|