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.
Files changed (43) hide show
  1. gitcode_api/__init__.py +8 -2
  2. gitcode_api/_base_client.py +7 -2
  3. gitcode_api/_exceptions.py +8 -0
  4. gitcode_api/exceptions.py +19 -0
  5. gitcode_api/llm/mcp.py +5 -5
  6. gitcode_api/models.py +205 -0
  7. gitcode_api/resources/__init__.py +1 -1
  8. gitcode_api/resources/_shared/__init__.py +18 -0
  9. gitcode_api/resources/_shared/base.py +129 -0
  10. gitcode_api/resources/{_shared.py → _shared/fetch_template.py} +133 -259
  11. gitcode_api/resources/account/__init__.py +17 -0
  12. gitcode_api/resources/account/oauth_resource_group.py +159 -0
  13. gitcode_api/resources/account/orgs_resource_group.py +422 -0
  14. gitcode_api/resources/account/search_resource_group.py +236 -0
  15. gitcode_api/resources/account/users_resource_group.py +249 -0
  16. gitcode_api/resources/collaboration/__init__.py +20 -0
  17. gitcode_api/resources/collaboration/_helpers.py +10 -0
  18. gitcode_api/resources/collaboration/issues_resource_group.py +855 -0
  19. gitcode_api/resources/collaboration/labels_resource_group.py +248 -0
  20. gitcode_api/resources/collaboration/members_resource_group.py +195 -0
  21. gitcode_api/resources/collaboration/milestones_resource_group.py +192 -0
  22. gitcode_api/resources/collaboration/pulls_resource_group.py +1300 -0
  23. gitcode_api/resources/misc/__init__.py +14 -0
  24. gitcode_api/resources/misc/releases_resource_group.py +445 -0
  25. gitcode_api/resources/misc/tags_resource_group.py +286 -0
  26. gitcode_api/resources/misc/webhooks_resource_group.py +192 -0
  27. gitcode_api/resources/repositories/__init__.py +17 -0
  28. gitcode_api/resources/repositories/branches_resource_group.py +151 -0
  29. gitcode_api/resources/repositories/commits_resource_group.py +333 -0
  30. gitcode_api/resources/repositories/repo_contents_resource_group.py +459 -0
  31. gitcode_api/resources/repositories/repos_resource_group.py +1279 -0
  32. gitcode_api/version.txt +1 -1
  33. {gitcode_api-1.2.20.dist-info → gitcode_api-1.3.0.dist-info}/METADATA +3 -3
  34. gitcode_api-1.3.0.dist-info/RECORD +52 -0
  35. gitcode_api/resources/account.py +0 -1086
  36. gitcode_api/resources/collaboration.py +0 -2818
  37. gitcode_api/resources/misc.py +0 -901
  38. gitcode_api/resources/repositories.py +0 -2197
  39. gitcode_api-1.2.20.dist-info/RECORD +0 -31
  40. {gitcode_api-1.2.20.dist-info → gitcode_api-1.3.0.dist-info}/WHEEL +0 -0
  41. {gitcode_api-1.2.20.dist-info → gitcode_api-1.3.0.dist-info}/entry_points.txt +0 -0
  42. {gitcode_api-1.2.20.dist-info → gitcode_api-1.3.0.dist-info}/licenses/LICENSE +0 -0
  43. {gitcode_api-1.2.20.dist-info → gitcode_api-1.3.0.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,136 @@
1
- """Shared resource base classes for the GitCode SDK."""
1
+ """Template resolution helpers for the GitCode SDK."""
2
2
 
3
- from typing import Any, Dict, List, Optional, Pattern, Tuple, Union
3
+ from typing import Any, List, Optional, Pattern, Tuple
4
4
 
5
- from .._base_client import AsyncAPIClient, SyncAPIClient
6
- from .._base_resource import BaseResource
7
- from .._exceptions import GitCodeHTTPStatusError
8
- from .._models import APIObject, ModelT, as_model, as_model_list
9
- from ..constants import GITCODE_TEMPLATE_REPO
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 = ReposResource(client).get(owner=so, repo=sr)
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 AsyncReposResource(client).get(owner=so, repo=sr)
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()))