gitcode-api 1.1.3__py3-none-any.whl → 1.2.1__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/__main__.py CHANGED
@@ -1,3 +1,5 @@
1
+ """Run the package CLI via ``python -m gitcode_api``."""
2
+
1
3
  from .cli import main
2
4
 
3
5
  if __name__ == "__main__":
@@ -10,6 +10,7 @@ from urllib.parse import quote
10
10
 
11
11
  import httpx
12
12
 
13
+ from ._base_resource import BaseResource
13
14
  from ._exceptions import GitCodeConfigurationError, GitCodeHTTPStatusError
14
15
 
15
16
  DEFAULT_BASE_URL = "https://api.gitcode.com/api/v5"
@@ -181,6 +182,13 @@ class BaseGitCodeClient:
181
182
  except ValueError:
182
183
  return response.text
183
184
 
185
+ def _close_resources(self) -> None:
186
+ """Release resource group caches to allow for garbage collection."""
187
+ for attr in dir(self):
188
+ resource = getattr(self, attr)
189
+ if isinstance(resource, BaseResource):
190
+ resource.method_signature.cache_clear()
191
+
184
192
 
185
193
  class SyncAPIClient(BaseGitCodeClient):
186
194
  """Low-level synchronous HTTP transport used by resource classes.
@@ -207,7 +215,6 @@ class SyncAPIClient(BaseGitCodeClient):
207
215
  ) -> None:
208
216
  """Create or reuse an ``httpx.Client`` for synchronous requests."""
209
217
  super().__init__(api_key=api_key, owner=owner, repo=repo, base_url=base_url, timeout=timeout, decrypt=decrypt)
210
- self._owns_client = http_client is None
211
218
  self._client = http_client or httpx.Client(timeout=self.timeout)
212
219
 
213
220
  def request(
@@ -244,7 +251,8 @@ class SyncAPIClient(BaseGitCodeClient):
244
251
  return self._parse_response(response, raw=raw)
245
252
 
246
253
  def close(self) -> None:
247
- """Close the underlying HTTP client."""
254
+ """Close the underlying HTTP client and clear cache for garbage collection."""
255
+ self._close_resources()
248
256
  self._client.close()
249
257
 
250
258
  def __enter__(self) -> "SyncAPIClient":
@@ -281,7 +289,6 @@ class AsyncAPIClient(BaseGitCodeClient):
281
289
  ) -> None:
282
290
  """Create or reuse an ``httpx.AsyncClient`` for asynchronous requests."""
283
291
  super().__init__(api_key=api_key, owner=owner, repo=repo, base_url=base_url, timeout=timeout, decrypt=decrypt)
284
- self._owns_client = http_client is None
285
292
  self._client = http_client or httpx.AsyncClient(timeout=self.timeout)
286
293
 
287
294
  async def request(
@@ -318,7 +325,8 @@ class AsyncAPIClient(BaseGitCodeClient):
318
325
  return self._parse_response(response, raw=raw)
319
326
 
320
327
  async def close(self) -> None:
321
- """Close the underlying async HTTP client."""
328
+ """Close the underlying async HTTP client and clear cache for garbage collection."""
329
+ self._close_resources()
322
330
  await self._client.aclose()
323
331
 
324
332
  async def __aenter__(self) -> "AsyncAPIClient":
@@ -0,0 +1,60 @@
1
+ """Resource base classe for the GitCode SDK."""
2
+
3
+ from functools import cached_property, lru_cache
4
+
5
+ _UTILITY_METHODS = {"methods", "method_signature"}
6
+ _DATA_MODEL_PATH = "gitcode_api._models."
7
+
8
+
9
+ class BaseResource:
10
+ """Resource group base class."""
11
+
12
+ def __del__(self) -> None:
13
+ """Attempt to clear the lru cache."""
14
+ self.method_signature.cache_clear()
15
+
16
+ @cached_property
17
+ def methods(self) -> tuple[str, ...]:
18
+ """Public callable names on this resource group in stable SDK order.
19
+
20
+ Ordering uses :func:`sorted` with a key derived from each name's
21
+ underscore-separated segments (for example two-part names are ordered
22
+ as if ``second_first``). This is not plain lexicographic order on the
23
+ full method string. Excludes ``methods``, names starting with ``_``,
24
+ and non-callables. The result is cached on first access.
25
+
26
+ :returns: Tuple of method names in that order.
27
+ """
28
+
29
+ def _is_valid_func(name: str):
30
+ return not (name.startswith("_") or name in _UTILITY_METHODS) and callable(getattr(self, name))
31
+
32
+ def _sort_helper(method_name: str):
33
+ name_parts = method_name.split("_")
34
+ num_part = len(name_parts)
35
+ if num_part == 2:
36
+ return f"{name_parts[1]}_{name_parts[0]}"
37
+ if num_part == 3:
38
+ return f"{{{name_parts[0]}_"
39
+ return "_" + method_name
40
+
41
+ return tuple(
42
+ sorted(
43
+ (func for func in dir(self) if _is_valid_func(func)),
44
+ key=_sort_helper,
45
+ )
46
+ )
47
+
48
+ @lru_cache(maxsize=50)
49
+ def method_signature(self, method_name: str) -> str:
50
+ """Return signature for a method in this resource group, result is cached.
51
+
52
+ For example ``client.pulls.method_signature("list_issues")`` would return:
53
+ list_issues(*, number: Union[int, str], owner: Optional[str] = None, repo: Optional[str] = None) -> List[Issue]
54
+
55
+ :param method_name: Attribute name of a callable on this resource.
56
+ :returns: Formatted signature.
57
+ """
58
+ import inspect
59
+
60
+ return method_name + str(inspect.signature(getattr(self, method_name))).replace(_DATA_MODEL_PATH, "")
@@ -0,0 +1,31 @@
1
+ """ASCII art banner for command-line interface."""
2
+
3
+ from functools import lru_cache
4
+ import sys
5
+
6
+ # ANSI Colour Codes
7
+ CREDBG = "\033[41m"
8
+ CRED = "\033[91m"
9
+ CBLU = "\033[94m"
10
+ CCYN = "\033[96m"
11
+ CGRN = "\033[92m"
12
+ CYEL = "\033[41m"
13
+ CEND = "\033[0m"
14
+
15
+ # ASCII Art Banner
16
+ BANNER = r"""
17
+ _____ _ _ _____ _ _____ _____
18
+ / ____(_) | / ____| | | /\ | __ \_ _|
19
+ | | __ _| |_| | ___ __| | ___ / \ | |__) || |
20
+ | | |_ | | __| | / _ \ / _` |/ _ \ / /\ \ | ___/ | |
21
+ | |__| | | |_| |___| (_) | (_| | __/ / ____ \| | _| |_
22
+ \_____|_|\__|\_____\___/ \__,_|\___| /_/ \_\_| |_____|
23
+ """.strip("\n")
24
+
25
+
26
+ @lru_cache(maxsize=1)
27
+ def format_default_welcome(version: str) -> str:
28
+ """Colored banner and version line for the no-arguments launcher (plain if not a TTY)."""
29
+ if sys.stdout.isatty():
30
+ return f"{CRED}{BANNER}{CEND}\n{CBLU} Ver. {version}{CEND}\n\n"
31
+ return f"{BANNER}\n Ver. {version}\n\n"
gitcode_api/_client.py CHANGED
@@ -144,6 +144,7 @@ class GitCode(SyncAPIClient):
144
144
  self.oauth = OAuthResource(self)
145
145
 
146
146
  def __enter__(self) -> "GitCode":
147
+ """Enter synchronous context and return this client."""
147
148
  return self
148
149
 
149
150
 
@@ -246,4 +247,5 @@ class AsyncGitCode(AsyncAPIClient):
246
247
  self.oauth = AsyncOAuthResource(self)
247
248
 
248
249
  async def __aenter__(self) -> "AsyncGitCode":
250
+ """Enter asynchronous context and return this client."""
249
251
  return self
gitcode_api/_models.py CHANGED
@@ -73,6 +73,7 @@ class APIObject(Mapping[str, Any]):
73
73
  data: MutableMapping[str, Any] = field(init=False, repr=False)
74
74
 
75
75
  def __init__(self, data: Mapping[str, Any]):
76
+ """Initialize from API response data."""
76
77
  payload = dict(data)
77
78
  object.__setattr__(self, "data", payload)
78
79
  type_hints = get_type_hints(self.__class__)
@@ -95,24 +96,30 @@ class APIObject(Mapping[str, Any]):
95
96
  object.__setattr__(self, model_field.name, value)
96
97
 
97
98
  def __getitem__(self, key: str) -> Any:
99
+ """Return the value for ``key`` from the backing mapping."""
98
100
  return _wrap_value(self.data[key])
99
101
 
100
102
  def __iter__(self) -> Iterator[str]:
103
+ """Iterate keys of the backing mapping."""
101
104
  return iter(self.data)
102
105
 
103
106
  def __len__(self) -> int:
107
+ """Return the number of keys in the backing mapping."""
104
108
  return len(self.data)
105
109
 
106
110
  def __getattr__(self, name: str) -> Any:
111
+ """Resolve unknown attributes from the backing mapping."""
107
112
  try:
108
113
  return _wrap_value(self.data[name])
109
114
  except KeyError as exc:
110
115
  raise AttributeError(name) from exc
111
116
 
112
117
  def get(self, key: str, default: Any = None) -> Any:
118
+ """Return the value for ``key``, or ``default`` if missing."""
113
119
  return _wrap_value(self.data.get(key, default))
114
120
 
115
121
  def to_dict(self) -> Dict[str, Any]:
122
+ """Return a shallow copy of the backing mapping."""
116
123
  return dict(self.data)
117
124
 
118
125
 
gitcode_api/cli.py CHANGED
@@ -3,17 +3,39 @@
3
3
  import argparse
4
4
  import inspect
5
5
  import json
6
+ import re
6
7
  import sys
8
+ import textwrap
7
9
  from collections.abc import Mapping, Sequence
8
10
  from pathlib import Path
9
11
  from typing import Any, List, Optional, Union, get_args, get_origin
10
12
 
13
+ import httpx
14
+
11
15
  from . import GitCode, __version__
12
16
  from ._base_client import DEFAULT_BASE_URL, DEFAULT_TOKEN_ENV
17
+ from ._cli_banner import format_default_welcome
13
18
  from ._exceptions import GitCodeError
14
19
  from .resources._shared import SyncResource
15
20
 
16
21
 
22
+ class _CLIHelpFormatter(argparse.RawDescriptionHelpFormatter, argparse.ArgumentDefaultsHelpFormatter):
23
+ """Preserve paragraph breaks in descriptions; wider columns for signatures."""
24
+
25
+ def __init__(self, prog: str) -> None:
26
+ super().__init__(prog, width=108, max_help_position=32)
27
+
28
+
29
+ def _plain_cli_inline(text: str) -> str:
30
+ """Strip Sphinx-style inline markup (roles, doubled literals) for terminal text."""
31
+ if not text:
32
+ return ""
33
+ t = re.sub(r"``\s*([^`]+?)\s*``", r"\1", text)
34
+ t = re.sub(r":\w+:`([^`]*)`", r"\1", t)
35
+ t = re.sub(r"`([^`]+)`", r"\1", t)
36
+ return t
37
+
38
+
17
39
  def _unwrap_optional(annotation: Any) -> Any:
18
40
  origin = get_origin(annotation)
19
41
  if origin is Union:
@@ -51,8 +73,17 @@ def _argument_kwargs(parameter: inspect.Parameter) -> dict[str, Any]:
51
73
 
52
74
 
53
75
  def _first_doc_line(obj: Any) -> str:
54
- doc = inspect.getdoc(obj) or ""
55
- return doc.splitlines()[0] if doc else ""
76
+ doc = inspect.getdoc(obj).removeprefix("Synchronous ").capitalize() or ""
77
+ for line in doc.splitlines():
78
+ stripped = line.strip()
79
+ if stripped:
80
+ return _plain_cli_inline(stripped)
81
+ return ""
82
+
83
+
84
+ def _method_cli_summary(doc: str) -> str:
85
+ """Text before the first ':param' in the docstring, collapsed to one line for CLI help."""
86
+ return doc.split(":param", 1)[0].strip().replace("\n", " ")
56
87
 
57
88
 
58
89
  def _resource_types() -> dict[str, type[SyncResource]]:
@@ -63,13 +94,25 @@ def _resource_types() -> dict[str, type[SyncResource]]:
63
94
  return resources
64
95
 
65
96
 
66
- def _iter_resource_methods(resource_type: type[SyncResource]) -> list[tuple[str, Any]]:
67
- methods: list[tuple[str, Any]] = []
68
- for name, value in resource_type.__dict__.items():
69
- if name.startswith("_") or not inspect.isfunction(value):
70
- continue
71
- methods.append((name, value))
72
- return methods
97
+ def _probe_gitcode() -> GitCode:
98
+ """Lightweight client used only while building -h/--help metadata (no network)."""
99
+ transport = httpx.MockTransport(lambda _request: httpx.Response(200, json={}))
100
+ return GitCode(api_key="__cli_help__", http_client=httpx.Client(transport=transport))
101
+
102
+
103
+ def _resource_commands_epilog(resource: SyncResource) -> str:
104
+ """List CLI subcommand names in the same order as resource.methods on the client."""
105
+ kebab_names = [_kebab_case(name) for name in resource.methods]
106
+ joined = ", ".join(kebab_names)
107
+ wrapped = textwrap.fill(
108
+ joined,
109
+ width=120,
110
+ initial_indent=" ",
111
+ subsequent_indent=" ",
112
+ break_long_words=False,
113
+ break_on_hyphens=False,
114
+ )
115
+ return "Subcommands (stable SDK order, same as resource.methods):\n" + wrapped
73
116
 
