gitcode-api 1.2.13__py3-none-any.whl → 1.2.15__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 CHANGED
@@ -9,6 +9,7 @@ from ._exceptions import (
9
9
  GitCodeError,
10
10
  GitCodeHTTPStatusError,
11
11
  )
12
+ from .utils import as_dict
12
13
 
13
14
  __version__ = (Path(__file__).parent / "version.txt").read_text().strip()
14
15
 
@@ -20,4 +21,5 @@ __all__ = [
20
21
  "GitCodeConfigurationError",
21
22
  "GitCodeError",
22
23
  "GitCodeHTTPStatusError",
24
+ "as_dict",
23
25
  ]
gitcode_api/_models.py CHANGED
@@ -9,7 +9,6 @@ from typing import (
9
9
  Mapping,
10
10
  MutableMapping,
11
11
  Optional,
12
- TypedDict,
13
12
  TypeVar,
14
13
  Union,
15
14
  get_args,
@@ -136,60 +135,6 @@ def as_model_list(data: List[Mapping[str, Any]], model_type: type[ModelT]) -> Li
136
135
  return [as_model(item, model_type) for item in data]
137
136
 
138
137
 
139
- class RepositoryCreateParams(TypedDict, total=False):
140
- """Typed fields accepted by repository creation endpoints."""
141
-
142
- name: str
143
- description: str
144
- has_issues: bool
145
- has_wiki: bool
146
- auto_init: bool
147
- gitignore_template: str
148
- license_template: str
149
- path: str
150
- private: bool
151
- public: int
152
- default_branch: str
153
- homepage: str
154
- can_comment: bool
155
-
156
-
157
- class IssueCreateParams(TypedDict, total=False):
158
- """Typed fields accepted by issue creation endpoints."""
159
-
160
- title: str
161
- body: str
162
- assignee: str
163
- labels: List[str]
164
- milestone: Union[int, str]
165
-
166
-
167
- class PullRequestCreateParams(TypedDict, total=False):
168
- """Typed fields accepted by pull request creation endpoints."""
169
-
170
- title: str
171
- body: str
172
- head: str
173
- base: str
174
- assignees: List[str]
175
- testers: List[str]
176
- labels: List[str]
177
- draft: bool
178
-
179
-
180
- class WebhookCreateParams(TypedDict, total=False):
181
- """Typed fields accepted by webhook creation endpoints."""
182
-
183
- url: str
184
- encryption_type: int
185
- password: str
186
- push_events: bool
187
- tag_push_events: bool
188
- issues_events: bool
189
- note_events: bool
190
- merge_requests_events: bool
191
-
192
-
193
138
  @dataclass(init=False)
194
139
  class UserRef(APIObject):
195
140
  """Compact user payload embedded in other responses.
@@ -513,6 +458,8 @@ class Repository(APIObject):
513
458
  :vartype relation: Optional[str]
514
459
  :ivar members: Members returned in the repository payload.
515
460
  :vartype members: Optional[List[str]]
461
+ :ivar parent: Upstream source repository information
462
+ :vartype parent: Optional[Dict[str, Any]]
516
463
  :ivar permission: Permission summary for this object.
517
464
  :vartype permission: Optional[RepositoryPermission]
518
465
  :ivar enterprise: Enterprise associated with the repository.
@@ -566,6 +513,7 @@ class Repository(APIObject):
566
513
  status: Optional[str] = None
567
514
  relation: Optional[str] = None
568
515
  members: Optional[List[str]] = None
516
+ parent: Optional[Dict[str, Any]] = None
569
517
  permission: Optional[RepositoryPermission] = None
570
518
  enterprise: Optional[Union[EnterpriseRef, str]] = None
571
519
  issue_template_source: Optional[str] = None
@@ -760,6 +708,31 @@ class ContentObject(APIObject):
760
708
  _links: Optional[ContentLinks] = None
761
709
 
762
710
 
711
+ @dataclass(init=False)
712
+ class RepositoryGitCodeTemplate(APIObject):
713
+ """Metadata for a ``.gitcode`` issue or pull request template file.
714
+
715
+ Returned by :meth:`~gitcode_api.resources.collaboration.IssuesResource.list_templates`
716
+ and :meth:`~gitcode_api.resources.collaboration.PullsResource.list_templates`. Use
717
+ :meth:`~gitcode_api.resources.collaboration.IssuesResource.get_template` or
718
+ :meth:`~gitcode_api.resources.collaboration.PullsResource.get_template` to load the body.
719
+
720
+ :ivar path: Repository-relative path (under ``.gitcode/``).
721
+ :vartype path: Optional[str]
722
+ :ivar sha: Object SHA for the file.
723
+ :vartype sha: Optional[str]
724
+ :ivar template_owner: Owner path of the repository the template was resolved from.
725
+ :vartype template_owner: Optional[str]
726
+ :ivar template_repo: Repository name the template was resolved from.
727
+ :vartype template_repo: Optional[str]
728
+ """
729
+
730
+ path: Optional[str] = None
731
+ sha: Optional[str] = None
732
+ template_owner: Optional[str] = None
733
+ template_repo: Optional[str] = None
734
+
735
+
763
736
  @dataclass(init=False)
764
737
  class CommitIdentity(APIObject):
765
738
  """Commit author/committer identity.
gitcode_api/llm/jiuwen.py CHANGED
@@ -9,7 +9,7 @@ from gitcode_api._base_client import DEFAULT_BASE_URL
9
9
  from ._tool import TOOL_DESCRIPTION, TOOL_NAME, TOOL_PARAMETERS, GitCodeLLMTool
10
10
 
11
11
  if TYPE_CHECKING:
12
- from openjiuwen.core.foundation.tool import LocalFunction
12
+ from openjiuwen.core.foundation.tool import LocalFunction # type: ignore[import-untyped]
13
13
 
14
14
 
15
15
  def _missing_openjiuwen_error() -> ImportError:
gitcode_api/llm/openai.py CHANGED
@@ -75,6 +75,7 @@ class GitCodeOpenAITool(GitCodeLLMTool):
75
75
  return self.tool
76
76
 
77
77
  async def __async_call__(self, *args, **kwargs) -> str:
78
+ """Invoke the configured async tool callable."""
78
79
  result = await super().__async_call__(*args, **kwargs)
79
80
  return json.dumps(result, ensure_ascii=False, indent=self.indent)
80
81
 
@@ -1,11 +1,317 @@
1
1
  """Shared resource base classes for the GitCode SDK."""
2
2
 
3
- from typing import Any, Dict, List, Optional, Union
3
+ import re
4
+ from typing import Any, Dict, List, Optional, Pattern, Tuple, Union
4
5
 
5
6
  from .._base_client import AsyncAPIClient, SyncAPIClient
6
7
  from .._base_resource import BaseResource
8
+ from .._exceptions import GitCodeHTTPStatusError
7
9
  from .._models import APIObject, ModelT, as_model, as_model_list
8
10
 
11
+ GITCODE_ISSUE_TEMPLATE_PATH_RE: Pattern[str] = re.compile(
12
+ r"\.gitcode/ISSUE_TEMPLATE.*\.(md|markdown|ya?ml)$", re.IGNORECASE
13
+ )
14
+ GITCODE_PULL_REQUEST_TEMPLATE_PATH_RE: Pattern[str] = re.compile(
15
+ r"\.gitcode/PULL_REQUEST_TEMPLATE.*\.(md|markdown|ya?ml)$", re.IGNORECASE
16
+ )
17
+
18
+
19
+ def _parse_parent_owner_repo(repo_obj: Any) -> Optional[Tuple[str, str]]:
20
+ parent = getattr(repo_obj, "parent", None)
21
+ if parent is None and hasattr(repo_obj, "get"):
22
+ parent = repo_obj.get("parent")
23
+ if not isinstance(parent, dict):
24
+ return None
25
+ full = str(parent.get("full_name") or "").strip()
26
+ if "/" not in full:
27
+ return None
28
+ owner_path, name = full.rsplit("/", 1)
29
+ owner_path, name = owner_path.strip(), name.strip()
30
+ if not owner_path or not name:
31
+ return None
32
+ return owner_path, name
33
+
34
+
35
+ def _resolution_sources_sync(client: SyncAPIClient, owner: str, repo: str) -> List[Tuple[str, str]]:
36
+ """Build ordered repo candidates.
37
+
38
+ Starts with ``(owner, repo)`` and ``(owner, ".gitcode")``. For every candidate that is a
39
+ fork (per ``GET /repos/{owner}/{repo}``), appends that repository's parent main repo and
40
+ ``(parent_owner, ".gitcode")`` when not already listed (deduplicated, breadth-first).
41
+ """
42
+ from .repositories import ReposResource
43
+
44
+ sources: List[Tuple[str, str]] = []
45
+ seen: set[Tuple[str, str]] = set()
46
+
47
+ def add(pair: Tuple[str, str]) -> None:
48
+ if pair not in seen:
49
+ seen.add(pair)
50
+ sources.append(pair)
51
+
52
+ add((owner, repo))
53
+ add((owner, ".gitcode"))
54
+
55
+ max_candidates = 64
56
+ index = 0
57
+ while index < len(sources) and len(sources) < max_candidates:
58
+ so, sr = sources[index]
59
+ index += 1
60
+ try:
61
+ meta = ReposResource(client).get(owner=so, repo=sr)
62
+ except GitCodeHTTPStatusError:
63
+ continue
64
+ if not getattr(meta, "fork", None):
65
+ continue
66
+ parsed = _parse_parent_owner_repo(meta)
67
+ if not parsed:
68
+ continue
69
+ po, pr = parsed
70
+ add((po, pr))
71
+ add((po, ".gitcode"))
72
+
73
+ return sources
74
+
75
+
76
+ async def _resolution_sources_async(client: AsyncAPIClient, owner: str, repo: str) -> List[Tuple[str, str]]:
77
+ """Async counterpart of :func:`_resolution_sources_sync`."""
78
+ from .repositories import AsyncReposResource
79
+
80
+ sources: List[Tuple[str, str]] = []
81
+ seen: set[Tuple[str, str]] = set()
82
+
83
+ def add(pair: Tuple[str, str]) -> None:
84
+ if pair not in seen:
85
+ seen.add(pair)
86
+ sources.append(pair)
87
+
88
+ add((owner, repo))
89
+ add((owner, ".gitcode"))
90
+
91
+ max_candidates = 64
92
+ index = 0
93
+ while index < len(sources) and len(sources) < max_candidates:
94
+ so, sr = sources[index]
95
+ index += 1
96
+ try:
97
+ meta = await AsyncReposResource(client).get(owner=so, repo=sr)
98
+ except GitCodeHTTPStatusError:
99
+ continue
100
+ if not getattr(meta, "fork", None):
101
+ continue
102
+ parsed = _parse_parent_owner_repo(meta)
103
+ if not parsed:
104
+ continue
105
+ po, pr = parsed
106
+ add((po, pr))
107
+ add((po, ".gitcode"))
108
+
109
+ return sources
110
+
111
+
112
+ def _walk_dot_gitcode_contents_sync(
113
+ client: SyncAPIClient,
114
+ template_owner: str,
115
+ template_repo: str,
116
+ path: str,
117
+ acc: List[Tuple[str, str, str, str]],
118
+ ) -> None:
119
+ try:
120
+ data = client.request(
121
+ "GET",
122
+ client._repo_file_path("contents", path, owner=template_owner, repo=template_repo),
123
+ )
124
+ except GitCodeHTTPStatusError as exc:
125
+ if exc.status_code == 404:
126
+ return
127
+ raise
128
+
129
+ if isinstance(data, dict):
130
+ t = (data.get("type") or "").lower()
131
+ if t == "file":
132
+ sha = data.get("sha") or ""
133
+ pth = data.get("path") or path
134
+ if sha and pth:
135
+ acc.append((template_owner, template_repo, str(pth), str(sha)))
136
+ return
137
+ if t in ("dir", "directory", "tree"):
138
+ pth = data.get("path") or path
139
+ _walk_dot_gitcode_contents_sync(client, template_owner, template_repo, pth, acc)
140
+ return
141
+ return
142
+
143
+ if isinstance(data, list):
144
+ for item in data:
145
+ if not isinstance(item, dict):
146
+ continue
147
+ t = (item.get("type") or "").lower()
148
+ pth = item.get("path") or ""
149
+ if t == "file":
150
+ sha = item.get("sha") or ""
151
+ if sha and pth:
152
+ acc.append((template_owner, template_repo, str(pth), str(sha)))
153
+ elif t in ("dir", "directory", "tree") and pth:
154
+ _walk_dot_gitcode_contents_sync(client, template_owner, template_repo, str(pth), acc)
155
+
156
+
157
+ async def _walk_dot_gitcode_contents_async(
158
+ client: AsyncAPIClient,
159
+ template_owner: str,
160
+ template_repo: str,
161
+ path: str,
162
+ acc: List[Tuple[str, str, str, str]],
163
+ ) -> None:
164
+ try:
165
+ data = await client.request(
166
+ "GET",
167
+ client._repo_file_path("contents", path, owner=template_owner, repo=template_repo),
168
+ )
169
+ except GitCodeHTTPStatusError as exc:
170
+ if exc.status_code == 404:
171
+ return
172
+ raise
173
+
174
+ if isinstance(data, dict):
175
+ t = (data.get("type") or "").lower()
176
+ if t == "file":
177
+ sha = data.get("sha") or ""
178
+ pth = data.get("path") or path
179
+ if sha and pth:
180
+ acc.append((template_owner, template_repo, str(pth), str(sha)))
181
+ return
182
+ if t in ("dir", "directory", "tree"):
183
+ pth = data.get("path") or path
184
+ await _walk_dot_gitcode_contents_async(client, template_owner, template_repo, pth, acc)
185
+ return
186
+ return
187
+
188
+ if isinstance(data, list):
189
+ for item in data:
190
+ if not isinstance(item, dict):
191
+ continue
192
+ t = (item.get("type") or "").lower()
193
+ pth = item.get("path") or ""
194
+ if t == "file":
195
+ sha = item.get("sha") or ""
196
+ if sha and pth:
197
+ acc.append((template_owner, template_repo, str(pth), str(sha)))
198
+ elif t in ("dir", "directory", "tree") and pth:
199
+ await _walk_dot_gitcode_contents_async(client, template_owner, template_repo, str(pth), acc)
200
+
201
+
202
+ def list_gitcode_template_rows_sync(
203
+ client: SyncAPIClient,
204
+ owner: str,
205
+ repo: str,
206
+ path_pattern: Pattern[str],
207
+ ) -> List[Tuple[str, str, str, str]]:
208
+ """Return ``(template_owner, template_repo, path, sha)`` from the first resolution source with matches."""
209
+ for so, sr in _resolution_sources_sync(client, owner, repo):
210
+ acc: List[Tuple[str, str, str, str]] = []
211
+ try:
212
+ _walk_dot_gitcode_contents_sync(client, so, sr, ".gitcode", acc)
213
+ except GitCodeHTTPStatusError:
214
+ continue
215
+ rows = [(t_o, t_r, p, s) for t_o, t_r, p, s in acc if path_pattern.match(p)]
216
+ if rows:
217
+ return rows
218
+ return []
219
+
220
+
221
+ async def list_gitcode_template_rows_async(
222
+ client: AsyncAPIClient,
223
+ owner: str,
224
+ repo: str,
225
+ path_pattern: Pattern[str],
226
+ ) -> List[Tuple[str, str, str, str]]:
227
+ for so, sr in await _resolution_sources_async(client, owner, repo):
228
+ acc: List[Tuple[str, str, str, str]] = []
229
+ try:
230
+ await _walk_dot_gitcode_contents_async(client, so, sr, ".gitcode", acc)
231
+ except GitCodeHTTPStatusError:
232
+ continue
233
+ rows = [(t_o, t_r, p, s) for t_o, t_r, p, s in acc if path_pattern.match(p)]
234
+ if rows:
235
+ return rows
236
+ return []
237
+
238
+
239
+ def get_gitcode_template_body_sync(
240
+ client: SyncAPIClient,
241
+ path: str,
242
+ path_pattern: Pattern[str],
243
+ owner: str,
244
+ repo: str,
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("utf-8")
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
+ ) -> str:
285
+ if not path_pattern.match(path):
286
+ raise GitCodeHTTPStatusError(
287
+ "Path does not match the expected GitCode template pattern for this resource.",
288
+ status_code=404,
289
+ payload=None,
290
+ )
291
+ last_error: Optional[GitCodeHTTPStatusError] = None
292
+ for so, sr in await _resolution_sources_async(client, owner, repo):
293
+ try:
294
+ raw = await client.request(
295
+ "GET",
296
+ client._repo_file_path("raw", path, owner=so, repo=sr),
297
+ raw=True,
298
+ )
299
+ except GitCodeHTTPStatusError as exc:
300
+ last_error = exc
301
+ continue
302
+ if isinstance(raw, bytes):
303
+ return raw.decode("utf-8")
304
+ if isinstance(raw, str):
305
+ return raw
306
+ return str(raw)
307
+ if last_error is not None:
308
+ raise last_error
309
+ raise GitCodeHTTPStatusError(
310
+ "No template found for the given path.",
311
+ status_code=404,
312
+ payload=None,
313
+ )
314
+
9
315
 
10
316
  class SyncResource(BaseResource):
11
317
  """Base class for synchronous resource groups."""
@@ -21,10 +21,20 @@ from .._models import (
21
21
  RepoMember,
22
22
  RepoMemberPermission,
23
23
  RepositoryCollaboratorCheck,
24
+ RepositoryGitCodeTemplate,
24
25
  UserSummary,
25
26
  as_model,
26
27
  )
27
- from ._shared import AsyncResource, SyncResource
28
+ from ._shared import (
29
+ GITCODE_ISSUE_TEMPLATE_PATH_RE,
30
+ GITCODE_PULL_REQUEST_TEMPLATE_PATH_RE,
31
+ AsyncResource,
32
+ SyncResource,
33
+ get_gitcode_template_body_async,
34
+ get_gitcode_template_body_sync,
35
+ list_gitcode_template_rows_async,
36
+ list_gitcode_template_rows_sync,
37
+ )
28
38
 
29
39
 
30
40
  def _comma_join(values: Optional[List[str]]) -> Optional[str]:
@@ -392,6 +402,62 @@ class IssuesResource(SyncResource):
392
402
  params={"page": page, "per_page": per_page},
393
403
  )
