github-rest-api 0.48.0__py3-none-any.whl → 0.49.0__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,19 +1,27 @@
1
1
  """GitHub REST APIs."""
2
2
 
3
3
  from .github import (
4
+ UNSET,
5
+ IssueState,
6
+ IssueStateReason,
4
7
  MergeMethod,
5
8
  Organization,
6
9
  Repository,
7
10
  RepositoryType,
8
11
  SecretVisibility,
12
+ Unset,
9
13
  User,
10
14
  )
11
15
 
12
16
  __all__ = [
17
+ "UNSET",
18
+ "IssueState",
19
+ "IssueStateReason",
13
20
  "MergeMethod",
14
21
  "Organization",
15
22
  "Repository",
16
23
  "RepositoryType",
17
24
  "SecretVisibility",
25
+ "Unset",
18
26
  "User",
19
27
  ]
github_rest_api/github.py CHANGED
@@ -20,11 +20,18 @@ from github_rest_api.pr_content import (
20
20
  deterministic_title,
21
21
  generate_pr_content,
22
22
  )
23
+ from github_rest_api.utils import as_str_sequence
23
24
 
24
25
  logger = logging.getLogger(__name__)
25
26
 
26
27
  URL_API = "https://api.github.com"
27
28
 
29
+ # Default connect/read timeout (in seconds) for a GitHub REST API request. It is a
30
+ # per-socket-operation deadline rather than a total-transfer one, so a slow but
31
+ # progressing response is not cut off. Callers may pass their own `timeout` to any
32
+ # of the request helpers, but never `None`: see `_apply_timeout`.
33
+ DEFAULT_TIMEOUT = 10
34
+
28
35
  # Default LiteLLM 'provider/model' used to generate PR titles and descriptions.
29
36
  DEFAULT_PR_MODEL = "anthropic/claude-haiku-4-5-20251001"
30
37
 
@@ -66,6 +73,25 @@ def _validate_secret_name(name: str) -> None:
66
73
  )
67
74
 
68
75
 
76
+ def _apply_timeout(kwargs: dict[str, Any]) -> None:
77
+ """Apply the default request timeout in place, rejecting an infinite one.
78
+
79
+ `requests` treats `timeout=None` as "wait forever", which can hang a caller
80
+ indefinitely on an unresponsive endpoint. No GitHub REST API call is worth
81
+ that, so `None` is rejected rather than silently replaced: a caller that
82
+ passed it meant to disable the deadline and should say what it wants instead.
83
+
84
+ :param kwargs: Keyword arguments destined for a `requests` call. A `timeout`
85
+ entry is added when absent; any other entry is left untouched.
86
+ :raises ValueError: If `timeout` is explicitly None.
87
+ """
88
+ if kwargs.setdefault("timeout", DEFAULT_TIMEOUT) is None:
89
+ raise ValueError(
90
+ "timeout=None would wait indefinitely; pass a positive number of "
91
+ f"seconds, or omit timeout to use the default of {DEFAULT_TIMEOUT}."
92
+ )
93
+
94
+
69
95
  def _encrypt_secret(public_key: str, value: str) -> str:
70
96
  """Encrypt a secret value using a LibSodium sealed box.
71
97
  :param public_key: The base64-encoded public key to encrypt against.
@@ -284,12 +310,13 @@ class GitHub:
284
310
  :param url: The endpoint URL to request.
285
311
  :param raise_for_status: Whether to raise on a non-2xx response.
286
312
  :param kwargs: Additional keyword arguments (e.g. `params`) forwarded
287
- to `requests.get`.
313
+ to `requests.get`. `timeout` defaults to `DEFAULT_TIMEOUT`
314
+ seconds and must not be None (see `_apply_timeout`).
288
315
  """
316
+ _apply_timeout(kwargs)
289
317
  resp = requests.get(
290
318
  url=url,
291
319
  headers=self._headers,
292
- timeout=10,
293
320
  **kwargs,
294
321
  )
295
322
  if raise_for_status:
@@ -304,22 +331,37 @@ class GitHub:
304
331
  :param headers: Request headers; defaults to the standard auth headers.
305
332
  :param raise_for_status: Whether to raise on a non-2xx response.
306
333
  :param kwargs: Additional keyword arguments (e.g. `json`) forwarded
307
- to `requests.post`.
334
+ to `requests.post`. `timeout` defaults to `DEFAULT_TIMEOUT`
335
+ seconds and must not be None (see `_apply_timeout`).
308
336
  """
309
337
  if headers is None:
310
338
  headers = self._headers
339
+ _apply_timeout(kwargs)
311
340
  resp = requests.post(
312
341
  url=url,
313
342
  headers=headers,
314
- timeout=10,
315
343
  **kwargs,
316
344
  )
317
345
  if raise_for_status:
318
346
  resp.raise_for_status()
319
347
  return resp
320
348
 
321
- def _delete(self, url, raise_for_status: bool = True) -> requests.Response:
322
- resp = requests.delete(url=url, headers=self._headers, timeout=10)
349
+ def _delete(
350
+ self, url, raise_for_status: bool = True, **kwargs
351
+ ) -> requests.Response:
352
+ """Send a DELETE request to a GitHub REST API endpoint.
353
+ :param url: The endpoint URL to request.
354
+ :param raise_for_status: Whether to raise on a non-2xx response.
355
+ :param kwargs: Additional keyword arguments (e.g. `json`) forwarded
356
+ to `requests.delete`. `timeout` defaults to `DEFAULT_TIMEOUT`
357
+ seconds and must not be None (see `_apply_timeout`).
358
+ """
359
+ _apply_timeout(kwargs)
360
+ resp = requests.delete(
361
+ url=url,
362
+ headers=self._headers,
363
+ **kwargs,
364
+ )
323
365
  if raise_for_status:
324
366
  resp.raise_for_status()
325
367
  return resp
@@ -331,12 +373,13 @@ class GitHub:
331
373
  :param url: The endpoint URL to request.
332
374
  :param raise_for_status: Whether to raise on a non-2xx response.
333
375
  :param kwargs: Additional keyword arguments (e.g. `json`) forwarded
334
- to `requests.put`.
376
+ to `requests.put`. `timeout` defaults to `DEFAULT_TIMEOUT`
377
+ seconds and must not be None (see `_apply_timeout`).
335
378
  """
379
+ _apply_timeout(kwargs)
336
380
  resp = requests.put(
337
381
  url=url,
338
382
  headers=self._headers,
339
- timeout=10,
340
383
  **kwargs,
341
384
  )
342
385
  if raise_for_status:
@@ -348,12 +391,13 @@ class GitHub:
348
391
  :param url: The endpoint URL to request.
349
392
  :param raise_for_status: Whether to raise on a non-2xx response.