74
117
 
75
118
  def _kebab_case(value: str) -> str:
@@ -130,76 +173,135 @@ def _write_output(value: Any, *, output_file: Optional[str], compact: bool) -> N
130
173
  print(text)
131
174
 
132
175
 
133
- def _global_parent_parser() -> argparse.ArgumentParser:
176
+ def _invocation_parent_parser() -> argparse.ArgumentParser:
177
+ """Parser with flags for real API calls (attached only to leaf METHOD parsers, not top-level usage)."""
134
178
  parser = argparse.ArgumentParser(add_help=False)
135
179
  parser.add_argument("--api-key", help=f"GitCode access token. Defaults to {DEFAULT_TOKEN_ENV}.")
136
180
  parser.add_argument("--owner", help="Default repository owner.")
137
181
  parser.add_argument("--repo", help="Default repository name.")
138
- parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="Base URL for the GitCode REST API.")
182
+ parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="Base URL for the REST API.")
139
183
  parser.add_argument("--timeout", type=float, default=None, help="Request timeout in seconds.")
140
184
  parser.add_argument("--output-file", help="Write the response to a file instead of stdout.")
141
185
  parser.add_argument("--compact", action="store_true", help="Print JSON without indentation.")
142
186
  return parser
143
187
 
144
188
 