394
404
 
405
+ def list_templates(
406
+ self,
407
+ *,
408
+ owner: Optional[str] = None,
409
+ repo: Optional[str] = None,
410
+ ) -> List[RepositoryGitCodeTemplate]:
411
+ """List active issue templates under ``.gitcode/`` for this repository.
412
+
413
+ Matches Markdown (``.md``, ``.markdown``) and YAML (``.yml`` / ``.yaml``) paths whose
414
+ names start with ``ISSUE_TEMPLATE`` under ``.gitcode/`` (case-insensitive), including
415
+ files inside localized directories such as ``.gitcode/ISSUE_TEMPLATE.en/``.
416
+
417
+ Resolution tries the repository, then the owner's ``.gitcode`` repository, then any
418
+ parent main and ``parent/.gitcode`` pairs discovered from forks (each candidate is
419
+ inspected with ``GET /repos/{owner}/{repo}``; when ``fork`` is true, its parent's repos
420
+ are appended, deduplicated, and visited in order). The first source with matching
421
+ templates wins.
422
+
423
+ :param owner: Repository owner path. Uses the client default when omitted.
424
+ :param repo: Repository name. Uses the client default when omitted.
425
+ :returns: Template metadata entries (paths and SHAs); empty when none match.
426
+ """
427
+ resolved_owner, resolved_repo = self._client._resolve_repo_context(owner, repo)
428
+ rows = list_gitcode_template_rows_sync(
429
+ self._client, resolved_owner, resolved_repo, GITCODE_ISSUE_TEMPLATE_PATH_RE
430
+ )
431
+ return [
432
+ as_model(
433
+ {
434
+ "path": path,
435
+ "sha": sha,
436
+ "template_owner": template_owner,
437
+ "template_repo": template_repo,
438
+ },
439
+ RepositoryGitCodeTemplate,
440
+ )
441
+ for template_owner, template_repo, path, sha in rows
442
+ ]
443
+
444
+ def get_template(self, *, path: str, owner: Optional[str] = None, repo: Optional[str] = None) -> str:
445
+ """Load a single issue template file body from the default branch.
446
+
447
+ Uses the same resolution order as :meth:`list_templates`. The path must match the
448
+ active issue template naming convention (see GitCode ``.gitcode`` documentation).
449
+
450
+ :param path: Repository-relative path such as ``.gitcode/ISSUE_TEMPLATE_bug.md`` or
451
+ ``.gitcode/ISSUE_TEMPLATE/config.yml``.
452
+ :param owner: Repository owner path. Uses the client default when omitted.
453
+ :param repo: Repository name. Uses the client default when omitted.
454
+ :returns: Decoded template file contents (UTF-8).
455
+ """
456
+ resolved_owner, resolved_repo = self._client._resolve_repo_context(owner, repo)
457
+ return get_gitcode_template_body_sync(
458
+ self._client, path, GITCODE_ISSUE_TEMPLATE_PATH_RE, resolved_owner, resolved_repo
459
+ )
460
+
395
461
 
