gitcode-api 1.2.11__tar.gz → 1.2.13__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/PKG-INFO +6 -4
  2. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/README.md +4 -2
  3. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/_models.py +43 -0
  4. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/cli.py +72 -7
  5. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/resources/misc.py +342 -19
  6. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/run_mcp.py +1 -1
  7. gitcode_api-1.2.13/gitcode_api/version.txt +1 -0
  8. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api.egg-info/PKG-INFO +6 -4
  9. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/pyproject.toml +12 -4
  10. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/tests/test_cli.py +2 -0
  11. gitcode_api-1.2.13/tests/test_resources_misc.py +280 -0
  12. gitcode_api-1.2.11/gitcode_api/version.txt +0 -1
  13. gitcode_api-1.2.11/tests/test_resources_misc.py +0 -38
  14. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/LICENSE +0 -0
  15. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/__init__.py +0 -0
  16. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/__main__.py +0 -0
  17. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/_base_client.py +0 -0
  18. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/_base_resource.py +0 -0
  19. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/_cli_banner.py +0 -0
  20. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/_client.py +0 -0
  21. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/_exceptions.py +0 -0
  22. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/llm/__init__.py +0 -0
  23. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/llm/_tool.py +0 -0
  24. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/llm/jiuwen.py +0 -0
  25. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/llm/mcp.py +0 -0
  26. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/llm/openai.py +0 -0
  27. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/py.typed +0 -0
  28. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/resources/__init__.py +0 -0
  29. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/resources/_shared.py +0 -0
  30. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/resources/account.py +0 -0
  31. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/resources/collaboration.py +0 -0
  32. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api/resources/repositories.py +0 -0
  33. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api.egg-info/SOURCES.txt +0 -0
  34. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api.egg-info/dependency_links.txt +0 -0
  35. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api.egg-info/entry_points.txt +0 -0
  36. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api.egg-info/requires.txt +0 -0
  37. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/gitcode_api.egg-info/top_level.txt +0 -0
  38. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/setup.cfg +0 -0
  39. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/tests/test_base_client.py +0 -0
  40. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/tests/test_build_manifest.py +0 -0
  41. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/tests/test_client.py +0 -0
  42. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/tests/test_llm_tools.py +0 -0
  43. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/tests/test_models.py +0 -0
  44. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/tests/test_resources_account.py +0 -0
  45. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/tests/test_resources_collaboration.py +0 -0
  46. {gitcode_api-1.2.11 → gitcode_api-1.2.13}/tests/test_resources_repositories.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitcode-api
3
- Version: 1.2.11
3
+ Version: 1.2.13
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
@@ -9,7 +9,7 @@ Project-URL: issues, https://github.com/Trenza1ore/GitCode-API/issues
9
9
  Project-URL: documentation, https://gitcode-api.readthedocs.io
10
10
  Project-URL: gitcode, https://gitcode.com/SushiNinja/GitCode-API
11
11
  Project-URL: github, https://github.com/Trenza1ore/GitCode-API
12
- Project-URL: homepage, https://linktr.ee/Hugoooo
12
+ Project-URL: homepage, https://hugohuang.com/gitcode-api
13
13
  Project-URL: author, https://hugohuang.com
14
14
  Keywords: gitcode,git,devops,api,sdk,python,httpx,client,mcp,agent,fastmcp,llm,openjiuwen,mcp client,mcp server,model context protocol
15
15
  Classifier: Development Status :: 4 - Beta
@@ -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)](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)](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=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)
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
 
@@ -111,12 +111,14 @@ With `gitcode-api[mcp]` installed (Python 3.10+), you can start the bundled Fast
111
111
  gitcode-api serve --api-key "$GITCODE_ACCESS_TOKEN"
112
112
  ```
113
113
 
114
- Use `gitcode-api serve -h` for defaults such as `--owner`, `--repo`, and `--transport`.
114
+ Use `gitcode-api serve -h` for defaults such as `--owner`, `--repo`, `--transport` (`stdio`, `http`, or `sse`), and other options.
115
115
 
116
116
  Commands mirror the synchronous resource methods on `GitCode`, using the pattern
117
117
  `gitcode-api <resource> <method> ...`. For methods that accept extra `**params`
118
118
  or `**payload`, pass repeated `--set key=value` flags or `--set-json '{"key": "value"}'`.
119
119
 
120
+ When using string arguments with escape sequence (such as line break `\n`), pass `-e` / `--escape` with the sequences to un-escape, such as `-e '\n\t'`.
121
+
120
122
  ## Quick Start
121
123
 
122
124
  ### Sync client
@@ -1,6 +1,6 @@
1
1
  # GitCode-API
2
2
 
3
- [![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) [![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) [![CodeFactor](https://www.codefactor.io/repository/github/trenza1ore/gitcode-api/badge)](https://www.codefactor.io/repository/github/trenza1ore/gitcode-api)
3
+ [![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)
4
4
  [![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)
5
5
  [![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)
6
6
 
@@ -74,12 +74,14 @@ With `gitcode-api[mcp]` installed (Python 3.10+), you can start the bundled Fast
74
74
  gitcode-api serve --api-key "$GITCODE_ACCESS_TOKEN"
75
75
  ```
76
76
 
77
- Use `gitcode-api serve -h` for defaults such as `--owner`, `--repo`, and `--transport`.
77
+ Use `gitcode-api serve -h` for defaults such as `--owner`, `--repo`, `--transport` (`stdio`, `http`, or `sse`), and other options.
78
78
 
79
79
  Commands mirror the synchronous resource methods on `GitCode`, using the pattern
80
80
  `gitcode-api <resource> <method> ...`. For methods that accept extra `**params`
81
81
  or `**payload`, pass repeated `--set key=value` flags or `--set-json '{"key": "value"}'`.
82
82
 
83
+ When using string arguments with escape sequence (such as line break `\n`), pass `-e` / `--escape` with the sequences to un-escape, such as `-e '\n\t'`.
84
+
83
85
  ## Quick Start
84
86
 
85
87
  ### Sync client
@@ -2171,6 +2171,23 @@ class RepoMemberPermission(APIObject):
2171
2171
  permission: Optional[str] = None
2172
2172
 
2173
2173
 
2174
+ @dataclass(init=False)
2175
+ class ReleaseAsset(APIObject):
2176
+ """Asset metadata embedded in release payloads.
2177
+
2178
+ :ivar browser_download_url: Browser download URL for this asset.
2179
+ :vartype browser_download_url: Optional[str]
2180
+ :ivar name: Asset file name.
2181
+ :vartype name: Optional[str]
2182
+ :ivar type: Asset media type.
2183
+ :vartype type: Optional[str]
2184
+ """
2185
+
2186
+ browser_download_url: Optional[str] = None
2187
+ name: Optional[str] = None
2188
+ type: Optional[str] = None
2189
+
2190
+
2174
2191
  @dataclass(init=False)
