qontract-reconcile 0.10.2.dev135__py3-none-any.whl → 0.10.2.dev136__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {qontract_reconcile-0.10.2.dev135.dist-info → qontract_reconcile-0.10.2.dev136.dist-info}/METADATA +1 -1
  2. {qontract_reconcile-0.10.2.dev135.dist-info → qontract_reconcile-0.10.2.dev136.dist-info}/RECORD +35 -29
  3. reconcile/aws_ami_share.py +4 -2
  4. reconcile/aws_ecr_image_pull_secrets.py +15 -7
  5. reconcile/aws_garbage_collector.py +1 -1
  6. reconcile/aws_iam_keys.py +24 -17
  7. reconcile/aws_iam_password_reset.py +4 -2
  8. reconcile/aws_support_cases_sos.py +19 -6
  9. reconcile/closedbox_endpoint_monitoring_base.py +4 -3
  10. reconcile/cna/client.py +3 -3
  11. reconcile/cna/integration.py +4 -5
  12. reconcile/cna/state.py +3 -3
  13. reconcile/email_sender.py +65 -56
  14. reconcile/gcr_mirror.py +11 -9
  15. reconcile/github_org.py +57 -52
  16. reconcile/github_owners.py +8 -5
  17. reconcile/github_repo_invites.py +1 -1
  18. reconcile/github_repo_permissions_validator.py +3 -3
  19. reconcile/github_users.py +16 -12
  20. reconcile/github_validator.py +1 -1
  21. reconcile/gitlab_fork_compliance.py +9 -8
  22. reconcile/gitlab_labeler.py +1 -1
  23. reconcile/gitlab_mr_sqs_consumer.py +6 -3
  24. reconcile/gitlab_owners.py +29 -12
  25. reconcile/gql_definitions/email_sender/__init__.py +0 -0
  26. reconcile/gql_definitions/email_sender/apps.py +64 -0
  27. reconcile/gql_definitions/email_sender/emails.py +133 -0
  28. reconcile/gql_definitions/email_sender/users.py +62 -0
  29. reconcile/gql_definitions/fragments/email_service.py +32 -0
  30. reconcile/gql_definitions/fragments/email_user.py +28 -0
  31. reconcile/queries.py +0 -44
  32. reconcile/utils/jinja2/utils.py +4 -2
  33. reconcile/utils/smtp_client.py +1 -1
  34. {qontract_reconcile-0.10.2.dev135.dist-info → qontract_reconcile-0.10.2.dev136.dist-info}/WHEEL +0 -0
  35. {qontract_reconcile-0.10.2.dev135.dist-info → qontract_reconcile-0.10.2.dev136.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import sys
3
+ from typing import Any, Self
3
4
 
4
5
  from gitlab import GitlabGetError
5
6
  from gitlab.const import MAINTAINER_ACCESS
@@ -36,7 +37,7 @@ class GitlabForkCompliance:
36
37
  ERR_NOT_A_MEMBER = 0x0002
37
38
  ERR_NOT_A_MAINTAINER = 0x0004
38
39
 
39
- def __init__(self, project_id, mr_id, maintainers_group):
40
+ def __init__(self, project_id: str, mr_id: str, maintainers_group: str) -> None:
40
41
  self.exit_code = self.OK
41
42
 
42
43
  self.maintainers_group = maintainers_group
@@ -50,15 +51,15 @@ class GitlabForkCompliance:
50
51
  )
51
52
  self.mr = self.gl_cli.get_merge_request(mr_id)
52
53
 
53
- def __enter__(self):
54
+ def __enter__(self) -> Self:
54
55
  return self
55
56
 
56
- def __exit__(self, *exc):
57
+ def __exit__(self, *exc: Any) -> None:
57
58
  self.gl_cli.cleanup()
58
59
  if hasattr(self, "src") and self.src is not None:
59
60
  self.src.cleanup()
60
61
 
61
- def run(self):
62
+ def run(self) -> None:
62
63
  self.exit_code |= self.check_branch()
63
64
  self.exit_code |= self.check_bot_access()
64
65
  if self.exit_code:
@@ -86,7 +87,7 @@ class GitlabForkCompliance:
86
87
 
87
88
  sys.exit(self.exit_code)
88
89
 
89
- def check_branch(self):
90
+ def check_branch(self) -> int:
90
91
  # The Merge Request can use the 'master' source branch
91
92
  if self.mr.source_branch == "master":
92
93
  self.handle_error("source branch can not be master", MSG_BRANCH)
@@ -94,7 +95,7 @@ class GitlabForkCompliance:
94
95
 
95
96
  return self.OK
96
97
 
97
- def check_bot_access(self):
98
+ def check_bot_access(self) -> int:
98
99
  try:
99
100
  self.src = GitLabApi(
100
101
  self.instance,
@@ -113,7 +114,7 @@ class GitlabForkCompliance:
113
114
 
114
115
  return self.OK
115
116
 
116
- def handle_error(self, log_msg, mr_msg):
117
+ def handle_error(self, log_msg: str, mr_msg: str) -> None:
117
118
  LOG.error([log_msg.format(bot=self.gl_cli.user.username)])
118
119
  self.gl_cli.add_label_to_merge_request(self.mr, BLOCKED_BOT_ACCESS)
119
120
  comment = mr_msg.format(
@@ -124,6 +125,6 @@ class GitlabForkCompliance:
124
125
  self.mr.notes.create({"body": comment})
125
126
 
126
127
 
127
- def run(dry_run, project_id, mr_id, maintainers_group):
128
+ def run(dry_run: bool, project_id: str, mr_id: str, maintainers_group: str) -> None:
128
129
  with GitlabForkCompliance(project_id, mr_id, maintainers_group) as gfc:
129
130
  gfc.run()
@@ -122,7 +122,7 @@ def guess_labels(
122
122
  return guesses
123
123
 
124
124
 
125
- def run(dry_run, gitlab_project_id=None, gitlab_merge_request_id=None) -> None:
125
+ def run(dry_run: bool, gitlab_project_id: str, gitlab_merge_request_id: str) -> None:
126
126
  instance = queries.get_gitlab_instance()
127
127
  settings = queries.get_app_interface_settings()
128
128
  with GitLabApi(instance, project_id=gitlab_project_id, settings=settings) as gl:
@@ -5,6 +5,7 @@ SQS Consumer to create Gitlab merge requests.
5
5
  import json
6
6
  import logging
7
7
  import sys
8
+ from collections.abc import Callable
8
9
 
9
10
  from reconcile import queries
10
11
  from reconcile.utils import mr
@@ -17,18 +18,20 @@ QONTRACT_INTEGRATION = "gitlab-mr-sqs-consumer"
17
18
 
18
19
 
19
20
  @defer
20
- def run(dry_run, gitlab_project_id, defer=None):
21
+ def run(dry_run: str, gitlab_project_id: str, defer: Callable | None = None) -> None:
21
22
  secret_reader = SecretReader(queries.get_secret_reader_settings())
22
23
 
23
24
  accounts = queries.get_queue_aws_accounts()
24
25
  sqs_cli = SQSGateway(accounts, secret_reader)
25
- defer(sqs_cli.cleanup)
26
+ if defer:
27
+ defer(sqs_cli.cleanup)
26
28
 
27
29
  instance = queries.get_gitlab_instance()
28
30
  gitlab_cli = GitLabApi(
29
31
  instance, project_id=gitlab_project_id, secret_reader=secret_reader
30
32
  )
31
- defer(gitlab_cli.cleanup)
33
+ if defer:
34
+ defer(gitlab_cli.cleanup)
32
35
 
33
36
  errors_occured = False
34
37
  while True:
@@ -1,6 +1,9 @@
1
1
  import logging
2
+ from collections.abc import Callable, Mapping
3
+ from typing import Any
2
4
 
3
5
  from dateutil import parser as dateparser
6
+ from gitlab.v4.objects import ProjectMergeRequest
4
7
  from sretoolbox.utils import threaded
5
8
 
6
9
  from reconcile import queries
@@ -31,7 +34,14 @@ class MRApproval:
31
34
  between the approval messages the the project owners.
32
35
  """
33
36
 
34
- def __init__(self, gitlab_client, merge_request, owners, dry_run, persistent_lgtm):
37
+ def __init__(
38
+ self,
39
+ gitlab_client: GitLabApi,
40
+ merge_request: ProjectMergeRequest,
41
+ owners: RepoOwners,
42
+ dry_run: bool,
43
+ persistent_lgtm: bool,
44
+ ) -> None:
35
45
  self.gitlab = gitlab_client
36
46
  self.mr = merge_request
37
47
  self.owners = owners
@@ -45,7 +55,7 @@ class MRApproval:
45
55
  top_commit = next(commits)
46
56
  self.top_commit_created_at = dateparser.parse(top_commit.created_at)
47
57
 
48
- def get_change_owners_map(self):
58
+ def get_change_owners_map(self) -> dict[str, dict[str, list[str]]]:
49
59
  """
50
60
  Maps each change path to the list of owners that can approve
51
61
  that change.
@@ -71,7 +81,7 @@ class MRApproval:
71
81
  }
72
82
  return change_owners_map
73
83
 
74
- def get_lgtms(self):
84
+ def get_lgtms(self) -> list[str]:
75
85
  """
76
86
  Collects the usernames of all the '/lgtm' comments.
77
87
  """
@@ -101,8 +111,8 @@ class MRApproval:
101
111
  members.update(m.username for m in self.gitlab.get_group_members(group))
102
112
  return members.union(owners)
103
113
 
104
- def get_approval_status(self):
105
- approval_status = {"approved": False, "report": None}
114
+ def get_approval_status(self) -> dict[str, Any]:
115
+ approval_status: dict[str, Any] = {"approved": False, "report": None}
106
116
 
107
117
  try:
108
118
  change_owners_map = self.get_change_owners_map()
@@ -114,7 +124,7 @@ class MRApproval:
114
124
  if not change_owners_map:
115
125
  return approval_status
116
126
 
117
- report = {}
127
+ report: dict[str, Any] = {}
118
128
  lgtms = self.get_lgtms()
119
129
 
120
130
  approval_status["approved"] = True
@@ -200,18 +210,18 @@ class MRApproval:
200
210
  approval_status["report"] = formatted_report
201
211
  return approval_status
202
212
 
203
- def has_approval_label(self):
213
+ def has_approval_label(self) -> bool:
204
214
  return APPROVED in self.mr.labels
205
215
 
206
216
  @staticmethod
207
- def format_report(report):
217
+ def format_report(report: Mapping[str, Any]) -> str:
208
218
  """
209
219
  Gets a report dictionary and creates the corresponding Markdown
210
220
  comment message.
211
221
  """
212
222
  markdown_report = ""
213
223
 
214
- closest_approvers = []
224
+ closest_approvers: list[list[str]] = []
215
225
  for owners in report.values():
216
226
  new_group = []
217
227
 
@@ -323,9 +333,16 @@ class MRApproval:
323
333
 
324
334
 
325
335
  @defer
326
- def act(repo, dry_run, instance, settings, defer=None):
336
+ def act(
337
+ repo: Mapping,
338
+ dry_run: bool,
339
+ instance: Mapping,
340
+ settings: Mapping,
341
+ defer: Callable | None = None,
342
+ ) -> None:
327
343
  gitlab_cli = GitLabApi(instance, project_url=repo["url"], settings=settings)
328
- defer(gitlab_cli.cleanup)
344
+ if defer:
345
+ defer(gitlab_cli.cleanup)
329
346
  project_owners = RepoOwners(
330
347
  git_cli=gitlab_cli, ref=gitlab_cli.project.default_branch
331
348
  )
@@ -392,7 +409,7 @@ def act(repo, dry_run, instance, settings, defer=None):
392
409
  ])