396
462
  class PullsResource(SyncResource):
397
463
  """Synchronous pull request endpoints."""
@@ -987,6 +1053,57 @@ class PullsResource(SyncResource):
987
1053
  PullRequest,
988
1054
  )
989
1055
 
1056
+ def list_templates(
1057
+ self,
1058
+ *,
1059
+ owner: Optional[str] = None,
1060
+ repo: Optional[str] = None,
1061
+ ) -> List[RepositoryGitCodeTemplate]:
1062
+ """List active pull request templates under ``.gitcode/`` for this repository.
1063
+
1064
+ Resolution tries the repository, then the owner's ``.gitcode`` repository, then any
1065
+ parent main and ``parent/.gitcode`` pairs discovered from forks (each candidate is
1066
+ inspected with ``GET /repos/{owner}/{repo}``; when ``fork`` is true, its parent's repos
1067
+ are appended, deduplicated, and visited in order). The first source with matching
1068
+ templates wins.
1069
+
1070
+ :param owner: Repository owner path. Uses the client default when omitted.
1071
+ :param repo: Repository path. Uses the client default when omitted.
1072
+ :returns: Template metadata entries (paths and SHAs); empty when none match.
1073
+ """
1074
+ resolved_owner, resolved_repo = self._client._resolve_repo_context(owner, repo)
1075
+ rows = list_gitcode_template_rows_sync(
1076
+ self._client, resolved_owner, resolved_repo, GITCODE_PULL_REQUEST_TEMPLATE_PATH_RE
1077
+ )
1078
+ return [
1079
+ as_model(
1080
+ {
1081
+ "path": path,
1082
+ "sha": sha,
1083
+ "template_owner": template_owner,
1084
+ "template_repo": template_repo,
1085
+ },
1086
+ RepositoryGitCodeTemplate,
1087
+ )
1088
+ for template_owner, template_repo, path, sha in rows
1089
+ ]
1090
+
1091
+ def get_template(self, *, path: str, owner: Optional[str] = None, repo: Optional[str] = None) -> str:
1092
+ """Load a single pull request template file body from the default branch.
1093
+
1094
+ Uses the same resolution order as :meth:`list_templates`. The path must match the
1095
+ active pull request template naming convention (see GitCode ``.gitcode`` documentation).
1096
+
1097
+ :param path: Repository-relative path such as ``.gitcode/PULL_REQUEST_TEMPLATE.md``.
1098
+ :param owner: Repository owner path. Uses the client default when omitted.
1099
+ :param repo: Repository path. Uses the client default when omitted.
1100
+ :returns: Decoded template file contents (UTF-8).
1101
+ """
1102
+ resolved_owner, resolved_repo = self._client._resolve_repo_context(owner, repo)
1103
+ return get_gitcode_template_body_sync(
1104
+ self._client, path, GITCODE_PULL_REQUEST_TEMPLATE_PATH_RE, resolved_owner, resolved_repo
1105
+ )
1106
+
990
1107
 
