logion-cli 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.
- cli/__init__.py +2 -0
- cli/_config.py +51 -0
- cli/_confirm.py +16 -0
- cli/_context.py +17 -0
- cli/_course_bundle.py +46 -0
- cli/_course_capabilities.py +580 -0
- cli/_credentials.py +104 -0
- cli/_errors.py +82 -0
- cli/_first_run.py +90 -0
- cli/_harness/__init__.py +68 -0
- cli/_harness/base.py +106 -0
- cli/_harness/claude_code.py +168 -0
- cli/_harness/codex.py +79 -0
- cli/_harness/custom.py +55 -0
- cli/_harness/hermes.py +93 -0
- cli/_harness/opencode.py +255 -0
- cli/_local_state.py +1053 -0
- cli/_options.py +36 -0
- cli/_output.py +47 -0
- cli/_parser.py +73 -0
- cli/_recall_calibration.py +90 -0
- cli/_recall_ranker.py +74 -0
- cli/_taxonomy.py +120 -0
- cli/_update_policy.py +152 -0
- cli/_utils.py +16 -0
- cli/_version.py +26 -0
- cli/commands/__init__.py +2 -0
- cli/commands/admin.py +535 -0
- cli/commands/bounties.py +490 -0
- cli/commands/course_reviews/__init__.py +6 -0
- cli/commands/course_reviews/_download_handler.py +104 -0
- cli/commands/course_reviews/_render.py +129 -0
- cli/commands/course_reviews/handlers.py +197 -0
- cli/commands/course_reviews/parser.py +93 -0
- cli/commands/courses/__init__.py +6 -0
- cli/commands/courses/_capability_render.py +183 -0
- cli/commands/courses/_cmd_help.py +18 -0
- cli/commands/courses/_purchase.py +76 -0
- cli/commands/courses/_review_helpers.py +93 -0
- cli/commands/courses/_taxonomy_data.py +173 -0
- cli/commands/courses/_upload_bundle_validation.py +28 -0
- cli/commands/courses/_uploads_push.py +243 -0
- cli/commands/courses/capabilities.py +250 -0
- cli/commands/courses/capability_frontmatter.py +150 -0
- cli/commands/courses/handlers.py +50 -0
- cli/commands/courses/mutations.py +217 -0
- cli/commands/courses/parser.py +66 -0
- cli/commands/courses/parser_capabilities.py +95 -0
- cli/commands/courses/parser_sections.py +239 -0
- cli/commands/courses/parser_uploads.py +84 -0
- cli/commands/courses/parser_utils.py +65 -0
- cli/commands/courses/publication.py +60 -0
- cli/commands/courses/report_usage.py +131 -0
- cli/commands/courses/reviews.py +237 -0
- cli/commands/courses/taxonomy_handler.py +61 -0
- cli/commands/courses/taxonomy_suggest.py +197 -0
- cli/commands/courses/uploads.py +142 -0
- cli/commands/courses/versions.py +65 -0
- cli/commands/credits/__init__.py +6 -0
- cli/commands/credits/_helpers.py +153 -0
- cli/commands/credits/handlers.py +218 -0
- cli/commands/credits/parser.py +115 -0
- cli/commands/docs/__init__.py +6 -0
- cli/commands/docs/handlers.py +137 -0
- cli/commands/docs/parser.py +27 -0
- cli/commands/health/__init__.py +6 -0
- cli/commands/health/handlers.py +26 -0
- cli/commands/health/parser.py +20 -0
- cli/commands/identity/__init__.py +6 -0
- cli/commands/identity/_autopost.py +97 -0
- cli/commands/identity/_closing_copy.py +89 -0
- cli/commands/identity/_companion.py +232 -0
- cli/commands/identity/_companion_source.py +135 -0
- cli/commands/identity/_harness_select.py +85 -0
- cli/commands/identity/_onboarding_helpers.py +168 -0
- cli/commands/identity/handlers.py +173 -0
- cli/commands/identity/onboarding.py +246 -0
- cli/commands/identity/parser.py +72 -0
- cli/commands/listings/__init__.py +6 -0
- cli/commands/listings/handlers.py +135 -0
- cli/commands/listings/parser.py +57 -0
- cli/commands/notifications/__init__.py +6 -0
- cli/commands/notifications/handlers.py +120 -0
- cli/commands/notifications/parser.py +49 -0
- cli/commands/payments/__init__.py +6 -0
- cli/commands/payments/_orders_helpers.py +114 -0
- cli/commands/payments/handlers.py +138 -0
- cli/commands/payments/parser.py +97 -0
- cli/commands/recall/__init__.py +7 -0
- cli/commands/recall/handlers.py +87 -0
- cli/commands/recall/parser.py +70 -0
- cli/commands/referrals/__init__.py +6 -0
- cli/commands/referrals/_helpers.py +63 -0
- cli/commands/referrals/handlers.py +100 -0
- cli/commands/referrals/parser.py +65 -0
- cli/commands/reports/__init__.py +6 -0
- cli/commands/reports/handlers.py +57 -0
- cli/commands/reports/parser.py +52 -0
- cli/commands/skills/__init__.py +7 -0
- cli/commands/skills/_agent_symlink.py +161 -0
- cli/commands/skills/_finalize.py +112 -0
- cli/commands/skills/_inspect_handler.py +218 -0
- cli/commands/skills/_install_helpers.py +186 -0
- cli/commands/skills/_query_handlers.py +83 -0
- cli/commands/skills/_search_handler.py +136 -0
- cli/commands/skills/_update_handler.py +110 -0
- cli/commands/skills/_verify_handler.py +109 -0
- cli/commands/skills/handlers.py +202 -0
- cli/commands/skills/parser.py +154 -0
- cli/commands/workspace.py +406 -0
- cli/docs/README.md +5 -0
- cli/docs/__init__.py +1 -0
- cli/docs/bounties-and-referrals.md +18 -0
- cli/docs/concepts.md +47 -0
- cli/docs/creating-courses.md +25 -0
- cli/docs/credits-and-purchases.md +30 -0
- cli/docs/credits-terms.md +23 -0
- cli/docs/getting-started.md +95 -0
- cli/docs/marketplace-loop.md +108 -0
- cli/docs/privacy.md +30 -0
- cli/docs/referral-terms.md +24 -0
- cli/docs/reviews.md +47 -0
- cli/docs/safety.md +28 -0
- cli/docs/terms.md +54 -0
- cli/main.py +84 -0
- cli/templates/__init__.py +2 -0
- cli/templates/course_capabilities.template.yaml +189 -0
- cli/templates/course_license_apache-2.0.template.txt +30 -0
- cli/templates/course_license_logion-standard-course-v1.template.txt +49 -0
- cli/templates/course_license_mit.template.txt +21 -0
- logion_cli-0.1.0.dist-info/METADATA +49 -0
- logion_cli-0.1.0.dist-info/RECORD +135 -0
- logion_cli-0.1.0.dist-info/WHEEL +4 -0
- logion_cli-0.1.0.dist-info/entry_points.txt +4 -0
- logion_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
cli/commands/bounties.py
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Bounties commands — create, list, get, lifecycle, submissions."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from cli._config import resolve_config_from_args
|
|
12
|
+
from cli._confirm import require_yes
|
|
13
|
+
from cli._context import make_client
|
|
14
|
+
from cli._errors import (
|
|
15
|
+
handle_error,
|
|
16
|
+
print_err,
|
|
17
|
+
validate_uuid_id,
|
|
18
|
+
)
|
|
19
|
+
from cli._options import COMMON_PARSER
|
|
20
|
+
from cli._output import emit, emit_json, to_data
|
|
21
|
+
from cli._utils import only_not_none
|
|
22
|
+
from cli.commands import workspace as _workspace
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def parse_datetime(value: str | None) -> datetime | None:
|
|
26
|
+
"""Parse an ISO-8601 datetime string, treating trailing Z as UTC."""
|
|
27
|
+
if value is None:
|
|
28
|
+
return None
|
|
29
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_evidence(path: Path | None) -> dict[str, object] | None:
|
|
33
|
+
"""Load a JSON evidence file, returning None if *path* is None.
|
|
34
|
+
|
|
35
|
+
Returns ``None`` and prints a user-facing error when the file
|
|
36
|
+
is missing or contains invalid JSON.
|
|
37
|
+
"""
|
|
38
|
+
if path is None:
|
|
39
|
+
return None
|
|
40
|
+
try:
|
|
41
|
+
return json.loads(path.read_text()) # type: ignore[no-any-return]
|
|
42
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
43
|
+
print_err(f"Error: evidence JSON must be valid: {exc}")
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def register(subparsers: argparse._SubParsersAction) -> None:
|
|
48
|
+
"""Register the ``bounties`` subcommand group."""
|
|
49
|
+
parser = subparsers.add_parser(
|
|
50
|
+
"bounties",
|
|
51
|
+
help="Manage bounties",
|
|
52
|
+
)
|
|
53
|
+
sub = parser.add_subparsers(
|
|
54
|
+
dest="bounties_command",
|
|
55
|
+
required=True,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# ── create ──────────────────────────────────────────────────
|
|
59
|
+
create = sub.add_parser(
|
|
60
|
+
"create",
|
|
61
|
+
help="Create a new bounty",
|
|
62
|
+
parents=[COMMON_PARSER],
|
|
63
|
+
)
|
|
64
|
+
create.add_argument("--course-id", required=True)
|
|
65
|
+
create.add_argument("--title", required=True)
|
|
66
|
+
create.add_argument("--description", required=True)
|
|
67
|
+
create.add_argument("--reward-cents", required=True, type=int)
|
|
68
|
+
create.add_argument("--currency")
|
|
69
|
+
create.add_argument("--submission-deadline")
|
|
70
|
+
create.set_defaults(handler=handle_create)
|
|
71
|
+
|
|
72
|
+
# ── list ────────────────────────────────────────────────────
|
|
73
|
+
ls = sub.add_parser(
|
|
74
|
+
"list",
|
|
75
|
+
help="List bounties",
|
|
76
|
+
parents=[COMMON_PARSER],
|
|
77
|
+
)
|
|
78
|
+
ls.add_argument("--scope", choices=["mine", "open", "funded"])
|
|
79
|
+
ls.set_defaults(handler=handle_list)
|
|
80
|
+
|
|
81
|
+
# ── get ──────────────────────────────────────────────────────
|
|
82
|
+
get = sub.add_parser(
|
|
83
|
+
"get",
|
|
84
|
+
help="Get bounty details",
|
|
85
|
+
parents=[COMMON_PARSER],
|
|
86
|
+
)
|
|
87
|
+
get.add_argument("bounty_id", metavar="BOUNTY_ID")
|
|
88
|
+
get.set_defaults(handler=handle_get)
|
|
89
|
+
|
|
90
|
+
# ── lifecycle commands (open / fund / cancel) ───────────────
|
|
91
|
+
# `logion bounties payout` is gone — accept accrues a
|
|
92
|
+
# creator-payable balance directly, and contributors cash out via
|
|
93
|
+
# `logion payments cash-out`. No separate payout step.
|
|
94
|
+
for cmd, sdk_method, action in [
|
|
95
|
+
("open", "update_status", "open this bounty"),
|
|
96
|
+
(
|
|
97
|
+
"fund",
|
|
98
|
+
"update_funding",
|
|
99
|
+
"fund this bounty (credits will be debited)",
|
|
100
|
+
),
|
|
101
|
+
("cancel", "delete", "cancel this bounty"),
|
|
102
|
+
]:
|
|
103
|
+
p = sub.add_parser(
|
|
104
|
+
cmd,
|
|
105
|
+
help=f"{cmd.capitalize()} a bounty",
|
|
106
|
+
parents=[COMMON_PARSER],
|
|
107
|
+
)
|
|
108
|
+
p.add_argument("bounty_id", metavar="BOUNTY_ID")
|
|
109
|
+
p.add_argument("--yes", action="store_true")
|
|
110
|
+
handler = _make_lifecycle_handler(cmd, sdk_method, action)
|
|
111
|
+
p.set_defaults(handler=handler)
|
|
112
|
+
|
|
113
|
+
# ── submissions sub-group ────────────────────────────────────
|
|
114
|
+
submissions = sub.add_parser(
|
|
115
|
+
"submissions",
|
|
116
|
+
help="Manage bounty submissions",
|
|
117
|
+
)
|
|
118
|
+
sub_sub = submissions.add_subparsers(
|
|
119
|
+
dest="bounties_submissions_command",
|
|
120
|
+
required=True,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# submissions create
|
|
124
|
+
sc = sub_sub.add_parser(
|
|
125
|
+
"create",
|
|
126
|
+
help="Create a submission for a bounty",
|
|
127
|
+
parents=[COMMON_PARSER],
|
|
128
|
+
)
|
|
129
|
+
sc.add_argument("bounty_id", metavar="BOUNTY_ID")
|
|
130
|
+
sc.add_argument("--title", required=True)
|
|
131
|
+
sc.add_argument("--description")
|
|
132
|
+
sc.add_argument("--evidence-json", type=Path, metavar="PATH")
|
|
133
|
+
sc.add_argument("--proposed-course-version-id")
|
|
134
|
+
sc.set_defaults(handler=handle_submissions_create)
|
|
135
|
+
|
|
136
|
+
# submissions list
|
|
137
|
+
sl = sub_sub.add_parser(
|
|
138
|
+
"list",
|
|
139
|
+
help="List submissions for a bounty",
|
|
140
|
+
parents=[COMMON_PARSER],
|
|
141
|
+
)
|
|
142
|
+
sl.add_argument("bounty_id", metavar="BOUNTY_ID")
|
|
143
|
+
sl.set_defaults(handler=handle_submissions_list)
|
|
144
|
+
|
|
145
|
+
# submissions get
|
|
146
|
+
sg = sub_sub.add_parser(
|
|
147
|
+
"get",
|
|
148
|
+
help="Get submission details",
|
|
149
|
+
parents=[COMMON_PARSER],
|
|
150
|
+
)
|
|
151
|
+
sg.add_argument("bounty_id", metavar="BOUNTY_ID")
|
|
152
|
+
sg.add_argument("submission_id", metavar="SUBMISSION_ID")
|
|
153
|
+
sg.set_defaults(handler=handle_submissions_get)
|
|
154
|
+
|
|
155
|
+
# submissions accept
|
|
156
|
+
sa = sub_sub.add_parser(
|
|
157
|
+
"accept",
|
|
158
|
+
help="Accept a submission",
|
|
159
|
+
parents=[COMMON_PARSER],
|
|
160
|
+
)
|
|
161
|
+
sa.add_argument("bounty_id", metavar="BOUNTY_ID")
|
|
162
|
+
sa.add_argument("submission_id", metavar="SUBMISSION_ID")
|
|
163
|
+
sa.add_argument("--yes", action="store_true")
|
|
164
|
+
sa.set_defaults(handler=handle_submissions_accept)
|
|
165
|
+
|
|
166
|
+
# submissions reject
|
|
167
|
+
sr = sub_sub.add_parser(
|
|
168
|
+
"reject",
|
|
169
|
+
help="Reject a submission",
|
|
170
|
+
parents=[COMMON_PARSER],
|
|
171
|
+
)
|
|
172
|
+
sr.add_argument("bounty_id", metavar="BOUNTY_ID")
|
|
173
|
+
sr.add_argument("submission_id", metavar="SUBMISSION_ID")
|
|
174
|
+
sr.add_argument("--yes", action="store_true")
|
|
175
|
+
sr.set_defaults(handler=handle_submissions_reject)
|
|
176
|
+
|
|
177
|
+
# submissions withdraw
|
|
178
|
+
sw = sub_sub.add_parser(
|
|
179
|
+
"withdraw",
|
|
180
|
+
help="Withdraw a submission",
|
|
181
|
+
parents=[COMMON_PARSER],
|
|
182
|
+
)
|
|
183
|
+
sw.add_argument("bounty_id", metavar="BOUNTY_ID")
|
|
184
|
+
sw.add_argument("submission_id", metavar="SUBMISSION_ID")
|
|
185
|
+
sw.add_argument("--yes", action="store_true")
|
|
186
|
+
sw.set_defaults(handler=handle_submissions_withdraw)
|
|
187
|
+
|
|
188
|
+
# ── workspace sub-group ──────────────────────────────────────
|
|
189
|
+
_workspace.register(sub)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ── Lifecycle handler factory ────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _make_lifecycle_handler(cmd: str, sdk_method: str, action: str):
|
|
196
|
+
"""Return a handler function for a bounty lifecycle command."""
|
|
197
|
+
|
|
198
|
+
def handler(args: argparse.Namespace) -> int:
|
|
199
|
+
bad_id = validate_uuid_id(args.bounty_id, "BOUNTY_ID")
|
|
200
|
+
if bad_id is not None:
|
|
201
|
+
return bad_id
|
|
202
|
+
refusal = require_yes(args.yes, action)
|
|
203
|
+
if refusal is not None:
|
|
204
|
+
return refusal
|
|
205
|
+
config = resolve_config_from_args(args)
|
|
206
|
+
client = make_client(config)
|
|
207
|
+
try:
|
|
208
|
+
method = getattr(client.v1.bounties, sdk_method)
|
|
209
|
+
result = method(bounty_id=args.bounty_id)
|
|
210
|
+
emit(result, json_output=config.json_output)
|
|
211
|
+
except Exception as exc:
|
|
212
|
+
return handle_error(exc)
|
|
213
|
+
else:
|
|
214
|
+
return 0
|
|
215
|
+
finally:
|
|
216
|
+
client.close()
|
|
217
|
+
|
|
218
|
+
handler.__name__ = f"handle_{cmd}"
|
|
219
|
+
handler.__doc__ = f"Execute the bounties {cmd} command."
|
|
220
|
+
return handler
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ── Handlers ──────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def handle_create(args: argparse.Namespace) -> int:
|
|
227
|
+
"""Execute the bounties create command."""
|
|
228
|
+
bad_id = validate_uuid_id(args.course_id, "--course-id")
|
|
229
|
+
if bad_id is not None:
|
|
230
|
+
return bad_id
|
|
231
|
+
# Validate --submission-deadline format early
|
|
232
|
+
if args.submission_deadline is not None:
|
|
233
|
+
try:
|
|
234
|
+
parse_datetime(args.submission_deadline)
|
|
235
|
+
except (ValueError, TypeError) as exc:
|
|
236
|
+
print_err(f"Error: --submission-deadline: {exc}")
|
|
237
|
+
return 2
|
|
238
|
+
config = resolve_config_from_args(args)
|
|
239
|
+
client = make_client(config)
|
|
240
|
+
try:
|
|
241
|
+
kwargs = only_not_none(
|
|
242
|
+
{
|
|
243
|
+
"course_id": args.course_id,
|
|
244
|
+
"title": args.title,
|
|
245
|
+
"description": args.description,
|
|
246
|
+
"reward_amount_cents": args.reward_cents,
|
|
247
|
+
},
|
|
248
|
+
currency=args.currency,
|
|
249
|
+
submission_deadline=parse_datetime(args.submission_deadline),
|
|
250
|
+
)
|
|
251
|
+
result = client.v1.bounties.create(**kwargs)
|
|
252
|
+
emit(result, json_output=config.json_output)
|
|
253
|
+
except Exception as exc:
|
|
254
|
+
return handle_error(exc)
|
|
255
|
+
else:
|
|
256
|
+
return 0
|
|
257
|
+
finally:
|
|
258
|
+
client.close()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def handle_list(args: argparse.Namespace) -> int:
|
|
262
|
+
"""Execute the bounties list command."""
|
|
263
|
+
config = resolve_config_from_args(args)
|
|
264
|
+
client = make_client(config)
|
|
265
|
+
try:
|
|
266
|
+
kwargs = only_not_none(
|
|
267
|
+
{},
|
|
268
|
+
scope=args.scope,
|
|
269
|
+
)
|
|
270
|
+
result = client.v1.bounties.list(**kwargs)
|
|
271
|
+
if config.json_output:
|
|
272
|
+
data = (
|
|
273
|
+
result.model_dump(mode="json")
|
|
274
|
+
if hasattr(result, "model_dump")
|
|
275
|
+
else to_data(result)
|
|
276
|
+
)
|
|
277
|
+
emit_json("logion.bounties.list", data)
|
|
278
|
+
else:
|
|
279
|
+
emit(result, json_output=False)
|
|
280
|
+
except Exception as exc:
|
|
281
|
+
return handle_error(exc)
|
|
282
|
+
else:
|
|
283
|
+
return 0
|
|
284
|
+
finally:
|
|
285
|
+
client.close()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def handle_get(args: argparse.Namespace) -> int:
|
|
289
|
+
"""Execute the bounties get command."""
|
|
290
|
+
bad_id = validate_uuid_id(args.bounty_id, "BOUNTY_ID")
|
|
291
|
+
if bad_id is not None:
|
|
292
|
+
return bad_id
|
|
293
|
+
config = resolve_config_from_args(args)
|
|
294
|
+
client = make_client(config)
|
|
295
|
+
try:
|
|
296
|
+
result = client.v1.bounties.get(bounty_id=args.bounty_id)
|
|
297
|
+
if config.json_output:
|
|
298
|
+
data = (
|
|
299
|
+
result.model_dump(mode="json")
|
|
300
|
+
if hasattr(result, "model_dump")
|
|
301
|
+
else to_data(result)
|
|
302
|
+
)
|
|
303
|
+
emit_json("logion.bounties.get", data)
|
|
304
|
+
else:
|
|
305
|
+
emit(result, json_output=False)
|
|
306
|
+
except Exception as exc:
|
|
307
|
+
return handle_error(exc)
|
|
308
|
+
else:
|
|
309
|
+
return 0
|
|
310
|
+
finally:
|
|
311
|
+
client.close()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ── Submission handlers ──────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def handle_submissions_create(args: argparse.Namespace) -> int:
|
|
318
|
+
"""Execute the bounties submissions create command."""
|
|
319
|
+
bad_id = validate_uuid_id(args.bounty_id, "BOUNTY_ID")
|
|
320
|
+
if bad_id is not None:
|
|
321
|
+
return bad_id
|
|
322
|
+
if args.proposed_course_version_id is not None:
|
|
323
|
+
bad_id = validate_uuid_id(
|
|
324
|
+
args.proposed_course_version_id,
|
|
325
|
+
"--proposed-course-version-id",
|
|
326
|
+
)
|
|
327
|
+
if bad_id is not None:
|
|
328
|
+
return bad_id
|
|
329
|
+
evidence = load_evidence(args.evidence_json)
|
|
330
|
+
if args.evidence_json is not None and evidence is None:
|
|
331
|
+
return 2
|
|
332
|
+
config = resolve_config_from_args(args)
|
|
333
|
+
client = make_client(config)
|
|
334
|
+
try:
|
|
335
|
+
kwargs = only_not_none(
|
|
336
|
+
{
|
|
337
|
+
"bounty_id": args.bounty_id,
|
|
338
|
+
"title": args.title,
|
|
339
|
+
},
|
|
340
|
+
description=args.description,
|
|
341
|
+
evidence=evidence,
|
|
342
|
+
proposed_course_version_id=args.proposed_course_version_id,
|
|
343
|
+
)
|
|
344
|
+
result = client.v1.bounties.create_submission(**kwargs)
|
|
345
|
+
emit(result, json_output=config.json_output)
|
|
346
|
+
except Exception as exc:
|
|
347
|
+
return handle_error(exc)
|
|
348
|
+
else:
|
|
349
|
+
return 0
|
|
350
|
+
finally:
|
|
351
|
+
client.close()
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def handle_submissions_list(args: argparse.Namespace) -> int:
|
|
355
|
+
"""Execute the bounties submissions list command."""
|
|
356
|
+
bad_id = validate_uuid_id(args.bounty_id, "BOUNTY_ID")
|
|
357
|
+
if bad_id is not None:
|
|
358
|
+
return bad_id
|
|
359
|
+
config = resolve_config_from_args(args)
|
|
360
|
+
client = make_client(config)
|
|
361
|
+
try:
|
|
362
|
+
result = client.v1.bounties.list_submissions(bounty_id=args.bounty_id)
|
|
363
|
+
if config.json_output:
|
|
364
|
+
data = (
|
|
365
|
+
result.model_dump(mode="json")
|
|
366
|
+
if hasattr(result, "model_dump")
|
|
367
|
+
else to_data(result)
|
|
368
|
+
)
|
|
369
|
+
emit_json("logion.bounties.submissions.list", data)
|
|
370
|
+
else:
|
|
371
|
+
emit(result, json_output=False)
|
|
372
|
+
except Exception as exc:
|
|
373
|
+
return handle_error(exc)
|
|
374
|
+
else:
|
|
375
|
+
return 0
|
|
376
|
+
finally:
|
|
377
|
+
client.close()
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def handle_submissions_get(args: argparse.Namespace) -> int:
|
|
381
|
+
"""Execute the bounties submissions get command."""
|
|
382
|
+
bad_id = validate_uuid_id(args.bounty_id, "BOUNTY_ID")
|
|
383
|
+
if bad_id is not None:
|
|
384
|
+
return bad_id
|
|
385
|
+
bad_id = validate_uuid_id(args.submission_id, "SUBMISSION_ID")
|
|
386
|
+
if bad_id is not None:
|
|
387
|
+
return bad_id
|
|
388
|
+
config = resolve_config_from_args(args)
|
|
389
|
+
client = make_client(config)
|
|
390
|
+
try:
|
|
391
|
+
result = client.v1.bounties.get_submission(
|
|
392
|
+
bounty_id=args.bounty_id,
|
|
393
|
+
submission_id=args.submission_id,
|
|
394
|
+
)
|
|
395
|
+
if config.json_output:
|
|
396
|
+
data = (
|
|
397
|
+
result.model_dump(mode="json")
|
|
398
|
+
if hasattr(result, "model_dump")
|
|
399
|
+
else to_data(result)
|
|
400
|
+
)
|
|
401
|
+
emit_json("logion.bounties.submissions.get", data)
|
|
402
|
+
else:
|
|
403
|
+
emit(result, json_output=False)
|
|
404
|
+
except Exception as exc:
|
|
405
|
+
return handle_error(exc)
|
|
406
|
+
else:
|
|
407
|
+
return 0
|
|
408
|
+
finally:
|
|
409
|
+
client.close()
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def handle_submissions_accept(args: argparse.Namespace) -> int:
|
|
413
|
+
"""Execute the bounties submissions accept command."""
|
|
414
|
+
bad_id = validate_uuid_id(args.bounty_id, "BOUNTY_ID")
|
|
415
|
+
if bad_id is not None:
|
|
416
|
+
return bad_id
|
|
417
|
+
bad_id = validate_uuid_id(args.submission_id, "SUBMISSION_ID")
|
|
418
|
+
if bad_id is not None:
|
|
419
|
+
return bad_id
|
|
420
|
+
refusal = require_yes(args.yes, "accept this submission")
|
|
421
|
+
if refusal is not None:
|
|
422
|
+
return refusal
|
|
423
|
+
config = resolve_config_from_args(args)
|
|
424
|
+
client = make_client(config)
|
|
425
|
+
try:
|
|
426
|
+
result = client.v1.bounties.accept_submission(
|
|
427
|
+
bounty_id=args.bounty_id,
|
|
428
|
+
submission_id=args.submission_id,
|
|
429
|
+
)
|
|
430
|
+
emit(result, json_output=config.json_output)
|
|
431
|
+
except Exception as exc:
|
|
432
|
+
return handle_error(exc)
|
|
433
|
+
else:
|
|
434
|
+
return 0
|
|
435
|
+
finally:
|
|
436
|
+
client.close()
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def handle_submissions_reject(args: argparse.Namespace) -> int:
|
|
440
|
+
"""Execute the bounties submissions reject command."""
|
|
441
|
+
bad_id = validate_uuid_id(args.bounty_id, "BOUNTY_ID")
|
|
442
|
+
if bad_id is not None:
|
|
443
|
+
return bad_id
|
|
444
|
+
bad_id = validate_uuid_id(args.submission_id, "SUBMISSION_ID")
|
|
445
|
+
if bad_id is not None:
|
|
446
|
+
return bad_id
|
|
447
|
+
refusal = require_yes(args.yes, "reject this submission")
|
|
448
|
+
if refusal is not None:
|
|
449
|
+
return refusal
|
|
450
|
+
config = resolve_config_from_args(args)
|
|
451
|
+
client = make_client(config)
|
|
452
|
+
try:
|
|
453
|
+
result = client.v1.bounties.reject_submission(
|
|
454
|
+
bounty_id=args.bounty_id,
|
|
455
|
+
submission_id=args.submission_id,
|
|
456
|
+
)
|
|
457
|
+
emit(result, json_output=config.json_output)
|
|
458
|
+
except Exception as exc:
|
|
459
|
+
return handle_error(exc)
|
|
460
|
+
else:
|
|
461
|
+
return 0
|
|
462
|
+
finally:
|
|
463
|
+
client.close()
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def handle_submissions_withdraw(args: argparse.Namespace) -> int:
|
|
467
|
+
"""Execute the bounties submissions withdraw command."""
|
|
468
|
+
bad_id = validate_uuid_id(args.bounty_id, "BOUNTY_ID")
|
|
469
|
+
if bad_id is not None:
|
|
470
|
+
return bad_id
|
|
471
|
+
bad_id = validate_uuid_id(args.submission_id, "SUBMISSION_ID")
|
|
472
|
+
if bad_id is not None:
|
|
473
|
+
return bad_id
|
|
474
|
+
refusal = require_yes(args.yes, "withdraw this submission")
|
|
475
|
+
if refusal is not None:
|
|
476
|
+
return refusal
|
|
477
|
+
config = resolve_config_from_args(args)
|
|
478
|
+
client = make_client(config)
|
|
479
|
+
try:
|
|
480
|
+
result = client.v1.bounties.delete_submission(
|
|
481
|
+
bounty_id=args.bounty_id,
|
|
482
|
+
submission_id=args.submission_id,
|
|
483
|
+
)
|
|
484
|
+
emit(result, json_output=config.json_output)
|
|
485
|
+
except Exception as exc:
|
|
486
|
+
return handle_error(exc)
|
|
487
|
+
else:
|
|
488
|
+
return 0
|
|
489
|
+
finally:
|
|
490
|
+
client.close()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""``course-reviews download`` — fetch the bundle under review.
|
|
3
|
+
|
|
4
|
+
Fetches the bundle manifest through the typed SDK
|
|
5
|
+
(``client.v1.course_reviews.get_bundle``), then streams each file from
|
|
6
|
+
its presigned URL and reconstructs the bundle's directory tree under
|
|
7
|
+
TARGET so the reviewer can read SKILL.md and references before
|
|
8
|
+
approve/reject.
|
|
9
|
+
|
|
10
|
+
The manifest call goes through the SDK like every other API call. The
|
|
11
|
+
per-file downloads hit short-lived presigned object-storage URLs
|
|
12
|
+
(carried in the manifest), which have no SDK wrapper — those use httpx
|
|
13
|
+
directly and are allowlisted in ``scripts/check_cli_http.lock``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import httpx
|
|
24
|
+
|
|
25
|
+
from cli._config import resolve_config_from_args
|
|
26
|
+
from cli._context import make_client
|
|
27
|
+
from cli._errors import handle_error, print_err, validate_uuid_id
|
|
28
|
+
from cli._output import emit
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def handle_download(args: argparse.Namespace) -> int:
|
|
32
|
+
"""Execute course-reviews download — fetch bundle for review."""
|
|
33
|
+
bad_id = validate_uuid_id(args.review_id, "REVIEW_ID")
|
|
34
|
+
if bad_id is not None:
|
|
35
|
+
return bad_id
|
|
36
|
+
|
|
37
|
+
config = resolve_config_from_args(args)
|
|
38
|
+
target = Path(
|
|
39
|
+
args.target if args.target else f"./review-bundles/{args.review_id}"
|
|
40
|
+
).resolve()
|
|
41
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
client = make_client(config)
|
|
44
|
+
try:
|
|
45
|
+
bundle = client.v1.course_reviews.get_bundle(review_id=args.review_id)
|
|
46
|
+
except Exception as exc:
|
|
47
|
+
return handle_error(exc)
|
|
48
|
+
finally:
|
|
49
|
+
client.close()
|
|
50
|
+
|
|
51
|
+
files = bundle.files
|
|
52
|
+
if not files:
|
|
53
|
+
print_err("Error: review has no assets")
|
|
54
|
+
return 1
|
|
55
|
+
|
|
56
|
+
# Presigned object-storage URLs (download_url) are external and have
|
|
57
|
+
# no SDK wrapper — stream them directly. See scripts/check_cli_http.lock.
|
|
58
|
+
try:
|
|
59
|
+
with httpx.Client(timeout=config.timeout) as http:
|
|
60
|
+
rc = _download_files(http, files, target)
|
|
61
|
+
if rc != 0:
|
|
62
|
+
return rc
|
|
63
|
+
except Exception as exc:
|
|
64
|
+
return handle_error(exc)
|
|
65
|
+
|
|
66
|
+
_emit_result(bundle, files, target, json_output=config.json_output)
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _download_files(http: httpx.Client, files: list[Any], target: Path) -> int:
|
|
71
|
+
for f in files:
|
|
72
|
+
filename = f.filename
|
|
73
|
+
dest = target / filename
|
|
74
|
+
if not dest.resolve().is_relative_to(target):
|
|
75
|
+
print_err(f"Error: refusing path escape: {filename}")
|
|
76
|
+
return 1
|
|
77
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
with http.stream("GET", f.download_url) as r:
|
|
79
|
+
r.raise_for_status()
|
|
80
|
+
with open(dest, "wb") as out:
|
|
81
|
+
for chunk in r.iter_bytes():
|
|
82
|
+
out.write(chunk)
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _emit_result(
|
|
87
|
+
bundle: Any, files: list[Any], target: Path, *, json_output: bool
|
|
88
|
+
) -> None:
|
|
89
|
+
if json_output:
|
|
90
|
+
emit(
|
|
91
|
+
{
|
|
92
|
+
"review_id": bundle.review_id,
|
|
93
|
+
"target": str(target),
|
|
94
|
+
"files": [f.filename for f in files],
|
|
95
|
+
},
|
|
96
|
+
json_output=True,
|
|
97
|
+
)
|
|
98
|
+
return
|
|
99
|
+
sys.stdout.write(f"Downloaded {len(files)} file(s) to {target}\n")
|
|
100
|
+
for f in files:
|
|
101
|
+
sys.stdout.write(f" {f.filename}\n")
|
|
102
|
+
sys.stdout.write(
|
|
103
|
+
"\nNext: read SKILL.md and references/* before approve/reject.\n"
|
|
104
|
+
)
|