feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
welcome: |-
Thank you for taking the survey. The response is anonymous and will only be used to improve our product. Please DO NOT report bugs here. Use $ gcloud feedback instead to help us address the issue faster.
questions:
- question_type: SatisfactionQuestion
properties:
question: Overall, how satisfied are you with gcloud CLI?
instruction: "Please answer the question by typing the number that corresponds to your answer: "
instruction_on_rejection: "Answer is invalid, please type a number from 1 to 5: "
choices:
- Very satisfied
- Somewhat satisfied
- Neither satisfied nor dissatisfied
- Somewhat dissatisfied
- Very dissatisfied
- question_type: NPSQuestion
properties:
question: |-
On a scale from 0-10, where 0 means "Not at all likely" and 10 means "Extremely likely", how likely are you to recommend gcloud CLI to a friend or colleague?
instruction: "Please answer the question by typing the number that corresponds to your answer: "
instruction_on_rejection: "Answer is invalid, please type a number from 0 to 10: "
min_answer: 0
max_answer: 10
- question_type: FreeTextQuestion
properties:
question: |-
What are the reasons for the ratings you gave? [Please DO NOT enter personal info]
instruction: "Please type your answer: "
- question_type: FreeTextQuestion
properties:
question: |-
What could we do to improve your ratings? [Please DO NOT enter personal info]
instruction: "Please type your answer: "

View File

