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