gitcode-api 1.1.1__tar.gz → 1.1.3__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 (33) hide show
  1. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/PKG-INFO +39 -26
  2. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/README.md +38 -25
  3. gitcode_api-1.1.3/gitcode_api/__main__.py +4 -0
  4. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/_base_client.py +18 -12
  5. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/_client.py +7 -1
  6. gitcode_api-1.1.3/gitcode_api/cli.py +255 -0
  7. gitcode_api-1.1.3/gitcode_api/version.txt +1 -0
  8. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api.egg-info/PKG-INFO +39 -26
  9. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api.egg-info/SOURCES.txt +5 -0
  10. gitcode_api-1.1.3/gitcode_api.egg-info/entry_points.txt +2 -0
  11. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/pyproject.toml +5 -2
  12. gitcode_api-1.1.3/tests/test_cli.py +127 -0
  13. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/LICENSE +0 -0
  14. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/__init__.py +0 -0
  15. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/_exceptions.py +0 -0
  16. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/_models.py +0 -0
  17. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/resources/__init__.py +0 -0
  18. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/resources/_shared.py +0 -0
  19. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/resources/account.py +0 -0
  20. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/resources/collaboration.py +0 -0
  21. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/resources/misc.py +0 -0
  22. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/resources/repositories.py +0 -0
  23. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api.egg-info/dependency_links.txt +0 -0
  24. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api.egg-info/requires.txt +0 -0
  25. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api.egg-info/top_level.txt +0 -0
  26. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/setup.cfg +0 -0
  27. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/tests/test_base_client.py +0 -0
  28. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/tests/test_client.py +0 -0
  29. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/tests/test_models.py +0 -0
  30. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/tests/test_resources_account.py +0 -0
  31. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/tests/test_resources_collaboration.py +0 -0
  32. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/tests/test_resources_misc.py +0 -0
  33. {gitcode_api-1.1.1 → gitcode_api-1.1.3}/tests/test_resources_repositories.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitcode-api
3
- Version: 1.1.1
3
+ Version: 1.1.3
4
4
  Summary: Easy to use Python SDK for the GitCode REST API, community-maintained.
5
5
  Author-email: Hugo Huang <hugo@hugohuang.com>
6
6
  License-Expression: MIT
@@ -27,9 +27,9 @@ Dynamic: license-file
27
27
 
28
28
  # GitCode-API
29
29
 
