gitcode-api 1.1.2__py3-none-any.whl → 1.1.3__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.
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
gitcode_api/cli.py ADDED
@@ -0,0 +1,255 @@
1
+ """Command-line interface for the GitCode SDK."""
2
+
3
+ import argparse
4
+ import inspect
5
+ import json
6
+ import sys
7
+ from collections.abc import Mapping, Sequence
8
+ from pathlib import Path
9
+ from typing import Any, List, Optional, Union, get_args, get_origin
10
+
11
+ from . import GitCode, __version__
12
+ from ._base_client import DEFAULT_BASE_URL, DEFAULT_TOKEN_ENV
13
+ from ._exceptions import GitCodeError
14
+ from .resources._shared import SyncResource
15
+
16
+
17
+ def _unwrap_optional(annotation: Any) -> Any:
18
+ origin = get_origin(annotation)
19
+ if origin is Union:
20
+ args = [arg for arg in get_args(annotation) if arg is not type(None)]
21
+ if len(args) == 1:
22
+ return args[0]
23
+ return annotation
24
+
25
+
26
+ def _is_list_annotation(annotation: Any) -> bool:
27
+ annotation = _unwrap_optional(annotation)
28
+ return get_origin(annotation) in (list, List)
29
+
30
+
31
+ def _list_item_type(annotation: Any) -> Any:
32
+ annotation = _unwrap_optional(annotation)
33
+ args = get_args(annotation)
34
+ if not args:
35
+ return str
36
+ item_type = _unwrap_optional(args[0])
37
+ if item_type in (int, float):
38
+ return item_type
39
+ return str
40
+
41
+
42
+ def _argument_kwargs(parameter: inspect.Parameter) -> dict[str, Any]:
43
+ annotation = _unwrap_optional(parameter.annotation)
44
+ if annotation is bool:
45
+ return {"action": argparse.BooleanOptionalAction, "default": parameter.default}
46
+ if _is_list_annotation(parameter.annotation):
47
+ return {"nargs": "+", "type": _list_item_type(parameter.annotation), "default": None}
48
+ if annotation in (int, float):
49
+ return {"type": annotation}
50
+ return {"type": str}
51
+
52
+
53
+ def _first_doc_line(obj: Any) -> str:
54
+ doc = inspect.getdoc(obj) or ""
55
+ return doc.splitlines()[0] if doc else ""
56
+
57
+
58
+ def _resource_types() -> dict[str, type[SyncResource]]:
59
+ resources: dict[str, type[SyncResource]] = {}
60
+ for name, annotation in GitCode.__annotations__.items():
61
+ if inspect.isclass(annotation) and issubclass(annotation, SyncResource):
62
+ resources[name] = annotation
63
+ return resources
64
+
65
+
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
73
+
74
+
75
+ def _kebab_case(value: str) -> str:
76
+ return value.replace("_", "-")
77
+
78
+
79
+ def _load_json_value(raw: str) -> Any:
80
+ if raw.startswith("@"):
81
+ return json.loads(Path(raw[1:]).read_text(encoding="utf-8"))
82
+ return json.loads(raw)
83
+
84
+
85
+ def _parse_scalar(raw: str) -> Any:
86
+ try:
87
+ return _load_json_value(raw)
88
+ except (OSError, ValueError, json.JSONDecodeError):
89
+ return raw
90
+
91
+
92
+ def _parse_key_value(raw: str) -> tuple[str, Any]:
93
+ if "=" not in raw:
94
+ raise ValueError(f"Expected KEY=VALUE, got: {raw}")
95
+ key, value = raw.split("=", maxsplit=1)
96
+ if not key:
97
+ raise ValueError(f"Expected KEY=VALUE, got: {raw}")
98
+ return key, _parse_scalar(value)
99
+
100
+
101
+ def _to_data(value: Any) -> Any:
102
+ if hasattr(value, "to_dict") and callable(value.to_dict):
103
+ return _to_data(value.to_dict())
104
+ if isinstance(value, Mapping):
105
+ return {key: _to_data(item) for key, item in value.items()}
106
+ if isinstance(value, list):
107
+ return [_to_data(item) for item in value]
108
+ return value
109
+
110
+
111
+ def _write_output(value: Any, *, output_file: Optional[str], compact: bool) -> None:
112
+ if value is None:
113
+ return
114
+ if isinstance(value, bytes):
115
+ if output_file:
116
+ Path(output_file).write_bytes(value)
117
+ else:
118
+ sys.stdout.buffer.write(value)
119
+ return
120
+
121
+ payload = _to_data(value)
122
+ if isinstance(payload, str):
123
+ text = payload
124
+ else:
125
+ text = json.dumps(payload, indent=None if compact else 2, ensure_ascii=True, sort_keys=True)
126
+
127
+ if output_file:
128
+ Path(output_file).write_text(text + ("\n" if not text.endswith("\n") else ""), encoding="utf-8")
129
+ else:
130
+ print(text)
131
+
132
+
133
+ def _global_parent_parser() -> argparse.ArgumentParser:
134
+ parser = argparse.ArgumentParser(add_help=False)
135
+ parser.add_argument("--api-key", help=f"GitCode access token. Defaults to {DEFAULT_TOKEN_ENV}.")
136
+ parser.add_argument("--owner", help="Default repository owner.")
137
+ 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.")
139
+ parser.add_argument("--timeout", type=float, default=None, help="Request timeout in seconds.")
140
+ parser.add_argument("--output-file", help="Write the response to a file instead of stdout.")
141
+ parser.add_argument("--compact", action="store_true", help="Print JSON without indentation.")
142
+ return parser
143
+
144
+
145
+ def build_parser() -> argparse.ArgumentParser:
146
+ common = _global_parent_parser()
147
+ parser = argparse.ArgumentParser(
148
+ 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],
153
+ )
154
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
155
+
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),
161
+ )
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],
171
+ )
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)
203
+
204
+ return parser
205
+
206
+
207
+ def _collect_kwargs(args: argparse.Namespace, method: Any) -> dict[str, Any]:
208
+ signature = inspect.signature(method)
209
+ kwargs: dict[str, Any] = {}
210
+ for parameter in signature.parameters.values():
211
+ if parameter.name == "self":
212
+ continue
213
+ if parameter.kind == inspect.Parameter.VAR_KEYWORD:
214
+ extra_kwargs: dict[str, Any] = {}
215
+ if getattr(args, "extra_json", None):
216
+ raw_extra = _load_json_value(args.extra_json)
217
+ if not isinstance(raw_extra, dict):
218
+ raise ValueError("--set-json must decode to a JSON object.")
219
+ extra_kwargs.update(raw_extra)
220
+ for item in getattr(args, "extra_items", []) or []:
221
+ key, value = _parse_key_value(item)
222
+ extra_kwargs[key] = value
223
+ kwargs.update(extra_kwargs)
224
+ continue
225
+
226
+ value = getattr(args, parameter.name)
227
+ if value is None:
228
+ if parameter.default is inspect.Signature.empty:
229
+ raise ValueError(f"--{parameter.name.replace('_', '-')} is required.")
230
+ continue
231
+ kwargs[parameter.name] = value
232
+ return kwargs
233
+
234
+
235
+ def main(argv: Optional[Sequence[str]] = None) -> int:
236
+ parser = build_parser()
237
+ args = parser.parse_args(argv)
238
+
239
+ try:
240
+ with GitCode(
241
+ api_key=args.api_key,
242
+ owner=args.owner,
243
+ repo=args.repo,
244
+ base_url=args.base_url,
245
+ timeout=args.timeout,
246
+ ) as client:
247
+ resource = getattr(client, args.resource_name)
248
+ method = getattr(resource, args.method_name)
249
+ result = method(**_collect_kwargs(args, method))
250
+ except (GitCodeError, OSError, TypeError, ValueError) as exc: # pragma: no cover - integration style
251
+ print(f"error: {exc}", file=sys.stderr)
252
+ return 1
253
+
254
+ _write_output(result, output_file=args.output_file, compact=args.compact)
255
+ return 0
gitcode_api/version.txt CHANGED
@@ -1 +1 @@
1
- 1.1.2
1
+ 1.1.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitcode-api
3
- Version: 1.1.2
3
+ Version: 1.1.3
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
@@ -46,7 +46,7 @@ Dynamic: license-file
46
46
  Install from PyPI:
47
47
 
48
48
  ```bash
49
- pip install gitcode-api
49
+ pip install -U gitcode-api
50
50
  ```
51
51
 
52
52
  ## Authentication
@@ -71,6 +71,19 @@ client = GitCode(
71
71
  )
72
72
  ```
73
73
 
74
+ ## CLI
75
+
76
+ After installation, you can invoke the SDK directly from the command line:
77
+
78
+ ```bash
79
+ gitcode-api repos get --api-key "$GITCODE_ACCESS_TOKEN" --owner SushiNinja --repo GitCode-API
80
+ python -m gitcode_api pulls list --api-key "$GITCODE_ACCESS_TOKEN" --owner SushiNinja --repo GitCode-API --state open
81
+ ```
82
+
83
+ Commands mirror the synchronous resource methods on `GitCode`, using the pattern
84
+ `gitcode-api <resource> <method> ...`. For methods that accept extra `**params`
85
+ or `**payload`, pass repeated `--set key=value` flags or `--set-json '{"key": "value"}'`.
86
+
74
87
  ## Quick Start
75
88
 
76
89
  ### Sync client
@@ -1,17 +1,20 @@
1
1
  gitcode_api/__init__.py,sha256=NvLp28D9brVbmv2ROSZ8mxKDuIamE9xeutBKGl9pra8,497
2
+ gitcode_api/__main__.py,sha256=MHKZ_ae3fSLGTLUUMOx15fWdeOnJSHhq-zslRP5F5Lc,79
2
3
  gitcode_api/_base_client.py,sha256=1COCZXzSiCe_LNawsOsTNvh0WMh1JW4A5yYgX56HwjY,12894
3
4
  gitcode_api/_client.py,sha256=AOpp1P7vdSlMcn0XP3JMPnAKbQMftMO9WMubBGw2LZc,8129
4
5
  gitcode_api/_exceptions.py,sha256=T5N8gBGmPSktDkLP5P_hxbzOHw3W378TzxN1xja40pA,1140
5
6
  gitcode_api/_models.py,sha256=2bR6Wxc3Ie7iM3UFNY0ThWeNjxFHzDAC-8QFgcgcT_0,108599
6
- gitcode_api/version.txt,sha256=g9DE-d3AiatY4yLimLYZDyv1kdwiX_XN130F5qhCBhk,5
7
+ gitcode_api/cli.py,sha256=R4HUPzrrs_ejHjIAEZw0P5ozUuEfFScJvfVRyrR2HXw,9573
8
+ gitcode_api/version.txt,sha256=QVh-oXE9O_Eto7b8aIGNtIm6f6enRYGYzqERAJEy8sk,5
7
9
  gitcode_api/resources/__init__.py,sha256=nsCKW0bFDZ5ombJZxLThmO82sOuF7o4OKUMRkAmwbwk,1725
8
10
  gitcode_api/resources/_shared.py,sha256=EB9fr4eQUc9IOdZCn4AoI946SJYCNVAtOOHEo_OzJAQ,5317
9
11
  gitcode_api/resources/account.py,sha256=mnc2p7wI-nBnHFNdWPNiHfmZpT6d3RDQC777gewtm4M,38801
10
12
  gitcode_api/resources/collaboration.py,sha256=26rQCeayZfUQ_6dIdl5HOGXnYkMLx8V7XsF_Wi-4Qt4,101417
11
13
  gitcode_api/resources/misc.py,sha256=guDwh4cxbTVsSa7EivaYM3bKMJ8_Op4KucGbKEoayKE,22412
12
14
  gitcode_api/resources/repositories.py,sha256=EAK2znZhEsgVUu-NDEQslSEEYJzvb-kHuh4mW57y6sc,78178
13
- gitcode_api-1.1.2.dist-info/licenses/LICENSE,sha256=gOACXuWhMu6PJKVLr9RQbxX3HULnZIGNXCaMFJIXhoA,1067
14
- gitcode_api-1.1.2.dist-info/METADATA,sha256=0N2qnZGKbZf_4jxrfQwWDd2daaRhdA3GRw0OqC75C0o,7151
15
- gitcode_api-1.1.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
16
- gitcode_api-1.1.2.dist-info/top_level.txt,sha256=gIlg0ptyOUHJT64ajOjWIhRPYgIQnMIvnhhnesw9fxU,12
17
- gitcode_api-1.1.2.dist-info/RECORD,,
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,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gitcode-api = gitcode_api.cli:main