350
393
  :param kwargs: Additional keyword arguments (e.g. `json`) forwarded
351
- to `requests.patch`.
394
+ to `requests.patch`. `timeout` defaults to `DEFAULT_TIMEOUT`
395
+ seconds and must not be None (see `_apply_timeout`).
352
396
  """
397
+ _apply_timeout(kwargs)
353
398
  resp = requests.patch(
354
399
  url=url,
355
400
  headers=self._headers,
356
- timeout=10,
357
401
  **kwargs,
358
402
  )
359
403
  if raise_for_status:
@@ -398,12 +442,56 @@ class IssueState(StrEnum):
398
442
  ALL = "all"
399
443
 
400
444
 
445
+ class IssueStateReason(StrEnum):
446
+ """Why an issue was closed or reopened, accompanying an ``IssueState``."""
447
+
448
+ COMPLETED = "completed"
449
+ NOT_PLANNED = "not_planned"
450
+ DUPLICATE = "duplicate"
451
+ REOPENED = "reopened"
452
+
453
+
401
454
  class MergeMethod(StrEnum):
402
455
  MERGE = "merge"
403
456
  SQUASH = "squash"
404
457
  REBASE = "rebase"
405
458
 
406
459
 
460
+ class Unset:
461
+ """The type of :data:`UNSET`."""
462
+
463
+ def __repr__(self) -> str:
464
+ return "UNSET"
465
+
466
+
467
+ # Sentinel marking an update field the caller did not mention, so that a field
468
+ # left out is distinguishable from one explicitly set to an empty/null value.
469
+ # `None` cannot serve as that marker because GitHub gives it a meaning of its
470
+ # own: `{"milestone": None}` detaches the milestone, while omitting the key
471
+ # leaves it alone. See `Repository.update_issue`.
472
+ UNSET = Unset()
473
+
474
+
475
+ def _validate_issue_state(state: str, noun: str = "issue") -> None:
476
+ """Validate a state an issue or milestone can be *set* to.
477
+
478
+ ``IssueState.ALL`` is a filter for listing, not a state anything can hold;
479
+ GitHub rejects it with a 422. It is caught here so the mistake is reported
480
+ against the argument rather than as an opaque HTTP error.
481
+
482
+ :param state: The state to set (``open`` or ``closed``).
483
+ :param noun: What is being updated, for the error message.
484
+ :raises ValueError: If the state is anything other than open or closed.
485
+ """
486
+ if state not in (IssueState.OPEN, IssueState.CLOSED):
487
+ # `str(state)` so a StrEnum renders as 'all' rather than
488
+ # <IssueState.ALL: 'all'>, matching how a plain string argument reads.
489
+ raise ValueError(
490
+ f"Invalid {noun} state {str(state)!r}: a {noun} can only be set to "
491
+ f"'{IssueState.OPEN}' or '{IssueState.CLOSED}'."
492
+ )
493
+
494
+
407
495
  class Repository(GitHub):
408
496
  """Abstraction of a GitHub repository."""
409
497
 
@@ -421,6 +509,10 @@ class Repository(GitHub):
421
509
  self._url_branches = f"{self._url_repo}/branches"
422
510
  self._url_refs = f"{self._url_repo}/git/refs"
423
511
  self._url_issues = f"{self._url_repo}/issues"
512
+ # Issue comments are edited/deleted by comment id, not issue number, so
513
+ # this endpoint is not nested under a specific issue.
514
+ self._url_issue_comments = f"{self._url_issues}/comments"
515
+ self._url_milestones = f"{self._url_repo}/milestones"
424
516
  self._url_releases = f"{self._url_repo}/releases"
425
517
  self._url_secrets = f"{self._url_repo}/actions/secrets"
426
518
  self._url_compare = f"{self._url_repo}/compare"
@@ -505,6 +597,28 @@ class Repository(GitHub):
505
597
  """
506
598
  return self._extract_all(url=f"{self._url_pull}/{pr_number}/reviews", n=n)
507
599
 
600
+ def get_pull_request_review_comments(
601
+ self, pr_number: int, n: int = 0
602
+ ) -> list[dict[str, Any]]:
603
+ """List the review comments on a pull request.
604
+
605
+ These are the line-anchored comments left on the diff, each carrying
606
+ ``path``, ``line``/``original_line`` and ``diff_hunk``, and grouped into
607
+ threads via ``in_reply_to_id``. They are a different resource from the
608
+ conversation-level comments returned by ``get_issue_comments``, which the
609
+ pull request shares with its issue; a full picture of a PR's discussion
610
+ needs both.
611
+
612
+ Every review comment is listed, whether it belongs to a submitted review
613
+ or was posted as a standalone comment. Comments still pending in an
614
+ unsubmitted review are not visible to anyone but their author, so they
615
+ are absent.
616
+
617
+ :param pr_number: The number of the pull request.
618
+ :param n: The maximum number of review comments to return (0 means all).
619
+ """
620
+ return self._extract_all(url=f"{self._url_pull}/{pr_number}/comments", n=n)
621
+
508
622
  def get_issues(
509
623
  self, state: IssueState = IssueState.OPEN, n: int = 0
510
624
  ) -> list[dict[str, Any]]:
@@ -550,7 +664,7 @@ class Repository(GitHub):
550
664
 
551
665
  def create_pull_request(
552
666
  self,
553
- json: dict[str, str],
667
+ json: dict[str, Any],
554
668
  model: str = "",
555
669
  ) -> dict[str, Any] | None:
556
670
  """Create a pull request.
@@ -598,7 +712,7 @@ class Repository(GitHub):
598
712
  def merge_pull_request(
599
713
  self,
600
714
  pr_number: int,
601
- merge_method: MergeMethod | str = MergeMethod.MERGE,
715
+ merge_method: MergeMethod = MergeMethod.MERGE,
602
716
  sha: str = "",
603
717
  ) -> dict[str, Any]:
604
718
  """Merge a pull request in this repository.
@@ -662,9 +776,9 @@ class Repository(GitHub):
662
776
  self,
663
777
  pr_number: int,
664
778
  *,
665
- authors: Sequence[str],
666
- approvers: Sequence[str],
667
- allowed_types: Sequence[str] = DEFAULT_AUTO_MERGE_TYPES,
779
+ authors: str | Sequence[str],
780
+ approvers: str | Sequence[str],
781
+ allowed_types: str | Sequence[str] = DEFAULT_AUTO_MERGE_TYPES,
668
782
  min_age_minutes: int = DEFAULT_MIN_AGE_MINUTES,
669
783
  marker: str = DEFAULT_AUTO_MERGE_MARKER,
670
784
  ) -> str | None:
@@ -687,9 +801,12 @@ class Repository(GitHub):
687
801
  the first failure so extra requests are avoided.
