diff --git a/.github/pr.py b/.github/pr.py index 5428c58..af1f049 100755 --- a/.github/pr.py +++ b/.github/pr.py @@ -53,7 +53,7 @@ def main(): # skip branches with the pattern _* if args.head_branch.startswith("_"): return - repo = Repository(args.token, "legendu-net", "github_rest_api") + repo = Repository(args.token, "legendu-net/github_rest_api") repo.create_pull_request( { "base": args.base_branch, diff --git a/github_rest_api/actions/cargo/benchmark.py b/github_rest_api/actions/cargo/benchmark.py index b700722..b884405 100644 --- a/github_rest_api/actions/cargo/benchmark.py +++ b/github_rest_api/actions/cargo/benchmark.py @@ -5,20 +5,20 @@ from pathlib import Path import datetime import shutil +import subprocess as sp +from dulwich import porcelain from ..utils import ( config_git, - create_branch, switch_branch, push_branch, gen_temp_branch, commit_benchmarks, ) -from ...utils import run_cmd def _copy_last_dev_bench(bench_dir: Path) -> None: branch = gen_temp_branch() - create_branch(branch) + porcelain.checkout(repo=".", new_branch=branch) switch_branch(branch="gh-pages", fetch=True) src = bench_dir / "dev/criterion" tmpdir = tempfile.mkdtemp() @@ -37,7 +37,11 @@ def _cargo_criterion(bench_dir: Path, env: str = "") -> None: :param branch: The branch to benchmark. """ _copy_last_dev_bench(bench_dir=bench_dir) - run_cmd(f"{env} cargo criterion --all-features --message-format=json") + sp.run( + f"{env} cargo criterion --all-features --message-format=json", + shell=True, + check=True, + ) def _copy_bench_results(bench_dir: Path, storage: str) -> None: @@ -63,7 +67,7 @@ def _git_push_gh_pages(bench_dir: Path, pr_number: str) -> str: commit_benchmarks(bench_dir=bench_dir) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") branch = f"gh-pages_{pr_number}_{timestamp}" - create_branch(branch=branch) + porcelain.checkout(repo=".", new_branch=branch) push_branch(branch=branch) return branch diff --git a/github_rest_api/actions/cargo/profiling.py b/github_rest_api/actions/cargo/profiling.py index 1032fac..b55094c 100644 --- a/github_rest_api/actions/cargo/profiling.py +++ b/github_rest_api/actions/cargo/profiling.py @@ -8,7 +8,7 @@ import psutil from .utils import build_project from ..utils import config_git, switch_branch, push_branch, commit_profiling -from ...utils import partition, run_cmd +from ...utils import partition def launch_application(cmd: list[str]) -> int: @@ -64,13 +64,13 @@ def nperf(pid: int, prof_name: str, prof_dir: str | Path = ".") -> Path: yymmdd = time.strftime("%Y%m%d") prof_dir.mkdir(exist_ok=True, parents=True) data_file = prof_dir / f"{yymmdd}_{prof_name}" - run_cmd(f"nperf record -p {pid} -o '{data_file}'") + sp.run(f"nperf record -p {pid} -o '{data_file}'", shell=True, check=True) return _gen_flamegraph(data_file) def _gen_flamegraph(data_file: Path) -> Path: flamegraph = data_file.with_name(data_file.name + ".svg") - run_cmd(f"nperf flamegraph '{data_file}' > '{flamegraph}'") + sp.run(f"nperf flamegraph '{data_file}' > '{flamegraph}'", shell=True, check=True) return flamegraph diff --git a/github_rest_api/actions/cargo/utils.py b/github_rest_api/actions/cargo/utils.py index 76be94c..2b9e5c6 100644 --- a/github_rest_api/actions/cargo/utils.py +++ b/github_rest_api/actions/cargo/utils.py @@ -1,10 +1,12 @@ """Util functions for building GitHub Actions for Rust projects.""" -from ...utils import run_cmd +import subprocess as sp def build_project(profile: str = "release") -> None: """Build the Rust project. :param profile: The profile for building. """ - run_cmd(f"RUSTFLAGS=-Awarnings cargo build --profile {profile}") + sp.run( + f"RUSTFLAGS=-Awarnings cargo build --profile {profile}", shell=True, check=True + ) diff --git a/github_rest_api/actions/utils.py b/github_rest_api/actions/utils.py index 5e7c3d8..2b42c68 100644 --- a/github_rest_api/actions/utils.py +++ b/github_rest_api/actions/utils.py @@ -3,17 +3,8 @@ from typing import Iterable from pathlib import Path import random -from ..utils import run_cmd - - -class FailToPushToGitHubException(Exception): - """Exception for failure to push a branch to GitHub.""" - - def __init__(self, branch: str, branch_alt: str): - msg = f"Failed to push the branch {branch} to GitHub!" - if branch_alt: - msg += f" Pushed to {branch_alt} instead." - super().__init__(msg) +from dulwich import porcelain +from dulwich.repo import Repo def config_git(local_repo_dir: str | Path, user_email: str, user_name: str): @@ -22,18 +13,9 @@ def config_git(local_repo_dir: str | Path, user_email: str, user_name: str): :param user_email: The email of the user (no need to be a valid one). :param user_name: The name of the user. """ - cmd = f"""git config --global --add safe.directory {local_repo_dir} \ - && git config --global user.email "{user_email}" \ - && git config --global user.name "{user_name}" - """ - run_cmd(cmd) - - -def create_branch(branch: str) -> None: - """Create a new local branch. - :param branch: The new local branch to create. - """ - run_cmd(f"git checkout -b {branch}") + config = Repo(local_repo_dir).get_config() + config.set(b"user", b"email", user_email.encode()) + config.set(b"user", b"name", user_name.encode()) def switch_branch(branch: str, fetch: bool) -> None: @@ -42,8 +24,8 @@ def switch_branch(branch: str, fetch: bool) -> None: :param fetch: If true, fetch the branch from remote first. """ if fetch: - run_cmd(f"git fetch origin {branch}") - run_cmd(f"git checkout {branch}") + porcelain.fetch(repo=".") + porcelain.checkout(repo=".", target=branch) def gen_temp_branch( @@ -67,26 +49,27 @@ def push_branch(branch: str, branch_alt: str = ""): :param branch_alt: An alternative branch name to push to GitHub. """ try: - run_cmd(f"git push origin {branch}") + porcelain.push(repo=".", refspecs=branch) except Exception as err: if branch_alt: - cmd = f"""git checkout {branch} \ - && git checkout -b {branch_alt} \ - && git push origin {branch_alt} - """ - run_cmd(cmd) - raise FailToPushToGitHubException(branch, branch_alt) from err + porcelain.checkout(repo=".", target=branch) + porcelain.checkout(repo=".", new_branch=branch_alt) + porcelain.push(repo=".", refspecs=branch_alt) + else: + raise err def commit_benchmarks(bench_dir: str | Path): """Commit changes in the benchmark directory. :param bench_dir: The benchmark directory. """ - run_cmd(f"git add {bench_dir} && git commit -m 'add benchmarks'") + porcelain.add(paths=bench_dir) + porcelain.commit(message="Add benchmarks.") def commit_profiling(prof_dir: str | Path): """Commit changes in the profiling directory. :param prof_dir: The profiling directory. """ - run_cmd(f"git add {prof_dir} && git commit -m 'update profiling results'") + porcelain.add(paths=prof_dir) + porcelain.commit(message="Updating profiling results.") diff --git a/github_rest_api/github.py b/github_rest_api/github.py index 4dc3442..808820e 100644 --- a/github_rest_api/github.py +++ b/github_rest_api/github.py @@ -80,20 +80,18 @@ def put(self, url, raise_for_status: bool = True) -> requests.Response: class Repository(GitHub): """Abstraction of a GitHub repository.""" - def __init__(self, token: str, owner: str, repo: str): + def __init__(self, token: str, repo: str): """Initialize Repository. :param token: An authorization token for GitHub REST APIs. - :param owner: The owner of the repository. - :param repo: The name of the repository. + :param repo: A GitHub repository (in the format of owner/repo). """ super().__init__(token) - self._owner = owner self._repo = repo - self._url_pull = f"https://api.github.com/repos/{owner}/{repo}/pulls" - self._url_branches = f"https://api.github.com/repos/{owner}/{repo}/branches" - self._url_refs = f"https://api.github.com/repos/{owner}/{repo}/git/refs" - self._url_issues = f"https://api.github.com/repos/{owner}/{repo}/issues" - self._url_releases = f"https://api.github.com/repos/{owner}/{repo}/releases" + self._url_pull = f"https://api.github.com/repos/{repo}/pulls" + self._url_branches = f"https://api.github.com/repos/{repo}/branches" + self._url_refs = f"https://api.github.com/repos/{repo}/git/refs" + self._url_issues = f"https://api.github.com/repos/{repo}/issues" + self._url_releases = f"https://api.github.com/repos/{repo}/releases" def get_releases(self) -> list[dict[str, Any]]: """List releases in this repository.""" @@ -315,4 +313,4 @@ def get_repositories( return repos def instantiate_repository(self, repo: str) -> Repository: - return Repository(token=self._token, owner=self._owner, repo=repo) + return Repository(token=self._token, repo=f"{self._owner}/{repo}") diff --git a/github_rest_api/utils.py b/github_rest_api/utils.py index fecf948..26f6b7e 100644 --- a/github_rest_api/utils.py +++ b/github_rest_api/utils.py @@ -1,8 +1,6 @@ """Some generally useful util functions.""" from itertools import tee, filterfalse -import logging -import subprocess as sp def partition(pred, iterable): @@ -12,15 +10,3 @@ def partition(pred, iterable): """ it1, it2 = tee(iterable) return filter(pred, it1), filterfalse(pred, it2) - - -def run_cmd(cmd: list | str, capture_output: bool = False) -> None: - """Run a shell command. - - :param cmd: The command to run. - :param capture_output: Whether to capture stdout and stderr of the command. - """ - proc = sp.run( - cmd, shell=isinstance(cmd, str), check=True, capture_output=capture_output - ) - logging.debug(proc.args) diff --git a/pyproject.toml b/pyproject.toml index c2f9834..d8ca3a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "github_rest_api" -version = "0.26.0" +version = "0.27.0" description = "Simple wrapper of GitHub REST APIs." authors = [{ name = "Ben Du", email = "longendu@yahoo.com" }] requires-python = ">=3.11,<4" @@ -8,6 +8,7 @@ readme = "README.md" dependencies = [ "requests>=2.28.2", "psutil>=5.9.4", + "dulwich>=0.25.1", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index 0776145..4bdbc05 100644 --- a/uv.lock +++ b/uv.lock @@ -134,11 +134,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/b8/68d6ca1d8a16061e79693587560f6d24ac18ba9617804d7808b2c988d9d5/deptry-0.24.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:03d375db3e56821803aeca665dbb4c2fd935024310350cc18e8d8b6421369d2b", size = 1629786, upload-time = "2025-11-09T00:31:49.469Z" }, ] +[[package]] +name = "dulwich" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/75/5c1d2c82e4e4542fdc44614f42dafb1c1ec1627a8e8bd5800d37bcb7b33b/dulwich-0.25.1.tar.gz", hash = "sha256:ce50b588981d30cd700fbf7352eea6f9aed9a9fc2823dda5a7d183a4e13d0ab8", size = 1125985, upload-time = "2026-01-08T22:30:00.915Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/7b/e613675c6ac2de6563391b030a074c6c2aaf24aecc6f4aa29c9a7dffc91a/dulwich-0.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ffa8166321cf84a1aa31d091c74f11480831347c5a89f8082b2ad519c40d8ae", size = 1338636, upload-time = "2026-01-08T22:29:24.054Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b4/f6a31ab73f77e700abb7151df81a3ed8629626506fe0822529f5b6d428d2/dulwich-0.25.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:fe517a1a56c69a0dad252c391122d1d7ff063c7eb31066172396976aa99098a0", size = 1401921, upload-time = "2026-01-08T22:29:26.346Z" }, + { url = "https://files.pythonhosted.org/packages/f6/89/253055ec2388a73d6fa635181f65405d3c7eff7aaf993db294c0327f1080/dulwich-0.25.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:6d796c5f5c1e3951a7422046419414f88c4bf282ffb29b0e9b5e7ff821a9ac81", size = 1430622, upload-time = "2026-01-08T22:29:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4c/434f33c450c7aa56f4e0e5188c19db922126fe9393b9d973e2c0fcb0012b/dulwich-0.25.1-cp311-cp311-win32.whl", hash = "sha256:7170de4380e1e0fd1016e567fb932d94f28577fc6a259fd2e25bc25fc86a416b", size = 987284, upload-time = "2026-01-08T22:29:30.231Z" }, + { url = "https://files.pythonhosted.org/packages/70/db/c5db5af35ceef7889bf859bc09dd722931d063852fa235acdedc63977458/dulwich-0.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:a70de3b625cd4cfc249bd24dd5292d1409e76e1d45344e83f408be0a24906862", size = 1003791, upload-time = "2026-01-08T22:29:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c0/cfca073c55a90a01bea57d6152f3237c3d9feca318c58894bbf3d18a0afc/dulwich-0.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c7756d6f22d11da42fc803584edb290b2cd2a72a6460834644349c739dccab1", size = 1317978, upload-time = "2026-01-08T22:29:33.53Z" }, + { url = "https://files.pythonhosted.org/packages/93/6e/b2e23f55c17631b178e6e15e19bad85eb2c268e83e5360b30304e53bd889/dulwich-0.25.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:849195ae2cc245ae2e1b2d67859582e923a7e363f5f9ae5212029429762d9901", size = 1394770, upload-time = "2026-01-08T22:29:36.045Z" }, + { url = "https://files.pythonhosted.org/packages/b4/95/aa64c3eee368f2aa9c6e6643d9bf8f6c9404fe17a1f64c53c37dfbf66781/dulwich-0.25.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:bd670ef6716ac98888ebef423addf25aa921d99d7a8e8199bce59aa9c34525dd", size = 1422758, upload-time = "2026-01-08T22:29:37.775Z" }, + { url = "https://files.pythonhosted.org/packages/ac/29/6262998a83c0e4d1f51647688a12a49095b1258678633a050e05d338b581/dulwich-0.25.1-cp312-cp312-win32.whl", hash = "sha256:efa3922dbf1b9694cf592682f722bbead9eebccc84fa8adf75d3a40b2cd8eb6c", size = 982455, upload-time = "2026-01-08T22:29:39.479Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b0/f6a38a00b4a98c25e92fee0f8f898f1ddb0fe79f868ec2ee13f76f9f5259/dulwich-0.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb69358e22a7241e398273e16c9754c10a1f01a2f4e66eb3272b17be3016554e", size = 999874, upload-time = "2026-01-08T22:29:40.908Z" }, + { url = "https://files.pythonhosted.org/packages/4a/61/e7a9e4e0ded1ee18a727027a4cfaef4ec90d4ee481a93385ac4928b73b18/dulwich-0.25.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:d86d028109b3fe24c1d5f87304f2078ae4b8813e954d742049755161bd14b2e6", size = 1419488, upload-time = "2026-01-08T22:29:42.999Z" }, + { url = "https://files.pythonhosted.org/packages/25/a9/96c427fad6a35d0266bd6d5bad9439d5b6a14a66ec077cc7320c5befb3e9/dulwich-0.25.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:7b4534dffe836d5654a54cc4ecaecfd909846f065ae148a6fdde3d8dd8091552", size = 1419480, upload-time = "2026-01-08T22:29:44.975Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0d/ba0b11b1dadccc128641db97873a6b7f74738685f91b0fe56fb817b11267/dulwich-0.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ba06d579e61ac23a3ad61f9853dd3f5c219d1b4a3bfdda2b1e92ff5f2d952655", size = 1317875, upload-time = "2026-01-08T22:29:46.353Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f3/b84550c438baf2600bdca430092d71991975e1a2f495179c78a5d9166073/dulwich-0.25.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c10685eb7f8ef6e072709a766325816a2e2c6cc2e1e2d7fc744f4afc019db542", size = 1394255, upload-time = "2026-01-08T22:29:48.144Z" }, + { url = "https://files.pythonhosted.org/packages/32/0f/1f26e67bf07a2aa1f7828a9e99b7002db35db2af67888d30497ac8bb5326/dulwich-0.25.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6c20393b9e68f68bd772d008ad9a0ce92c9e4cbdc98ab04953e3af668b5dd7e1", size = 1422149, upload-time = "2026-01-08T22:29:49.948Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d1/a56770ef4bd7c243a1a4a255f03ca0679372a57ec6fd9585ac8c0d83ec8a/dulwich-0.25.1-cp313-cp313-win32.whl", hash = "sha256:1316bfd979f708a894bb953c6a1618762169ce7b2db0ea58edffb0e1fc3733ce", size = 983220, upload-time = "2026-01-08T22:29:51.885Z" }, + { url = "https://files.pythonhosted.org/packages/29/2b/a4b69d5d3b5905f0075fa8bc323b3cd83e347e7f379e8172b4a096133b71/dulwich-0.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:5c394bcfea736b380b1e5490b28f791f21769bae7aa198c4bd200ad7aa94b9bd", size = 999799, upload-time = "2026-01-08T22:29:53.897Z" }, + { url = "https://files.pythonhosted.org/packages/b7/42/53f88691f8a6004bc339fd5778de454b76a1e0212e25aad4c8f43ff27d44/dulwich-0.25.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:fa6832891409d59f9e4996df308794e20bed266428cc523ffbb6f515ff95fad4", size = 1437419, upload-time = "2026-01-08T22:29:55.607Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c1/e1f16e87c2934260eb62d13f0248e1d8cd22e4c47e456a122d7ae74d2e82/dulwich-0.25.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:ed00537e01eb1533f2f06dab7c266ea166da9d9ffe88104fe81be84facdb9252", size = 1437411, upload-time = "2026-01-08T22:29:57.326Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ba/20ea6da9f837c509195e630ee854de24ff473a3f4e0036016df41fe37dbd/dulwich-0.25.1-py3-none-any.whl", hash = "sha256:06009f7f86cf3d3cdad3ad113c8667b7d2e7bd6606638462dc10f58fc5e647c3", size = 649717, upload-time = "2026-01-08T22:29:58.965Z" }, +] + [[package]] name = "github-rest-api" version = "0.26.0" source = { editable = "." } dependencies = [ + { name = "dulwich" }, { name = "psutil" }, { name = "requests" }, ] @@ -153,6 +186,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "dulwich", specifier = ">=0.25.1" }, { name = "psutil", specifier = ">=5.9.4" }, { name = "requests", specifier = ">=2.28.2" }, ]