991
1108
  class LabelsResource(SyncResource):
992
1109
  """Synchronous label endpoints."""
@@ -1599,6 +1716,62 @@ class AsyncIssuesResource(AsyncResource):
1599
1716
  params=params,
1600
1717
  )
1601
1718
 
1719
+ async def list_templates(
1720
+ self,
1721
+ *,
1722
+ owner: Optional[str] = None,
1723
+ repo: Optional[str] = None,
1724
+ ) -> List[RepositoryGitCodeTemplate]:
1725
+ """List active issue templates under ``.gitcode/`` for this repository.
1726
+
1727
+ Matches Markdown (``.md``, ``.markdown``) and YAML (``.yml`` / ``.yaml``) paths whose
1728
+ names start with ``ISSUE_TEMPLATE`` under ``.gitcode/`` (case-insensitive), including
1729
+ files inside localized directories such as ``.gitcode/ISSUE_TEMPLATE.en/``.
1730
+
1731
+ Resolution tries the repository, then the owner's ``.gitcode`` repository, then any
1732
+ parent main and ``parent/.gitcode`` pairs discovered from forks (each candidate is
1733
+ inspected with ``GET /repos/{owner}/{repo}``; when ``fork`` is true, its parent's repos
1734
+ are appended, deduplicated, and visited in order). The first source with matching
1735
+ templates wins.
1736
+
1737
+ :param owner: Repository owner path. Uses the client default when omitted.
1738
+ :param repo: Repository name. Uses the client default when omitted.
1739
+ :returns: Template metadata entries (paths and SHAs); empty when none match.
1740
+ """
1741
+ resolved_owner, resolved_repo = self._client._resolve_repo_context(owner, repo)
1742
+ rows = await list_gitcode_template_rows_async(
1743
+ self._client, resolved_owner, resolved_repo, GITCODE_ISSUE_TEMPLATE_PATH_RE
1744
+ )
1745
+ return [
1746
+ as_model(
1747
+ {
1748
+ "path": path,
1749
+ "sha": sha,
1750
+ "template_owner": template_owner,
1751
+ "template_repo": template_repo,
1752
+ },
1753
+ RepositoryGitCodeTemplate,
1754
+ )
1755
+ for template_owner, template_repo, path, sha in rows
1756
+ ]
1757
+
1758
+ async def get_template(self, *, path: str, owner: Optional[str] = None, repo: Optional[str] = None) -> str:
1759
+ """Load a single issue template file body from the default branch.
1760
+
1761
+ Uses the same resolution order as :meth:`list_templates`. The path must match the
1762
+ active issue template naming convention (see GitCode ``.gitcode`` documentation).
1763
+
1764
+ :param path: Repository-relative path such as ``.gitcode/ISSUE_TEMPLATE_bug.md`` or
1765
+ ``.gitcode/ISSUE_TEMPLATE/config.yml``.
1766
+ :param owner: Repository owner path. Uses the client default when omitted.
1767
+ :param repo: Repository name. Uses the client default when omitted.
1768
+ :returns: Decoded template file contents (UTF-8).
1769
+ """
1770
+ resolved_owner, resolved_repo = self._client._resolve_repo_context(owner, repo)
1771
+ return await get_gitcode_template_body_async(
1772
+ self._client, path, GITCODE_ISSUE_TEMPLATE_PATH_RE, resolved_owner, resolved_repo
1773
+ )
1774
+
1602
1775
 