688
802
 
689
803
  :param pr_number: The number of the pull request.
690
- :param authors: Logins whose PRs are eligible for auto-merge.
691
- :param approvers: Logins whose reviews/marker comments grant approval.
692
- :param allowed_types: Conventional-Commits title types eligible for auto-merge.
804
+ :param authors: Logins whose PRs are eligible for auto-merge, or a
805
+ single login as a bare string.
806
+ :param approvers: Logins whose reviews/marker comments grant approval,
807
+ or a single login as a bare string.
808
+ :param allowed_types: Conventional-Commits title types eligible for
809
+ auto-merge, or a single type as a bare string.
693
810
  :param min_age_minutes: The minimum head-commit age in minutes.
694
811
  :param marker: The marker substring an approver may comment to approve.
695
812
  :return: The validated head SHA when the PR is eligible, else ``None``.
@@ -697,6 +814,9 @@ class Repository(GitHub):
697
814
  commit checked here, so a push landing between this gate and the merge
698
815
  is rejected rather than silently merged.
699
816
  """
817
+ authors = as_str_sequence(authors)
818
+ approvers = as_str_sequence(approvers)
819
+ allowed_types = as_str_sequence(allowed_types)
700
820
  pr = self.get_pull_request(pr_number)
701
821
 
702
822
  def skip(reason: str) -> None:
@@ -735,20 +855,23 @@ class Repository(GitHub):
735
855
  def auto_merge_pull_requests(
736
856
  self,
737
857
  *,
738
- authors: Sequence[str],
739
- approvers: Sequence[str],
740
- allowed_types: Sequence[str] = DEFAULT_AUTO_MERGE_TYPES,
858
+ authors: str | Sequence[str],
859
+ approvers: str | Sequence[str],
860
+ allowed_types: str | Sequence[str] = DEFAULT_AUTO_MERGE_TYPES,
741
861
  min_age_minutes: int = DEFAULT_MIN_AGE_MINUTES,
742
862
  marker: str = DEFAULT_AUTO_MERGE_MARKER,
743
- merge_method: MergeMethod | str = MergeMethod.MERGE,
863
+ merge_method: MergeMethod = MergeMethod.MERGE,
744
864
  dry_run: bool = False,
745
865
  ) -> list[int]:
746
866
  """Auto-merge every open pull request that passes ``should_auto_merge``.
747
867
 
748
- :param authors: Logins whose PRs are eligible for auto-merge. An empty
749
- allowlist makes nothing eligible (fail-safe).
750
- :param approvers: Logins whose reviews/marker comments grant approval.
751
- :param allowed_types: Conventional-Commits title types eligible for auto-merge.
868
+ :param authors: Logins whose PRs are eligible for auto-merge, or a
869
+ single login as a bare string. An empty allowlist makes nothing
870
+ eligible (fail-safe).
871
+ :param approvers: Logins whose reviews/marker comments grant approval,
872
+ or a single login as a bare string.
873
+ :param allowed_types: Conventional-Commits title types eligible for
874
+ auto-merge, or a single type as a bare string.
752
875
  :param min_age_minutes: The minimum head-commit age in minutes.
753
876
  :param marker: The marker substring an approver may comment to approve.
