github-rest-api 0.41.0__tar.gz → 0.42.1__tar.gz

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 (43) hide show
  1. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/PKG-INFO +1 -1
  2. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/github.py +67 -10
  3. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/cargo/benchmark.py +8 -6
  4. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/cargo/profiling.py +7 -5
  5. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/container/build_container_images.py +7 -6
  6. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/container/config_container.py +3 -2
  7. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/container/update_version_containerfile.py +4 -2
  8. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/github/add_github_repo.py +2 -1
  9. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/github/create_pull_request.py +2 -1
  10. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/github/release_on_github.py +3 -2
  11. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/github/remove_branch.py +2 -1
  12. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/utils.py +4 -3
  13. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/utils.py +2 -2
  14. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/pyproject.toml +6 -1
  15. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/tests/test_build_container_images.py +1 -0
  16. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/tests/test_github.py +36 -1
  17. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/tests/test_release_on_github.py +2 -0
  18. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/tests/test_utils.py +1 -0
  19. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/uv.lock +1 -1
  20. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/.devcontainer/devcontainer.json +0 -0
  21. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/.github/workflows/create_pr_to_main.yaml +0 -0
  22. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/.github/workflows/lint.yaml +0 -0
  23. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/.github/workflows/release.yaml +0 -0
  24. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/.github/workflows/remove_branch.yaml +0 -0
  25. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/.github/workflows/test.yaml +0 -0
  26. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/.gitignore +0 -0
  27. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/GEMINI.md +0 -0
  28. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/README.md +0 -0
  29. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/__init__.py +0 -0
  30. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/__init__.py +0 -0
  31. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/cargo/__init__.py +0 -0
  32. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/cargo/utils.py +0 -0
  33. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/container/__init__.py +0 -0
  34. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/github/__init__.py +0 -0
  35. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/github/workflows/create_pr_dev_to_main.yaml +0 -0
  36. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/github/workflows/create_pr_to_dev.yaml +0 -0
  37. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/github/workflows/create_pr_to_main.yaml +0 -0
  38. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/github/workflows/python/lint.yaml +0 -0
  39. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/github/workflows/python/test.yaml +0 -0
  40. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/github_rest_api/scripts/github/workflows/remove_branch.yaml +0 -0
  41. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/memory/MEMORY.md +0 -0
  42. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/memory/feedback_test_runner.md +0 -0
  43. {github_rest_api-0.41.0 → github_rest_api-0.42.1}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-rest-api
3
- Version: 0.41.0
3
+ Version: 0.42.1
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,16 +1,45 @@
1
1
  """Simple wrapper of GitHub REST APIs."""
2
2
 
3
+ import re
3
4
  from abc import ABCMeta, abstractmethod
4
5
  from base64 import b64encode
5
6
  from collections.abc import Sequence
6
7
  from enum import StrEnum
7
- from typing import Any, Callable
8
8
  from pathlib import Path
9
+ from typing import Any, Callable
10
+
9
11
  import requests
10
12
  from nacl import encoding, public
11
13
 
12
14
  URL_API = "https://api.github.com"
13
15
 
16
+ _SECRET_NAME_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
17
+
18
+
19
+ def _validate_secret_name(name: str) -> None:
20
+ """Validate a secret name against GitHub's naming rules.
21
+
22
+ GitHub rejects invalid secret names with a 422 response. Validating the
23
+ name client-side surfaces a clear error before the request is sent.
24
+
25
+ :param name: The name of the secret.
26
+ :raises ValueError: If the name is empty, starts with the reserved
27
+ ``GITHUB_`` prefix, starts with a digit, or contains characters other
28
+ than alphanumerics and underscores.
29
+ """
30
+ if not name:
31
+ raise ValueError("A secret name must not be empty.")
32
+ if name.upper().startswith("GITHUB_"):
33
+ raise ValueError(
34
+ f"Invalid secret name {name!r}: names must not start with the "
35
+ "reserved 'GITHUB_' prefix."
36
+ )
37
+ if not _SECRET_NAME_PATTERN.fullmatch(name):
38
+ raise ValueError(
39
+ f"Invalid secret name {name!r}: names may only contain alphanumeric "
40
+ "characters and underscores, and must not start with a digit."
41
+ )
42
+
14
43
 
15
44
  def _encrypt_secret(public_key: str, value: str) -> str:
