upscaler-cli 0.2.3.dev6696__py3-none-any.whl → 0.2.3.dev6697__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.
- src/cli/asset.py +211 -48
- src/cli/context.py +1 -0
- src/cli/entry.py +99 -88
- src/cli/get.py +25 -8
- src/cli/helpers.py +135 -0
- src/cli/list_cmd.py +29 -6
- src/cli/main.py +102 -20
- src/client.py +11 -1
- {upscaler_cli-0.2.3.dev6696.dist-info → upscaler_cli-0.2.3.dev6697.dist-info}/METADATA +1 -1
- {upscaler_cli-0.2.3.dev6696.dist-info → upscaler_cli-0.2.3.dev6697.dist-info}/RECORD +13 -13
- {upscaler_cli-0.2.3.dev6696.dist-info → upscaler_cli-0.2.3.dev6697.dist-info}/WHEEL +0 -0
- {upscaler_cli-0.2.3.dev6696.dist-info → upscaler_cli-0.2.3.dev6697.dist-info}/entry_points.txt +0 -0
- {upscaler_cli-0.2.3.dev6696.dist-info → upscaler_cli-0.2.3.dev6697.dist-info}/top_level.txt +0 -0
src/cli/asset.py
CHANGED
|
@@ -79,14 +79,29 @@ def asset_find(ctx, title, description, asset_type, limit, offset):
|
|
|
79
79
|
returned = metadata.get("returned_count", len(data))
|
|
80
80
|
if total > returned:
|
|
81
81
|
click.echo(f"Showing {returned} of {total} results (use --offset to paginate)\n")
|
|
82
|
-
click.echo(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
click.echo(
|
|
83
|
+
format_table(
|
|
84
|
+
data,
|
|
85
|
+
columns=["asset_id", "title", "asset_type", "description"],
|
|
86
|
+
)
|
|
87
|
+
)
|
|
86
88
|
|
|
87
89
|
|
|
88
90
|
@asset_group.command("create")
|
|
89
|
-
@click.option(
|
|
91
|
+
@click.option(
|
|
92
|
+
"--type",
|
|
93
|
+
"asset_type",
|
|
94
|
+
required=True,
|
|
95
|
+
type=click.Choice(
|
|
96
|
+
[
|
|
97
|
+
"document_definition",
|
|
98
|
+
"register_definition",
|
|
99
|
+
"record_definition",
|
|
100
|
+
"course_definition",
|
|
101
|
+
]
|
|
102
|
+
),
|
|
103
|
+
help="Asset type to create.",
|
|
104
|
+
)
|
|
90
105
|
@click.option("--data", "data_input", required=True, help="JSON data (value, - for stdin, @file).")
|
|
91
106
|
@click.option(
|
|
92
107
|
"--values-type",
|
|
@@ -179,7 +194,13 @@ def asset_update(ctx, asset_id, data_input, dry_run):
|
|
|
179
194
|
@click.option("--dry-run", is_flag=True, help="Preview without updating.")
|
|
180
195
|
@pass_context
|
|
181
196
|
def asset_update_content(
|
|
182
|
-
ctx,
|
|
197
|
+
ctx,
|
|
198
|
+
asset_id,
|
|
199
|
+
data_input,
|
|
200
|
+
values_type,
|
|
201
|
+
file_pairs,
|
|
202
|
+
content_types,
|
|
203
|
+
dry_run,
|
|
183
204
|
):
|
|
184
205
|
"""Update asset content (Slate JSON or markdown), optionally uploading files.
|
|
185
206
|
|
|
@@ -228,7 +249,9 @@ def asset_update_content(
|
|
|
228
249
|
@asset_group.command("upload-file")
|
|
229
250
|
@click.option("--asset-id", required=True, help="Document ID to upload into.")
|
|
230
251
|
@click.option(
|
|
231
|
-
"--field",
|
|
252
|
+
"--field",
|
|
253
|
+
"field_name",
|
|
254
|
+
required=True,
|
|
232
255
|
help="File-block name (options.name) on the Slate tree.",
|
|
233
256
|
)
|
|
234
257
|
@click.option(
|
|
@@ -361,7 +384,11 @@ def asset_add_task(ctx, asset_id, title, description, dry_run):
|
|
|
361
384
|
data["description"] = description
|
|
362
385
|
|
|
363
386
|
_execute_asset(
|
|
364
|
-
ctx,
|
|
387
|
+
ctx,
|
|
388
|
+
"add_task_definition",
|
|
389
|
+
asset_id=asset_id,
|
|
390
|
+
data=data,
|
|
391
|
+
dry_run=dry_run,
|
|
365
392
|
)
|
|
366
393
|
|
|
367
394
|
|
|
@@ -379,7 +406,8 @@ def asset_add_task(ctx, asset_id, title, description, dry_run):
|
|
|
379
406
|
"values_file",
|
|
380
407
|
default=None,
|
|
381
408
|
type=click.Path(exists=True, dir_okay=False),
|
|
382
|
-
help="Path to
|
|
409
|
+
help="Path to the body file. With --values-type markdown the file is sent "
|
|
410
|
+
"as a raw string; otherwise it is parsed as a Slate JSON array.",
|
|
383
411
|
)
|
|
384
412
|
@click.option(
|
|
385
413
|
"--values-type",
|
|
@@ -390,7 +418,13 @@ def asset_add_task(ctx, asset_id, title, description, dry_run):
|
|
|
390
418
|
@click.option("--dry-run", is_flag=True, help="Preview without updating.")
|
|
391
419
|
@pass_context
|
|
392
420
|
def asset_set_task_values(
|
|
393
|
-
ctx,
|
|
421
|
+
ctx,
|
|
422
|
+
asset_id,
|
|
423
|
+
task_definition_id,
|
|
424
|
+
data_input,
|
|
425
|
+
values_file,
|
|
426
|
+
values_type,
|
|
427
|
+
dry_run,
|
|
394
428
|
):
|
|
395
429
|
"""Set the body of a single task definition (markdown or SlateJS).
|
|
396
430
|
|
|
@@ -403,30 +437,9 @@ def asset_set_task_values(
|
|
|
403
437
|
upscaler asset set-task-values --asset-id rd_abc --task-id td_xyz \\
|
|
404
438
|
--data '{"values": "## Header\\n..."}' --values-type markdown
|
|
405
439
|
"""
|
|
406
|
-
from src.cli.helpers import
|
|
440
|
+
from src.cli.helpers import build_values_payload
|
|
407
441
|
|
|
408
|
-
|
|
409
|
-
click.echo("Provide either --data or --values-file.", err=True)
|
|
410
|
-
sys.exit(1)
|
|
411
|
-
return
|
|
412
|
-
if data_input and values_file:
|
|
413
|
-
click.echo("--data and --values-file are mutually exclusive.", err=True)
|
|
414
|
-
sys.exit(1)
|
|
415
|
-
return
|
|
416
|
-
|
|
417
|
-
if values_file:
|
|
418
|
-
data = {"values": Path(values_file).read_text()}
|
|
419
|
-
else:
|
|
420
|
-
try:
|
|
421
|
-
data = parse_data(data_input)
|
|
422
|
-
except Exception as e:
|
|
423
|
-
click.echo(str(e), err=True)
|
|
424
|
-
sys.exit(1)
|
|
425
|
-
return
|
|
426
|
-
if "values" not in data:
|
|
427
|
-
click.echo("--data must include a 'values' key.", err=True)
|
|
428
|
-
sys.exit(1)
|
|
429
|
-
return
|
|
442
|
+
data = build_values_payload(data_input, values_file, values_type)
|
|
430
443
|
|
|
431
444
|
data["taskDefinitionId"] = task_definition_id
|
|
432
445
|
if values_type:
|
|
@@ -488,10 +501,164 @@ def asset_set_task_condition(ctx, asset_id, task_definition_id, data_input, dry_
|
|
|
488
501
|
)
|
|
489
502
|
|
|
490
503
|
|
|
504
|
+
@asset_group.command("add-lesson")
|
|
505
|
+
@click.option("--asset-id", required=True, help="Course definition ID (cd_...).")
|
|
506
|
+
@click.option("--title", required=True, help="Lesson title.")
|
|
507
|
+
@click.option("--description", default=None, help="Lesson description (optional).")
|
|
508
|
+
@click.option("--dry-run", is_flag=True, help="Preview without creating.")
|
|
509
|
+
@pass_context
|
|
510
|
+
def asset_add_lesson(ctx, asset_id, title, description, dry_run):
|
|
511
|
+
"""Append a new lesson definition to a course_definition.
|
|
512
|
+
|
|
513
|
+
Returns the assigned lesson definition ID (cl_...) which is needed to set
|
|
514
|
+
the lesson's body via `set-lesson-values`, rename it, or reorder it.
|
|
515
|
+
|
|
516
|
+
Example:
|
|
517
|
+
upscaler asset add-lesson --asset-id cd_abc --title "Phishing Basics"
|
|
518
|
+
"""
|
|
519
|
+
data = {"title": title}
|
|
520
|
+
if description:
|
|
521
|
+
data["description"] = description
|
|
522
|
+
|
|
523
|
+
_execute_asset(
|
|
524
|
+
ctx,
|
|
525
|
+
"add_lesson_definition",
|
|
526
|
+
asset_id=asset_id,
|
|
527
|
+
data=data,
|
|
528
|
+
dry_run=dry_run,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
@asset_group.command("set-lesson-title")
|
|
533
|
+
@click.option("--asset-id", required=True, help="Course definition ID (cd_...).")
|
|
534
|
+
@click.option("--lesson-id", "lesson_definition_id", required=True, help="Lesson ID (cl_...).")
|
|
535
|
+
@click.option("--title", required=True, help="New lesson title.")
|
|
536
|
+
@click.option("--dry-run", is_flag=True, help="Preview without updating.")
|
|
537
|
+
@pass_context
|
|
538
|
+
def asset_set_lesson_title(ctx, asset_id, lesson_definition_id, title, dry_run):
|
|
539
|
+
"""Rename a single lesson definition."""
|
|
540
|
+
_execute_asset(
|
|
541
|
+
ctx,
|
|
542
|
+
"set_lesson_definition_title",
|
|
543
|
+
asset_id=asset_id,
|
|
544
|
+
data={"lessonDefinitionId": lesson_definition_id, "title": title},
|
|
545
|
+
dry_run=dry_run,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@asset_group.command("set-lesson-description")
|
|
550
|
+
@click.option("--asset-id", required=True, help="Course definition ID (cd_...).")
|
|
551
|
+
@click.option("--lesson-id", "lesson_definition_id", required=True, help="Lesson ID (cl_...).")
|
|
552
|
+
@click.option("--description", required=True, help="New lesson description.")
|
|
553
|
+
@click.option("--dry-run", is_flag=True, help="Preview without updating.")
|
|
554
|
+
@pass_context
|
|
555
|
+
def asset_set_lesson_description(ctx, asset_id, lesson_definition_id, description, dry_run):
|
|
556
|
+
"""Set the description of a single lesson definition."""
|
|
557
|
+
_execute_asset(
|
|
558
|
+
ctx,
|
|
559
|
+
"set_lesson_definition_description",
|
|
560
|
+
asset_id=asset_id,
|
|
561
|
+
data={"lessonDefinitionId": lesson_definition_id, "description": description},
|
|
562
|
+
dry_run=dry_run,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
@asset_group.command("set-lesson-values")
|
|
567
|
+
@click.option("--asset-id", required=True, help="Course definition ID (cd_...).")
|
|
568
|
+
@click.option("--lesson-id", "lesson_definition_id", required=True, help="Lesson ID (cl_...).")
|
|
569
|
+
@click.option(
|
|
570
|
+
"--data",
|
|
571
|
+
"data_input",
|
|
572
|
+
default=None,
|
|
573
|
+
help="JSON object with 'values' key. Mutually exclusive with --values-file.",
|
|
574
|
+
)
|
|
575
|
+
@click.option(
|
|
576
|
+
"--values-file",
|
|
577
|
+
"values_file",
|
|
578
|
+
default=None,
|
|
579
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
580
|
+
help="Path to the body file. With --values-type markdown the file is sent "
|
|
581
|
+
"as a raw string; otherwise it is parsed as a Slate JSON array.",
|
|
582
|
+
)
|
|
583
|
+
@click.option(
|
|
584
|
+
"--values-type",
|
|
585
|
+
type=click.Choice(["markdown", "slateJson"]),
|
|
586
|
+
default=None,
|
|
587
|
+
help="Content format: 'markdown' or 'slateJson' (default: slateJson).",
|
|
588
|
+
)
|
|
589
|
+
@click.option("--dry-run", is_flag=True, help="Preview without updating.")
|
|
590
|
+
@pass_context
|
|
591
|
+
def asset_set_lesson_values(
|
|
592
|
+
ctx,
|
|
593
|
+
asset_id,
|
|
594
|
+
lesson_definition_id,
|
|
595
|
+
data_input,
|
|
596
|
+
values_file,
|
|
597
|
+
values_type,
|
|
598
|
+
dry_run,
|
|
599
|
+
):
|
|
600
|
+
"""Set the body of a single lesson definition (markdown or SlateJS).
|
|
601
|
+
|
|
602
|
+
Provide content via either --data (JSON object with 'values') or --values-file
|
|
603
|
+
(raw markdown or Slate JSON file). Pass --values-type markdown for markdown bodies.
|
|
604
|
+
|
|
605
|
+
Examples:
|
|
606
|
+
upscaler asset set-lesson-values --asset-id cd_abc --lesson-id cl_xyz \\
|
|
607
|
+
--values-file lesson1-body.md --values-type markdown
|
|
608
|
+
"""
|
|
609
|
+
from src.cli.helpers import build_values_payload
|
|
610
|
+
|
|
611
|
+
data = build_values_payload(data_input, values_file, values_type)
|
|
612
|
+
|
|
613
|
+
data["lessonDefinitionId"] = lesson_definition_id
|
|
614
|
+
if values_type:
|
|
615
|
+
data["valuesType"] = values_type
|
|
616
|
+
|
|
617
|
+
_execute_asset(
|
|
618
|
+
ctx,
|
|
619
|
+
"set_lesson_definition_values",
|
|
620
|
+
asset_id=asset_id,
|
|
621
|
+
data=data,
|
|
622
|
+
dry_run=dry_run,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
@asset_group.command("remove-lesson")
|
|
627
|
+
@click.option("--asset-id", required=True, help="Course definition ID (cd_...).")
|
|
628
|
+
@click.option("--lesson-id", "lesson_definition_id", required=True, help="Lesson ID (cl_...).")
|
|
629
|
+
@click.option("--dry-run", is_flag=True, help="Preview without removing.")
|
|
630
|
+
@pass_context
|
|
631
|
+
def asset_remove_lesson(ctx, asset_id, lesson_definition_id, dry_run):
|
|
632
|
+
"""Remove a single lesson definition from a course_definition."""
|
|
633
|
+
_execute_asset(
|
|
634
|
+
ctx,
|
|
635
|
+
"remove_lesson_definition",
|
|
636
|
+
asset_id=asset_id,
|
|
637
|
+
data={"lessonDefinitionId": lesson_definition_id},
|
|
638
|
+
dry_run=dry_run,
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
@asset_group.command("move-lesson")
|
|
643
|
+
@click.option("--asset-id", required=True, help="Course definition ID (cd_...).")
|
|
644
|
+
@click.option("--from-id", "from_id", required=True, help="Lesson ID to move (cl_...).")
|
|
645
|
+
@click.option("--to-id", "to_id", required=True, help="Target sibling lesson ID (cl_...).")
|
|
646
|
+
@click.option("--dry-run", is_flag=True, help="Preview without reordering.")
|
|
647
|
+
@pass_context
|
|
648
|
+
def asset_move_lesson(ctx, asset_id, from_id, to_id, dry_run):
|
|
649
|
+
"""Reorder a lesson definition within a course_definition."""
|
|
650
|
+
_execute_asset(
|
|
651
|
+
ctx,
|
|
652
|
+
"move_lesson_definition",
|
|
653
|
+
asset_id=asset_id,
|
|
654
|
+
data={"fromId": from_id, "toId": to_id},
|
|
655
|
+
dry_run=dry_run,
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
|
|
491
659
|
def _execute_asset(ctx, operation, asset_id=None, asset_type=None, data=None, dry_run=False):
|
|
492
660
|
"""Execute an asset operation."""
|
|
493
|
-
from src.cli.helpers import make_client
|
|
494
|
-
from src.formatters.json_fmt import format_json
|
|
661
|
+
from src.cli.helpers import emit_action_result, emit_dry_run, make_client
|
|
495
662
|
|
|
496
663
|
payload = {"operation": operation}
|
|
497
664
|
if asset_id:
|
|
@@ -502,11 +669,11 @@ def _execute_asset(ctx, operation, asset_id=None, asset_type=None, data=None, dr
|
|
|
502
669
|
payload["data"] = data
|
|
503
670
|
|
|
504
671
|
if dry_run:
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
672
|
+
emit_dry_run(
|
|
673
|
+
ctx,
|
|
674
|
+
summary=f"would {operation} {asset_type or asset_id or 'asset'}",
|
|
675
|
+
payload=payload,
|
|
676
|
+
)
|
|
510
677
|
return
|
|
511
678
|
|
|
512
679
|
client = make_client(ctx)
|
|
@@ -517,11 +684,9 @@ def _execute_asset(ctx, operation, asset_id=None, asset_type=None, data=None, dr
|
|
|
517
684
|
handle_error(ctx, e)
|
|
518
685
|
return
|
|
519
686
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
d = result.get("data", {})
|
|
524
|
-
click.echo(f"Asset {operation}: {d.get('assetId', d.get('asset_id', ''))}")
|
|
687
|
+
emit_action_result(
|
|
688
|
+
ctx, result, label=f"Asset {operation}", id_keys=("assetId", "asset_id", "id")
|
|
689
|
+
)
|
|
525
690
|
|
|
526
691
|
|
|
527
692
|
# File-upload flow for Pattern A (Documents). Documents persist `values` as
|
|
@@ -678,9 +843,7 @@ def _dry_run_asset_with_files(ctx, asset_id, files, data):
|
|
|
678
843
|
if ctx.json_mode:
|
|
679
844
|
click.echo(format_json({"dry_run": True, "payload": dry_payload}, compact=True))
|
|
680
845
|
else:
|
|
681
|
-
click.echo(
|
|
682
|
-
f"[dry-run] Would upload {len(files)} file(s) and update-content asset:"
|
|
683
|
-
)
|
|
846
|
+
click.echo(f"[dry-run] Would upload {len(files)} file(s) and update-content asset:")
|
|
684
847
|
click.echo(format_json(dry_payload))
|
|
685
848
|
|
|
686
849
|
|
src/cli/context.py
CHANGED
src/cli/entry.py
CHANGED
|
@@ -34,7 +34,8 @@ def entry_group():
|
|
|
34
34
|
|
|
35
35
|
@entry_group.command("create")
|
|
36
36
|
@click.option(
|
|
37
|
-
"--definition-id",
|
|
37
|
+
"--definition-id",
|
|
38
|
+
required=True,
|
|
38
39
|
help="Definition ID (rd_ = record, rg_ = register).",
|
|
39
40
|
)
|
|
40
41
|
@click.option("--data", "data_input", required=True, help="JSON data (value, - for stdin, @file).")
|
|
@@ -126,9 +127,7 @@ def entry_update(ctx, entry_id, data_input, file_pairs, task_id, content_types,
|
|
|
126
127
|
|
|
127
128
|
@entry_group.command("upload-file")
|
|
128
129
|
@click.option("--entry-id", required=True, help="Entry ID to upload into.")
|
|
129
|
-
@click.option(
|
|
130
|
-
"--field", "field_name", required=True, help="File-field name (options.name)."
|
|
131
|
-
)
|
|
130
|
+
@click.option("--field", "field_name", required=True, help="File-field name (options.name).")
|
|
132
131
|
@click.option(
|
|
133
132
|
"--path",
|
|
134
133
|
"file_path",
|
|
@@ -230,9 +229,12 @@ def entry_complete_task(ctx, entry_id, task_id, data_input, dry_run):
|
|
|
230
229
|
return
|
|
231
230
|
|
|
232
231
|
_execute_entry(
|
|
233
|
-
ctx,
|
|
234
|
-
|
|
235
|
-
|
|
232
|
+
ctx,
|
|
233
|
+
"complete_task",
|
|
234
|
+
entry_id=entry_id,
|
|
235
|
+
task_id=task_id,
|
|
236
|
+
data=data,
|
|
237
|
+
dry_run=dry_run,
|
|
236
238
|
)
|
|
237
239
|
|
|
238
240
|
|
|
@@ -262,7 +264,7 @@ def _execute_entry(
|
|
|
262
264
|
files=None,
|
|
263
265
|
):
|
|
264
266
|
"""Execute an entry operation."""
|
|
265
|
-
from src.cli.helpers import make_client
|
|
267
|
+
from src.cli.helpers import emit_action_result, make_client
|
|
266
268
|
from src.formatters.json_fmt import format_json
|
|
267
269
|
|
|
268
270
|
payload = {"operation": operation}
|
|
@@ -281,10 +283,10 @@ def _execute_entry(
|
|
|
281
283
|
# then issue ONE mutation. The no-files path below stays unchanged.
|
|
282
284
|
if dry_run:
|
|
283
285
|
dry_payload = dict(payload)
|
|
284
|
-
dry_payload["files"] = [
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
dry_payload["files"] = [{"field": f["field"], "path": str(f["path"])} for f in files]
|
|
287
|
+
if ctx.quiet_mode:
|
|
288
|
+
click.echo(f"[dry-run] would upload {len(files)} file(s) and {operation} entry")
|
|
289
|
+
elif ctx.json_mode:
|
|
288
290
|
click.echo(format_json({"dry_run": True, "payload": dry_payload}, compact=True))
|
|
289
291
|
else:
|
|
290
292
|
click.echo(f"[dry-run] Would upload {len(files)} file(s) and {operation} entry:")
|
|
@@ -300,15 +302,16 @@ def _execute_entry(
|
|
|
300
302
|
return
|
|
301
303
|
|
|
302
304
|
if dry_run:
|
|
305
|
+
if ctx.quiet_mode:
|
|
306
|
+
click.echo(f"[dry-run] would {operation} entry")
|
|
307
|
+
return
|
|
303
308
|
dry_run_data = {"dry_run": True, "payload": payload}
|
|
304
309
|
|
|
305
310
|
# For complete_task, fetch and show the task's form fields
|
|
306
311
|
if operation == "complete_task" and task_id:
|
|
307
312
|
try:
|
|
308
313
|
client = make_client(ctx)
|
|
309
|
-
task_result = asyncio.run(
|
|
310
|
-
client.request("GET", f"/api/v1/tasks/{task_id}")
|
|
311
|
-
)
|
|
314
|
+
task_result = asyncio.run(client.request("GET", f"/api/v1/tasks/{task_id}"))
|
|
312
315
|
task_data = task_result.get("data", {})
|
|
313
316
|
definition = task_data.get("definition", [])
|
|
314
317
|
fields = []
|
|
@@ -317,12 +320,14 @@ def _execute_entry(
|
|
|
317
320
|
if block.get("type", "").startswith("form-"):
|
|
318
321
|
opts = block.get("options", {})
|
|
319
322
|
if opts.get("name"):
|
|
320
|
-
fields.append(
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
323
|
+
fields.append(
|
|
324
|
+
{
|
|
325
|
+
"key": opts["name"],
|
|
326
|
+
"label": opts.get("title", opts["name"]),
|
|
327
|
+
"type": block["type"],
|
|
328
|
+
"required": opts.get("required", False),
|
|
329
|
+
}
|
|
330
|
+
)
|
|
326
331
|
if fields:
|
|
327
332
|
dry_run_data["task_fields"] = fields
|
|
328
333
|
current_values = task_data.get("values", {})
|
|
@@ -346,13 +351,13 @@ def _execute_entry(
|
|
|
346
351
|
handle_error(ctx, e)
|
|
347
352
|
return
|
|
348
353
|
|
|
349
|
-
# After record creation, enrich tasks with their form field definitions
|
|
350
|
-
|
|
354
|
+
# After record creation, enrich tasks with their form field definitions.
|
|
355
|
+
# Skipped in --quiet mode: the enrichment makes extra calls and only
|
|
356
|
+
# populates output that quiet mode discards anyway.
|
|
357
|
+
if not ctx.quiet_mode and operation == "create" and result.get("data", {}).get("tasks"):
|
|
351
358
|
try:
|
|
352
359
|
for task in result["data"]["tasks"]:
|
|
353
|
-
task_detail = asyncio.run(
|
|
354
|
-
client.request("GET", f"/api/v1/tasks/{task['id']}")
|
|
355
|
-
)
|
|
360
|
+
task_detail = asyncio.run(client.request("GET", f"/api/v1/tasks/{task['id']}"))
|
|
356
361
|
task_def = task_detail.get("data", {}).get("definition", [])
|
|
357
362
|
if isinstance(task_def, list):
|
|
358
363
|
fields = []
|
|
@@ -360,22 +365,20 @@ def _execute_entry(
|
|
|
360
365
|
if block.get("type", "").startswith("form-"):
|
|
361
366
|
opts = block.get("options", {})
|
|
362
367
|
if opts.get("name"):
|
|
363
|
-
fields.append(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
368
|
+
fields.append(
|
|
369
|
+
{
|
|
370
|
+
"key": opts["name"],
|
|
371
|
+
"label": opts.get("title", opts["name"]),
|
|
372
|
+
"type": block["type"],
|
|
373
|
+
"required": opts.get("required", False),
|
|
374
|
+
}
|
|
375
|
+
)
|
|
369
376
|
if fields:
|
|
370
377
|
task["fields"] = fields
|
|
371
378
|
except Exception:
|
|
372
379
|
pass # Non-critical enrichment
|
|
373
380
|
|
|
374
|
-
|
|
375
|
-
click.echo(format_json(result, compact=True))
|
|
376
|
-
else:
|
|
377
|
-
d = result.get("data", {})
|
|
378
|
-
click.echo(f"Entry {operation}: {d.get('id', d.get('entryId', ''))}")
|
|
381
|
+
emit_action_result(ctx, result, label=f"Entry {operation}", id_keys=("id", "entryId"))
|
|
379
382
|
|
|
380
383
|
|
|
381
384
|
# File-upload flow for Pattern B (Items + Records, which persist `values`
|
|
@@ -397,12 +400,21 @@ def _execute_entry_with_files(ctx, *, entry_id, task_id, data, files):
|
|
|
397
400
|
client = make_client(ctx)
|
|
398
401
|
landed_uids: list[str] = []
|
|
399
402
|
|
|
400
|
-
|
|
403
|
+
# Records hold their file fields on individual tasks, so an upload must name
|
|
404
|
+
# the task: it drives both the schema lookup and the values read/merge.
|
|
405
|
+
if _is_record_id(entry_id) and not task_id:
|
|
406
|
+
click.echo(
|
|
407
|
+
"Record file uploads require --task-id (records hold fields on tasks).",
|
|
408
|
+
err=True,
|
|
409
|
+
)
|
|
410
|
+
sys.exit(1)
|
|
411
|
+
|
|
412
|
+
schema_fields = _fetch_schema_or_die(ctx, client, entry_id, task_id=task_id)
|
|
401
413
|
|
|
402
414
|
# Hard-fail before any S3 upload on unknown/ambiguous/non-file fields.
|
|
403
415
|
resolved = _resolve_field_descriptors(ctx, schema_fields, files)
|
|
404
416
|
|
|
405
|
-
values, expected_version = _fetch_entry_values(ctx, client, entry_id)
|
|
417
|
+
values, expected_version = _fetch_entry_values(ctx, client, entry_id, task_id=task_id)
|
|
406
418
|
|
|
407
419
|
# The splice spec is built once during upload and re-applied on
|
|
408
420
|
# every conflict retry. Top-level entries land via append_file_to_form_field
|
|
@@ -427,9 +439,7 @@ def _execute_entry_with_files(ctx, *, entry_id, task_id, data, files):
|
|
|
427
439
|
landed_uids.append(file_item["uid"])
|
|
428
440
|
|
|
429
441
|
if "table_key" in descriptor:
|
|
430
|
-
table_rows = splice_spec["nested"].setdefault(
|
|
431
|
-
descriptor["table_key"], {}
|
|
432
|
-
)
|
|
442
|
+
table_rows = splice_spec["nested"].setdefault(descriptor["table_key"], {})
|
|
433
443
|
table_rows.setdefault(descriptor["col_key"], []).append(file_item)
|
|
434
444
|
else:
|
|
435
445
|
splice_spec["top_level"].append((descriptor["ff_key"], file_item))
|
|
@@ -485,11 +495,18 @@ def _apply_splice_spec(values, splice_spec):
|
|
|
485
495
|
return values
|
|
486
496
|
|
|
487
497
|
|
|
488
|
-
def
|
|
498
|
+
def _is_record_id(entry_id):
|
|
499
|
+
return bool(entry_id) and (entry_id.startswith("r_") or entry_id.startswith("rec_"))
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _fetch_schema_or_die(ctx, client, entry_id, task_id=None):
|
|
503
|
+
path = f"/api/v1/assets/{entry_id}/schema"
|
|
504
|
+
if task_id:
|
|
505
|
+
# Records carry fields per-task; name the task so the backend returns
|
|
506
|
+
# that task's field schema instead of a (non-existent) record schema.
|
|
507
|
+
path += f"?taskId={task_id}"
|
|
489
508
|
try:
|
|
490
|
-
resp = asyncio.run(
|
|
491
|
-
client.request("GET", f"/api/v1/assets/{entry_id}/schema")
|
|
492
|
-
)
|
|
509
|
+
resp = asyncio.run(client.request("GET", path))
|
|
493
510
|
except Exception as e:
|
|
494
511
|
handle_error(ctx, e)
|
|
495
512
|
sys.exit(1)
|
|
@@ -518,12 +535,8 @@ def _resolve_field_descriptors(ctx, schema_fields, files):
|
|
|
518
535
|
All resolution failures (unknown table, unknown column, ambiguous label,
|
|
519
536
|
non-file target) raise SystemExit before any S3 traffic.
|
|
520
537
|
"""
|
|
521
|
-
top_file_fields = [
|
|
522
|
-
|
|
523
|
-
]
|
|
524
|
-
table_fields = [
|
|
525
|
-
f for f in schema_fields if f.get("type") == AGENT_SCHEMA_TABLE_TYPE
|
|
526
|
-
]
|
|
538
|
+
top_file_fields = [f for f in schema_fields if f.get("type") == AGENT_SCHEMA_FILE_UPLOAD_TYPE]
|
|
539
|
+
table_fields = [f for f in schema_fields if f.get("type") == AGENT_SCHEMA_TABLE_TYPE]
|
|
527
540
|
by_key = {f["key"]: f for f in schema_fields if f.get("key")}
|
|
528
541
|
titles_to_keys: dict[str, list[str]] = {}
|
|
529
542
|
for f in top_file_fields:
|
|
@@ -603,9 +616,7 @@ def _resolve_nested(ctx, descriptor, raw_stripped, table_fields):
|
|
|
603
616
|
_fail_loud_unknown_table(ctx, table_raw, table_fields)
|
|
604
617
|
|
|
605
618
|
columns = [c for c in (table.get("columns") or []) if isinstance(c, dict)]
|
|
606
|
-
file_columns = [
|
|
607
|
-
c for c in columns if c.get("type") == AGENT_SCHEMA_FILE_UPLOAD_TYPE
|
|
608
|
-
]
|
|
619
|
+
file_columns = [c for c in columns if c.get("type") == AGENT_SCHEMA_FILE_UPLOAD_TYPE]
|
|
609
620
|
|
|
610
621
|
# Match the column by its full schema key OR its bare last segment: the
|
|
611
622
|
# backend reports table columns as the dotted `ff_table.ff_col`, but a user
|
|
@@ -621,9 +632,7 @@ def _resolve_nested(ctx, descriptor, raw_stripped, table_fields):
|
|
|
621
632
|
if col_raw in col_by_key:
|
|
622
633
|
col = col_by_key[col_raw]
|
|
623
634
|
if col.get("type") != AGENT_SCHEMA_FILE_UPLOAD_TYPE:
|
|
624
|
-
_fail_loud_non_file_column(
|
|
625
|
-
ctx, table, _bare_col_key(col.get("key")), file_columns
|
|
626
|
-
)
|
|
635
|
+
_fail_loud_non_file_column(ctx, table, _bare_col_key(col.get("key")), file_columns)
|
|
627
636
|
return {
|
|
628
637
|
**descriptor,
|
|
629
638
|
"table_key": table["key"],
|
|
@@ -645,7 +654,7 @@ def _resolve_nested(ctx, descriptor, raw_stripped, table_fields):
|
|
|
645
654
|
if len(candidates) > 1:
|
|
646
655
|
click.echo(
|
|
647
656
|
f'Column "{col_raw}" in table "{table.get("label") or table["key"]}" '
|
|
648
|
-
f
|
|
657
|
+
f"matches multiple file columns: "
|
|
649
658
|
f'{", ".join(_bare_col_key(k) for k in candidates)}. '
|
|
650
659
|
f"Pass the ff_* key to disambiguate.",
|
|
651
660
|
err=True,
|
|
@@ -659,10 +668,7 @@ def _match_table(table_raw, table_fields):
|
|
|
659
668
|
if table_raw in by_key:
|
|
660
669
|
return by_key[table_raw]
|
|
661
670
|
norm = table_raw.lower()
|
|
662
|
-
label_matches = [
|
|
663
|
-
t for t in table_fields
|
|
664
|
-
if (t.get("label") or "").strip().lower() == norm
|
|
665
|
-
]
|
|
671
|
+
label_matches = [t for t in table_fields if (t.get("label") or "").strip().lower() == norm]
|
|
666
672
|
if len(label_matches) == 1:
|
|
667
673
|
return label_matches[0]
|
|
668
674
|
# Ambiguous tables (two with the same label) fall through to the
|
|
@@ -671,14 +677,12 @@ def _match_table(table_raw, table_fields):
|
|
|
671
677
|
|
|
672
678
|
|
|
673
679
|
def _fail_loud_unknown(ctx, field_name, file_fields, table_fields=()):
|
|
674
|
-
available = ", ".join(
|
|
675
|
-
f'"{f.get("label")}" → {f.get("key")}' for f in file_fields
|
|
676
|
-
) or "(none)"
|
|
680
|
+
available = ", ".join(f'"{f.get("label")}" → {f.get("key")}' for f in file_fields) or "(none)"
|
|
677
681
|
extra = ""
|
|
678
682
|
if table_fields:
|
|
679
683
|
nested = []
|
|
680
684
|
for t in table_fields:
|
|
681
|
-
for c in
|
|
685
|
+
for c in t.get("columns") or []:
|
|
682
686
|
if isinstance(c, dict) and c.get("type") == AGENT_SCHEMA_FILE_UPLOAD_TYPE:
|
|
683
687
|
nested.append(
|
|
684
688
|
f'"{t.get("label") or t.get("key")}.'
|
|
@@ -686,9 +690,7 @@ def _fail_loud_unknown(ctx, field_name, file_fields, table_fields=()):
|
|
|
686
690
|
f'{t.get("key")}.{_bare_col_key(c.get("key"))}'
|
|
687
691
|
)
|
|
688
692
|
if nested:
|
|
689
|
-
extra = (
|
|
690
|
-
f" Nested file columns (use TABLE.COLUMN form): {', '.join(nested)}."
|
|
691
|
-
)
|
|
693
|
+
extra = f" Nested file columns (use TABLE.COLUMN form): {', '.join(nested)}."
|
|
692
694
|
click.echo(
|
|
693
695
|
f'No file field named "{field_name}" on this entry. '
|
|
694
696
|
f"Available file fields (title → id): {available}.{extra}",
|
|
@@ -706,17 +708,14 @@ def _fail_loud_non_file(ctx, key, file_fields, table_fields=()):
|
|
|
706
708
|
"into a nested file column)"
|
|
707
709
|
)
|
|
708
710
|
click.echo(
|
|
709
|
-
f'Field "{key}" is not a file-upload field{extra}. '
|
|
710
|
-
f"Available file fields: {available}.",
|
|
711
|
+
f'Field "{key}" is not a file-upload field{extra}. ' f"Available file fields: {available}.",
|
|
711
712
|
err=True,
|
|
712
713
|
)
|
|
713
714
|
sys.exit(1)
|
|
714
715
|
|
|
715
716
|
|
|
716
717
|
def _fail_loud_unknown_table(ctx, table_raw, table_fields):
|
|
717
|
-
available = ", ".join(
|
|
718
|
-
f'"{t.get("label")}" → {t.get("key")}' for t in table_fields
|
|
719
|
-
) or "(none)"
|
|
718
|
+
available = ", ".join(f'"{t.get("label")}" → {t.get("key")}' for t in table_fields) or "(none)"
|
|
720
719
|
click.echo(
|
|
721
720
|
f'No table field named "{table_raw}" on this entry. '
|
|
722
721
|
f"Available tables (title → id): {available}.",
|
|
@@ -726,9 +725,10 @@ def _fail_loud_unknown_table(ctx, table_raw, table_fields):
|
|
|
726
725
|
|
|
727
726
|
|
|
728
727
|
def _fail_loud_unknown_column(ctx, table, col_raw, file_columns):
|
|
729
|
-
available =
|
|
730
|
-
f'"{c.get("label")}" → {_bare_col_key(c.get("key"))}' for c in file_columns
|
|
731
|
-
|
|
728
|
+
available = (
|
|
729
|
+
", ".join(f'"{c.get("label")}" → {_bare_col_key(c.get("key"))}' for c in file_columns)
|
|
730
|
+
or "(none)"
|
|
731
|
+
)
|
|
732
732
|
click.echo(
|
|
733
733
|
f'No file column "{col_raw}" in table '
|
|
734
734
|
f'"{table.get("label") or table.get("key")}". '
|
|
@@ -739,9 +739,7 @@ def _fail_loud_unknown_column(ctx, table, col_raw, file_columns):
|
|
|
739
739
|
|
|
740
740
|
|
|
741
741
|
def _fail_loud_non_file_column(ctx, table, col_key, file_columns):
|
|
742
|
-
available = ", ".join(
|
|
743
|
-
_bare_col_key(c.get("key")) for c in file_columns
|
|
744
|
-
) or "(none)"
|
|
742
|
+
available = ", ".join(_bare_col_key(c.get("key")) for c in file_columns) or "(none)"
|
|
745
743
|
click.echo(
|
|
746
744
|
f'Column "{col_key}" in table '
|
|
747
745
|
f'"{table.get("label") or table.get("key")}" is not a file-upload column. '
|
|
@@ -751,7 +749,24 @@ def _fail_loud_non_file_column(ctx, table, col_key, file_columns):
|
|
|
751
749
|
sys.exit(1)
|
|
752
750
|
|
|
753
751
|
|
|
754
|
-
def _fetch_entry_values(ctx, client, entry_id):
|
|
752
|
+
def _fetch_entry_values(ctx, client, entry_id, task_id=None):
|
|
753
|
+
# A record's values live on its tasks, not at the record level, so a file
|
|
754
|
+
# upload to a record reads (and later merges into) the named task's values.
|
|
755
|
+
# completeTask is last-writer-wins, so no version is needed for records.
|
|
756
|
+
if _is_record_id(entry_id) and task_id:
|
|
757
|
+
try:
|
|
758
|
+
task = asyncio.run(client.request("GET", f"/api/v1/tasks/{task_id}"))
|
|
759
|
+
except Exception as e:
|
|
760
|
+
handle_error(ctx, e)
|
|
761
|
+
sys.exit(1)
|
|
762
|
+
task_data = (task or {}).get("data") or {}
|
|
763
|
+
values = task_data.get("values") if isinstance(task_data, dict) else None
|
|
764
|
+
if values is None:
|
|
765
|
+
values = {}
|
|
766
|
+
if not isinstance(values, dict):
|
|
767
|
+
click.echo("Task values are not a flat dict.", err=True)
|
|
768
|
+
sys.exit(1)
|
|
769
|
+
return values, None
|
|
755
770
|
try:
|
|
756
771
|
entry = asyncio.run(client.request("GET", f"/api/v1/assets/{entry_id}"))
|
|
757
772
|
except Exception as e:
|
|
@@ -796,9 +811,7 @@ def _submit_with_retry(
|
|
|
796
811
|
expected_version=current_version if not task_id else None,
|
|
797
812
|
)
|
|
798
813
|
try:
|
|
799
|
-
result = asyncio.run(
|
|
800
|
-
client.request("POST", "/api/v1/entries", json=payload)
|
|
801
|
-
)
|
|
814
|
+
result = asyncio.run(client.request("POST", "/api/v1/entries", json=payload))
|
|
802
815
|
except Exception as e:
|
|
803
816
|
if _is_version_conflict(e) and not task_id and attempt < _CONFLICT_RETRY_BUDGET:
|
|
804
817
|
current_values, current_version = _refresh_and_reapply(
|
|
@@ -935,9 +948,7 @@ def _emit_retry_exhausted(ctx, landed_uids):
|
|
|
935
948
|
else:
|
|
936
949
|
click.echo(err_body["message"], err=True)
|
|
937
950
|
if landed_uids:
|
|
938
|
-
click.echo(
|
|
939
|
-
"Files uploaded to S3 (subject to lifecycle cleanup):", err=True
|
|
940
|
-
)
|
|
951
|
+
click.echo("Files uploaded to S3 (subject to lifecycle cleanup):", err=True)
|
|
941
952
|
for uid in landed_uids:
|
|
942
953
|
click.echo(f" {uid}", err=True)
|
|
943
954
|
sys.exit(1)
|
src/cli/get.py
CHANGED
|
@@ -17,7 +17,7 @@ _PREFIX_ROUTES = {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
# Known asset prefixes (routed to /api/v1/assets)
|
|
20
|
-
_ASSET_PREFIXES = ("d_", "doc_", "rg_", "rd_", "r_", "rec_", "i_")
|
|
20
|
+
_ASSET_PREFIXES = ("d_", "doc_", "rg_", "rd_", "r_", "rec_", "i_", "cd_")
|
|
21
21
|
|
|
22
22
|
# Explicit --type → endpoint
|
|
23
23
|
_TYPE_ROUTES = {
|
|
@@ -62,8 +62,13 @@ def _detect_route(resource_id, resource_type):
|
|
|
62
62
|
type=click.Choice(["member", "group", "task", "todo"]),
|
|
63
63
|
help="Resource type (for IDs without a known prefix, e.g. member).",
|
|
64
64
|
)
|
|
65
|
+
@click.option(
|
|
66
|
+
"--draft", is_flag=True, default=False,
|
|
67
|
+
help="Read the unpublished working copy instead of the published version "
|
|
68
|
+
"(record definitions: shows edits not yet released).",
|
|
69
|
+
)
|
|
65
70
|
@pass_context
|
|
66
|
-
def get_asset(ctx, resource_id, fmt, resource_type):
|
|
71
|
+
def get_asset(ctx, resource_id, fmt, resource_type, draft):
|
|
67
72
|
"""Retrieve an asset, member, or group by ID.
|
|
68
73
|
|
|
69
74
|
Asset and group IDs are auto-detected by prefix (rg_, d_, g_, etc.).
|
|
@@ -72,6 +77,7 @@ def get_asset(ctx, resource_id, fmt, resource_type):
|
|
|
72
77
|
Examples:
|
|
73
78
|
upscaler get rg_abc123
|
|
74
79
|
upscaler get rg_abc123 --format schema
|
|
80
|
+
upscaler get rd_abc123 --format schema --draft
|
|
75
81
|
upscaler get g_abc123
|
|
76
82
|
upscaler get <uid> --type member
|
|
77
83
|
upscaler --json get <uid> --type member
|
|
@@ -84,10 +90,19 @@ def get_asset(ctx, resource_id, fmt, resource_type):
|
|
|
84
90
|
endpoint, is_asset = _detect_route(resource_id, resource_type)
|
|
85
91
|
|
|
86
92
|
if endpoint is None:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
if resource_id.startswith("cl_"):
|
|
94
|
+
# Lessons are not standalone assets: they live inside a course and
|
|
95
|
+
# are read through it. `get <cd_…>` returns every lesson with its
|
|
96
|
+
# id, title, description, and body.
|
|
97
|
+
msg = (
|
|
98
|
+
f"{resource_id} is a lesson, which is read through its course. "
|
|
99
|
+
"Run `upscaler get <cd_…>` to see every lesson (id, title, body)."
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
msg = (
|
|
103
|
+
f"Unknown resource ID format: {resource_id}. "
|
|
104
|
+
"Use --type member or --type group for IDs without a known prefix."
|
|
105
|
+
)
|
|
91
106
|
if ctx.json_mode:
|
|
92
107
|
import json
|
|
93
108
|
click.echo(json.dumps({"error": msg, "exit_code": 1}), err=True)
|
|
@@ -97,14 +112,16 @@ def get_asset(ctx, resource_id, fmt, resource_type):
|
|
|
97
112
|
return
|
|
98
113
|
|
|
99
114
|
if is_asset:
|
|
100
|
-
_get_asset(ctx, client, resource_id, fmt, format_json)
|
|
115
|
+
_get_asset(ctx, client, resource_id, fmt, format_json, draft=draft)
|
|
101
116
|
else:
|
|
102
117
|
_get_simple(ctx, client, f"{endpoint}/{resource_id}", format_json)
|
|
103
118
|
|
|
104
119
|
|
|
105
|
-
def _get_asset(ctx, client, asset_id, fmt, format_json):
|
|
120
|
+
def _get_asset(ctx, client, asset_id, fmt, format_json, draft=False):
|
|
106
121
|
"""Fetch and display an asset with format control."""
|
|
107
122
|
params = {}
|
|
123
|
+
if draft:
|
|
124
|
+
params["draft"] = "true"
|
|
108
125
|
if fmt:
|
|
109
126
|
params["format"] = fmt
|
|
110
127
|
|
src/cli/helpers.py
CHANGED
|
@@ -50,6 +50,60 @@ def parse_data(value: str) -> dict:
|
|
|
50
50
|
return data
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
def build_values_payload(data_input, values_file, values_type) -> dict:
|
|
54
|
+
"""Build the ``{"values": ...}`` payload for a ``set-*-values`` command.
|
|
55
|
+
|
|
56
|
+
Content comes from exactly one of ``--data`` or ``--values-file``. A
|
|
57
|
+
``--values-file`` is read as a markdown string, or parsed as a JSON Slate
|
|
58
|
+
body when ``--values-type`` is not ``markdown`` (packing slateJson as a raw
|
|
59
|
+
string sends a string where the platform expects a list, which it silently
|
|
60
|
+
drops, so a malformed file must fail loudly). On any usage error this prints
|
|
61
|
+
to stderr and exits non-zero, matching the rest of the CLI.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
data_input: The ``--data`` flag value (or None).
|
|
65
|
+
values_file: The ``--values-file`` path (or None).
|
|
66
|
+
values_type: The ``--values-type`` value (e.g. "markdown", "slateJson").
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
A dict with a ``values`` key, ready for the command's id/valuesType keys.
|
|
70
|
+
"""
|
|
71
|
+
import click
|
|
72
|
+
|
|
73
|
+
if not data_input and not values_file:
|
|
74
|
+
click.echo("Provide either --data or --values-file.", err=True)
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
if data_input and values_file:
|
|
77
|
+
click.echo("--data and --values-file are mutually exclusive.", err=True)
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
|
|
80
|
+
if values_file:
|
|
81
|
+
raw = Path(values_file).read_text()
|
|
82
|
+
if values_type == "markdown":
|
|
83
|
+
values = raw
|
|
84
|
+
else:
|
|
85
|
+
try:
|
|
86
|
+
values = json.loads(raw)
|
|
87
|
+
except json.JSONDecodeError as e:
|
|
88
|
+
click.echo(
|
|
89
|
+
f"--values-file expects a JSON Slate body for --values-type "
|
|
90
|
+
f"slateJson, but {values_file} is not valid JSON: {e}",
|
|
91
|
+
err=True,
|
|
92
|
+
)
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
return {"values": values}
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
data = parse_data(data_input)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
click.echo(str(e), err=True)
|
|
100
|
+
sys.exit(1)
|
|
101
|
+
if "values" not in data:
|
|
102
|
+
click.echo("--data must include a 'values' key.", err=True)
|
|
103
|
+
sys.exit(1)
|
|
104
|
+
return data
|
|
105
|
+
|
|
106
|
+
|
|
53
107
|
def validate_entry_data(data: dict) -> dict:
|
|
54
108
|
"""Reject entry payloads that put ff_* keys at the top level.
|
|
55
109
|
|
|
@@ -88,6 +142,87 @@ def handle_error(ctx, e) -> None:
|
|
|
88
142
|
sys.exit(exit_code)
|
|
89
143
|
|
|
90
144
|
|
|
145
|
+
def _resolve_id(data: dict, id_keys) -> str:
|
|
146
|
+
"""Return the first non-empty id value among id_keys, or ''."""
|
|
147
|
+
for key in id_keys:
|
|
148
|
+
value = (data or {}).get(key)
|
|
149
|
+
if value:
|
|
150
|
+
return value
|
|
151
|
+
return ""
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
_RESULT_ID_KEYS = ("id", "entryId", "entry_id", "assetId", "asset_id")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _envelope_error_message(error) -> str:
|
|
158
|
+
"""Extract a human-readable message from a native-tool error envelope.
|
|
159
|
+
|
|
160
|
+
`error` is the unified envelope dict ({error_code, message, ...}) on most
|
|
161
|
+
failures, but tolerate a bare string or missing value too.
|
|
162
|
+
"""
|
|
163
|
+
if isinstance(error, dict):
|
|
164
|
+
return error.get("message") or error.get("error_code") or "Operation failed"
|
|
165
|
+
if error:
|
|
166
|
+
return str(error)
|
|
167
|
+
return "Operation failed"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def emit_action_result(ctx, result: dict, *, label: str, id_keys=_RESULT_ID_KEYS) -> None:
|
|
171
|
+
"""Print the outcome of a create/update mutation, honoring --quiet/--json.
|
|
172
|
+
|
|
173
|
+
quiet : print only the resolved id (one line); nothing else.
|
|
174
|
+
json : print the full compact envelope.
|
|
175
|
+
human : print "<label>: <id>".
|
|
176
|
+
|
|
177
|
+
A native-tool envelope can report failure with HTTP 200
|
|
178
|
+
({"success": false, "error": {...}}); surface that error and exit non-zero
|
|
179
|
+
instead of printing an empty id.
|
|
180
|
+
"""
|
|
181
|
+
import click
|
|
182
|
+
|
|
183
|
+
from src.formatters.json_fmt import format_json
|
|
184
|
+
|
|
185
|
+
if result.get("success") is False:
|
|
186
|
+
if ctx.json_mode:
|
|
187
|
+
click.echo(format_json(result, compact=True), err=True)
|
|
188
|
+
else:
|
|
189
|
+
click.echo(f"{label} failed: {_envelope_error_message(result.get('error'))}", err=True)
|
|
190
|
+
sys.exit(1)
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
if getattr(ctx, "quiet_mode", False):
|
|
194
|
+
rid = _resolve_id(result.get("data", {}), id_keys)
|
|
195
|
+
if rid:
|
|
196
|
+
click.echo(rid)
|
|
197
|
+
return
|
|
198
|
+
if ctx.json_mode:
|
|
199
|
+
click.echo(format_json(result, compact=True))
|
|
200
|
+
else:
|
|
201
|
+
rid = _resolve_id(result.get("data", {}), id_keys)
|
|
202
|
+
click.echo(f"{label}: {rid}")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def emit_dry_run(ctx, *, summary: str, payload: dict) -> None:
|
|
206
|
+
"""Print a dry-run preview, honoring --quiet/--json.
|
|
207
|
+
|
|
208
|
+
quiet : print only the one-line summary (no body).
|
|
209
|
+
json : print {"dry_run": true, "payload": ...} compact.
|
|
210
|
+
human : print the summary line followed by the pretty payload.
|
|
211
|
+
"""
|
|
212
|
+
import click
|
|
213
|
+
|
|
214
|
+
from src.formatters.json_fmt import format_json
|
|
215
|
+
|
|
216
|
+
if getattr(ctx, "quiet_mode", False):
|
|
217
|
+
click.echo(f"[dry-run] {summary}")
|
|
218
|
+
return
|
|
219
|
+
if ctx.json_mode:
|
|
220
|
+
click.echo(format_json({"dry_run": True, "payload": payload}, compact=True))
|
|
221
|
+
else:
|
|
222
|
+
click.echo(f"[dry-run] {summary}")
|
|
223
|
+
click.echo(format_json(payload))
|
|
224
|
+
|
|
225
|
+
|
|
91
226
|
def make_client(ctx):
|
|
92
227
|
"""Create an UpscalerClient from CLI context.
|
|
93
228
|
|
src/cli/list_cmd.py
CHANGED
|
@@ -116,10 +116,16 @@ def list_group():
|
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
@list_group.command("definitions")
|
|
119
|
+
@click.option("--limit", default=20, type=int, help="Max results to return (1-200).")
|
|
120
|
+
@click.option("--offset", default=0, type=int, help="Offset for pagination.")
|
|
119
121
|
@pass_context
|
|
120
|
-
def list_definitions(ctx):
|
|
121
|
-
"""List all definitions (registers, records).
|
|
122
|
-
|
|
122
|
+
def list_definitions(ctx, limit, offset):
|
|
123
|
+
"""List all definitions (registers, records).
|
|
124
|
+
|
|
125
|
+
Results are paginated; use --limit/--offset to page through, matching
|
|
126
|
+
`list entries`.
|
|
127
|
+
"""
|
|
128
|
+
_do_list(ctx, type_name="definitions", limit=limit, offset=offset)
|
|
123
129
|
|
|
124
130
|
|
|
125
131
|
@list_group.command("entries")
|
|
@@ -167,6 +173,16 @@ def list_definitions(ctx):
|
|
|
167
173
|
"human label (case-insensitive). Implies --include-values."
|
|
168
174
|
),
|
|
169
175
|
)
|
|
176
|
+
@click.option(
|
|
177
|
+
"--fields",
|
|
178
|
+
"fields_csv",
|
|
179
|
+
default=None,
|
|
180
|
+
help=(
|
|
181
|
+
"Comma-separated field keys/labels to return under `values` "
|
|
182
|
+
"(e.g. --fields status,owner). Convenience alias for repeated "
|
|
183
|
+
"--select-value; both may be combined. Implies --include-values."
|
|
184
|
+
),
|
|
185
|
+
)
|
|
170
186
|
@click.option(
|
|
171
187
|
"--resolve-labels",
|
|
172
188
|
is_flag=True,
|
|
@@ -187,6 +203,7 @@ def list_entries(
|
|
|
187
203
|
offset,
|
|
188
204
|
include_values,
|
|
189
205
|
select_values_raw,
|
|
206
|
+
fields_csv,
|
|
190
207
|
resolve_labels,
|
|
191
208
|
):
|
|
192
209
|
"""List entries for a definition with optional filtering and sorting."""
|
|
@@ -199,13 +216,19 @@ def list_entries(
|
|
|
199
216
|
|
|
200
217
|
sort = _parse_sort(sort_raw) if sort_raw else None
|
|
201
218
|
|
|
219
|
+
# --fields is a comma-separated convenience alias for repeated
|
|
220
|
+
# --select-value; combine both into one projection spec list.
|
|
221
|
+
select_specs = list(select_values_raw)
|
|
222
|
+
if fields_csv:
|
|
223
|
+
select_specs.extend(f.strip() for f in fields_csv.split(",") if f.strip())
|
|
224
|
+
|
|
202
225
|
schema_fields = None
|
|
203
|
-
if
|
|
226
|
+
if select_specs or resolve_labels:
|
|
204
227
|
schema_fields = _fetch_schema_fields(ctx, definition_id)
|
|
205
228
|
|
|
206
229
|
select_values = None
|
|
207
|
-
if
|
|
208
|
-
select_values = _resolve_select_values(
|
|
230
|
+
if select_specs:
|
|
231
|
+
select_values = _resolve_select_values(select_specs, schema_fields)
|
|
209
232
|
include_values = True
|
|
210
233
|
|
|
211
234
|
_do_list(
|
src/cli/main.py
CHANGED
|
@@ -6,7 +6,9 @@ Global flags:
|
|
|
6
6
|
--json / --no-json Output structured JSON to stdout. Default from
|
|
7
7
|
$UPSCALER_OUTPUT (set to 'json' to make JSON the
|
|
8
8
|
shell-wide default; --no-json forces human output
|
|
9
|
-
when the env var is set).
|
|
9
|
+
when the env var is set). Passing --json explicitly
|
|
10
|
+
is always accepted and idempotent, even when the env
|
|
11
|
+
var already makes JSON the default.
|
|
10
12
|
--verbose Log HTTP request/response to stderr
|
|
11
13
|
--server Override REST API server URL
|
|
12
14
|
--profile NAME Use a named profile (auth + config). Default: prod.
|
|
@@ -25,8 +27,7 @@ from src.profile import resolve_profile
|
|
|
25
27
|
|
|
26
28
|
AGENT_SKILLS_URL = "https://github.com/upscaler-io/agent-skills"
|
|
27
29
|
_VERSION_MESSAGE = (
|
|
28
|
-
f"%(prog)s %(version)s\n"
|
|
29
|
-
f"Agent skills: {AGENT_SKILLS_URL}/tree/{src.RECOMMENDED_SKILLS_REF}"
|
|
30
|
+
f"%(prog)s %(version)s\n" f"Agent skills: {AGENT_SKILLS_URL}/tree/{src.RECOMMENDED_SKILLS_REF}"
|
|
30
31
|
)
|
|
31
32
|
|
|
32
33
|
|
|
@@ -34,27 +35,100 @@ def _default_json_mode():
|
|
|
34
35
|
return os.environ.get("UPSCALER_OUTPUT", "").lower() == "json"
|
|
35
36
|
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
# Global options that GlobalFlagGroup accepts in any position. Boolean flags
|
|
39
|
+
# take no value; value options carry the following token (or the --opt=value
|
|
40
|
+
# form).
|
|
41
|
+
_GLOBAL_BOOL_FLAGS = frozenset({"--json", "--no-json", "--quiet", "-q", "--verbose", "-v"})
|
|
42
|
+
_GLOBAL_VALUE_OPTS = frozenset({"--server", "--profile"})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _hoist_global_options(args):
|
|
46
|
+
"""Move the CLI's global options to the front of the argument list.
|
|
47
|
+
|
|
48
|
+
Click only consumes group-level options that appear before the subcommand,
|
|
49
|
+
so `upscaler health --json` fails with "No such option" even though
|
|
50
|
+
`upscaler --json health` works. Agents and people routinely place `--json`
|
|
51
|
+
(and friends) after the subcommand, so the root group hoists the known
|
|
52
|
+
global options to the front before Click parses the command.
|
|
53
|
+
|
|
54
|
+
Scanning stops at `--`, the end-of-options marker, leaving genuinely
|
|
55
|
+
positional arguments after it untouched. A global flag's literal spelling
|
|
56
|
+
passed as another option's value would be hoisted too; that pathological
|
|
57
|
+
case is not supported.
|
|
58
|
+
"""
|
|
59
|
+
hoisted = []
|
|
60
|
+
rest = []
|
|
61
|
+
i = 0
|
|
62
|
+
n = len(args)
|
|
63
|
+
while i < n:
|
|
64
|
+
tok = args[i]
|
|
65
|
+
if tok == "--":
|
|
66
|
+
rest.extend(args[i:])
|
|
67
|
+
break
|
|
68
|
+
if tok in _GLOBAL_BOOL_FLAGS:
|
|
69
|
+
hoisted.append(tok)
|
|
70
|
+
i += 1
|
|
71
|
+
elif tok in _GLOBAL_VALUE_OPTS:
|
|
72
|
+
hoisted.append(tok)
|
|
73
|
+
if i + 1 < n:
|
|
74
|
+
hoisted.append(args[i + 1])
|
|
75
|
+
i += 2
|
|
76
|
+
else:
|
|
77
|
+
i += 1
|
|
78
|
+
elif "=" in tok and tok.split("=", 1)[0] in _GLOBAL_VALUE_OPTS:
|
|
79
|
+
hoisted.append(tok)
|
|
80
|
+
i += 1
|
|
81
|
+
else:
|
|
82
|
+
rest.append(tok)
|
|
83
|
+
i += 1
|
|
84
|
+
return hoisted + rest
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class GlobalFlagGroup(click.Group):
|
|
88
|
+
"""Root group that accepts the CLI's global options in any position.
|
|
89
|
+
|
|
90
|
+
Global flags are defined on this group, which by default Click only parses
|
|
91
|
+
before the subcommand. Hoisting them (see _hoist_global_options) makes
|
|
92
|
+
`upscaler list entries --json` behave like `upscaler --json list entries`.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def parse_args(self, ctx, args):
|
|
96
|
+
return super().parse_args(ctx, _hoist_global_options(args))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@click.group(cls=GlobalFlagGroup, epilog=f"Agent skills for Upscaler: {AGENT_SKILLS_URL}")
|
|
38
100
|
@click.option(
|
|
39
101
|
"--json/--no-json",
|
|
40
102
|
"json_mode",
|
|
41
103
|
default=_default_json_mode,
|
|
42
104
|
help="Output structured JSON. Default from $UPSCALER_OUTPUT=json.",
|
|
43
105
|
)
|
|
106
|
+
@click.option(
|
|
107
|
+
"--quiet",
|
|
108
|
+
"-q",
|
|
109
|
+
"quiet_mode",
|
|
110
|
+
is_flag=True,
|
|
111
|
+
help="Print only the resulting id(s); suppress full payload/schema echo.",
|
|
112
|
+
)
|
|
44
113
|
@click.option("--verbose", "-v", is_flag=True, help="Log HTTP details to stderr.")
|
|
45
114
|
@click.option("--server", default=None, help="Override REST API server URL.")
|
|
46
115
|
@click.option(
|
|
47
116
|
"--profile",
|
|
48
117
|
default=None,
|
|
49
|
-
help="Profile name (own auth + config). Default: prod. "
|
|
50
|
-
"Overrides $UPSCALER_PROFILE.",
|
|
118
|
+
help="Profile name (own auth + config). Default: prod. " "Overrides $UPSCALER_PROFILE.",
|
|
51
119
|
)
|
|
52
120
|
@click.version_option(version=src.__version__, prog_name="upscaler", message=_VERSION_MESSAGE)
|
|
53
121
|
@click.pass_context
|
|
54
|
-
def cli(ctx, json_mode, verbose, server, profile):
|
|
55
|
-
"""Upscaler CLI: search, retrieve, and manage documents, records, and workflows.
|
|
122
|
+
def cli(ctx, json_mode, quiet_mode, verbose, server, profile):
|
|
123
|
+
"""Upscaler CLI: search, retrieve, and manage documents, records, and workflows.
|
|
124
|
+
|
|
125
|
+
Global flags (--json, --quiet, --profile, --server, --verbose) are options on
|
|
126
|
+
this top-level group but are accepted in any position, so both
|
|
127
|
+
`upscaler --json list entries ...` and `upscaler list entries --json` work.
|
|
128
|
+
"""
|
|
56
129
|
ctx.ensure_object(Context)
|
|
57
130
|
ctx.obj.json_mode = json_mode
|
|
131
|
+
ctx.obj.quiet_mode = quiet_mode
|
|
58
132
|
ctx.obj.verbose = verbose
|
|
59
133
|
ctx.obj.server_url = server
|
|
60
134
|
try:
|
|
@@ -86,8 +160,7 @@ def health(ctx):
|
|
|
86
160
|
verify_ssl = config.resolve_verify_ssl()
|
|
87
161
|
if not server_url:
|
|
88
162
|
click.echo(
|
|
89
|
-
"Server URL not configured. "
|
|
90
|
-
"Use --server or: upscaler config set server_url <url>",
|
|
163
|
+
"Server URL not configured. " "Use --server or: upscaler config set server_url <url>",
|
|
91
164
|
err=True,
|
|
92
165
|
)
|
|
93
166
|
sys.exit(1)
|
|
@@ -98,21 +171,30 @@ def health(ctx):
|
|
|
98
171
|
)
|
|
99
172
|
data = response.json()
|
|
100
173
|
if ctx.obj.json_mode:
|
|
101
|
-
click.echo(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
174
|
+
click.echo(
|
|
175
|
+
json.dumps(
|
|
176
|
+
{
|
|
177
|
+
"success": True,
|
|
178
|
+
"status": data.get("status"),
|
|
179
|
+
"server": server_url,
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
)
|
|
106
183
|
else:
|
|
107
184
|
click.echo(f"Server: {server_url}")
|
|
108
185
|
click.echo(f"Status: {data.get('status', 'unknown')}")
|
|
109
186
|
except Exception as e:
|
|
110
187
|
if ctx.obj.json_mode:
|
|
111
|
-
click.echo(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
188
|
+
click.echo(
|
|
189
|
+
json.dumps(
|
|
190
|
+
{
|
|
191
|
+
"success": False,
|
|
192
|
+
"error": str(e),
|
|
193
|
+
"server": server_url,
|
|
194
|
+
}
|
|
195
|
+
),
|
|
196
|
+
err=True,
|
|
197
|
+
)
|
|
116
198
|
else:
|
|
117
199
|
click.echo(f"Server: {server_url}")
|
|
118
200
|
click.echo(f"Status: unreachable ({e})", err=True)
|
src/client.py
CHANGED
|
@@ -105,7 +105,17 @@ class UpscalerClient:
|
|
|
105
105
|
raise APIError(f"Invalid JSON response from {path}", response.status_code)
|
|
106
106
|
|
|
107
107
|
if response.status_code >= 400:
|
|
108
|
-
|
|
108
|
+
err = result.get("error")
|
|
109
|
+
if isinstance(err, dict):
|
|
110
|
+
# Unified error envelope ({error_code, message, ...}); pass a
|
|
111
|
+
# readable string to APIError, not the raw dict.
|
|
112
|
+
error_msg = (
|
|
113
|
+
err.get("message")
|
|
114
|
+
or err.get("error_code")
|
|
115
|
+
or f"HTTP {response.status_code}"
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
error_msg = err or f"HTTP {response.status_code}"
|
|
109
119
|
raise APIError(error_msg, response.status_code)
|
|
110
120
|
|
|
111
121
|
return result
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
src/__init__.py,sha256=uPUKlWJW1lQznOQiQGxfCjKn6GR6sNapNL-2ihXFIqs,272
|
|
2
|
-
src/client.py,sha256=
|
|
2
|
+
src/client.py,sha256=uQbutI4MXPupXOIVUfKUIr_Z0PTM1W5uqDfjwxrvTZ8,5107
|
|
3
3
|
src/config.py,sha256=nOfHEXnUnxLNIv6Ko1LVbrCtrJyvLpGDoe_m7qG_0tw,4803
|
|
4
4
|
src/errors.py,sha256=BMaD32cmXavKfH6SdhsoD3jhBZgap-YpTWZC0IZO_Vo,832
|
|
5
5
|
src/profile.py,sha256=XhCkmVhs_G-ELM6avTytrpTeJ-REoMgx6a-iSA-liOk,3918
|
|
@@ -9,20 +9,20 @@ src/auth/encryption.py,sha256=ct3pC9kW94LnfZN3puGf9_PeRHSvNzfwK78Yf_YuYo0,2397
|
|
|
9
9
|
src/auth/oauth.py,sha256=PQ7Ov6LLkRYUyQp5lUuhFDBGIqQ-TGfLeaxqbn9qW6U,21213
|
|
10
10
|
src/auth/token_store.py,sha256=oQOArbgTozxNyypz6KFhC7OJxee0xCsQIMML6Jy88CI,4005
|
|
11
11
|
src/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
src/cli/asset.py,sha256=
|
|
12
|
+
src/cli/asset.py,sha256=SouscANT7rmkRMrdjnoF4GJYS8-RvmkLQDk7drDJijU,29487
|
|
13
13
|
src/cli/auth.py,sha256=N04ITXNrMfc8IvnjJDmgJRm3R5S62SdYDPm_s3tny9A,13976
|
|
14
14
|
src/cli/automation.py,sha256=jwHaE2zfRJ-k5d1p2rwdrOrGe56pqsqKopxpW3IYaZU,7062
|
|
15
15
|
src/cli/completions.py,sha256=skCxzy7h92Rt2a9xq_oFzWmf6ApaIGCVu0Dsc9TauYQ,786
|
|
16
16
|
src/cli/config_cmd.py,sha256=qqQNRaDKdtGajpOyIx0qFFgAH5lJbbrOLNcc0Dhq_oQ,1186
|
|
17
|
-
src/cli/context.py,sha256=
|
|
18
|
-
src/cli/entry.py,sha256=
|
|
17
|
+
src/cli/context.py,sha256=uqL_EWjLcDLsFv2YmNBtJlFud5Jd1mJv63mZMu4eIuo,480
|
|
18
|
+
src/cli/entry.py,sha256=WQba9TqUr4X8FYGeAhktu3iZpHGCfSgXVW-jb75D6tI,34728
|
|
19
19
|
src/cli/files.py,sha256=g8yiX2Nr2Nbg3bFbSDucLKUNOBCBHokNwqOAV_XyFeM,2349
|
|
20
20
|
src/cli/framework.py,sha256=XI5PU3oDbzzSJqPXkbuxkJxKoXaf6UbyzpRp23SD90M,14820
|
|
21
|
-
src/cli/get.py,sha256=
|
|
22
|
-
src/cli/helpers.py,sha256=
|
|
21
|
+
src/cli/get.py,sha256=40SVjx5aDcYT8Pj-TRuAUCcI5qEuPfTpRa3ZlOkeC2I,5261
|
|
22
|
+
src/cli/helpers.py,sha256=o4iGNlxmm_cU4qtoJqrKgU4ZKm5FElXejsFSTRgaZ2U,10107
|
|
23
23
|
src/cli/hierarchy.py,sha256=EUwkAMlstWqYI9dEazpmKAhDUMIM2qE3e8QTlvxYER8,1247
|
|
24
|
-
src/cli/list_cmd.py,sha256=
|
|
25
|
-
src/cli/main.py,sha256=
|
|
24
|
+
src/cli/list_cmd.py,sha256=Ldo0Dw-e39wG0tVcOMxP_z0UlpPTjgtxsBpoX0jpKm4,13648
|
|
25
|
+
src/cli/main.py,sha256=gDigKVHnSWIK7j7Up3CUeAtCaogiwxOcj2IoT1yrltE,8053
|
|
26
26
|
src/cli/profile_cmd.py,sha256=jhBg_Ac63Mwbiz7tgjOZ263vaexoIyWwHnbzSmAWa_Y,3365
|
|
27
27
|
src/cli/search.py,sha256=PfhWfULvglmwvp5Gm2XFA6Bu1yuiqkD-q7Muo1V7iGU,3735
|
|
28
28
|
src/cli/todo.py,sha256=Y51t5vvP4LiKKDlqf4dTALIw6VAan0W4X3UEVMowGKA,3586
|
|
@@ -30,8 +30,8 @@ src/formatters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
30
30
|
src/formatters/json_fmt.py,sha256=S66lMzKTlfG5ImLOO1y7TCEvbAOsg-bIBmVaYX--zLs,1091
|
|
31
31
|
src/formatters/table.py,sha256=rd2mHRIDpmiGH-QQnuwJWS4L0QC6cq7v14pe00GvKa4,1374
|
|
32
32
|
src/formatters/tree.py,sha256=5rC58JqZvsQLq68v0WMcyl6dI2t0dyBPNI9CicBO0gg,1311
|
|
33
|
-
upscaler_cli-0.2.3.
|
|
34
|
-
upscaler_cli-0.2.3.
|
|
35
|
-
upscaler_cli-0.2.3.
|
|
36
|
-
upscaler_cli-0.2.3.
|
|
37
|
-
upscaler_cli-0.2.3.
|
|
33
|
+
upscaler_cli-0.2.3.dev6697.dist-info/METADATA,sha256=oUhv-oaCXbh-M0ELnTQ1_jAzIuqZAygitVhTcpNAST0,601
|
|
34
|
+
upscaler_cli-0.2.3.dev6697.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
35
|
+
upscaler_cli-0.2.3.dev6697.dist-info/entry_points.txt,sha256=fG1SBWk0NO-b7H3BTDlxANSf2a9mztWytQ08xeI6U8I,46
|
|
36
|
+
upscaler_cli-0.2.3.dev6697.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
|
|
37
|
+
upscaler_cli-0.2.3.dev6697.dist-info/RECORD,,
|
|
File without changes
|
{upscaler_cli-0.2.3.dev6696.dist-info → upscaler_cli-0.2.3.dev6697.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|