30
- ![PyPI - Version](https://img.shields.io/pypi/v/gitcode-api?link=https%3A%2F%2Fpypi.org%2Fproject%2Fgitcode-api%2F) ![GitHub Badge](https://img.shields.io/badge/github-repo-blue?logo=github&link=https%3A%2F%2Fgithub.com%2FTrenza1ore%2FGitCode-API) ![GitCode Badge](https://img.shields.io/badge/gitcode-repo-brown?logo=gitcode&link=https%3A%2F%2Fgitcode.com%2FSushiNinja%2FGitCode-API) ![PyPI - License](https://img.shields.io/pypi/l/gitcode-api)
30
+ [![PyPI - Version](https://img.shields.io/pypi/v/gitcode-api?link=https%3A%2F%2Fpypi.org%2Fproject%2Fgitcode-api%2F)](https://pypi.org/project/gitcode-api) [![GitHub Badge](https://img.shields.io/badge/github-repo-blue?logo=github&link=https%3A%2F%2Fgithub.com%2FTrenza1ore%2FGitCode-API)](https://github.com/Trenza1ore/GitCode-API) [![GitCode Badge](https://img.shields.io/badge/gitcode-repo-brown?logo=gitcode&link=https%3A%2F%2Fgitcode.com%2FSushiNinja%2FGitCode-API)](https://gitcode.com/SushiNinja/GitCode-API) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/gitcode-api?period=total&units=INTERNATIONAL_SYSTEM&left_color=GRAY&right_color=RED&left_text=downloads)](https://pepy.tech/projects/gitcode-api)
31
31
 
32
- ![Docs](https://img.shields.io/badge/%E6%96%87%E6%A1%A3-Docs-cyan?style=for-the-badge&logo=readthedocs&link=https%3A%2F%2Fgitcode-api.readthedocs.io%2Fen%2Flatest%2Findex.html) ![中文README](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-README-brown?style=for-the-badge&logo=googledocs&link=README.zh.md) ![English README](https://img.shields.io/badge/English-README-blue?style=for-the-badge&logo=googledocs&link=README.md)
32
+ [![Docs](https://img.shields.io/badge/%E6%96%87%E6%A1%A3-Docs-cyan?style=for-the-badge&logo=readthedocs&link=https%3A%2F%2Fgitcode-api.readthedocs.io%2Fen%2Flatest%2Findex.html)](https://gitcode-api.readthedocs.io) [![中文README](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-README-brown?style=for-the-badge&logo=googledocs&link=README.zh.md)](README.zh.md) [![English README](https://img.shields.io/badge/English-README-blue?style=for-the-badge&logo=googledocs&link=README.md)](README.md)
33
33
 
34
34
  `gitcode-api` is a community-maintained Python SDK for the GitCode REST API. It provides easy-to-use synchronous and asynchronous clients, repository-scoped helpers, and lightweight response models so you can work with GitCode from Python without hand-writing raw HTTP requests.
35
35
 
@@ -46,7 +46,7 @@ Dynamic: license-file
46
46
  Install from PyPI:
47
47
 
48
48
  ```bash
49
- pip install gitcode-api
49
+ pip install -U gitcode-api
50
50
  ```
51
51
 
52
52
  ## Authentication
@@ -57,6 +57,33 @@ Pass `api_key=` directly, or set `GITCODE_ACCESS_TOKEN` in your environment:
57
57
  export GITCODE_ACCESS_TOKEN="your-token"
58
58
  ```
59
59
 
60
+ If your token is stored in encrypted form, pass `decrypt=` to decode either an
61
+ encrypted `api_key=` value or an encrypted `GITCODE_ACCESS_TOKEN` value before
62
+ the client uses it.
63
+
64
+ ```python
65
+ from gitcode_api import GitCode
66
+ from trusted_library import decrypt_token
67
+
68
+ client = GitCode(
69
+ api_key="encrypted-token",
70
+ decrypt=decrypt_token,
71
+ )
72
+ ```
73
+
74
+ ## CLI
75
+
76
+ After installation, you can invoke the SDK directly from the command line:
77
+
78
+ ```bash
79
+ gitcode-api repos get --api-key "$GITCODE_ACCESS_TOKEN" --owner SushiNinja --repo GitCode-API
80
+ python -m gitcode_api pulls list --api-key "$GITCODE_ACCESS_TOKEN" --owner SushiNinja --repo GitCode-API --state open
81
+ ```
82
+
83
+ Commands mirror the synchronous resource methods on `GitCode`, using the pattern
84
+ `gitcode-api <resource> <method> ...`. For methods that accept extra `**params`
85
+ or `**payload`, pass repeated `--set key=value` flags or `--set-json '{"key": "value"}'`.
86
+
60
87
  ## Quick Start
61
88
 
62
89
  ### Sync client
@@ -69,40 +96,31 @@ client = GitCode(
69
96
  repo="GitCode-API",
70
97
  )
71
98
 
72
- try:
73
- repo = client.repos.get()
74
- branches = client.branches.list(per_page=5)
99
+ repo = client.repos.get()
100
+ branches = client.branches.list(per_page=5)
75
101
 
76
- print(repo.full_name)
77
- for branch in branches:
78
- print(branch.name)
79
- finally:
80
- client.close()
102
+ print(repo.full_name)
103
+ for branch in branches:
104
+ print(branch.name)
81
105
  ```
82
106
 
83
107
  ### Async client
84
108
 
85
109
  ```python
86
110
  import asyncio
87
-
88
111
  from gitcode_api import AsyncGitCode
89
112
 
90
-
91
113
  async def main() -> None:
92
114
  client = AsyncGitCode(owner="SushiNinja", repo="GitCode-API")
93
- try:
94
- pulls = await client.pulls.list(state="open", per_page=20)
95
- print(len(pulls))
96
- finally:
97
- await client.close()
98
-
115
+ pulls = await client.pulls.list(state="open", per_page=20)
116
+ print(len(pulls))
99
117
 
100
118
  asyncio.run(main())
101
119
  ```
102
120
 
103
121
  ### Context managers
104
122
 
105
- `GitCode` and `AsyncGitCode` (and the lower-level `SyncAPIClient` / `AsyncAPIClient`) support `with` / `async with`. When the SDK creates the underlying httpx client for you, leaving the block calls `close()` / `await close()` on that client automatically.
123
+ `GitCode` and `AsyncGitCode` (and the lower-level `SyncAPIClient` / `AsyncAPIClient`) support `with` / `async with`. Leaving the block calls `close()` / `await close()` on the underlying client automatically, including a custom `http_client=` you passed in.
106
124
 
107
125
  ```python
108
126
  from gitcode_api import GitCode
@@ -114,21 +132,16 @@ with GitCode(owner="SushiNinja", repo="GitCode-API") as client:
114
132
 
115
133
  ```python
116
134
  import asyncio
117
-
118
135
  from gitcode_api import AsyncGitCode
119
136
 
120
-
121
137
  async def main() -> None:
122
138
  async with AsyncGitCode(owner="SushiNinja", repo="GitCode-API") as client:
123
139
  pulls = await client.pulls.list(state="open", per_page=20)
124
140
  print(len(pulls))
125
141
 
126
-
127
142
  asyncio.run(main())
128
143
  ```
129
144
 
130
- If you pass a custom `http_client=`, the SDK does not close it; you still own that client’s lifecycle (for example `async with httpx.AsyncClient(...) as http:` plus `AsyncGitCode(http_client=http)`).
131
-
132
145
  ## Common Workflows
133
146
 
134
147
  Create a pull request:
@@ -1,8 +1,8 @@
1
1
  # GitCode-API
2
2
 
3
- ![PyPI - Version](https://img.shields.io/pypi/v/gitcode-api?link=https%3A%2F%2Fpypi.org%2Fproject%2Fgitcode-api%2F) ![GitHub Badge](https://img.shields.io/badge/github-repo-blue?logo=github&link=https%3A%2F%2Fgithub.com%2FTrenza1ore%2FGitCode-API) ![GitCode Badge](https://img.shields.io/badge/gitcode-repo-brown?logo=gitcode&link=https%3A%2F%2Fgitcode.com%2FSushiNinja%2FGitCode-API) ![PyPI - License](https://img.shields.io/pypi/l/gitcode-api)
3
+ [![PyPI - Version](https://img.shields.io/pypi/v/gitcode-api?link=https%3A%2F%2Fpypi.org%2Fproject%2Fgitcode-api%2F)](https://pypi.org/project/gitcode-api) [![GitHub Badge](https://img.shields.io/badge/github-repo-blue?logo=github&link=https%3A%2F%2Fgithub.com%2FTrenza1ore%2FGitCode-API)](https://github.com/Trenza1ore/GitCode-API) [![GitCode Badge](https://img.shields.io/badge/gitcode-repo-brown?logo=gitcode&link=https%3A%2F%2Fgitcode.com%2FSushiNinja%2FGitCode-API)](https://gitcode.com/SushiNinja/GitCode-API) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/gitcode-api?period=total&units=INTERNATIONAL_SYSTEM&left_color=GRAY&right_color=RED&left_text=downloads)](https://pepy.tech/projects/gitcode-api)
4
4
 
5
- ![Docs](https://img.shields.io/badge/%E6%96%87%E6%A1%A3-Docs-cyan?style=for-the-badge&logo=readthedocs&link=https%3A%2F%2Fgitcode-api.readthedocs.io%2Fen%2Flatest%2Findex.html) ![中文README](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-README-brown?style=for-the-badge&logo=googledocs&link=README.zh.md) ![English README](https://img.shields.io/badge/English-README-blue?style=for-the-badge&logo=googledocs&link=README.md)
5
+ [![Docs](https://img.shields.io/badge/%E6%96%87%E6%A1%A3-Docs-cyan?style=for-the-badge&logo=readthedocs&link=https%3A%2F%2Fgitcode-api.readthedocs.io%2Fen%2Flatest%2Findex.html)](https://gitcode-api.readthedocs.io) [![中文README](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-README-brown?style=for-the-badge&logo=googledocs&link=README.zh.md)](README.zh.md) [![English README](https://img.shields.io/badge/English-README-blue?style=for-the-badge&logo=googledocs&link=README.md)](README.md)
6
6
 
7
7
  `gitcode-api` is a community-maintained Python SDK for the GitCode REST API. It provides easy-to-use synchronous and asynchronous clients, repository-scoped helpers, and lightweight response models so you can work with GitCode from Python without hand-writing raw HTTP requests.
8
8
 
@@ -19,7 +19,7 @@
19
19
  Install from PyPI:
20
20
 
21
21
  ```bash
22
- pip install gitcode-api
22
+ pip install -U gitcode-api
23
23
  ```
24
24
 
25
25
  ## Authentication
@@ -30,6 +30,33 @@ Pass `api_key=` directly, or set `GITCODE_ACCESS_TOKEN` in your environment:
30
30
  export GITCODE_ACCESS_TOKEN="your-token"
31
31
  ```
32
32
 
33
+ If your token is stored in encrypted form, pass `decrypt=` to decode either an
34
+ encrypted `api_key=` value or an encrypted `GITCODE_ACCESS_TOKEN` value before
35
+ the client uses it.
36
+
37
+ ```python
38
+ from gitcode_api import GitCode
39
+ from trusted_library import decrypt_token
40
+
41
+ client = GitCode(
42
+ api_key="encrypted-token",
43
+ decrypt=decrypt_token,
44
+ )
45
+ ```
46
+
47
+ ## CLI
48
+
49
+ After installation, you can invoke the SDK directly from the command line:
50
+
51
+ ```bash
52
+ gitcode-api repos get --api-key "$GITCODE_ACCESS_TOKEN" --owner SushiNinja --repo GitCode-API
53
+ python -m gitcode_api pulls list --api-key "$GITCODE_ACCESS_TOKEN" --owner SushiNinja --repo GitCode-API --state open
54
+ ```
55
+
56
+ Commands mirror the synchronous resource methods on `GitCode`, using the pattern
57
+ `gitcode-api <resource> <method> ...`. For methods that accept extra `**params`
58
+ or `**payload`, pass repeated `--set key=value` flags or `--set-json '{"key": "value"}'`.
59
+
33
60
  ## Quick Start
34
61
 
35
62
  ### Sync client
@@ -42,40 +69,31 @@ client = GitCode(
42
69
  repo="GitCode-API",
43
70
  )
44
71
 
45
- try:
46
- repo = client.repos.get()
47
- branches = client.branches.list(per_page=5)
72
+ repo = client.repos.get()
73
+ branches = client.branches.list(per_page=5)
48
74
 
49
- print(repo.full_name)
50
- for branch in branches:
51
- print(branch.name)
52
- finally:
53
- client.close()
75
+ print(repo.full_name)
76
+ for branch in branches:
77
+ print(branch.name)
54
78
  ```
55
79
 
56
80
  ### Async client
57
81
 
58
82
  ```python
59
83
  import asyncio
60
-
61
84
  from gitcode_api import AsyncGitCode
62
85
 
63
-
64
86
  async def main() -> None:
65
87
  client = AsyncGitCode(owner="SushiNinja", repo="GitCode-API")
66
- try:
67
- pulls = await client.pulls.list(state="open", per_page=20)
68
- print(len(pulls))
69
- finally:
70
- await client.close()
71
-
88
+ pulls = await client.pulls.list(state="open", per_page=20)
89
+ print(len(pulls))
72
90
 
73
91
  asyncio.run(main())
74
92
  ```
75
93
 
76
94
  ### Context managers
77
95
 
78
- `GitCode` and `AsyncGitCode` (and the lower-level `SyncAPIClient` / `AsyncAPIClient`) support `with` / `async with`. When the SDK creates the underlying httpx client for you, leaving the block calls `close()` / `await close()` on that client automatically.
96
+ `GitCode` and `AsyncGitCode` (and the lower-level `SyncAPIClient` / `AsyncAPIClient`) support `with` / `async with`. Leaving the block calls `close()` / `await close()` on the underlying client automatically, including a custom `http_client=` you passed in.
79
97
 
80
98
  ```python
81
99
  from gitcode_api import GitCode
@@ -87,21 +105,16 @@ with GitCode(owner="SushiNinja", repo="GitCode-API") as client:
87
105
 
88
106
  ```python
89
107
  import asyncio
90
-
91
108
  from gitcode_api import AsyncGitCode
92
109
 
93
-
94
110
  async def main() -> None:
95
111
  async with AsyncGitCode(owner="SushiNinja", repo="GitCode-API") as client:
96
112
  pulls = await client.pulls.list(state="open", per_page=20)
97
113
  print(len(pulls))
98
114
 
99
-
100
115
  asyncio.run(main())
101
116
  ```
102
117
 
103
- If you pass a custom `http_client=`, the SDK does not close it; you still own that client’s lifecycle (for example `async with httpx.AsyncClient(...) as http:` plus `AsyncGitCode(http_client=http)`).
104
-
105
118
  ## Common Workflows
106
119
 
107
120
  Create a pull request:
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -5,7 +5,7 @@ payload cleanup, and response parsing for both sync and async clients.
5
5
  """
6
6
 
7
7
  import os
8
- from typing import Any, Dict, Optional, Tuple, Union
8
+ from typing import Any, Callable, Dict, Optional, Tuple, Union
9
9
  from urllib.parse import quote
10
10
 
11
11
  import httpx
@@ -30,6 +30,7 @@ class BaseGitCodeClient:
30
30
  :param repo: Default repository name for repository-scoped calls.
31
31
  :param base_url: Base URL for the GitCode REST API.
32
32
  :param timeout: Request timeout in seconds.
33
+ :param decrypt: Optional decryption function for encrypted access token.
33
34
  """
34
35
 
35
36
  def __init__(
@@ -40,20 +41,23 @@ class BaseGitCodeClient:
40
41
  repo: Optional[str] = None,
41
42
  base_url: str = DEFAULT_BASE_URL,
42
43
  timeout: Optional[float] = None,
44
+ decrypt: Optional[Callable] = None,
43
45
  ) -> None:
44
46
  """Store client configuration and resolve authentication."""
45
- self.api_key = self._resolve_api_key(api_key)
47
+ self.api_key = self._resolve_api_key(api_key, decrypt)
46
48
  self.owner = owner
47
49
  self.repo = repo
48
50
  self.base_url = base_url.rstrip("/")
49
51
  self.timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
50
52
 
51
- def _resolve_api_key(self, api_key: Optional[str]) -> str:
53
+ def _resolve_api_key(self, api_key: Optional[str], decrypt: Optional[Callable] = None) -> str:
52
54
  """Resolve the access token from an argument or environment variable."""
53
55
  token = api_key or os.getenv(DEFAULT_TOKEN_ENV)
56
+ if callable(decrypt):
57
+ token = decrypt(token)
54
58
  if not token:
55
59
  raise GitCodeConfigurationError("No API key provided. Pass api_key=... or set GITCODE_ACCESS_TOKEN.")
56
- return token
60
+ return str(token)
57
61
 
58
62
  def _resolve_repo_context(
59
63
  self,
@@ -187,6 +191,7 @@ class SyncAPIClient(BaseGitCodeClient):
187
191
  :param base_url: Base URL for the GitCode REST API.
188
192
  :param timeout: Request timeout in seconds.
189
193
  :param http_client: Optional pre-configured ``httpx.Client`` instance.
194
+ :param decrypt: Optional decryption function for encrypted access token.
190
195
  """
191
196
 
192
197
  def __init__(
@@ -198,9 +203,10 @@ class SyncAPIClient(BaseGitCodeClient):
198
203
  base_url: str = DEFAULT_BASE_URL,
199
204
  timeout: Optional[float] = None,
200
205
  http_client: Optional[httpx.Client] = None,
206
+ decrypt: Optional[Callable] = None,
201
207
  ) -> None:
202
208
  """Create or reuse an ``httpx.Client`` for synchronous requests."""
203
- super().__init__(api_key=api_key, owner=owner, repo=repo, base_url=base_url, timeout=timeout)
209
+ super().__init__(api_key=api_key, owner=owner, repo=repo, base_url=base_url, timeout=timeout, decrypt=decrypt)
204
210
  self._owns_client = http_client is None
205
211
  self._client = http_client or httpx.Client(timeout=self.timeout)
206
212
 
@@ -238,9 +244,8 @@ class SyncAPIClient(BaseGitCodeClient):
238
244
  return self._parse_response(response, raw=raw)
239
245
 
240
246
  def close(self) -> None:
241
- """Close the underlying HTTP client if this instance created it."""
242
- if self._owns_client:
243
- self._client.close()
247
+ """Close the underlying HTTP client."""
248
+ self._client.close()
244
249
 
245
250
  def __enter__(self) -> "SyncAPIClient":
246
251
  """Enter a context manager and return the client instance."""
@@ -260,6 +265,7 @@ class AsyncAPIClient(BaseGitCodeClient):
260
265
  :param base_url: Base URL for the GitCode REST API.
261
266
  :param timeout: Request timeout in seconds.
262
267
  :param http_client: Optional pre-configured ``httpx.AsyncClient`` instance.
268
+ :param decrypt: Optional decryption function for encrypted access token.
263
269
  """
264
270
 
265
271
  def __init__(
@@ -271,9 +277,10 @@ class AsyncAPIClient(BaseGitCodeClient):
271
277
  base_url: str = DEFAULT_BASE_URL,
272
278
  timeout: Optional[float] = None,
273
279
  http_client: Optional[httpx.AsyncClient] = None,
280
+ decrypt: Optional[Callable] = None,
274
281
  ) -> None:
275
282
  """Create or reuse an ``httpx.AsyncClient`` for asynchronous requests."""
276
- super().__init__(api_key=api_key, owner=owner, repo=repo, base_url=base_url, timeout=timeout)
283
+ super().__init__(api_key=api_key, owner=owner, repo=repo, base_url=base_url, timeout=timeout, decrypt=decrypt)
277
284
  self._owns_client = http_client is None
278
285
  self._client = http_client or httpx.AsyncClient(timeout=self.timeout)
279
286
 
@@ -311,9 +318,8 @@ class AsyncAPIClient(BaseGitCodeClient):
311
318
  return self._parse_response(response, raw=raw)
312
319
 
313
320
  async def close(self) -> None:
314
- """Close the underlying async HTTP client if this instance created it."""
315
- if self._owns_client:
316
- await self._client.aclose()
321
+ """Close the underlying async HTTP client."""
322
+ await self._client.aclose()
317
323
 
318
324
  async def __aenter__(self) -> "AsyncAPIClient":
319
325
  """Enter an async context manager and return the client instance."""
@@ -4,7 +4,7 @@ These client classes expose grouped resource helpers that mirror the
4
4
  published GitCode REST API documentation.
5
5
  """
6
6
 
7
- from typing import Optional
7
+ from typing import Callable, Optional
8
8
 
9
9
  import httpx
10
10
 
@@ -54,6 +54,7 @@ class GitCode(SyncAPIClient):
54
54
  :param base_url: Base URL for the GitCode REST API.
55
55
  :param timeout: Request timeout in seconds.
56
56
  :param http_client: Optional pre-configured ``httpx.Client`` instance.
57
+ :param decrypt: Optional decryption function for encrypted access token.
57
58
  """
58
59
 
59
60
  repos: ReposResource
@@ -113,6 +114,7 @@ class GitCode(SyncAPIClient):
113
114
  base_url: str = DEFAULT_BASE_URL,
114
115
  timeout: Optional[float] = None,
115
116
  http_client: Optional[httpx.Client] = None,
117
+ decrypt: Optional[Callable] = None,
116
118
  ) -> None:
117
119
  """Create a synchronous client and attach resource groups."""
118
120
  super().__init__(
@@ -122,6 +124,7 @@ class GitCode(SyncAPIClient):
122
124
  base_url=base_url,
123
125
  timeout=timeout,
124
126
  http_client=http_client,
127
+ decrypt=decrypt,
125
128
  )
126
129
  self.repos = ReposResource(self)
127
130
  self.contents = RepoContentsResource(self)
@@ -153,6 +156,7 @@ class AsyncGitCode(AsyncAPIClient):
153
156
  :param base_url: Base URL for the GitCode REST API.
154
157
  :param timeout: Request timeout in seconds.
155
158
  :param http_client: Optional pre-configured ``httpx.AsyncClient`` instance.
159
+ :param decrypt: Optional decryption function for encrypted access token.
156
160
  """
157
161
 
158
162
  repos: AsyncReposResource
@@ -212,6 +216,7 @@ class AsyncGitCode(AsyncAPIClient):
212
216
  base_url: str = DEFAULT_BASE_URL,
213
217
  timeout: Optional[float] = None,
214
218
  http_client: Optional[httpx.AsyncClient] = None,
219
+ decrypt: Optional[Callable] = None,
215
220
  ) -> None:
216
221
  """Create an asynchronous client and attach resource groups."""
217
222
  super().__init__(
@@ -221,6 +226,7 @@ class AsyncGitCode(AsyncAPIClient):
221
226
  base_url=base_url,
222
227
  timeout=timeout,
223
228
  http_client=http_client,
229
+ decrypt=decrypt,
224
230
  )
225
231
  self.repos = AsyncReposResource(self)
226
232
  self.contents = AsyncRepoContentsResource(self)
@@ -0,0 +1,255 @@
1
+ """Command-line interface for the GitCode SDK."""
2
+
3
+ import argparse
4
+ import inspect
5
+ import json
6
+ import sys
7
+ from collections.abc import Mapping, Sequence
8
+ from pathlib import Path
9
+ from typing import Any, List, Optional, Union, get_args, get_origin
10
+
11
+ from . import GitCode, __version__
12
+ from ._base_client import DEFAULT_BASE_URL, DEFAULT_TOKEN_ENV
13
+ from ._exceptions import GitCodeError
14
+ from .resources._shared import SyncResource
15
+
16
+
17
+ def _unwrap_optional(annotation: Any) -> Any:
18
+ origin = get_origin(annotation)
19
+ if origin is Union:
20
+ args = [arg for arg in get_args(annotation) if arg is not type(None)]
21
+ if len(args) == 1:
22
+ return args[0]
23
+ return annotation
24
+
25
+
26
+ def _is_list_annotation(annotation: Any) -> bool:
27
+ annotation = _unwrap_optional(annotation)
28
+ return get_origin(annotation) in (list, List)
29
+
30
+
31
+ def _list_item_type(annotation: Any) -> Any:
32
+ annotation = _unwrap_optional(annotation)
33
+ args = get_args(annotation)
34
+ if not args:
35
+ return str
36
+ item_type = _unwrap_optional(args[0])
37
+ if item_type in (int, float):
38
+ return item_type
39
+ return str
40
+
41
+
42
+ def _argument_kwargs(parameter: inspect.Parameter) -> dict[str, Any]:
43
+ annotation = _unwrap_optional(parameter.annotation)
44
+ if annotation is bool:
45
+ return {"action": argparse.BooleanOptionalAction, "default": parameter.default}
46
+ if _is_list_annotation(parameter.annotation):
47
+ return {"nargs": "+", "type": _list_item_type(parameter.annotation), "default": None}
48
+ if annotation in (int, float):
49
+ return {"type": annotation}
50
+ return {"type": str}
51
+
52
+
53
+ def _first_doc_line(obj: Any) -> str:
54
+ doc = inspect.getdoc(obj) or ""
55
+ return doc.splitlines()[0] if doc else ""
56
+
57
+
58
+ def _resource_types() -> dict[str, type[SyncResource]]:
59
+ resources: dict[str, type[SyncResource]] = {}
60
+ for name, annotation in GitCode.__annotations__.items():
61
+ if inspect.isclass(annotation) and issubclass(annotation, SyncResource):
62
+ resources[name] = annotation
63
+ return resources
64
+
65
+
66
+ def _iter_resource_methods(resource_type: type[SyncResource]) -> list[tuple[str, Any]]:
67
+ methods: list[tuple[str, Any]] = []
68
+ for name, value in resource_type.__dict__.items():
69
+ if name.startswith("_") or not inspect.isfunction(value):
70
+ continue
71
+ methods.append((name, value))
72
+ return methods
73
+
74
+
75
+ def _kebab_case(value: str) -> str:
76
+ return value.replace("_", "-")
77
+
78
+
79
+ def _load_json_value(raw: str) -> Any:
80
+ if raw.startswith("@"):
81
+ return json.loads(Path(raw[1:]).read_text(encoding="utf-8"))
82
+ return json.loads(raw)
83
+
84
+
85
+ def _parse_scalar(raw: str) -> Any:
86
+ try:
87
+ return _load_json_value(raw)
88
+ except (OSError, ValueError, json.JSONDecodeError):
89
+ return raw
90
+
91
+
92
+ def _parse_key_value(raw: str) -> tuple[str, Any]:
93
+ if "=" not in raw:
94
+ raise ValueError(f"Expected KEY=VALUE, got: {raw}")
95
+ key, value = raw.split("=", maxsplit=1)
96
+ if not key:
97
+ raise ValueError(f"Expected KEY=VALUE, got: {raw}")
98
+ return key, _parse_scalar(value)
99
+
100
+
101
+ def _to_data(value: Any) -> Any:
102
+ if hasattr(value, "to_dict") and callable(value.to_dict):
103
+ return _to_data(value.to_dict())
104
+ if isinstance(value, Mapping):
105
+ return {key: _to_data(item) for key, item in value.items()}
106
+ if isinstance(value, list):
107
+ return [_to_data(item) for item in value]
108
+ return value
109
+
110
+
111
+ def _write_output(value: Any, *, output_file: Optional[str], compact: bool) -> None:
112
+ if value is None:
113
+ return
114
+ if isinstance(value, bytes):
115
+ if output_file:
116
+ Path(output_file).write_bytes(value)
117
+ else:
118
+ sys.stdout.buffer.write(value)
119
+ return
120
+
121
+ payload = _to_data(value)
122
+ if isinstance(payload, str):
123
+ text = payload
124
+ else:
125
+ text = json.dumps(payload, indent=None if compact else 2, ensure_ascii=True, sort_keys=True)
126
+
127
+ if output_file:
128
+ Path(output_file).write_text(text + ("\n" if not text.endswith("\n") else ""), encoding="utf-8")
129
+ else:
130
+ print(text)
131
+
132
+
133
+ def _global_parent_parser() -> argparse.ArgumentParser:
134
+ parser = argparse.ArgumentParser(add_help=False)
135
+ parser.add_argument("--api-key", help=f"GitCode access token. Defaults to {DEFAULT_TOKEN_ENV}.")
136
+ parser.add_argument("--owner", help="Default repository owner.")
137
+ parser.add_argument("--repo", help="Default repository name.")
138
+ parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="Base URL for the GitCode REST API.")
139
+ parser.add_argument("--timeout", type=float, default=None, help="Request timeout in seconds.")
140
+ parser.add_argument("--output-file", help="Write the response to a file instead of stdout.")
141
+ parser.add_argument("--compact", action="store_true", help="Print JSON without indentation.")
142
+ return parser
143
+
144
+
145
+ def build_parser() -> argparse.ArgumentParser:
146
+ common = _global_parent_parser()
147
+ parser = argparse.ArgumentParser(
148
+ prog="gitcode-api",
149
+ description="Invoke any synchronous gitcode-api resource method from the command line.",
150
+ epilog='Use `--set key=value` and `--set-json \'{"key": "value"}\'` for methods with `**params` or `**payload`.',
151
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
152
+ parents=[common],
153
+ )
154
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
155
+
156
+ resource_parsers = parser.add_subparsers(dest="resource", required=True)
157
+ for resource_name, resource_type in _resource_types().items():
158
+ resource_parser = resource_parsers.add_parser(
159
+ _kebab_case(resource_name),
160
+ help=_first_doc_line(resource_type),
161
+ )
162
+ method_parsers = resource_parser.add_subparsers(dest="method", required=True)
163
+
164
+ for method_name, method in _iter_resource_methods(resource_type):
165
+ method_parser = method_parsers.add_parser(
166
+ _kebab_case(method_name),
167
+ help=_first_doc_line(method),
168
+ description=inspect.getdoc(method),
169
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
170
+ parents=[common],
171
+ )
172
+ signature = inspect.signature(method)
173
+ for parameter in signature.parameters.values():
174
+ if parameter.name == "self":
175
+ continue
176
+ if parameter.kind == inspect.Parameter.VAR_KEYWORD:
177
+ method_parser.add_argument(
178
+ "--set",
179
+ dest="extra_items",
180
+ action="append",
181
+ default=None,
182
+ metavar="KEY=VALUE",
183
+ help="Additional keyword arguments for `**params` or `**payload`.",
184
+ )
185
+ method_parser.add_argument(
186
+ "--set-json",
187
+ dest="extra_json",
188
+ default=None,
189
+ metavar="JSON_OR_@FILE",
190
+ help="JSON object merged into extra keyword arguments.",
191
+ )
192
+ continue
193
+
194
+ flag = f"--{parameter.name.replace('_', '-')}"
195
+ if flag in method_parser._option_string_actions:
196
+ continue
197
+ kwargs = _argument_kwargs(parameter)
198
+ kwargs["dest"] = parameter.name
199
+ kwargs["required"] = parameter.default is inspect.Signature.empty
200
+ method_parser.add_argument(flag, **kwargs)
201
+
202
+ method_parser.set_defaults(resource_name=resource_name, method_name=method_name)
203
+
204
+ return parser
205
+
206
+
207
+ def _collect_kwargs(args: argparse.Namespace, method: Any) -> dict[str, Any]:
208
+ signature = inspect.signature(method)
209
+ kwargs: dict[str, Any] = {}
210
+ for parameter in signature.parameters.values():
211
+ if parameter.name == "self":
212
+ continue
213
+ if parameter.kind == inspect.Parameter.VAR_KEYWORD:
214
+ extra_kwargs: dict[str, Any] = {}
215
+ if getattr(args, "extra_json", None):
216
+ raw_extra = _load_json_value(args.extra_json)
217
+ if not isinstance(raw_extra, dict):
218
+ raise ValueError("--set-json must decode to a JSON object.")
219
+ extra_kwargs.update(raw_extra)
220
+ for item in getattr(args, "extra_items", []) or []:
221
+ key, value = _parse_key_value(item)
222
+ extra_kwargs[key] = value
223
+ kwargs.update(extra_kwargs)
224
+ continue
225
+
226
+ value = getattr(args, parameter.name)
227
+ if value is None:
228
+ if parameter.default is inspect.Signature.empty:
229
+ raise ValueError(f"--{parameter.name.replace('_', '-')} is required.")
230
+ continue
231
+ kwargs[parameter.name] = value
232
+ return kwargs
233
+
234
+
235
+ def main(argv: Optional[Sequence[str]] = None) -> int:
236
+ parser = build_parser()
237
+ args = parser.parse_args(argv)
238
+
239
+ try:
240
+ with GitCode(
241
+ api_key=args.api_key,
242
+ owner=args.owner,
243
+ repo=args.repo,
244
+ base_url=args.base_url,
245
+ timeout=args.timeout,
246
+ ) as client:
247
+ resource = getattr(client, args.resource_name)
248
+ method = getattr(resource, args.method_name)
249
+ result = method(**_collect_kwargs(args, method))
250
+ except (GitCodeError, OSError, TypeError, ValueError) as exc: # pragma: no cover - integration style
251
+ print(f"error: {exc}", file=sys.stderr)
252
+ return 1
253
+
254
+ _write_output(result, output_file=args.output_file, compact=args.compact)
255
+ return 0
@@ -0,0 +1 @@
1
+ 1.1.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitcode-api
3
- Version: 1.1.1
3
+ Version: 1.1.3
4
4
  Summary: Easy to use Python SDK for the GitCode REST API, community-maintained.
5
5
  Author-email: Hugo Huang <hugo@hugohuang.com>
6
6
  License-Expression: MIT
@@ -27,9 +27,9 @@ Dynamic: license-file
27
27
 
28
28
  # GitCode-API
29
29
 
30
- ![PyPI - Version](https://img.shields.io/pypi/v/gitcode-api?link=https%3A%2F%2Fpypi.org%2Fproject%2Fgitcode-api%2F) ![GitHub Badge](https://img.shields.io/badge/github-repo-blue?logo=github&link=https%3A%2F%2Fgithub.com%2FTrenza1ore%2FGitCode-API) ![GitCode Badge](https://img.shields.io/badge/gitcode-repo-brown?logo=gitcode&link=https%3A%2F%2Fgitcode.com%2FSushiNinja%2FGitCode-API) ![PyPI - License](https://img.shields.io/pypi/l/gitcode-api)
30
+ [![PyPI - Version](https://img.shields.io/pypi/v/gitcode-api?link=https%3A%2F%2Fpypi.org%2Fproject%2Fgitcode-api%2F)](https://pypi.org/project/gitcode-api) [![GitHub Badge](https://img.shields.io/badge/github-repo-blue?logo=github&link=https%3A%2F%2Fgithub.com%2FTrenza1ore%2FGitCode-API)](https://github.com/Trenza1ore/GitCode-API) [![GitCode Badge](https://img.shields.io/badge/gitcode-repo-brown?logo=gitcode&link=https%3A%2F%2Fgitcode.com%2FSushiNinja%2FGitCode-API)](https://gitcode.com/SushiNinja/GitCode-API) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/gitcode-api?period=total&units=INTERNATIONAL_SYSTEM&left_color=GRAY&right_color=RED&left_text=downloads)](https://pepy.tech/projects/gitcode-api)
31
31
 
32
- ![Docs](https://img.shields.io/badge/%E6%96%87%E6%A1%A3-Docs-cyan?style=for-the-badge&logo=readthedocs&link=https%3A%2F%2Fgitcode-api.readthedocs.io%2Fen%2Flatest%2Findex.html) ![中文README](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-README-brown?style=for-the-badge&logo=googledocs&link=README.zh.md) ![English README](https://img.shields.io/badge/English-README-blue?style=for-the-badge&logo=googledocs&link=README.md)
32
+ [![Docs](https://img.shields.io/badge/%E6%96%87%E6%A1%A3-Docs-cyan?style=for-the-badge&logo=readthedocs&link=https%3A%2F%2Fgitcode-api.readthedocs.io%2Fen%2Flatest%2Findex.html)](https://gitcode-api.readthedocs.io) [![中文README](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-README-brown?style=for-the-badge&logo=googledocs&link=README.zh.md)](README.zh.md) [![English README](https://img.shields.io/badge/English-README-blue?style=for-the-badge&logo=googledocs&link=README.md)](README.md)
33
33
 
34
34
  `gitcode-api` is a community-maintained Python SDK for the GitCode REST API. It provides easy-to-use synchronous and asynchronous clients, repository-scoped helpers, and lightweight response models so you can work with GitCode from Python without hand-writing raw HTTP requests.
35
35
 
@@ -46,7 +46,7 @@ Dynamic: license-file
46
46
  Install from PyPI:
47
47
 
48
48
  ```bash
49
- pip install gitcode-api
49
+ pip install -U gitcode-api
50
50
  ```
51
51
 
52
52
  ## Authentication
@@ -57,6 +57,33 @@ Pass `api_key=` directly, or set `GITCODE_ACCESS_TOKEN` in your environment:
57
57
  export GITCODE_ACCESS_TOKEN="your-token"
58
58
  ```
59
59
 
60
+ If your token is stored in encrypted form, pass `decrypt=` to decode either an
61
+ encrypted `api_key=` value or an encrypted `GITCODE_ACCESS_TOKEN` value before
62
+ the client uses it.
63
+
64
+ ```python
65
+ from gitcode_api import GitCode
66
+ from trusted_library import decrypt_token
67
+
68
+ client = GitCode(
69
+ api_key="encrypted-token",
70
+ decrypt=decrypt_token,
71
+ )
72
+ ```
73
+
74
+ ## CLI
75
+
76
+ After installation, you can invoke the SDK directly from the command line:
77
+
78
+ ```bash
79
+ gitcode-api repos get --api-key "$GITCODE_ACCESS_TOKEN" --owner SushiNinja --repo GitCode-API
80
+ python -m gitcode_api pulls list --api-key "$GITCODE_ACCESS_TOKEN" --owner SushiNinja --repo GitCode-API --state open
81
+ ```
82
+
83
+ Commands mirror the synchronous resource methods on `GitCode`, using the pattern
84
+ `gitcode-api <resource> <method> ...`. For methods that accept extra `**params`
85
+ or `**payload`, pass repeated `--set key=value` flags or `--set-json '{"key": "value"}'`.
86
+
60
87
  ## Quick Start
61
88
 
62
89
  ### Sync client
@@ -69,40 +96,31 @@ client = GitCode(
69
96
  repo="GitCode-API",
70
97
  )
71
98
 
72
- try:
73
- repo = client.repos.get()
74
- branches = client.branches.list(per_page=5)
99
+ repo = client.repos.get()
100
+ branches = client.branches.list(per_page=5)
75
101
 
76
- print(repo.full_name)
77
- for branch in branches:
78
- print(branch.name)
79
- finally:
80
- client.close()
102
+ print(repo.full_name)
103
+ for branch in branches:
104
+ print(branch.name)
81
105
  ```
82
106
 
83
107
  ### Async client
84
108
 
85
109
  ```python
86
110
  import asyncio
87
-
88
111
  from gitcode_api import AsyncGitCode
89
112
 
90
-
91
113
  async def main() -> None:
92
114
  client = AsyncGitCode(owner="SushiNinja", repo="GitCode-API")
93
- try:
94
- pulls = await client.pulls.list(state="open", per_page=20)
95
- print(len(pulls))
96
- finally:
97
- await client.close()
98
-
115
+ pulls = await client.pulls.list(state="open", per_page=20)
116
+ print(len(pulls))
99
117
 
100
118
  asyncio.run(main())
101
119
  ```
102
120
 
103
121
  ### Context managers
104
122
 
105
- `GitCode` and `AsyncGitCode` (and the lower-level `SyncAPIClient` / `AsyncAPIClient`) support `with` / `async with`. When the SDK creates the underlying httpx client for you, leaving the block calls `close()` / `await close()` on that client automatically.
123
+ `GitCode` and `AsyncGitCode` (and the lower-level `SyncAPIClient` / `AsyncAPIClient`) support `with` / `async with`. Leaving the block calls `close()` / `await close()` on the underlying client automatically, including a custom `http_client=` you passed in.
106
124
 
107
125
  ```python
108
126
  from gitcode_api import GitCode
@@ -114,21 +132,16 @@ with GitCode(owner="SushiNinja", repo="GitCode-API") as client:
114
132
 
115
133
  ```python
116
134
  import asyncio
117
-
118
135
  from gitcode_api import AsyncGitCode
119
136
 
120
-
121
137
  async def main() -> None:
122
138
  async with AsyncGitCode(owner="SushiNinja", repo="GitCode-API") as client:
123
139
  pulls = await client.pulls.list(state="open", per_page=20)
124
140
  print(len(pulls))
125
141
 
126
-
127
142
  asyncio.run(main())
128
143
  ```
129
144
 
130
- If you pass a custom `http_client=`, the SDK does not close it; you still own that client’s lifecycle (for example `async with httpx.AsyncClient(...) as http:` plus `AsyncGitCode(http_client=http)`).
131
-
132
145
  ## Common Workflows
133
146
 
134
147
  Create a pull request:
@@ -2,13 +2,17 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  gitcode_api/__init__.py
5
+ gitcode_api/__main__.py
5
6
  gitcode_api/_base_client.py
6
7
  gitcode_api/_client.py
7
8
  gitcode_api/_exceptions.py
8
9
  gitcode_api/_models.py
10
+ gitcode_api/cli.py
11
+ gitcode_api/version.txt
9
12
  gitcode_api.egg-info/PKG-INFO
10
13
  gitcode_api.egg-info/SOURCES.txt
11
14
  gitcode_api.egg-info/dependency_links.txt
15
+ gitcode_api.egg-info/entry_points.txt
12
16
  gitcode_api.egg-info/requires.txt
13
17
  gitcode_api.egg-info/top_level.txt
14
18
  gitcode_api/resources/__init__.py
@@ -18,6 +22,7 @@ gitcode_api/resources/collaboration.py
18
22
  gitcode_api/resources/misc.py
19
23
  gitcode_api/resources/repositories.py
20
24
  tests/test_base_client.py
25
+ tests/test_cli.py
21
26
  tests/test_client.py
22
27
  tests/test_models.py
23
28
  tests/test_resources_account.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gitcode-api = gitcode_api.cli:main
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gitcode-api"
3
- version = "1.1.1"
3
+ version = "1.1.3"
4
4
  description = "Easy to use Python SDK for the GitCode REST API, community-maintained."
5
5
  keywords = ["gitcode", "git", "devops", "api", "sdk", "python", "httpx", "client"]
6
6
  readme = "README.md"
@@ -51,11 +51,14 @@ documentation = "https://gitcode-api.readthedocs.io"
51
51
  gitcode = "https://gitcode.com/SushiNinja/GitCode-API"
52
52
  github = "https://github.com/Trenza1ore/GitCode-API"
53
53
 
54
+ [project.scripts]
55
+ gitcode-api = "gitcode_api.cli:main"
56
+
54
57
  [tool.uv]
55
58
  default-groups = ['test']
56
59
 
57
60
  [tool.setuptools.package-data]
58
- gitcode-api = ["/**/version.txt"]
61
+ gitcode_api = ["**/version.txt"]
59
62
 
60
63
  [tool.coverage.run]
61
64
  omit = ["tests/*"]
@@ -0,0 +1,127 @@
1
+ from pathlib import Path
2
+ from typing import Any
3
+
4
+ import httpx
5
+
6
+ from gitcode_api.cli import build_parser, main
7
+
8
+
9
+ def _mock_sync_client(monkeypatch: Any, handler: Any) -> None:
10
+ mock_transport = httpx.MockTransport(handler)
11
+ real_client = httpx.Client
12
+
13
+ def client_with_mock_transport(*args: object, **kwargs: object) -> httpx.Client:
14
+ merged = dict(kwargs)
15
+ merged.setdefault("transport", mock_transport)
16
+ return real_client(*args, **merged)
17
+
18
+ monkeypatch.setattr(httpx, "Client", client_with_mock_transport)
19
+
20
+
21
+ def test_cli_build_parser_exposes_generated_commands() -> None:
22
+ parser = build_parser()
23
+ args = parser.parse_args(
24
+ [
25
+ "oauth",
26
+ "build-authorize-url",
27
+ "--api-key",
28
+ "test-token",
29
+ "--client-id",
30
+ "cid",
31
+ "--redirect-uri",
32
+ "https://example.com/callback",
33
+ ]
34
+ )
35
+
36
+ assert args.resource_name == "oauth"
37
+ assert args.method_name == "build_authorize_url"
38
+
39
+
40
+ def test_cli_invokes_resource_methods_and_prints_json(capsys: Any, monkeypatch: Any) -> None:
41
+ captured: dict[str, Any] = {}
42
+
43
+ def handler(request: httpx.Request) -> httpx.Response:
44
+ captured["url"] = str(request.url)
45
+ return httpx.Response(200, json={"full_name": "SushiNinja/GitCode-API"})
46
+
47
+ _mock_sync_client(monkeypatch, handler)
48
+
49
+ exit_code = main(
50
+ [
51
+ "repos",
52
+ "get",
53
+ "--api-key",
54
+ "test-token",
55
+ "--owner",
56
+ "SushiNinja",
57
+ "--repo",
58
+ "GitCode-API",
59
+ ]
60
+ )
61
+
62
+ stdout = capsys.readouterr().out
63
+ assert exit_code == 0
64
+ assert "/repos/SushiNinja/GitCode-API" in captured["url"]
65
+ assert '"full_name": "SushiNinja/GitCode-API"' in stdout
66
+
67
+
68
+ def test_cli_supports_extra_kwargs_via_set_flags(capsys: Any, monkeypatch: Any) -> None:
69
+ captured: dict[str, Any] = {}
70
+
71
+ def handler(request: httpx.Request) -> httpx.Response:
72
+ captured["params"] = dict(request.url.params)
73
+ return httpx.Response(200, json={"open": 3, "closed": 1})
74
+
75
+ _mock_sync_client(monkeypatch, handler)
76
+
77
+ exit_code = main(
78
+ [
79
+ "pulls",
80
+ "list",
81
+ "--api-key",
82
+ "test-token",
83
+ "--owner",
84
+ "SushiNinja",
85
+ "--repo",
86
+ "GitCode-API",
87
+ "--set",
88
+ "only_count=true",
89
+ "--set",
90
+ "reviewer=octocat",
91
+ ]
92
+ )
93
+
94
+ stdout = capsys.readouterr().out
95
+ assert exit_code == 0
96
+ assert captured["params"]["only_count"] == "true"
97
+ assert captured["params"]["reviewer"] == "octocat"
98
+ assert '"open": 3' in stdout
99
+
100
+
101
+ def test_cli_writes_raw_bytes_to_output_file(tmp_path: Path, monkeypatch: Any) -> None:
102
+ output_path = tmp_path / "README.txt"
103
+
104
+ def handler(_request: httpx.Request) -> httpx.Response:
105
+ return httpx.Response(200, content=b"hello from gitcode")
106
+
107
+ _mock_sync_client(monkeypatch, handler)
108
+
109
+ exit_code = main(
110
+ [
111
+ "contents",
112
+ "get-raw",
113
+ "--api-key",
114
+ "test-token",
115
+ "--owner",
116
+ "SushiNinja",
117
+ "--repo",
118
+ "GitCode-API",
119
+ "--path",
120
+ "README.md",
121
+ "--output-file",
122
+ str(output_path),
123
+ ]
124
+ )
125
+
126
+ assert exit_code == 0
127
+ assert output_path.read_bytes() == b"hello from gitcode"
File without changes
File without changes