2175
2192
  class Release(APIObject):
2176
2193
  """Release payload.
@@ -2179,6 +2196,10 @@ class Release(APIObject):
2179
2196
  :vartype id: Optional[Union[int, str]]
2180
2197
  :ivar tag_name: Tag name.
2181
2198
  :vartype tag_name: Optional[str]
2199
+ :ivar target_commitish: Branch or commit SHA targeted by the release tag.
2200
+ :vartype target_commitish: Optional[str]
2201
+ :ivar prerelease: Whether this release is a pre-release.
2202
+ :vartype prerelease: Optional[bool]
2182
2203
  :ivar name: Display name.
2183
2204
  :vartype name: Optional[str]
2184
2205
  :ivar body: Body text of the object.
@@ -2191,16 +2212,38 @@ class Release(APIObject):
2191
2212
  :vartype published_at: Optional[str]
2192
2213
  :ivar html_url: Web URL for this object.
2193
2214
  :vartype html_url: Optional[str]
2215
+ :ivar assets: Release source packages and uploaded attachments.
2216
+ :vartype assets: Optional[List[ReleaseAsset]]
2217
+ :ivar release_status: Release status, for example ``pre`` or ``latest``.
2218
+ :vartype release_status: Optional[str]
2194
2219
  """
2195
2220
 
2196
2221
  id: Optional[Union[int, str]] = None
2197
2222
  tag_name: Optional[str] = None
2223
+ target_commitish: Optional[str] = None
2224
+ prerelease: Optional[bool] = None
2198
2225
  name: Optional[str] = None
2199
2226
  body: Optional[str] = None
2200
2227
  author: Optional[UserRef] = None
2201
2228
  created_at: Optional[str] = None
2202
2229
  published_at: Optional[str] = None
2203
2230
  html_url: Optional[str] = None
2231
+ assets: Optional[List[ReleaseAsset]] = None
2232
+ release_status: Optional[str] = None
2233
+
2234
+
2235
+ @dataclass(init=False)
2236
+ class ReleaseUploadURL(APIObject):
2237
+ """Pre-signed release attachment upload target.
2238
+
2239
+ :ivar url: Pre-signed object storage URL for uploading with ``PUT``.
2240
+ :vartype url: Optional[str]
2241
+ :ivar headers: Headers to send unchanged with the upload request.
2242
+ :vartype headers: Optional[Dict[str, str]]
2243
+ """
2244
+
2245
+ url: Optional[str] = None
2246
+ headers: Optional[Dict[str, str]] = None
2204
2247
 
2205
2248
 
2206
2249
  @dataclass(init=False)
@@ -1,6 +1,7 @@
1
1
  """Command-line interface for the GitCode SDK."""
2
2
 
3
3
  import argparse
4
+ import codecs
4
5
  import inspect
5
6
  import json
6
7
  import re
@@ -8,7 +9,7 @@ import sys
8
9
  import textwrap
9
10
  from collections.abc import Mapping, Sequence
10
11
  from pathlib import Path
11
- from typing import Any, List, Optional, Union, get_args, get_origin
12
+ from typing import Any, Dict, List, Optional, Union, get_args, get_origin
12
13
 
13
14
  import httpx
14
15
 
@@ -36,6 +37,40 @@ def _plain_cli_inline(text: str) -> str:
36
37
  return t
37
38
 
38
39
 
40
+ _ESCAPE_PATTERN = re.compile(r"\\\w+")
41
+
42
+
43
+ def _unescape_input(value: str, replacements: Dict[str, str]) -> str:
44
+ """Unescape CLI input with provided escape sequences."""
45
+ for token, decoded in replacements.items():
46
+ value = value.replace(token, decoded)
47
+ return value
48
+
49
+
50
+ def _unescape_namespace(args: argparse.Namespace) -> None:
51
+ mapping = ((token, codecs.decode(token, "unicode-escape")) for token in _ESCAPE_PATTERN.findall(str(args.escape)))
52
+ replacements = {token: decoded for token, decoded in mapping if token and (token != decoded)}
53
+ if not replacements:
54
+ raise argparse.ArgumentTypeError("No valid escape sequence provided!")
55
+ for key, value in vars(args).items():
56
+ if key in {"escape", "base_url", "resource", "method", "api_key"}:
57
+ continue
58
+ if isinstance(value, str):
59
+ setattr(args, key, _unescape_input(value, replacements=replacements))
60
+ continue
61
+ if isinstance(value, list):
62
+ updated = []
63
+ changed = False
64
+ for item in value:
65
+ if isinstance(item, str):
66
+ updated.append(_unescape_input(item, replacements=replacements))
67
+ changed = True
68
+ else:
69
+ updated.append(item)
70
+ if changed:
71
+ setattr(args, key, updated)
72
+
73
+
39
74
  def _unwrap_optional(annotation: Any) -> Any:
40
75
  origin = get_origin(annotation)
41
76
  if origin is Union:
@@ -179,12 +214,17 @@ def _invocation_parent_parser() -> argparse.ArgumentParser:
179
214
  """Parser with flags for real API calls (attached only to leaf METHOD parsers, not top-level usage)."""
180
215
  parser = argparse.ArgumentParser(add_help=False)
181
216
  parser.add_argument("--api-key", help=f"GitCode access token. Defaults to {DEFAULT_TOKEN_ENV}.")
182
- parser.add_argument("--owner", help="Default repository owner.")
183
- parser.add_argument("--repo", help="Default repository name.")
184
217
  parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="Base URL for the REST API.")
185
218
  parser.add_argument("--timeout", type=float, default=None, help="Request timeout in seconds.")
186
219
  parser.add_argument("--output-file", help="Write the response to a file instead of stdout.")
187
220
  parser.add_argument("--compact", action="store_true", help="Print JSON without indentation.")
221
+ parser.add_argument(
222
+ "-e",
223
+ "--escape",
224
+ default="",
225
+ metavar="SEQUENCES",
226
+ help='Unescape certain escape sequences (e.g. -e "\\n\\t") in arguments.',
227
+ )
188
228
  return parser
189
229
 
190
230
 
@@ -192,6 +232,15 @@ def _root_banner() -> str:
192
232
  return "Connection and defaults are documented on each method's help: %(prog)s RESOURCE METHOD -h."
193
233
 
194
234
 
235
+ def _parse_true_false(raw: str) -> bool:
236
+ value = raw.strip().lower()
237
+ if value == "true":
238
+ return True
239
+ if value == "false":
240
+ return False
241
+ raise argparse.ArgumentTypeError("Expected 'true' or 'false'.")
242
+
243
+
195
244
  def _serve_parser() -> argparse.ArgumentParser:
196
245
  """Parser with options for starting the bundled MCP server."""
197
246
  parser = argparse.ArgumentParser(add_help=False)
@@ -201,10 +250,25 @@ def _serve_parser() -> argparse.ArgumentParser:
201
250
  parser.add_argument("--repo", help="Default repository name.")
202
251
  parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="Base URL for the REST API.")
203
252
  parser.add_argument("--timeout", type=float, default=None, help="Request timeout in seconds.")
204
- parser.add_argument("--transport", default="stdio", help="FastMCP transport to run, such as stdio or http.")
253
+ parser.add_argument(
254
+ "--transport",
255
+ default="stdio",
256
+ choices=("stdio", "http", "sse"),
257
+ metavar="{stdio,http,sse}",
258
+ help="FastMCP transport to run, such as stdio or http.",
259
+ )
205
260
  parser.add_argument("--host", default=None, help="Host for HTTP-based transports.")
206
261
  parser.add_argument("--port", type=int, default=None, help="Port for HTTP-based transports.")
207
262
  parser.add_argument("--path", default=None, help="Path for HTTP-based transports.")
263
+ parser.add_argument(
264
+ "-b",
265
+ "--show-banner",
266
+ type=_parse_true_false,
267
+ choices=(True, False),
268
+ default=None,
269
+ metavar="{true,false}",
270
+ help="Show the FastMCP startup banner.",
271
+ )
208
272
  return parser
209
273
 
210
274
 
@@ -356,7 +420,7 @@ def _run_mcp_server(args: argparse.Namespace) -> int:
356
420
  if value is not None:
357
421
  run_kwargs[key] = value
358
422
 
359
- server.run(**run_kwargs)
423
+ server.run(show_banner=args.show_banner, **run_kwargs)
360
424
  return 0
361
425
 
362
426
 
@@ -402,6 +466,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
402
466
  parser.epilog = saved_epilog
403
467
  return 0
404
468
  args = parser.parse_args(effective)
469
+ escape_seq = getattr(args, "escape", None)
470
+ if escape_seq and isinstance(escape_seq, str):
471
+ _unescape_namespace(args)
405
472
 
406
473
  try:
407
474
  if getattr(args, "command", None) == "serve":
@@ -409,8 +476,6 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
409
476
 
410
477
  with GitCode(
411
478
  api_key=args.api_key,
412
- owner=args.owner,
413
- repo=args.repo,
414
479
  base_url=args.base_url,
415
480
  timeout=args.timeout,
416
481
  ) as client:
@@ -1,39 +1,128 @@
1
1
  """Release, tag, and webhook resource groups."""
2
2
 
3
+ from pathlib import Path
3
4
  from typing import List, Optional, Union
4
5
 
5
- from .._models import ProtectedTag, Release, Tag, Webhook
6
+ from .._models import ProtectedTag, Release, ReleaseUploadURL, Tag, Webhook
6
7
  from ._shared import AsyncResource, SyncResource
7
8
 
8
9
 
9
10
  class ReleasesResource(SyncResource):
10
11
  """Synchronous release endpoints."""
11
12
 
13
+ def create(
14
+ self,
15
+ *,
16
+ tag: str,
17
+ name: str,
18
+ body: str,
19
+ owner: Optional[str] = None,
20
+ repo: Optional[str] = None,
21
+ target_commitish: Optional[str] = None,
22
+ release_status: Optional[str] = None,
23
+ ) -> Release:
24
+ """Create a repository release.
25
+
26
+ :param tag: Tag name for the release.
27
+ :param name: Release title.
28
+ :param body: Release description.
29
+ :param owner: Repository owner path. Uses the client default when omitted.
30
+ :param repo: Repository path. Uses the client default when omitted.
31
+ :param target_commitish: Branch name or commit SHA for creating a missing tag.
32
+ :param release_status: Release status, such as ``pre`` or ``latest``.
33
+ :returns: Created release payload.
34
+ """
35
+ return self._model(
36
+ "POST",
37
+ self._client._repo_path("releases", owner=owner, repo=repo),
38
+ Release,
39
+ json={
40
+ "tag_name": tag,
41
+ "name": name,
42
+ "body": body,
43
+ "target_commitish": target_commitish,
44
+ "release_status": release_status,
45
+ },
46
+ )
47
+
12
48
  def update(
13
49
  self,
14
50
  *,
15
- release_id: Union[int, str],
16
- tag_name: str,
51
+ tag: str,
17
52
  name: str,
18
53
  body: str,
19
54
  owner: Optional[str] = None,
20
55
  repo: Optional[str] = None,
56
+ release_status: Optional[str] = None,
21
57
  ) -> Release:
22
58
  """Update a repository release.
23
59
 
24
- :param release_id: Release identifier.
25
- :param tag_name: Tag name for the release.
60
+ :param tag: Tag name in the release URL path.
26
61
  :param name: Release name.
27
62
  :param body: Release description.
28
63
  :param owner: Repository owner path. Uses the client default when omitted.
29
- :param repo: Repository name. Uses the client default when omitted.
64
+ :param repo: Repository path. Uses the client default when omitted.
65
+ :param release_status: Release status, such as ``pre`` or ``latest``.
30
66
  :returns: Updated release payload.
31
67
  """
32
68
  return self._model(
33
69
  "PATCH",
34
- self._client._repo_path("releases", release_id, owner=owner, repo=repo),
70
+ self._client._repo_path("releases", tag, owner=owner, repo=repo),
35
71
  Release,
36
- json={"tag_name": tag_name, "name": name, "body": body},
72
+ json={"name": name, "body": body, "release_status": release_status},
73
+ )
74
+
75
+ def get_upload_url(
76
+ self, *, tag: str, file_name: str, owner: Optional[str] = None, repo: Optional[str] = None
77
+ ) -> ReleaseUploadURL:
78
+ """Get a pre-signed URL for uploading a release attachment.
79
+
80
+ :param tag: Tag name in the release URL path.
81
+ :param file_name: Attachment file name to upload.
82
+ :param owner: Repository owner path. Uses the client default when omitted.
83
+ :param repo: Repository path. Uses the client default when omitted.
84
+ :returns: Upload URL and required headers.
85
+ """
86
+ return self._model(
87
+ "GET",
88
+ self._client._repo_path("releases", tag, "upload_url", owner=owner, repo=repo),
89
+ ReleaseUploadURL,
90
+ params={"file_name": file_name},
91
+ )
92
+
93
+ def upload(
94
+ self,
95
+ *,
96
+ tag: str,
97
+ file_name: str,
98
+ content: Union[bytes, str],
99
+ owner: Optional[str] = None,
100
+ repo: Optional[str] = None,
101
+ upload_timeout: Optional[float] = 300.0,
102
+ ) -> None:
103
+ """Upload a release attachment through the pre-signed upload URL.
104
+
105
+ :param tag: Tag name in the release URL path.
106
+ :param file_name: Attachment file name to upload.
107
+ :param content: Attachment bytes, or a local file path to read as bytes.
108
+ :param owner: Repository owner path. Uses the client default when omitted.
109
+ :param repo: Repository path. Uses the client default when omitted.
110
+ :param upload_timeout: Timeout for upload operation. Default to 300 (5 minutes).
111
+ """
112
+ upload_content = Path(content).read_bytes() if isinstance(content, str) else content
113
+ if not isinstance(upload_content, bytes):
114
+ raise TypeError("content must be bytes or a file path string.")
115
+
116
+ upload_url = self.get_upload_url(tag=tag, file_name=file_name, owner=owner, repo=repo)
117
+ if not upload_url.url:
118
+ raise ValueError("Release upload URL response did not include a URL.")
119
+
120
+ self._client._client.request(
121
+ "PUT",
122
+ upload_url.url,
123
+ content=upload_content,
124
+ headers=upload_url.headers or {},
125
+ timeout=upload_timeout,
37
126
  )
