From 4d09d62c0f7fef7fb4ab5db97aa569dd715e3afe Mon Sep 17 00:00:00 2001 From: Ben Du Date: Mon, 20 Apr 2026 09:13:59 -0700 Subject: [PATCH 1/2] refactor; add create_repository --- github_rest_api/github.py | 57 ++++++++++++++++++++++++++++++++++++--- pyproject.toml | 38 +++++++++++++++----------- uv.lock | 34 ++++++++++++++++++++++- 3 files changed, 110 insertions(+), 19 deletions(-) diff --git a/github_rest_api/github.py b/github_rest_api/github.py index ff8dd30..7f1c8c0 100644 --- a/github_rest_api/github.py +++ b/github_rest_api/github.py @@ -1,5 +1,6 @@ """Simple wrapper of GitHub REST APIs.""" +from abc import ABCMeta, abstractmethod from enum import StrEnum from typing import Any, Callable from pathlib import Path @@ -334,15 +335,21 @@ class RepositoryType(StrEnum): PRIVATE = "private" -class Organization(GitHub): +class Owner(GitHub, metaclass=ABCMeta): + """An abstract owner class representing an organization or user.""" + def __init__(self, token: str, owner: str): """Initialize Repository. :param token: An authorization token for GitHub REST APIs. - :param owner: The owner of the repository. + :param owner: The name of the owner (organization or user). """ super().__init__(token) self._owner = owner - self._url_repos = f"https://api.github.com/orgs/{owner}/repos" + self._url_repos = "" + + @abstractmethod + def _set_url_repos(self) -> None: + pass def get_repositories( self, type_: RepositoryType = RepositoryType.ALL @@ -355,3 +362,47 @@ def get_repositories( def instantiate_repository(self, repo: str) -> Repository: return Repository(token=self._token, repo=f"{self._owner}/{repo}") + + def create_repository( + self, name: str, description: str = "", private: bool = True, **kwargs + ) -> dict[str, Any]: + data = { + "name": name, + "description": description, + "homepage": "https://github.com", + "private": private, + "has_issues": True, + "has_projects": True, + "has_wiki": True, + } + return self._post(url=self._url_repos, json=data, **kwargs).json() + + +class User(Owner): + """A GitHub user.""" + + def __init__(self, token: str, user: str): + """Initialize a User. + :param token: An authorization token for GitHub REST APIs. + :param user: The name of the user. + """ + super().__init__(token=token, owner=user) + self._set_url_repos() + + def _set_url_repos(self) -> None: + self._url_repos = f"https://api.github.com/{self._owner}/repos" + + +class Organization(Owner): + """A GitHub organization.""" + + def __init__(self, token: str, org: str): + """Initialize an Organization. + :param token: An authorization token for GitHub REST APIs. + :param org: The name of the organization. + """ + super().__init__(token=token, owner=org) + self._set_url_repos() + + def _set_url_repos(self) -> None: + self._url_repos = f"https://api.github.com/orgs/{self._owner}/repos" diff --git a/pyproject.toml b/pyproject.toml index 2701db9..3f3cc6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,25 +1,33 @@ +[build-system] +build-backend = "hatchling.build" +requires = [ "hatchling" ] + [project] -name = "github_rest_api" +name = "github-rest-api" version = "0.32.0" description = "Simple wrapper of GitHub REST APIs." -authors = [{ name = "Ben Du", email = "longendu@yahoo.com" }] -requires-python = ">=3.11,<4" readme = "README.md" +authors = [ { name = "Ben Du", email = "longendu@yahoo.com" } ] +requires-python = ">=3.11,<4" +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] dependencies = [ - "requests>=2.28.2", - "psutil>=5.9.4", - "dulwich>=0.25.1", + "dulwich>=0.25.1", + "psutil>=5.9.4", + "requests>=2.28.2", ] [dependency-groups] dev = [ - "deptry>=0.24.0", - "pyright>=1.1.407", - "pytest>=9.0.2", - "ruff>=0.14.10", - "ty>=0.0.8", + "deptry>=0.24", + "pyproject-fmt>=2.21.1", + "pyright>=1.1.407", + "pytest>=9.0.2", + "ruff>=0.14.10", + "ty>=0.0.8", ] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" diff --git a/uv.lock b/uv.lock index c066908..5f3b4bd 100644 --- a/uv.lock +++ b/uv.lock @@ -195,7 +195,7 @@ wheels = [ [[package]] name = "github-rest-api" -version = "0.31.0" +version = "0.32.0" source = { editable = "." } dependencies = [ { name = "dulwich" }, @@ -206,6 +206,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "deptry" }, + { name = "pyproject-fmt" }, { name = "pyright" }, { name = "pytest" }, { name = "ruff" }, @@ -222,6 +223,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "deptry", specifier = ">=0.24.0" }, + { name = "pyproject-fmt", specifier = ">=2.21.1" }, { name = "pyright", specifier = ">=1.1.407" }, { name = "pytest", specifier = ">=9.0.2" }, { name = "ruff", specifier = ">=0.14.10" }, @@ -310,6 +312,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyproject-fmt" +version = "2.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "toml-fmt-common" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/8a/e7bc1bf2cf53a4ce24f10c2514193080f0d8d88876161602d33152dce9cc/pyproject_fmt-2.21.1.tar.gz", hash = "sha256:28221e42c4eca81a73ceacef519f3bcc0eec390d632f2fd1c14ba71d4f6362b7", size = 152372, upload-time = "2026-04-13T16:40:22.95Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/c9/c1c6f0110924a817a186581cbda7d1a88095ee2e7969ae8e1c89240fdb05/pyproject_fmt-2.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:208aaef634ed52d12c7c12f974c583c0ac22658d28dd8a93bec7e8e2760e2196", size = 5054496, upload-time = "2026-04-13T16:39:59.982Z" }, + { url = "https://files.pythonhosted.org/packages/43/4e/f493594631dc5e0a72a515cf9d3fd97abf7e3a05a407c32d90a0ad36c54a/pyproject_fmt-2.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:537c644804d4bd9940fd6a00d7bf299e5bbe5121aa07e16fb1d4124f9ce83e53", size = 4841497, upload-time = "2026-04-13T16:40:02.579Z" }, + { url = "https://files.pythonhosted.org/packages/08/fb/b636556c22fb277bf1530b147116dcb4ab74bb0cc83c060a7270eaa2ef83/pyproject_fmt-2.21.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:75ceb6b81df5f4e9185566593b25bce460478af3361f17d4012f8045aa4f11ab", size = 4999780, upload-time = "2026-04-13T16:40:04.943Z" }, + { url = "https://files.pythonhosted.org/packages/dd/54/f8c13b4489dbd2b40b49a5af2736826319c41e911dce3df38b20c9c9e36d/pyproject_fmt-2.21.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3960d501049bb21656ff2da87341fd1f8ba9e77a1dd0af25444f712a44be7cb7", size = 5375416, upload-time = "2026-04-13T16:40:07.498Z" }, + { url = "https://files.pythonhosted.org/packages/16/e9/c0f1db7f453be63a2395189bc826e74f4b7dad8409f1c8919a9c3a8033c8/pyproject_fmt-2.21.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:d8027fa0b0ba402a37289ea4a8e3651d74c7f932e09dbe97188b1cefde80c3d0", size = 5058679, upload-time = "2026-04-13T16:40:09.76Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/e20aece5a422c3de5b79d8d35f21dcd3e56f2db3ad2076f6093cb8ce19d7/pyproject_fmt-2.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f6a55372be2d44ed2345a03163e9b75d7202cd898f3dff4574ac26d39f211458", size = 5573347, upload-time = "2026-04-13T16:40:11.994Z" }, + { url = "https://files.pythonhosted.org/packages/9e/25/6e2567f6d637408ace630762d8f6c558f046f3fb35cad6d480a818b325c1/pyproject_fmt-2.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:d8adf52702ddb8d0f8b92656aa44aea5519516a8340b46921a0af86ded4e285d", size = 5282373, upload-time = "2026-04-13T16:40:14.181Z" }, + { url = "https://files.pythonhosted.org/packages/df/e1/6736632e4554406b9da1992db0c50aceecff3ab405e2da999e73eacf783c/pyproject_fmt-2.21.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e5a3d409d122fc431713d6fd287f118aff748d0fcb08d8fae9ed5aadfac67710", size = 5049892, upload-time = "2026-04-13T16:40:16.553Z" }, + { url = "https://files.pythonhosted.org/packages/bb/51/f0cdbc1799867e17f8c431ce5c0d8bece72665eee3e3bc4fb27fbe7b296e/pyproject_fmt-2.21.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:29bf5eddd1b3d166487d4a1b660143a2da79e7701b5e483af64cfd4677402d83", size = 4840014, upload-time = "2026-04-13T16:40:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6a/5a49c7b8556c5426edc25375afae954f6f9d70ba46234cf8f3aa6b43c408/pyproject_fmt-2.21.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ece81933e20059d4c7b3f1d8345e08f11aa1687ceb494f1835b4a8daf21c25a8", size = 5369023, upload-time = "2026-04-13T16:40:21.151Z" }, +] + [[package]] name = "pyright" version = "1.1.408" @@ -391,6 +414,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" }, ] +[[package]] +name = "toml-fmt-common" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/f3/1a54191399599a229d04868083c30234c62e569cfaaf367f6c792ac53e2d/toml_fmt_common-1.3.2.tar.gz", hash = "sha256:d0da45f0244e8d410787fa8ddf5a5594016abb2a7b97f400ce20ec8b0a6a4804", size = 7583, upload-time = "2026-03-20T17:32:11.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/27/403387a0b8ad8e5d57bec82ed0e5528432859ea8e1475916c8c8ac45197c/toml_fmt_common-1.3.2-py3-none-any.whl", hash = "sha256:f3329fbdd962f629d1777071a7f37813f6cf17b10a33830a5393996fc2064bef", size = 5557, upload-time = "2026-03-20T17:32:10.263Z" }, +] + [[package]] name = "tomli" version = "2.4.1" From 8cfa13003c9909ace0ad17771376f479f0f639cb Mon Sep 17 00:00:00 2001 From: Ben Du Date: Mon, 20 Apr 2026 10:23:58 -0700 Subject: [PATCH 2/2] add tests and fix issues --- github_rest_api/__init__.py | 4 ++-- github_rest_api/github.py | 22 ++++++++++++---------- pyproject.toml | 2 +- tests/test_github.py | 17 +++++++++++++++++ uv.lock | 4 ++-- 5 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 tests/test_github.py diff --git a/github_rest_api/__init__.py b/github_rest_api/__init__.py index 600f003..829e259 100644 --- a/github_rest_api/__init__.py +++ b/github_rest_api/__init__.py @@ -1,5 +1,5 @@ """GitHub REST APIs.""" -from .github import Organization, Repository, RepositoryType +from .github import Organization, Repository, RepositoryType, User -__all__ = ["Organization", "Repository", "RepositoryType"] +__all__ = ["Organization", "Repository", "RepositoryType", "User"] diff --git a/github_rest_api/github.py b/github_rest_api/github.py index 7f1c8c0..4a07a58 100644 --- a/github_rest_api/github.py +++ b/github_rest_api/github.py @@ -91,14 +91,13 @@ def _patch(self, url, raise_for_status: bool = True, **kwargs) -> requests.Respo def _extract_all( self, url: str, params: dict[str, Any] | None = None ) -> list[dict[str, Any]]: - if params is None: - params = {} + params = params.copy() if params else {} if "per_page" not in params: params["per_page"] = 100 params["page"] = 1 res = [] while True: - resp = self._get(url=url, params=params) + resp = self._get(url=url, params=params.copy()) resp.raise_for_status() data = resp.json() res.extend(data) @@ -346,9 +345,10 @@ def __init__(self, token: str, owner: str): super().__init__(token) self._owner = owner self._url_repos = "" + self._url_create_repo = "" @abstractmethod - def _set_url_repos(self) -> None: + def _set_urls(self) -> None: pass def get_repositories( @@ -375,7 +375,7 @@ def create_repository( "has_projects": True, "has_wiki": True, } - return self._post(url=self._url_repos, json=data, **kwargs).json() + return self._post(url=self._url_create_repo, json=data, **kwargs).json() class User(Owner): @@ -387,10 +387,11 @@ def __init__(self, token: str, user: str): :param user: The name of the user. """ super().__init__(token=token, owner=user) - self._set_url_repos() + self._set_urls() - def _set_url_repos(self) -> None: - self._url_repos = f"https://api.github.com/{self._owner}/repos" + def _set_urls(self) -> None: + self._url_repos = f"https://api.github.com/users/{self._owner}/repos" + self._url_create_repo = "https://api.github.com/user/repos" class Organization(Owner): @@ -402,7 +403,8 @@ def __init__(self, token: str, org: str): :param org: The name of the organization. """ super().__init__(token=token, owner=org) - self._set_url_repos() + self._set_urls() - def _set_url_repos(self) -> None: + def _set_urls(self) -> None: self._url_repos = f"https://api.github.com/orgs/{self._owner}/repos" + self._url_create_repo = self._url_repos diff --git a/pyproject.toml b/pyproject.toml index 3f3cc6c..6ffd289 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = [ "hatchling" ] [project] name = "github-rest-api" -version = "0.32.0" +version = "0.33.0" description = "Simple wrapper of GitHub REST APIs." readme = "README.md" authors = [ { name = "Ben Du", email = "longendu@yahoo.com" } ] diff --git a/tests/test_github.py b/tests/test_github.py new file mode 100644 index 0000000..3c39683 --- /dev/null +++ b/tests/test_github.py @@ -0,0 +1,17 @@ +from github_rest_api.github import User, Organization + + +def test_user_get_repositories(): + token = "" + username = "dclong" + user = User(token, username) + repos = user.get_repositories() + assert len(repos) > 0 + + +def test_organization_get_repositories(): + token = "" + org_name = "legendu-net" + org = Organization(token, org_name) + repos = org.get_repositories() + assert len(repos) > 0 diff --git a/uv.lock b/uv.lock index 5f3b4bd..b139d14 100644 --- a/uv.lock +++ b/uv.lock @@ -195,7 +195,7 @@ wheels = [ [[package]] name = "github-rest-api" -version = "0.32.0" +version = "0.33.0" source = { editable = "." } dependencies = [ { name = "dulwich" }, @@ -222,7 +222,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "deptry", specifier = ">=0.24.0" }, + { name = "deptry", specifier = ">=0.24" }, { name = "pyproject-fmt", specifier = ">=2.21.1" }, { name = "pyright", specifier = ">=1.1.407" }, { name = "pytest", specifier = ">=9.0.2" },