upscaler-cli 0.2.3.dev6635__tar.gz → 0.2.3.dev6646__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.dev6635 → upscaler_cli-0.2.3.dev6646}/PKG-INFO +1 -1
  2. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/pyproject.toml +1 -1
  3. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/cli/entry.py +54 -11
  4. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/cli/framework.py +75 -9
  5. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/cli/list_cmd.py +47 -5
  6. upscaler_cli-0.2.3.dev6646/src/cli/search.py +129 -0
  7. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/upscaler_cli.egg-info/PKG-INFO +1 -1
  8. upscaler_cli-0.2.3.dev6635/src/cli/search.py +0 -49
  9. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/README.md +0 -0
  10. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/setup.cfg +0 -0
  11. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/__init__.py +0 -0
  12. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/auth/__init__.py +0 -0
  13. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/auth/encryption.py +0 -0
  14. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/auth/oauth.py +0 -0
  15. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/auth/token_store.py +0 -0
  16. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/cli/__init__.py +0 -0
  17. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/cli/asset.py +0 -0
  18. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/cli/auth.py +0 -0
  19. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/cli/automation.py +0 -0
  20. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/cli/completions.py +0 -0
  21. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/cli/config_cmd.py +0 -0
  22. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/cli/context.py +0 -0
  23. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/cli/files.py +0 -0
  24. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/cli/get.py +0 -0
  25. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/cli/helpers.py +0 -0
  26. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/cli/hierarchy.py +0 -0
  27. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/cli/main.py +0 -0
  28. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/cli/profile_cmd.py +0 -0
  29. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/cli/todo.py +0 -0
  30. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/client.py +0 -0
  31. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/config.py +0 -0
  32. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/errors.py +0 -0
  33. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/formatters/__init__.py +0 -0
  34. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/formatters/json_fmt.py +0 -0
  35. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/formatters/table.py +0 -0
  36. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/formatters/tree.py +0 -0
  37. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/profile.py +0 -0
  38. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/src/uploads.py +0 -0
  39. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/tests/test_client.py +0 -0
  40. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/tests/test_config.py +0 -0
  41. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/tests/test_profile.py +0 -0
  42. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/tests/test_uploads.py +0 -0
  43. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/upscaler_cli.egg-info/SOURCES.txt +0 -0
  44. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/upscaler_cli.egg-info/dependency_links.txt +0 -0
  45. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/upscaler_cli.egg-info/entry_points.txt +0 -0
  46. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/upscaler_cli.egg-info/requires.txt +0 -0
  47. {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6646}/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.dev6635
3
+ Version: 0.2.3.dev6646
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.dev6635"
7
+ version = "0.2.3.dev6646"
8
8
  description = "Upscaler CLI - search, retrieve, and manage documents, records, and workflows"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -567,8 +567,29 @@ def _resolve_field_descriptors(ctx, schema_fields, files):
567
567
  return resolved
568
568
 
569
569
 
570
+ def _bare_col_key(col_key):
571
+ """Reduce a possibly-dotted form-table column key to its bare column id.
572
+
573
+ The backend schema endpoint surfaces a table column's key as the dotted
574
+ composite ``ff_table.ff_col`` (the flattened form-field key), with a null
575
+ ``dataIndex``. The persisted row and every UI renderer, however, key the
576
+ cell by the BARE column id ``ff_col`` (``column.dataIndex``/``column.key``),
577
+ and the store writes values verbatim. Splicing under the dotted key
578
+ therefore produces a row that persists but never renders. We always reduce
579
+ to the bare last segment. Field ids never contain a literal dot, so the last
580
+ dot-segment is the column's own id; idempotent for already-bare keys. See B9
581
+ in the agent-skills alignment proposal for the upstream schema fix.
582
+ """
583
+ return col_key.rsplit(".", 1)[-1] if col_key else col_key
584
+
585
+
570
586
  def _resolve_nested(ctx, descriptor, raw_stripped, table_fields):