393
410
 
394
411
 
395
- def run(dry_run, thread_pool_size=10):
412
+ def run(dry_run: bool, thread_pool_size: int = 10) -> None:
396
413
  instance = queries.get_gitlab_instance()
397
414
  settings = queries.get_app_interface_settings()
398
415
  repos = queries.get_repos_gitlab_owner(server=instance["url"])
File without changes
@@ -0,0 +1,64 @@
1
+ """
2
+ Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
+ """
4
+ from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
+ from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
+ from enum import Enum # noqa: F401 # pylint: disable=W0611
7
+ from typing import ( # noqa: F401 # pylint: disable=W0611
8
+ Any,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
+ BaseModel,
15
+ Extra,
16
+ Field,
17
+ Json,
18
+ )
19
+
20
+ from reconcile.gql_definitions.fragments.email_service import EmailServiceOwners
21
+
22
+
23
+ DEFINITION = """
24
+ fragment EmailServiceOwners on App_v1 {
25
+ serviceOwners {
26
+ email
27
+ }
28
+ }
29
+
30
+ query EmailApps {
31
+ apps: apps_v1 {
32
+ ...EmailServiceOwners
33
+ }
34
+ }
35
+ """
36
+
37
+
38
+ class ConfiguredBaseModel(BaseModel):
39
+ class Config:
40
+ smart_union=True
41
+ extra=Extra.forbid
42
+
43
+
44
+ class EmailAppsQueryData(ConfiguredBaseModel):
45
+ apps: Optional[list[EmailServiceOwners]] = Field(..., alias="apps")
46
+
47
+
48
+ def query(query_func: Callable, **kwargs: Any) -> EmailAppsQueryData:
49
+ """
50
+ This is a convenience function which queries and parses the data into
51
+ concrete types. It should be compatible with most GQL clients.
52
+ You do not have to use it to consume the generated data classes.
53
+ Alternatively, you can also mime and alternate the behavior
54
+ of this function in the caller.
55
+
56
+ Parameters:
57
+ query_func (Callable): Function which queries your GQL Server
58
+ kwargs: optional arguments that will be passed to the query function
59
+
60
+ Returns:
61
+ EmailAppsQueryData: queried data parsed into generated classes
62
+ """
63
+ raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
64
+ return EmailAppsQueryData(**raw_data)
@@ -0,0 +1,133 @@
1
+ """
2
+ Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
+ """
4
+ from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
+ from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
+ from enum import Enum # noqa: F401 # pylint: disable=W0611
7
+ from typing import ( # noqa: F401 # pylint: disable=W0611
8
+ Any,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
+ BaseModel,
15
+ Extra,
16
+ Field,
17
+ Json,
18
+ )
19
+
20
+ from reconcile.gql_definitions.fragments.email_service import EmailServiceOwners
21
+ from reconcile.gql_definitions.fragments.email_user import EmailUser
22
+
23
+
24
+ DEFINITION = """
25
+ fragment EmailServiceOwners on App_v1 {
26
+ serviceOwners {
27
+ email
28
+ }
29
+ }
30
+
31
+ fragment EmailUser on User_v1 {
32
+ org_username
33
+ }
34
+
35
+ query Emails {
36
+ emails: app_interface_emails_v1 {
37
+ name
38
+ subject
39
+ to {
40
+ aliases
41
+ services {
42
+ ... EmailServiceOwners
43
+ }
44
+ clusters {
45
+ name
46
+ }
47
+ namespaces {
48
+ name
49
+ }
50
+ aws_accounts {
51
+ accountOwners {
52
+ email
53
+ }
54
+ }
55
+ roles {
56
+ users {
57
+ ... EmailUser
58
+ }
59
+ }
60
+ users {
61
+ ... EmailUser
62
+ }
63
+ }
64
+ body
65
+ }
66
+ }
67
+ """
68
+
69
+
70
+ class ConfiguredBaseModel(BaseModel):
71
+ class Config:
72
+ smart_union=True
73
+ extra=Extra.forbid
74
+
75
+
76
+ class ClusterV1(ConfiguredBaseModel):
77
+ name: str = Field(..., alias="name")
78
+
79
+
80
+ class NamespaceV1(ConfiguredBaseModel):
81
+ name: str = Field(..., alias="name")
82
+
83
+
84
+ class OwnerV1(ConfiguredBaseModel):
85
+ email: str = Field(..., alias="email")
86
+
87
+
88
+ class AWSAccountV1(ConfiguredBaseModel):
89
+ account_owners: list[OwnerV1] = Field(..., alias="accountOwners")
90
+
91
+
92
+ class RoleV1(ConfiguredBaseModel):
93
+ users: list[EmailUser] = Field(..., alias="users")
94
+
95
+
96
+ class AppInterfaceEmailAudienceV1(ConfiguredBaseModel):
97
+ aliases: Optional[list[str]] = Field(..., alias="aliases")
98
+ services: Optional[list[EmailServiceOwners]] = Field(..., alias="services")
99
+ clusters: Optional[list[ClusterV1]] = Field(..., alias="clusters")
100
+ namespaces: Optional[list[NamespaceV1]] = Field(..., alias="namespaces")
101
+ aws_accounts: Optional[list[AWSAccountV1]] = Field(..., alias="aws_accounts")
102
+ roles: Optional[list[RoleV1]] = Field(..., alias="roles")
103
+ users: Optional[list[EmailUser]] = Field(..., alias="users")
104
+
105
+
106
+ class AppInterfaceEmailV1(ConfiguredBaseModel):
107
+ name: str = Field(..., alias="name")
108
+ subject: str = Field(..., alias="subject")
109
+ q_to: AppInterfaceEmailAudienceV1 = Field(..., alias="to")
110
+ body: str = Field(..., alias="body")
111
+
112
+
113
+ class EmailsQueryData(ConfiguredBaseModel):
114
+ emails: Optional[list[AppInterfaceEmailV1]] = Field(..., alias="emails")
115
+
116
+
117
+ def query(query_func: Callable, **kwargs: Any) -> EmailsQueryData:
118
+ """
119
+ This is a convenience function which queries and parses the data into
120
+ concrete types. It should be compatible with most GQL clients.
121
+ You do not have to use it to consume the generated data classes.
122
+ Alternatively, you can also mime and alternate the behavior
123
+ of this function in the caller.
124
+
125
+ Parameters:
126
+ query_func (Callable): Function which queries your GQL Server
127
+ kwargs: optional arguments that will be passed to the query function
128
+
129
+ Returns:
130
+ EmailsQueryData: queried data parsed into generated classes
131
+ """
132
+ raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
133
+ return EmailsQueryData(**raw_data)
@@ -0,0 +1,62 @@
1
+ """
2
+ Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
+ """
4
+ from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
+ from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
+ from enum import Enum # noqa: F401 # pylint: disable=W0611
7
+ from typing import ( # noqa: F401 # pylint: disable=W0611
8
+ Any,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
+ BaseModel,
15
+ Extra,
16
+ Field,
17
+ Json,
18
+ )
19
+
20
+ from reconcile.gql_definitions.fragments.email_user import EmailUser
21
+
22
+
23
+ DEFINITION = """
24
+ fragment EmailUser on User_v1 {
25
+ org_username
26
+ }
27
+
28
+ query EmailUsers {
29
+ users: users_v1 {
30
+ ... EmailUser
31
+ }
32
+ }
33
+ """
34
+
35
+
36
+ class ConfiguredBaseModel(BaseModel):
37
+ class Config:
38
+ smart_union=True
39
+ extra=Extra.forbid
40
+
41
+
42
+ class EmailUsersQueryData(ConfiguredBaseModel):
43
+ users: Optional[list[EmailUser]] = Field(..., alias="users")
44
+
45
+
46
+ def query(query_func: Callable, **kwargs: Any) -> EmailUsersQueryData:
47
+ """
48
+ This is a convenience function which queries and parses the data into
49
+ concrete types. It should be compatible with most GQL clients.
50
+ You do not have to use it to consume the generated data classes.
51
+ Alternatively, you can also mime and alternate the behavior
52
+ of this function in the caller.
53
+
54
+ Parameters:
55
+ query_func (Callable): Function which queries your GQL Server
56
+ kwargs: optional arguments that will be passed to the query function
57
+
58
+ Returns:
59
+ EmailUsersQueryData: queried data parsed into generated classes
60
+ """
61
+ raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
62
+ return EmailUsersQueryData(**raw_data)
@@ -0,0 +1,32 @@
1
+ """
2
+ Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
+ """
4
+ from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
+ from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
+ from enum import Enum # noqa: F401 # pylint: disable=W0611
7
+ from typing import ( # noqa: F401 # pylint: disable=W0611
8
+ Any,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
+ BaseModel,
15
+ Extra,
16
+ Field,
17
+ Json,
18
+ )
19
+
20
+
21
+ class ConfiguredBaseModel(BaseModel):
22
+ class Config:
23
+ smart_union=True
24
+ extra=Extra.forbid
25
+
26
+
27
+ class OwnerV1(ConfiguredBaseModel):
28
+ email: str = Field(..., alias="email")
29
+
30
+
31
+ class EmailServiceOwners(ConfiguredBaseModel):
32
+ service_owners: Optional[list[OwnerV1]] = Field(..., alias="serviceOwners")
@@ -0,0 +1,28 @@
1
+ """
2
+ Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
+ """
4
+ from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
+ from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
+ from enum import Enum # noqa: F401 # pylint: disable=W0611
7
+ from typing import ( # noqa: F401 # pylint: disable=W0611
8
+ Any,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
+ BaseModel,
15
+ Extra,
16
+ Field,
17
+ Json,
18
+ )
19
+
20
+
21
+ class ConfiguredBaseModel(BaseModel):
22
+ class Config:
23
+ smart_union=True
24
+ extra=Extra.forbid
25
+
26
+
27
+ class EmailUser(ConfiguredBaseModel):
28
+ org_username: str = Field(..., alias="org_username")
reconcile/queries.py CHANGED
@@ -124,50 +124,6 @@ def get_app_interface_settings():
124
124
  return None