38
127
 
39
128
  def get_by_tag(self, *, tag: str, owner: Optional[str] = None, repo: Optional[str] = None) -> Release:
@@ -50,14 +139,87 @@ class ReleasesResource(SyncResource):
50
139
  Release,
51
140
  )
52
141
 
53
- def list(self, *, owner: Optional[str] = None, repo: Optional[str] = None) -> List[Release]:
142
+ def list(
143
+ self,
144
+ *,
145
+ owner: Optional[str] = None,
146
+ repo: Optional[str] = None,
147
+ direction: Optional[str] = None,
148
+ page: Optional[int] = None,
149
+ per_page: Optional[int] = None,
150
+ ) -> List[Release]:
54
151
  """List releases for a repository.
55
152
 
56
153
  :param owner: Repository owner path. Uses the client default when omitted.
57
154
  :param repo: Repository path. Uses the client default when omitted.
155
+ :param direction: Sort direction, for example ``asc`` or ``desc``.
156
+ :param page: Page number.
157
+ :param per_page: Page size, up to 100.
58
158
  :returns: Releases ordered as returned by the API.
59
159
  """
60
- return self._models("GET", self._client._repo_path("releases", owner=owner, repo=repo), Release)
160
+ return self._models(
161
+ "GET",
162
+ self._client._repo_path("releases", owner=owner, repo=repo),
163
+ Release,
164
+ params={"direction": direction, "page": page, "per_page": per_page},
165
+ )
166
+
167
+ def get_latest(
168
+ self, *, owner: Optional[str] = None, repo: Optional[str] = None, type: Optional[str] = None
169
+ ) -> Release:
170
+ """Get the latest repository release.
171
+
172
+ :param owner: Repository owner path. Uses the client default when omitted.
173
+ :param repo: Repository path. Uses the client default when omitted.
174
+ :param type: Selection type, either ``updated`` or ``latest``.
175
+ :returns: Latest release metadata.
176
+ """
177
+ return self._model(
178
+ "GET",
179
+ self._client._repo_path("releases", "latest", owner=owner, repo=repo),
180
+ Release,
181
+ params={"type": type},
182
+ )
183
+
184
+ def get(
185
+ self,
186
+ *,
187
+ tag: str,
188
+ owner: Optional[str] = None,
189
+ repo: Optional[str] = None,
190
+ temp_download_url: Optional[Union[bool, str]] = None,
191
+ ) -> Release:
192
+ """Get a repository release by tag path.
193
+
194
+ :param tag: Tag name in the release URL path.
195
+ :param owner: Repository owner path. Uses the client default when omitted.
196
+ :param repo: Repository path. Uses the client default when omitted.
197
+ :param temp_download_url: Whether to return temporary source package and attachment URLs.
198
+ :returns: Release metadata.
199
+ """
200
+ return self._model(
201
+ "GET",
202
+ self._client._repo_path("releases", tag, owner=owner, repo=repo),
203
+ Release,
204
+ params={"temp_download_url": temp_download_url},
205
+ )
206
+
207
+ def download_attachment(
208
+ self, *, tag: str, file_name: str, owner: Optional[str] = None, repo: Optional[str] = None
209
+ ) -> bytes:
210
+ """Download a release attachment as bytes.
211
+
212
+ :param tag: Tag name in the release URL path.
213
+ :param file_name: Attachment file name.
214
+ :param owner: Repository owner path. Uses the client default when omitted.
215
+ :param repo: Repository path. Uses the client default when omitted.
216
+ :returns: Attachment bytes.
217
+ """
218
+ return self._request(
219
+ "GET",
220
+ self._client._repo_path("releases", tag, "attach_files", file_name, "download", owner=owner, repo=repo),
221
+ raw=True,
222
+ )
61
223
 
62
224
 
63
225
  class TagsResource(SyncResource):
@@ -288,31 +450,119 @@ class AsyncReleasesResource(AsyncResource):
288
450
  Mirrors :class:`ReleasesResource`; see that class for parameters (Release API in ``docs/rest_api``).
289
451
  """
290
452
 
453
+ async def create(
454
+ self,
455
+ *,
456
+ tag: str,
457
+ name: str,
458
+ body: str,
459
+ owner: Optional[str] = None,
460
+ repo: Optional[str] = None,
461
+ target_commitish: Optional[str] = None,
462
+ release_status: Optional[str] = None,
463
+ ) -> Release:
464
+ """Create a repository release.
465
+
466
+ :param tag: Tag name for the release.
467
+ :param name: Release title.
468
+ :param body: Release description.
469
+ :param owner: Repository owner path. Uses the client default when omitted.
470
+ :param repo: Repository path. Uses the client default when omitted.
471
+ :param target_commitish: Branch name or commit SHA for creating a missing tag.
472
+ :param release_status: Release status, such as ``pre`` or ``latest``.
473
+ :returns: Created release payload.
474
+ """
475
+ return await self._model(
476
+ "POST",
477
+ self._client._repo_path("releases", owner=owner, repo=repo),
478
+ Release,
479
+ json={
480
+ "tag_name": tag,
481
+ "name": name,
482
+ "body": body,
483
+ "target_commitish": target_commitish,
484
+ "release_status": release_status,
485
+ },
486
+ )
487
+
291
488
  async def update(
292
489
  self,
293
490
  *,
294
- release_id: Union[int, str],
295
- tag_name: str,
491
+ tag: str,
296
492
  name: str,
297
493
  body: str,
298
494
  owner: Optional[str] = None,
299
495
  repo: Optional[str] = None,
496
+ release_status: Optional[str] = None,
300
497
  ) -> Release:
301
498
  """Update a repository release.
