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.
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/PKG-INFO +39 -26
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/README.md +38 -25
- gitcode_api-1.1.3/gitcode_api/__main__.py +4 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/_base_client.py +18 -12
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/_client.py +7 -1
- gitcode_api-1.1.3/gitcode_api/cli.py +255 -0
- gitcode_api-1.1.3/gitcode_api/version.txt +1 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api.egg-info/PKG-INFO +39 -26
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api.egg-info/SOURCES.txt +5 -0
- gitcode_api-1.1.3/gitcode_api.egg-info/entry_points.txt +2 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/pyproject.toml +5 -2
- gitcode_api-1.1.3/tests/test_cli.py +127 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/LICENSE +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/__init__.py +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/_exceptions.py +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/_models.py +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/resources/__init__.py +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/resources/_shared.py +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/resources/account.py +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/resources/collaboration.py +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/resources/misc.py +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api/resources/repositories.py +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api.egg-info/dependency_links.txt +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api.egg-info/requires.txt +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/gitcode_api.egg-info/top_level.txt +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/setup.cfg +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/tests/test_base_client.py +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/tests/test_client.py +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/tests/test_models.py +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/tests/test_resources_account.py +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/tests/test_resources_collaboration.py +0 -0
- {gitcode_api-1.1.1 → gitcode_api-1.1.3}/tests/test_resources_misc.py +0 -0
- {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.
|
|
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
|
-
   ](https://pypi.org/project/gitcode-api) [](https://github.com/Trenza1ore/GitCode-API) [](https://gitcode.com/SushiNinja/GitCode-API) [](https://pepy.tech/projects/gitcode-api)
|
|
31
31
|
|
|
32
|
-
  
|
|
32
|
+
[](https://gitcode-api.readthedocs.io) [](README.zh.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
|
-
|
|
73
|
-
|
|
74
|
-
branches = client.branches.list(per_page=5)
|
|
99
|
+
repo = client.repos.get()
|
|
100
|
+
branches = client.branches.list(per_page=5)
|
|
75
101
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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`.
|
|
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
|
-
   ](https://pypi.org/project/gitcode-api) [](https://github.com/Trenza1ore/GitCode-API) [](https://gitcode.com/SushiNinja/GitCode-API) [](https://pepy.tech/projects/gitcode-api)
|
|
4
4
|
|
|
5
|
-
  
|
|
5
|
+
[](https://gitcode-api.readthedocs.io) [](README.zh.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
|
-
|
|
46
|
-
|
|
47
|
-
branches = client.branches.list(per_page=5)
|
|
72
|
+
repo = client.repos.get()
|
|
73
|
+
branches = client.branches.list(per_page=5)
|
|
48
74
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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`.
|
|
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:
|
|
@@ -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
|
|
242
|
-
|
|
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
|
|
315
|
-
|
|
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.
|
|
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
|
-
   ](https://pypi.org/project/gitcode-api) [](https://github.com/Trenza1ore/GitCode-API) [](https://gitcode.com/SushiNinja/GitCode-API) [](https://pepy.tech/projects/gitcode-api)
|
|
31
31
|
|
|
32
|
-
  
|
|
32
|
+
[](https://gitcode-api.readthedocs.io) [](README.zh.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
|
-
|
|
73
|
-
|
|
74
|
-
branches = client.branches.list(per_page=5)
|
|
99
|
+
repo = client.repos.get()
|
|
100
|
+
branches = client.branches.list(per_page=5)
|
|
75
101
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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`.
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "gitcode-api"
|
|
3
|
-
version = "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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|