125
125
 
126
126
 
127
- APP_INTERFACE_EMAILS_QUERY = """
128
- {
129
- emails: app_interface_emails_v1 {
130
- name
131
- subject
132
- to {
133
- aliases
134
- services {
135
- serviceOwners {
136
- email
137
- }
138
- }
139
- clusters {
140
- name
141
- }
142
- namespaces {
143
- name
144
- }
145
- aws_accounts {
146
- accountOwners {
147
- email
148
- }
149
- }
150
- roles {
151
- users {
152
- org_username
153
- }
154
- }
155
- users {
156
- org_username
157
- }
158
- }
159
- body
160
- }
161
- }
162
- """
163
-
164
-
165
- def get_app_interface_emails():
166
- """Returns Email resources defined in app-interface"""
167
- gqlapi = gql.get_api()
168
- return gqlapi.query(APP_INTERFACE_EMAILS_QUERY)["emails"]
169
-
170
-
171
127
  CREDENTIALS_REQUESTS_QUERY = """
172
128
  {
173
129
  credentials_requests: credentials_requests_v1 {
@@ -116,8 +116,10 @@ def lookup_github_file_content(
116
116
  )
117
117
 
118
118
  gh = init_github()
119
- c = gh.get_repo(repo).get_contents(path, ref).decoded_content
120
- return c.decode("utf-8")
119
+ content = gh.get_repo(repo).get_contents(path, ref)
120
+ if isinstance(content, list):
121
+ raise Exception(f"multiple files found for {repo}/{path}/{ref}")
122
+ return content.decoded_content.decode("utf-8")
121
123
 
122
124
 
123
125
  def lookup_graphql_query_results(query: str, **kwargs: dict[str, Any]) -> list[Any]:
@@ -70,7 +70,7 @@ class SmtpClient:
70
70
  self.send_mail([name], subject, body)
71
71
 
72
72
  @retry()
73
- def send_mail(self, names: str, subject: str, body: str) -> None:
73
+ def send_mail(self, names: Iterable[str], subject: str, body: str) -> None:
74
74
  msg = MIMEMultipart()
75
75
  from_name = str(Header("App SRE team automation", "utf-8"))
76
76
  msg["From"] = formataddr((from_name, self.user))