302
499
 
303
- :param release_id: Release identifier.
304
- :param tag_name: Tag name for the release.
500
+ :param tag: Tag name in the release URL path.
305
501
  :param name: Release name.
306
502
  :param body: Release description.
307
503
  :param owner: Repository owner path. Uses the client default when omitted.
308
- :param repo: Repository name. Uses the client default when omitted.
504
+ :param repo: Repository path. Uses the client default when omitted.
505
+ :param release_status: Release status, such as ``pre`` or ``latest``.
309
506
  :returns: Updated release payload.
310
507
  """
311
508
  return await self._model(
312
509
  "PATCH",
313
- self._client._repo_path("releases", release_id, owner=owner, repo=repo),
510
+ self._client._repo_path("releases", tag, owner=owner, repo=repo),
314
511
  Release,
315
- json={"tag_name": tag_name, "name": name, "body": body},
512
+ json={"name": name, "body": body, "release_status": release_status},
513
+ )
514
+
515
+ async def get_upload_url(
516
+ self, *, tag: str, file_name: str, owner: Optional[str] = None, repo: Optional[str] = None
517
+ ) -> ReleaseUploadURL:
518
+ """Get a pre-signed URL for uploading a release attachment.
519
+
520
+ :param tag: Tag name in the release URL path.
521
+ :param file_name: Attachment file name to upload.
522
+ :param owner: Repository owner path. Uses the client default when omitted.
523
+ :param repo: Repository path. Uses the client default when omitted.
524
+ :returns: Upload URL and required headers.
525
+ """
526
+ return await self._model(
527
+ "GET",
528
+ self._client._repo_path("releases", tag, "upload_url", owner=owner, repo=repo),
529
+ ReleaseUploadURL,
530
+ params={"file_name": file_name},
531
+ )
532
+
533
+ async def upload(
534
+ self,
535
+ *,
536
+ tag: str,
537
+ file_name: str,
538
+ content: Union[bytes, str],
539
+ owner: Optional[str] = None,
540
+ repo: Optional[str] = None,
541
+ upload_timeout: Optional[float] = 300.0,
542
+ ) -> None:
543
+ """Upload a release attachment through the pre-signed upload URL.
544
+
545
+ :param tag: Tag name in the release URL path.
546
+ :param file_name: Attachment file name to upload.
547
+ :param content: Attachment bytes, or a local file path to read as bytes.
548
+ :param owner: Repository owner path. Uses the client default when omitted.
549
+ :param repo: Repository path. Uses the client default when omitted.
550
+ :param upload_timeout: Timeout for upload operation. Default to 300 (5 minutes).
551
+ """
552
+ upload_content = Path(content).read_bytes() if isinstance(content, str) else content
553
+ if not isinstance(upload_content, bytes):
554
+ raise TypeError("content must be bytes or a file path string.")
555
+
556
+ upload_url = await self.get_upload_url(tag=tag, file_name=file_name, owner=owner, repo=repo)
557
+ if not upload_url.url:
558
+ raise ValueError("Release upload URL response did not include a URL.")
559
+
560
+ await self._client._client.request(
561
+ "PUT",
562
+ upload_url.url,
563
+ content=upload_content,
564
+ headers=upload_url.headers or {},
565
+ timeout=upload_timeout,
316
566
  )
317
567
 
318
568
  async def get_by_tag(self, *, tag: str, owner: Optional[str] = None, repo: Optional[str] = None) -> Release:
@@ -327,14 +577,87 @@ class AsyncReleasesResource(AsyncResource):
327
577
  "GET", self._client._repo_path("releases", "tags", tag, owner=owner, repo=repo), Release
328
578
  )
329
579
 
330
- async def list(self, *, owner: Optional[str] = None, repo: Optional[str] = None) -> List[Release]:
580
+ async def list(
581
+ self,
582
+ *,
583
+ owner: Optional[str] = None,
584
+ repo: Optional[str] = None,
585
+ direction: Optional[str] = None,
586
+ page: Optional[int] = None,
587
+ per_page: Optional[int] = None,
588
+ ) -> List[Release]:
331
589
  """List releases for a repository.
332
590
 
333
591
  :param owner: Repository owner path. Uses the client default when omitted.
334
592
  :param repo: Repository path. Uses the client default when omitted.
593
+ :param direction: Sort direction, for example ``asc`` or ``desc``.
594
+ :param page: Page number.
595
+ :param per_page: Page size, up to 100.
335
596
  :returns: Releases ordered as returned by the API.