189
+ def _root_banner() -> str:
190
+ return "Connection and defaults are documented on each method's help: %(prog)s RESOURCE METHOD -h."
191
+
192
+
145
193
  def build_parser() -> argparse.ArgumentParser:
146
- common = _global_parent_parser()
194
+ """Build main parser."""
195
+ common = _invocation_parent_parser()
196
+ epilog = """\
197
+ Examples:
198
+ %(prog)s pulls list --api-key "$GITCODE_ACCESS_TOKEN" --owner my-org --repo my-repo
199
+ %(prog)s users me --api-key "$GITCODE_ACCESS_TOKEN"
200
+ %(prog)s pulls list --set only_count=true --set reviewer=demo
201
+
202
+ Extra keyword arguments (**params / **payload on some methods):
203
+ Repeat --set key=value (value is JSON if it parses as JSON; otherwise a string).
204
+ --set-json merges one JSON object (or @path/to/file.json) into those kwargs.
205
+
206
+ Each resource -h lists subcommands in SDK order (resource.methods).
207
+ Each method -h opens with resource.method_signature("<name>") from the Python SDK.
208
+ """
147
209
  parser = argparse.ArgumentParser(
148
210
  prog="gitcode-api",
149
- description="Invoke any synchronous gitcode-api resource method from the command line.",
150
- epilog='Use `--set key=value` and `--set-json \'{"key": "value"}\'` for methods with `**params` or `**payload`.',
151
- formatter_class=argparse.ArgumentDefaultsHelpFormatter,
152
- parents=[common],
211
+ usage="%(prog)s [-h] [--version] RESOURCE ...",
212
+ description=_root_banner(),
213
+ epilog=epilog,
214
+ formatter_class=_CLIHelpFormatter,
153
215
  )
154
216
  parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
155
217
 
