gitcode-api 1.2.20__py3-none-any.whl → 1.3.0__py3-none-any.whl
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/__init__.py +8 -2
- gitcode_api/_base_client.py +7 -2
- gitcode_api/_exceptions.py +8 -0
- gitcode_api/exceptions.py +19 -0
- gitcode_api/llm/mcp.py +5 -5
- gitcode_api/models.py +205 -0
- gitcode_api/resources/__init__.py +1 -1
- gitcode_api/resources/_shared/__init__.py +18 -0
- gitcode_api/resources/_shared/base.py +129 -0
- gitcode_api/resources/{_shared.py → _shared/fetch_template.py} +133 -259
- gitcode_api/resources/account/__init__.py +17 -0
- gitcode_api/resources/account/oauth_resource_group.py +159 -0
- gitcode_api/resources/account/orgs_resource_group.py +422 -0
- gitcode_api/resources/account/search_resource_group.py +236 -0
- gitcode_api/resources/account/users_resource_group.py +249 -0
- gitcode_api/resources/collaboration/__init__.py +20 -0
- gitcode_api/resources/collaboration/_helpers.py +10 -0
- gitcode_api/resources/collaboration/issues_resource_group.py +855 -0
- gitcode_api/resources/collaboration/labels_resource_group.py +248 -0
- gitcode_api/resources/collaboration/members_resource_group.py +195 -0
- gitcode_api/resources/collaboration/milestones_resource_group.py +192 -0
- gitcode_api/resources/collaboration/pulls_resource_group.py +1300 -0
- gitcode_api/resources/misc/__init__.py +14 -0
- gitcode_api/resources/misc/releases_resource_group.py +445 -0
- gitcode_api/resources/misc/tags_resource_group.py +286 -0
- gitcode_api/resources/misc/webhooks_resource_group.py +192 -0
- gitcode_api/resources/repositories/__init__.py +17 -0
- gitcode_api/resources/repositories/branches_resource_group.py +151 -0
- gitcode_api/resources/repositories/commits_resource_group.py +333 -0
- gitcode_api/resources/repositories/repo_contents_resource_group.py +459 -0
- gitcode_api/resources/repositories/repos_resource_group.py +1279 -0
- gitcode_api/version.txt +1 -1
- {gitcode_api-1.2.20.dist-info → gitcode_api-1.3.0.dist-info}/METADATA +3 -3
- gitcode_api-1.3.0.dist-info/RECORD +52 -0
- gitcode_api/resources/account.py +0 -1086
- gitcode_api/resources/collaboration.py +0 -2818
- gitcode_api/resources/misc.py +0 -901
- gitcode_api/resources/repositories.py +0 -2197
- gitcode_api-1.2.20.dist-info/RECORD +0 -31
- {gitcode_api-1.2.20.dist-info → gitcode_api-1.3.0.dist-info}/WHEEL +0 -0
- {gitcode_api-1.2.20.dist-info → gitcode_api-1.3.0.dist-info}/entry_points.txt +0 -0
- {gitcode_api-1.2.20.dist-info → gitcode_api-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {gitcode_api-1.2.20.dist-info → gitcode_api-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -1,12 +1,136 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Template resolution helpers for the GitCode SDK."""
|
|
2
2
|
|
|
3
|
-
from typing import Any,
|
|
3
|
+
from typing import Any, List, Optional, Pattern, Tuple
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
from ..._base_client import AsyncAPIClient, SyncAPIClient
|
|
6
|
+
from ..._exceptions import GitCodeHTTPStatusError
|
|
7
|
+
from ...constants import GITCODE_TEMPLATE_REPO
|
|
8
|
+
|
|
9
|
+
# pylint: disable=invalid-overridden-method,protected-access,redefined-builtin
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def list_gitcode_template_rows_sync(
|
|
13
|
+
client: SyncAPIClient,
|
|
14
|
+
owner: str,
|
|
15
|
+
repo: str,
|
|
16
|
+
path_pattern: Pattern[str],
|
|
17
|
+
) -> List[Tuple[str, str, str, str]]:
|
|
18
|
+
"""Return ``(template_owner, template_repo, path, sha)`` from the first resolution source with matches."""
|
|
19
|
+
for so, sr in _resolution_sources_sync(client, owner, repo):
|
|
20
|
+
acc: List[Tuple[str, str, str, str]] = []
|
|
21
|
+
try:
|
|
22
|
+
_walk_dot_gitcode_contents_sync(client, so, sr, ".gitcode", acc)
|
|
23
|
+
if sr != GITCODE_TEMPLATE_REPO:
|
|
24
|
+
_walk_dot_gitcode_contents_sync(client, so, sr, ".github", acc)
|
|
25
|
+
except GitCodeHTTPStatusError:
|
|
26
|
+
continue
|
|
27
|
+
rows = [(t_o, t_r, p, s) for t_o, t_r, p, s in acc if path_pattern.match(p)]
|
|
28
|
+
if rows:
|
|
29
|
+
return rows
|
|
30
|
+
return []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def list_gitcode_template_rows_async(
|
|
34
|
+
client: AsyncAPIClient,
|
|
35
|
+
owner: str,
|
|
36
|
+
repo: str,
|
|
37
|
+
path_pattern: Pattern[str],
|
|
38
|
+
) -> List[Tuple[str, str, str, str]]:
|
|
39
|
+
"""Return ``(template_owner, template_repo, path, sha)`` from the first resolution source with matches."""
|
|
40
|
+
for so, sr in await _resolution_sources_async(client, owner, repo):
|
|
41
|
+
acc: List[Tuple[str, str, str, str]] = []
|
|
42
|
+
try:
|
|
43
|
+
await _walk_dot_gitcode_contents_async(client, so, sr, ".gitcode", acc)
|
|
44
|
+
if sr != GITCODE_TEMPLATE_REPO:
|
|
45
|
+
await _walk_dot_gitcode_contents_async(client, so, sr, ".github", acc)
|
|
46
|
+
except GitCodeHTTPStatusError:
|
|
47
|
+
continue
|
|
48
|
+
rows = [(t_o, t_r, p, s) for t_o, t_r, p, s in acc if path_pattern.match(p)]
|
|
49
|
+
if rows:
|
|
50
|
+
return rows
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_gitcode_template_body_sync(
|
|
55
|
+
client: SyncAPIClient,
|
|
56
|
+
path: str,
|
|
57
|
+
path_pattern: Pattern[str],
|
|
58
|
+
owner: str,
|
|
59
|
+
repo: str,
|
|
60
|
+
encoding: str = "utf-8",
|
|
61
|
+
**decoding_kwargs,
|
|
62
|
+
) -> str:
|
|
63
|
+
"""Fetch raw template text from the first resolution source that serves ``path``."""
|
|
64
|
+
if not path_pattern.match(path):
|
|
65
|
+
raise GitCodeHTTPStatusError(
|
|
66
|
+
"Path does not match the expected GitCode template pattern for this resource.",
|
|
67
|
+
status_code=404,
|
|
68
|
+
payload=None,
|
|
69
|
+
)
|
|
70
|
+
last_error: Optional[GitCodeHTTPStatusError] = None
|
|
71
|
+
for so, sr in _resolution_sources_sync(client, owner, repo):
|
|
72
|
+
try:
|
|
73
|
+
raw = client.request(
|
|
74
|
+
"GET",
|
|
75
|
+
client._repo_file_path("raw", path, owner=so, repo=sr),
|
|
76
|
+
raw=True,
|
|
77
|
+
)
|
|
78
|
+
except GitCodeHTTPStatusError as exc:
|
|
79
|
+
last_error = exc
|
|
80
|
+
continue
|
|
81
|
+
if isinstance(raw, bytes):
|
|
82
|
+
return raw.decode(encoding=encoding, **decoding_kwargs)
|
|
83
|
+
if isinstance(raw, str):
|
|
84
|
+
return raw
|
|
85
|
+
return str(raw)
|
|
86
|
+
if last_error is not None:
|
|
87
|
+
raise last_error
|
|
88
|
+
raise GitCodeHTTPStatusError(
|
|
89
|
+
"No template found for the given path.",
|
|
90
|
+
status_code=404,
|
|
91
|
+
payload=None,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def get_gitcode_template_body_async(
|
|
96
|
+
client: AsyncAPIClient,
|
|
97
|
+
path: str,
|
|
98
|
+
path_pattern: Pattern[str],
|
|
99
|
+
owner: str,
|
|
100
|
+
repo: str,
|
|
101
|
+
encoding: str = "utf-8",
|
|
102
|
+
**decoding_kwargs,
|
|
103
|
+
) -> str:
|
|
104
|
+
"""Fetch raw template text from the first resolution source that serves ``path``."""
|
|
105
|
+
if not path_pattern.match(path):
|
|
106
|
+
raise GitCodeHTTPStatusError(
|
|
107
|
+
"Path does not match the expected GitCode template pattern for this resource.",
|
|
108
|
+
status_code=404,
|
|
109
|
+
payload=None,
|
|
110
|
+
)
|
|
111
|
+
last_error: Optional[GitCodeHTTPStatusError] = None
|
|
112
|
+
for so, sr in await _resolution_sources_async(client, owner, repo):
|
|
113
|
+
try:
|
|
114
|
+
raw = await client.request(
|
|
115
|
+
"GET",
|
|
116
|
+
client._repo_file_path("raw", path, owner=so, repo=sr),
|
|
117
|
+
raw=True,
|
|
118
|
+
)
|
|
119
|
+
except GitCodeHTTPStatusError as exc:
|
|
120
|
+
last_error = exc
|
|
121
|
+
continue
|
|
122
|
+
if isinstance(raw, bytes):
|
|
123
|
+
return raw.decode(encoding=encoding, **decoding_kwargs)
|
|
124
|
+
if isinstance(raw, str):
|
|
125
|
+
return raw
|
|
126
|
+
return str(raw)
|
|
127
|
+
if last_error is not None:
|
|
128
|
+
raise last_error
|
|
129
|
+
raise GitCodeHTTPStatusError(
|
|
130
|
+
"No template found for the given path.",
|
|
131
|
+
status_code=404,
|
|
132
|
+
payload=None,
|
|
133
|
+
)
|
|
10
134
|
|
|
11
135
|
|
|
12
136
|
def _parse_parent_owner_repo(repo_obj: Any) -> Optional[Tuple[str, str]]:
|
|
@@ -32,8 +156,6 @@ def _resolution_sources_sync(client: SyncAPIClient, owner: str, repo: str) -> Li
|
|
|
32
156
|
fork (per ``GET /repos/{owner}/{repo}``), appends that repository's parent main repo and
|
|
33
157
|
``(parent_owner, ".gitcode")`` when not already listed (deduplicated, breadth-first).
|
|
34
158
|
"""
|
|
35
|
-
from .repositories import ReposResource
|
|
36
|
-
|
|
37
159
|
sources: List[Tuple[str, str]] = []
|
|
38
160
|
seen: set[Tuple[str, str]] = set()
|
|
39
161
|
|
|
@@ -51,7 +173,7 @@ def _resolution_sources_sync(client: SyncAPIClient, owner: str, repo: str) -> Li
|
|
|
51
173
|
so, sr = sources[index]
|
|
52
174
|
index += 1
|
|
53
175
|
try:
|
|
54
|
-
meta =
|
|
176
|
+
meta = getattr(client, "repos").get(owner=so, repo=sr)
|
|
55
177
|
except GitCodeHTTPStatusError:
|
|
56
178
|
continue
|
|
57
179
|
if not getattr(meta, "fork", None):
|
|
@@ -68,8 +190,6 @@ def _resolution_sources_sync(client: SyncAPIClient, owner: str, repo: str) -> Li
|
|
|
68
190
|
|
|
69
191
|
async def _resolution_sources_async(client: AsyncAPIClient, owner: str, repo: str) -> List[Tuple[str, str]]:
|
|
70
192
|
"""Async counterpart of :func:`_resolution_sources_sync`."""
|
|
71
|
-
from .repositories import AsyncReposResource
|
|
72
|
-
|
|
73
193
|
sources: List[Tuple[str, str]] = []
|
|
74
194
|
seen: set[Tuple[str, str]] = set()
|
|
75
195
|
|
|
@@ -87,7 +207,7 @@ async def _resolution_sources_async(client: AsyncAPIClient, owner: str, repo: st
|
|
|
87
207
|
so, sr = sources[index]
|
|
88
208
|
index += 1
|
|
89
209
|
try:
|
|
90
|
-
meta = await
|
|
210
|
+
meta = await getattr(client, "repos").get(owner=so, repo=sr)
|
|
91
211
|
except GitCodeHTTPStatusError:
|
|
92
212
|
continue
|
|
93
213
|
if not getattr(meta, "fork", None):
|
|
@@ -190,249 +310,3 @@ async def _walk_dot_gitcode_contents_async(
|
|
|
190
310
|
acc.append((template_owner, template_repo, str(pth), str(sha)))
|
|
191
311
|
elif t in ("dir", "directory", "tree") and pth:
|
|
192
312
|
await _walk_dot_gitcode_contents_async(client, template_owner, template_repo, str(pth), acc)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def list_gitcode_template_rows_sync(
|
|
196
|
-
client: SyncAPIClient,
|
|
197
|
-
owner: str,
|
|
198
|
-
repo: str,
|
|
199
|
-
path_pattern: Pattern[str],
|
|
200
|
-
) -> List[Tuple[str, str, str, str]]:
|
|
201
|
-
"""Return ``(template_owner, template_repo, path, sha)`` from the first resolution source with matches."""
|
|
202
|
-
for so, sr in _resolution_sources_sync(client, owner, repo):
|
|
203
|
-
acc: List[Tuple[str, str, str, str]] = []
|
|
204
|
-
try:
|
|
205
|
-
_walk_dot_gitcode_contents_sync(client, so, sr, ".gitcode", acc)
|
|
206
|
-
if sr != GITCODE_TEMPLATE_REPO:
|
|
207
|
-
_walk_dot_gitcode_contents_sync(client, so, sr, ".github", acc)
|
|
208
|
-
except GitCodeHTTPStatusError:
|
|
209
|
-
continue
|
|
210
|
-
rows = [(t_o, t_r, p, s) for t_o, t_r, p, s in acc if path_pattern.match(p)]
|
|
211
|
-
if rows:
|
|
212
|
-
return rows
|
|
213
|
-
return []
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
async def list_gitcode_template_rows_async(
|
|
217
|
-
client: AsyncAPIClient,
|
|
218
|
-
owner: str,
|
|
219
|
-
repo: str,
|
|
220
|
-
path_pattern: Pattern[str],
|
|
221
|
-
) -> List[Tuple[str, str, str, str]]:
|
|
222
|
-
"""Return ``(template_owner, template_repo, path, sha)`` from the first resolution source with matches."""
|
|
223
|
-
for so, sr in await _resolution_sources_async(client, owner, repo):
|
|
224
|
-
acc: List[Tuple[str, str, str, str]] = []
|
|
225
|
-
try:
|
|
226
|
-
await _walk_dot_gitcode_contents_async(client, so, sr, ".gitcode", acc)
|
|
227
|
-
if sr != GITCODE_TEMPLATE_REPO:
|
|
228
|
-
await _walk_dot_gitcode_contents_async(client, so, sr, ".github", acc)
|
|
229
|
-
except GitCodeHTTPStatusError:
|
|
230
|
-
continue
|
|
231
|
-
rows = [(t_o, t_r, p, s) for t_o, t_r, p, s in acc if path_pattern.match(p)]
|
|
232
|
-
if rows:
|
|
233
|
-
return rows
|
|
234
|
-
return []
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
def get_gitcode_template_body_sync(
|
|
238
|
-
client: SyncAPIClient,
|
|
239
|
-
path: str,
|
|
240
|
-
path_pattern: Pattern[str],
|
|
241
|
-
owner: str,
|
|
242
|
-
repo: str,
|
|
243
|
-
encoding: str = "utf-8",
|
|
244
|
-
**decoding_kwargs,
|
|
245
|
-
) -> str:
|
|
246
|
-
"""Fetch raw template text from the first resolution source that serves ``path``."""
|
|
247
|
-
if not path_pattern.match(path):
|
|
248
|
-
raise GitCodeHTTPStatusError(
|
|
249
|
-
"Path does not match the expected GitCode template pattern for this resource.",
|
|
250
|
-
status_code=404,
|
|
251
|
-
payload=None,
|
|
252
|
-
)
|
|
253
|
-
last_error: Optional[GitCodeHTTPStatusError] = None
|
|
254
|
-
for so, sr in _resolution_sources_sync(client, owner, repo):
|
|
255
|
-
try:
|
|
256
|
-
raw = client.request(
|
|
257
|
-
"GET",
|
|
258
|
-
client._repo_file_path("raw", path, owner=so, repo=sr),
|
|
259
|
-
raw=True,
|
|
260
|
-
)
|
|
261
|
-
except GitCodeHTTPStatusError as exc:
|
|
262
|
-
last_error = exc
|
|
263
|
-
continue
|
|
264
|
-
if isinstance(raw, bytes):
|
|
265
|
-
return raw.decode(encoding=encoding, **decoding_kwargs)
|
|
266
|
-
if isinstance(raw, str):
|
|
267
|
-
return raw
|
|
268
|
-
return str(raw)
|
|
269
|
-
if last_error is not None:
|
|
270
|
-
raise last_error
|
|
271
|
-
raise GitCodeHTTPStatusError(
|
|
272
|
-
"No template found for the given path.",
|
|
273
|
-
status_code=404,
|
|
274
|
-
payload=None,
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
async def get_gitcode_template_body_async(
|
|
279
|
-
client: AsyncAPIClient,
|
|
280
|
-
path: str,
|
|
281
|
-
path_pattern: Pattern[str],
|
|
282
|
-
owner: str,
|
|
283
|
-
repo: str,
|
|
284
|
-
encoding: str = "utf-8",
|
|
285
|
-
**decoding_kwargs,
|
|
286
|
-
) -> str:
|
|
287
|
-
"""Fetch raw template text from the first resolution source that serves ``path``."""
|
|
288
|
-
if not path_pattern.match(path):
|
|
289
|
-
raise GitCodeHTTPStatusError(
|
|
290
|
-
"Path does not match the expected GitCode template pattern for this resource.",
|
|
291
|
-
status_code=404,
|
|
292
|
-
payload=None,
|
|
293
|
-
)
|
|
294
|
-
last_error: Optional[GitCodeHTTPStatusError] = None
|
|
295
|
-
for so, sr in await _resolution_sources_async(client, owner, repo):
|
|
296
|
-
try:
|
|
297
|
-
raw = await client.request(
|
|
298
|
-
"GET",
|
|
299
|
-
client._repo_file_path("raw", path, owner=so, repo=sr),
|
|
300
|
-
raw=True,
|
|
301
|
-
)
|
|
302
|
-
except GitCodeHTTPStatusError as exc:
|
|
303
|
-
last_error = exc
|
|
304
|
-
continue
|
|
305
|
-
if isinstance(raw, bytes):
|
|
306
|
-
return raw.decode(encoding=encoding, **decoding_kwargs)
|
|
307
|
-
if isinstance(raw, str):
|
|
308
|
-
return raw
|
|
309
|
-
return str(raw)
|
|
310
|
-
if last_error is not None:
|
|
311
|
-
raise last_error
|
|
312
|
-
raise GitCodeHTTPStatusError(
|
|
313
|
-
"No template found for the given path.",
|
|
314
|
-
status_code=404,
|
|
315
|
-
payload=None,
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
class SyncResource(BaseResource):
|
|
320
|
-
"""Base class for synchronous resource groups."""
|
|
321
|
-
|
|
322
|
-
def __init__(self, client: SyncAPIClient) -> None:
|
|
323
|
-
"""Bind the resource to a synchronous API client."""
|
|
324
|
-
self._client = client
|
|
325
|
-
|
|
326
|
-
def _request(
|
|
327
|
-
self,
|
|
328
|
-
method: str,
|
|
329
|
-
path: str,
|
|
330
|
-
*,
|
|
331
|
-
params: Optional[Dict[str, Any]] = None,
|
|
332
|
-
json: Any = None,
|
|
333
|
-
data: Optional[Dict[str, Any]] = None,
|
|
334
|
-
raw: bool = False,
|
|
335
|
-
) -> Any:
|
|
336
|
-
"""Dispatch a low-level request through the owning client.
|
|
337
|
-
|
|
338
|
-
:param method: HTTP verb (for example ``GET`` or ``POST``).
|
|
339
|
-
:param path: Absolute or root-relative URL path.
|
|
340
|
-
:param params: Optional query string parameters.
|
|
341
|
-
:param json: Optional JSON-serializable request body.
|
|
342
|
-
:param data: Optional form or body fields.
|
|
343
|
-
:param raw: When ``True``, return the raw response body (for example bytes).
|
|
344
|
-
:returns: Parsed JSON, raw body, or other value from the client.
|
|
345
|
-
"""
|
|
346
|
-
return self._client.request(method, path, params=params, json=json, data=data, raw=raw)
|
|
347
|
-
|
|
348
|
-
def _model(self, method: str, path: str, model_type: type[ModelT], **kwargs) -> ModelT:
|
|
349
|
-
"""Send a request and wrap a JSON object in ``model_type``.
|
|
350
|
-
|
|
351
|
-
:param method: HTTP verb.
|
|
352
|
-
:param path: Request path.
|
|
353
|
-
:param model_type: Model class for the top-level JSON object.
|
|
354
|
-
:param kwargs: Forwarded to :meth:`_request` (``params``, ``json``, ``data``, ``raw``, etc.).
|
|
355
|
-
:returns: An instance of ``model_type``.
|
|
356
|
-
"""
|
|
357
|
-
data = self._request(method, path, **kwargs)
|
|
358
|
-
return as_model(data, model_type)
|
|
359
|
-
|
|
360
|
-
def _models(self, method: str, path: str, model_type: type[ModelT], **kwargs) -> List[ModelT]:
|
|
361
|
-
"""Send a request and wrap a JSON array in ``model_type`` instances.
|
|
362
|
-
|
|
363
|
-
:param method: HTTP verb.
|
|
364
|
-
:param path: Request path.
|
|
365
|
-
:param model_type: Model class for each element of the JSON array.
|
|
366
|
-
:param kwargs: Forwarded to :meth:`_request`.
|
|
367
|
-
:returns: A list of ``model_type`` instances.
|
|
368
|
-
"""
|
|
369
|
-
data = self._request(method, path, **kwargs)
|
|
370
|
-
return as_model_list(data, model_type)
|
|
371
|
-
|
|
372
|
-
def _maybe_model(self, method: str, path: str, model_type: type[ModelT], **kwargs) -> Union[ModelT, APIObject]:
|
|
373
|
-
"""Wrap dict responses as models and scalar responses as ``APIObject``.
|
|
374
|
-
|
|
375
|
-
:param method: HTTP verb.
|
|
376
|
-
:param path: Request path.
|
|
377
|
-
:param model_type: Model class when the response is a JSON object.
|
|
378
|
-
:param kwargs: Forwarded to :meth:`_request`.
|
|
379
|
-
:returns: ``model_type`` for dict bodies; otherwise ``APIObject`` wrapping a ``value`` field.
|
|
380
|
-
"""
|
|
381
|
-
data = self._request(method, path, **kwargs)
|
|
382
|
-
if isinstance(data, dict):
|
|
383
|
-
return as_model(data, model_type)
|
|
384
|
-
return APIObject({"value": data})
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
class AsyncResource(BaseResource):
|
|
388
|
-
"""Base class for asynchronous resource groups."""
|
|
389
|
-
|
|
390
|
-
def __init__(self, client: AsyncAPIClient) -> None:
|
|
391
|
-
"""Bind the resource to an asynchronous API client."""
|
|
392
|
-
self._client = client
|
|
393
|
-
|
|
394
|
-
async def _request(
|
|
395
|
-
self,
|
|
396
|
-
method: str,
|
|
397
|
-
path: str,
|
|
398
|
-
*,
|
|
399
|
-
params: Optional[Dict[str, Any]] = None,
|
|
400
|
-
json: Any = None,
|
|
401
|
-
data: Optional[Dict[str, Any]] = None,
|
|
402
|
-
raw: bool = False,
|
|
403
|
-
) -> Any:
|
|
404
|
-
"""Dispatch a low-level async request through the owning client.
|
|
405
|
-
|
|
406
|
-
:param method: HTTP verb (for example ``GET`` or ``POST``).
|
|
407
|
-
:param path: Absolute or root-relative URL path.
|
|
408
|
-
:param params: Optional query string parameters.
|
|
409
|
-
:param json: Optional JSON-serializable request body.
|
|
410
|
-
:param data: Optional form or body fields.
|
|
411
|
-
:param raw: When ``True``, return the raw response body (for example bytes).
|
|
412
|
-
:returns: Parsed JSON, raw body, or other value from the client.
|
|
413
|
-
"""
|
|
414
|
-
return await self._client.request(method, path, params=params, json=json, data=data, raw=raw)
|
|
415
|
-
|
|
416
|
-
async def _model(self, method: str, path: str, model_type: type[ModelT], **kwargs) -> ModelT:
|
|
417
|
-
"""Send a request and wrap a JSON object in ``model_type``.
|
|
418
|
-
|
|
419
|
-
:param method: HTTP verb.
|
|
420
|
-
:param path: Request path.
|
|
421
|
-
:param model_type: Model class for the top-level JSON object.
|
|
422
|
-
:param kwargs: Forwarded to :meth:`_request`.
|
|
423
|
-
:returns: An instance of ``model_type``.
|
|
424
|
-
"""
|
|
425
|
-
data = await self._request(method, path, **kwargs)
|
|
426
|
-
return as_model(data, model_type)
|
|
427
|
-
|
|
428
|
-
async def _models(self, method: str, path: str, model_type: type[ModelT], **kwargs) -> List[ModelT]:
|
|
429
|
-
"""Send a request and wrap a JSON array in ``model_type`` instances.
|
|
430
|
-
|
|
431
|
-
:param method: HTTP verb.
|
|
432
|
-
:param path: Request path.
|
|
433
|
-
:param model_type: Model class for each element of the JSON array.
|
|
434
|
-
:param kwargs: Forwarded to :meth:`_request`.
|
|
435
|
-
:returns: A list of ``model_type`` instances.
|
|
436
|
-
"""
|
|
437
|
-
data = await self._request(method, path, **kwargs)
|
|
438
|
-
return as_model_list(data, model_type)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Classes for resource groups: OAuth, organizations, search, and users."""
|
|
2
|
+
|
|
3
|
+
from .oauth_resource_group import AsyncOAuthResource, OAuthResource
|
|
4
|
+
from .orgs_resource_group import AsyncOrgsResource, OrgsResource
|
|
5
|
+
from .search_resource_group import AsyncSearchResource, SearchResource
|
|
6
|
+
from .users_resource_group import AsyncUsersResource, UsersResource
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AsyncOAuthResource",
|
|
10
|
+
"AsyncOrgsResource",
|
|
11
|
+
"AsyncSearchResource",
|
|
12
|
+
"AsyncUsersResource",
|
|
13
|
+
"OAuthResource",
|
|
14
|
+
"OrgsResource",
|
|
15
|
+
"SearchResource",
|
|
16
|
+
"UsersResource",
|
|
17
|
+
]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""AbstractOAuthResource, OAuthResource, and AsyncOAuthResource resource group."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from urllib.parse import urlencode
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ..._models import OAuthToken
|
|
10
|
+
from .._shared import AsyncResource, SyncResource
|
|
11
|
+
|
|
12
|
+
# mypy: disable-error-code=override
|
|
13
|
+
# pylint: disable=invalid-overridden-method,protected-access,redefined-builtin
|
|
14
|
+
|
|
15
|
+
OAUTH_BASE_URL = "https://gitcode.com"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AbstractOAuthResource(ABC):
|
|
19
|
+
"""Interface for OAuth resource endpoints."""
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def build_authorize_url(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
client_id: str,
|
|
26
|
+
redirect_uri: str,
|
|
27
|
+
scope: Optional[str] = None,
|
|
28
|
+
state: Optional[str] = None,
|
|
29
|
+
response_type: str = "code",
|
|
30
|
+
) -> str:
|
|
31
|
+
"""Build the GitCode OAuth authorization URL.
|
|
32
|
+
|
|
33
|
+
:param client_id: OAuth application client ID.
|
|
34
|
+
:param redirect_uri: Registered redirect URI.
|
|
35
|
+
:param scope: Optional OAuth scopes.
|
|
36
|
+
:param state: Optional CSRF protection value.
|
|
37
|
+
:param response_type: OAuth response type, defaults to ``"code"``.
|
|
38
|
+
:returns: Browser URL for the authorization step.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def exchange_token(self, *, code: str, client_id: str, client_secret: str) -> OAuthToken:
|
|
43
|
+
"""Exchange an authorization code for an OAuth token.
|
|
44
|
+
|
|
45
|
+
:param code: Authorization code returned by GitCode.
|
|
46
|
+
:param client_id: OAuth application client ID.
|
|
47
|
+
:param client_secret: OAuth application client secret.
|
|
48
|
+
:returns: OAuth access token payload.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def refresh_token(self, *, refresh_token: str) -> OAuthToken:
|
|
53
|
+
"""Refresh an OAuth token.
|
|
54
|
+
|
|
55
|
+
:param refresh_token: Refresh token previously issued by GitCode.
|
|
56
|
+
:returns: Refreshed OAuth token payload.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class OAuthResource(SyncResource, AbstractOAuthResource):
|
|
61
|
+
"""Helpers for GitCode OAuth URLs and token exchange."""
|
|
62
|
+
|
|
63
|
+
def build_authorize_url(
|
|
64
|
+
self,
|
|
65
|
+
*,
|
|
66
|
+
client_id: str,
|
|
67
|
+
redirect_uri: str,
|
|
68
|
+
scope: Optional[str] = None,
|
|
69
|
+
state: Optional[str] = None,
|
|
70
|
+
response_type: str = "code",
|
|
71
|
+
) -> str:
|
|
72
|
+
query = urlencode(
|
|
73
|
+
{
|
|
74
|
+
key: value
|
|
75
|
+
for key, value in {
|
|
76
|
+
"client_id": client_id,
|
|
77
|
+
"redirect_uri": redirect_uri,
|
|
78
|
+
"response_type": response_type,
|
|
79
|
+
"scope": scope,
|
|
80
|
+
"state": state,
|
|
81
|
+
}.items()
|
|
82
|
+
if value is not None
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
return f"{OAUTH_BASE_URL}/oauth/authorize?{query}"
|
|
86
|
+
|
|
87
|
+
def exchange_token(self, *, code: str, client_id: str, client_secret: str) -> OAuthToken:
|
|
88
|
+
response = httpx.post(
|
|
89
|
+
f"{OAUTH_BASE_URL}/oauth/token",
|
|
90
|
+
params={"grant_type": "authorization_code", "code": code, "client_id": client_id},
|
|
91
|
+
data={"client_secret": client_secret},
|
|
92
|
+
headers={"Accept": "application/json"},
|
|
93
|
+
timeout=self._client.timeout,
|
|
94
|
+
)
|
|
95
|
+
response.raise_for_status()
|
|
96
|
+
return OAuthToken(dict(response.json()))
|
|
97
|
+
|
|
98
|
+
def refresh_token(self, *, refresh_token: str) -> OAuthToken:
|
|
99
|
+
response = httpx.post(
|
|
100
|
+
f"{OAUTH_BASE_URL}/oauth/token",
|
|
101
|
+
params={"grant_type": "refresh_token", "refresh_token": refresh_token},
|
|
102
|
+
headers={"Accept": "application/json"},
|
|
103
|
+
timeout=self._client.timeout,
|
|
104
|
+
)
|
|
105
|
+
response.raise_for_status()
|
|
106
|
+
return OAuthToken(dict(response.json()))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AsyncOAuthResource(AsyncResource, AbstractOAuthResource):
|
|
110
|
+
"""Asynchronous helpers for GitCode OAuth flows.
|
|
111
|
+
|
|
112
|
+
``build_authorize_url`` matches :class:`OAuthResource` exactly. Token helpers mirror
|
|
113
|
+
:meth:`OAuthResource.exchange_token` and :meth:`OAuthResource.refresh_token` (see ``docs/rest_api/oauth``).
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def build_authorize_url(
|
|
117
|
+
self,
|
|
118
|
+
*,
|
|
119
|
+
client_id: str,
|
|
120
|
+
redirect_uri: str,
|
|
121
|
+
scope: Optional[str] = None,
|
|
122
|
+
state: Optional[str] = None,
|
|
123
|
+
response_type: str = "code",
|
|
124
|
+
) -> str:
|
|
125
|
+
query = urlencode(
|
|
126
|
+
{
|
|
127
|
+
key: value
|
|
128
|
+
for key, value in {
|
|
129
|
+
"client_id": client_id,
|
|
130
|
+
"redirect_uri": redirect_uri,
|
|
131
|
+
"response_type": response_type,
|
|
132
|
+
"scope": scope,
|
|
133
|
+
"state": state,
|
|
134
|
+
}.items()
|
|
135
|
+
if value is not None
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
return f"{OAUTH_BASE_URL}/oauth/authorize?{query}"
|
|
139
|
+
|
|
140
|
+
async def exchange_token(self, *, code: str, client_id: str, client_secret: str) -> OAuthToken:
|
|
141
|
+
async with httpx.AsyncClient(timeout=self._client.timeout) as client:
|
|
142
|
+
response = await client.post(
|
|
143
|
+
f"{OAUTH_BASE_URL}/oauth/token",
|
|
144
|
+
params={"grant_type": "authorization_code", "code": code, "client_id": client_id},
|
|
145
|
+
data={"client_secret": client_secret},
|
|
146
|
+
headers={"Accept": "application/json"},
|
|
147
|
+
)
|
|
148
|
+
response.raise_for_status()
|
|
149
|
+
return OAuthToken(dict(response.json()))
|
|
150
|
+
|
|
151
|
+
async def refresh_token(self, *, refresh_token: str) -> OAuthToken:
|
|
152
|
+
async with httpx.AsyncClient(timeout=self._client.timeout) as client:
|
|
153
|
+
response = await client.post(
|
|
154
|
+
f"{OAUTH_BASE_URL}/oauth/token",
|
|
155
|
+
params={"grant_type": "refresh_token", "refresh_token": refresh_token},
|
|
156
|
+
headers={"Accept": "application/json"},
|
|
157
|
+
)
|
|
158
|
+
response.raise_for_status()
|
|
159
|
+
return OAuthToken(dict(response.json()))
|