16
45
  """Encrypt a secret value using a LibSodium sealed box.
@@ -132,8 +161,19 @@ class GitHub:
132
161
  return resp
133
162
 
134
163
  def _extract_all(
135
- self, url: str, params: dict[str, Any] | None = None, n: int = 0
164
+ self,
165
+ url: str,
166
+ params: dict[str, Any] | None = None,
167
+ n: int = 0,
168
+ key: str | None = None,
136
169
  ) -> list[dict[str, Any]]:
170
+ """Collect all paginated items from a GitHub REST API endpoint.
171
+ :param url: The endpoint URL to request.
172
+ :param params: Query parameters forwarded to each request.
173
+ :param n: The maximum number of items to return (0 means all).
174
+ :param key: The key under which the list is nested in the response.
175
+ Defaults to None for endpoints that return a plain JSON array.
176
+ """
137
177
  params = params.copy() if params else {}
138
178
  if "per_page" not in params:
139
179
  params["per_page"] = min(100, n) if n > 0 else 100
@@ -143,10 +183,11 @@ class GitHub:
143
183
  resp = self._get(url=url, params=params.copy())
144
184
  resp.raise_for_status()
145
185
  data = resp.json()
146
- res.extend(data)
186
+ items = data if key is None else data[key]
187
+ res.extend(items)
147
188
  if n and len(res) >= n:
148
189
  return res[:n]
149
- if len(data) < params["per_page"]:
190
+ if len(items) < params["per_page"]:
150
191
  return res
151
192
  params["page"] += 1
152
193
 
@@ -315,6 +356,10 @@ class Repository(GitHub):
315
356
  """
316
357
  return self.delete_ref(ref=f"heads/{branch}")
317
358
 
359
+ def get_secrets(self, n: int = 0) -> list[dict[str, Any]]:
360
+ """List secrets in this repository."""
361
+ return self._extract_all(url=self._url_secrets, n=n, key="secrets")
362
+
318
363
  def delete_secret(self, name: str) -> requests.Response:
319
364
  """Delete a secret from this repository.
320
365
  :param name: The name of the secret to delete.
@@ -328,15 +373,19 @@ class Repository(GitHub):
328
373
  return self._get(url=f"{self._url_secrets}/public-key").json()
329
374
 
330
375
  def create_or_update_secret(
331
- self, name: str, value: str, public_key: dict[str, Any]
376
+ self, name: str, value: str, public_key: dict[str, Any] | None = None
332
377
  ) -> requests.Response:
333
378
  """Create or update a secret in this repository.
334
379
  :param name: The name of the secret.
335
380
  :param value: The plaintext value of the secret.
336
381
  :param public_key: A public key (as returned by `get_secret_public_key`)
337
- to encrypt the secret with. Fetch it once and reuse it to avoid a
338
- redundant request when creating or updating multiple secrets.
382
+ to encrypt the secret with. If not provided, it is fetched
383
+ automatically. Fetch it once and reuse it to avoid a redundant
384
+ request when creating or updating multiple secrets.
339
385
  """
386
+ _validate_secret_name(name)
387
+ if public_key is None:
388
+ public_key = self.get_secret_public_key()
340
389
  return self._put(
341
390
  url=f"{self._url_secrets}/{name}",
342
391
  json={
@@ -494,6 +543,10 @@ class Organization(Owner):
494
543
  self._url_create_repo = self._url_repos
495
544
  self._url_secrets = f"{self._url_owner}/actions/secrets"
496
545
 
546
+ def get_secrets(self, n: int = 0) -> list[dict[str, Any]]:
547
+ """List secrets in this organization."""
548
+ return self._extract_all(url=self._url_secrets, n=n, key="secrets")
549
+
497
550
  def delete_secret(self, name: str) -> requests.Response:
498
551
  """Delete an organization secret.
499
552
  :param name: The name of the secret to delete.
@@ -510,7 +563,7 @@ class Organization(Owner):
510
563
  self,
511
564
  name: str,
512
565
  value: str,
513
- public_key: dict[str, Any],
566
+ public_key: dict[str, Any] | None = None,
514
567
  visibility: SecretVisibility = SecretVisibility.ALL,
515
568
  selected_repository_ids: Sequence[int] = (),
516
569
  ) -> requests.Response:
@@ -518,17 +571,21 @@ class Organization(Owner):
518
571
  :param name: The name of the secret.
519
572
  :param value: The plaintext value of the secret.
520
573
  :param public_key: A public key (as returned by `get_secret_public_key`)
521
- to encrypt the secret with. Fetch it once and reuse it to avoid a
522
- redundant request when creating or updating multiple secrets.
574
+ to encrypt the secret with. If not provided, it is fetched
575
+ automatically. Fetch it once and reuse it to avoid a redundant
576
+ request when creating or updating multiple secrets.
523
577
  :param visibility: Which repositories can access the secret
524
578
  (all, private, or selected).
525
579
  :param selected_repository_ids: Repository IDs that can access the secret
526
580
  when visibility is `selected`.