156
- resource_parsers = parser.add_subparsers(dest="resource", required=True)
157
- for resource_name, resource_type in _resource_types().items():
158
- resource_parser = resource_parsers.add_parser(
159
- _kebab_case(resource_name),
160
- help=_first_doc_line(resource_type),
218
+ client = _probe_gitcode()
219
+ try:
220
+ resource_parsers = parser.add_subparsers(
221
+ dest="resource",
222
+ required=True,
223
+ metavar="RESOURCE",
224
+ title="resources",
225
+ description="Pick a resource group (same attribute names as on GitCode, e.g. pulls, repos).",
161
226
  )
162
- method_parsers = resource_parser.add_subparsers(dest="method", required=True)
163
-
164
- for method_name, method in _iter_resource_methods(resource_type):
165
- method_parser = method_parsers.add_parser(
166
- _kebab_case(method_name),
167
- help=_first_doc_line(method),
168
- description=inspect.getdoc(method),
169
- formatter_class=argparse.ArgumentDefaultsHelpFormatter,
170
- parents=[common],
227
+ for resource_name, resource_type in _resource_types().items():
228
+ resource = getattr(client, resource_name)
229
+ class_doc = inspect.getdoc(resource_type).removeprefix("Synchronous ").capitalize() or ""
230
+ class_lead = class_doc.split("\n\n", maxsplit=1)[0].strip() if class_doc else ""
231
+ class_lead = _plain_cli_inline(class_lead) if class_lead else ""
232
+ resource_desc_parts = [
233
+ class_lead or _first_doc_line(resource_type),
234
+ "",
235
+ _resource_commands_epilog(resource),
236
+ ]
237
+ resource_parser = resource_parsers.add_parser(
238
+ _kebab_case(resource_name),
239
+ help=_first_doc_line(resource_type) or f"GitCode {resource_name} endpoints.",
240
+ description="\n".join(resource_desc_parts),
241
+ formatter_class=_CLIHelpFormatter,
171
242
  )
172
- signature = inspect.signature(method)
173
- for parameter in signature.parameters.values():
174
- if parameter.name == "self":
175
- continue
176
- if parameter.kind == inspect.Parameter.VAR_KEYWORD:
177
- method_parser.add_argument(
178
- "--set",
179
- dest="extra_items",
180
- action="append",
181
- default=None,
182
- metavar="KEY=VALUE",
183
- help="Additional keyword arguments for `**params` or `**payload`.",
184
- )
185
- method_parser.add_argument(
186
- "--set-json",
187
- dest="extra_json",
188
- default=None,
189
- metavar="JSON_OR_@FILE",
190
- help="JSON object merged into extra keyword arguments.",
191
- )
192
- continue
193
-
194
- flag = f"--{parameter.name.replace('_', '-')}"
195
- if flag in method_parser._option_string_actions:
196
- continue
197
- kwargs = _argument_kwargs(parameter)
198
- kwargs["dest"] = parameter.name
199
- kwargs["required"] = parameter.default is inspect.Signature.empty
200
- method_parser.add_argument(flag, **kwargs)
201
-
202
- method_parser.set_defaults(resource_name=resource_name, method_name=method_name)
243
+ method_parsers = resource_parser.add_subparsers(
244
+ dest="method",
245
+ required=True,
246
+ title="methods",
247
+ metavar="METHOD",
248
+ description=(
249
+ "Method names use kebab-case. The opening line of each command's help matches "
250
+ "resource.method_signature('<name>') on the Python client."
251
+ ),
252
+ )
253
+
254
+ for method_name in resource.methods:
255
+ method = getattr(resource, method_name)
256
+ sig_line = resource.method_signature(method_name)
257
+ doc = inspect.getdoc(method).removeprefix("Synchronous ").capitalize() or ""
258
+ summary = _method_cli_summary(doc)
259
+ if summary and len(summary) > 90:
260
+ method_help = summary[:87] + "..."
261
+ else:
262
+ method_help = summary or (sig_line if len(sig_line) <= 90 else sig_line[:87] + "...")
263
+ method_description = f"{summary}\n\n{sig_line}" if summary else sig_line
264
+
265
+ method_parser = method_parsers.add_parser(
266
+ _kebab_case(method_name),
267
+ help=method_help,
268
+ description=method_description,
269
+ formatter_class=_CLIHelpFormatter,
270
+ parents=[common],
271
+ )
272
+ signature = inspect.signature(method)
273
+ for parameter in signature.parameters.values():
274
+ if parameter.name == "self":
275
+ continue
276
+ if parameter.kind == inspect.Parameter.VAR_KEYWORD:
277
+ method_parser.add_argument(
278
+ "--set",
279
+ dest="extra_items",
280
+ action="append",
281
+ default=None,
282
+ metavar="KEY=VALUE",
283
+ help="Extra keyword argument (merged into **params / **payload).",
284
+ )
285
+ method_parser.add_argument(
286
+ "--set-json",
287
+ dest="extra_json",
288
+ default=None,
289
+ metavar="JSON_OR_@FILE",
290
+ help="JSON object merged into extra keyword arguments.",
291
+ )
292
+ continue
293
+
294
+ flag = f"--{parameter.name.replace('_', '-')}"
295
+ if flag in method_parser._option_string_actions:
296
+ continue
297
+ kwargs = _argument_kwargs(parameter)
298
+ kwargs["dest"] = parameter.name
299
+ kwargs["required"] = parameter.default is inspect.Signature.empty
300
+ method_parser.add_argument(flag, **kwargs)
301
+
302
+ method_parser.set_defaults(resource_name=resource_name, method_name=method_name)
303
+ finally:
304
+ client.close()
203
305
 
204
306
  return parser
205
307
 
@@ -233,8 +335,19 @@ def _collect_kwargs(args: argparse.Namespace, method: Any) -> dict[str, Any]:
233
335
 
234
336
 
235
337
  def main(argv: Optional[Sequence[str]] = None) -> int:
338
+ """CLI entry point."""
236
339
  parser = build_parser()
237
- args = parser.parse_args(argv)
340
+ effective = list(sys.argv[1:] if argv is None else argv)
341
+ if not effective:
342
+ print(format_default_welcome(__version__), end="")
343
+ saved_epilog = parser.epilog
344
+ parser.epilog = None
345
+ try:
346
+ parser.print_help()
347
+ finally:
348
+ parser.epilog = saved_epilog
349
+ return 0
350
+ args = parser.parse_args(effective)
238
351
 
239
352
  try:
240
353
  with GitCode(
@@ -3,10 +3,11 @@
3
3
  from typing import Any, Dict, List, Optional, Union
4
4
 
5
5
  from .._base_client import AsyncAPIClient, SyncAPIClient
6
+ from .._base_resource import BaseResource
6
7
  from .._models import APIObject, ModelT, as_model, as_model_list
7
8
 
8
9
 
9
- class SyncResource:
10
+ class SyncResource(BaseResource):
10
11
  """Base class for synchronous resource groups."""
11
12
 
12
13
  def __init__(self, client: SyncAPIClient) -> None:
@@ -74,7 +75,7 @@ class SyncResource:
74
75
  return APIObject({"value": data})
75
76
 
76
77
 
77
- class AsyncResource:
78
+ class AsyncResource(BaseResource):
78
79
  """Base class for asynchronous resource groups."""
79
80
 
80
81
  def __init__(self, client: AsyncAPIClient) -> None:
@@ -476,7 +476,7 @@ class PullsResource(SyncResource):
476
476
  """Create a pull request.
477
477
 
478
478
  :param title: Pull request title.
479
- :param head: Source branch ref (head branch).
479
+ :param head: Source branch ref (head branch, in forks use ``owner:branch`` like "SushiNinja:develop").
480
480
  :param base: Target branch ref (base branch).
481
481
  :param owner: Repository owner path. Uses the client default when omitted.
482
482
  :param repo: Repository path. Uses the client default when omitted.
@@ -490,7 +490,7 @@ class PullsResource(SyncResource):
490
490
  :param prune_source_branch: Whether to delete the source branch after merge when applicable.
491
491
  :param squash: Squash-merge preference where supported.
492
492
  :param squash_commit_message: Custom squash commit message.
493
- :param fork_path: Fork namespace/path when opening a PR from a fork.
493
+ :param fork_path: Fork ``owner/repo`` when opening a PR from a fork, like "SushiNinja/agent-core-contrib".
494
494
  :returns: Created pull request.
495
495
  """
496
496
  return self._model(
@@ -1662,7 +1662,7 @@ class AsyncPullsResource(AsyncResource):
1662
1662
  """Create a pull request.
1663
1663
 
1664
1664
  :param title: Pull request title.
1665
- :param head: Source branch ref (head branch).
1665
+ :param head: Source branch ref (head branch, in forks use ``owner:branch`` like "SushiNinja:develop").
1666
1666
  :param base: Target branch ref (base branch).
1667
1667
  :param owner: Repository owner path. Uses the client default when omitted.
1668
1668
  :param repo: Repository path. Uses the client default when omitted.
@@ -1676,7 +1676,7 @@ class AsyncPullsResource(AsyncResource):
1676
1676
  :param prune_source_branch: Whether to delete the source branch after merge when applicable.
1677
1677
  :param squash: Squash-merge preference where supported.
1678
1678
  :param squash_commit_message: Custom squash commit message.
1679
- :param fork_path: Fork namespace/path when opening a PR from a fork.
1679
+ :param fork_path: Fork ``owner/repo`` when opening a PR from a fork, like "SushiNinja/agent-core-contrib".
1680
1680
  :returns: Created pull request.
1681
1681
  """
1682
1682
  return await self._model(
gitcode_api/version.txt CHANGED
@@ -1 +1 @@
1
- 1.1.3
1
+ 1.2.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitcode-api
3
- Version: 1.1.3
3
+ Version: 1.2.1
4
4
  Summary: Easy to use Python SDK for the GitCode REST API, community-maintained.
5
5
  Author-email: Hugo Huang <hugo@hugohuang.com>
6
6
  License-Expression: MIT
@@ -29,7 +29,7 @@ Dynamic: license-file
29
29
 
30
30
  [![PyPI - Version](https://img.shields.io/pypi/v/gitcode-api?link=https%3A%2F%2Fpypi.org%2Fproject%2Fgitcode-api%2F)](https://pypi.org/project/gitcode-api) [![GitHub Badge](https://img.shields.io/badge/github-repo-blue?logo=github&link=https%3A%2F%2Fgithub.com%2FTrenza1ore%2FGitCode-API)](https://github.com/Trenza1ore/GitCode-API) [![GitCode Badge](https://img.shields.io/badge/gitcode-repo-brown?logo=gitcode&link=https%3A%2F%2Fgitcode.com%2FSushiNinja%2FGitCode-API)](https://gitcode.com/SushiNinja/GitCode-API) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/gitcode-api?period=total&units=INTERNATIONAL_SYSTEM&left_color=GRAY&right_color=RED&left_text=downloads)](https://pepy.tech/projects/gitcode-api)
31
31
 
32
- [![Docs](https://img.shields.io/badge/%E6%96%87%E6%A1%A3-Docs-cyan?style=for-the-badge&logo=readthedocs&link=https%3A%2F%2Fgitcode-api.readthedocs.io%2Fen%2Flatest%2Findex.html)](https://gitcode-api.readthedocs.io) [![中文README](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-README-brown?style=for-the-badge&logo=googledocs&link=README.zh.md)](README.zh.md) [![English README](https://img.shields.io/badge/English-README-blue?style=for-the-badge&logo=googledocs&link=README.md)](README.md)
32
+ [![Docs](https://img.shields.io/badge/%E6%96%87%E6%A1%A3-Docs-cyan?style=for-the-badge&logo=readthedocs&link=https%3A%2F%2Fgitcode-api.readthedocs.io%2Fen%2Flatest%2Findex.html)](https://gitcode-api.readthedocs.io) [![中文README](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-README-brown?style=for-the-badge&logo=googledocs&link=README.zh.md)](README.zh.md)
33
33
 
34
34
  `gitcode-api` is a community-maintained Python SDK for the GitCode REST API. It provides easy-to-use synchronous and asynchronous clients, repository-scoped helpers, and lightweight response models so you can work with GitCode from Python without hand-writing raw HTTP requests.
35
35
 
@@ -37,7 +37,7 @@ Dynamic: license-file
37
37
 
38
38
  - Community project for developers who want a practical GitCode Python library.
39
39
  - Sync and async clients with a consistent API surface.
40
- - Resource namespaces such as `client.repos`, `client.pulls`, and `client.users`.
40
+ - Resource groups such as `client.repos`, `client.pulls`, and `client.users`.
41
41
  - Repository defaults via `owner=` and `repo=` on the client.
42
42
  - Sphinx docs plus a mirrored GitCode REST API reference in `docs/`.
43
43
 
@@ -120,7 +120,7 @@ asyncio.run(main())
120
120
 
121
121
  ### Context managers
122
122
 
123
- `GitCode` and `AsyncGitCode` (and the lower-level `SyncAPIClient` / `AsyncAPIClient`) support `with` / `async with`. Leaving the block calls `close()` / `await close()` on the underlying client automatically, including a custom `http_client=` you passed in.
123
+ `GitCode` and `AsyncGitCode` (and the lower-level `SyncAPIClient` / `AsyncAPIClient`) support `with` / `async with`. Leaving the block calls `close()` / `await close()` on the underlying client automatically, including a custom `http_client=` you passed in. `close()` also clears the LRU cache used by each resource group's `method_signature(...)` helper (see the [Available Resources](#available-resources) section).
124
124
 
125
125
  ```python
126
126
  from gitcode_api import GitCode
@@ -151,16 +151,13 @@ from gitcode_api import GitCode
151
151
 
152
152
  client = GitCode(owner="SushiNinja", repo="GitCode-API")
153
153
 
154
- try:
155
- pull = client.pulls.create(
156
- title="Add feature",
157
- head="feature-branch",
158
- base="main",
159
- body="Implements the new flow.",
160
- )
161
- print(pull.number)
162
- finally:
163
- client.close()
154
+ pull = client.pulls.create(
155
+ title="Add feature",
156
+ head="feature-branch",
157
+ base="main",
158
+ body="Implements the new flow.",
159
+ )
160
+ print(pull.number)
164
161
  ```
165
162
 
166
163
  Get the authenticated user:
@@ -170,11 +167,8 @@ from gitcode_api import GitCode
170
167
 
171
168
  client = GitCode()
172
169
 
173
- try:
174
- user = client.users.me()
175
- print(user.login)
176
- finally:
177
- client.close()
170
+ user = client.users.me()
171
+ print(user.login)
178
172
  ```
179
173
 
180
174
  Search repositories:
@@ -184,12 +178,9 @@ from gitcode_api import GitCode
184
178
 
185
179
  client = GitCode()
186
180
 
187
- try:
188
- repos = client.search.repositories(q="sdk language:python", per_page=10)
189
- for repo in repos:
190
- print(repo.full_name)
191
- finally:
192
- client.close()
181
+ repos = client.search.repositories(q="sdk language:python", per_page=10)
182
+ for repo in repos:
183
+ print(repo.full_name)
193
184
  ```
194
185
 
195
186
  ## Available Resources
@@ -203,6 +194,8 @@ Both `GitCode` and `AsyncGitCode` expose:
203
194
  - `releases`, `tags`, and `webhooks`
204
195
  - `users`, `orgs`, `search`, and `oauth`
205
196
 
197
+ Every resource group inherits a cached `methods` property from the shared resource base: a `tuple` of public callable names in stable SDK order (underscore-segment sort key, not plain A–Z on the full identifier). Private names and the introspection helpers `methods` and `method_signature` are omitted. For example, `client.pulls.methods` helps with discovery or tooling without reading the full manual list. For one method’s parameters and return type, call `client.pulls.method_signature("list_issues")` (a cached string from `inspect.signature`, with `gitcode_api._models.` stripped from annotations).
198
+
206
199
  ## Examples
207
200
 
208
201
  Runnable examples live in `examples/`:
@@ -0,0 +1,22 @@
1
+ gitcode_api/__init__.py,sha256=NvLp28D9brVbmv2ROSZ8mxKDuIamE9xeutBKGl9pra8,497
2
+ gitcode_api/__main__.py,sha256=Yd8P4MSNcWFqUTKnUaNibbWX4Vd-MVVNmhPFaohzqcA,137
3
+ gitcode_api/_base_client.py,sha256=PFsTMZJI4p5INjubRa4H4JcybYLGt-B9VkSAfTgpxfI,13280
4
+ gitcode_api/_base_resource.py,sha256=mlKe1b_1AKcqxhptaCpP-AOjKkLNzCbYG-Pkp1HYWrA,2238
5
+ gitcode_api/_cli_banner.py,sha256=res6C4V_rVSLyHMr1PwCpjV2OLn05lWJkRfxai3SCb8,978
6
+ gitcode_api/_client.py,sha256=bmZxBHdfshM5Kv_EurHUVu8rsEj0k3Up3ATSIPaFrvc,8258
7
+ gitcode_api/_exceptions.py,sha256=T5N8gBGmPSktDkLP5P_hxbzOHw3W378TzxN1xja40pA,1140
8
+ gitcode_api/_models.py,sha256=v-GZzCGAb3_frY6wFiQww9m271U5MigivpEHDHnoDcI,109030
9
+ gitcode_api/cli.py,sha256=GyqHg6cn_AqzJis7WshgdQaQv7w2imNWWoBjxiLVpzA,14194
10
+ gitcode_api/version.txt,sha256=bPTghLR_M8mwLveSedFXgzho-PcFFBaadovjU-4yj-o,6
11
+ gitcode_api/resources/__init__.py,sha256=nsCKW0bFDZ5ombJZxLThmO82sOuF7o4OKUMRkAmwbwk,1725
12
+ gitcode_api/resources/_shared.py,sha256=7bCym8bIfs818SiYYrBGI7-ZtiYlxECSDG3RduInu10,5387
13
+ gitcode_api/resources/account.py,sha256=mnc2p7wI-nBnHFNdWPNiHfmZpT6d3RDQC777gewtm4M,38801
14
+ gitcode_api/resources/collaboration.py,sha256=8lyk78GTjVXddiE9fieutsMGovRjteGfTJcAhwLoR0M,101607
15
+ gitcode_api/resources/misc.py,sha256=guDwh4cxbTVsSa7EivaYM3bKMJ8_Op4KucGbKEoayKE,22412
16
+ gitcode_api/resources/repositories.py,sha256=EAK2znZhEsgVUu-NDEQslSEEYJzvb-kHuh4mW57y6sc,78178
17
+ gitcode_api-1.2.1.dist-info/licenses/LICENSE,sha256=gOACXuWhMu6PJKVLr9RQbxX3HULnZIGNXCaMFJIXhoA,1067
18
+ gitcode_api-1.2.1.dist-info/METADATA,sha256=kcLmsYVqck8eQzFVfttnwc-QrV2sC4N6Soul-XJzuP8,8202
19
+ gitcode_api-1.2.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
20
+ gitcode_api-1.2.1.dist-info/entry_points.txt,sha256=dIPylJcgohIE2RRIlt3In2WzcwDK8TOdkL_ReKuij4o,53
21
+ gitcode_api-1.2.1.dist-info/top_level.txt,sha256=gIlg0ptyOUHJT64ajOjWIhRPYgIQnMIvnhhnesw9fxU,12
22
+ gitcode_api-1.2.1.dist-info/RECORD,,
@@ -1,20 +0,0 @@
1
- gitcode_api/__init__.py,sha256=NvLp28D9brVbmv2ROSZ8mxKDuIamE9xeutBKGl9pra8,497
2
- gitcode_api/__main__.py,sha256=MHKZ_ae3fSLGTLUUMOx15fWdeOnJSHhq-zslRP5F5Lc,79
3
- gitcode_api/_base_client.py,sha256=1COCZXzSiCe_LNawsOsTNvh0WMh1JW4A5yYgX56HwjY,12894
4
- gitcode_api/_client.py,sha256=AOpp1P7vdSlMcn0XP3JMPnAKbQMftMO9WMubBGw2LZc,8129
5
- gitcode_api/_exceptions.py,sha256=T5N8gBGmPSktDkLP5P_hxbzOHw3W378TzxN1xja40pA,1140
6
- gitcode_api/_models.py,sha256=2bR6Wxc3Ie7iM3UFNY0ThWeNjxFHzDAC-8QFgcgcT_0,108599
7
- gitcode_api/cli.py,sha256=R4HUPzrrs_ejHjIAEZw0P5ozUuEfFScJvfVRyrR2HXw,9573
8
- gitcode_api/version.txt,sha256=QVh-oXE9O_Eto7b8aIGNtIm6f6enRYGYzqERAJEy8sk,5
9
- gitcode_api/resources/__init__.py,sha256=nsCKW0bFDZ5ombJZxLThmO82sOuF7o4OKUMRkAmwbwk,1725
10
- gitcode_api/resources/_shared.py,sha256=EB9fr4eQUc9IOdZCn4AoI946SJYCNVAtOOHEo_OzJAQ,5317
11
- gitcode_api/resources/account.py,sha256=mnc2p7wI-nBnHFNdWPNiHfmZpT6d3RDQC777gewtm4M,38801
12
- gitcode_api/resources/collaboration.py,sha256=26rQCeayZfUQ_6dIdl5HOGXnYkMLx8V7XsF_Wi-4Qt4,101417
13
- gitcode_api/resources/misc.py,sha256=guDwh4cxbTVsSa7EivaYM3bKMJ8_Op4KucGbKEoayKE,22412
14
- gitcode_api/resources/repositories.py,sha256=EAK2znZhEsgVUu-NDEQslSEEYJzvb-kHuh4mW57y6sc,78178
15
- gitcode_api-1.1.3.dist-info/licenses/LICENSE,sha256=gOACXuWhMu6PJKVLr9RQbxX3HULnZIGNXCaMFJIXhoA,1067
16
- gitcode_api-1.1.3.dist-info/METADATA,sha256=Cts4NepBSIfeffRrEec2u7jeHcrqmTCzWLIh-LE08mg,7715
17
- gitcode_api-1.1.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
18
- gitcode_api-1.1.3.dist-info/entry_points.txt,sha256=dIPylJcgohIE2RRIlt3In2WzcwDK8TOdkL_ReKuij4o,53
19
- gitcode_api-1.1.3.dist-info/top_level.txt,sha256=gIlg0ptyOUHJT64ajOjWIhRPYgIQnMIvnhhnesw9fxU,12
20
- gitcode_api-1.1.3.dist-info/RECORD,,