gitcode-api 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ """Public package exports for the GitCode SDK."""
2
+
3
+ from pathlib import Path
4
+
5
+ from ._client import AsyncGitCode, GitCode
6
+ from ._exceptions import (
7
+ GitCodeAPIError,
8
+ GitCodeConfigurationError,
9
+ GitCodeError,
10
+ GitCodeHTTPStatusError,
11
+ )
12
+
13
+ __version__ = (Path(__file__).parent / "version.txt").read_text().strip()
14
+
15
+ __all__ = [
16
+ "__version__",
17
+ "AsyncGitCode",
18
+ "GitCode",
19
+ "GitCodeAPIError",
20
+ "GitCodeConfigurationError",
21
+ "GitCodeError",
22
+ "GitCodeHTTPStatusError",
23
+ ]
@@ -0,0 +1,324 @@
1
+ """Shared transport helpers for the GitCode SDK.
2
+
3
+ The classes in this module normalize authentication, URL construction,
4
+ payload cleanup, and response parsing for both sync and async clients.
5
+ """
6
+
7
+ import os
8
+ from typing import Any
9
+ from urllib.parse import quote
10
+
11
+ import httpx
12
+
13
+ from ._exceptions import GitCodeConfigurationError, GitCodeHTTPStatusError
14
+
15
+ DEFAULT_BASE_URL = "https://api.gitcode.com/api/v5"
16
+ DEFAULT_TIMEOUT = 30.0
17
+ DEFAULT_TOKEN_ENV = "GITCODE_ACCESS_TOKEN"
18
+
19
+
20
+ def _drop_none_values(mapping: dict[str, Any]) -> dict[str, Any]:
21
+ """Return a copy of ``mapping`` without keys that have ``None`` values."""
22
+ return {key: value for key, value in mapping.items() if value is not None}
23
+
24
+
25
+ class BaseGitCodeClient:
26
+ """Base configuration shared by synchronous and asynchronous clients.
27
+
28
+ :param api_key: Personal access token for the GitCode API.
29
+ :param owner: Default repository owner for repository-scoped calls.
30
+ :param repo: Default repository name for repository-scoped calls.
31
+ :param base_url: Base URL for the GitCode REST API.
32
+ :param timeout: Request timeout in seconds.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ *,
38
+ api_key: str | None = None,
39
+ owner: str | None = None,
40
+ repo: str | None = None,
41
+ base_url: str = DEFAULT_BASE_URL,
42
+ timeout: float | None = None,
43
+ ) -> None:
44
+ """Store client configuration and resolve authentication."""
45
+ self.api_key = self._resolve_api_key(api_key)
46
+ self.owner = owner
47
+ self.repo = repo
48
+ self.base_url = base_url.rstrip("/")
49
+ self.timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
50
+
51
+ def _resolve_api_key(self, api_key: str | None) -> str:
52
+ """Resolve the access token from an argument or environment variable."""
53
+ token = api_key or os.getenv(DEFAULT_TOKEN_ENV)
54
+ if not token:
55
+ raise GitCodeConfigurationError("No API key provided. Pass api_key=... or set GITCODE_ACCESS_TOKEN.")
56
+ return token
57
+
58
+ def _resolve_repo_context(
59
+ self,
60
+ owner: str | None = None,
61
+ repo: str | None = None,
62
+ ) -> tuple[str, str]:
63
+ """Return the effective repository owner and name for a request."""
64
+ resolved_owner = owner or self.owner
65
+ resolved_repo = repo or self.repo
66
+ if not resolved_owner or not resolved_repo:
67
+ raise GitCodeConfigurationError(
68
+ "Repository methods require owner and repo, either per-call or on the client."
69
+ )
70
+ return resolved_owner, resolved_repo
71
+
72
+ def _headers(self, extra_headers: dict[str, str] | None = None) -> dict[str, str]:
73
+ """Build request headers for authenticated JSON API calls."""
74
+ headers = {
75
+ "Accept": "application/json",
76
+ "Authorization": f"Bearer {self.api_key}",
77
+ }
78
+ if extra_headers:
79
+ headers.update(extra_headers)
80
+ return headers
81
+
82
+ def _encode_segment(self, value: str | int) -> str:
83
+ """Percent-encode a single URL path segment."""
84
+ return quote(str(value), safe="")
85
+
86
+ def _encode_path_value(self, value: str) -> str:
87
+ """Percent-encode a repository path while preserving slashes."""
88
+ return quote(value, safe="/")
89
+
90
+ def _join_path(self, *segments: str | int) -> str:
91
+ """Join URL path segments into an API path."""
92
+ return "/" + "/".join(self._encode_segment(segment) for segment in segments)
93
+
94
+ def _repo_path(
95
+ self,
96
+ *segments: str | int,
97
+ owner: str | None = None,
98
+ repo: str | None = None,
99
+ ) -> str:
100
+ """Build a path under ``/repos/{owner}/{repo}``."""
101
+ resolved_owner, resolved_repo = self._resolve_repo_context(owner, repo)
102
+ return self._join_path("repos", resolved_owner, resolved_repo, *segments)
103
+
104
+ def _repo_file_path(
105
+ self,
106
+ prefix: str,
107
+ file_path: str,
108
+ *,
109
+ owner: str | None = None,
110
+ repo: str | None = None,
111
+ ) -> str:
112
+ """Build a file-oriented repository path such as ``contents`` or ``raw``."""
113
+ resolved_owner, resolved_repo = self._resolve_repo_context(owner, repo)
114
+ return (
115
+ f"/repos/{self._encode_segment(resolved_owner)}/"
116
+ f"{self._encode_segment(resolved_repo)}/{self._encode_segment(prefix)}/"
117
+ f"{self._encode_path_value(file_path)}"
118
+ )
119
+
120
+ def _path(self, *segments: str | int) -> str:
121
+ """Build a non-repository API path."""
122
+ return self._join_path(*segments)
123
+
124
+ def _full_url(self, path: str) -> str:
125
+ """Return an absolute URL for a relative API path."""
126
+ if path.startswith("http://") or path.startswith("https://"):
127
+ return path
128
+ return f"{self.base_url}{path}"
129
+
130
+ def _coerce_payload(
131
+ self,
132
+ *,
133
+ params: dict[str, Any] | None = None,
134
+ json: Any = None,
135
+ data: dict[str, Any] | None = None,
136
+ ) -> tuple[dict[str, Any] | None, Any, dict[str, Any] | None]:
137
+ """Drop ``None`` values from params and form payloads before sending."""
138
+ clean_params = _drop_none_values(params or {}) or None
139
+ clean_data = _drop_none_values(data or {}) or None
140
+ return clean_params, json, clean_data
141
+
142
+ def _raise_for_error(self, response: httpx.Response) -> None:
143
+ """Raise a typed SDK error for non-successful HTTP responses."""
144
+ if response.is_success:
145
+ return
146
+
147
+ payload: Any
148
+ message = response.text
149
+ try:
150
+ payload = response.json()
151
+ if isinstance(payload, dict) and payload.get("message"):
152
+ message = str(payload["message"])
153
+ except ValueError:
154
+ payload = response.text
155
+
156
+ raise GitCodeHTTPStatusError(
157
+ message or f"GitCode API request failed with status {response.status_code}.",
158
+ status_code=response.status_code,
159
+ request_id=response.headers.get("X-Request-Id"),
160
+ payload=payload,
161
+ )
162
+
163
+ def _parse_response(self, response: httpx.Response, *, raw: bool = False) -> Any:
164
+ """Parse a GitCode API response as raw bytes, JSON, text, or ``None``."""
165
+ self._raise_for_error(response)
166
+ if response.status_code == 204:
167
+ return None
168
+ if raw:
169
+ return response.content
170
+ if not response.content:
171
+ return None
172
+ content_type = response.headers.get("Content-Type", "")
173
+ if "application/json" in content_type or "text/json" in content_type:
174
+ return response.json()
175
+ try:
176
+ return response.json()
177
+ except ValueError:
178
+ return response.text
179
+
180
+
181
+ class SyncAPIClient(BaseGitCodeClient):
182
+ """Low-level synchronous HTTP transport used by resource classes.
183
+
184
+ :param api_key: Personal access token for the GitCode API.
185
+ :param owner: Default repository owner for repository-scoped calls.
186
+ :param repo: Default repository name for repository-scoped calls.
187
+ :param base_url: Base URL for the GitCode REST API.
188
+ :param timeout: Request timeout in seconds.
189
+ :param http_client: Optional pre-configured ``httpx.Client`` instance.
190
+ """
191
+
192
+ def __init__(
193
+ self,
194
+ *,
195
+ api_key: str | None = None,
196
+ owner: str | None = None,
197
+ repo: str | None = None,
198
+ base_url: str = DEFAULT_BASE_URL,
199
+ timeout: float | None = None,
200
+ http_client: httpx.Client | None = None,
201
+ ) -> None:
202
+ """Create or reuse an ``httpx.Client`` for synchronous requests."""
203
+ super().__init__(api_key=api_key, owner=owner, repo=repo, base_url=base_url, timeout=timeout)
204
+ self._owns_client = http_client is None
205
+ self._client = http_client or httpx.Client(timeout=self.timeout)
206
+
207
+ def request(
208
+ self,
209
+ method: str,
210
+ path: str,
211
+ *,
212
+ params: dict[str, Any] | None = None,
213
+ json: Any = None,
214
+ data: dict[str, Any] | None = None,
215
+ headers: dict[str, str] | None = None,
216
+ raw: bool = False,
217
+ ) -> Any:
218
+ """Send an HTTP request to the GitCode API and parse the response.
219
+
220
+ :param method: HTTP method such as ``"GET"`` or ``"POST"``.
221
+ :param path: Relative API path or absolute URL.
222
+ :param params: Optional query parameters.
223
+ :param json: Optional JSON request body.
224
+ :param data: Optional form payload.
225
+ :param headers: Optional extra HTTP headers.
226
+ :param raw: When ``True``, return response bytes instead of parsed JSON.
227
+ :returns: Parsed JSON payload, raw bytes, text, or ``None``.
228
+ """
229
+ clean_params, clean_json, clean_data = self._coerce_payload(params=params, json=json, data=data)
230
+ response = self._client.request(
231
+ method,
232
+ self._full_url(path),
233
+ params=clean_params,
234
+ json=clean_json,
235
+ data=clean_data,
236
+ headers=self._headers(headers),
237
+ )
238
+ return self._parse_response(response, raw=raw)
239
+
240
+ def close(self) -> None:
241
+ """Close the underlying HTTP client if this instance created it."""
242
+ if self._owns_client:
243
+ self._client.close()
244
+
245
+ def __enter__(self) -> "SyncAPIClient":
246
+ """Enter a context manager and return the client instance."""
247
+ return self
248
+
249
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
250
+ """Close the client when leaving a context manager."""
251
+ self.close()
252
+
253
+
254
+ class AsyncAPIClient(BaseGitCodeClient):
255
+ """Low-level asynchronous HTTP transport used by async resource classes.
256
+
257
+ :param api_key: Personal access token for the GitCode API.
258
+ :param owner: Default repository owner for repository-scoped calls.
259
+ :param repo: Default repository name for repository-scoped calls.
260
+ :param base_url: Base URL for the GitCode REST API.
261
+ :param timeout: Request timeout in seconds.
262
+ :param http_client: Optional pre-configured ``httpx.AsyncClient`` instance.
263
+ """
264
+
265
+ def __init__(
266
+ self,
267
+ *,
268
+ api_key: str | None = None,
269
+ owner: str | None = None,
270
+ repo: str | None = None,
271
+ base_url: str = DEFAULT_BASE_URL,
272
+ timeout: float | None = None,
273
+ http_client: httpx.AsyncClient | None = None,
274
+ ) -> None:
275
+ """Create or reuse an ``httpx.AsyncClient`` for asynchronous requests."""
276
+ super().__init__(api_key=api_key, owner=owner, repo=repo, base_url=base_url, timeout=timeout)
277
+ self._owns_client = http_client is None
278
+ self._client = http_client or httpx.AsyncClient(timeout=self.timeout)
279
+
280
+ async def request(
281
+ self,
282
+ method: str,
283
+ path: str,
284
+ *,
285
+ params: dict[str, Any] | None = None,
286
+ json: Any = None,
287
+ data: dict[str, Any] | None = None,
288
+ headers: dict[str, str] | None = None,
289
+ raw: bool = False,
290
+ ) -> Any:
291
+ """Send an asynchronous HTTP request to the GitCode API.
292
+
293
+ :param method: HTTP method such as ``"GET"`` or ``"POST"``.
294
+ :param path: Relative API path or absolute URL.
295
+ :param params: Optional query parameters.
296
+ :param json: Optional JSON request body.
297
+ :param data: Optional form payload.
298
+ :param headers: Optional extra HTTP headers.
299
+ :param raw: When ``True``, return response bytes instead of parsed JSON.
300
+ :returns: Parsed JSON payload, raw bytes, text, or ``None``.
301
+ """
302
+ clean_params, clean_json, clean_data = self._coerce_payload(params=params, json=json, data=data)
303
+ response = await self._client.request(
304
+ method,
305
+ self._full_url(path),
306
+ params=clean_params,
307
+ json=clean_json,
308
+ data=clean_data,
309
+ headers=self._headers(headers),
310
+ )
311
+ return self._parse_response(response, raw=raw)
312
+
313
+ async def close(self) -> None:
314
+ """Close the underlying async HTTP client if this instance created it."""
315
+ if self._owns_client:
316
+ await self._client.aclose()
317
+
318
+ async def __aenter__(self) -> "AsyncAPIClient":
319
+ """Enter an async context manager and return the client instance."""
320
+ return self
321
+
322
+ async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
323
+ """Close the client when leaving an async context manager."""
324
+ await self.close()
gitcode_api/_client.py ADDED
@@ -0,0 +1,235 @@
1
+ """Top-level synchronous and asynchronous GitCode API clients.
2
+
3
+ These client classes expose grouped resource helpers that mirror the
4
+ published GitCode REST API documentation.
5
+ """
6
+
7
+ import httpx
8
+
9
+ from ._base_client import DEFAULT_BASE_URL, AsyncAPIClient, SyncAPIClient
10
+ from .resources import (
11
+ AsyncBranchesResource,
12
+ AsyncCommitsResource,
13
+ AsyncIssuesResource,
14
+ AsyncLabelsResource,
15
+ AsyncMembersResource,
16
+ AsyncMilestonesResource,
17
+ AsyncOAuthResource,
18
+ AsyncOrgsResource,
19
+ AsyncPullsResource,
20
+ AsyncReleasesResource,
21
+ AsyncRepoContentsResource,
22
+ AsyncReposResource,
23
+ AsyncSearchResource,
24
+ AsyncTagsResource,
25
+ AsyncUsersResource,
26
+ AsyncWebhooksResource,
27
+ BranchesResource,
28
+ CommitsResource,
29
+ IssuesResource,
30
+ LabelsResource,
31
+ MembersResource,
32
+ MilestonesResource,
33
+ OAuthResource,
34
+ OrgsResource,
35
+ PullsResource,
36
+ ReleasesResource,
37
+ RepoContentsResource,
38
+ ReposResource,
39
+ SearchResource,
40
+ TagsResource,
41
+ UsersResource,
42
+ WebhooksResource,
43
+ )
44
+
45
+
46
+ class GitCode(SyncAPIClient):
47
+ """Synchronous GitCode API client.
48
+
49
+ :param api_key: Personal access token used for GitCode API requests.
50
+ :param owner: Default repository owner used by repository-scoped helpers.
51
+ :param repo: Default repository name used by repository-scoped helpers.
52
+ :param base_url: Base URL for the GitCode REST API.
53
+ :param timeout: Request timeout in seconds.
54
+ :param http_client: Optional pre-configured ``httpx.Client`` instance.
55
+ """
56
+
57
+ repos: ReposResource
58
+ """Repository endpoints exposed as ``client.repos``."""
59
+
60
+ contents: RepoContentsResource
61
+ """Repository content endpoints exposed as ``client.contents``."""
62
+
63
+ branches: BranchesResource
64
+ """Repository branch endpoints exposed as ``client.branches``."""
65
+
66
+ commits: CommitsResource
67
+ """Repository commit endpoints exposed as ``client.commits``."""
68
+
69
+ issues: IssuesResource
70
+ """Issue endpoints exposed as ``client.issues``."""
71
+
72
+ pulls: PullsResource
73
+ """Pull request endpoints exposed as ``client.pulls``."""
74
+
75
+ labels: LabelsResource
76
+ """Label endpoints exposed as ``client.labels``."""
77
+
78
+ milestones: MilestonesResource
79
+ """Milestone endpoints exposed as ``client.milestones``."""
80
+
81
+ members: MembersResource
82
+ """Repository member endpoints exposed as ``client.members``."""
83
+
84
+ releases: ReleasesResource
85
+ """Release endpoints exposed as ``client.releases``."""
86
+
87
+ tags: TagsResource
88
+ """Tag endpoints exposed as ``client.tags``."""
89
+
90
+ webhooks: WebhooksResource
91
+ """Webhook endpoints exposed as ``client.webhooks``."""
92
+
93
+ users: UsersResource
94
+ """User endpoints exposed as ``client.users``."""
95
+
96
+ orgs: OrgsResource
97
+ """Organization endpoints exposed as ``client.orgs``."""
98
+
99
+ search: SearchResource
100
+ """Search endpoints exposed as ``client.search``."""
101
+
102
+ oauth: OAuthResource
103
+ """OAuth helper endpoints exposed as ``client.oauth``."""
104
+
105
+ def __init__(
106
+ self,
107
+ *,
108
+ api_key: str | None = None,
109
+ owner: str | None = None,
110
+ repo: str | None = None,
111
+ base_url: str = DEFAULT_BASE_URL,
112
+ timeout: float | None = None,
113
+ http_client: httpx.Client | None = None,
114
+ ) -> None:
115
+ """Create a synchronous client and attach resource groups."""
116
+ super().__init__(
117
+ api_key=api_key,
118
+ owner=owner,
119
+ repo=repo,
120
+ base_url=base_url,
121
+ timeout=timeout,
122
+ http_client=http_client,
123
+ )
124
+ self.repos = ReposResource(self)
125
+ self.contents = RepoContentsResource(self)
126
+ self.branches = BranchesResource(self)
127
+ self.commits = CommitsResource(self)
128
+ self.issues = IssuesResource(self)
129
+ self.pulls = PullsResource(self)
130
+ self.labels = LabelsResource(self)
131
+ self.milestones = MilestonesResource(self)
132
+ self.members = MembersResource(self)
133
+ self.releases = ReleasesResource(self)
134
+ self.tags = TagsResource(self)
135
+ self.webhooks = WebhooksResource(self)
136
+ self.users = UsersResource(self)
137
+ self.orgs = OrgsResource(self)
138
+ self.search = SearchResource(self)
139
+ self.oauth = OAuthResource(self)
140
+
141
+
142
+ class AsyncGitCode(AsyncAPIClient):
143
+ """Asynchronous GitCode API client.
144
+
145
+ :param api_key: Personal access token used for GitCode API requests.
146
+ :param owner: Default repository owner used by repository-scoped helpers.
147
+ :param repo: Default repository name used by repository-scoped helpers.
148
+ :param base_url: Base URL for the GitCode REST API.
149
+ :param timeout: Request timeout in seconds.
150
+ :param http_client: Optional pre-configured ``httpx.AsyncClient`` instance.
151
+ """
152
+
153
+ repos: AsyncReposResource
154
+ """Repository endpoints exposed as ``client.repos``."""
155
+
156
+ contents: AsyncRepoContentsResource
157
+ """Repository content endpoints exposed as ``client.contents``."""
158
+
159
+ branches: AsyncBranchesResource
160
+ """Repository branch endpoints exposed as ``client.branches``."""
161
+
162
+ commits: AsyncCommitsResource
163
+ """Repository commit endpoints exposed as ``client.commits``."""
164
+
165
+ issues: AsyncIssuesResource
166
+ """Issue endpoints exposed as ``client.issues``."""
167
+
168
+ pulls: AsyncPullsResource
169
+ """Pull request endpoints exposed as ``client.pulls``."""
170
+
171
+ labels: AsyncLabelsResource
172
+ """Label endpoints exposed as ``client.labels``."""
173
+
174
+ milestones: AsyncMilestonesResource
175
+ """Milestone endpoints exposed as ``client.milestones``."""
176
+
177
+ members: AsyncMembersResource
178
+ """Repository member endpoints exposed as ``client.members``."""
179
+
180
+ releases: AsyncReleasesResource
181
+ """Release endpoints exposed as ``client.releases``."""
182
+
183
+ tags: AsyncTagsResource
184
+ """Tag endpoints exposed as ``client.tags``."""
185
+
186
+ webhooks: AsyncWebhooksResource
187
+ """Webhook endpoints exposed as ``client.webhooks``."""
188
+
189
+ users: AsyncUsersResource
190
+ """User endpoints exposed as ``client.users``."""
191
+
192
+ orgs: AsyncOrgsResource
193
+ """Organization endpoints exposed as ``client.orgs``."""
194
+
195
+ search: AsyncSearchResource
196
+ """Search endpoints exposed as ``client.search``."""
197
+
198
+ oauth: AsyncOAuthResource
199
+ """OAuth helper endpoints exposed as ``client.oauth``."""
200
+
201
+ def __init__(
202
+ self,
203
+ *,
204
+ api_key: str | None = None,
205
+ owner: str | None = None,
206
+ repo: str | None = None,
207
+ base_url: str = DEFAULT_BASE_URL,
208
+ timeout: float | None = None,
209
+ http_client: httpx.AsyncClient | None = None,
210
+ ) -> None:
211
+ """Create an asynchronous client and attach resource groups."""
212
+ super().__init__(
213
+ api_key=api_key,
214
+ owner=owner,
215
+ repo=repo,
216
+ base_url=base_url,
217
+ timeout=timeout,
218
+ http_client=http_client,
219
+ )
220
+ self.repos = AsyncReposResource(self)
221
+ self.contents = AsyncRepoContentsResource(self)
222
+ self.branches = AsyncBranchesResource(self)
223
+ self.commits = AsyncCommitsResource(self)
224
+ self.issues = AsyncIssuesResource(self)
225
+ self.pulls = AsyncPullsResource(self)
226
+ self.labels = AsyncLabelsResource(self)
227
+ self.milestones = AsyncMilestonesResource(self)
228
+ self.members = AsyncMembersResource(self)
229
+ self.releases = AsyncReleasesResource(self)
230
+ self.tags = AsyncTagsResource(self)
231
+ self.webhooks = AsyncWebhooksResource(self)
232
+ self.users = AsyncUsersResource(self)
233
+ self.orgs = AsyncOrgsResource(self)
234
+ self.search = AsyncSearchResource(self)
235
+ self.oauth = AsyncOAuthResource(self)
@@ -0,0 +1,39 @@
1
+ """Custom exceptions raised by the GitCode SDK."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ class GitCodeError(Exception):
7
+ """Base exception for all GitCode SDK errors."""
8
+
9
+
10
+ class GitCodeConfigurationError(GitCodeError):
11
+ """Raised when client configuration is incomplete or invalid."""
12
+
13
+
14
+ class GitCodeAPIError(GitCodeError):
15
+ """Raised when the GitCode API returns an error response.
16
+
17
+ :param message: Human-readable error message.
18
+ :param status_code: HTTP status code returned by the API.
19
+ :param request_id: Optional GitCode request identifier.
20
+ :param payload: Parsed error payload when available.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ message: str,
26
+ *,
27
+ status_code: int,
28
+ request_id: str | None = None,
29
+ payload: Any = None,
30
+ ) -> None:
31
+ """Store structured error metadata from a failed API response."""
32
+ super().__init__(message)
33
+ self.status_code = status_code
34
+ self.request_id = request_id
35
+ self.payload = payload
36
+
37
+
38
+ class GitCodeHTTPStatusError(GitCodeAPIError):
39
+ """Raised for non-success HTTP responses from the GitCode API."""