754
877
  :param merge_method: The merge method to use (``merge``, ``squash`` or
@@ -758,6 +881,9 @@ class Repository(GitHub):
758
881
  :return: The numbers of the PRs that were merged (or, under ``dry_run``,
759
882
  that would have been merged).
760
883
  """
884
+ authors = as_str_sequence(authors)
885
+ approvers = as_str_sequence(approvers)
886
+ allowed_types = as_str_sequence(allowed_types)
761
887
  eligible = []
762
888
  for pr in self.get_pull_requests():
763
889
  number = pr["number"]
@@ -937,6 +1063,389 @@ class Repository(GitHub):
937
1063
  """
938
1064
  return self.pr_has_change(pr_number=pr_number, pred=pred)
939
1065
 
1066
+ def create_issue(
1067
+ self,
1068
+ title: str,
1069
+ body: str = "",
1070
+ labels: str | Sequence[str] = (),
1071
+ assignees: str | Sequence[str] = (),
1072
+ milestone: int | None = None,
1073
+ ) -> dict[str, Any]:
1074
+ """Create an issue in this repository.
1075
+
1076
+ :param title: The title of the new issue.
1077
+ :param body: The Markdown body of the new issue.
1078
+ :param labels: Names of labels to attach to the new issue, or a single
1079
+ name as a bare string. Silently dropped unless the authenticated
1080
+ user has push access to the repository. Labels that do not already
1081
+ exist are created by GitHub.
1082
+ :param assignees: Logins of users to assign to the new issue, or a
1083
+ single login as a bare string. Silently dropped unless the
1084
+ authenticated user has push access to the repository.
1085
+ :param milestone: The number of the milestone to associate the new issue
1086
+ with (see ``get_milestones``). Silently dropped unless the
1087
+ authenticated user has push access to the repository.
1088
+ """
1089
+ labels = as_str_sequence(labels)
1090
+ assignees = as_str_sequence(assignees)
1091
+ json: dict[str, Any] = {"title": title}
1092
+ if body:
1093
+ json["body"] = body
1094
+ if labels:
1095
+ json["labels"] = list(labels)
1096
+ if assignees:
1097
+ json["assignees"] = list(assignees)
1098
+ if milestone is not None:
1099
+ json["milestone"] = milestone
1100
+ return self._post(url=self._url_issues, json=json).json()
1101
+
1102
+ def get_issue(self, issue_number: int) -> dict[str, Any]:
1103
+ """Get a single issue in this repository.
1104
+
1105
+ :param issue_number: The number of the issue. A pull request number is
1106
+ also accepted, since GitHub models pull requests as issues; the
1107
+ payload then carries a ``pull_request`` key.
1108
+ """
1109
+ return self._get(url=f"{self._url_issues}/{issue_number}").json()
1110
+
1111
+ def update_issue(
1112
+ self,
1113
+ issue_number: int,
1114
+ *,
1115
+ title: str | Unset = UNSET,
1116
+ body: str | Unset = UNSET,
1117
+ state: IssueState | Unset = UNSET,
1118
+ state_reason: IssueStateReason | None | Unset = UNSET,
1119
+ duplicate_issue_id: int | Unset = UNSET,
1120
+ labels: str | Sequence[str] | Unset = UNSET,
1121
+ assignees: str | Sequence[str] | Unset = UNSET,
1122
+ milestone: int | None | Unset = UNSET,
1123
+ ) -> dict[str, Any]:
1124
+ """Update an issue in this repository.
1125
+
1126
+ Only the fields passed are sent, so an update never disturbs a field the
1127
+ caller did not mention. That distinction is what the ``UNSET`` default
1128
+ buys: ``None`` and the empty sequence are meaningful values here (they
1129
+ detach the milestone and clear the labels/assignees respectively), so
1130
+ they cannot double as "leave this alone".
1131
+
1132
+ ``labels`` and ``assignees`` replace the existing set rather than adding
1133
+ to it; to add or remove a few entries without knowing the rest, use
1134
+ ``add_issue_labels``/``remove_issue_label`` or
1135
+ ``add_issue_assignees``/``remove_issue_assignees``.
1136
+
1137
+ :param issue_number: The number of the issue.
1138
+ :param title: A new title.
1139
+ :param body: A new Markdown body; pass ``""`` to empty it.
1140
+ :param state: ``open`` or ``closed`` (see also ``close_issue``).
1141
+ :param state_reason: Why the issue is in that state (``completed``,
1142
+ ``not_planned``, ``duplicate`` or ``reopened``), or None to clear it.
1143
+ It must be passed alongside ``state``, and GitHub applies it only
1144
+ when the state actually changes: re-closing an already-closed issue
1145
+ keeps the reason it was closed with.
1146
+ :param duplicate_issue_id: The id (not the number) of the issue this one
1147
+ duplicates. GitHub requires it when ``state_reason`` is ``duplicate``
1148
+ and ignores it otherwise, so the two must be passed together.
1149
+ :param labels: Names of the labels the issue should carry, replacing any
1150
+ current ones; a bare string is one label, and an empty sequence
1151
+ clears them all. Labels that do not already exist are created.
1152
+ :param assignees: Logins the issue should be assigned to, replacing any
1153
+ current ones; a bare string is one login, and an empty sequence
1154
+ unassigns everyone.
1155
+ :param milestone: The number of the milestone to associate the issue
1156
+ with; pass None to detach it.
1157
+ :raises ValueError: If no field is given (the request would be a no-op),
1158
+ if ``state`` is neither open nor closed, if ``state_reason`` is given
1159
+ without ``state``, or if ``state_reason='duplicate'`` and
1160
+ ``duplicate_issue_id`` are not given together.
1161
+ """
1162
+ # GitHub documents state_reason as "ignored unless state is changed", so
1163
+ # sending it alone silently does nothing. Refuse it rather than let the
1164
+ # caller believe an update happened.
1165
+ if isinstance(state, Unset) and not isinstance(state_reason, Unset):
1166
+ raise ValueError(
1167
+ "state_reason is ignored by GitHub unless state changes too; "
1168
+ "pass state (e.g. IssueState.CLOSED) alongside it."
1169
+ )
1170
+ # `duplicate` and `duplicate_issue_id` are only meaningful together:
1171
+ # without the id GitHub rejects the request, and without the reason it
1172
+ # drops the id. Comparison is by value so an equivalent plain string
1173
+ # still takes this path, as it does everywhere else a state is checked.
1174
+ is_duplicate = (
1175
+ not isinstance(state_reason, Unset)
1176
+ and state_reason is not None
1177
+ and str(state_reason) == IssueStateReason.DUPLICATE
1178
+ )
1179
+ if is_duplicate and isinstance(duplicate_issue_id, Unset):
1180
+ raise ValueError(
1181
+ "state_reason='duplicate' requires duplicate_issue_id, the id "
1182
+ "of the issue this one duplicates."
1183
+ )
1184
+ if not is_duplicate and not isinstance(duplicate_issue_id, Unset):
1185
+ raise ValueError(
1186
+ "duplicate_issue_id is ignored by GitHub unless state_reason is "
1187
+ "'duplicate'; pass IssueStateReason.DUPLICATE alongside it."
1188
+ )
1189
+ json: dict[str, Any] = {}
1190
+ if not isinstance(title, Unset):
1191
+ json["title"] = title
1192
+ if not isinstance(body, Unset):
1193
+ json["body"] = body
1194
+ if not isinstance(state, Unset):
1195
+ _validate_issue_state(state)
1196
+ json["state"] = str(state)
1197
+ if not isinstance(state_reason, Unset):
1198
+ json["state_reason"] = None if state_reason is None else str(state_reason)
1199
+ if not isinstance(duplicate_issue_id, Unset):
1200
+ json["duplicate_issue_id"] = duplicate_issue_id
1201
+ if not isinstance(labels, Unset):
1202
+ json["labels"] = list(as_str_sequence(labels))
1203
+ if not isinstance(assignees, Unset):
1204
+ json["assignees"] = list(as_str_sequence(assignees))
1205
+ if not isinstance(milestone, Unset):
1206
+ json["milestone"] = milestone
1207
+ if not json:
1208
+ raise ValueError(
1209
+ f"No field to update was given for issue #{issue_number}; "
1210
+ "pass at least one of title, body, state, state_reason, "
1211
+ "labels, assignees or milestone."
1212
+ )
1213
+ return self._patch(url=f"{self._url_issues}/{issue_number}", json=json).json()
1214
+
1215
+ def close_issue(
1216
+ self,
1217
+ issue_number: int,
1218
+ state_reason: IssueStateReason = IssueStateReason.COMPLETED,
1219
+ duplicate_issue_id: int | Unset = UNSET,
1220
+ ) -> dict[str, Any]:
1221
+ """Close an issue in this repository.
1222
+
1223
+ Closing an already-closed issue is not an error, but it is not a state
1224
+ change either, so GitHub keeps the existing ``state_reason``. Reopen the
1225
+ issue first to re-close it with a different reason.
1226
+
1227
+ :param issue_number: The number of the issue.
1228
+ :param state_reason: Why the issue is being closed (``completed``, the
1229
+ default, ``not_planned`` or ``duplicate``).
1230
+ :param duplicate_issue_id: The id (not the number) of the issue this one
1231
+ duplicates. Required when ``state_reason`` is ``duplicate``, and
1232
+ rejected otherwise (see ``update_issue``).
1233
+ """
1234
+ return self.update_issue(
1235
+ issue_number,
1236
+ state=IssueState.CLOSED,
1237
+ state_reason=state_reason,
1238
+ duplicate_issue_id=duplicate_issue_id,
1239
+ )
1240
+
1241
+ def reopen_issue(self, issue_number: int) -> dict[str, Any]:
1242
+ """Reopen a closed issue in this repository.
1243
+
1244
+ :param issue_number: The number of the issue.
1245
+ """
1246
+ return self.update_issue(
1247
+ issue_number, state=IssueState.OPEN, state_reason=IssueStateReason.REOPENED
1248
+ )
1249
+
1250
+ def update_issue_comment(self, comment_id: int, body: str) -> dict[str, Any]:
1251
+ """Edit an existing comment on an issue or pull request.
1252
+
1253
+ :param comment_id: The id of the comment (its ``id`` field, which is
1254
+ unique repository-wide and unrelated to the issue number).
1255
+ :param body: The new Markdown body, replacing the current one.
1256
+ """
1257
+ return self._patch(
1258
+ url=f"{self._url_issue_comments}/{comment_id}",
1259
+ json={"body": body},
1260
+ ).json()
1261
+
1262
+ def delete_issue_comment(self, comment_id: int) -> requests.Response:
1263
+ """Delete a comment from an issue or pull request.
1264
+
1265
+ :param comment_id: The id of the comment (its ``id`` field, which is
1266
+ unique repository-wide and unrelated to the issue number).
1267
+ """
1268
+ return self._delete(url=f"{self._url_issue_comments}/{comment_id}")
1269
+
1270
+ def get_issue_labels(self, issue_number: int, n: int = 0) -> list[dict[str, Any]]:
1271
+ """List the labels on an issue.
1272
+
1273
+ :param issue_number: The number of the issue.
1274
+ :param n: The maximum number of labels to return (0 means all).
1275
+ """
1276
+ return self._extract_all(url=f"{self._url_issues}/{issue_number}/labels", n=n)
1277
+
1278
+ def add_issue_labels(
1279
+ self, issue_number: int, labels: str | Sequence[str]
1280
+ ) -> list[dict[str, Any]]:
1281
+ """Add labels to an issue, keeping the ones already on it.
1282
+
1283
+ :param issue_number: The number of the issue.
1284
+ :param labels: Names of the labels to add, or a single name as a bare
1285
+ string. Labels that do not already exist are created by GitHub.
1286
+ :return: All labels on the issue after the addition.
1287
+ :raises ValueError: If no label is given. GitHub rejects an empty list
1288
+ here with a 422; use ``set_issue_labels`` to clear labels instead.
1289
+ """
1290
+ names = list(as_str_sequence(labels))
1291
+ if not names:
1292
+ raise ValueError(
1293
+ f"No label to add was given for issue #{issue_number}; "
1294
+ "pass at least one name, or use set_issue_labels(..., ()) to "
1295
+ "remove every label."
1296
+ )
1297
+ return self._post(
1298
+ url=f"{self._url_issues}/{issue_number}/labels",
1299
+ json={"labels": names},
1300
+ ).json()
1301
+
1302
+ def set_issue_labels(
1303
+ self, issue_number: int, labels: str | Sequence[str]
1304
+ ) -> list[dict[str, Any]]:
1305
+ """Replace all labels on an issue with the given ones.
1306
+
1307
+ :param issue_number: The number of the issue.
1308
+ :param labels: Names of the labels the issue should carry, or a single
1309
+ name as a bare string; an empty sequence removes every label.
1310
+ :return: All labels on the issue after the replacement.
1311
+ """
1312
+ return self._put(
1313
+ url=f"{self._url_issues}/{issue_number}/labels",
1314
+ json={"labels": list(as_str_sequence(labels))},
1315
+ ).json()
1316
+
1317
+ def remove_issue_label(self, issue_number: int, label: str) -> list[dict[str, Any]]:
1318
+ """Remove a single label from an issue.
1319
+
1320
+ :param issue_number: The number of the issue.
1321
+ :param label: The name of the label to remove. GitHub responds with 404
1322
+ if the issue does not carry it.
1323
+ :return: The labels remaining on the issue.
1324
+ """
1325
+ # A label name may contain spaces and slashes (e.g. "good first issue"),
1326
+ # so it is URL-encoded rather than interpolated raw into the path.
1327
+ return self._delete(
1328
+ url=f"{self._url_issues}/{issue_number}/labels/{quote(label, safe='')}",
1329
+ ).json()
1330
+
1331
+ def add_issue_assignees(
1332
+ self, issue_number: int, assignees: str | Sequence[str]
1333
+ ) -> dict[str, Any]:
1334
+ """Assign users to an issue, keeping those already assigned.
1335
+
1336
+ :param issue_number: The number of the issue.
1337
+ :param assignees: Logins to assign, or a single login as a bare string.
1338
+ A login without push access to the repository is silently ignored.
1339
+ :return: The updated issue.
1340
+ """
1341
+ return self._post(
1342
+ url=f"{self._url_issues}/{issue_number}/assignees",
1343
+ json={"assignees": list(as_str_sequence(assignees))},
1344
+ ).json()
1345
+
1346
+ def remove_issue_assignees(
1347
+ self, issue_number: int, assignees: str | Sequence[str]
1348
+ ) -> dict[str, Any]:
1349
+ """Unassign users from an issue, leaving the other assignees in place.
1350
+
1351
+ :param issue_number: The number of the issue.
1352
+ :param assignees: Logins to unassign, or a single login as a bare string.
1353
+ :return: The updated issue.
1354
+ """
1355
+ return self._delete(
1356
+ url=f"{self._url_issues}/{issue_number}/assignees",
1357
+ json={"assignees": list(as_str_sequence(assignees))},
1358
+ ).json()
1359
+
1360
+ def get_milestones(
1361
+ self, state: IssueState = IssueState.OPEN, n: int = 0
1362
+ ) -> list[dict[str, Any]]:
1363
+ """List milestones in this repository.
1364
+
1365
+ :param state: Filter milestones by state: ``open`` (the default),
1366
+ ``closed``, or ``all``.
1367
+ :param n: The maximum number of milestones to return (0 means all).
1368
+ """
1369
+ return self._extract_all(url=self._url_milestones, params={"state": state}, n=n)
1370
+
1371
+ def create_milestone(
1372
+ self,
1373
+ title: str,
1374
+ description: str = "",
1375
+ due_on: str = "",
1376
+ ) -> dict[str, Any]:
1377
+ """Create a milestone in this repository.
1378
+
1379
+ :param title: The title of the milestone.
1380
+ :param description: A description of the milestone.
1381
+ :param due_on: When the milestone is due, as an ISO-8601 timestamp
1382
+ (e.g. ``2026-12-31T00:00:00Z``).
1383
+ :return: The new milestone, whose ``number`` is what ``create_issue``
1384
+ and ``update_issue`` take as their ``milestone``.
1385
+ """
1386
+ json: dict[str, Any] = {"title": title}
1387
+ if description:
1388
+ json["description"] = description
1389
+ if due_on:
1390
+ json["due_on"] = due_on
1391
+ return self._post(url=self._url_milestones, json=json).json()
1392
+
1393
+ def update_milestone(
1394
+ self,
1395
+ milestone_number: int,
1396
+ *,
1397
+ title: str | Unset = UNSET,
1398
+ description: str | Unset = UNSET,
1399
+ state: IssueState | Unset = UNSET,
1400
+ due_on: str | Unset = UNSET,
1401
+ ) -> dict[str, Any]:
1402
+ """Update a milestone in this repository.
1403
+
1404
+ As in ``update_issue``, only the fields passed are sent.
1405
+
1406
+ :param milestone_number: The ``number`` of the milestone (not its id).
1407
+ :param title: A new title.
1408
+ :param description: A new description; pass ``""`` to empty it.
1409
+ :param state: ``open`` or ``closed``.
1410
+ :param due_on: A new due date, as an ISO-8601 timestamp. GitHub's
1411
+ milestone endpoint takes no null due date, so an existing one can be
1412
+ moved but not removed.
1413
+ :raises ValueError: If no field is given, if ``state`` is neither open
1414
+ nor closed, or if ``due_on`` is empty.
1415
+ """
1416
+ json: dict[str, Any] = {}
1417
+ if not isinstance(title, Unset):
1418
+ json["title"] = title
1419
+ if not isinstance(description, Unset):
1420
+ json["description"] = description
1421
+ if not isinstance(state, Unset):
1422
+ _validate_issue_state(state, noun="milestone")
1423
+ json["state"] = str(state)
1424
+ if not isinstance(due_on, Unset):
1425
+ if not due_on:
1426
+ raise ValueError(
1427
+ "due_on must be a non-empty ISO-8601 timestamp; GitHub "
1428
+ "rejects an empty one and offers no way to clear a due date."
1429
+ )
1430
+ json["due_on"] = due_on
1431
+ if not json:
1432
+ raise ValueError(
1433
+ f"No field to update was given for milestone #{milestone_number}; "
1434
+ "pass at least one of title, description, state or due_on."
1435
+ )
1436
+ return self._patch(
1437
+ url=f"{self._url_milestones}/{milestone_number}", json=json
1438
+ ).json()
1439
+
1440
+ def delete_milestone(self, milestone_number: int) -> requests.Response:
1441
+ """Delete a milestone from this repository.
1442
+
1443
+ Issues associated with the milestone are not deleted; they simply lose it.
1444
+
1445
+ :param milestone_number: The ``number`` of the milestone (not its id).
1446
+ """
1447
+ return self._delete(url=f"{self._url_milestones}/{milestone_number}")
1448
+
940
1449
  def get_issue_comments(self, issue_number: int, n: int = 0) -> list[dict[str, Any]]:
941
1450
  """List comments on an issue in this repository.
942
1451
 
@@ -954,7 +1463,6 @@ class Repository(GitHub):
954
1463
  return self._post(
955
1464
  url=f"{self._url_issues}/{issue_number}/comments",
956
1465
  json={"body": body},
957
- timeout=10,
958
1466
  ).json()
959
1467
 
960
1468
  def archive(self) -> requests.Response:
@@ -14,6 +14,8 @@ from dulwich.refs import Ref
14
14
  from dulwich.repo import Repo
15
15
  from tenacity import retry, stop_after_attempt, wait_exponential
16
16
 
17
+ from github_rest_api.utils import as_str_sequence
18
+
17
19
 
18
20
  def _get_commit(name: bytes) -> bytes:
19
21
  """Resolve a commit SHA or branch name to a commit SHA string."""
@@ -69,6 +71,8 @@ def has_relevant_changes(
69
71
  ) -> bool:
70
72
  if not commit1 or not commit2:
71
73
  return True
74
+ image_dirs = as_str_sequence(image_dirs)
75
+ paths_monitoring = as_str_sequence(paths_monitoring)
72
76
  if isinstance(commit1, str):
73
77
  commit1 = commit1.encode()
74
78
  if isinstance(commit2, str):
@@ -149,6 +153,8 @@ def build_images(
149
153
  tool: str = "podman",
150
154
  registry: str = "quay.io/legendu",
151
155
  ):
156
+ image_dirs = as_str_sequence(image_dirs)
157
+ paths_monitoring = as_str_sequence(paths_monitoring)
152
158
  _validate_paths_exist(image_dirs, "image dirs")
153
159
  _validate_paths_exist(paths_monitoring, "monitored paths")
154
160
  if not has_relevant_changes(commit1, commit2, image_dirs, paths_monitoring):
@@ -70,8 +70,9 @@ def parse_args(args=None, namespace=None) -> Namespace:
70
70
  parser.add_argument(
71
71
  "--merge-method",
72
72
  dest="merge_method",
73
- choices=[m.value for m in MergeMethod],
74
- default=MergeMethod.MERGE.value,
73
+ type=MergeMethod,
74
+ choices=list(MergeMethod),
75
+ default=MergeMethod.MERGE,
75
76
  help="The merge method to use.",
76
77
  )
77
78
  parser.add_argument(
@@ -10,8 +10,10 @@ from collections.abc import Sequence
10
10
  from pathlib import Path
11
11
 
12
12
  from dulwich import porcelain
13
+ from dulwich.refs import HEADREF, LOCAL_BRANCH_PREFIX, Ref
13
14
 
14
15
  from github_rest_api import Organization, User
16
+ from github_rest_api.utils import as_str_sequence
15
17
 
16
18
  logger = logging.getLogger(__name__)
17
19
 
@@ -55,12 +57,27 @@ def _active_branch(path: Path) -> str:
55
57
  return porcelain.active_branch(path).decode()
56
58
  except (IndexError, ValueError) as e:
57
59
  raise ValueError(
58
- f"HEAD of the local repo at '{path}' is detached (not on any branch), "
59
- "which is unsupported (this happens, e.g., with a colocated Jujutsu repo). "
60
+ f"Cannot determine the initial branch of the local repo at '{path}' "
61
+ "as HEAD does not point at a branch. "
60
62
  "Check out a branch (e.g. `git switch -c main`) before running this command."
61
63
  ) from e
62
64
 
63
65
 
66
+ def _head_and_branches(path: Path) -> tuple[str | None, set[str]]:
67
+ """Resolve HEAD and the local branches with a single open of the repo.
68
+
69
+ Returns the commit SHA HEAD points at (None if it does not resolve to one)
70
+ and the existing local branch names (without the `refs/heads/` prefix).
71
+ """
72
+ with porcelain.open_repo_closing(path) as repo:
73
+ try:
74
+ head = repo.refs[HEADREF].decode()
75
+ except KeyError:
76
+ head = None
77
+ branches = {b.decode() for b in repo.refs.keys(base=Ref(LOCAL_BRANCH_PREFIX))}
78
+ return head, branches
79
+
80
+
64
81
  def _init_local_repo(
65
82
  repo: str,
66
83
  language: str,
@@ -83,7 +100,9 @@ def _init_local_repo(
83
100
  if not (path / ".git").exists():
84
101
  porcelain.init(path=path)
85
102
  logger.info("Initialized empty Git repository in %s", path)
86
- if not porcelain.branch_list(path):
103
+ head, existing_branches = _head_and_branches(path)
104
+ if head is None and not existing_branches:
105
+ # A brand new repository: no commit and no branch yet.
87
106
  initial_branch = _active_branch(path)
88
107
  porcelain.add(repo=path, paths=["README.md"])
89
108
  logger.info("Added README.md to staging")
@@ -99,11 +118,28 @@ def _init_local_repo(
99
118
  porcelain.branch_delete(repo=path, name=initial_branch)
100
119
  logger.info("Deleted initial branch '%s'", initial_branch)
101
120
  else:
102
- existing_branches = {b.decode() for b in porcelain.branch_list(path)}
103
- for branch in branches:
104
- if branch not in existing_branches:
105
- porcelain.branch_create(repo=path, name=branch)
106
- logger.info("Created branch '%s' from HEAD", branch)
121
+ missing = [b for b in branches if b not in existing_branches]
122
+ if missing and head is None:
123
+ # HEAD is unborn (e.g. after `git switch --orphan`) but branches
124
+ # already exist, so there is no commit to branch from. Without this,
125
+ # dulwich falls back to its "HEAD" objectish default and fails deep
126
+ # inside the object store with "Invalid object name b'HEAD'".
127
+ # `existing_branches` is never empty here: an empty one with a None
128
+ # head takes the brand-new-repository path above.
129
+ raise ValueError(
130
+ "Cannot create the branch(es) "
131
+ f"{', '.join(repr(b) for b in missing)} in the local repo at "
132
+ f"'{path}' as HEAD does not point at a commit to branch from. "
133
+ f"Check out an existing branch (e.g. "
134
+ f"`git switch {sorted(existing_branches)[0]}`) "
135
+ "before running this command."
136
+ )
137
+ # Point new branches at HEAD's commit explicitly. Dulwich fails to resolve
138
+ # its default "HEAD" objectish when HEAD is detached, which is the normal
139
+ # state of a colocated Jujutsu repository.
140
+ for branch in missing:
141
+ porcelain.branch_create(repo=path, name=branch, objectish=head)
142
+ logger.info("Created branch '%s' from HEAD", branch)
107
143
  _ensure_remote(path, repo, protocol)
108
144
  if push:
109
145
  for branch in branches:
@@ -132,6 +168,7 @@ def create_github_repo(
132
168
  push: bool,
133
169
  branches: Sequence[str] = ("main",),
134
170
  ) -> None:
171
+ branches = as_str_sequence(branches)
135
172
  token = token or os.getenv("GITHUB_TOKEN", "")
136
173
  if not token:
137
174
  token = getpass.getpass("Please enter your GitHub token: ")
@@ -5,6 +5,7 @@ The branch is updated (using dev) before creating the PR.
5
5
  import os
6
6
  import sys
7
7
  from argparse import ArgumentParser, Namespace
8
+ from typing import Any
8
9
 
9
10
  from github_rest_api import Repository
10
11
  from github_rest_api.github import DEFAULT_PR_MODEL
@@ -64,6 +65,12 @@ def parse_args(args=None, namespace=None) -> Namespace:
64
65
  action="store_true",
65
66
  help="Update the head branch using the base branch before creating the pull request.",
66
67
  )
68
+ parser.add_argument(
69
+ "--draft",
70
+ dest="draft",
71
+ action="store_true",
72
+ help="Create the pull request as a draft.",
73
+ )
67
74
  parser.add_argument(
68
75
  "--model",
69
76
  dest="model",
@@ -100,7 +107,7 @@ def main() -> int:
100
107
  repo = Repository(args.token, os.environ["GITHUB_REPOSITORY"])
101
108
  if args.update:
102
109
  repo.update_branch(update=args.head_branch, upstream=args.base_branch)
103
- pull_request = {
110
+ pull_request: dict[str, Any] = {
104
111
  "base": args.base_branch,
105
112
  "head": args.head_branch,
106
113
  }
@@ -108,6 +115,8 @@ def main() -> int:
108
115
  pull_request["title"] = args.title
109
116
  if args.body:
110
117
  pull_request["body"] = args.body
118
+ if args.draft:
119
+ pull_request["draft"] = True
111
120
  # An empty model generates the title/body deterministically; a --model uses
112
121
  # an LLM (falling back to deterministic generation on failure).
113
122
  repo.create_pull_request(pull_request, model=args.model)
@@ -9,6 +9,8 @@ from typing import Any, Iterable
9
9
  from dulwich import porcelain
10
10
  from dulwich.repo import Repo
11
11
 
12
+ from github_rest_api.utils import as_str_sequence
13
+
12
14
 
13
15
  def config_git(local_repo_dir: str | Path, user_email: str, user_name: str):
14
16
  """Config Git.
@@ -89,6 +91,7 @@ def find_project_root(path: Path | None = None) -> Path | None:
89
91
 
90
92
 
91
93
  def get_toml_value(path: Path, keys: Sequence[str]) -> Any:
94
+ keys = as_str_sequence(keys)
92
95
  if not keys or not path.exists():
93
96
  return None
94
97
  try:
github_rest_api/utils.py CHANGED
@@ -14,6 +14,25 @@ def partition(pred, iterable):
14
14
  return filter(pred, it1), filterfalse(pred, it2)
15
15
 
16
16
 
17
+ def as_str_sequence(value: str | Sequence[str]) -> Sequence[str]:
18
+ """Wrap a bare string into a one-element sequence, leaving others untouched.
19
+
20
+ A `str` is itself a `Sequence[str]` of its own characters, so a caller who
21
+ passes a single value where a collection is expected is silently exploded
22
+ into one entry per character. No type checker flags this, because the
23
+ annotation really is satisfied.
24
+
25
+ An empty string yields an empty sequence rather than `[""]`, so a caller
26
+ passing an unset optional (from `os.getenv`, a config field or an argparse
27
+ default) keeps meaning "nothing" instead of "one empty entry".
28
+
29
+ :param value: A sequence of strings, or a single string.
30
+ """
31
+ if isinstance(value, str):
32
+ return [value] if value else []
33
+ return value
34
+
35
+
17
36
  def compile_patterns(patterns: str | Sequence[str] | None) -> list[re.Pattern[str]]:
18
37
  """Compile a list of regular expression patterns.
19
38
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-rest-api
3
- Version: 0.48.0
3
+ Version: 0.49.0
4
4
  Summary: Simple wrapper of GitHub REST APIs.
5
5
  Author-email: Ben Du <longendu@yahoo.com>
6
6
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -1,21 +1,21 @@
1
- github_rest_api/__init__.py,sha256=VVpiUHVLbjjJGNN88OmEvCwq4xmBnXv68T8Apk_lwVc,282
2
- github_rest_api/github.py,sha256=tcP2ETyIySFto82S73IZLDaRaRrj9bZjd9wi-VWKUIo,45427
1
+ github_rest_api/__init__.py,sha256=cY28SPof_s_VfcsDe10lqfEFWFTpRkrmygz_EBsB-ec,410
2
+ github_rest_api/github.py,sha256=K2zdt0faIC0CodI-Iu1gM97JYrVq7bzy-8CYimewoXU,68602
3
3
  github_rest_api/pr_content.py,sha256=og1VwsMtwqwl-HUJcJrkwjUjFPw9E1k5xDtJVMAXBRo,10039
4
- github_rest_api/utils.py,sha256=RspoIZik0g105KpyFXCf4epAtgu1dUsxwlwUSSGhXPI,2697
4
+ github_rest_api/utils.py,sha256=it5zHCNhXTut4M-PWR-BtWm63_rpFSc_xRc4rbDJ8Qw,3496
5
5
  github_rest_api/scripts/__init__.py,sha256=NkHTngnnWXPc0JmO5mqVKjDf4JPofuwWjDW1SU3aE7Y,36
6
- github_rest_api/scripts/utils.py,sha256=o5GzgSNyGHUIHhaaG5QUfXvv6fsTQPCHD9clGRNMggU,4588
6
+ github_rest_api/scripts/utils.py,sha256=y-qF_2cpf0KhMp9TWvpb6eBc8NW7Bu3fkQDdYlXG9Cc,4672
7
7
  github_rest_api/scripts/cargo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  github_rest_api/scripts/cargo/benchmark.py,sha256=wtxM9nQxELEUliWu8EGWqb42TRTOK9ufj4GxqxJeIS4,8541
9
9
  github_rest_api/scripts/cargo/profiling.py,sha256=5jjTejhz3MmvOTz7cvdQGhkd6PB5salXnPwmhxeo2Ys,4279
10
10
  github_rest_api/scripts/cargo/utils.py,sha256=tXp5mnVz0OSWfZSIP8COiB9rjX9XKILcCV62kf0ZAXw,339
11
11
  github_rest_api/scripts/container/__init__.py,sha256=9faBHMYJhqU2BmVZootquPJIaYgW8hf6PfIlPnHyaU0,48
12
- github_rest_api/scripts/container/build_container_images.py,sha256=-ZPPKVUpc0v-_5DuPldBykSCBo8ojzriIgrU5ODIZXg,8384
12
+ github_rest_api/scripts/container/build_container_images.py,sha256=7ifuyqZeOjjgx1B-R1lUc53uU4kbmWXMhPkO18TnwkQ,8639
13
13
  github_rest_api/scripts/container/config_container.py,sha256=BtOqgZlQFM9cWZMoHKvOzNsyXxCl_DLqYWbMqu1YOWM,2835
14
14
  github_rest_api/scripts/container/update_version_containerfile.py,sha256=1PkuJkAsJBCEMdacMSndbuZKcmgUyXy6nBArnxKoPZk,4535
15
15
  github_rest_api/scripts/github/__init__.py,sha256=tEU1SdCS51M4M13WoqXZt3rjGEAI_QgNljt5XAyQJ_w,38
16
- github_rest_api/scripts/github/auto_merge_pull_request.py,sha256=_5191Xt_b8DtxjIqHrdY-gKLxvkzWd8oWoSbF2uauc8,3527
17
- github_rest_api/scripts/github/create_github_repo.py,sha256=6WBMcHtY0rn_ZaF_3TN_sX2QFpklE4TPKNSXOPBwmw0,8919
18
- github_rest_api/scripts/github/create_pull_request.py,sha256=gXtRRsxzegGxagHb6k3kxtRd3mt4QVwSPZQpsFnzz1Y,3968
16
+ github_rest_api/scripts/github/auto_merge_pull_request.py,sha256=x7oUN6wvlM61mHUic3r2SKnPz2xxFcX15c5ZuAmU78A,3534
17
+ github_rest_api/scripts/github/create_github_repo.py,sha256=OYvlhzH65mIj41Z2daHs4EX-aoyG1Y1_tGSYYKSeb54,10812
18
+ github_rest_api/scripts/github/create_pull_request.py,sha256=vmvZkQcQDjYHnECUW5q7B4DHkPDVaHEqKQPb2IzxewY,4216
19
19
  github_rest_api/scripts/github/release_on_github.py,sha256=9psaIv-GtrHdvE-tkw0yxCMDmW0bPts-db2vUxhoKXo,4368
20
20
  github_rest_api/scripts/github/remove_branch.py,sha256=iKZ4Y_lQlSOIfifC4plqP-B6pEMnFrknGMvV1oLnuLc,3538
21
21
  github_rest_api/scripts/github/workflows/auto_merge_pull_request.yaml,sha256=LOCYzTnubAqBbeGunSdnEt453ZLwJ2870YxRlcPJ7GE,768
@@ -25,7 +25,7 @@ github_rest_api/scripts/github/workflows/create_pr_to_main.yaml,sha256=mgHPlewkC
25
25
  github_rest_api/scripts/github/workflows/remove_branch.yaml,sha256=KDpB76ovrhuRdP0HN5j6Th9gjIQWSDdh8b4b4yAgtoI,544
26
26
  github_rest_api/scripts/github/workflows/python/lint.yaml,sha256=toP2oUdVIKyu_n20Dr14k5cJYxN8rPksfbX4sfflXxc,699
27
27
  github_rest_api/scripts/github/workflows/python/test.yaml,sha256=86MljYEWX1E9kJYmhMCLwu46WNEWvoyO9PwqeRew540,887
28
- github_rest_api-0.48.0.dist-info/METADATA,sha256=GUo2AkySz1q5KewbBDYUPV-2Yvbchmhd4WXyT2zescs,915
29
- github_rest_api-0.48.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
30
- github_rest_api-0.48.0.dist-info/entry_points.txt,sha256=OfbP9GuQC7SyGbymfCYrXEG-TCcKuu31v0EPNvHc1Q4,659
31
- github_rest_api-0.48.0.dist-info/RECORD,,
28
+ github_rest_api-0.49.0.dist-info/METADATA,sha256=yXdqqS70hYq-w5uu3-dMaCTAAh0xmtNWmOWBFlZgU9Q,915
29
+ github_rest_api-0.49.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
30
+ github_rest_api-0.49.0.dist-info/entry_points.txt,sha256=OfbP9GuQC7SyGbymfCYrXEG-TCcKuu31v0EPNvHc1Q4,659
31
+ github_rest_api-0.49.0.dist-info/RECORD,,