qontract-reconcile 0.10.2.dev233__py3-none-any.whl → 0.10.2.dev234__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qontract-reconcile
3
- Version: 0.10.2.dev233
3
+ Version: 0.10.2.dev234
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Project-URL: homepage, https://github.com/app-sre/qontract-reconcile
6
6
  Project-URL: repository, https://github.com/app-sre/qontract-reconcile
@@ -43,7 +43,7 @@ Requires-Dist: pygithub<1.59,>=1.58
43
43
  Requires-Dist: pyjwt~=2.7
44
44
  Requires-Dist: pyopenssl~=23.0
45
45
  Requires-Dist: pypd<1.2.0,>=1.1.0
46
- Requires-Dist: python-gitlab==5.6.0
46
+ Requires-Dist: python-gitlab==6.0.0
47
47
  Requires-Dist: requests-oauthlib~=1.3
48
48
  Requires-Dist: requests~=2.32
49
49
  Requires-Dist: rich<14.0.0,>=13.3.0
@@ -27,12 +27,12 @@ reconcile/github_owners.py,sha256=viE1KJ-zaTxuZ5yItg2C263J0brn-Q-3hR_DkYDMbhY,31
27
27
  reconcile/github_repo_invites.py,sha256=U9UCzNVwrZ7MqODtFah8ogH0NNY-XjBin7G9gqHtCUY,2690
28
28
  reconcile/github_repo_permissions_validator.py,sha256=PNqL4dqa2OaNBy-NmLVN-t1HZa6eS6HgSYmfOunYqtA,1798
29
29
  reconcile/github_validator.py,sha256=-j17tn3csFVjPMSPL3te48iWVkPZCncRXdeKeLdGjjQ,931
30
- reconcile/gitlab_fork_compliance.py,sha256=RbHckzLnE9zkOFHJANzoejEMMbMAivmqJVs3Suvp9lU,4591
31
- reconcile/gitlab_housekeeping.py,sha256=JEnerWbirICkJQy91ZVepcHge2XWrVUazfIeKnZ94ZQ,26345
30
+ reconcile/gitlab_fork_compliance.py,sha256=kRbHeVIqFkGIIxgPlS36jBtgCH4huWbSYK-bfyfahrU,4552
31
+ reconcile/gitlab_housekeeping.py,sha256=pZZn7Ww0FobqJrepVWoK44srh4W5P4WyAs3u81Xf4yM,26622
32
32
  reconcile/gitlab_labeler.py,sha256=BA2dbXsN9hErUwJl22qcxfeH7XiPCuQ9LN3NddWdnpo,4540
33
- reconcile/gitlab_members.py,sha256=yRZOZqwB9_FJ5DWIFEod6hoG0X38z36atInNshAWddI,8263
33
+ reconcile/gitlab_members.py,sha256=O_RR3sOgK0Sv8fXSC3QwV4wcCJyKRE2MzJRiaWEqTgQ,8294
34
34
  reconcile/gitlab_mr_sqs_consumer.py,sha256=i_MDVfA3Uk_TJiNkfEJzhO6_rwR7z3I3dH9oEw686U4,2681
35
- reconcile/gitlab_owners.py,sha256=nIEsf3QWI3yIw_Bxy5oMaCmszTaNZDwQVaaZZxPgh4g,14447
35
+ reconcile/gitlab_owners.py,sha256=C0Pzlsi9Z8o6JlpuvYOdYVBEHvPjS-1L4q6-E7s1agk,14503
36
36
  reconcile/gitlab_permissions.py,sha256=Do8npcSK89dlA2T9LLdkhngTtv9ovmI4sohTFp7gglc,8373
37
37
  reconcile/gitlab_projects.py,sha256=JIB1UP8CnwSkngEMZE7DFQETMX6sJMp4DXaKoS-Pdkc,1879
38
38
  reconcile/integrations_manager.py,sha256=KKMui_zXHNbMT0o5jnW2JpDzIKuMPqBvamit3I84pDE,9313
@@ -167,10 +167,10 @@ reconcile/change_owners/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
167
167
  reconcile/change_owners/approver.py,sha256=Z3_11vnK2WNOxjEEXVDh0224-_-qbt9d6mBeVE-7fsc,2259
168
168
  reconcile/change_owners/bundle.py,sha256=h30fU-JmLH5a-rCAovpzTeTkkkgZztsZ5A2raee0YuU,5355
169
169
  reconcile/change_owners/change_log_tracking.py,sha256=lRHdC6-sqzUClmoVNq8v5rygAzeQJRxbKbJNDB3YI8E,9499
170
- reconcile/change_owners/change_owners.py,sha256=7tESnSX2-psNGPIF63OeK5f-m3iqwanaGjt03FIjWXU,17111
170
+ reconcile/change_owners/change_owners.py,sha256=U0KmAkDlzxmj4odk2sRrGotfEOq1hC_mb85iCwAzVD0,17105
171
171
  reconcile/change_owners/change_types.py,sha256=5eSvS2_npUriq9RN4LdAWdYUiNzF91K1pDUEVYDIQ4k,32023