571
- """Resolve `Table.Column` into a {table_key, col_key} descriptor."""
587
+ """Resolve `Table.Column` into a {table_key, col_key} descriptor.
588
+
589
+ ``col_key`` is always returned BARE (see ``_bare_col_key``) so the splice
590
+ writes the row shape the store and UI expect, regardless of whether the
591
+ schema reported the column key as bare or dotted.
592
+ """
572
593
  table_raw, col_raw = (part.strip() for part in raw_stripped.split(".", 1))
573
594
  if not table_raw or not col_raw:
574
595
  click.echo(
@@ -586,13 +607,28 @@ def _resolve_nested(ctx, descriptor, raw_stripped, table_fields):
586
607
  c for c in columns if c.get("type") == AGENT_SCHEMA_FILE_UPLOAD_TYPE
587
608
  ]
588
609
 
589
- # Exact ff_* match against columns first, then case-insensitive label.
590
- col_by_key = {c["key"]: c for c in columns if c.get("key")}
610
+ # Match the column by its full schema key OR its bare last segment: the
611
+ # backend reports table columns as the dotted `ff_table.ff_col`, but a user
612
+ # (or the skill) may pass either the dotted path or the bare `ff_col`.
613
+ col_by_key = {}
614
+ for c in columns:
615
+ k = c.get("key")
616
+ if not k:
617
+ continue
618
+ col_by_key.setdefault(k, c)
619
+ col_by_key.setdefault(_bare_col_key(k), c)
620
+
591
621
  if col_raw in col_by_key:
592
622
  col = col_by_key[col_raw]
593
623
  if col.get("type") != AGENT_SCHEMA_FILE_UPLOAD_TYPE:
594
- _fail_loud_non_file_column(ctx, table, col_raw, file_columns)
595
- return {**descriptor, "table_key": table["key"], "col_key": col_raw}
624
+ _fail_loud_non_file_column(
625
+ ctx, table, _bare_col_key(col.get("key")), file_columns
626
+ )
627
+ return {
628
+ **descriptor,
629
+ "table_key": table["key"],
630
+ "col_key": _bare_col_key(col["key"]),
631
+ }
596
632
 
597
633
  norm = col_raw.lower()
598
634
  candidates = [
@@ -601,11 +637,16 @@ def _resolve_nested(ctx, descriptor, raw_stripped, table_fields):
601
637
  if (c.get("label") or "").strip().lower() == norm and c.get("key")
602
638
  ]
603
639
  if len(candidates) == 1:
604
- return {**descriptor, "table_key": table["key"], "col_key": candidates[0]}
640
+ return {
641
+ **descriptor,
642
+ "table_key": table["key"],
643
+ "col_key": _bare_col_key(candidates[0]),
644
+ }
605
645
  if len(candidates) > 1:
606
646
  click.echo(
607
647
  f'Column "{col_raw}" in table "{table.get("label") or table["key"]}" '
608
- f'matches multiple file columns: {", ".join(candidates)}. '
648
+ f'matches multiple file columns: '
649
+ f'{", ".join(_bare_col_key(k) for k in candidates)}. '
609
650
  f"Pass the ff_* key to disambiguate.",
610
651
  err=True,
611
652
  )
@@ -641,8 +682,8 @@ def _fail_loud_unknown(ctx, field_name, file_fields, table_fields=()):
641
682
  if isinstance(c, dict) and c.get("type") == AGENT_SCHEMA_FILE_UPLOAD_TYPE:
642
683
  nested.append(
643
684
  f'"{t.get("label") or t.get("key")}.'
644
- f'{c.get("label") or c.get("key")}" → '
645
- f'{t.get("key")}.{c.get("key")}'
685
+ f'{c.get("label") or _bare_col_key(c.get("key"))}" → '
686
+ f'{t.get("key")}.{_bare_col_key(c.get("key"))}'
646
687
  )
647
688
  if nested:
648
689
  extra = (
@@ -686,7 +727,7 @@ def _fail_loud_unknown_table(ctx, table_raw, table_fields):
686
727
 
687
728
  def _fail_loud_unknown_column(ctx, table, col_raw, file_columns):
688
729
  available = ", ".join(
689
- f'"{c.get("label")}" → {c.get("key")}' for c in file_columns
730
+ f'"{c.get("label")}" → {_bare_col_key(c.get("key"))}' for c in file_columns
690
731
  ) or "(none)"
691
732
  click.echo(
692
733
  f'No file column "{col_raw}" in table '
@@ -698,7 +739,9 @@ def _fail_loud_unknown_column(ctx, table, col_raw, file_columns):
698
739
 
699
740
 
700
741
  def _fail_loud_non_file_column(ctx, table, col_key, file_columns):
701
- available = ", ".join(c.get("key") for c in file_columns) or "(none)"
742
+ available = ", ".join(
743
+ _bare_col_key(c.get("key")) for c in file_columns
744
+ ) or "(none)"
702
745
  click.echo(
703
746
  f'Column "{col_key}" in table '
704
747
  f'"{table.get("label") or table.get("key")}" is not a file-upload column. '
@@ -16,17 +16,21 @@ from src.cli.helpers import execute_rest_action
16
16
  def framework_group():
17
17
  """Manage installed compliance frameworks (ISO 27001 etc.).
18
18
 
19
+ The installed-framework id is the catalog id (a colon, e.g. ``iso27001:2022``),
20
+ NOT an ``if_*`` value and NOT the hyphen form. A hyphen form (``iso27001-2022``)
21
+ is normalized to the colon form for convenience.
22
+
19
23
  Examples:
20
24
  upscaler framework list-installed
21
- upscaler framework get-installed iso27001-2022
25
+ upscaler framework get-installed iso27001:2022
22
26
  upscaler framework list-contributions --asset-id d_xyz
23
27
  upscaler framework list-test-bindings --asset-id d_xyz
24
28
  upscaler framework list-requirement-contributions \\
25
- --framework-id iso27001-2022 --requirement-id A.5.1
26
- upscaler framework bind --framework-id iso27001-2022 --data @binding.json
27
- upscaler framework set-test-binding --framework-id iso27001-2022 \\
29
+ --framework-id iso27001:2022 --requirement-id A.5.1
30
+ upscaler framework bind --framework-id iso27001:2022 --data @binding.json
31
+ upscaler framework set-test-binding --framework-id iso27001:2022 \\
28
32
  --requirement-id A.5.1 --test-id t-exists --asset-id d_xyz
29
- upscaler framework evaluate --framework-id iso27001-2022 --requirement-id A.5.1
33
+ upscaler framework evaluate --framework-id iso27001:2022 --requirement-id A.5.1
30
34
  """
31
35
  pass
32
36
 
@@ -76,11 +80,60 @@ def list_requirement_contributions(ctx, framework_id, requirement_id):
76
80
 
77
81
 
78
82
  @framework_group.command("list-test-bindings")
79
- @click.option("--asset-id", required=True)
83
+ @click.option("--asset-id", default=None, help="Per-asset: Tests bound to this asset.")
84
+ @click.option(
85
+ "--framework-id",
86
+ default=None,
87
+ help="Framework-wide: every Test/binding row for this installed framework.",
88
+ )
89
+ @click.option(
90
+ "--source",
91
+ default=None,
92
+ type=click.Choice(["autoBind", "override", "ambiguous"]),
93
+ help="Framework-wide filter: only bindings with this source.",
94
+ )
95
+ @click.option(
96
+ "--unbound-only",
97
+ is_flag=True,
98
+ help="Framework-wide filter: only Tests with no bound asset.",
99
+ )
100
+ @click.option(
101
+ "--coverage-state",
102
+ default=None,
103
+ help="Framework-wide filter: only requirements in this coverage state.",
104
+ )
80
105
  @pass_context
81
- def list_test_bindings(ctx, asset_id):
82
- """List framework Tests bound to an asset."""
83
- _execute(ctx, {"action": "list_asset_test_bindings", "asset_id": asset_id})
106
+ def list_test_bindings(ctx, asset_id, framework_id, source, unbound_only, coverage_state):
107
+ """List framework Tests bound to an asset, or framework-wide.
108
+
109
+ Pass --asset-id for the per-asset inverse (Tests bound to one asset), or
110
+ --framework-id to get every Test/binding row across the framework in one
111
+ call (optionally filtered by --source / --unbound-only / --coverage-state)
112
+ to answer "which bindings are ambiguous / how many untested?".
113
+ """
114
+ if framework_id:
115
+ data = {}
116
+ if source:
117
+ data["source"] = source
118
+ if unbound_only:
119
+ data["unbound_only"] = True
120
+ if coverage_state:
121
+ data["coverage_state"] = coverage_state
122
+ payload = {
123
+ "action": "list_framework_test_bindings",
124
+ "framework_id": framework_id,
125
+ }
126
+ if data:
127
+ payload["data"] = data
128
+ _execute(ctx, payload)
129
+ elif asset_id:
130
+ _execute(ctx, {"action": "list_asset_test_bindings", "asset_id": asset_id})
131
+ else:
132
+ click.echo(
133
+ "Provide either --asset-id (per-asset) or --framework-id (framework-wide).",
134
+ err=True,
135
+ )
136
+ sys.exit(1)
84
137
 
85
138
 
86
139
  # ---------------------------------------------------------------------------
@@ -330,6 +383,19 @@ def evaluate(ctx, framework_id, requirement_id, test_id):
330
383
  _execute(ctx, payload)
331
384
 
332
385
 
386
+ @framework_group.command("sweep")
387
+ @click.option("--framework-id", required=True)
388
+ @pass_context
389
+ def sweep(ctx, framework_id):
390
+ """Re-evaluate every Test on every Requirement in one call (framework-wide).
391
+
392
+ One call replaces a per-Requirement ``evaluate`` loop after a batch of
393
+ binding/Test changes. Respects the SoA "Included = No" gate. Returns the
394
+ full installed-framework projection with refreshed coverageState.
395
+ """
396
+ _execute(ctx, {"action": "sweep_installed", "framework_id": framework_id})
397
+
398
+
333
399
  # ---------------------------------------------------------------------------
334
400
  # Helpers
335
401
  # ---------------------------------------------------------------------------
@@ -357,12 +357,51 @@ def _do_list(
357
357
  click.echo(format_table(data))
358
358
 
359
359
 
360
- def _rewrite_values_to_labels(result, schema_fields):
361
- """Rewrite each item's `values` dict keys from ff_* IDs to human labels.
360
+ def _relabel_nested(value, subid_to_label):
361
+ """Recurse into table rows / composite (date-range) sub-values.
362
+
363
+ Table/composite values are dicts (or lists of dicts) keyed by the column's
364
+ trailing sub-id; rewrite those sub-ids to their column labels. Scalars pass
365
+ through unchanged.
366
+ """
367
+ if isinstance(value, dict):
368
+ return {
369
+ subid_to_label.get(sub_k, sub_k): _relabel_nested(sub_v, subid_to_label)
370
+ for sub_k, sub_v in value.items()
371
+ }
372
+ if isinstance(value, list):
373
+ return [_relabel_nested(item, subid_to_label) for item in value]
374
+ return value
362
375
 
363
- Unmapped keys are left as-is to tolerate schema drift mid-paginate.
376
+
377
+ def _rewrite_values_to_labels(result, schema_fields):
378
+ """Rewrite each item's `values` keys from ff_* IDs to human labels.
379
+
380
+ Handles three field shapes:
381
+ - scalar fields: schema `key` is a bare `ff_...` matching the values key.
382
+ - table / composite fields: schema `key` is prefixed `values.ff_...` (the
383
+ values dict uses the bare `ff_...`), and the value is a dict or list of
384
+ dicts keyed by a column sub-id; `columns[].key` is `values.ff_....<subid>`.
385
+ Both the field key and nested column sub-ids are resolved. Unmapped keys
386
+ are left as-is to tolerate schema drift mid-paginate.
364
387
  """
365
- key_to_label = {f["key"]: f["label"] for f in schema_fields if f.get("key") and f.get("label")}
388
+ key_to_label = {}
389
+ subid_to_label = {}
390
+ for field in schema_fields:
391
+ key = field.get("key")
392
+ label = field.get("label")
393
+ if key and label:
394
+ key_to_label[key] = label
395
+ # Table/composite field keys carry a `values.` prefix that the
396
+ # item's values dict does not; map the bare form too.
397
+ if key.startswith("values."):
398
+ key_to_label[key[len("values."):]] = label
399
+ for col in field.get("columns") or []:
400
+ col_key = col.get("key")
401
+ col_label = col.get("label")
402
+ if col_key and col_label:
403
+ subid_to_label[col_key.rsplit(".", 1)[-1]] = col_label
404
+
366
405
  data = result.get("data") if isinstance(result, dict) else None
367
406
  items = data.get("items") if isinstance(data, dict) else None
368
407
  if not isinstance(items, list):
@@ -371,4 +410,7 @@ def _rewrite_values_to_labels(result, schema_fields):
371
410
  values = item.get("values") if isinstance(item, dict) else None
372
411
  if not isinstance(values, dict):
373
412
  continue
374
- item["values"] = {key_to_label.get(k, k): v for k, v in values.items()}
413
+ item["values"] = {
414
+ key_to_label.get(k, k): _relabel_nested(v, subid_to_label)
415
+ for k, v in values.items()
416
+ }
@@ -0,0 +1,129 @@
1
+ """Search command for document search."""
2
+
3
+ import asyncio
4
+
5
+ import click
6
+
7
+ from src.cli.context import pass_context
8
+ from src.cli.helpers import handle_error
9
+
10
+
11
+ @click.command()
12
+ @click.argument("query")
13
+ @click.option("--limit", default=10, help="Number of results to return.")
14
+ @click.option(
15
+ "--type",
16
+ "asset_type",
17
+ default=None,
18
+ help="Filter by asset type (raw enum, e.g. document_definition, item).",
19
+ )
20
+ @click.option(
21
+ "--parent-id",
22
+ "parent_id",
23
+ default=None,
24
+ help="Restrict the search to descendants of this asset (e.g. the ISMS root d_*).",
25
+ )
26
+ @click.option(
27
+ "--published-after",
28
+ "published_after",
29
+ default=None,
30
+ help="Only results published on/after this ISO 8601 date (e.g. 2025-01-01).",
31
+ )
32
+ @click.option(
33
+ "--published-before",
34
+ "published_before",
35
+ default=None,
36
+ help="Only results published on/before this ISO 8601 date.",
37
+ )
38
+ @click.option("--tag", "tags", multiple=True, help="Filter by tag id (repeatable).")
39
+ @click.option(
40
+ "--sort-by",
41
+ "sort_by",
42
+ default=None,
43
+ type=click.Choice(["relevance", "date", "title"]),
44
+ help="Sort order (default: relevance).",
45
+ )
46
+ @click.option(
47
+ "--score-threshold",
48
+ "score_threshold",
49
+ default=None,
50
+ type=float,
51
+ help="Minimum relevance score (0.0-1.0).",
52
+ )
53
+ @click.option(
54
+ "--include-metadata/--no-include-metadata",
55
+ "include_metadata",
56
+ default=False,
57
+ help=(
58
+ "Include the per-result raw chunk metadata block. Off by default "
59
+ "(lean response); turn on only when you need raw chunk metadata."
60
+ ),
61
+ )
62
+ @pass_context
63
+ def search(
64
+ ctx,
65
+ query,
66
+ limit,
67
+ asset_type,
68
+ parent_id,
69
+ published_after,
70
+ published_before,
71
+ tags,
72
+ sort_by,
73
+ score_threshold,
74
+ include_metadata,
75
+ ):
76
+ """Search Upscaler documents.
77
+
78
+ Hybrid semantic + keyword search over document AND register-entry content.
79
+ Results are embedding chunks: cite by ``asset_id`` (not the top-level ``id``)
80
+ and de-duplicate by ``asset_id``.
81
+
82
+ Examples:
83
+ upscaler search "safety procedures"
84
+ upscaler search "compliance" --limit 5 --type document_definition
85
+ upscaler search "access control" --parent-id d_isms --published-after 2025-01-01
86
+ upscaler --json search "audit"
87
+ """
88
+ from src.cli.helpers import make_client
89
+ from src.formatters.json_fmt import format_json
90
+ from src.formatters.table import format_table
91
+
92
+ client = make_client(ctx)
93
+
94
+ # Build the request body. Send only the params the caller set so the
95
+ # server's defaults apply to everything else. The REST /api/v1/search
96
+ # model accepts all of these (SearchBase).
97
+ body = {"query": query, "limit": limit, "include_metadata": include_metadata}
98
+ if asset_type is not None:
99
+ body["asset_type"] = asset_type
100
+ if parent_id is not None:
101
+ body["parent_id"] = parent_id
102
+ if published_after is not None:
103
+ body["published_after"] = published_after
104
+ if published_before is not None:
105
+ body["published_before"] = published_before
106
+ if tags:
107
+ body["tags"] = list(tags)
108
+ if sort_by is not None:
109
+ body["sort_by"] = sort_by
110
+ if score_threshold is not None:
111
+ body["score_threshold"] = score_threshold
112
+
113
+ try:
114
+ result = asyncio.run(client.request("POST", "/api/v1/search", json=body))
115
+ except Exception as e:
116
+ handle_error(ctx, e)
117
+ return
118
+
119
+ if ctx.json_mode:
120
+ click.echo(format_json(result, compact=True))
121
+ else:
122
+ data = result.get("data", [])
123
+ if not data:
124
+ click.echo("No results found.")
125
+ return
126
+ click.echo(format_table(
127
+ data,
128
+ columns=["score", "asset_id", "asset_title", "asset_type"],
129
+ ))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: upscaler-cli
3
- Version: 0.2.3.dev6635
3
+ Version: 0.2.3.dev6646
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
@@ -1,49 +0,0 @@
1
- """Search command for document search."""
2
-
3
- import asyncio
4
-
5
- import click
6
-
7
- from src.cli.context import pass_context
8
- from src.cli.helpers import handle_error
9
-
10
-
11
- @click.command()
12
- @click.argument("query")
13
- @click.option("--limit", default=10, help="Number of results to return.")
14
- @click.option("--type", "asset_type", default=None, help="Filter by asset type.")
15
- @pass_context
16
- def search(ctx, query, limit, asset_type):
17
- """Search Upscaler documents.
18
-
19
- Examples:
20
- upscaler search "safety procedures"
21
- upscaler search "compliance" --limit 5 --type policy
22
- upscaler --json search "audit"
23
- """
24
- from src.cli.helpers import make_client
25
- from src.formatters.json_fmt import format_json
26
- from src.formatters.table import format_table
27
-
28
- client = make_client(ctx)
29
-
30
- try:
31
- result = asyncio.run(client.request(
32
- "POST", "/api/v1/search",
33
- json={"query": query, "limit": limit, "asset_type": asset_type},
34
- ))
35
- except Exception as e:
36
- handle_error(ctx, e)
37
- return
38
-
39
- if ctx.json_mode:
40
- click.echo(format_json(result, compact=True))
41
- else:
42
- data = result.get("data", [])
43
- if not data:
44
- click.echo("No results found.")
45
- return
46
- click.echo(format_table(
47
- data,
48
- columns=["score", "asset_id", "asset_title", "asset_type"],
49
- ))