1603
1776
  class AsyncPullsResource(AsyncResource):
1604
1777
  """Asynchronous pull request endpoints.
@@ -2141,6 +2314,57 @@ class AsyncPullsResource(AsyncResource):
2141
2314
  "GET", self._client._path("enterprises", enterprise, "issues", number, "pull_requests"), PullRequest
2142
2315
  )
2143
2316
 
2317
+ async def list_templates(
2318
+ self,
2319
+ *,
2320
+ owner: Optional[str] = None,
2321
+ repo: Optional[str] = None,
2322
+ ) -> List[RepositoryGitCodeTemplate]:
2323
+ """List active pull request templates under ``.gitcode/`` for this repository.
2324
+
2325
+ Resolution tries the repository, then the owner's ``.gitcode`` repository, then any
2326
+ parent main and ``parent/.gitcode`` pairs discovered from forks (each candidate is
2327
+ inspected with ``GET /repos/{owner}/{repo}``; when ``fork`` is true, its parent's repos
2328
+ are appended, deduplicated, and visited in order). The first source with matching
2329
+ templates wins.
2330
+
2331
+ :param owner: Repository owner path. Uses the client default when omitted.
2332
+ :param repo: Repository path. Uses the client default when omitted.
2333
+ :returns: Template metadata entries (paths and SHAs); empty when none match.
2334
+ """
2335
+ resolved_owner, resolved_repo = self._client._resolve_repo_context(owner, repo)
2336
+ rows = await list_gitcode_template_rows_async(
2337
+ self._client, resolved_owner, resolved_repo, GITCODE_PULL_REQUEST_TEMPLATE_PATH_RE
2338
+ )
2339
+ return [
2340
+ as_model(
2341
+ {
2342
+ "path": path,
2343
+ "sha": sha,
2344
+ "template_owner": template_owner,
2345
+ "template_repo": template_repo,
2346
+ },
2347
+ RepositoryGitCodeTemplate,
2348
+ )
2349
+ for template_owner, template_repo, path, sha in rows
2350
+ ]
2351
+
2352
+ async def get_template(self, *, path: str, owner: Optional[str] = None, repo: Optional[str] = None) -> str:
2353
+ """Load a single pull request template file body from the default branch.
2354
+
2355
+ Uses the same resolution order as :meth:`list_templates`. The path must match the
2356
+ active pull request template naming convention (see GitCode ``.gitcode`` documentation).
2357
+
2358
+ :param path: Repository-relative path such as ``.gitcode/PULL_REQUEST_TEMPLATE.md``.
2359
+ :param owner: Repository owner path. Uses the client default when omitted.
2360
+ :param repo: Repository path. Uses the client default when omitted.
2361
+ :returns: Decoded template file contents (UTF-8).
2362
+ """
2363
+ resolved_owner, resolved_repo = self._client._resolve_repo_context(owner, repo)
2364
+ return await get_gitcode_template_body_async(
2365
+ self._client, path, GITCODE_PULL_REQUEST_TEMPLATE_PATH_RE, resolved_owner, resolved_repo
2366
+ )
2367
+
2144
2368
 
2145
2369
  class AsyncLabelsResource(AsyncResource):
2146
2370
  """Asynchronous label endpoints.
