upscaler-cli 0.2.3.dev6635__tar.gz → 0.2.3.dev6696__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.
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/PKG-INFO +1 -1
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/pyproject.toml +1 -1
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/cli/entry.py +54 -11
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/cli/framework.py +75 -9
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/cli/list_cmd.py +47 -5
- upscaler_cli-0.2.3.dev6696/src/cli/search.py +129 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/upscaler_cli.egg-info/PKG-INFO +1 -1
- upscaler_cli-0.2.3.dev6635/src/cli/search.py +0 -49
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/README.md +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/setup.cfg +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/__init__.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/auth/__init__.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/auth/encryption.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/auth/oauth.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/auth/token_store.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/cli/__init__.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/cli/asset.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/cli/auth.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/cli/automation.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/cli/completions.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/cli/config_cmd.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/cli/context.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/cli/files.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/cli/get.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/cli/helpers.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/cli/hierarchy.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/cli/main.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/cli/profile_cmd.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/cli/todo.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/client.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/config.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/errors.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/formatters/__init__.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/formatters/json_fmt.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/formatters/table.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/formatters/tree.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/profile.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/src/uploads.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/tests/test_client.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/tests/test_config.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/tests/test_profile.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/tests/test_uploads.py +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/upscaler_cli.egg-info/SOURCES.txt +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/upscaler_cli.egg-info/dependency_links.txt +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/upscaler_cli.egg-info/entry_points.txt +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/upscaler_cli.egg-info/requires.txt +0 -0
- {upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/upscaler_cli.egg-info/top_level.txt +0 -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.
|
|
7
|
+
version = "0.2.3.dev6696"
|
|
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
|
-
#
|
|
590
|
-
|
|
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(
|
|
595
|
-
|
|
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 {
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
26
|
-
upscaler framework bind --framework-id iso27001
|
|
27
|
-
upscaler framework set-test-binding --framework-id iso27001
|
|
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
|
|
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",
|
|
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
|
-
|
|
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
|
|
361
|
-
"""
|
|
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
|
-
|
|
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 = {
|
|
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"] = {
|
|
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,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
|
-
))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/upscaler_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/upscaler_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/upscaler_cli.egg-info/requires.txt
RENAMED
|
File without changes
|
{upscaler_cli-0.2.3.dev6635 → upscaler_cli-0.2.3.dev6696}/upscaler_cli.egg-info/top_level.txt
RENAMED
|
File without changes
|