blob: cb26c9bed5f0815a59e42949bf1bb43f8027655d [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2020 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style
3# license that can be found in the LICENSE file or at
4# https://developers.google.com/open-source/licenses/bsd
5
6"""Methods for converting resource names to protorpc objects and back.
7
8IngestFoo methods take resource names and return the IDs of the resources.
9While some Ingest methods need to check for the existence of resources as
10a side-effect of producing their IDs, other layers that call these methods
11should always do their own validity checking.
12
13ConvertFoo methods take object ids
14(and sometimes a MonorailConnection and ServiceManager)
15and return resource names.
16"""
17
18import re
19import logging
20
21from features import features_constants
22from framework import exceptions
23from framework import validate
24from project import project_constants
25from tracker import tracker_constants
26from proto import tracker_pb2
27
28# Constants that hold regex patterns for resource names.
29PROJECT_NAME_PATTERN = (
30 r'projects\/(?P<project_name>%s)' % project_constants.PROJECT_NAME_PATTERN)
31PROJECT_NAME_RE = re.compile(r'%s$' % PROJECT_NAME_PATTERN)
32
33FIELD_DEF_NAME_RE = re.compile(
34 r'%s\/fieldDefs\/(?P<field_def>\d+)$' % (PROJECT_NAME_PATTERN))
35
36APPROVAL_DEF_NAME_PATTERN = (
37 r'%s\/approvalDefs\/(?P<approval_def>\d+)' % PROJECT_NAME_PATTERN)
38APPROVAL_DEF_NAME_RE = re.compile(r'%s$' % APPROVAL_DEF_NAME_PATTERN)
39
40HOTLIST_PATTERN = r'hotlists\/(?P<hotlist_id>\d+)'
41HOTLIST_NAME_RE = re.compile(r'%s$' % HOTLIST_PATTERN)
42HOTLIST_ITEM_NAME_RE = re.compile(
43 r'%s\/items\/(?P<project_name>%s)\.(?P<local_id>\d+)$' % (
44 HOTLIST_PATTERN,
45 project_constants.PROJECT_NAME_PATTERN))
46
47ISSUE_PATTERN = (r'projects\/(?P<project>%s)\/issues\/(?P<local_id>\d+)' %
48 project_constants.PROJECT_NAME_PATTERN)
49ISSUE_NAME_RE = re.compile(r'%s$' % ISSUE_PATTERN)
50
51COMMENT_PATTERN = (r'%s\/comments\/(?P<comment_num>\d+)' % ISSUE_PATTERN)
52COMMENT_NAME_RE = re.compile(r'%s$' % COMMENT_PATTERN)
53
54USER_NAME_RE = re.compile(r'users\/((?P<user_id>\d+)|(?P<potential_email>.+))$')
55APPROVAL_VALUE_RE = re.compile(
56 r'%s\/approvalValues\/(?P<approval_id>\d+)$' % ISSUE_PATTERN)
57
58ISSUE_TEMPLATE_RE = re.compile(
59 r'%s\/templates\/(?P<template_id>\d+)$' % (PROJECT_NAME_PATTERN))
60
61# Constants that hold the template patterns for creating resource names.
62PROJECT_NAME_TMPL = 'projects/{project_name}'
63PROJECT_CONFIG_TMPL = 'projects/{project_name}/config'
64PROJECT_MEMBER_NAME_TMPL = 'projects/{project_name}/members/{user_id}'
65HOTLIST_NAME_TMPL = 'hotlists/{hotlist_id}'
66HOTLIST_ITEM_NAME_TMPL = '%s/items/{project_name}.{local_id}' % (
67 HOTLIST_NAME_TMPL)
68
69ISSUE_NAME_TMPL = 'projects/{project}/issues/{local_id}'
70COMMENT_NAME_TMPL = '%s/comments/{comment_id}' % ISSUE_NAME_TMPL
71APPROVAL_VALUE_NAME_TMPL = '%s/approvalValues/{approval_id}' % ISSUE_NAME_TMPL
72
73USER_NAME_TMPL = 'users/{user_id}'
74PROJECT_STAR_NAME_TMPL = 'users/{user_id}/projectStars/{project_name}'
75PROJECT_SQ_NAME_TMPL = 'projects/{project_name}/savedQueries/{query_name}'
76
77ISSUE_TEMPLATE_TMPL = 'projects/{project_name}/templates/{template_id}'
78STATUS_DEF_TMPL = 'projects/{project_name}/statusDefs/{status}'
79LABEL_DEF_TMPL = 'projects/{project_name}/labelDefs/{label}'
80COMPONENT_DEF_TMPL = 'projects/{project_name}/componentDefs/{component_id}'
81COMPONENT_DEF_RE = re.compile(
82 r'%s\/componentDefs\/((?P<component_id>\d+)|(?P<path>%s))$' %
83 (PROJECT_NAME_PATTERN, tracker_constants.COMPONENT_PATH_PATTERN))
84FIELD_DEF_TMPL = 'projects/{project_name}/fieldDefs/{field_id}'
85APPROVAL_DEF_TMPL = 'projects/{project_name}/approvalDefs/{approval_id}'
86
87
88def _GetResourceNameMatch(name, regex):
89 # type: (str, Pattern[str]) -> Match[str]
90 """Takes a resource name and returns the regex match.
91
92 Args:
93 name: Resource name.
94 regex: Compiled regular expression Pattern object used to match name.
95
96 Raises:
97 InputException if there is not match.
98 """
99 match = regex.match(name)
100 if not match:
101 raise exceptions.InputException(
102 'Invalid resource name: %s.' % name)
103 return match
104
105
106def _IssueIdsFromLocalIds(cnxn, project_local_id_pairs, services):
107 # type: (MonorailConnection, Sequence[Tuple(str, int)], Services ->
108 # Sequence[int]
109 """Fetches issue IDs using the given project/local ID pairs."""
110 # Fetch Project ids from Project names.
111 project_ids_by_name = services.project.LookupProjectIDs(
112 cnxn, [pair[0] for pair in project_local_id_pairs])
113
114 # Create (project_id, issue_local_id) pairs from project_local_id_pairs.
115 project_id_local_ids = []
116 with exceptions.ErrorAggregator(exceptions.NoSuchProjectException) as err_agg:
117 for project_name, local_id in project_local_id_pairs:
118 try:
119 project_id = project_ids_by_name[project_name]
120 project_id_local_ids.append((project_id, local_id))
121 except KeyError:
122 err_agg.AddErrorMessage('Project %s not found.' % project_name)
123
124 issue_ids, misses = services.issue.LookupIssueIDsFollowMoves(
125 cnxn, project_id_local_ids)
126 if misses:
127 # Raise error with resource names rather than backend IDs.
128 project_names_by_id = {
129 p_id: p_name for p_name, p_id in project_ids_by_name.iteritems()
130 }
131 misses_by_resource_name = [
132 _ConstructIssueName(project_names_by_id[p_id], local_id)
133 for (p_id, local_id) in misses
134 ]
135 raise exceptions.NoSuchIssueException(
136 'Issue(s) %r not found' % misses_by_resource_name)
137 return issue_ids
138
139# FieldDefs
140
141
142def IngestFieldDefName(cnxn, name, services):
143 # type: (MonorailConnection, str, Services) -> (int, int)
144 """Ingests a FieldDef's resource name.
145
146 Args:
147 cnxn: MonorailConnection to the database.
148 name: Resource name of a FieldDef.
149 services: Services object for connections to backend services.
150
151 Returns:
152 The Project's ID and the FieldDef's ID. FieldDef is not guaranteed to exist.
153 TODO(jessan): This order should be consistent throughout the file.
154
155 Raises:
156 InputException if the given name does not have a valid format.
157 NoSuchProjectException if the given project name does not exist.
158 """
159 match = _GetResourceNameMatch(name, FIELD_DEF_NAME_RE)
160 field_id = int(match.group('field_def'))
161 project_name = match.group('project_name')
162 id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
163 project_id = id_dict.get(project_name)
164 if project_id is None:
165 raise exceptions.NoSuchProjectException(
166 'Project not found: %s.' % project_name)
167
168 return project_id, field_id
169
170# Hotlists
171
172def IngestHotlistName(name):
173 # type: (str) -> int
174 """Takes a Hotlist resource name and returns the Hotlist ID.
175
176 Args:
177 name: Resource name of a Hotlist.
178
179 Returns:
180 The Hotlist's ID
181
182 Raises:
183 InputException if the given name does not have a valid format.
184 """
185 match = _GetResourceNameMatch(name, HOTLIST_NAME_RE)
186 return int(match.group('hotlist_id'))
187
188
189def IngestHotlistItemNames(cnxn, names, services):
190 # type: (MonorailConnection, Sequence[str], Services -> Sequence[int]
191 """Takes HotlistItem resource names and returns the associated Issues' IDs.
192
193 Args:
194 cnxn: MonorailConnection to the database.
195 names: List of HotlistItem resource names.
196 services: Services object for connections to backend services.
197
198 Returns:
199 List of Issue IDs associated with the given HotlistItems.
200
201 Raises:
202 InputException if a resource name does not have a valid format.
203 NoSuchProjectException if an Issue's Project is not found.
204 NoSuchIssueException if an Issue is not found.
205 """
206 project_local_id_pairs = []
207 for name in names:
208 match = _GetResourceNameMatch(name, HOTLIST_ITEM_NAME_RE)
209 project_local_id_pairs.append(
210 (match.group('project_name'), int(match.group('local_id'))))
211 return _IssueIdsFromLocalIds(cnxn, project_local_id_pairs, services)
212
213
214def ConvertHotlistName(hotlist_id):
215 # type: (int) -> str
216 """Takes a Hotlist and returns the Hotlist's resource name.
217
218 Args:
219 hotlist_id: ID of the Hotlist.
220
221 Returns:
222 The resource name of the Hotlist.
223 """
224 return HOTLIST_NAME_TMPL.format(hotlist_id=hotlist_id)
225
226
227def ConvertHotlistItemNames(cnxn, hotlist_id, issue_ids, services):
228 # type: (MonorailConnection, int, Collection[int], Services) ->
229 # Mapping[int, str]
230 """Takes a Hotlist ID and HotlistItem's issue_ids and returns
231 the Hotlist items' resource names.
232
233 Args:
234 cnxn: MonorailConnection object.
235 hotlist_id: ID of the Hotlist the items belong to.
236 issue_ids: List of Issue IDs that are part of the hotlist's items.
237 services: Services object for connections to backend services.
238
239 Returns:
240 Dict of Issue IDs to HotlistItem resource names for Issues that are found.
241 """
242 # {issue_id: (project_name, local_id),...}
243 issue_refs_dict = services.issue.LookupIssueRefs(cnxn, issue_ids)
244
245 issue_ids_to_names = {}
246 for issue_id in issue_ids:
247 project_name, local_id = issue_refs_dict.get(issue_id, (None, None))
248 if project_name and local_id:
249 issue_ids_to_names[issue_id] = HOTLIST_ITEM_NAME_TMPL.format(
250 hotlist_id=hotlist_id, project_name=project_name, local_id=local_id)
251
252 return issue_ids_to_names
253
254# Issues
255
256
257def IngestCommentName(cnxn, name, services):
258 # type: (MonorailConnection, str, Services) -> Tuple[int, int, int]
259 """Ingests a Comment's resource name.
260
261 Args:
262 cnxn: MonorailConnection to the database.
263 name: Resource name of a Comment.
264 services: Services object for connections to backend services.
265
266 Returns:
267 Tuple containing three items:
268 1. Global ID of the parent project.
269 2. Global Issue id of the parent issue.
270 3. Sequence number of the comment. This is not checked for existence.
271
272 Raises:
273 InputException if the given name does not have a valid format.
274 NoSuchIssueException if the parent Issue does not exist.
275 NoSuchProjectException if the parent Project does not exist.
276 """
277 match = _GetResourceNameMatch(name, COMMENT_NAME_RE)
278
279 # Project
280 project_name = match.group('project')
281 id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
282 project_id = id_dict.get(project_name)
283 if project_id is None:
284 raise exceptions.NoSuchProjectException(
285 'Project not found: %s.' % project_name)
286 # Issue
287 local_id = int(match.group('local_id'))
288 issue_pair = [(project_name, local_id)]
289 issue_id = _IssueIdsFromLocalIds(cnxn, issue_pair, services)[0]
290
291 return project_id, issue_id, int(match.group('comment_num'))
292
293
294def CreateCommentNames(issue_local_id, issue_project, comment_sequence_nums):
295 # type: (int, str, Sequence[int]) -> Mapping[int, str]
296 """Returns the resource names for the given comments.
297
298 Note: crbug.com/monorail/7507 has important context about guarantees required
299 for comment resource names to be permanent references.
300
301 Args:
302 issue_local_id: local id of the issue for which we're converting comments.
303 issue_project: the project of the issue for which we're converting comments.
304 comment_sequence_nums: sequence numbers of comments on the given issue.
305
306 Returns:
307 A mapping from comment sequence number to comment resource names.
308 """
309 sequence_nums_to_names = {}
310 for comment_sequence_num in comment_sequence_nums:
311 sequence_nums_to_names[comment_sequence_num] = COMMENT_NAME_TMPL.format(
312 project=issue_project,
313 local_id=issue_local_id,
314 comment_id=comment_sequence_num)
315 return sequence_nums_to_names
316
317def IngestApprovalDefName(cnxn, name, services):
318 # type: (MonorailConnection, str, Services) -> int
319 """Ingests an ApprovalDef's resource name.
320
321 Args:
322 cnxn: MonorailConnection to the database.
323 name: Resource name of an ApprovalDef.
324 services: Services object for connections to backend services.
325
326 Returns:
327 The ApprovalDef ID specified in `name`.
328 The ApprovalDef is not guaranteed to exist.
329
330 Raises:
331 InputException if the given name does not have a valid format.
332 NoSuchProjectException if the given project name does not exist.
333 """
334 match = _GetResourceNameMatch(name, APPROVAL_DEF_NAME_RE)
335
336 # Project
337 project_name = match.group('project_name')
338 id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
339 project_id = id_dict.get(project_name)
340 if project_id is None:
341 raise exceptions.NoSuchProjectException(
342 'Project not found: %s.' % project_name)
343
344 return int(match.group('approval_def'))
345
346def IngestApprovalValueName(cnxn, name, services):
347 # type: (MonorailConnection, str, Services) -> Tuple[int, int, int]
348 """Ingests the three components of an ApprovalValue resource name.
349
350 Args:
351 cnxn: MonorailConnection object.
352 name: Resource name of an ApprovalValue.
353 services: Services object for connections to backend services.
354
355 Returns:
356 Tuple containing three items
357 1. Global ID of the parent project.
358 2. Global Issue ID of the parent issue.
359 3. The approval_id portion of the resource name. This is not checked
360 for existence.
361
362 Raises:
363 InputException if the given name does not have a valid format.
364 NoSuchIssueException if the parent Issue does not exist.
365 NoSuchProjectException if the parent Project does not exist.
366 """
367 match = _GetResourceNameMatch(name, APPROVAL_VALUE_RE)
368
369 # Project
370 project_name = match.group('project')
371 id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
372 project_id = id_dict.get(project_name)
373 if project_id is None:
374 raise exceptions.NoSuchProjectException(
375 'Project not found: %s.' % project_name)
376 # Issue
377 local_id = int(match.group('local_id'))
378 issue_pair = [(project_name, local_id)]
379 issue_id = _IssueIdsFromLocalIds(cnxn, issue_pair, services)[0]
380
381 return project_id, issue_id, int(match.group('approval_id'))
382
383
384def IngestIssueName(cnxn, name, services):
385 # type: (MonorailConnection, str, Services) -> int
386 """Takes an Issue resource name and returns its global ID.
387
388 Args:
389 cnxn: MonorailConnection object.
390 name: Resource name of an Issue.
391 services: Services object for connections to backend services.
392
393 Returns:
394 The global Issue ID associated with the name.
395
396 Raises:
397 InputException if the given name does not have a valid format.
398 NoSuchIssueException if the Issue does not exist.
399 NoSuchProjectException if an Issue's Project is not found.
400
401 """
402 return IngestIssueNames(cnxn, [name], services)[0]
403
404
405def IngestIssueNames(cnxn, names, services):
406 # type: (MonorailConnection, Sequence[str], Services) -> Sequence[int]
407 """Returns global IDs for the given Issue resource names.
408
409 Args:
410 cnxn: MonorailConnection object.
411 names: Resource names of zero or more issues.
412 services: Services object for connections to backend services.
413
414 Returns:
415 The global IDs for the issues.
416
417 Raises:
418 InputException if a resource name does not have a valid format.
419 NoSuchIssueException if an Issue is not found.
420 NoSuchProjectException if an Issue's Project is not found.
421 """
422 project_local_id_pairs = []
423 with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
424 for name in names:
425 try:
426 match = _GetResourceNameMatch(name, ISSUE_NAME_RE)
427 project_local_id_pairs.append(
428 (match.group('project'), int(match.group('local_id'))))
429 except exceptions.InputException as e:
430 err_agg.AddErrorMessage(e.message)
431 return _IssueIdsFromLocalIds(cnxn, project_local_id_pairs, services)
432
433
434def IngestProjectFromIssue(issue_name):
435 # type: (str) -> str
436 """Takes an issue resource_name and returns its project name.
437
438 TODO(crbug/monorail/7614): This method should only be needed for the
439 workaround for the referenced issue. When the cleanup is completed, this
440 method should be able to be removed.
441
442 Args:
443 issue_name: A resource name for an issue.
444
445 Returns:
446 The project section of the resource name (e.g for 'projects/xyz/issue/1'),
447 the method would return 'xyz'. The associated project is not guaranteed to
448 exist.
449
450 Raises:
451 InputException if 'issue_name' does not have a valid format.
452 """
453 match = _GetResourceNameMatch(issue_name, ISSUE_NAME_RE)
454 return match.group('project')
455
456
457def ConvertIssueName(cnxn, issue_id, services):
458 # type: (MonorailConnection, int, Services) -> str
459 """Takes an Issue ID and returns the corresponding Issue resource name.
460
461 Args:
462 cnxn: MonorailConnection object.
463 issue_id: The ID of the issue.
464 services: Services object.
465
466 Returns:
467 The resource name of the Issue.
468
469 Raises:
470 NoSuchIssueException if the issue is not found.
471 """
472 name = ConvertIssueNames(cnxn, [issue_id], services).get(issue_id)
473 if not name:
474 raise exceptions.NoSuchIssueException()
475 return name
476
477
478def ConvertIssueNames(cnxn, issue_ids, services):
479 # type: (MonorailConnection, Collection[int], Services) -> Mapping[int, str]
480 """Takes Issue IDs and returns the Issue resource names.
481
482 Args:
483 cnxn: MonorailConnection object.
484 issue_ids: List of Issue IDs
485 services: Services object.
486
487 Returns:
488 Dict of Issue IDs to Issue resource names for Issues that are found.
489 """
490 issue_ids_to_names = {}
491 issue_refs_dict = services.issue.LookupIssueRefs(cnxn, issue_ids)
492 for issue_id in issue_ids:
493 project, local_id = issue_refs_dict.get(issue_id, (None, None))
494 if project and local_id:
495 issue_ids_to_names[issue_id] = _ConstructIssueName(project, local_id)
496 return issue_ids_to_names
497
498
499def _ConstructIssueName(project, local_id):
500 # type: (str, int) -> str
501 """Takes project name and issue local id returns the Issue resource name."""
502 return ISSUE_NAME_TMPL.format(project=project, local_id=local_id)
503
504
505def ConvertApprovalValueNames(cnxn, issue_id, services):
506 # type: (MonorailConnection, int, Services)
507 # -> Mapping[int, str]
508 """Takes an Issue ID and returns the resource names of its ApprovalValues.
509
510 Args:
511 cnxn: MonorailConnection object.
512 issue_id: ID of the Issue the approval_values belong to.
513 services: Services object.
514
515 Returns:
516 Dict of ApprovalDef IDs to ApprovalValue resource names for
517 ApprovalDefs that are found.
518
519 Raises:
520 NoSuchIssueException if the Issue is not found.
521 """
522 issue = services.issue.GetIssue(cnxn, issue_id)
523 project = services.project.GetProject(cnxn, issue.project_id)
524 config = services.config.GetProjectConfig(cnxn, issue.project_id)
525
526 ads_by_id = {fd.field_id: fd for fd in config.field_defs
527 if fd.field_type is tracker_pb2.FieldTypes.APPROVAL_TYPE}
528
529 approval_def_ids = [av.approval_id for av in issue.approval_values]
530 approval_ids_to_names = {}
531 for ad_id in approval_def_ids:
532 fd = ads_by_id.get(ad_id)
533 if not fd:
534 logging.info('Approval type field with id %d not found.', ad_id)
535 continue
536 approval_ids_to_names[ad_id] = APPROVAL_VALUE_NAME_TMPL.format(
537 project=project.project_name,
538 local_id=issue.local_id,
539 approval_id=ad_id)
540 return approval_ids_to_names
541
542# Users
543
544
545def IngestUserName(cnxn, name, services, autocreate=False):
546 # type: (MonorailConnection, str, Services) -> int
547 """Takes a User resource name and returns a User ID.
548
549 Args:
550 cnxn: MonorailConnection object.
551 name: The User resource name.
552 services: Services object.
553 autocreate: set to True if new Users should be created for
554 emails in resource names that do not belong to existing
555 Users.
556
557 Returns:
558 The ID of the User.
559
560 Raises:
561 InputException if the resource name does not have a valid format.
562 NoSuchUserException if autocreate is False and the given email
563 was not found.
564 """
565 match = _GetResourceNameMatch(name, USER_NAME_RE)
566 user_id = match.group('user_id')
567 if user_id:
568 return int(user_id)
569 elif validate.IsValidEmail(match.group('potential_email')):
570 return services.user.LookupUserID(
571 cnxn, match.group('potential_email'), autocreate=autocreate)
572 else:
573 raise exceptions.InputException(
574 'Invalid email format found in User resource name: %s' % name)
575
576
577def IngestUserNames(cnxn, names, services, autocreate=False):
578 # Type: (MonorailConnection, Sequence[str], Services, Optional[bool]) ->
579 # Sequence[int]
580 """Takes User resource names and returns the User IDs.
581
582 Args:
583 cnxn: MonorailConnection object.
584 names: List of User resource names.
585 services: Services object.
586 autocreate: set to True if new Users should be created for
587 emails in resource names that do not belong to existing
588 Users.
589
590 Returns:
591 List of User IDs in the same order as names.
592
593 Raises:
594 InputException if an resource name does not have a valid format.
595 NoSuchUserException if autocreate is False and some users with given
596 emails were not found.
597 """
598 ids = []
599 for name in names:
600 ids.append(IngestUserName(cnxn, name, services, autocreate))
601
602 return ids
603
604
605def ConvertUserName(user_id):
606 # type: (int) -> str
607 """Takes a User ID and returns the User's resource name."""
608 return ConvertUserNames([user_id])[user_id]
609
610
611def ConvertUserNames(user_ids):
612 # type: (Collection[int]) -> Mapping[int, str]
613 """Takes User IDs and returns the Users' resource names.
614
615 Args:
616 user_ids: List of User IDs.
617
618 Returns:
619 Dict of User IDs to User resource names for all given user_ids.
620 """
621 user_ids_to_names = {}
622 for user_id in user_ids:
623 user_ids_to_names[user_id] = USER_NAME_TMPL.format(user_id=user_id)
624
625 return user_ids_to_names
626
627
628def ConvertProjectStarName(cnxn, user_id, project_id, services):
629 # type: (MonorailConnection, int, int, Services) -> str
630 """Takes User ID and Project ID and returns the ProjectStar resource name.
631
632 Args:
633 user_id: User ID associated with the star.
634 project_id: ID of the starred project.
635
636 Returns:
637 The ProjectStar's name.
638
639 Raises:
640 NoSuchProjectException if the project_id is not found.
641 """
642 project_name = services.project.LookupProjectNames(
643 cnxn, [project_id]).get(project_id)
644
645 return PROJECT_STAR_NAME_TMPL.format(
646 user_id=user_id, project_name=project_name)
647
648# Projects
649
650
651def IngestProjectName(cnxn, name, services):
652 # type: (str) -> int
653 """Takes a Project resource name and returns the project id.
654
655 Args:
656 name: Resource name of a Project.
657
658 Returns:
659 The project's id
660
661 Raises:
662 InputException if the given name does not have a valid format.
663 NoSuchProjectException if no project exists with the given name.
664 """
665 match = _GetResourceNameMatch(name, PROJECT_NAME_RE)
666 project_name = match.group('project_name')
667
668 id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
669
670 return id_dict.get(project_name)
671
672
673def ConvertTemplateNames(cnxn, project_id, template_ids, services):
674 # type: (MonorailConnection, int, Collection[int] Services) ->
675 # Mapping[int, str]
676 """Takes Template IDs and returns the Templates' resource names
677
678 Args:
679 cnxn: MonorailConnection object.
680 project_id: Project ID of Project that Templates must belong to.
681 template_ids: Template IDs to convert.
682 services: Services object.
683
684 Returns:
685 Dict of template ID to template resource names for all found template IDs
686 within the given project.
687
688 Raises:
689 NoSuchProjectException if no project exists with given id.
690 """
691 id_to_resource_names = {}
692
693 project_name = services.project.LookupProjectNames(
694 cnxn, [project_id]).get(project_id)
695 project_templates = services.template.GetProjectTemplates(cnxn, project_id)
696 tmpl_by_id = {tmpl.template_id: tmpl for tmpl in project_templates}
697
698 for template_id in template_ids:
699 if template_id not in tmpl_by_id:
700 logging.info(
701 'Ignoring template referencing a non-existent id: %s, ' \
702 'or not in project: %s', template_id, project_id)
703 continue
704 id_to_resource_names[template_id] = ISSUE_TEMPLATE_TMPL.format(
705 project_name=project_name,
706 template_id=template_id)
707
708 return id_to_resource_names
709
710
711def IngestTemplateName(cnxn, name, services):
712 # type: (MonorailConnection, str, Services) -> Tuple[int, int]
713 """Ingests an IssueTemplate resource name.
714
715 Args:
716 cnxn: MonorailConnection object.
717 name: Resource name of an IssueTemplate.
718 services: Services object.
719
720 Returns:
721 The IssueTemplate's ID and the Project's ID.
722
723 Raises:
724 InputException if the given name does not have a valid format.
725 NoSuchProjectException if the given project name does not exist.
726 """
727 match = _GetResourceNameMatch(name, ISSUE_TEMPLATE_RE)
728 template_id = int(match.group('template_id'))
729 project_name = match.group('project_name')
730
731 id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
732 project_id = id_dict.get(project_name)
733 if project_id is None:
734 raise exceptions.NoSuchProjectException(
735 'Project not found: %s.' % project_name)
736 return template_id, project_id
737
738
739def ConvertStatusDefNames(cnxn, statuses, project_id, services):
740 # type: (MonorailConnection, Collection[str], int, Services) ->
741 # Mapping[str, str]
742 """Takes list of status strings and returns StatusDef resource names
743
744 Args:
745 cnxn: MonorailConnection object.
746 statuses: List of status name strings
747 project_id: project id of project this belongs to
748 services: Services object.
749
750 Returns:
751 Mapping of string to resource name for all given `statuses`.
752
753 Raises:
754 NoSuchProjectException if no project exists with given id.
755 """
756 project = services.project.GetProject(cnxn, project_id)
757
758 name_dict = {}
759 for status in statuses:
760 name_dict[status] = STATUS_DEF_TMPL.format(
761 project_name=project.project_name, status=status)
762
763 return name_dict
764
765
766def ConvertLabelDefNames(cnxn, labels, project_id, services):
767 # type: (MonorailConnection, Collection[str], int, Services) ->
768 # Mapping[str, str]
769 """Takes a list of labels and returns LabelDef resource names
770
771 Args:
772 cnxn: MonorailConnection object.
773 labels: List of labels as string
774 project_id: project id of project this belongs to
775 services: Services object.
776
777 Returns:
778 Dict of label string to label's resource name for all given `labels`.
779
780 Raises:
781 NoSuchProjectException if no project exists with given id.
782 """
783 project = services.project.GetProject(cnxn, project_id)
784
785 name_dict = {}
786
787 for label in labels:
788 name_dict[label] = LABEL_DEF_TMPL.format(
789 project_name=project.project_name, label=label)
790
791 return name_dict
792
793
794def ConvertComponentDefNames(cnxn, component_ids, project_id, services):
795 # type: (MonorailConnection, Collection[int], int, Services) ->
796 # Mapping[int, str]
797 """Takes Component IDs and returns ComponentDef resource names
798
799 Args:
800 cnxn: MonorailConnection object.
801 component_ids: List of component ids
802 project_id: project id of project this belongs to
803 services: Services object.
804
805 Returns:
806 Dict of component ID to component's resource name for all given
807 `component_ids`
808
809 Raises:
810 NoSuchProjectException if no project exists with given id.
811 """
812 project = services.project.GetProject(cnxn, project_id)
813
814 id_dict = {}
815
816 for component_id in component_ids:
817 id_dict[component_id] = COMPONENT_DEF_TMPL.format(
818 project_name=project.project_name, component_id=component_id)
819
820 return id_dict
821
822
823def IngestComponentDefNames(cnxn, names, services):
824 # type: (MonorailConnection, Sequence[str], Services)
825 # -> Sequence[Tuple[int, int]]
826 """Takes a list of component resource names and returns their IDs.
827
828 Args:
829 cnxn: MonorailConnection object.
830 names: List of component resource names.
831 services: Services object.
832
833 Returns:
834 List of (project ID, component ID)s in the same order as names.
835
836 Raises:
837 InputException if a resource name does not have a valid format.
838 NoSuchProjectException if no project exists with given id.
839 NoSuchComponentException if a component is not found.
840 """
841 # Parse as many (component id or path, project name) pairs as possible.
842 parsed_comp_projectnames = []
843 with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
844 for name in names:
845 try:
846 match = _GetResourceNameMatch(name, COMPONENT_DEF_RE)
847 project_name = match.group('project_name')
848 component_id = match.group('component_id')
849 if component_id:
850 parsed_comp_projectnames.append((int(component_id), project_name))
851 else:
852 parsed_comp_projectnames.append(
853 (str(match.group('path')), project_name))
854 except exceptions.InputException as e:
855 err_agg.AddErrorMessage(e.message)
856
857 # Validate as many projects as possible.
858 project_names = {project_name for _, project_name in parsed_comp_projectnames}
859 project_ids_by_name = services.project.LookupProjectIDs(cnxn, project_names)
860 with exceptions.ErrorAggregator(exceptions.NoSuchProjectException) as err_agg:
861 for _, project_name in parsed_comp_projectnames:
862 if project_name not in project_ids_by_name:
863 err_agg.AddErrorMessage('Project not found: %s.' % project_name)
864
865 configs_by_pid = services.config.GetProjectConfigs(
866 cnxn, project_ids_by_name.values())
867 compid_by_pid = {}
868 comp_path_by_pid = {}
869 for pid, config in configs_by_pid.items():
870 compid_by_pid[pid] = {comp.component_id for comp in config.component_defs}
871 comp_path_by_pid[pid] = {
872 comp.path.lower(): comp.component_id for comp in config.component_defs
873 }
874
875 # Find as many components as possible
876 pid_cid_pairs = []
877 with exceptions.ErrorAggregator(
878 exceptions.NoSuchComponentException) as err_agg:
879 for comp, pname in parsed_comp_projectnames:
880 pid = project_ids_by_name[pname]
881 if isinstance(comp, int) and comp in compid_by_pid[pid]:
882 pid_cid_pairs.append((pid, comp))
883 elif isinstance(comp, str) and comp.lower() in comp_path_by_pid[pid]:
884 pid_cid_pairs.append((pid, comp_path_by_pid[pid][comp.lower()]))
885 else:
886 err_agg.AddErrorMessage('Component not found: %r.' % comp)
887
888 return pid_cid_pairs
889
890
891def ConvertFieldDefNames(cnxn, field_ids, project_id, services):
892 # type: (MonorailConnection, Collection[int], int, Services) ->
893 # Mapping[int, str]
894 """Takes Field IDs and returns FieldDef resource names.
895
896 Args:
897 cnxn: MonorailConnection object.
898 field_ids: List of Field IDs
899 project_id: project ID that each Field must belong to.
900 services: Services object.
901
902 Returns:
903 Dict of Field ID to FieldDef resource name for FieldDefs that are found.
904
905 Raises:
906 NoSuchProjectException if no project exists with given ID.
907 """
908 project = services.project.GetProject(cnxn, project_id)
909 config = services.config.GetProjectConfig(cnxn, project_id)
910
911 fds_by_id = {fd.field_id: fd for fd in config.field_defs}
912
913 id_dict = {}
914
915 for field_id in field_ids:
916 field_def = fds_by_id.get(field_id)
917 if not field_def:
918 logging.info('Ignoring field referencing a non-existent id: %s', field_id)
919 continue
920 id_dict[field_id] = FIELD_DEF_TMPL.format(
921 project_name=project.project_name, field_id=field_id)
922
923 return id_dict
924
925
926def ConvertApprovalDefNames(cnxn, approval_ids, project_id, services):
927 # type: (MonorailConnection, Collection[int], int, Services) ->
928 # Mapping[int, str]
929 """Takes Approval IDs and returns ApprovalDef resource names.
930
931 Args:
932 cnxn: MonorailConnection object.
933 approval_ids: List of Approval IDs.
934 project_id: Project ID these approvals must belong to.
935 services: Services object.
936
937 Returns:
938 Dict of Approval ID to ApprovalDef resource name for ApprovalDefs
939 that are found.
940
941 Raises:
942 NoSuchProjectException if no project exists with given ID.
943 """
944 project = services.project.GetProject(cnxn, project_id)
945 config = services.config.GetProjectConfig(cnxn, project_id)
946
947 fds_by_id = {fd.field_id: fd for fd in config.field_defs}
948
949 id_dict = {}
950
951 for approval_id in approval_ids:
952 approval_def = fds_by_id.get(approval_id)
953 if not approval_def:
954 logging.info(
955 'Ignoring approval referencing a non-existent id: %s', approval_id)
956 continue
957 id_dict[approval_id] = APPROVAL_DEF_TMPL.format(
958 project_name=project.project_name, approval_id=approval_id)
959
960 return id_dict
961
962
963def ConvertProjectName(cnxn, project_id, services):
964 # type: (MonorailConnection, int, Services) -> str
965 """Takes a Project ID and returns the Project's resource name.
966
967 Args:
968 cnxn: MonorailConnection object.
969 project_id: ID of the Project.
970 services: Services object.
971
972 Returns:
973 The resource name of the Project.
974
975 Raises:
976 NoSuchProjectException if no project exists with given id.
977 """
978 project_name = services.project.LookupProjectNames(
979 cnxn, [project_id]).get(project_id)
980 return PROJECT_NAME_TMPL.format(project_name=project_name)
981
982
983def ConvertProjectConfigName(cnxn, project_id, services):
984 # type: (MonorailConnection, int, Services) -> str
985 """Takes a Project ID and returns that project's config resource name.
986
987 Args:
988 cnxn: MonorailConnection object.
989 project_id: ID of the Project.
990 services: Services object.
991
992 Returns:
993 The resource name of the ProjectConfig.
994
995 Raises:
996 NoSuchProjectException if no project exists with given id.
997 """
998 project_name = services.project.LookupProjectNames(
999 cnxn, [project_id]).get(project_id)
1000 return PROJECT_CONFIG_TMPL.format(project_name=project_name)
1001
1002
1003def ConvertProjectMemberName(cnxn, project_id, user_id, services):
1004 # type: (MonorailConnection, int, int, Services) -> str
1005 """Takes Project and User ID then returns the ProjectMember resource name.
1006
1007 Args:
1008 cnxn: MonorailConnection object.
1009 project_id: ID of the Project.
1010 user_id: ID of the User.
1011 services: Services object.
1012
1013 Returns:
1014 The resource name of the ProjectMember.
1015
1016 Raises:
1017 NoSuchProjectException if no project exists with given id.
1018 """
1019 project_name = services.project.LookupProjectNames(
1020 cnxn, [project_id]).get(project_id)
1021
1022 return PROJECT_MEMBER_NAME_TMPL.format(
1023 project_name=project_name, user_id=user_id)
1024
1025
1026def ConvertProjectSavedQueryNames(cnxn, query_ids, project_id, services):
1027 # type: (MonorailConnection, Collection[int], int, Services) ->
1028 # Mapping[int, str]
1029 """Takes SavedQuery IDs and returns ProjectSavedQuery resource names.
1030
1031 Args:
1032 cnxn: MonorailConnection object.
1033 query_ids: List of SavedQuery ids
1034 project_id: project id of project this belongs to
1035 services: Services object.
1036
1037 Returns:
1038 Dict of ids to ProjectSavedQuery resource names for all found query ids
1039 that belong to given project_id.
1040
1041 Raises:
1042 NoSuchProjectException if no project exists with given id.
1043 """
1044 project_name = services.project.LookupProjectNames(
1045 cnxn, [project_id]).get(project_id)
1046 all_project_queries = services.features.GetCannedQueriesByProjectID(
1047 cnxn, project_id)
1048 query_by_ids = {query.query_id: query for query in all_project_queries}
1049 ids_to_names = {}
1050 for query_id in query_ids:
1051 query = query_by_ids.get(query_id)
1052 if not query:
1053 logging.info(
1054 'Ignoring saved query referencing a non-existent id: %s '
1055 'or not in project: %s', query_id, project_id)
1056 continue
1057 ids_to_names[query_id] = PROJECT_SQ_NAME_TMPL.format(
1058 project_name=project_name, query_name=query.name)
1059 return ids_to_names