gitcode_api/utils.py ADDED
@@ -0,0 +1,37 @@
1
+ """Utility helper functions."""
2
+
3
+ from copy import deepcopy
4
+ from typing import Any, Dict, List, Union, overload
5
+
6
+ from ._models import APIObject
7
+
8
+
9
+ @overload
10
+ def as_dict(data: APIObject, deep_copy: bool = False) -> Dict[str, Any]: ...
11
+
12
+
13
+ @overload
14
+ def as_dict(data: List[APIObject], deep_copy: bool = False) -> List[Dict[str, Any]]: ...
15
+
16
+
17
+ def as_dict(data: Union[APIObject, List[APIObject]], deep_copy: bool = False) -> Union[Dict, List[Dict]]:
18
+ """Convert one :class:`~gitcode_api._models.APIObject` to a plain ``dict``, or several to a list of dicts.
19
+
20
+ Similar to :func:`dataclasses.asdict`. Input is converted with :meth:`~gitcode_api._models.APIObject.to_dict`.
21
+ When ``deep_copy`` is true, each mapping is copied with :func:`copy.deepcopy`.
22
+
23
+ :param data: A single response model or a list of them.
24
+ :param deep_copy: When true, return deep-copied dicts.
25
+ :returns: A dict for a single input, or a list of dicts for a list input.
26
+ """
27
+ if isinstance(data, list):
28
+ items = data
29
+ else:
30
+ items = [data]
31
+ if not all(hasattr(item, "to_dict") for item in items):
32
+ raise TypeError("Input is not APIObject or list[APIObject]")
33
+ if deep_copy:
34
+ out = [deepcopy(item.to_dict()) for item in items]
35
+ else:
36
+ out = [item.to_dict() for item in items]
37
+ return out[0] if len(out) == 1 else out
gitcode_api/version.txt CHANGED
@@ -1 +1 @@
1
- 1.2.13
1
+ 1.2.15
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitcode-api
3
- Version: 1.2.13
3
+ Version: 1.2.15
4
4
  Summary: Easy to use Python SDK for the GitCode REST API. Providing builtin CLI tool, and optional LLM integration (MCP, OpenAI tool, and openJiuwen tool) for agents. Community-maintained.
5
5
  Author-email: Hugo Huang <hugo@hugohuang.com>
6
6
  License-Expression: MIT
@@ -37,7 +37,7 @@ Dynamic: license-file
37
37
 
38
38
  # GitCode-API
39
39
 