336
597
  """
337
- return await self._models("GET", self._client._repo_path("releases", owner=owner, repo=repo), Release)
598
+ return await self._models(
599
+ "GET",
600
+ self._client._repo_path("releases", owner=owner, repo=repo),
601
+ Release,
602
+ params={"direction": direction, "page": page, "per_page": per_page},
603
+ )
604
+
605
+ async def get_latest(
606
+ self, *, owner: Optional[str] = None, repo: Optional[str] = None, type: Optional[str] = None
607
+ ) -> Release:
608
+ """Get the latest repository release.
609
+
610
+ :param owner: Repository owner path. Uses the client default when omitted.
611
+ :param repo: Repository path. Uses the client default when omitted.
612
+ :param type: Selection type, either ``updated`` or ``latest``.
613
+ :returns: Latest release metadata.
614
+ """
615
+ return await self._model(
616
+ "GET",
617
+ self._client._repo_path("releases", "latest", owner=owner, repo=repo),
618
+ Release,
619
+ params={"type": type},
620
+ )
621
+
622
+ async def get(
623
+ self,
624
+ *,
625
+ tag: str,
626
+ owner: Optional[str] = None,
627
+ repo: Optional[str] = None,
628
+ temp_download_url: Optional[Union[bool, str]] = None,
629
+ ) -> Release:
630
+ """Get a repository release by tag path.
631
+
632
+ :param tag: Tag name in the release URL path.
633
+ :param owner: Repository owner path. Uses the client default when omitted.
634
+ :param repo: Repository path. Uses the client default when omitted.
635
+ :param temp_download_url: Whether to return temporary source package and attachment URLs.
636
+ :returns: Release metadata.
637
+ """
638
+ return await self._model(
639
+ "GET",
640
+ self._client._repo_path("releases", tag, owner=owner, repo=repo),
641
+ Release,
642
+ params={"temp_download_url": temp_download_url},
643
+ )
644
+
645
+ async def download_attachment(
646
+ self, *, tag: str, file_name: str, owner: Optional[str] = None, repo: Optional[str] = None
647
+ ) -> bytes:
648
+ """Download a release attachment as bytes.
649
+
650
+ :param tag: Tag name in the release URL path.
651
+ :param file_name: Attachment file name.
652
+ :param owner: Repository owner path. Uses the client default when omitted.
653
+ :param repo: Repository path. Uses the client default when omitted.
654
+ :returns: Attachment bytes.
655
+ """
656
+ return await self._request(
657
+ "GET",
658
+ self._client._repo_path("releases", tag, "attach_files", file_name, "download", owner=owner, repo=repo),
659
+ raw=True,
660
+ )
338
661
 
339
662
 
340
663
  class AsyncTagsResource(AsyncResource):
@@ -3,4 +3,4 @@
3
3
  from gitcode_api.llm.mcp import GitCodeMCP
4
4
 
5
5
  if __name__ == "__main__":
6
- GitCodeMCP().mcp.run()
6
+ GitCodeMCP().mcp.run(show_banner=False)
@@ -0,0 +1 @@
1
+ 1.2.13
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitcode-api
3
- Version: 1.2.11
3
+ Version: 1.2.13
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
@@ -9,7 +9,7 @@ Project-URL: issues, https://github.com/Trenza1ore/GitCode-API/issues
9
9
  Project-URL: documentation, https://gitcode-api.readthedocs.io
10
10
  Project-URL: gitcode, https://gitcode.com/SushiNinja/GitCode-API
11
11
  Project-URL: github, https://github.com/Trenza1ore/GitCode-API
12
- Project-URL: homepage, https://linktr.ee/Hugoooo
12
+ Project-URL: homepage, https://hugohuang.com/gitcode-api
13
13
  Project-URL: author, https://hugohuang.com
14
14
  Keywords: gitcode,git,devops,api,sdk,python,httpx,client,mcp,agent,fastmcp,llm,openjiuwen,mcp client,mcp server,model context protocol
15
15
  Classifier: Development Status :: 4 - Beta
@@ -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)](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)](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=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)
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
 
@@ -111,12 +111,14 @@ With `gitcode-api[mcp]` installed (Python 3.10+), you can start the bundled Fast
111
111
  gitcode-api serve --api-key "$GITCODE_ACCESS_TOKEN"
112
112
  ```
113
113
 
114
- Use `gitcode-api serve -h` for defaults such as `--owner`, `--repo`, and `--transport`.
114
+ Use `gitcode-api serve -h` for defaults such as `--owner`, `--repo`, `--transport` (`stdio`, `http`, or `sse`), and other options.
115
115
 
116
116
  Commands mirror the synchronous resource methods on `GitCode`, using the pattern
117
117
  `gitcode-api <resource> <method> ...`. For methods that accept extra `**params`
118
118
  or `**payload`, pass repeated `--set key=value` flags or `--set-json '{"key": "value"}'`.
119
119
 
120
+ When using string arguments with escape sequence (such as line break `\n`), pass `-e` / `--escape` with the sequences to un-escape, such as `-e '\n\t'`.
121
+
120
122
  ## Quick Start
121
123
 
122
124
  ### Sync client
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gitcode-api"
3
- version = "1.2.11"
3
+ version = "1.2.13"
4
4
  description = "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
  keywords = [
6
6
  "gitcode", "git", "devops", "api", "sdk", "python", "httpx", "client",
@@ -64,11 +64,19 @@ binary = [
64
64
  "pyinstaller>=6.0,<7",
65
65
  ]
66
66
 
67
+ # FastMCP
67
68
  mcp = [
68
69
  "fastmcp; python_version >= '3.10'",
69
70
  ]
70
71
 
71
- dev = [{ include-group = "test" }, { include-group = "format" }, { include-group = "docs" }]
72
+ dev = [
73
+ { include-group = "test" },
74
+ { include-group = "format" },
75
+ { include-group = "docs" },
76
+ "githubkit>=0.15.4",
77
+ "openai",
78
+ "setuptools",
79
+ ]
72
80
 
73
81
  [project.urls]
74
82
  changelog = "https://gitcode-api.readthedocs.io/en/latest/changelog.html"
@@ -76,14 +84,14 @@ issues = "https://github.com/Trenza1ore/GitCode-API/issues"
76
84
  documentation = "https://gitcode-api.readthedocs.io"
77
85
  gitcode = "https://gitcode.com/SushiNinja/GitCode-API"
78
86
  github = "https://github.com/Trenza1ore/GitCode-API"
79
- homepage = "https://linktr.ee/Hugoooo"
87
+ homepage = "https://hugohuang.com/gitcode-api"
80
88
  author = "https://hugohuang.com"
81
89
 
82
90
  [project.scripts]
83
91
  gitcode-api = "gitcode_api.cli:main"
84
92
 
85
93
  [tool.uv]
86
- default-groups = ['test']
94
+ default-groups = ['dev']
87
95
 
88
96
  [tool.setuptools.package-data]
89
97
  gitcode_api = ["**/version.txt"]
@@ -161,12 +161,14 @@ def test_run_mcp_server_passes_fastmcp_options(monkeypatch: Any) -> None:
161
161
  host="127.0.0.1",
162
162
  port=8000,
163
163
  path="/mcp",
164
+ show_banner=None,
164
165
  )
165
166
 
166
167
  assert _run_mcp_server(args) == 0
167
168
  assert captured["server_kwargs"]["name"] == "GitCode API"
168
169
  assert captured["server_kwargs"]["tool"]._client_kwargs["api_key"] == "test-token"
169
170
  assert captured["run_kwargs"] == {
171
+ "show_banner": None,
170
172
  "transport": "http",
171
173
  "host": "127.0.0.1",
172
174
  "port": 8000,
@@ -0,0 +1,280 @@
1
+ import json
2
+ from typing import Optional
3
+
4
+ import httpx
5
+ import pytest
6
+
7
+ from gitcode_api._models import ReleaseAsset, ReleaseUploadURL
8
+
9
+ RELEASE_PAYLOAD = {
10
+ "tag_name": "v1.0.0",
11
+ "target_commitish": "main",
12
+ "prerelease": False,
13
+ "name": "v1.0.0",
14
+ "body": "Release notes",
15
+ "author": {"id": "1", "login": "octo", "name": "Octo", "avatar_url": "https://example.com/a.png"},
16
+ "created_at": "2026-05-12T10:00:00+08:00",
17
+ "assets": [
18
+ {"browser_download_url": "https://example.com/checksums.txt", "name": "checksums.txt", "type": "text/plain"}
19
+ ],
20
+ "release_status": "latest",
21
+ }
22
+
23
+
24
+ def test_labels_list_enterprise_v8_uses_absolute_versioned_url(sync_client_factory) -> None:
25
+ seen_url: Optional[str] = None
26
+
27
+ def handler(request: httpx.Request) -> httpx.Response:
28
+ nonlocal seen_url
29
+ seen_url = str(request.url)
30
+ return httpx.Response(200, json=[{"name": "bug", "color": "#f00"}])
31
+
32
+ client, http_client = sync_client_factory(handler, base_url="https://api.gitcode.com/api/v5")
33
+ try:
34
+ labels = client.labels.list_enterprise(enterprise="SushiNinja", api_version="v8", search="bug")
35
+ assert labels[0].name == "bug"
36
+ assert seen_url == "https://api.gitcode.com/api/v8/enterprises/SushiNinja/labels?search=bug"
37
+ finally:
38
+ client.close()
39
+ http_client.close()
40
+
41
+
42
+ @pytest.mark.asyncio
43
+ async def test_async_webhooks_test_hits_expected_endpoint(async_client_factory) -> None:
44
+ def handler(request: httpx.Request) -> httpx.Response:
45
+ assert request.method == "POST"
46
+ assert request.url.path == "/api/v5/repos/SushiNinja/GitCode-API/hooks/44/tests"
47
+ return httpx.Response(204)
48
+
49
+ client, http_client = async_client_factory(handler, owner="SushiNinja", repo="GitCode-API")
50
+ try:
51
+ result = await client.webhooks.test(hook_id=44)
52
+ assert result is None
53
+ finally:
54
+ await client.close()
55
+ await http_client.aclose()
56
+
57
+
58
+ def test_releases_create_posts_documented_payload_and_models_assets(sync_client_factory) -> None:
59
+ def handler(request: httpx.Request) -> httpx.Response:
60
+ assert request.method == "POST"
61
+ assert request.url.path == "/api/v5/repos/SushiNinja/GitCode-API/releases"
62
+ assert json.loads(request.content) == {
63
+ "tag_name": "v1.0.0",
64
+ "name": "v1.0.0",
65
+ "body": "Release notes",
66
+ "target_commitish": "main",
67
+ "release_status": "latest",
68
+ }
69
+ return httpx.Response(200, json=RELEASE_PAYLOAD, request=request)
70
+
71
+ client, http_client = sync_client_factory(handler, owner="SushiNinja", repo="GitCode-API")
72
+ try:
73
+ release = client.releases.create(
74
+ tag_name="v1.0.0",
75
+ name="v1.0.0",
76
+ body="Release notes",
77
+ target_commitish="main",
78
+ release_status="latest",
79
+ )
80
+ finally:
81
+ client.close()
82
+ http_client.close()
83
+
84
+ assert release.target_commitish == "main"
85
+ assert release.prerelease is False
86
+ assert release.release_status == "latest"
87
+ assert release.assets is not None
88
+ assert isinstance(release.assets[0], ReleaseAsset)
89
+ assert release.assets[0].browser_download_url == "https://example.com/checksums.txt"
90
+
91
+
92
+ def test_releases_update_uses_tag_path_and_documented_payload(sync_client_factory) -> None:
93
+ def handler(request: httpx.Request) -> httpx.Response:
94
+ assert request.method == "PATCH"
95
+ assert request.url.path == "/api/v5/repos/SushiNinja/GitCode-API/releases/v1.0.0"
96
+ assert json.loads(request.content) == {
97
+ "name": "v1.0.0",
98
+ "body": "Updated notes",
99
+ "release_status": "pre",
100
+ }
101
+ return httpx.Response(200, json={**RELEASE_PAYLOAD, "body": "Updated notes", "release_status": "pre"})
102
+
103
+ client, http_client = sync_client_factory(handler, owner="SushiNinja", repo="GitCode-API")
104
+ try:
105
+ release = client.releases.update(tag="v1.0.0", name="v1.0.0", body="Updated notes", release_status="pre")
106
+ finally:
107
+ client.close()
108
+ http_client.close()
109
+
110
+ assert release.body == "Updated notes"
111
+ assert release.release_status == "pre"
112
+
113
+
114
+ def test_releases_list_sends_documented_query_params(sync_client_factory) -> None:
115
+ def handler(request: httpx.Request) -> httpx.Response:
116
+ assert request.method == "GET"
117
+ assert request.url.path == "/api/v5/repos/SushiNinja/GitCode-API/releases"
118
+ assert request.url.params["direction"] == "desc"
119
+ assert request.url.params["page"] == "2"
120
+ assert request.url.params["per_page"] == "50"
121
+ return httpx.Response(200, json=[RELEASE_PAYLOAD], request=request)
122
+
123
+ client, http_client = sync_client_factory(handler, owner="SushiNinja", repo="GitCode-API")
124
+ try:
125
+ releases = client.releases.list(direction="desc", page=2, per_page=50)
126
+ finally:
127
+ client.close()
128
+ http_client.close()
129
+
130
+ assert releases[0].tag_name == "v1.0.0"
131
+
132
+
133
+ def test_releases_get_upload_url_returns_model(sync_client_factory) -> None:
134
+ payload = {
135
+ "url": "https://obs.example.com/upload",
136
+ "headers": {"x-obs-acl": "public-read", "Content-Type": "application/pdf"},
137
+ }
138
+
139
+ def handler(request: httpx.Request) -> httpx.Response:
140
+ assert request.method == "GET"
141
+ assert request.url.path == "/api/v5/repos/SushiNinja/GitCode-API/releases/v1.0.0/upload_url"
142
+ assert request.url.params["file_name"] == "notes.pdf"
143
+ return httpx.Response(200, json=payload, request=request)
144
+
145
+ client, http_client = sync_client_factory(handler, owner="SushiNinja", repo="GitCode-API")
146
+ try:
147
+ upload_url = client.releases.get_upload_url(tag="v1.0.0", file_name="notes.pdf")
148
+ finally:
149
+ client.close()
150
+ http_client.close()
151
+
152
+ assert isinstance(upload_url, ReleaseUploadURL)
153
+ assert upload_url.url == "https://obs.example.com/upload"
154
+ assert upload_url.headers == payload["headers"]
155
+
156
+
157
+ def test_releases_upload_puts_file_content_to_presigned_url(sync_client_factory, tmp_path) -> None:
158
+ upload_path = tmp_path / "notes.pdf"
159
+ upload_path.write_bytes(b"pdf-bytes")
160
+ seen_upload = False
161
+
162
+ def handler(request: httpx.Request) -> httpx.Response:
163
+ nonlocal seen_upload
164
+ if request.method == "GET":
165
+ assert request.url.path == "/api/v5/repos/SushiNinja/GitCode-API/releases/v1.0.0/upload_url"
166
+ assert request.url.params["file_name"] == "notes.pdf"
167
+ upload_path.write_bytes(b"mutated-after-upload-url-request")
168
+ return httpx.Response(
169
+ 200,
170
+ json={
171
+ "url": "https://obs.example.com/upload",
172
+ "headers": {"x-obs-acl": "public-read", "Content-Type": "application/pdf"},
173
+ },
174
+ request=request,
175
+ )
176
+
177
+ assert request.method == "PUT"
178
+ assert str(request.url) == "https://obs.example.com/upload"
179
+ assert "authorization" not in request.headers
180
+ assert request.headers["x-obs-acl"] == "public-read"
181
+ assert request.headers["content-type"] == "application/pdf"
182
+ assert request.content == b"pdf-bytes"
183
+ seen_upload = True
184
+ return httpx.Response(200, request=request)
185
+
186
+ client, http_client = sync_client_factory(handler, owner="SushiNinja", repo="GitCode-API")
187
+ try:
188
+ result = client.releases.upload(tag="v1.0.0", file_name="notes.pdf", content=str(upload_path))
189
+ finally:
190
+ client.close()
191
+ http_client.close()
192
+
193
+ assert result is None
194
+ assert seen_upload is True
195
+
196
+
197
+ def test_releases_upload_rejects_non_bytes_content_before_request(sync_client_factory) -> None:
198
+ def handler(request: httpx.Request) -> httpx.Response:
199
+ raise AssertionError(f"Unexpected request: {request.method} {request.url}")
200
+
201
+ client, http_client = sync_client_factory(handler, owner="SushiNinja", repo="GitCode-API")
202
+ try:
203
+ with pytest.raises(TypeError, match="content must be bytes or a file path string"):
204
+ client.releases.upload(tag="v1.0.0", file_name="notes.pdf", content=object()) # type: ignore[arg-type]
205
+ finally:
206
+ client.close()
207
+ http_client.close()
208
+
209
+
210
+ def test_releases_download_attachment_returns_raw_bytes(sync_client_factory) -> None:
211
+ def handler(request: httpx.Request) -> httpx.Response:
212
+ assert request.method == "GET"
213
+ assert request.url.path == (
214
+ "/api/v5/repos/SushiNinja/GitCode-API/releases/v1.0.0/attach_files/checksums.txt/download"
215
+ )
216
+ return httpx.Response(200, content=b"checksum-bytes", request=request)
217
+
218
+ client, http_client = sync_client_factory(handler, owner="SushiNinja", repo="GitCode-API")
219
+ try:
220
+ content = client.releases.download_attachment(tag="v1.0.0", file_name="checksums.txt")
221
+ finally:
222
+ client.close()
223
+ http_client.close()
224
+
225
+ assert content == b"checksum-bytes"
226
+
227
+
228
+ @pytest.mark.asyncio
229
+ async def test_async_releases_get_upload_and_download_attachment(async_client_factory, tmp_path) -> None:
230
+ upload_path = tmp_path / "async-notes.pdf"
231
+ upload_path.write_bytes(b"async-pdf-bytes")
232
+ seen_upload = False
233
+
234
+ def handler(request: httpx.Request) -> httpx.Response:
235
+ nonlocal seen_upload
236
+ if request.url.path.endswith("/download"):
237
+ assert request.method == "GET"
238
+ assert request.url.path == (
239
+ "/api/v5/repos/SushiNinja/GitCode-API/releases/v1.0.0/attach_files/checksums.txt/download"
240
+ )
241
+ return httpx.Response(200, content=b"async-checksum-bytes", request=request)
242
+
243
+ if request.method == "PUT":
244
+ assert str(request.url) == "https://obs.example.com/async-upload"
245
+ assert "authorization" not in request.headers
246
+ assert request.headers["x-obs-acl"] == "public-read"
247
+ assert request.content == b"async-pdf-bytes"
248
+ seen_upload = True
249
+ return httpx.Response(204, request=request)
250
+
251
+ if request.url.path.endswith("/upload_url"):
252
+ assert request.method == "GET"
253
+ upload_path.write_bytes(b"mutated-async-pdf-bytes")
254
+ return httpx.Response(
255
+ 200,
256
+ json={
257
+ "url": "https://obs.example.com/async-upload",
258
+ "headers": {"x-obs-acl": "public-read", "Content-Type": "application/pdf"},
259
+ },
260
+ request=request,
261
+ )
262
+
263
+ assert request.method == "GET"
264
+ assert request.url.path == "/api/v5/repos/SushiNinja/GitCode-API/releases/v1.0.0"
265
+ assert request.url.params["temp_download_url"] == "true"
266
+ return httpx.Response(200, json=RELEASE_PAYLOAD, request=request)
267
+
268
+ client, http_client = async_client_factory(handler, owner="SushiNinja", repo="GitCode-API")
269
+ try:
270
+ release = await client.releases.get(tag="v1.0.0", temp_download_url="true")
271
+ result = await client.releases.upload(tag="v1.0.0", file_name="notes.pdf", content=str(upload_path))
272
+ content = await client.releases.download_attachment(tag="v1.0.0", file_name="checksums.txt")
273
+ finally:
274
+ await client.close()
275
+ await http_client.aclose()
276
+
277
+ assert release.tag_name == "v1.0.0"
278
+ assert result is None
279
+ assert seen_upload is True
280
+ assert content == b"async-checksum-bytes"
@@ -1 +0,0 @@
1
- 1.2.11
@@ -1,38 +0,0 @@
1
- from typing import Optional
2
-
3
- import httpx
4
- import pytest
5
-
6
-
7
- def test_labels_list_enterprise_v8_uses_absolute_versioned_url(sync_client_factory) -> None:
8
- seen_url: Optional[str] = None
9
-
10
- def handler(request: httpx.Request) -> httpx.Response:
11
- nonlocal seen_url
12
- seen_url = str(request.url)
13
- return httpx.Response(200, json=[{"name": "bug", "color": "#f00"}])
14
-
15
- client, http_client = sync_client_factory(handler, base_url="https://api.gitcode.com/api/v5")
16
- try:
17
- labels = client.labels.list_enterprise(enterprise="SushiNinja", api_version="v8", search="bug")
18
- assert labels[0].name == "bug"
19
- assert seen_url == "https://api.gitcode.com/api/v8/enterprises/SushiNinja/labels?search=bug"
20
- finally:
21
- client.close()
22
- http_client.close()
23
-
24
-
25
- @pytest.mark.asyncio
26
- async def test_async_webhooks_test_hits_expected_endpoint(async_client_factory) -> None:
27
- def handler(request: httpx.Request) -> httpx.Response:
28
- assert request.method == "POST"
29
- assert request.url.path == "/api/v5/repos/SushiNinja/GitCode-API/hooks/44/tests"
30
- return httpx.Response(204)
31
-
32
- client, http_client = async_client_factory(handler, owner="SushiNinja", repo="GitCode-API")
33
- try:
34
- result = await client.webhooks.test(hook_id=44)
35
- assert result is None
36
- finally:
37
- await client.close()
38
- await http_client.aclose()
File without changes
File without changes