upscaler-cli 0.2.3.dev6696__tar.gz → 0.2.3.dev6699__tar.gz

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.
Files changed (47) hide show
  1. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/PKG-INFO +1 -1
  2. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/pyproject.toml +1 -1
  3. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/auth/oauth.py +5 -1
  4. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/cli/asset.py +211 -48
  5. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/cli/auth.py +11 -0
  6. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/cli/context.py +1 -0
  7. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/cli/entry.py +99 -88
  8. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/cli/get.py +30 -9
  9. upscaler_cli-0.2.3.dev6699/src/cli/helpers.py +334 -0
  10. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/cli/hierarchy.py +3 -1
  11. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/cli/list_cmd.py +33 -7
  12. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/cli/main.py +102 -20
  13. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/cli/search.py +3 -1
  14. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/cli/todo.py +3 -1
  15. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/client.py +11 -1
  16. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/upscaler_cli.egg-info/PKG-INFO +1 -1
  17. upscaler_cli-0.2.3.dev6696/src/cli/helpers.py +0 -182
  18. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/README.md +0 -0
  19. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/setup.cfg +0 -0
  20. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/__init__.py +0 -0
  21. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/auth/__init__.py +0 -0
  22. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/auth/encryption.py +0 -0
  23. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/auth/token_store.py +0 -0
  24. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/cli/__init__.py +0 -0
  25. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/cli/automation.py +0 -0
  26. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/cli/completions.py +0 -0
  27. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/cli/config_cmd.py +0 -0
  28. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/cli/files.py +0 -0
  29. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/cli/framework.py +0 -0
  30. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/cli/profile_cmd.py +0 -0
  31. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/config.py +0 -0
  32. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/errors.py +0 -0
  33. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/formatters/__init__.py +0 -0
  34. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/formatters/json_fmt.py +0 -0
  35. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/formatters/table.py +0 -0
  36. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/formatters/tree.py +0 -0
  37. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/profile.py +0 -0
  38. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/src/uploads.py +0 -0
  39. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/tests/test_client.py +0 -0
  40. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/tests/test_config.py +0 -0
  41. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/tests/test_profile.py +0 -0
  42. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/tests/test_uploads.py +0 -0
  43. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/upscaler_cli.egg-info/SOURCES.txt +0 -0
  44. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/upscaler_cli.egg-info/dependency_links.txt +0 -0
  45. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/upscaler_cli.egg-info/entry_points.txt +0 -0
  46. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/upscaler_cli.egg-info/requires.txt +0 -0
  47. {upscaler_cli-0.2.3.dev6696 → upscaler_cli-0.2.3.dev6699}/upscaler_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: upscaler-cli
3
- Version: 0.2.3.dev6696
3
+ Version: 0.2.3.dev6699
4
4
  Summary: Upscaler CLI - search, retrieve, and manage documents, records, and workflows
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: click>=8.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "upscaler-cli"
7
- version = "0.2.3.dev6696"
7
+ version = "0.2.3.dev6699"
8
8
  description = "Upscaler CLI - search, retrieve, and manage documents, records, and workflows"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -477,7 +477,11 @@ class OAuthFlow:
477
477
  expires_at=time.time() + data.get("expires_in", 86400),
478
478
  client_id=token_data.client_id,
479
479
  client_secret=token_data.client_secret,
480
- organization_id=token_data.organization_id,
480
+ # Read org from the refresh response (matching the login path) so a
481
+ # refreshed session reflects the token's real scope. Fall back to the
482
+ # existing value when the endpoint omits it, to avoid nulling a
483
+ # previously-valid org.
484
+ organization_id=data.get("organization_id") or token_data.organization_id,
481
485
  token_endpoint=token_data.token_endpoint,
482
486
  )
483
487
 
@@ -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(format_table(
83
- data,
84
- columns=["asset_id", "title", "asset_type", "description"],
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("--type", "asset_type", required=True, help="Asset type to create.")
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, asset_id, data_input, values_type, file_pairs, content_types, dry_run,
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", "field_name", required=True,
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, "add_task_definition", asset_id=asset_id, data=data, dry_run=dry_run,
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 a markdown (or Slate JSON) file. Packed as the 'values' field.",
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, asset_id, task_definition_id, data_input, values_file, values_type, dry_run,
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 parse_data
440
+ from src.cli.helpers import build_values_payload
407
441
 
408
- if not data_input and not values_file:
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
- if ctx.json_mode:
506
- click.echo(format_json({"dry_run": True, "payload": payload}, compact=True))
507
- else:
508
- click.echo(f"[dry-run] Would {operation} asset:")
509
- click.echo(format_json(payload))
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
- if ctx.json_mode:
521
- click.echo(format_json(result, compact=True))
522
- else:
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
 
@@ -388,6 +388,7 @@ def status(ctx):
388
388
  return
389
389
 
390
390
  mode = "device" if token_data.client_id == "device" else "oauth"
391
+ refresh_present = bool(token_data.refresh_token)
391
392
 
392
393
  if ctx.json_mode:
393
394
  expires_in = int(token_data.expires_at - time.time())
@@ -397,11 +398,21 @@ def status(ctx):
397
398
  "expires_in": max(0, expires_in),
398
399
  "organization_id": token_data.organization_id,
399
400
  "mode": mode,
401
+ "refresh_token_present": refresh_present,
400
402
  }))
401
403
  else:
404
+ expires_in = int(token_data.expires_at - time.time())
402
405
  click.echo(f"Status: Authenticated ({mode})")
403
406
  if token_data.organization_id:
404
407
  click.echo(f"Organization: {token_data.organization_id}")
408
+ if expires_in <= 0:
409
+ click.echo("Token: expired (run: upscaler refresh)")
410
+ else:
411
+ click.echo(f"Expires in: {_format_duration(expires_in)}")
412
+ if refresh_present:
413
+ click.echo("Refresh token: present")
414
+ else:
415
+ click.echo("Refresh token: missing (re-login required on expiry)")
405
416
 
406
417
 
407
418
  @click.command()
@@ -12,6 +12,7 @@ class Context:
12
12
 
13
13
  def __init__(self):
14
14
  self.json_mode: bool = False
15
+ self.quiet_mode: bool = False
15
16
  self.verbose: bool = False
16
17
  self.server_url: Optional[str] = None
17
18
  self.profile: str = DEFAULT_PROFILE