pygitcode 0.1.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,497 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+
6
+ import click
7
+
8
+ from ..cli_compat import (
9
+ get_body_from_options,
10
+ get_default_base_branch,
11
+ get_fill_info,
12
+ normalize_multi_values,
13
+ resolve_pr_identifier_or_current_branch,
14
+ )
15
+ from ..formatters import format_pr_detail, format_pr_list, output_result
16
+ from ..repo import resolve_repo
17
+ from ..services import PullRequestService
18
+ from ..utils import get_current_git_branch, open_in_browser, prompt_if_missing, resolve_pr_arg
19
+
20
+
21
+ @click.group("pr")
22
+ def pr_group() -> None:
23
+ pass
24
+
25
+
26
+ @pr_group.command("list")
27
+ @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
28
+ @click.option("-s", "--state")
29
+ @click.option("-A", "--author")
30
+ @click.option("-B", "--base")
31
+ @click.option("--assignee")
32
+ @click.option("--draft", is_flag=True, default=None)
33
+ @click.option("--head")
34
+ @click.option("-l", "--label", "labels", multiple=True)
35
+ @click.option("-S", "--search")
36
+ @click.option("-L", "--limit", type=int, default=30, show_default=True, help="Maximum number of items to fetch.")
37
+ @click.option("-w", "--web", is_flag=True, help="Open the pull requests list in the web browser.")
38
+ @click.option("--json", "json_fields", help="Output JSON. Optionally specify comma-separated fields.")
39
+ @click.option("-q", "--jq", "jq_query", help="Filter JSON output using a jq expression.")
40
+ @click.option("-t", "--template", help="Format output using a Go template string.")
41
+ @click.pass_context
42
+ def pr_list(
43
+ ctx: click.Context,
44
+ repo_name: str | None,
45
+ state: str | None,
46
+ author: str | None,
47
+ base: str | None,
48
+ assignee: str | None,
49
+ draft: bool | None,
50
+ head: str | None,
51
+ labels: tuple[str, ...] | None,
52
+ search: str | None,
53
+ limit: int | None,
54
+ web: bool,
55
+ json_fields: str | None,
56
+ jq_query: str | None,
57
+ template: str | None,
58
+ ) -> None:
59
+ app = ctx.obj["app"]
60
+ owner, repo = resolve_repo(repo_name or app.repo)
61
+ if web:
62
+ open_in_browser(f"https://gitcode.com/{owner}/{repo}/pulls")
63
+ return
64
+ service = PullRequestService(app.client())
65
+ labels_str = normalize_multi_values(labels)
66
+ items = service.list(
67
+ owner,
68
+ repo,
69
+ state=state,
70
+ author=author,
71
+ base=base,
72
+ assignee=assignee,
73
+ draft=draft,
74
+ head=head,
75
+ labels=labels_str,
76
+ search=search,
77
+ )
78
+ if limit is not None:
79
+ items = items[:limit]
80
+
81
+ def default_formatter(data):
82
+ output = format_pr_list(data)
83
+ if output:
84
+ click.echo(output)
85
+
86
+ output_result(
87
+ items,
88
+ json_fields,
89
+ jq_query,
90
+ template,
91
+ default_formatter=default_formatter,
92
+ )
93
+
94
+
95
+ @pr_group.command("view")
96
+ @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
97
+ @click.argument("identifier", required=False)
98
+ @click.option("-w", "--web", is_flag=True, help="Open the pull request in the web browser.")
99
+ @click.option("--json", "json_fields", help="Output JSON. Optionally specify comma-separated fields.")
100
+ @click.option("-q", "--jq", "jq_query", help="Filter JSON output using a jq expression.")
101
+ @click.option("-t", "--template", help="Format output using a Go template string.")
102
+ @click.pass_context
103
+ def pr_view(
104
+ ctx: click.Context,
105
+ repo_name: str | None,
106
+ identifier: str | None,
107
+ web: bool,
108
+ json_fields: str | None,
109
+ jq_query: str | None,
110
+ template: str | None,
111
+ ) -> None:
112
+ app = ctx.obj["app"]
113
+ owner, repo = resolve_repo(repo_name or app.repo)
114
+ service = PullRequestService(app.client())
115
+ resolved_identifier = resolve_pr_identifier_or_current_branch(identifier)
116
+ owner, repo, number = resolve_pr_arg(resolved_identifier, owner, repo, service)
117
+ number = int(number)
118
+ item = service.get(owner, repo, number)
119
+ if web:
120
+ open_in_browser(item["html_url"])
121
+ return
122
+ output_result(
123
+ item,
124
+ json_fields,
125
+ jq_query,
126
+ template,
127
+ default_formatter=lambda data: click.echo(format_pr_detail(data)),
128
+ )
129
+
130
+
131
+ @pr_group.command("create")
132
+ @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
133
+ @click.option("-t", "--title")
134
+ @click.option("-b", "--body")
135
+ @click.option("-F", "--body-file")
136
+ @click.option("--editor", is_flag=True)
137
+ @click.option("--fill", is_flag=True, help="Use commit info for title and body.")
138
+ @click.option("--fill-first", is_flag=True, help="Use first commit info for title and body.")
139
+ @click.option("--fill-verbose", is_flag=True, help="Use all commits for body description.")
140
+ @click.option("--dry-run", is_flag=True)
141
+ @click.option("-B", "--base")
142
+ @click.option("-H", "--head")
143
+ @click.option("-d", "--draft", is_flag=True)
144
+ @click.option("--milestone")
145
+ @click.option("-l", "--label", "labels", multiple=True)
146
+ @click.option("-r", "--reviewer", "reviewers", multiple=True)
147
+ @click.option("-a", "--assignee", "assignees", multiple=True)
148
+ @click.option("-w", "--web", is_flag=True, help="Open the pull request in the web browser.")
149
+ @click.option("--json", "json_fields", help="Output JSON. Optionally specify comma-separated fields.")
150
+ @click.option("-q", "--jq", "jq_query", help="Filter JSON output using a jq expression.")
151
+ @click.option("--template", help="Format output using a Go template string.")
152
+ @click.pass_context
153
+ def pr_create(
154
+ ctx: click.Context,
155
+ repo_name: str | None,
156
+ title: str | None,
157
+ body: str | None,
158
+ body_file: str | None,
159
+ editor: bool,
160
+ fill: bool,
161
+ fill_first: bool,
162
+ fill_verbose: bool,
163
+ dry_run: bool,
164
+ base: str | None,
165
+ head: str | None,
166
+ draft: bool,
167
+ milestone: str | None,
168
+ labels: tuple[str, ...] | None,
169
+ reviewers: tuple[str, ...] | None,
170
+ assignees: tuple[str, ...] | None,
171
+ web: bool,
172
+ json_fields: str | None,
173
+ jq_query: str | None,
174
+ template: str | None,
175
+ ) -> None:
176
+ app = ctx.obj["app"]
177
+ owner, repo = resolve_repo(repo_name or app.repo)
178
+ if web:
179
+ open_in_browser(f"https://gitcode.com/{owner}/{repo}/pulls/new")
180
+ return
181
+ if not head:
182
+ head = get_current_git_branch()
183
+ if not head:
184
+ raise click.ClickException("Unable to detect current branch. Use --head.")
185
+ if not base:
186
+ base = get_default_base_branch()
187
+
188
+ fill_mode = None
189
+ if fill_verbose:
190
+ fill_mode = "verbose"
191
+ elif fill_first:
192
+ fill_mode = "first"
193
+ elif fill:
194
+ fill_mode = "last"
195
+
196
+ if fill_mode:
197
+ fill_title, fill_body = get_fill_info(fill_mode)
198
+ if title is None:
199
+ title = fill_title
200
+ if body is None:
201
+ body = fill_body
202
+
203
+ title = prompt_if_missing(title, "Title")
204
+ body = get_body_from_options(body=body, body_file=body_file, editor=editor)
205
+ payload = {
206
+ "title": title,
207
+ "body": body,
208
+ "base": base,
209
+ "head": head,
210
+ "draft": draft,
211
+ "labels": normalize_multi_values(labels),
212
+ "assignees": normalize_multi_values(assignees),
213
+ "reviewers": normalize_multi_values(reviewers),
214
+ "milestone": milestone,
215
+ }
216
+ if dry_run:
217
+ click.echo(json.dumps({k: v for k, v in payload.items() if v is not None}, indent=2, sort_keys=True))
218
+ return
219
+ service = PullRequestService(app.client())
220
+ item = service.create(owner, repo, **payload)
221
+ output_result(
222
+ item,
223
+ json_fields,
224
+ jq_query,
225
+ template,
226
+ default_formatter=lambda data: click.echo(data["html_url"]),
227
+ )
228
+
229
+
230
+ @pr_group.command("close")
231
+ @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
232
+ @click.argument("identifier", required=False)
233
+ @click.option("-c", "--comment")
234
+ @click.option("-d", "--delete-branch", is_flag=True, help="Delete the remote branch after closing.")
235
+ @click.pass_context
236
+ def pr_close(
237
+ ctx: click.Context, repo_name: str | None, identifier: str | None, comment: str | None, delete_branch: bool
238
+ ) -> None:
239
+ app = ctx.obj["app"]
240
+ owner, repo = resolve_repo(repo_name or app.repo)
241
+ service = PullRequestService(app.client())
242
+ resolved_identifier = resolve_pr_identifier_or_current_branch(identifier)
243
+ owner, repo, number = resolve_pr_arg(resolved_identifier, owner, repo, service)
244
+ number = int(number)
245
+ if comment:
246
+ service.comment(owner, repo, number, body=comment)
247
+ item = service.update(owner, repo, number, state="closed")
248
+ if delete_branch:
249
+ try:
250
+ branch = item.get("head", {}).get("ref")
251
+ if branch:
252
+ subprocess.run(["git", "push", "origin", "--delete", branch], check=True)
253
+ click.echo(f"Deleted remote branch {branch}")
254
+ else:
255
+ click.echo("Warning: could not determine branch to delete.", err=True)
256
+ except Exception as exc:
257
+ click.echo(f"Warning: could not delete remote branch: {exc}", err=True)
258
+ click.echo(f"Closed pull request #{item['number']}")
259
+
260
+
261
+ @pr_group.command("merge")
262
+ @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
263
+ @click.argument("identifier", required=False)
264
+ @click.option("-m", "--merge", "merge_mode", flag_value="merge")
265
+ @click.option("-s", "--squash", "merge_mode", flag_value="squash")
266
+ @click.option("-r", "--rebase", "merge_mode", flag_value="rebase")
267
+ @click.pass_context
268
+ def pr_merge(ctx: click.Context, repo_name: str | None, identifier: str | None, merge_mode: str | None) -> None:
269
+ app = ctx.obj["app"]
270
+ owner, repo = resolve_repo(repo_name or app.repo)
271
+ service = PullRequestService(app.client())
272
+ resolved_identifier = resolve_pr_identifier_or_current_branch(identifier)
273
+ owner, repo, number = resolve_pr_arg(resolved_identifier, owner, repo, service)
274
+ number = int(number)
275
+ item = service.merge(owner, repo, number, merge_method=merge_mode or "merge")
276
+ click.echo(item["message"])
277
+
278
+
279
+ @pr_group.command("comment")
280
+ @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
281
+ @click.argument("identifier", required=False)
282
+ @click.option("-b", "--body")
283
+ @click.option("--path")
284
+ @click.option("--position", type=int)
285
+ @click.pass_context
286
+ def pr_comment(
287
+ ctx: click.Context,
288
+ repo_name: str | None,
289
+ identifier: str | None,
290
+ body: str | None,
291
+ path: str | None,
292
+ position: int | None,
293
+ ) -> None:
294
+ app = ctx.obj["app"]
295
+ owner, repo = resolve_repo(repo_name or app.repo)
296
+ service = PullRequestService(app.client())
297
+ resolved_identifier = resolve_pr_identifier_or_current_branch(identifier)
298
+ owner, repo, number = resolve_pr_arg(resolved_identifier, owner, repo, service)
299
+ number = int(number)
300
+ body = prompt_if_missing(body, "Body")
301
+ item = service.comment(owner, repo, number, body=body, path=path, position=position)
302
+ click.echo(str(item["id"]))
303
+
304
+
305
+ @pr_group.command("review")
306
+ @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
307
+ @click.argument("identifier", required=False)
308
+ @click.option("-a", "--approve", is_flag=True, help="Approve the pull request. GitCode maps this to its review API.")
309
+ @click.option("--body")
310
+ @click.option("--comment", is_flag=True, help="Leave a review comment. Downgrades to a PR comment on GitCode.")
311
+ @click.option("--request-changes", is_flag=True, help="Request changes. Downgrades to a PR comment on GitCode.")
312
+ @click.option("--force", is_flag=True, help="Force review handling when supported by GitCode.")
313
+ @click.pass_context
314
+ def pr_review(
315
+ ctx: click.Context,
316
+ repo_name: str | None,
317
+ identifier: str | None,
318
+ approve: bool,
319
+ body: str | None,
320
+ comment: bool,
321
+ request_changes: bool,
322
+ force: bool,
323
+ ) -> None:
324
+ app = ctx.obj["app"]
325
+ owner, repo = resolve_repo(repo_name or app.repo)
326
+ service = PullRequestService(app.client())
327
+ resolved_identifier = resolve_pr_identifier_or_current_branch(identifier)
328
+ owner, repo, number = resolve_pr_arg(resolved_identifier, owner, repo, service)
329
+ number = int(number)
330
+
331
+ selected_modes = [approve, comment, request_changes]
332
+ if sum(1 for selected in selected_modes if selected) != 1:
333
+ raise click.ClickException("Specify exactly one of --approve, --comment, or --request-changes.")
334
+
335
+ if comment or request_changes:
336
+ if not body:
337
+ raise click.ClickException("Body is required when using --comment or --request-changes.")
338
+ item = service.comment(owner, repo, number, body=body)
339
+ if comment:
340
+ click.echo("GitCode review API does not support comment reviews; posted a pull request comment instead.")
341
+ else:
342
+ click.echo(
343
+ "GitCode review API does not support request-changes reviews; posted a pull request comment instead."
344
+ )
345
+ click.echo(f"Posted pull request comment {item['id']}")
346
+ return
347
+
348
+ item = service.review(owner, repo, number, body=body, force=force)
349
+ click.echo(f"Reviewed pull request #{number}")
350
+
351
+
352
+ @pr_group.command("reopen")
353
+ @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
354
+ @click.argument("identifier", required=False)
355
+ @click.pass_context
356
+ def pr_reopen(ctx: click.Context, repo_name: str | None, identifier: str | None) -> None:
357
+ app = ctx.obj["app"]
358
+ owner, repo = resolve_repo(repo_name or app.repo)
359
+ service = PullRequestService(app.client())
360
+ resolved_identifier = resolve_pr_identifier_or_current_branch(identifier)
361
+ owner, repo, number = resolve_pr_arg(resolved_identifier, owner, repo, service)
362
+ number = int(number)
363
+ item = service.update(owner, repo, number, state="open")
364
+ click.echo(f"Reopened pull request #{item['number']}")
365
+
366
+
367
+ @pr_group.command("edit")
368
+ @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
369
+ @click.argument("identifier", required=False)
370
+ @click.option("-t", "--title")
371
+ @click.option("-b", "--body")
372
+ @click.option("-B", "--base")
373
+ @click.option("-a", "--add-assignee")
374
+ @click.option("-l", "--add-label")
375
+ @click.option("-r", "--add-reviewer")
376
+ @click.option("--remove-assignee")
377
+ @click.option("--remove-label")
378
+ @click.option("--remove-reviewer")
379
+ @click.pass_context
380
+ def pr_edit(
381
+ ctx: click.Context,
382
+ repo_name: str | None,
383
+ identifier: str | None,
384
+ title: str | None,
385
+ body: str | None,
386
+ base: str | None,
387
+ add_assignee: str | None,
388
+ add_label: str | None,
389
+ add_reviewer: str | None,
390
+ remove_assignee: str | None,
391
+ remove_label: str | None,
392
+ remove_reviewer: str | None,
393
+ ) -> None:
394
+ app = ctx.obj["app"]
395
+ owner, repo = resolve_repo(repo_name or app.repo)
396
+ service = PullRequestService(app.client())
397
+ resolved_identifier = resolve_pr_identifier_or_current_branch(identifier)
398
+ owner, repo, number = resolve_pr_arg(resolved_identifier, owner, repo, service)
399
+ number = int(number)
400
+ data = {
401
+ k: v
402
+ for k, v in {
403
+ "title": title,
404
+ "body": body,
405
+ "base": base,
406
+ "assignee": add_assignee,
407
+ "labels": add_label,
408
+ "reviewer": add_reviewer,
409
+ "unassignee": remove_assignee,
410
+ "unset_labels": remove_label,
411
+ "unset_reviewer": remove_reviewer,
412
+ }.items()
413
+ if v is not None
414
+ }
415
+ if not data:
416
+ raise click.UsageError("must specify at least one field to edit")
417
+ item = service.update(owner, repo, number, **data)
418
+ click.echo(f"Edited pull request #{item['number']}")
419
+
420
+
421
+ @pr_group.command("diff")
422
+ @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
423
+ @click.argument("identifier", required=False)
424
+ @click.pass_context
425
+ def pr_diff(ctx: click.Context, repo_name: str | None, identifier: str | None) -> None:
426
+ app = ctx.obj["app"]
427
+ owner, repo = resolve_repo(repo_name or app.repo)
428
+ service = PullRequestService(app.client())
429
+ resolved_identifier = resolve_pr_identifier_or_current_branch(identifier)
430
+ owner, repo, number = resolve_pr_arg(resolved_identifier, owner, repo, service)
431
+ diff_text = service.diff(owner, repo, int(number))
432
+ click.echo(diff_text)
433
+
434
+
435
+ @pr_group.command("checkout")
436
+ @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
437
+ @click.argument("identifier", required=False)
438
+ @click.option("-b", "--branch", help="Local branch name to checkout into.")
439
+ @click.pass_context
440
+ def pr_checkout(ctx: click.Context, repo_name: str | None, identifier: str | None, branch: str | None) -> None:
441
+ app = ctx.obj["app"]
442
+ owner, repo = resolve_repo(repo_name or app.repo)
443
+ service = PullRequestService(app.client())
444
+ resolved_identifier = resolve_pr_identifier_or_current_branch(identifier)
445
+ owner, repo, number = resolve_pr_arg(resolved_identifier, owner, repo, service)
446
+ item = service.get(owner, repo, int(number))
447
+ head_ref = item.get("head", {}).get("ref")
448
+ if not head_ref:
449
+ raise click.ClickException("Unable to determine PR branch.")
450
+ local_branch = branch or head_ref
451
+ try:
452
+ subprocess.run(["git", "fetch", "origin", head_ref], check=True)
453
+ subprocess.run(["git", "checkout", "-b", local_branch, f"origin/{head_ref}"], check=True)
454
+ click.echo(f"Checked out branch {local_branch}")
455
+ except subprocess.CalledProcessError as exc:
456
+ raise click.ClickException(f"Git checkout failed: {exc}") from exc
457
+
458
+
459
+ @pr_group.command("ready")
460
+ @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
461
+ @click.argument("identifier", required=False)
462
+ @click.option("--undo", is_flag=True, help="Convert a pull request to draft.")
463
+ @click.pass_context
464
+ def pr_ready(ctx: click.Context, repo_name: str | None, identifier: str | None, undo: bool) -> None:
465
+ app = ctx.obj["app"]
466
+ owner, repo = resolve_repo(repo_name or app.repo)
467
+ service = PullRequestService(app.client())
468
+ resolved_identifier = resolve_pr_identifier_or_current_branch(identifier)
469
+ owner, repo, number = resolve_pr_arg(resolved_identifier, owner, repo, service)
470
+ item = service.update(owner, repo, int(number), draft=undo)
471
+ if undo:
472
+ click.echo(f"Converted pull request #{item['number']} to draft")
473
+ else:
474
+ click.echo(f"Marked pull request #{item['number']} as ready for review")
475
+
476
+
477
+ @pr_group.command("status")
478
+ @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
479
+ @click.pass_context
480
+ def pr_status(ctx: click.Context, repo_name: str | None) -> None:
481
+ app = ctx.obj["app"]
482
+ owner, repo = resolve_repo(repo_name or app.repo)
483
+ service = PullRequestService(app.client())
484
+ items = service.list(owner, repo, state="open")
485
+ click.echo(
486
+ f"Open pull requests in {owner}/{repo} "
487
+ "(GitCode API approximation -- user-specific filtering is not available)"
488
+ )
489
+ if items:
490
+ for item in items:
491
+ click.echo(f" #{item['number']}\t{item['state']}\t{item['title']}")
492
+ else:
493
+ click.echo(" No open pull requests")
494
+
495
+
496
+ pr_group.add_command(pr_list, name="ls")
497
+ pr_group.add_command(pr_create, name="new")
gitcode_cli/config.py ADDED
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+
7
+ from .errors import ConfigError
8
+
9
+ CONFIG_DIR = Path.home() / ".config" / "gc"
10
+ CONFIG_PATH = CONFIG_DIR / "config.json"
11
+ TOKEN_ENV_VARS = ("GC_TOKEN",)
12
+
13
+
14
+ def load_config() -> dict:
15
+ if not CONFIG_PATH.exists():
16
+ return {}
17
+ try:
18
+ return json.loads(CONFIG_PATH.read_text())
19
+ except json.JSONDecodeError as exc:
20
+ raise ConfigError(f"Invalid config file: {CONFIG_PATH}") from exc
21
+
22
+
23
+ def save_config(data: dict) -> None:
24
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
25
+ CONFIG_PATH.write_text(json.dumps(data, indent=2) + "\n")
26
+
27
+
28
+ def get_token(explicit_token: str | None = None) -> str:
29
+ if explicit_token:
30
+ return explicit_token
31
+ for env_name in TOKEN_ENV_VARS:
32
+ value = os.getenv(env_name)
33
+ if value:
34
+ return value
35
+ config = load_config()
36
+ token = config.get("token")
37
+ if token:
38
+ return token
39
+ raise ConfigError("No token found. Set GC_TOKEN or run `gc auth login`.")
gitcode_cli/context.py ADDED
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .client import GitCodeClient
6
+
7
+
8
+ @dataclass
9
+ class AppContext:
10
+ token: str
11
+ repo: str | None
12
+
13
+ def client(self) -> GitCodeClient:
14
+ return GitCodeClient(token=self.token)
gitcode_cli/errors.py ADDED
@@ -0,0 +1,23 @@
1
+ from typing import Optional
2
+
3
+
4
+ class GCError(Exception):
5
+ pass
6
+
7
+
8
+ class ConfigError(GCError):
9
+ pass
10
+
11
+
12
+ class AuthError(GCError):
13
+ pass
14
+
15
+
16
+ class RepoResolutionError(GCError):
17
+ pass
18
+
19
+
20
+ class APIError(GCError):
21
+ def __init__(self, message: str, status_code: Optional[int] = None):
22
+ super().__init__(message)
23
+ self.status_code = status_code