blob: cb26c9bed5f0815a59e42949bf1bb43f8027655d [file] [log] [blame]
# Copyright 2020 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd
"""Methods for converting resource names to protorpc objects and back.
IngestFoo methods take resource names and return the IDs of the resources.
While some Ingest methods need to check for the existence of resources as
a side-effect of producing their IDs, other layers that call these methods
should always do their own validity checking.
ConvertFoo methods take object ids
(and sometimes a MonorailConnection and ServiceManager)
and return resource names.
"""
import re
import logging
from features import features_constants
from framework import exceptions
from framework import validate
from project import project_constants
from tracker import tracker_constants
from proto import tracker_pb2
# Constants that hold regex patterns for resource names.
PROJECT_NAME_PATTERN = (
r'projects\/(?P<project_name>%s)' % project_constants.PROJECT_NAME_PATTERN)
PROJECT_NAME_RE = re.compile(r'%s$' % PROJECT_NAME_PATTERN)
FIELD_DEF_NAME_RE = re.compile(
r'%s\/fieldDefs\/(?P<field_def>\d+)$' % (PROJECT_NAME_PATTERN))
APPROVAL_DEF_NAME_PATTERN = (
r'%s\/approvalDefs\/(?P<approval_def>\d+)' % PROJECT_NAME_PATTERN)
APPROVAL_DEF_NAME_RE = re.compile(r'%s$' % APPROVAL_DEF_NAME_PATTERN)
HOTLIST_PATTERN = r'hotlists\/(?P<hotlist_id>\d+)'
HOTLIST_NAME_RE = re.compile(r'%s$' % HOTLIST_PATTERN)
HOTLIST_ITEM_NAME_RE = re.compile(
r'%s\/items\/(?P<project_name>%s)\.(?P<local_id>\d+)$' % (
HOTLIST_PATTERN,
project_constants.PROJECT_NAME_PATTERN))
ISSUE_PATTERN = (r'projects\/(?P<project>%s)\/issues\/(?P<local_id>\d+)' %
project_constants.PROJECT_NAME_PATTERN)
ISSUE_NAME_RE = re.compile(r'%s$' % ISSUE_PATTERN)
COMMENT_PATTERN = (r'%s\/comments\/(?P<comment_num>\d+)' % ISSUE_PATTERN)
COMMENT_NAME_RE = re.compile(r'%s$' % COMMENT_PATTERN)
USER_NAME_RE = re.compile(r'users\/((?P<user_id>\d+)|(?P<potential_email>.+))$')
APPROVAL_VALUE_RE = re.compile(
r'%s\/approvalValues\/(?P<approval_id>\d+)$' % ISSUE_PATTERN)
ISSUE_TEMPLATE_RE = re.compile(
r'%s\/templates\/(?P<template_id>\d+)$' % (PROJECT_NAME_PATTERN))
# Constants that hold the template patterns for creating resource names.
PROJECT_NAME_TMPL = 'projects/{project_name}'
PROJECT_CONFIG_TMPL = 'projects/{project_name}/config'
PROJECT_MEMBER_NAME_TMPL = 'projects/{project_name}/members/{user_id}'
HOTLIST_NAME_TMPL = 'hotlists/{hotlist_id}'
HOTLIST_ITEM_NAME_TMPL = '%s/items/{project_name}.{local_id}' % (
HOTLIST_NAME_TMPL)
ISSUE_NAME_TMPL = 'projects/{project}/issues/{local_id}'
COMMENT_NAME_TMPL = '%s/comments/{comment_id}' % ISSUE_NAME_TMPL
APPROVAL_VALUE_NAME_TMPL = '%s/approvalValues/{approval_id}' % ISSUE_NAME_TMPL
USER_NAME_TMPL = 'users/{user_id}'
PROJECT_STAR_NAME_TMPL = 'users/{user_id}/projectStars/{project_name}'
PROJECT_SQ_NAME_TMPL = 'projects/{project_name}/savedQueries/{query_name}'
ISSUE_TEMPLATE_TMPL = 'projects/{project_name}/templates/{template_id}'
STATUS_DEF_TMPL = 'projects/{project_name}/statusDefs/{status}'
LABEL_DEF_TMPL = 'projects/{project_name}/labelDefs/{label}'
COMPONENT_DEF_TMPL = 'projects/{project_name}/componentDefs/{component_id}'
COMPONENT_DEF_RE = re.compile(
r'%s\/componentDefs\/((?P<component_id>\d+)|(?P<path>%s))$' %
(PROJECT_NAME_PATTERN, tracker_constants.COMPONENT_PATH_PATTERN))
FIELD_DEF_TMPL = 'projects/{project_name}/fieldDefs/{field_id}'
APPROVAL_DEF_TMPL = 'projects/{project_name}/approvalDefs/{approval_id}'
def _GetResourceNameMatch(name, regex):
# type: (str, Pattern[str]) -> Match[str]
"""Takes a resource name and returns the regex match.
Args:
name: Resource name.
regex: Compiled regular expression Pattern object used to match name.
Raises:
InputException if there is not match.
"""
match = regex.match(name)
if not match:
raise exceptions.InputException(
'Invalid resource name: %s.' % name)
return match
def _IssueIdsFromLocalIds(cnxn, project_local_id_pairs, services):
# type: (MonorailConnection, Sequence[Tuple(str, int)], Services ->
# Sequence[int]
"""Fetches issue IDs using the given project/local ID pairs."""
# Fetch Project ids from Project names.
project_ids_by_name = services.project.LookupProjectIDs(
cnxn, [pair[0] for pair in project_local_id_pairs])
# Create (project_id, issue_local_id) pairs from project_local_id_pairs.
project_id_local_ids = []
with exceptions.ErrorAggregator(exceptions.NoSuchProjectException) as err_agg:
for project_name, local_id in project_local_id_pairs:
try:
project_id = project_ids_by_name[project_name]
project_id_local_ids.append((project_id, local_id))
except KeyError:
err_agg.AddErrorMessage('Project %s not found.' % project_name)
issue_ids, misses = services.issue.LookupIssueIDsFollowMoves(
cnxn, project_id_local_ids)
if misses:
# Raise error with resource names rather than backend IDs.
project_names_by_id = {
p_id: p_name for p_name, p_id in project_ids_by_name.iteritems()
}
misses_by_resource_name = [
_ConstructIssueName(project_names_by_id[p_id], local_id)
for (p_id, local_id) in misses
]
raise exceptions.NoSuchIssueException(
'Issue(s) %r not found' % misses_by_resource_name)
return issue_ids
# FieldDefs
def IngestFieldDefName(cnxn, name, services):
# type: (MonorailConnection, str, Services) -> (int, int)
"""Ingests a FieldDef's resource name.
Args:
cnxn: MonorailConnection to the database.
name: Resource name of a FieldDef.
services: Services object for connections to backend services.
Returns:
The Project's ID and the FieldDef's ID. FieldDef is not guaranteed to exist.
TODO(jessan): This order should be consistent throughout the file.
Raises:
InputException if the given name does not have a valid format.
NoSuchProjectException if the given project name does not exist.
"""
match = _GetResourceNameMatch(name, FIELD_DEF_NAME_RE)
field_id = int(match.group('field_def'))
project_name = match.group('project_name')
id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
project_id = id_dict.get(project_name)
if project_id is None:
raise exceptions.NoSuchProjectException(
'Project not found: %s.' % project_name)
return project_id, field_id
# Hotlists
def IngestHotlistName(name):
# type: (str) -> int
"""Takes a Hotlist resource name and returns the Hotlist ID.
Args:
name: Resource name of a Hotlist.
Returns:
The Hotlist's ID
Raises:
InputException if the given name does not have a valid format.
"""
match = _GetResourceNameMatch(name, HOTLIST_NAME_RE)
return int(match.group('hotlist_id'))
def IngestHotlistItemNames(cnxn, names, services):
# type: (MonorailConnection, Sequence[str], Services -> Sequence[int]
"""Takes HotlistItem resource names and returns the associated Issues' IDs.
Args:
cnxn: MonorailConnection to the database.
names: List of HotlistItem resource names.
services: Services object for connections to backend services.
Returns:
List of Issue IDs associated with the given HotlistItems.
Raises:
InputException if a resource name does not have a valid format.
NoSuchProjectException if an Issue's Project is not found.
NoSuchIssueException if an Issue is not found.
"""
project_local_id_pairs = []
for name in names:
match = _GetResourceNameMatch(name, HOTLIST_ITEM_NAME_RE)
project_local_id_pairs.append(
(match.group('project_name'), int(match.group('local_id'))))
return _IssueIdsFromLocalIds(cnxn, project_local_id_pairs, services)
def ConvertHotlistName(hotlist_id):
# type: (int) -> str
"""Takes a Hotlist and returns the Hotlist's resource name.
Args:
hotlist_id: ID of the Hotlist.
Returns:
The resource name of the Hotlist.
"""
return HOTLIST_NAME_TMPL.format(hotlist_id=hotlist_id)
def ConvertHotlistItemNames(cnxn, hotlist_id, issue_ids, services):
# type: (MonorailConnection, int, Collection[int], Services) ->
# Mapping[int, str]
"""Takes a Hotlist ID and HotlistItem's issue_ids and returns
the Hotlist items' resource names.
Args:
cnxn: MonorailConnection object.
hotlist_id: ID of the Hotlist the items belong to.
issue_ids: List of Issue IDs that are part of the hotlist's items.
services: Services object for connections to backend services.
Returns:
Dict of Issue IDs to HotlistItem resource names for Issues that are found.
"""
# {issue_id: (project_name, local_id),...}
issue_refs_dict = services.issue.LookupIssueRefs(cnxn, issue_ids)
issue_ids_to_names = {}
for issue_id in issue_ids:
project_name, local_id = issue_refs_dict.get(issue_id, (None, None))
if project_name and local_id:
issue_ids_to_names[issue_id] = HOTLIST_ITEM_NAME_TMPL.format(
hotlist_id=hotlist_id, project_name=project_name, local_id=local_id)
return issue_ids_to_names
# Issues
def IngestCommentName(cnxn, name, services):
# type: (MonorailConnection, str, Services) -> Tuple[int, int, int]
"""Ingests a Comment's resource name.
Args:
cnxn: MonorailConnection to the database.
name: Resource name of a Comment.
services: Services object for connections to backend services.
Returns:
Tuple containing three items:
1. Global ID of the parent project.
2. Global Issue id of the parent issue.
3. Sequence number of the comment. This is not checked for existence.
Raises:
InputException if the given name does not have a valid format.
NoSuchIssueException if the parent Issue does not exist.
NoSuchProjectException if the parent Project does not exist.
"""
match = _GetResourceNameMatch(name, COMMENT_NAME_RE)
# Project
project_name = match.group('project')
id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
project_id = id_dict.get(project_name)
if project_id is None:
raise exceptions.NoSuchProjectException(
'Project not found: %s.' % project_name)
# Issue
local_id = int(match.group('local_id'))
issue_pair = [(project_name, local_id)]
issue_id = _IssueIdsFromLocalIds(cnxn, issue_pair, services)[0]
return project_id, issue_id, int(match.group('comment_num'))
def CreateCommentNames(issue_local_id, issue_project, comment_sequence_nums):
# type: (int, str, Sequence[int]) -> Mapping[int, str]
"""Returns the resource names for the given comments.
Note: crbug.com/monorail/7507 has important context about guarantees required
for comment resource names to be permanent references.
Args:
issue_local_id: local id of the issue for which we're converting comments.
issue_project: the project of the issue for which we're converting comments.
comment_sequence_nums: sequence numbers of comments on the given issue.
Returns:
A mapping from comment sequence number to comment resource names.
"""
sequence_nums_to_names = {}
for comment_sequence_num in comment_sequence_nums:
sequence_nums_to_names[comment_sequence_num] = COMMENT_NAME_TMPL.format(
project=issue_project,
local_id=issue_local_id,
comment_id=comment_sequence_num)
return sequence_nums_to_names
def IngestApprovalDefName(cnxn, name, services):
# type: (MonorailConnection, str, Services) -> int
"""Ingests an ApprovalDef's resource name.
Args:
cnxn: MonorailConnection to the database.
name: Resource name of an ApprovalDef.
services: Services object for connections to backend services.
Returns:
The ApprovalDef ID specified in `name`.
The ApprovalDef is not guaranteed to exist.
Raises:
InputException if the given name does not have a valid format.
NoSuchProjectException if the given project name does not exist.
"""
match = _GetResourceNameMatch(name, APPROVAL_DEF_NAME_RE)
# Project
project_name = match.group('project_name')
id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
project_id = id_dict.get(project_name)
if project_id is None:
raise exceptions.NoSuchProjectException(
'Project not found: %s.' % project_name)
return int(match.group('approval_def'))
def IngestApprovalValueName(cnxn, name, services):
# type: (MonorailConnection, str, Services) -> Tuple[int, int, int]
"""Ingests the three components of an ApprovalValue resource name.
Args:
cnxn: MonorailConnection object.
name: Resource name of an ApprovalValue.
services: Services object for connections to backend services.
Returns:
Tuple containing three items
1. Global ID of the parent project.
2. Global Issue ID of the parent issue.
3. The approval_id portion of the resource name. This is not checked
for existence.
Raises:
InputException if the given name does not have a valid format.
NoSuchIssueException if the parent Issue does not exist.
NoSuchProjectException if the parent Project does not exist.
"""
match = _GetResourceNameMatch(name, APPROVAL_VALUE_RE)
# Project
project_name = match.group('project')
id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
project_id = id_dict.get(project_name)
if project_id is None:
raise exceptions.NoSuchProjectException(
'Project not found: %s.' % project_name)
# Issue
local_id = int(match.group('local_id'))
issue_pair = [(project_name, local_id)]
issue_id = _IssueIdsFromLocalIds(cnxn, issue_pair, services)[0]
return project_id, issue_id, int(match.group('approval_id'))
def IngestIssueName(cnxn, name, services):
# type: (MonorailConnection, str, Services) -> int
"""Takes an Issue resource name and returns its global ID.
Args:
cnxn: MonorailConnection object.
name: Resource name of an Issue.
services: Services object for connections to backend services.
Returns:
The global Issue ID associated with the name.
Raises:
InputException if the given name does not have a valid format.
NoSuchIssueException if the Issue does not exist.
NoSuchProjectException if an Issue's Project is not found.
"""
return IngestIssueNames(cnxn, [name], services)[0]
def IngestIssueNames(cnxn, names, services):
# type: (MonorailConnection, Sequence[str], Services) -> Sequence[int]
"""Returns global IDs for the given Issue resource names.
Args:
cnxn: MonorailConnection object.
names: Resource names of zero or more issues.
services: Services object for connections to backend services.
Returns:
The global IDs for the issues.
Raises:
InputException if a resource name does not have a valid format.
NoSuchIssueException if an Issue is not found.
NoSuchProjectException if an Issue's Project is not found.
"""
project_local_id_pairs = []
with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
for name in names:
try:
match = _GetResourceNameMatch(name, ISSUE_NAME_RE)
project_local_id_pairs.append(
(match.group('project'), int(match.group('local_id'))))
except exceptions.InputException as e:
err_agg.AddErrorMessage(e.message)
return _IssueIdsFromLocalIds(cnxn, project_local_id_pairs, services)
def IngestProjectFromIssue(issue_name):
# type: (str) -> str
"""Takes an issue resource_name and returns its project name.
TODO(crbug/monorail/7614): This method should only be needed for the
workaround for the referenced issue. When the cleanup is completed, this
method should be able to be removed.
Args:
issue_name: A resource name for an issue.
Returns:
The project section of the resource name (e.g for 'projects/xyz/issue/1'),
the method would return 'xyz'. The associated project is not guaranteed to
exist.
Raises:
InputException if 'issue_name' does not have a valid format.
"""
match = _GetResourceNameMatch(issue_name, ISSUE_NAME_RE)
return match.group('project')
def ConvertIssueName(cnxn, issue_id, services):
# type: (MonorailConnection, int, Services) -> str
"""Takes an Issue ID and returns the corresponding Issue resource name.
Args:
cnxn: MonorailConnection object.
issue_id: The ID of the issue.
services: Services object.
Returns:
The resource name of the Issue.
Raises:
NoSuchIssueException if the issue is not found.
"""
name = ConvertIssueNames(cnxn, [issue_id], services).get(issue_id)
if not name:
raise exceptions.NoSuchIssueException()
return name
def ConvertIssueNames(cnxn, issue_ids, services):
# type: (MonorailConnection, Collection[int], Services) -> Mapping[int, str]
"""Takes Issue IDs and returns the Issue resource names.
Args:
cnxn: MonorailConnection object.
issue_ids: List of Issue IDs
services: Services object.
Returns:
Dict of Issue IDs to Issue resource names for Issues that are found.
"""
issue_ids_to_names = {}
issue_refs_dict = services.issue.LookupIssueRefs(cnxn, issue_ids)
for issue_id in issue_ids:
project, local_id = issue_refs_dict.get(issue_id, (None, None))
if project and local_id:
issue_ids_to_names[issue_id] = _ConstructIssueName(project, local_id)
return issue_ids_to_names
def _ConstructIssueName(project, local_id):
# type: (str, int) -> str
"""Takes project name and issue local id returns the Issue resource name."""
return ISSUE_NAME_TMPL.format(project=project, local_id=local_id)
def ConvertApprovalValueNames(cnxn, issue_id, services):
# type: (MonorailConnection, int, Services)
# -> Mapping[int, str]
"""Takes an Issue ID and returns the resource names of its ApprovalValues.
Args:
cnxn: MonorailConnection object.
issue_id: ID of the Issue the approval_values belong to.
services: Services object.
Returns:
Dict of ApprovalDef IDs to ApprovalValue resource names for
ApprovalDefs that are found.
Raises:
NoSuchIssueException if the Issue is not found.
"""
issue = services.issue.GetIssue(cnxn, issue_id)
project = services.project.GetProject(cnxn, issue.project_id)
config = services.config.GetProjectConfig(cnxn, issue.project_id)
ads_by_id = {fd.field_id: fd for fd in config.field_defs
if fd.field_type is tracker_pb2.FieldTypes.APPROVAL_TYPE}
approval_def_ids = [av.approval_id for av in issue.approval_values]
approval_ids_to_names = {}
for ad_id in approval_def_ids:
fd = ads_by_id.get(ad_id)
if not fd:
logging.info('Approval type field with id %d not found.', ad_id)
continue
approval_ids_to_names[ad_id] = APPROVAL_VALUE_NAME_TMPL.format(
project=project.project_name,
local_id=issue.local_id,
approval_id=ad_id)
return approval_ids_to_names
# Users
def IngestUserName(cnxn, name, services, autocreate=False):
# type: (MonorailConnection, str, Services) -> int
"""Takes a User resource name and returns a User ID.
Args:
cnxn: MonorailConnection object.
name: The User resource name.
services: Services object.
autocreate: set to True if new Users should be created for
emails in resource names that do not belong to existing
Users.
Returns:
The ID of the User.
Raises:
InputException if the resource name does not have a valid format.
NoSuchUserException if autocreate is False and the given email
was not found.
"""
match = _GetResourceNameMatch(name, USER_NAME_RE)
user_id = match.group('user_id')
if user_id:
return int(user_id)
elif validate.IsValidEmail(match.group('potential_email')):
return services.user.LookupUserID(
cnxn, match.group('potential_email'), autocreate=autocreate)
else:
raise exceptions.InputException(
'Invalid email format found in User resource name: %s' % name)
def IngestUserNames(cnxn, names, services, autocreate=False):
# Type: (MonorailConnection, Sequence[str], Services, Optional[bool]) ->
# Sequence[int]
"""Takes User resource names and returns the User IDs.
Args:
cnxn: MonorailConnection object.
names: List of User resource names.
services: Services object.
autocreate: set to True if new Users should be created for
emails in resource names that do not belong to existing
Users.
Returns:
List of User IDs in the same order as names.
Raises:
InputException if an resource name does not have a valid format.
NoSuchUserException if autocreate is False and some users with given
emails were not found.
"""
ids = []
for name in names:
ids.append(IngestUserName(cnxn, name, services, autocreate))
return ids
def ConvertUserName(user_id):
# type: (int) -> str
"""Takes a User ID and returns the User's resource name."""
return ConvertUserNames([user_id])[user_id]
def ConvertUserNames(user_ids):
# type: (Collection[int]) -> Mapping[int, str]
"""Takes User IDs and returns the Users' resource names.
Args:
user_ids: List of User IDs.
Returns:
Dict of User IDs to User resource names for all given user_ids.
"""
user_ids_to_names = {}
for user_id in user_ids:
user_ids_to_names[user_id] = USER_NAME_TMPL.format(user_id=user_id)
return user_ids_to_names
def ConvertProjectStarName(cnxn, user_id, project_id, services):
# type: (MonorailConnection, int, int, Services) -> str
"""Takes User ID and Project ID and returns the ProjectStar resource name.
Args:
user_id: User ID associated with the star.
project_id: ID of the starred project.
Returns:
The ProjectStar's name.
Raises:
NoSuchProjectException if the project_id is not found.
"""
project_name = services.project.LookupProjectNames(
cnxn, [project_id]).get(project_id)
return PROJECT_STAR_NAME_TMPL.format(
user_id=user_id, project_name=project_name)
# Projects
def IngestProjectName(cnxn, name, services):
# type: (str) -> int
"""Takes a Project resource name and returns the project id.
Args:
name: Resource name of a Project.
Returns:
The project's id
Raises:
InputException if the given name does not have a valid format.
NoSuchProjectException if no project exists with the given name.
"""
match = _GetResourceNameMatch(name, PROJECT_NAME_RE)
project_name = match.group('project_name')
id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
return id_dict.get(project_name)
def ConvertTemplateNames(cnxn, project_id, template_ids, services):
# type: (MonorailConnection, int, Collection[int] Services) ->
# Mapping[int, str]
"""Takes Template IDs and returns the Templates' resource names
Args:
cnxn: MonorailConnection object.
project_id: Project ID of Project that Templates must belong to.
template_ids: Template IDs to convert.
services: Services object.
Returns:
Dict of template ID to template resource names for all found template IDs
within the given project.
Raises:
NoSuchProjectException if no project exists with given id.
"""
id_to_resource_names = {}
project_name = services.project.LookupProjectNames(
cnxn, [project_id]).get(project_id)
project_templates = services.template.GetProjectTemplates(cnxn, project_id)
tmpl_by_id = {tmpl.template_id: tmpl for tmpl in project_templates}
for template_id in template_ids:
if template_id not in tmpl_by_id:
logging.info(
'Ignoring template referencing a non-existent id: %s, ' \
'or not in project: %s', template_id, project_id)
continue
id_to_resource_names[template_id] = ISSUE_TEMPLATE_TMPL.format(
project_name=project_name,
template_id=template_id)
return id_to_resource_names
def IngestTemplateName(cnxn, name, services):
# type: (MonorailConnection, str, Services) -> Tuple[int, int]
"""Ingests an IssueTemplate resource name.
Args:
cnxn: MonorailConnection object.
name: Resource name of an IssueTemplate.
services: Services object.
Returns:
The IssueTemplate's ID and the Project's ID.
Raises:
InputException if the given name does not have a valid format.
NoSuchProjectException if the given project name does not exist.
"""
match = _GetResourceNameMatch(name, ISSUE_TEMPLATE_RE)
template_id = int(match.group('template_id'))
project_name = match.group('project_name')
id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
project_id = id_dict.get(project_name)
if project_id is None:
raise exceptions.NoSuchProjectException(
'Project not found: %s.' % project_name)
return template_id, project_id
def ConvertStatusDefNames(cnxn, statuses, project_id, services):
# type: (MonorailConnection, Collection[str], int, Services) ->
# Mapping[str, str]
"""Takes list of status strings and returns StatusDef resource names
Args:
cnxn: MonorailConnection object.
statuses: List of status name strings
project_id: project id of project this belongs to
services: Services object.
Returns:
Mapping of string to resource name for all given `statuses`.
Raises:
NoSuchProjectException if no project exists with given id.
"""
project = services.project.GetProject(cnxn, project_id)
name_dict = {}
for status in statuses:
name_dict[status] = STATUS_DEF_TMPL.format(
project_name=project.project_name, status=status)
return name_dict
def ConvertLabelDefNames(cnxn, labels, project_id, services):
# type: (MonorailConnection, Collection[str], int, Services) ->
# Mapping[str, str]
"""Takes a list of labels and returns LabelDef resource names
Args:
cnxn: MonorailConnection object.
labels: List of labels as string
project_id: project id of project this belongs to
services: Services object.
Returns:
Dict of label string to label's resource name for all given `labels`.
Raises:
NoSuchProjectException if no project exists with given id.
"""
project = services.project.GetProject(cnxn, project_id)
name_dict = {}
for label in labels:
name_dict[label] = LABEL_DEF_TMPL.format(
project_name=project.project_name, label=label)
return name_dict
def ConvertComponentDefNames(cnxn, component_ids, project_id, services):
# type: (MonorailConnection, Collection[int], int, Services) ->
# Mapping[int, str]
"""Takes Component IDs and returns ComponentDef resource names
Args:
cnxn: MonorailConnection object.
component_ids: List of component ids
project_id: project id of project this belongs to
services: Services object.
Returns:
Dict of component ID to component's resource name for all given
`component_ids`
Raises:
NoSuchProjectException if no project exists with given id.
"""
project = services.project.GetProject(cnxn, project_id)
id_dict = {}
for component_id in component_ids:
id_dict[component_id] = COMPONENT_DEF_TMPL.format(
project_name=project.project_name, component_id=component_id)
return id_dict
def IngestComponentDefNames(cnxn, names, services):
# type: (MonorailConnection, Sequence[str], Services)
# -> Sequence[Tuple[int, int]]
"""Takes a list of component resource names and returns their IDs.
Args:
cnxn: MonorailConnection object.
names: List of component resource names.
services: Services object.
Returns:
List of (project ID, component ID)s in the same order as names.
Raises:
InputException if a resource name does not have a valid format.
NoSuchProjectException if no project exists with given id.
NoSuchComponentException if a component is not found.
"""
# Parse as many (component id or path, project name) pairs as possible.
parsed_comp_projectnames = []
with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
for name in names:
try:
match = _GetResourceNameMatch(name, COMPONENT_DEF_RE)
project_name = match.group('project_name')
component_id = match.group('component_id')
if component_id:
parsed_comp_projectnames.append((int(component_id), project_name))
else:
parsed_comp_projectnames.append(
(str(match.group('path')), project_name))
except exceptions.InputException as e:
err_agg.AddErrorMessage(e.message)
# Validate as many projects as possible.
project_names = {project_name for _, project_name in parsed_comp_projectnames}
project_ids_by_name = services.project.LookupProjectIDs(cnxn, project_names)
with exceptions.ErrorAggregator(exceptions.NoSuchProjectException) as err_agg:
for _, project_name in parsed_comp_projectnames:
if project_name not in project_ids_by_name:
err_agg.AddErrorMessage('Project not found: %s.' % project_name)
configs_by_pid = services.config.GetProjectConfigs(
cnxn, project_ids_by_name.values())
compid_by_pid = {}
comp_path_by_pid = {}
for pid, config in configs_by_pid.items():
compid_by_pid[pid] = {comp.component_id for comp in config.component_defs}
comp_path_by_pid[pid] = {
comp.path.lower(): comp.component_id for comp in config.component_defs
}
# Find as many components as possible
pid_cid_pairs = []
with exceptions.ErrorAggregator(
exceptions.NoSuchComponentException) as err_agg:
for comp, pname in parsed_comp_projectnames:
pid = project_ids_by_name[pname]
if isinstance(comp, int) and comp in compid_by_pid[pid]:
pid_cid_pairs.append((pid, comp))
elif isinstance(comp, str) and comp.lower() in comp_path_by_pid[pid]:
pid_cid_pairs.append((pid, comp_path_by_pid[pid][comp.lower()]))
else:
err_agg.AddErrorMessage('Component not found: %r.' % comp)
return pid_cid_pairs
def ConvertFieldDefNames(cnxn, field_ids, project_id, services):
# type: (MonorailConnection, Collection[int], int, Services) ->
# Mapping[int, str]
"""Takes Field IDs and returns FieldDef resource names.
Args:
cnxn: MonorailConnection object.
field_ids: List of Field IDs
project_id: project ID that each Field must belong to.
services: Services object.
Returns:
Dict of Field ID to FieldDef resource name for FieldDefs that are found.
Raises:
NoSuchProjectException if no project exists with given ID.
"""
project = services.project.GetProject(cnxn, project_id)
config = services.config.GetProjectConfig(cnxn, project_id)
fds_by_id = {fd.field_id: fd for fd in config.field_defs}
id_dict = {}
for field_id in field_ids:
field_def = fds_by_id.get(field_id)
if not field_def:
logging.info('Ignoring field referencing a non-existent id: %s', field_id)
continue
id_dict[field_id] = FIELD_DEF_TMPL.format(
project_name=project.project_name, field_id=field_id)
return id_dict
def ConvertApprovalDefNames(cnxn, approval_ids, project_id, services):
# type: (MonorailConnection, Collection[int], int, Services) ->
# Mapping[int, str]
"""Takes Approval IDs and returns ApprovalDef resource names.
Args:
cnxn: MonorailConnection object.
approval_ids: List of Approval IDs.
project_id: Project ID these approvals must belong to.
services: Services object.
Returns:
Dict of Approval ID to ApprovalDef resource name for ApprovalDefs
that are found.
Raises:
NoSuchProjectException if no project exists with given ID.
"""
project = services.project.GetProject(cnxn, project_id)
config = services.config.GetProjectConfig(cnxn, project_id)
fds_by_id = {fd.field_id: fd for fd in config.field_defs}
id_dict = {}
for approval_id in approval_ids:
approval_def = fds_by_id.get(approval_id)
if not approval_def:
logging.info(
'Ignoring approval referencing a non-existent id: %s', approval_id)
continue
id_dict[approval_id] = APPROVAL_DEF_TMPL.format(
project_name=project.project_name, approval_id=approval_id)
return id_dict
def ConvertProjectName(cnxn, project_id, services):
# type: (MonorailConnection, int, Services) -> str
"""Takes a Project ID and returns the Project's resource name.
Args:
cnxn: MonorailConnection object.
project_id: ID of the Project.
services: Services object.
Returns:
The resource name of the Project.
Raises:
NoSuchProjectException if no project exists with given id.
"""
project_name = services.project.LookupProjectNames(
cnxn, [project_id]).get(project_id)
return PROJECT_NAME_TMPL.format(project_name=project_name)
def ConvertProjectConfigName(cnxn, project_id, services):
# type: (MonorailConnection, int, Services) -> str
"""Takes a Project ID and returns that project's config resource name.
Args:
cnxn: MonorailConnection object.
project_id: ID of the Project.
services: Services object.
Returns:
The resource name of the ProjectConfig.
Raises:
NoSuchProjectException if no project exists with given id.
"""
project_name = services.project.LookupProjectNames(
cnxn, [project_id]).get(project_id)
return PROJECT_CONFIG_TMPL.format(project_name=project_name)
def ConvertProjectMemberName(cnxn, project_id, user_id, services):
# type: (MonorailConnection, int, int, Services) -> str
"""Takes Project and User ID then returns the ProjectMember resource name.
Args:
cnxn: MonorailConnection object.
project_id: ID of the Project.
user_id: ID of the User.
services: Services object.
Returns:
The resource name of the ProjectMember.
Raises:
NoSuchProjectException if no project exists with given id.
"""
project_name = services.project.LookupProjectNames(
cnxn, [project_id]).get(project_id)
return PROJECT_MEMBER_NAME_TMPL.format(
project_name=project_name, user_id=user_id)
def ConvertProjectSavedQueryNames(cnxn, query_ids, project_id, services):
# type: (MonorailConnection, Collection[int], int, Services) ->
# Mapping[int, str]
"""Takes SavedQuery IDs and returns ProjectSavedQuery resource names.
Args:
cnxn: MonorailConnection object.
query_ids: List of SavedQuery ids
project_id: project id of project this belongs to
services: Services object.
Returns:
Dict of ids to ProjectSavedQuery resource names for all found query ids
that belong to given project_id.
Raises:
NoSuchProjectException if no project exists with given id.
"""
project_name = services.project.LookupProjectNames(
cnxn, [project_id]).get(project_id)
all_project_queries = services.features.GetCannedQueriesByProjectID(
cnxn, project_id)
query_by_ids = {query.query_id: query for query in all_project_queries}
ids_to_names = {}
for query_id in query_ids:
query = query_by_ids.get(query_id)
if not query:
logging.info(
'Ignoring saved query referencing a non-existent id: %s '
'or not in project: %s', query_id, project_id)
continue
ids_to_names[query_id] = PROJECT_SQ_NAME_TMPL.format(
project_name=project_name, query_name=query.name)
return ids_to_names