40
- [![PyPI - Version](https://img.shields.io/pypi/v/gitcode-api?link=https%3A%2F%2Fpypi.org%2Fproject%2Fgitcode-api%2F&uuid=d15040ff268342bca07ad60f812924a7)](https://pypi.org/project/gitcode-api) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/gitcode-api?period=total&units=INTERNATIONAL_SYSTEM&left_color=GRAY&right_color=RED&left_text=downloads&uuid=b7f4a02b5f3e4776bd62a22cc539074b)](https://pepy.tech/projects/gitcode-api) [![CodeFactor](https://www.codefactor.io/repository/github/trenza1ore/gitcode-api/badge)](https://www.codefactor.io/repository/github/trenza1ore/gitcode-api)
40
+ [![PyPI - Version](https://img.shields.io/pypi/v/gitcode-api?link=https%3A%2F%2Fpypi.org%2Fproject%2Fgitcode-api%2F&uuid=1d56ca66edb448b88c30fbfd677b8542)](https://pypi.org/project/gitcode-api) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/gitcode-api?period=total&units=INTERNATIONAL_SYSTEM&left_color=GRAY&right_color=RED&left_text=downloads&uuid=03b2f61f19894ce49501bb9e856b434a)](https://pepy.tech/projects/gitcode-api) [![CodeFactor](https://www.codefactor.io/repository/github/trenza1ore/gitcode-api/badge)](https://www.codefactor.io/repository/github/trenza1ore/gitcode-api)
41
41
  [![Install in Cursor](https://img.shields.io/badge/Install_in-Cursor-000000?logoColor=white)](https://cursor.com/en/install-mcp?name=GitCode%20API&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyItLWZyb20iLCJnaXRjb2RlLWFwaVttY3BdIiwiZ2l0Y29kZS1hcGkiLCJzZXJ2ZSJdLCJlbnYiOnsiR0lUQ09ERV9BQ0NFU1NfVE9LRU4iOiIke2lucHV0OmdpdGNvZGVfYWNjZXNzX3Rva2VufSJ9LCJpbnB1dHMiOlt7ImlkIjoiZ2l0Y29kZV9hY2Nlc3NfdG9rZW4iLCJ0eXBlIjoicHJvbXB0U3RyaW5nIiwiZGVzY3JpcHRpb24iOiJFbnRlciBHSVRDT0RFX0FDQ0VTU19UT0tFTiIsInBhc3N3b3JkIjp0cnVlfV19) [![Install in VS Code](https://img.shields.io/badge/Install_in-VS_Code-0098FF?logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect/mcp/install?name=GitCode%20API&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22--from%22%2C%22gitcode-api%5Bmcp%5D%22%2C%22gitcode-api%22%2C%22serve%22%5D%2C%22env%22%3A%7B%22GITCODE_ACCESS_TOKEN%22%3A%22%24%7Binput%3Agitcode_access_token%7D%22%7D%2C%22inputs%22%3A%5B%7B%22id%22%3A%22gitcode_access_token%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22Enter%20GITCODE_ACCESS_TOKEN%22%2C%22password%22%3Atrue%7D%5D%7D)
42
42
  [![GitHub Badge](https://img.shields.io/badge/github-repo-blue?logo=github&link=https%3A%2F%2Fgithub.com%2FTrenza1ore%2FGitCode-API)](https://github.com/Trenza1ore/GitCode-API) [![GitCode Badge](https://img.shields.io/badge/gitcode-repo-brown?logo=gitcode&link=https%3A%2F%2Fgitcode.com%2FSushiNinja%2FGitCode-API)](https://gitcode.com/SushiNinja/GitCode-API)
43
43
 
@@ -49,6 +49,7 @@ Dynamic: license-file
49
49
 
50
50
  - Community project for developers who want a practical GitCode Python library.
51
51
  - Sync and async clients with a consistent API surface.
52
+ - Convenient methods not offered by REST API directly: such as fetching Issue and PR templates.
52
53
  - Resource groups such as `client.repos`, `client.pulls`, and `client.users`.
53
54
  - Repository defaults via `owner=` and `repo=` on the client.
54
55
  - Sphinx docs plus a mirrored GitCode REST API reference in `docs/`.
@@ -153,6 +154,21 @@ async def main() -> None:
153
154
  asyncio.run(main())
154
155
  ```
155
156
 
157
+ ### Convert typed return objects to dicts with `as_dict`
158
+
159
+ Response values are typed `APIObject` subclasses. For serialization or code that expects built-in `dict`, import `as_dict` from the package root: pass a single model to get a `dict`, or pass a list (for example from `client.pulls.list(...)`) to get `list[dict]`. This mirrors the idea of `dataclasses.asdict` but uses each model’s `to_dict()` method. Use `deep_copy=True` when you need detached deep copies.
160
+
161
+ ```python
162
+ from gitcode_api import GitCode, as_dict
163
+
164
+ client = GitCode(owner="SushiNinja")
165
+ pulls = client.pulls.list_templates(repo="GitCode-API")
166
+ payload = as_dict(pulls) # list[dict]
167
+
168
+ repo = client.repos.get(repo="GitCode-API")
169
+ meta = as_dict(repo) # dict
170
+ ```
171
+
156
172
  ### Context managers
157
173
 
158
174
  `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. `close()` also clears the LRU cache used by each resource group's `method_signature(...)` helper (see the [Available Resources](#available-resources) section).
@@ -354,6 +370,8 @@ Runnable examples live in `examples/`:
354
370
 
355
371
  - `get_current_user.py`
356
372
  - `get_repository_overview.py`
373
+ - `get_issue_templates.py`
374
+ - `get_pull_request_templates.py`
357
375
  - `list_pull_requests.py`
358
376
  - `async_list_branches.py`
359
377
 
@@ -362,6 +380,8 @@ Example scripts load shared configuration from `examples/.env` using `python-dot
362
380
  ```bash
363
381
  uv run python examples/get_current_user.py
364
382
  uv run python examples/get_repository_overview.py
383
+ uv run python examples/get_issue_templates.py
384
+ uv run python examples/get_pull_request_templates.py
365
385
  uv run python examples/list_pull_requests.py
366
386
  uv run python examples/async_list_branches.py
367
387
  ```
@@ -1,29 +1,30 @@
1
- gitcode_api/__init__.py,sha256=NvLp28D9brVbmv2ROSZ8mxKDuIamE9xeutBKGl9pra8,497
1
+ gitcode_api/__init__.py,sha256=vqRBOTuk4UygSh2y4wZSTiK2FNY8DSqjcSGjC3W0ifg,539
2
2
  gitcode_api/__main__.py,sha256=Yd8P4MSNcWFqUTKnUaNibbWX4Vd-MVVNmhPFaohzqcA,137
3
3
  gitcode_api/_base_client.py,sha256=PFsTMZJI4p5INjubRa4H4JcybYLGt-B9VkSAfTgpxfI,13280
4
4
  gitcode_api/_base_resource.py,sha256=mlKe1b_1AKcqxhptaCpP-AOjKkLNzCbYG-Pkp1HYWrA,2238
5
5
  gitcode_api/_cli_banner.py,sha256=3DsoJ2qZ-mWWB4yD-cnxDN_osXzrUKabrA5tbV6752M,978
6
6
  gitcode_api/_client.py,sha256=bmZxBHdfshM5Kv_EurHUVu8rsEj0k3Up3ATSIPaFrvc,8258
7
7
  gitcode_api/_exceptions.py,sha256=T5N8gBGmPSktDkLP5P_hxbzOHw3W378TzxN1xja40pA,1140
8
- gitcode_api/_models.py,sha256=7MqQJSEZiXkqcBAKdaKUQN_I61QjkHRIJYIR70tYMWU,110559
8
+ gitcode_api/_models.py,sha256=ip0xgdWao8Z3ATfSaPn3KzG81OXd25RVB1ansOaJaUM,110586
9
9
  gitcode_api/cli.py,sha256=Z-X5gK8iI2wCEXqT-igVKpVzO4ZhaRYaxmTbqMbIUx8,18735
10
10
  gitcode_api/py.typed,sha256=mDShSrm8qg9qjacQc2F-rI8ATllqP6EdgHuEYxuCXZ0,7
11
11
  gitcode_api/run_mcp.py,sha256=3_JOrjg9_yL-0M-H-F8mPgxdVKh7K2ggipu7UHeNCg0,147
12
- gitcode_api/version.txt,sha256=whaYM0seIwi4VWrB0QQCNCuNPoN_ZfocQx6yM5KCSx0,7
12
+ gitcode_api/utils.py,sha256=51QmPTQPeNsPSGf2IzhwmiEO1H2GkJrp2A7vkzhOOag,1351
13
+ gitcode_api/version.txt,sha256=CR1Vtk3iATNBumC-6iLKE5LhwljWj_KqLo-QHPlyPWM,7
13
14
  gitcode_api/llm/__init__.py,sha256=rU75ZlJvTWNVxBLc3QzdfWmSjqVc9z6hfQ8z6jVVKOk,1693
14
15
  gitcode_api/llm/_tool.py,sha256=b65iUiHo1H29uA6mFM3WlD0zZlISsENx1tpEqlkiUoA,16239
15
- gitcode_api/llm/jiuwen.py,sha256=axwAcoG86XkPkN37PTh47Cdzwh3yACUpe4bTjlaAFDc,4244
16
+ gitcode_api/llm/jiuwen.py,sha256=qca2y4544xoRYFOCMbkjiUZZLpJGMcBkK4w5bqs60-4,4276
16
17
  gitcode_api/llm/mcp.py,sha256=eeAuEETZ4THw31wbcnQaTlZQJ-9ZolvsejQY630OqHs,5702
17
- gitcode_api/llm/openai.py,sha256=r8LZi3oK9D80dfiPpv1TpP-PwKUXpAGqgNGE3hOIAaA,3200
18
+ gitcode_api/llm/openai.py,sha256=FSPA0Jv-k4n1Ud92TDfP1TWRlW4FB7smaLwY331nagk,3257
18
19
  gitcode_api/resources/__init__.py,sha256=nsCKW0bFDZ5ombJZxLThmO82sOuF7o4OKUMRkAmwbwk,1725
19
- gitcode_api/resources/_shared.py,sha256=7bCym8bIfs818SiYYrBGI7-ZtiYlxECSDG3RduInu10,5387
20
+ gitcode_api/resources/_shared.py,sha256=07f4Zk_bk6JKjEwubZ6iSd1Lq1rGi7CyWjvcMNMcYpo,15402
20
21
  gitcode_api/resources/account.py,sha256=mnc2p7wI-nBnHFNdWPNiHfmZpT6d3RDQC777gewtm4M,38801
21
- gitcode_api/resources/collaboration.py,sha256=8lyk78GTjVXddiE9fieutsMGovRjteGfTJcAhwLoR0M,101607
22
+ gitcode_api/resources/collaboration.py,sha256=TK0QXG_ymE4vbtHyP3Y-M39oG9zQE44UU5-UnPhF2WM,112388
22
23
  gitcode_api/resources/misc.py,sha256=w7bq8rmgKr2ScBKeWZ3EZJmAdylDdPtSPrhi3AQre7w,34747
23
24
  gitcode_api/resources/repositories.py,sha256=EAK2znZhEsgVUu-NDEQslSEEYJzvb-kHuh4mW57y6sc,78178
24
- gitcode_api-1.2.13.dist-info/licenses/LICENSE,sha256=gOACXuWhMu6PJKVLr9RQbxX3HULnZIGNXCaMFJIXhoA,1067
25
- gitcode_api-1.2.13.dist-info/METADATA,sha256=sqYMb5Au9bBv9TpDu8d6sK29DzIzl2XwsP3Mrllwfng,22335
26
- gitcode_api-1.2.13.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
27
- gitcode_api-1.2.13.dist-info/entry_points.txt,sha256=dIPylJcgohIE2RRIlt3In2WzcwDK8TOdkL_ReKuij4o,53
28
- gitcode_api-1.2.13.dist-info/top_level.txt,sha256=gIlg0ptyOUHJT64ajOjWIhRPYgIQnMIvnhhnesw9fxU,12
29
- gitcode_api-1.2.13.dist-info/RECORD,,
25
+ gitcode_api-1.2.15.dist-info/licenses/LICENSE,sha256=gOACXuWhMu6PJKVLr9RQbxX3HULnZIGNXCaMFJIXhoA,1067
26
+ gitcode_api-1.2.15.dist-info/METADATA,sha256=UpCCoL8teAQTmSRO5LDV7qx6dNV6EzgoIfeVnsoS81s,23323
27
+ gitcode_api-1.2.15.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
28
+ gitcode_api-1.2.15.dist-info/entry_points.txt,sha256=dIPylJcgohIE2RRIlt3In2WzcwDK8TOdkL_ReKuij4o,53
29
+ gitcode_api-1.2.15.dist-info/top_level.txt,sha256=gIlg0ptyOUHJT64ajOjWIhRPYgIQnMIvnhhnesw9fxU,12
30
+ gitcode_api-1.2.15.dist-info/RECORD,,