172
172
  reconcile/change_owners/changes.py,sha256=YTqwUYutQ6JVSSYmC2Ph5ROCiVix42Vnzy47-i57z4Q,18119
173
- reconcile/change_owners/decision.py,sha256=PqkcDfhX0T7YitNx6idVorYYk_KvcIndQTwQoJJ7bfs,7502
173
+ reconcile/change_owners/decision.py,sha256=755rHmrnhfM_xVKnCPlLPOVm_TCJVb3lSkkUvxFM61Q,7491
174
174
  reconcile/change_owners/diff.py,sha256=0vyu29xCL24ZhUa7hqBni0NaxoCYRXLwvA-h8V23YQ4,9009
175
175
  reconcile/change_owners/implicit_ownership.py,sha256=6BehZvx4IjrphmOt_LLLk9_02Fl5BY5jd00Wuz_PBZk,4234
176
176
  reconcile/change_owners/self_service_roles.py,sha256=xSe5AKZxXAIo0vWOMM5hImQ_rd-dQ38y_5dG5L6X0so,9655
@@ -611,7 +611,7 @@ reconcile/utils/external_resources.py,sha256=YzTb0xAcNdmKO326mGQy7BmST56CZcdru4l
611
611
  reconcile/utils/filtering.py,sha256=S4PbMHuFr3ED0P2Q_ea5CAaB7FimI62B-F5YTaKrphA,402
612
612
  reconcile/utils/git.py,sha256=o4p9m8jlzCJDcutl2HErvGLhL6sZ1NB4Aw3zGcQIzso,2427
613
613
  reconcile/utils/github_api.py,sha256=S1vO-hvYPzm5BIychVIHSYibMns0HBmLgS78MkPfunE,3402
614
- reconcile/utils/gitlab_api.py,sha256=4-DG5wV7k0jSl4Yp4ELQbp3qJY6uYuHukiZJ5tYkhCM,28648
614
+ reconcile/utils/gitlab_api.py,sha256=0wJObojbXXk8Cgh8ymNWlwD1CdENmpsMo1zDSTddnoE,28335
615
615
  reconcile/utils/gpg.py,sha256=EKG7_fdMv8BMlV5yUdPiqoTx-KrzmVSEAl2sLkaKwWI,1123
616
616
  reconcile/utils/gql.py,sha256=C0thIm_k9MBldfqwHzyqtYZk9sIvMdm9IbbnXLGwjD8,14158
617
617
  reconcile/utils/grouping.py,sha256=vr9SFHZ7bqmHYrvYcEZt-Er3-yQYfAAdq5sHLZVmXPY,456
@@ -667,7 +667,7 @@ reconcile/utils/three_way_diff_strategy.py,sha256=oQcHXd9LVhirJfoaOBoHUYuZVGfyL2
667
667
  reconcile/utils/throughput.py,sha256=iP4UWAe2LVhDo69mPPmgo9nQ7RxHD6_GS8MZe-aSiuM,344
668
668
  reconcile/utils/vault.py,sha256=aSA8l9cJlPUHpChFGl27nSY-Mpq9FMjBo7Dcgb1BVfM,15036
669
669
  reconcile/utils/vaultsecretref.py,sha256=0KUSzuvTRxPyKY919TO3-B_eYg4_76fzKvMF8j5s1G0,911
670
- reconcile/utils/vcs.py,sha256=X9wCe0aWrDxP9kuOABLjkex0fYW-6vuYwjboYYlNMZM,10225
670
+ reconcile/utils/vcs.py,sha256=AtAGxhGRL7JcG3mmXN2qRE5Cw89c4I6cRAYQ8x3Fq7Y,10222
671
671
  reconcile/utils/acs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
672
672
  reconcile/utils/acs/base.py,sha256=4UsDrCpAOuddL3PKNuIQYoJP1BtZQNNB8_KEX0lXneg,2532
673
673
  reconcile/utils/acs/policies.py,sha256=jpbi3qpGkBD_X6MfzsX12dPajUbmACmhIOz_0rDvYzs,5489
@@ -775,7 +775,7 @@ tools/app_sre_tekton_access_reporter.py,sha256=o9prLUgQpwO3msRWc2as1xT1y9OB3znkp
775
775
  tools/app_sre_tekton_access_revalidation.py,sha256=66nHEaY-bIqxIhpcmwN8AvQZu6ZXenfkg4Fut0pVZRM,2726
776
776
  tools/glitchtip_access_reporter.py,sha256=o01A6b88t3Wie6tj_tJWWVo2J01LxQ_a9giGm4UzEaU,2901
777
777
  tools/glitchtip_access_revalidation.py,sha256=PXN5wxl6OX8sxddPaakDF3X79nFLvpm-lz0mWLVelw0,2806
778
- tools/qontract_cli.py,sha256=kaKlRhxyqcK4wB454QyvMAY5BMIPMi_vRTLHhX_o954,159006
778
+ tools/qontract_cli.py,sha256=9TfODEGBzRjpTFp0UGPE_9iqj0LMBaNbT3DdXeqYLBg,159113
779
779
  tools/template_validation.py,sha256=qpKYaTgk0GOPGa2Ct5_5sKdwIHtCAKIBGzsMPuJU5fw,3371