527
581
  """
582
+ _validate_secret_name(name)
528
583
  if selected_repository_ids and visibility != SecretVisibility.SELECTED:
529
584
  raise ValueError(
530
585
  "`selected_repository_ids` can only be provided when `visibility` is 'selected'."
531
586
  )
587
+ if public_key is None:
588
+ public_key = self.get_secret_public_key()
532
589
  json: dict[str, Any] = {
533
590
  "encrypted_value": _encrypt_secret(public_key["key"], value),
534
591
  "key_id": public_key["key_id"],
@@ -1,18 +1,20 @@
1
1
  """Benchmark action using cargo criterion."""
2
2
 
3
- from typing import Callable
4
- import tempfile
5
- from pathlib import Path
6
3
  import datetime
7
4
  import shutil
8
5
  import subprocess as sp
6
+ import tempfile
7
+ from pathlib import Path
8
+ from typing import Callable
9
+
9
10
  from dulwich import porcelain
11
+
10
12
  from ..utils import (
13
+ commit_benchmarks,
11
14
  config_git,
12
- switch_branch,
13
- push_branch,
14
15
  gen_temp_branch,
15
- commit_benchmarks,
16
+ push_branch,
17
+ switch_branch,
16
18
  )
17
19
 
18
20
 
@@ -1,14 +1,16 @@
1
1
  """Utils for profiling Rust applications."""
2
2
 
3
- from typing import Iterable
4
- from pathlib import Path
5
- import time
6
3
  import datetime
7
4
  import subprocess as sp
5
+ import time
6
+ from pathlib import Path
7
+ from typing import Iterable
8
+
8
9
  import psutil
9
- from .utils import build_project
10
- from ..utils import config_git, switch_branch, push_branch, commit_profiling
10
+
11
11
  from ...utils import partition
12
+ from ..utils import commit_profiling, config_git, push_branch, switch_branch
13
+ from .utils import build_project
12
14
 
13
15
 
14
16
  def launch_application(cmd: list[str]) -> int:
@@ -1,16 +1,17 @@
1
1
  import argparse
2
- from collections.abc import Sequence
3
2
  import datetime
4
- from pathlib import Path
5
3
  import subprocess as sp
6
4
  import sys
5
+ from collections.abc import Sequence
6
+ from pathlib import Path
7
7
  from typing import cast
8
+
8
9
  import yaml
9
- from dulwich.repo import Repo
10
- from dulwich.refs import Ref
11
- from dulwich.objects import Commit
12
- from dulwich.errors import NotGitRepository
13
10
  from dulwich.diff_tree import tree_changes
11
+ from dulwich.errors import NotGitRepository
12
+ from dulwich.objects import Commit
13
+ from dulwich.refs import Ref
14
+ from dulwich.repo import Repo
14
15
  from tenacity import retry, stop_after_attempt, wait_exponential
15
16
 
16
17
 
@@ -1,11 +1,12 @@
1
1
  import argparse
2
2
  import json
3
3
  import shutil
4
+ import subprocess as sp
4
5
  import sys
5
6
  import tomllib
6
- import tomli_w
7
7
  from pathlib import Path
8
- import subprocess as sp
8
+
9
+ import tomli_w
9
10
 
10
11
 
11
12
  def config_docker(data_root: str = "/mnt/docker"):
@@ -1,13 +1,15 @@
1
1
  import argparse
2
2
  import datetime
3
3
  import os
4
+ import re
4
5
  import sys
5
6
  from pathlib import Path
6
- import re
7
+
7
8
  from dulwich import porcelain
9
+ from requests.exceptions import HTTPError
10
+
8
11
  from github_rest_api import Repository
9
12
  from github_rest_api.utils import next_minor_or_strip_patch
10
- from requests.exceptions import HTTPError
11
13
 
12
14
 
13
15
  def parse_latest_version(repo: str) -> str:
@@ -7,9 +7,10 @@ import shutil
7
7
  import sys
8
8
  from collections.abc import Sequence
9
9
  from pathlib import Path
10
+
10
11
  from dulwich import porcelain
11
12
 
12
- from github_rest_api import User, Organization
13
+ from github_rest_api import Organization, User
13
14
 
14
15
 
15
16
  def _validate_repo(repo: str) -> None:
@@ -2,9 +2,10 @@
2
2
  The branch is updated (using dev) before creating the PR.
3
3
  """
4
4
 
5
- from argparse import ArgumentParser, Namespace
6
5
  import os
7
6
  import sys
7
+ from argparse import ArgumentParser, Namespace
8
+
8
9
  from github_rest_api import Repository
9
10
  from github_rest_api.utils import compile_patterns
10
11
 
@@ -1,9 +1,10 @@
1
+ import argparse
2
+ import getpass
1
3
  import os
2
4
  import re
3
5
  import sys
