Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/api/resource_name_converters.py b/api/resource_name_converters.py
new file mode 100644
index 0000000..cb26c9b
--- /dev/null
+++ b/api/resource_name_converters.py
@@ -0,0 +1,1059 @@
+# 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