780
780
  tools/cli_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
781
781
  tools/cli_commands/container_images_report.py,sha256=_8GuBsl_qYP5qfa5aa9krwyKHYu82MSh02deHEroSa4,5459
@@ -801,7 +801,7 @@ tools/saas_promotion_state/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
801
801
  tools/saas_promotion_state/saas_promotion_state.py,sha256=UfwwRLS5Ya4_Nh1w5n1dvoYtchQvYE9yj1VANt2IKqI,3925
802
802
  tools/sre_checkpoints/__init__.py,sha256=CDaDaywJnmRCLyl_NCcvxi-Zc0hTi_3OdwKiFOyS39I,145
803
803
  tools/sre_checkpoints/util.py,sha256=zEDbGr18ZeHNQwW8pUsr2JRjuXIPz--WAGJxZo9sv_Y,894
804
- qontract_reconcile-0.10.2.dev233.dist-info/METADATA,sha256=smWMr5frcUeo5bVFtSiIZzfs85VvhvkhTqHHQGrPiNo,24352
805
- qontract_reconcile-0.10.2.dev233.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
806
- qontract_reconcile-0.10.2.dev233.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
807
- qontract_reconcile-0.10.2.dev233.dist-info/RECORD,,
804
+ qontract_reconcile-0.10.2.dev234.dist-info/METADATA,sha256=iNMIf9DSOc-sdEsqs0SNiNRMJkJ8Oy3g5h-s8CwN9p8,24352
805
+ qontract_reconcile-0.10.2.dev234.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
806
+ qontract_reconcile-0.10.2.dev234.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
807
+ qontract_reconcile-0.10.2.dev234.dist-info/RECORD,,
@@ -382,7 +382,7 @@ def run(
382
382
 
383
383
  comments = gl.get_merge_request_comments(merge_request)
384
384
  good_to_test_approvers = {
385
- c["username"] for c in comments if c["body"].strip() == "/good-to-test"
385
+ c.username for c in comments if c.body.strip() == "/good-to-test"
386
386
  }
387
387
 
388
388
  change_admitted = is_change_admitted(
@@ -1,12 +1,8 @@
1
- import operator
2
1
  from collections import defaultdict
3
- from collections.abc import (
4
- Iterable,
5
- Mapping,
6
- )
2
+ from collections.abc import Iterable
7
3
  from dataclasses import dataclass
8
4
  from enum import Enum
9
- from typing import Any
5
+ from operator import attrgetter
10
6
 
11
7
  from reconcile.change_owners.approver import (
12
8
  Approver,
@@ -16,6 +12,7 @@ from reconcile.change_owners.bundle import FileRef
16
12
  from reconcile.change_owners.change_types import ChangeTypeContext, DiffCoverage
17
13
  from reconcile.change_owners.changes import BundleFileChange
18
14
  from reconcile.change_owners.diff import Diff
15
+ from reconcile.utils.gitlab_api import Comment
19
16
 
20
17
 
21
18
  class DecisionCommand(Enum):
@@ -33,12 +30,12 @@ class Decision:
33
30
 
34
31
 
35
32
  def get_approver_decisions_from_mr_comments(
36
- comments: Iterable[Mapping[str, Any]],
33
+ comments: Iterable[Comment],
37
34
  ) -> list[Decision]:
38
35
  decisions: list[Decision] = []
39
- for c in sorted(comments, key=operator.itemgetter("created_at")):
40
- commenter = c["username"]
41
- comment_body = c.get("body")
36
+ for c in sorted(comments, key=attrgetter("created_at")):
37
+ commenter = c.username
38
+ comment_body = c.body
42
39
  for line in comment_body.split("\n") if comment_body else []:
43
40
  line = line.strip()
44
41
  if line == DecisionCommand.APPROVED.value:
@@ -77,8 +77,7 @@ class GitlabForkCompliance:
77
77
  continue
78
78
  LOG.info([f"adding {member.username} as maintainer"])
79
79
  user_payload = {"user_id": member.id, "access_level": MAINTAINER_ACCESS}
80
- member = self.src.project.members.create(user_payload)
81
- member.save()
80
+ self.src.project.members.create(user_payload)
82
81
 
83
82
  # Last but not least, we remove the blocked label, in case
84
83
  # it is set
@@ -14,10 +14,12 @@ from operator import itemgetter
14
14
  from typing import Any, cast
15
15
 
16
16
  import gitlab
17
+ from gitlab.const import PipelineStatus
17
18
  from gitlab.v4.objects import (
18
19
  ProjectCommit,
19
20
  ProjectIssue,
20
21
  ProjectMergeRequest,
22
+ ProjectMergeRequestPipeline,
21
23
  )
22
24
  from prometheus_client import (
23
25
  Counter,
@@ -133,11 +135,16 @@ def _calculate_time_since_approval(approved_at: str) -> float:
133
135
 
134
136
 
135
137
  def get_timed_out_pipelines(
136
- pipelines: list[dict], pipeline_timeout: int = 60
137
- ) -> list[dict]:
138
+ pipelines: list[ProjectMergeRequestPipeline],
139
+ pipeline_timeout: int = 60,
140
+ ) -> list[ProjectMergeRequestPipeline]:
138
141
  now = datetime.utcnow()
139
142
 
140
- pending_pipelines = [p for p in pipelines if p["status"] in {"pending", "running"}]
143
+ pending_pipelines = [
144
+ p
145
+ for p in pipelines
146
+ if p.status in {PipelineStatus.PENDING, PipelineStatus.RUNNING}
147
+ ]
141
148
 
142
149
  if not pending_pipelines:
143
150
  return []
@@ -145,7 +152,7 @@ def get_timed_out_pipelines(
145
152
  timed_out_pipelines = []
146
153
 
147
154
  for p in pending_pipelines:
148
- update_time = datetime.strptime(p["updated_at"], DATE_FORMAT)
155
+ update_time = datetime.strptime(p.updated_at, DATE_FORMAT)
149
156
 
150
157
  elapsed = (now - update_time).total_seconds()
151
158
 
@@ -160,20 +167,19 @@ def clean_pipelines(
160
167
  dry_run: bool,
161
168
  gl: GitLabApi,
162
169
  fork_project_id: int,
163
- pipelines: list[dict],
170
+ pipelines: list[ProjectMergeRequestPipeline],
164
171
  ) -> None:
165
172
  if not dry_run:
166
173
  gl_piplelines = gl.get_project_by_id(fork_project_id).pipelines
167
174
 
168
175
  for p in pipelines:
169
- logging.info(["canceling", p["web_url"]])
176
+ logging.info(["canceling", p.web_url])
170
177
  if not dry_run:
171
178
  try:
172
- gl_piplelines.get(p["id"]).cancel()
179
+ gl_piplelines.get(p.id, lazy=True).cancel()
173
180
  except gitlab.exceptions.GitlabPipelineCancelError as err:
174
181
  logging.error(
175
- f"unable to cancel {p['web_url']} - "
176
- f"error message {err.error_message}"
182
+ f"unable to cancel {p.web_url} - error message {err.error_message}"
177
183
  )
178
184
 
179
185
 
@@ -188,9 +194,9 @@ def verify_on_demand_tests(
188
194
  Check if MR has passed all necessary test jobs and add comments to indicate test results.
189
195
  """
190
196
  pipelines = gl.get_merge_request_pipelines(mr)
191
- running_pipelines = [p for p in pipelines if p["status"] == "running"]
197
+ running_pipelines = [p for p in pipelines if p.status == PipelineStatus.RUNNING]
192
198
  if running_pipelines:
193
- # wait for pipelines complate
199
+ # wait for pipelines completion
194
200
  return False
195
201
 
196
202
  commit = next(mr.commits())
@@ -510,7 +516,9 @@ def rebase_merge_requests(
510
516
  continue
511
517
  # possible statuses:
512
518
  # running, pending, success, failed, canceled, skipped
513
- running_pipelines = [p for p in pipelines if p["status"] == "running"]
519
+ running_pipelines = [
520
+ p for p in pipelines if p.status == PipelineStatus.RUNNING
521
+ ]
514
522
  if running_pipelines:
515
523
  continue
516
524
 
@@ -591,7 +599,9 @@ def merge_merge_requests(
591
599
  if wait_for_pipeline:
592
600
  # possible statuses:
593
601
  # running, pending, success, failed, canceled, skipped
594
- running_pipelines = [p for p in pipelines if p["status"] == "running"]
602
+ running_pipelines = [
603
+ p for p in pipelines if p.status == PipelineStatus.RUNNING
604
+ ]
595
605
  if running_pipelines:
596
606
  if insist:
597
607
  # This raise causes the method to restart due to the usage of
@@ -606,8 +616,8 @@ def merge_merge_requests(
606
616
  )
607
617
  continue
608
618
 
609
- last_pipeline_result = pipelines[0]["status"]
610
- if last_pipeline_result != "success":
619
+ last_pipeline_result = pipelines[0].status
620
+ if last_pipeline_result != PipelineStatus.SUCCESS:
611
621
  continue
612
622
 
613
623
  logging.info(["merge", gl.project.name, mr.iid])
@@ -216,7 +216,7 @@ def reconcile_gitlab_members(
216
216
  gl.get_access_level_string(gitlab_user.access_level),
217
217
  ])
218
218
  if not dry_run:
219
- gl.add_group_member(group, gitlab_user)
219
+ gl.add_group_member(group, gitlab_user.user, gitlab_user.access_level)
220
220
  for key, group_member in diff_data.delete.items():
221
221
  logging.info([
222
222
  key,
@@ -89,19 +89,19 @@ class MRApproval:
89
89
  comments = self.gitlab.get_merge_request_comments(self.mr)
90
90
  for comment in comments:
91
91
  # Only interested in '/lgtm' comments
92
- if comment["body"] != "/lgtm":
92
+ if comment.body != "/lgtm":
93
93
  continue
94
94
 
95
95
  # Only interested in comments created after the top commit
96
96
  # creation time
97
- comment_created_at = dateparser.parse(comment["created_at"])
97
+ comment_created_at = dateparser.parse(comment.created_at)
98
98
  if (
99
99
  comment_created_at < self.top_commit_created_at
100
100
  and not self.persistent_lgtm
101
101
  ):
102
102
  continue
103
103
 
104
- lgtms.append(comment["username"])
104
+ lgtms.append(comment.username)
105
105
  return lgtms
106
106
 
107
107
  def expand_groups(self, owners: list[str]) -> set[str]:
@@ -173,19 +173,22 @@ class MRApproval:
173
173
  comments = self.gitlab.get_merge_request_comments(self.mr)
174
174
  for comment in comments:
175
175
  # Only interested on our own comments
176
- if comment["username"] != self.gitlab.user.username:
176
+ if comment.username != self.gitlab.user.username:
177
177
  continue
178
178
 
179
179
  # Ignoring non-approval comments
180
- body = comment["body"]
180
+ body = comment.body
181
181
  if not body.startswith(COMMENT_PREFIX):
182
182
  continue
183
183
 
184
184
  # If the comment was created before the last commit,
185
185
  # it means we had a push after the comment. In this case,
186
186
  # we delete the comment and move on.
187
- comment_created_at = dateparser.parse(comment["created_at"])
188
- if comment_created_at < self.top_commit_created_at:
187
+ comment_created_at = dateparser.parse(comment.created_at)
188
+ if (
189
+ comment_created_at < self.top_commit_created_at
190
+ and comment.note is not None
191
+ ):
189
192
  # Deleting stale comments
190
193
  _LOG.info([
191
194
  f"Project:{self.gitlab.project.id} "
@@ -193,7 +196,7 @@ class MRApproval:
193
196
  f"- removing stale comment"
194
197
  ])
195
198
  if not self.dry_run:
196
- self.gitlab.delete_comment(comment["note"])
199
+ self.gitlab.delete_comment(comment.note)
197
200
  continue
198
201
 
199
202
  # At this point, we've found an approval comment comment
@@ -7,16 +7,12 @@ from collections.abc import (
7
7
  Iterable,
8
8
  Mapping,
9
9
  )
10
+ from dataclasses import dataclass
10
11
  from functools import cached_property
11
- from operator import (
12
- attrgetter,
13
- itemgetter,
14
- )
12
+ from operator import attrgetter
15
13
  from typing import (
16
14
  Any,
17
- Protocol,
18
15
  Self,
19
- TypedDict,
20
16
  cast,
21
17
  )
22
18
  from urllib.parse import urlparse
@@ -47,6 +43,7 @@ from gitlab.v4.objects import (
47
43
  ProjectMergeRequest,
48
44
  ProjectMergeRequestManager,
49
45
  ProjectMergeRequestNote,
46
+ ProjectMergeRequestPipeline,
50
47
  ProjectMergeRequestResourceLabelEvent,
51
48
  User,
52
49
  )
@@ -97,15 +94,19 @@ class MRStatus:
97
94
  GROUP_BOT_NAME_REGEX = re.compile(r"group_.+_bot_.+")
98
95
 
99
96
 
100
- class GLGroupMember(TypedDict):
101
- id: str
102
- user: str
103
- access_level: str
97
+ @dataclass(frozen=True)
98
+ class Assignment:
99
+ author: str
100
+ assignee: str
104
101
 
105
102
 
106
- class GitlabUser(Protocol):
107
- user: str
108
- access_level: int
103
+ @dataclass(frozen=True)
104
+ class Comment:
105
+ id: int
106
+ username: str
107
+ body: str
108
+ created_at: str
109
+ note: ProjectMergeRequestNote | None = None
109
110
 
110
111
 
111
112
  class GitLabApi:
@@ -254,7 +255,7 @@ class GitLabApi:
254
255
  "remove_source_branch": str(remove_source_branch),
255
256
  "labels": labels,
256
257
  }
257
- return cast(ProjectMergeRequest, self.project.mergerequests.create(data))
258
+ return self.project.mergerequests.create(data)
258
259
 
259
260
  def mr_exists(self, title: str) -> bool:
260
261
  mrs = self.get_merge_requests(state=MRState.OPENED)
@@ -274,10 +275,7 @@ class GitLabApi:
274
275
 
275
276
  def get_app_sre_group_users(self) -> list[GroupMember]:
276
277
  app_sre_group = self.gl.groups.get("app-sre")
277
- return cast(
278
- list[GroupMember],
279
- app_sre_group.members.list(get_all=True),
280
- )
278
+ return app_sre_group.members.list(get_all=True)
281
279
 
282
280
  def get_group_if_exists(self, group_name: str) -> Group | None:
283
281
  try:
@@ -312,7 +310,7 @@ class GitLabApi:
312
310
 
313
311
  def get_group_members(self, group: Group) -> list[GroupMember]:
314
312
  return [
315
- cast(GroupMember, m)
313
+ m
316
314
  for m in group.members.list(iterator=True)
317
315
  if not self._is_bot_username(m.username)
318
316
  ]
@@ -331,17 +329,17 @@ class GitLabApi:
331
329
  member.access_level = access_level
332
330
  member.save()
333
331
 
334
- def add_group_member(self, group: Group, user: GitlabUser) -> None:
335
- gitlab_user = self.get_user(user.user)
332
+ def add_group_member(self, group: Group, username: str, access_level: int) -> None:
333
+ gitlab_user = self.get_user(username)
336
334
  if gitlab_user is not None:
337
335
  try:
338
336
  group.members.create({
339
337
  "user_id": gitlab_user.id,
340
- "access_level": user.access_level,
338
+ "access_level": access_level,
341
339
  })
342
340
  except GitlabCreateError:
343
- member = group.members.get(user.user)
344
- member.access_level = user.access_level
341
+ member = group.members.get(gitlab_user.id)
342
+ member.access_level = access_level
345
343
  member.save()
346
344
 
347
345
  def remove_group_member(self, group: Group, user_id: str) -> None:
@@ -398,37 +396,27 @@ class GitLabApi:
398
396
  return self.gl.projects.get(project_id)
399
397
 
400
398
  def get_issues(self, state: str) -> list[ProjectIssue]:
401
- return cast(
402
- list[ProjectIssue],
403
- self.project.issues.list(state=state, get_all=True),
404
- )
399
+ return self.project.issues.list(state=state, get_all=True)
405
400
 
406
401
  def get_merge_request(self, mr_id: str | int) -> ProjectMergeRequest:
407
402
  return self.project.mergerequests.get(mr_id)
408
403
 
409
404
  def get_merge_requests(self, state: str) -> list[ProjectMergeRequest]:
410
- return cast(
411
- list[ProjectMergeRequest],
412
- self.project.mergerequests.list(state=state, get_all=True),
413
- )
405
+ return self.project.mergerequests.list(state=state, get_all=True)
414
406
 
415
407
  @staticmethod
416
408
  def get_merge_request_label_events(
417
409
  mr: ProjectMergeRequest,
418
410
  ) -> list[ProjectMergeRequestResourceLabelEvent]:
419
- return cast(
420
- list[ProjectMergeRequestResourceLabelEvent],
421
- mr.resourcelabelevents.list(get_all=True),
422
- )
411
+ return mr.resourcelabelevents.list(get_all=True)
423
412
 
424
413
  @staticmethod
425
- def get_merge_request_pipelines(mr: ProjectMergeRequest) -> list[dict]:
426
- # TODO: use typed object in return
427
- # TODO: use server side order_by
428
- items = mr.pipelines.list(iterator=True)
414
+ def get_merge_request_pipelines(
415
+ mr: ProjectMergeRequest,
416
+ ) -> list[ProjectMergeRequestPipeline]:
429
417
  return sorted(
430
- [i.asdict() for i in items],
431
- key=itemgetter("created_at"),
418
+ mr.pipelines.list(iterator=True),
419
+ key=attrgetter("created_at"),
432
420
  reverse=True,
433
421
  )
434
422
 
@@ -456,25 +444,28 @@ class GitLabApi:
456
444
  def get_merge_request_comments(
457
445
  merge_request: ProjectMergeRequest,
458
446
  include_description: bool = False,
459
- ) -> list[dict[str, Any]]:
447
+ ) -> list[Comment]:
460
448
  comments = []
461
449
  if include_description:
462
- comments.append({
463
- "username": merge_request.author["username"],
464
- "body": merge_request.description,
465
- "created_at": merge_request.created_at,
466
- "id": MR_DESCRIPTION_COMMENT_ID,
467
- })
468
- for note in merge_request.notes.list(iterator=True):
469
- if note.system:
470
- continue
471
- comments.append({
472
- "username": note.author["username"],
473
- "body": note.body,
474
- "created_at": note.created_at,
475
- "id": note.id,
476
- "note": note,
477
- })
450
+ comments.append(
451
+ Comment(
452
+ id=MR_DESCRIPTION_COMMENT_ID,
453
+ username=merge_request.author["username"],
454
+ body=merge_request.description or "",
455
+ created_at=merge_request.created_at,
456
+ )
457
+ )
458
+ comments.extend(
459
+ Comment(
460
+ id=note.id,
461
+ username=note.author["username"],
462
+ body=note.body or "",
463
+ created_at=note.created_at,
464
+ note=note,
465
+ )
466
+ for note in merge_request.notes.list(iterator=True)
467
+ if not note.system
468
+ )
478
469
  return comments
479
470
 
480
471
  @staticmethod
@@ -488,9 +479,12 @@ class GitLabApi:
488
479
  ) -> None:
489
480
  comments = self.get_merge_request_comments(merge_request)
490
481
  for c in comments:
491
- body = c["body"] or ""
492
- if c["username"] == self.user.username and body.startswith(startswith):
493
- self.delete_comment(c["note"])
482
+ if (
483
+ c.username == self.user.username
484
+ and c.body.startswith(startswith)
485
+ and c.note is not None
486
+ ):
487
+ self.delete_comment(c.note)
494
488
 
495
489
  @retry()
496
490
  def get_project_labels(self) -> set[str]:
@@ -631,7 +625,7 @@ class GitLabApi:
631
625
  item.save()
632
626
 
633
627
  def get_user(self, username: str) -> User | None:
634
- user = cast(list[User], self.gl.users.list(search=username, page=1, per_page=1))
628
+ user = self.gl.users.list(search=username, page=1, per_page=1)
635
629
  if not user:
636
630
  logging.error(f"{username} user not found")
637
631
  return None
@@ -732,25 +726,22 @@ class GitLabApi:
732
726
  self.create_branch("production", "master")
733
727
 
734
728
  def is_last_action_by_team(
735
- self, mr: ProjectMergeRequest, team_usernames: list[str], hold_labels: list[str]
729
+ self, mr: ProjectMergeRequest, team_usernames: set[str], hold_labels: list[str]
736
730
  ) -> bool:
737
731
  # what is the time of the last app-sre response?
738
732
  last_action_by_team = None
739
733
  # comments
740
734
  comments = self.get_merge_request_comments(mr)
741
- comments.sort(key=itemgetter("created_at"), reverse=True)
735
+ comments.sort(key=attrgetter("created_at"), reverse=True)
742
736
  for comment in comments:
743
- username = comment["username"]
737
+ username = comment.username
744
738
  if username == self.user.username:
745
739
  continue
746
740
  if username in team_usernames:
747
- last_action_by_team = comment["created_at"]
741
+ last_action_by_team = comment.created_at
748
742
  break
749
743
  # labels
750
- label_events = cast(
751
- list[ProjectMergeRequestResourceLabelEvent],
752
- mr.resourcelabelevents.list(get_all=True),
753
- )
744
+ label_events = mr.resourcelabelevents.list(get_all=True)
754
745
  for label in reversed(label_events):
755
746
  if label.action == "add" and label.label["name"] in hold_labels:
756
747
  username = label.user["username"]
@@ -774,11 +765,11 @@ class GitLabApi:
774
765
  break
775
766
  # comments
776
767
  for comment in comments:
777
- username = comment["username"]
768
+ username = comment.username
778
769
  if username == self.user.username:
779
770
  continue
780
771
  if username not in team_usernames:
781
- last_action_not_by_team = comment["created_at"]
772
+ last_action_not_by_team = comment.created_at
782
773
  break
783
774
 
784
775
  if not last_action_not_by_team:
@@ -787,19 +778,20 @@ class GitLabApi:
787
778
  return last_action_not_by_team < last_action_by_team
788
779
 
789
780
  def is_assigned_by_team(
790
- self, mr: ProjectMergeRequest, team_usernames: list[str]
781
+ self, mr: ProjectMergeRequest, team_usernames: set[str]
791
782
  ) -> bool:
792
783
  if not mr.assignee:
793
784
  return False
794
785
  last_assignment = self.last_assignment(mr)
795
- if not last_assignment:
786
+ if last_assignment is None:
796
787
  return False
797
-
798
- author, assignee = last_assignment[0], last_assignment[1]
799
- return author in team_usernames and mr.assignee["username"] == assignee
788
+ return (
789
+ last_assignment.author in team_usernames
790
+ and mr.assignee["username"] == last_assignment.assignee
791
+ )
800
792
 
801
793
  @staticmethod
802
- def last_assignment(mr: ProjectMergeRequest) -> tuple[str, str] | None:
794
+ def last_assignment(mr: ProjectMergeRequest) -> Assignment | None:
803
795
  """
804
796
  Get the last assignment of a merge request.
805
797
  :param mr: merge request
@@ -809,7 +801,10 @@ class GitLabApi:
809
801
  notes = mr.notes.list(sort="desc", order_by="created_at", iterator=True)
810
802
  return next(
811
803
  (
812
- (note.author["username"], note.body.removeprefix(body_format))
804
+ Assignment(
805
+ author=note.author["username"],
806
+ assignee=note.body.removeprefix(body_format),
807
+ )
813
808
  for note in notes
814
809
  if note.system and note.body.startswith(body_format)
815
810
  ),
@@ -818,15 +813,17 @@ class GitLabApi:
818
813
 
819
814
  def last_comment(
820
815
  self, mr: ProjectMergeRequest, exclude_bot: bool = True
821
- ) -> dict[str, Any] | None:
816
+ ) -> Comment | None:
822
817
  comments = self.get_merge_request_comments(mr)
823
- comments.sort(key=itemgetter("created_at"), reverse=True)
824
- for comment in comments:
825
- username = comment["username"]
826
- if username == self.user.username and exclude_bot:
827
- continue
828
- return comment
829
- return None
818
+ comments.sort(key=attrgetter("created_at"), reverse=True)
819
+ return next(
820
+ (
821
+ comment
822
+ for comment in comments
823
+ if not (exclude_bot and comment.username == self.user.username)
824
+ ),
825
+ None,
826
+ )
830
827
 
831
828
  def get_commit_sha(self, ref: str, repo_url: str) -> str:
832
829
  project = self.get_project(repo_url)
@@ -841,10 +838,7 @@ class GitLabApi:
841
838
  return response.get("commits", [])
842
839
 
843
840
  def get_personal_access_tokens(self) -> list[PersonalAccessToken]:
844
- return cast(
845
- list[PersonalAccessToken],
846
- self.gl.personal_access_tokens.list(get_all=True),
847
- )
841
+ return self.gl.personal_access_tokens.list(get_all=True)
848
842
 
849
843
  @staticmethod
850
844
  def get_directory_contents(
reconcile/utils/vcs.py CHANGED
@@ -9,6 +9,7 @@ from enum import Enum
9
9
  from typing import Literal
10
10
  from urllib.parse import urlparse
11
11
 
12
+ from gitlab.const import PipelineStatus
12
13
  from gitlab.v4.objects import ProjectMergeRequest
13
14
 
14
15
  from reconcile.typed_queries.github_orgs import GithubOrgV1
@@ -154,14 +155,13 @@ class VCS:
154
155
  pipelines = self._gitlab_instance.get_merge_request_pipelines(mr)
155
156
  if not pipelines:
156
157
  return MRCheckStatus.NONE
157
- # available status codes https://docs.gitlab.com/ee/api/pipelines.html
158
- last_pipeline_result = pipelines[0]["status"]
158
+ last_pipeline_result = pipelines[0].status
159
159
  match last_pipeline_result:
160
- case "success":
160
+ case PipelineStatus.SUCCESS:
161
161
  return MRCheckStatus.SUCCESS
162
- case "running":
162
+ case PipelineStatus.RUNNING:
163
163
  return MRCheckStatus.RUNNING
164
- case "failed":
164
+ case PipelineStatus.FAILED:
165
165
  return MRCheckStatus.FAILED
166
166
  case _:
167
167
  # Lets assume all other states as non-present
tools/qontract_cli.py CHANGED
@@ -27,6 +27,7 @@ import click
27
27
  import click.core
28
28
  import requests
29
29
  import yaml
30
+ from gitlab.const import PipelineStatus
30
31
  from rich import box
31
32
  from rich import print as rich_print
32
33
  from rich.console import Console, Group
@@ -2331,15 +2332,17 @@ def app_interface_review_queue(ctx: click.Context) -> None:
2331
2332
  pipelines = gl.get_merge_request_pipelines(mr)
2332
2333
  if not pipelines:
2333
2334
  continue
2334
- running_pipelines = [p for p in pipelines if p["status"] == "running"]
2335
+ running_pipelines = [
2336
+ p for p in pipelines if p.status == PipelineStatus.RUNNING
2337
+ ]
2335
2338
  if running_pipelines:
2336
2339
  continue
2337
- last_pipeline_result = pipelines[0]["status"]
2338
- if last_pipeline_result != "success":
2340
+ last_pipeline_result = pipelines[0].status
2341
+ if last_pipeline_result != PipelineStatus.SUCCESS:
2339
2342
  continue
2340
2343
 
2341
2344
  author = mr.author["username"]
2342
- app_sre_team_members = [u.username for u in gl.get_app_sre_group_users()]
2345
+ app_sre_team_members = {u.username for u in gl.get_app_sre_group_users()}
2343
2346
  if author in app_sre_team_members:
2344
2347
  continue
2345
2348
 
@@ -2357,7 +2360,7 @@ def app_interface_review_queue(ctx: click.Context) -> None:
2357
2360
  if (
2358
2361
  last_comment
2359
2362
  and trigger_phrases_regex
2360
- and not re.fullmatch(trigger_phrases_regex, last_comment["body"])
2363
+ and not re.fullmatch(trigger_phrases_regex, last_comment.body)
2361
2364
  ):
2362
2365
  continue
2363
2366
 
@@ -2417,7 +2420,7 @@ def app_interface_open_selfserviceable_mr_queue(ctx: click.Context) -> None:
2417
2420
 
2418
2421
  # skip MRs where AppSRE is involved already (author or assignee)
2419
2422
  author = mr.author["username"]
2420
- app_sre_team_members = [u.username for u in gl.get_app_sre_group_users()]
2423
+ app_sre_team_members = {u.username for u in gl.get_app_sre_group_users()}
2421
2424
  if author in app_sre_team_members:
2422
2425
  continue
2423
2426
  is_assigned_by_app_sre = gl.is_assigned_by_team(mr, app_sre_team_members)
@@ -2428,11 +2431,11 @@ def app_interface_open_selfserviceable_mr_queue(ctx: click.Context) -> None:
2428
2431
  pipelines = gl.get_merge_request_pipelines(mr)
2429
2432
  if not pipelines:
2430
2433
  continue
2431
- running_pipelines = [p for p in pipelines if p["status"] == "running"]
2434
+ running_pipelines = [p for p in pipelines if p.status == PipelineStatus.RUNNING]
2432
2435
  if running_pipelines:
2433
2436
  continue
2434
- last_pipeline_result = pipelines[0]["status"]
2435
- if last_pipeline_result != "success":
2437
+ last_pipeline_result = pipelines[0].status
2438
+ if last_pipeline_result != PipelineStatus.SUCCESS:
2436
2439
  continue
2437
2440
 
2438
2441
  item = {