4
- import argparse
5
- import getpass
6
6
  from pathlib import Path
7
+
7
8
  from github_rest_api import Repository
8
9
  from github_rest_api.scripts.utils import (
9
10
  find_project_root,
@@ -1,7 +1,8 @@
1
1
  import argparse
2
+ import datetime
2
3
  import re
3
4
  import sys
4
- import datetime
5
+
5
6
  from github_rest_api import Repository
6
7
 
7
8
 
@@ -1,10 +1,11 @@
1
1
  """Util functions for GitHub actions."""
2
2
 
3
+ import random
3
4
  import tomllib
4
- from typing import Any, Iterable
5
- from pathlib import Path
6
5
  from collections.abc import Sequence
7
- import random
6
+ from pathlib import Path
7
+ from typing import Any, Iterable
8
+
8
9
  from dulwich import porcelain
9
10
  from dulwich.repo import Repo
10
11
 
@@ -1,8 +1,8 @@
1
1
  """Some generally useful util functions."""
2
2
 
3
- from collections.abc import Sequence
4
- from itertools import tee, filterfalse
5
3
  import re
4
+ from collections.abc import Sequence
5
+ from itertools import filterfalse, tee
6
6
 
7
7
 
8
8
  def partition(pred, iterable):
@@ -4,7 +4,7 @@ requires = [ "hatchling" ]
4
4
 
5
5
  [project]
6
6
  name = "github-rest-api"
7
- version = "0.41.0"
7
+ version = "0.42.1"
8
8
  description = "Simple wrapper of GitHub REST APIs."
9
9
  readme = "README.md"
10
10
  authors = [ { name = "Ben Du", email = "longendu@yahoo.com" } ]
@@ -41,3 +41,8 @@ dev = [
41
41
  "ruff>=0.14.10",
42
42
  "ty>=0.0.8",
43
43
  ]
44
+
45
+ [tool.ruff.lint]
46
+ # select the 'I' rule set for import sorting
47
+ extend-select = ["I"]
48
+
@@ -1,5 +1,6 @@
1
1
  from pathlib import Path
2
2
  from unittest.mock import patch
3
+
3
4
  from github_rest_api.scripts.container.build_container_images import (
4
5
  has_relevant_changes,
5
6
  )
@@ -1,7 +1,16 @@
1
1
  import os
2
2
  from base64 import b64decode
3
+
4
+ import pytest
3
5
  from nacl import encoding, public
4
- from github_rest_api.github import User, Organization, Repository, _encrypt_secret
6
+
7
+ from github_rest_api.github import (
8
+ Organization,
9
+ Repository,
10
+ User,
11
+ _encrypt_secret,
12
+ _validate_secret_name,
13
+ )
5
14
 
6
15
  TOKEN = os.environ.get("GITHUB_TOKEN", "")
7
16
 
@@ -14,6 +23,32 @@ def test_encrypt_secret_roundtrip():
14
23
  assert decrypted == b"s3cret-value"
15
24
 
16
25
 
26
+ @pytest.mark.parametrize(
27
+ "name",
28
+ ["MY_SECRET", "_underscore", "Token123", "a"],
29
+ )
30
+ def test_validate_secret_name_valid(name):
31
+ _validate_secret_name(name)
32
+
33
+
34
+ @pytest.mark.parametrize(
35
+ "name",
36
+ [
37
+ "",
38
+ "GITHUB_ACTIONS",
39
+ "GITHUB_TOKEN",
40
+ "github_token",
41
+ "GitHub_Token",
42
+ "1SECRET",
43
+ "MY-SECRET",
44
+ "MY SECRET",
45
+ ],
46
+ )
47
+ def test_validate_secret_name_invalid(name):
48
+ with pytest.raises(ValueError):
49
+ _validate_secret_name(name)
50
+
51
+
17
52
  def test_user_get_repositories():
18
53
  user = User(TOKEN, "dclong")
19
54
  repos = user.get_repositories()
@@ -1,6 +1,8 @@
1
1
  from pathlib import Path
2
2
  from unittest.mock import patch
3
+
3
4
  import pytest
5
+
4
6
  from github_rest_api.scripts.github.release_on_github import _get_release_tag
5
7
 
6
8
  ROOT = Path(".")
@@ -1,4 +1,5 @@
1
1
  import pytest
2
+
2
3
  from github_rest_api.utils import next_minor_or_strip_patch, strip_patch_version
3
4
 
4
5
 
@@ -223,7 +223,7 @@ wheels = [
223
223
 
224
224
  [[package]]
225
225
  name = "github-rest-api"
226
- version = "0.40.0"
226
+ version = "0.42.1"
227
227
  source = { editable = "." }
228
228
  dependencies = [
229
229
  { name = "dulwich" },