@@ -0,0 +1,337 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 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.
"""This module contains all survey question types."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import abc
from googlecloudsdk.command_lib.survey import util as survey_util
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
import six
class Error(exceptions.Error):
"""Base error class for this module."""
pass
class AnswerRejectedError(Error):
"""Raises when answer is rejected."""
pass
class RetrieveAnswerOfUnansweredQuestion(Error):
"""Raises when retrieving answer from an unanswered question."""
pass
class QuestionCreationError(Error):
"""Raises when question cannot be created with the provided data."""
def __init__(self, required_fields):
required_fields_in_string = ', '.join(required_fields)
super(QuestionCreationError, self).__init__(
'Question cannot be created because either some '
'required field is missing or there are redundant fields. Required '
'fields are {}.'.format(required_fields_in_string))
class Question(six.with_metaclass(abc.ABCMeta, object)):
"""Base class for survey questions.
Attributes:
_question: str, the question to ask.
_instruction: str, instruction on how to answer the question.
_instruction_on_rejection: str, instruction after the answer is rejected.
_answer: str, the answer to the question.
"""
def __init__(self,
question,
instruction,
instruction_on_rejection=None,
answer=None):
self._question = question
self._instruction = instruction
self._instruction_on_rejection = instruction_on_rejection
self._answer = answer
@abc.abstractmethod
def FromDictionary(self, content):
pass
@property
def question(self):
return self._question
@property
def instruction(self):
return self._instruction
@property
def instruction_on_rejection(self):
return self._instruction_on_rejection
def PrintQuestion(self):
self._PrintQuestion()
log.out.flush()
@abc.abstractmethod
def _PrintQuestion(self):
pass
def PrintInstruction(self):
if self._instruction:
log.err.write(self._instruction)
def PrintInstructionOnRejection(self):
if self._instruction_on_rejection:
log.err.write(self._instruction_on_rejection)
@abc.abstractmethod
def AcceptAnswer(self, answer):
"""Returns True if answer is accepted, otherwise returns False."""
pass
def IsAnswered(self):
return self._answer is not None
def AnswerQuestion(self, answer):
if self.AcceptAnswer(answer):
self._answer = answer
else:
raise AnswerRejectedError('Answer is invalid.')
@property
def answer(self):
if self.IsAnswered():
return self._answer
else:
raise RetrieveAnswerOfUnansweredQuestion('No answer for this question.')
def __eq__(self, other):
if isinstance(other, self.__class__):
# pylint: disable=protected-access
return (self._question == other._question and
self._instruction == other._instruction and
self._instruction_on_rejection == other._instruction_on_rejection)
# pylint: enable=protected-access
return False
def __ne__(self, other):
return not self == other # pylint: disable=g-comparison-negation
def __hash__(self):
return hash((self._question, self._instruction,
self._instruction_on_rejection))
class MultiChoiceQuestion(Question):
"""Multi-choice question.
Attributes:
_choices: [str], list of choices.
"""
def __init__(self,
question,
instruction,
instruction_on_rejection,
choices,
answer=None):
super(MultiChoiceQuestion, self).__init__(
question, instruction, instruction_on_rejection, answer=answer)
self._choices = choices
@classmethod
def FromDictionary(cls, content):
try:
return cls(**content)
except TypeError:
raise QuestionCreationError(required_fields=[
'question', 'instruction', 'instruction_on_rejection', 'choices'
])
def _PrintQuestion(self):
"""Prints question and lists all choices."""
# index choices from 1
question_repr = self._FormatQuestion(
indexes=range(1,
len(self._choices) + 1))
log.Print(question_repr)
def _FormatQuestion(self, indexes):
"""Formats question to present to users."""
choices_repr = [
'[{}] {}'.format(index, msg)
for index, msg in zip(indexes, self._choices)
]
choices_repr = [survey_util.Indent(content, 2) for content in choices_repr]
choices_repr = '\n'.join(choices_repr)
question_repr = survey_util.Indent(self._question, 1)
return '\n'.join([question_repr, choices_repr])
def AcceptAnswer(self, answer):
"""Returns True if answer is accepted, otherwise returns False."""
try:
answer_int = int(answer)
except ValueError:
return False
else:
return 1 <= answer_int <= len(self._choices)
def Choice(self, index):
"""Gets the choice at the given index."""
# choices are indexed from 1
return self._choices[index - 1]
def __eq__(self, other):
if isinstance(other, self.__class__):
# pylint: disable=protected-access
return (self._question == other._question and
self._instruction == other._instruction and
self._instruction_on_rejection == other._instruction_on_rejection
and self._choices == other._choices)
# pylint: enable=protected-access
return False
def __hash__(self):
return hash((self._question, self._instruction,
self._instruction_on_rejection, tuple(self._choices)))
def __len__(self):
return len(self._choices)
class SatisfactionQuestion(MultiChoiceQuestion):
"""Customer satisfaction question."""
def IsSatisfied(self):
"""Returns true is user answers "Very satisfied" or "Somewhat satisfied"."""
if self.IsAnswered():
return int(self.answer) > 3
else:
return None
def _PrintQuestion(self):
# index choices in the reverse order, e.g. 5 is "Very satisfied" and 1 is
# "Very dissatisfied".
choice_indexes = range(len(self._choices), 0, -1)
question_repr = self._FormatQuestion(indexes=choice_indexes)
log.Print(question_repr)
def Choice(self, index):
"""Gets the choice at the given index."""
# choices are indexed in the reverse order
return self._choices[len(self._choices) - index]
class RatingQuestion(Question):
""""Rating question.
Attributes:
min_answer: int, minimum acceptable value for answer.
max_answer: int, maximum acceptable value for answer.
"""
@classmethod
def FromDictionary(cls, content):
try:
return cls(**content)
except TypeError:
raise QuestionCreationError(required_fields=[
'question', 'instruction', 'instruction_on_rejection', 'min_answer',
'max_answer'
])
def __init__(self,
question,
instruction,
instruction_on_rejection,
min_answer,
max_answer,
answer=None):
super(RatingQuestion, self).__init__(
question=question,
instruction=instruction,
instruction_on_rejection=instruction_on_rejection,
answer=answer)
self._min = min_answer
self._max = max_answer
def _PrintQuestion(self):
question = survey_util.Indent(self._question, 1)
log.Print(question)
def AcceptAnswer(self, answer):
try:
answer_int = int(answer)
return self._min <= answer_int <= self._max
except ValueError:
return False
def __eq__(self, other):
if isinstance(other, self.__class__):
# pylint: disable=protected-access
return (self._question == other._question and
self._instruction == other._instruction and
self._instruction_on_rejection == other._instruction_on_rejection
and self._min == other._min and self._max == other._max)
# pylint: enable=protected-access
return False
def __ne__(self, other):
return not self == other # pylint: disable=g-comparison-negation
def __hash__(self):
return hash((self._question, self._instruction,
self._instruction_on_rejection, self._min, self._max))
class NPSQuestion(RatingQuestion):
"""Net promoter score question."""
class FreeTextQuestion(Question):
"""Free text question."""
def _PrintQuestion(self):
question = survey_util.Indent(self._question, 1)
log.Print(question)
def AcceptAnswer(self, answer):
"""Returns True if answer is accepted, otherwise returns False.
Accepts any answer for free text question.
Args:
answer: str, the answer to check.
Returns:
True
"""
return True
@classmethod
def FromDictionary(cls, content):
try:
return cls(**content)
except TypeError:
raise QuestionCreationError(required_fields=['question', 'instruction'])

View File

@@ -0,0 +1,139 @@
# -*- coding: utf-8 -*- #
# Copyright 2015 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.
"""This module constructs surveys."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
import enum
from googlecloudsdk.command_lib.survey import question
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import yaml
from googlecloudsdk.core.util import encoding
from googlecloudsdk.core.util import pkg_resources
class Error(exceptions.Error):
"""Base error class for this module."""
pass
class QuestionTypeNotDefinedError(Error):
"""Raises when question type is not defined in the question module."""
pass
def _GetSurveyContentDirectory():
"""Get the directory containing all surveys in yaml format.
Returns:
Path to the surveys directory, i.e.
$CLOUDSDKROOT/lib/googlecloudsdk/command_lib/survey/contents
"""
return os.path.join(os.path.dirname(encoding.Decode(__file__)), 'contents')
class Survey(object):
"""The survey class.
Survey content are defined in yaml files in
googlecloudsdk/command_lib/survey/contents. Each yaml file represents one
survey.
Attributes:
name: str, name of the survey. It should match a name of one yaml file in
googlecloudsdk/command_lib/survey/contents (w/o the file extension).
_survey_content: parsed yaml data, raw content of the survey.
questions: [Question], list of questions in this survey.
welcome: str, welcome message when entering the survey.
"""
@enum.unique
class ControlOperation(enum.Enum):
EXIT_SURVEY = 'x'
SKIP_QUESTION = 's'
INSTRUCTION_MESSAGE = (
'To skip this question, type {}; to exit the survey, '
'type {}.').format(ControlOperation.SKIP_QUESTION.value,
ControlOperation.EXIT_SURVEY.value)
def __init__(self, name):
self.name = name
self._survey_content = self._LoadSurveyContent()
self._questions = list(self._LoadQuestions())
def _LoadSurveyContent(self):
"""Loads the survey yaml file and return the parsed data."""
survey_file = os.path.join(_GetSurveyContentDirectory(),
self.name + '.yaml')
survey_data = pkg_resources.GetResourceFromFile(survey_file)
return yaml.load(survey_data)
def _LoadQuestions(self):
"""Generator of questions in this survey."""
for q in self._survey_content['questions']:
question_type = q['question_type']
if not hasattr(question, question_type):
raise QuestionTypeNotDefinedError('The question type is not defined.')
yield getattr(question, question_type).FromDictionary(q['properties'])
@property
def questions(self):
return self._questions
@property
def welcome(self):
return self._survey_content['welcome']
def PrintWelcomeMsg(self):
log.err.Print(self.welcome)
@classmethod
def PrintInstruction(cls):
log.err.Print(cls.INSTRUCTION_MESSAGE)
def __iter__(self):
return iter(self._questions)
class GeneralSurvey(Survey):
"""GeneralSurvey defined in googlecloudsdk/command_lib/survey/contents."""
SURVEY_NAME = 'GeneralSurvey'
def __init__(self):
super(GeneralSurvey, self).__init__(self.SURVEY_NAME)
def __iter__(self):
yield self.questions[0] # satisfaction question
yield self.questions[1] # NPS question
if self.IsSatisfied() is None or self.IsSatisfied():
yield self.questions[2]
else:
yield self.questions[3]
def IsSatisfied(self):
"""Returns if survey respondent is satisfied."""
satisfaction_question = self.questions[0]
if satisfaction_question.IsAnswered():
return satisfaction_question.IsSatisfied()
else:
return None

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 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.
"""Utility module for CLI survey."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
def Indent(paragraph, indent_level=1, indent_size=2, indent_char=' '):
r"""Indent a paragraph.
Args:
paragraph: str, the paragraph to indent. Each line is separated by '\r',
'\n', or '\r\n'.
indent_level: int, the level of indentation.
indent_size: int, width of each indentation.
indent_char: character, padding character.
Returns:
Indented paragraph.
"""
lines = paragraph.splitlines(True)
lines_indent = [
(indent_char * indent_size * indent_level) + line for line in lines
]
return ''.join(lines_indent)