healthcloud-cli 3.7.2__tar.gz → 3.7.3__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.
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/PKG-INFO +43 -16
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/README.md +42 -15
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/agents/commands.py +79 -10
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/agents/operations.py +149 -24
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/errors.py +46 -23
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/pyproject.toml +1 -1
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/.gitignore +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/hatch_build.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/__init__.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/admins/__init__.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/admins/commands.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/admins/group.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/admins/operations.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/__init__.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/agents/__init__.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/agents/group.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/agents/projections.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/auth.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/commands.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/compatibility.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/context.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/data_sources/__init__.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/data_sources/commands.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/data_sources/group.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/data_sources/operations.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/data_sources/projections.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/departments/__init__.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/departments/commands.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/departments/group.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/departments/operations.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/departments/projections.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/domain_errors.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/group.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/guardrails/__init__.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/guardrails/commands.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/guardrails/group.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/guardrails/operations.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/guardrails/projections.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/organization/__init__.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/organization/commands.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/organization/group.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/organization/operations.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/organization/projections.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/output.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/policies/__init__.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/policies/commands.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/policies/group.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/policies/operations.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/policies/projections.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/pretty.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/registry.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/results.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/session.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/sessions/__init__.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/sessions/commands.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/sessions/group.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/sessions/operations.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/sessions/projections.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/skills/__init__.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/skills/commands.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/skills/group.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/skills/operations.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/skills/projections.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/target.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/teams/__init__.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/teams/commands.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/teams/group.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/teams/operations.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/teams/projections.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/tools/__init__.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/tools/commands.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/tools/group.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/tools/operations.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/tools/projections.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/tuning.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/ui.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/windows_files.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/cli.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/context.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/contracts/__init__.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/contracts/mcp-tools.manifest.json +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/main.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/manifest.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/output.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/params.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/tenants/__init__.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/tenants/commands.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/tenants/group.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/tenants/operations.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/util.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/scripts/__init__.py +0 -0
- {healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/scripts/bundle_manifest.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: healthcloud-cli
|
|
3
|
-
Version: 3.7.
|
|
3
|
+
Version: 3.7.3
|
|
4
4
|
Summary: Command-line interface for the HealthCloud MCP gateway, generated from the shared MCP tool manifest.
|
|
5
5
|
Project-URL: Issues, https://github.com/healthcloudservices/healthcloud-services/issues
|
|
6
6
|
Project-URL: Repository, https://github.com/healthcloudservices/healthcloud-services
|
|
@@ -576,11 +576,23 @@ raw JSON line. `--output json` prints the full envelope above, byte-identical to
|
|
|
576
576
|
before. An unknown outcome still prints its one-line `error:` plus the
|
|
577
577
|
reconciliation on stderr.
|
|
578
578
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
579
|
+
Without `--confirm` the command is a **preview**: it re-reads the agent exactly
|
|
580
|
+
once (the same one bounded `GET`), runs the same local pre-checks, prints
|
|
581
|
+
`preview: --confirm was not passed, so nothing has been sent.` and then the
|
|
582
|
+
resolved change to stderr — the target (`agent_id`, `name`, `type`,
|
|
583
|
+
`config_version`), `status` and `active` before -> after, and
|
|
584
|
+
`one PUT /agents/{agent_id} with body keys: status, active` — and exits 2 having
|
|
585
|
+
sent nothing. With `--confirm` the write path is unchanged: it re-reads before
|
|
586
|
+
the single `PUT`, so a preview is never an approval token. The preview text is
|
|
587
|
+
suppressed under `--output json`/`raw` (the same rule the confirmed path's
|
|
588
|
+
review follows) so those modes keep stderr machine-parsable.
|
|
589
|
+
|
|
590
|
+
Pre-checks refuse with exit 1 and **no write**: an agent outside the explicit
|
|
591
|
+
tenant, an already in-effect state, an unsupported `archived`/`draft` target
|
|
592
|
+
(named as having no supported reversible transition here), or a
|
|
593
|
+
`config_version` that no longer matches `--expected-config-version` — all of
|
|
594
|
+
these are also enforced by the preview, which still sends nothing. A missing
|
|
595
|
+
`--confirm` exits 2. That version check is a local best-effort
|
|
584
596
|
guard only: the route has no `If-Match` and the server never compares the
|
|
585
597
|
version, so it cannot prove atomicity and a request could still race between
|
|
586
598
|
the read and the PUT.
|
|
@@ -657,15 +669,23 @@ the server checks tenant scope only for the department, team, skill, policy and
|
|
|
657
669
|
guardrail references. This command does not imply otherwise, and it does not
|
|
658
670
|
re-implement the server's reference checks.
|
|
659
671
|
|
|
660
|
-
|
|
661
|
-
|
|
672
|
+
Without `--confirm` the command is a **preview**: it reads the agent exactly once
|
|
673
|
+
(the same one bounded `GET`), computes the same reviewed diff and runs the same
|
|
674
|
+
local pre-checks, prints `preview: --confirm was not passed, so nothing has been
|
|
675
|
+
sent.` followed by the reviewed diff, and exits 2 having sent no `PUT`. With
|
|
676
|
+
`--confirm` the write path is unchanged: it re-reads before the single `PUT`, so
|
|
677
|
+
a preview is never an approval token. The preview text is suppressed under
|
|
678
|
+
`--output json`/`raw`, like the confirmed path's review.
|
|
679
|
+
|
|
680
|
+
Pre-checks refuse with exit 1 and **no write**: no assignment option at all
|
|
662
681
|
(exit 2), setting and clearing the same family together (exit 2), an agent
|
|
663
682
|
outside the explicit tenant, a request where every reference already equals the
|
|
664
683
|
read state ("already in effect"), or a `config_version` that no longer matches
|
|
665
|
-
`--expected-config-version
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
the
|
|
684
|
+
`--expected-config-version` — all of these are also enforced by the preview,
|
|
685
|
+
which still sends nothing. A missing `--confirm` exits 2. That version check is
|
|
686
|
+
a local best-effort guard only: the route has no `If-Match` and the server never
|
|
687
|
+
compares the version, so it cannot prove atomicity and a request could still
|
|
688
|
+
race between the read and the PUT. Operation-specific approval here is the CLI's own `--confirm` plus the
|
|
669
689
|
reviewed diff, deliberately separate from the tuning/plan-apply approval path;
|
|
670
690
|
the server enforces tenant isolation, reference existence and the audit log, and
|
|
671
691
|
it has **no** assignment approval or consent entity.
|
|
@@ -733,10 +753,17 @@ disclosure: `agent_id`, `deleted: true`, `target`, `disclosure`, `evidence`
|
|
|
733
753
|
reviewed target and disclosure above are still printed to stderr in `pretty`
|
|
734
754
|
mode only; `--output json`/`raw` keep stderr machine-parsable.
|
|
735
755
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
756
|
+
Without `--confirm` the command is a **preview**: it reads the agent exactly once
|
|
757
|
+
(the same one bounded `GET`), prints `preview: --confirm was not passed, so
|
|
758
|
+
nothing has been sent.` plus the resolved destructive target and the whole
|
|
759
|
+
disclosure block to stderr, and exits 2 having sent no `DELETE`. With `--confirm`
|
|
760
|
+
the sequence above is unchanged: it re-reads before the single `DELETE`, so a
|
|
761
|
+
preview is never an approval token. The preview text is suppressed under
|
|
762
|
+
`--output json`/`raw`, like the confirmed path's review.
|
|
763
|
+
|
|
764
|
+
Refusals claim nothing and send nothing extra: an agent outside the explicit
|
|
765
|
+
tenant, the tenant identity mismatch and a delete target that no longer exists
|
|
766
|
+
are refused before the `DELETE` (the preview reports the same `not_found`); a `409` is a `conflict` refusal naming the referring
|
|
740
767
|
schedules; a `404` from the `DELETE` is `not_found`; and a `405` is the server's
|
|
741
768
|
refusal (`unsupported`). None of them is retried.
|
|
742
769
|
|
|
@@ -558,11 +558,23 @@ raw JSON line. `--output json` prints the full envelope above, byte-identical to
|
|
|
558
558
|
before. An unknown outcome still prints its one-line `error:` plus the
|
|
559
559
|
reconciliation on stderr.
|
|
560
560
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
561
|
+
Without `--confirm` the command is a **preview**: it re-reads the agent exactly
|
|
562
|
+
once (the same one bounded `GET`), runs the same local pre-checks, prints
|
|
563
|
+
`preview: --confirm was not passed, so nothing has been sent.` and then the
|
|
564
|
+
resolved change to stderr — the target (`agent_id`, `name`, `type`,
|
|
565
|
+
`config_version`), `status` and `active` before -> after, and
|
|
566
|
+
`one PUT /agents/{agent_id} with body keys: status, active` — and exits 2 having
|
|
567
|
+
sent nothing. With `--confirm` the write path is unchanged: it re-reads before
|
|
568
|
+
the single `PUT`, so a preview is never an approval token. The preview text is
|
|
569
|
+
suppressed under `--output json`/`raw` (the same rule the confirmed path's
|
|
570
|
+
review follows) so those modes keep stderr machine-parsable.
|
|
571
|
+
|
|
572
|
+
Pre-checks refuse with exit 1 and **no write**: an agent outside the explicit
|
|
573
|
+
tenant, an already in-effect state, an unsupported `archived`/`draft` target
|
|
574
|
+
(named as having no supported reversible transition here), or a
|
|
575
|
+
`config_version` that no longer matches `--expected-config-version` — all of
|
|
576
|
+
these are also enforced by the preview, which still sends nothing. A missing
|
|
577
|
+
`--confirm` exits 2. That version check is a local best-effort
|
|
566
578
|
guard only: the route has no `If-Match` and the server never compares the
|
|
567
579
|
version, so it cannot prove atomicity and a request could still race between
|
|
568
580
|
the read and the PUT.
|
|
@@ -639,15 +651,23 @@ the server checks tenant scope only for the department, team, skill, policy and
|
|
|
639
651
|
guardrail references. This command does not imply otherwise, and it does not
|
|
640
652
|
re-implement the server's reference checks.
|
|
641
653
|
|
|
642
|
-
|
|
643
|
-
|
|
654
|
+
Without `--confirm` the command is a **preview**: it reads the agent exactly once
|
|
655
|
+
(the same one bounded `GET`), computes the same reviewed diff and runs the same
|
|
656
|
+
local pre-checks, prints `preview: --confirm was not passed, so nothing has been
|
|
657
|
+
sent.` followed by the reviewed diff, and exits 2 having sent no `PUT`. With
|
|
658
|
+
`--confirm` the write path is unchanged: it re-reads before the single `PUT`, so
|
|
659
|
+
a preview is never an approval token. The preview text is suppressed under
|
|
660
|
+
`--output json`/`raw`, like the confirmed path's review.
|
|
661
|
+
|
|
662
|
+
Pre-checks refuse with exit 1 and **no write**: no assignment option at all
|
|
644
663
|
(exit 2), setting and clearing the same family together (exit 2), an agent
|
|
645
664
|
outside the explicit tenant, a request where every reference already equals the
|
|
646
665
|
read state ("already in effect"), or a `config_version` that no longer matches
|
|
647
|
-
`--expected-config-version
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
the
|
|
666
|
+
`--expected-config-version` — all of these are also enforced by the preview,
|
|
667
|
+
which still sends nothing. A missing `--confirm` exits 2. That version check is
|
|
668
|
+
a local best-effort guard only: the route has no `If-Match` and the server never
|
|
669
|
+
compares the version, so it cannot prove atomicity and a request could still
|
|
670
|
+
race between the read and the PUT. Operation-specific approval here is the CLI's own `--confirm` plus the
|
|
651
671
|
reviewed diff, deliberately separate from the tuning/plan-apply approval path;
|
|
652
672
|
the server enforces tenant isolation, reference existence and the audit log, and
|
|
653
673
|
it has **no** assignment approval or consent entity.
|
|
@@ -715,10 +735,17 @@ disclosure: `agent_id`, `deleted: true`, `target`, `disclosure`, `evidence`
|
|
|
715
735
|
reviewed target and disclosure above are still printed to stderr in `pretty`
|
|
716
736
|
mode only; `--output json`/`raw` keep stderr machine-parsable.
|
|
717
737
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
738
|
+
Without `--confirm` the command is a **preview**: it reads the agent exactly once
|
|
739
|
+
(the same one bounded `GET`), prints `preview: --confirm was not passed, so
|
|
740
|
+
nothing has been sent.` plus the resolved destructive target and the whole
|
|
741
|
+
disclosure block to stderr, and exits 2 having sent no `DELETE`. With `--confirm`
|
|
742
|
+
the sequence above is unchanged: it re-reads before the single `DELETE`, so a
|
|
743
|
+
preview is never an approval token. The preview text is suppressed under
|
|
744
|
+
`--output json`/`raw`, like the confirmed path's review.
|
|
745
|
+
|
|
746
|
+
Refusals claim nothing and send nothing extra: an agent outside the explicit
|
|
747
|
+
tenant, the tenant identity mismatch and a delete target that no longer exists
|
|
748
|
+
are refused before the `DELETE` (the preview reports the same `not_found`); a `409` is a `conflict` refusal naming the referring
|
|
722
749
|
schedules; a `404` from the `DELETE` is `not_found`; and a `405` is the server's
|
|
723
750
|
refusal (`unsupported`). None of them is retried.
|
|
724
751
|
|
|
@@ -7,7 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
import click
|
|
8
8
|
|
|
9
9
|
from ..context import AmsDependencies, pass_ams, preflight_agents
|
|
10
|
-
from ..errors import run_domain
|
|
10
|
+
from ..errors import run_domain, run_preview
|
|
11
11
|
from ..session import resolve_tenant_target
|
|
12
12
|
from .operations import (
|
|
13
13
|
ASSIGNMENT_FAMILIES,
|
|
@@ -16,6 +16,9 @@ from .operations import (
|
|
|
16
16
|
export_agent,
|
|
17
17
|
get_agent,
|
|
18
18
|
list_agents,
|
|
19
|
+
preview_assignments,
|
|
20
|
+
preview_delete,
|
|
21
|
+
preview_lifecycle,
|
|
19
22
|
set_assignments,
|
|
20
23
|
set_lifecycle,
|
|
21
24
|
)
|
|
@@ -37,6 +40,34 @@ def _agent(function):
|
|
|
37
40
|
return click.option("--agent-id", required=True)(function)
|
|
38
41
|
|
|
39
42
|
|
|
43
|
+
def _preview_then_refuse(
|
|
44
|
+
invocation,
|
|
45
|
+
operation: str,
|
|
46
|
+
target_data: dict,
|
|
47
|
+
preview,
|
|
48
|
+
refusal: str,
|
|
49
|
+
*,
|
|
50
|
+
login_hint: str,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Read the target once, print the reviewed change, then refuse with exit 2.
|
|
53
|
+
|
|
54
|
+
Nothing is written before the refusal, and the preview is still a read: a
|
|
55
|
+
failed read (not logged in, wrong tenant, 404, transport) keeps the bounded
|
|
56
|
+
error envelope instead of escaping as a traceback. The confirmed run
|
|
57
|
+
re-reads before its single write, so a preview is never an approval token.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def read() -> None:
|
|
61
|
+
_require_login(login_hint)
|
|
62
|
+
target = resolve_tenant_target(target_data["environment"], target_data["tenant_id"])
|
|
63
|
+
target_data.update(environment=target.environment, tenant_id=target.tenant_id)
|
|
64
|
+
preflight_agents(invocation, target, methods=("get_agent",))
|
|
65
|
+
preview(target)
|
|
66
|
+
|
|
67
|
+
run_preview(operation, target_data, read)
|
|
68
|
+
raise click.UsageError(refusal)
|
|
69
|
+
|
|
70
|
+
|
|
40
71
|
def _require_login(hint: str = LOGIN_HINT) -> None:
|
|
41
72
|
"""Agent writes need a saved `hc login` session, not env-var credentials."""
|
|
42
73
|
|
|
@@ -71,7 +102,8 @@ def create_lifecycle_command(dependencies: AmsDependencies, transition: str) ->
|
|
|
71
102
|
"--confirm",
|
|
72
103
|
is_flag=True,
|
|
73
104
|
default=False,
|
|
74
|
-
help="Required acknowledgement of the exact target and state; without it
|
|
105
|
+
help="Required acknowledgement of the exact target and state; without it the command reads the "
|
|
106
|
+
"target, prints the change and sends nothing.",
|
|
75
107
|
)
|
|
76
108
|
@pass_ams
|
|
77
109
|
def command(
|
|
@@ -82,14 +114,27 @@ def create_lifecycle_command(dependencies: AmsDependencies, transition: str) ->
|
|
|
82
114
|
expected_config_version: int,
|
|
83
115
|
confirm: bool,
|
|
84
116
|
) -> None:
|
|
85
|
-
if not confirm:
|
|
86
|
-
raise click.UsageError("--confirm is required; no lifecycle write was sent.")
|
|
87
117
|
target_data = {
|
|
88
118
|
"environment": environment,
|
|
89
119
|
"tenant_id": tenant_id,
|
|
90
120
|
"agent_id": agent_id,
|
|
91
121
|
"expected_config_version": expected_config_version,
|
|
92
122
|
}
|
|
123
|
+
if not confirm:
|
|
124
|
+
_preview_then_refuse(
|
|
125
|
+
invocation,
|
|
126
|
+
f"agents.{transition}",
|
|
127
|
+
target_data,
|
|
128
|
+
lambda target: preview_lifecycle(
|
|
129
|
+
dependencies,
|
|
130
|
+
target,
|
|
131
|
+
agent_id,
|
|
132
|
+
transition=transition,
|
|
133
|
+
expected_config_version=expected_config_version,
|
|
134
|
+
),
|
|
135
|
+
"--confirm is required; no lifecycle write was sent. Re-run with --confirm to send it.",
|
|
136
|
+
login_hint=LOGIN_HINT,
|
|
137
|
+
)
|
|
93
138
|
|
|
94
139
|
def execute():
|
|
95
140
|
_require_login()
|
|
@@ -123,13 +168,21 @@ def create_delete_command(dependencies: AmsDependencies) -> click.Command:
|
|
|
123
168
|
"--confirm",
|
|
124
169
|
is_flag=True,
|
|
125
170
|
default=False,
|
|
126
|
-
help="Required acknowledgement of the printed target and disclosure block; without it
|
|
171
|
+
help="Required acknowledgement of the printed target and disclosure block; without it the command "
|
|
172
|
+
"prints both and sends nothing.",
|
|
127
173
|
)
|
|
128
174
|
@pass_ams
|
|
129
175
|
def command(invocation, environment: str | None, tenant_id: str | None, agent_id: str, confirm: bool) -> None:
|
|
130
|
-
if not confirm:
|
|
131
|
-
raise click.UsageError("--confirm is required; no delete was sent.")
|
|
132
176
|
target_data = {"environment": environment, "tenant_id": tenant_id, "agent_id": agent_id}
|
|
177
|
+
if not confirm:
|
|
178
|
+
_preview_then_refuse(
|
|
179
|
+
invocation,
|
|
180
|
+
"agents.delete",
|
|
181
|
+
target_data,
|
|
182
|
+
lambda target: preview_delete(dependencies, target, agent_id),
|
|
183
|
+
"--confirm is required; no delete was sent. Re-run with --confirm to send it.",
|
|
184
|
+
login_hint=DELETE_LOGIN_HINT,
|
|
185
|
+
)
|
|
133
186
|
|
|
134
187
|
def execute():
|
|
135
188
|
_require_login(DELETE_LOGIN_HINT)
|
|
@@ -221,7 +274,10 @@ CONFIG_VERSION_HELP = (
|
|
|
221
274
|
"The config_version read before the write. A local best-effort guard only: the route has no "
|
|
222
275
|
"If-Match, so it never proves atomicity."
|
|
223
276
|
)
|
|
224
|
-
CONFIRM_HELP =
|
|
277
|
+
CONFIRM_HELP = (
|
|
278
|
+
"Required acknowledgement of the target and the printed reviewed diff; without it the command prints "
|
|
279
|
+
"the diff and sends nothing."
|
|
280
|
+
)
|
|
225
281
|
|
|
226
282
|
|
|
227
283
|
def _assignment_changes(values: dict, cleared: dict) -> dict:
|
|
@@ -320,8 +376,6 @@ def create_assignments_set_command(dependencies: AmsDependencies) -> click.Comma
|
|
|
320
376
|
clear_data_source_ids: bool,
|
|
321
377
|
confirm: bool,
|
|
322
378
|
) -> None:
|
|
323
|
-
if not confirm:
|
|
324
|
-
raise click.UsageError("--confirm is required; no assignment write was sent.")
|
|
325
379
|
changes = _assignment_changes(
|
|
326
380
|
{
|
|
327
381
|
"department_id": department_id,
|
|
@@ -351,6 +405,21 @@ def create_assignments_set_command(dependencies: AmsDependencies) -> click.Comma
|
|
|
351
405
|
"expected_config_version": expected_config_version,
|
|
352
406
|
"changed_fields": sorted(changes),
|
|
353
407
|
}
|
|
408
|
+
if not confirm:
|
|
409
|
+
_preview_then_refuse(
|
|
410
|
+
invocation,
|
|
411
|
+
"agents.assignments.set",
|
|
412
|
+
target_data,
|
|
413
|
+
lambda target: preview_assignments(
|
|
414
|
+
dependencies,
|
|
415
|
+
target,
|
|
416
|
+
agent_id,
|
|
417
|
+
changes,
|
|
418
|
+
expected_config_version=expected_config_version,
|
|
419
|
+
),
|
|
420
|
+
"--confirm is required; no assignment write was sent. Re-run with --confirm to send it.",
|
|
421
|
+
login_hint=ASSIGNMENT_LOGIN_HINT,
|
|
422
|
+
)
|
|
354
423
|
|
|
355
424
|
def execute():
|
|
356
425
|
_require_login(ASSIGNMENT_LOGIN_HINT)
|
|
@@ -136,6 +136,14 @@ def _lifecycle_target(target: TenantTarget, agent_id: str) -> dict:
|
|
|
136
136
|
return _target(target, agent_id)
|
|
137
137
|
|
|
138
138
|
|
|
139
|
+
def _lifecycle_write_pair(transition: str) -> tuple[str, bool]:
|
|
140
|
+
"""The fixed status/active pair for one transition, or a local refusal."""
|
|
141
|
+
|
|
142
|
+
if transition not in LIFECYCLE_TRANSITIONS:
|
|
143
|
+
raise AgentWriteRefused("validation", "Unsupported lifecycle transition; nothing was sent.")
|
|
144
|
+
return LIFECYCLE_TRANSITIONS[transition]
|
|
145
|
+
|
|
146
|
+
|
|
139
147
|
def _state(agent) -> dict:
|
|
140
148
|
return {
|
|
141
149
|
"status_field": agent.status,
|
|
@@ -325,9 +333,7 @@ def set_lifecycle(
|
|
|
325
333
|
from healthcloud.errors import HealthCloudHTTPError, HealthCloudNetworkError
|
|
326
334
|
|
|
327
335
|
agent_id = _agent_id(agent_id)
|
|
328
|
-
|
|
329
|
-
raise AgentWriteRefused("validation", "Unsupported lifecycle transition; nothing was sent.")
|
|
330
|
-
status, active = LIFECYCLE_TRANSITIONS[transition]
|
|
336
|
+
status, active = _lifecycle_write_pair(transition)
|
|
331
337
|
operation = f"agents.{transition}"
|
|
332
338
|
with dependencies.authenticated_sdk_factory(target) as sdk:
|
|
333
339
|
before = _verified_agent(sdk, target, agent_id)
|
|
@@ -478,6 +484,16 @@ def _assignment_diff(before, changes: dict) -> dict:
|
|
|
478
484
|
return changed
|
|
479
485
|
|
|
480
486
|
|
|
487
|
+
def _echo_preview_notice() -> None:
|
|
488
|
+
"""Say plainly that the review below is a preview and nothing has been sent."""
|
|
489
|
+
|
|
490
|
+
import click
|
|
491
|
+
|
|
492
|
+
if not _review_enabled():
|
|
493
|
+
return
|
|
494
|
+
click.echo("preview: --confirm was not passed, so nothing has been sent.", err=True)
|
|
495
|
+
|
|
496
|
+
|
|
481
497
|
def _review_enabled() -> bool:
|
|
482
498
|
"""A reviewed write rendering is human: json/raw keep stderr machine-parsable."""
|
|
483
499
|
|
|
@@ -509,6 +525,39 @@ def _echo_assignment_review(agent_id: str, before, changed: dict) -> None:
|
|
|
509
525
|
click.echo(f"one PUT /agents/{agent_id} with body keys: {', '.join(changed)}", err=True)
|
|
510
526
|
|
|
511
527
|
|
|
528
|
+
def _echo_lifecycle_review(agent, agent_id: str, transition: str, status: str, active: bool) -> None:
|
|
529
|
+
"""Print the exact lifecycle change the confirmed run would send."""
|
|
530
|
+
|
|
531
|
+
import click
|
|
532
|
+
|
|
533
|
+
if not _review_enabled():
|
|
534
|
+
return
|
|
535
|
+
target = _delete_target(agent, agent_id)
|
|
536
|
+
click.echo(
|
|
537
|
+
f"{transition} agent '{agent_id}' would change (name {target['name']!r}, type {target['type']!r}, "
|
|
538
|
+
f"config_version {target['config_version']}):",
|
|
539
|
+
err=True,
|
|
540
|
+
)
|
|
541
|
+
click.echo(f" status: {getattr(agent, 'status', None)!r} -> {status!r}", err=True)
|
|
542
|
+
click.echo(f" active: {getattr(agent, 'active', None)!r} -> {active!r}", err=True)
|
|
543
|
+
click.echo(f"one PUT /agents/{agent_id} with body keys: status, active", err=True)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _assignment_plan(sdk, target: TenantTarget, agent_id: str, changes: dict, expected_config_version: int):
|
|
547
|
+
"""One verified read, the reviewed diff, and the local version guard."""
|
|
548
|
+
|
|
549
|
+
before = _verified_agent(sdk, target, agent_id)
|
|
550
|
+
changed = _assignment_diff(before, changes)
|
|
551
|
+
if not changed:
|
|
552
|
+
raise AgentWriteRefused(
|
|
553
|
+
"validation",
|
|
554
|
+
f"No assignment option changes agent '{agent_id}'; every requested reference already equals "
|
|
555
|
+
"the read state and no write was sent.",
|
|
556
|
+
)
|
|
557
|
+
_assignment_precheck(before, agent_id, expected_config_version, changed)
|
|
558
|
+
return before, changed
|
|
559
|
+
|
|
560
|
+
|
|
512
561
|
def _assignment_precheck(before, agent_id: str, expected_config_version: int, changed: dict) -> None:
|
|
513
562
|
if before.config_version != expected_config_version:
|
|
514
563
|
raise AgentWriteRefused(
|
|
@@ -642,15 +691,7 @@ def set_assignments(
|
|
|
642
691
|
except ValueError as error:
|
|
643
692
|
raise AgentWriteRefused("validation", f"{error}; nothing was sent.") from None
|
|
644
693
|
with dependencies.authenticated_sdk_factory(target) as sdk:
|
|
645
|
-
before =
|
|
646
|
-
changed = _assignment_diff(before, changes)
|
|
647
|
-
if not changed:
|
|
648
|
-
raise AgentWriteRefused(
|
|
649
|
-
"validation",
|
|
650
|
-
f"No assignment option changes agent '{agent_id}'; every requested reference already equals "
|
|
651
|
-
"the read state and no write was sent.",
|
|
652
|
-
)
|
|
653
|
-
_assignment_precheck(before, agent_id, expected_config_version, changed)
|
|
694
|
+
before, changed = _assignment_plan(sdk, target, agent_id, changes, expected_config_version)
|
|
654
695
|
_echo_assignment_review(agent_id, before, changed)
|
|
655
696
|
body = {field: entry["after"] for field, entry in changed.items()}
|
|
656
697
|
try:
|
|
@@ -775,6 +816,22 @@ _DELETE_RUNTIME_UNVERIFIED = (
|
|
|
775
816
|
)
|
|
776
817
|
|
|
777
818
|
|
|
819
|
+
def _delete_review_target(sdk, target: TenantTarget, agent_id: str):
|
|
820
|
+
"""The one bounded read a delete reviews: a 404 is its own refusal."""
|
|
821
|
+
|
|
822
|
+
from healthcloud.errors import HealthCloudHTTPError
|
|
823
|
+
|
|
824
|
+
try:
|
|
825
|
+
return _verified_agent(sdk, target, agent_id)
|
|
826
|
+
except HealthCloudHTTPError as error:
|
|
827
|
+
if getattr(error, "status_code", 0) == 404:
|
|
828
|
+
raise AgentWriteRefused(
|
|
829
|
+
"not_found",
|
|
830
|
+
f"Agent '{agent_id}' was not found in this tenant; nothing was sent.",
|
|
831
|
+
) from None
|
|
832
|
+
raise
|
|
833
|
+
|
|
834
|
+
|
|
778
835
|
def _delete_target(agent, agent_id: str) -> dict:
|
|
779
836
|
"""The exact record the operator is approving: ids and bounded fields only."""
|
|
780
837
|
|
|
@@ -788,8 +845,12 @@ def _delete_target(agent, agent_id: str) -> dict:
|
|
|
788
845
|
}
|
|
789
846
|
|
|
790
847
|
|
|
791
|
-
def _echo_delete_review(agent, agent_id: str) -> None:
|
|
792
|
-
"""Print the destructive target and the disclosure before the single DELETE.
|
|
848
|
+
def _echo_delete_review(agent, agent_id: str, *, preview: bool = False) -> None:
|
|
849
|
+
"""Print the destructive target and the disclosure before the single DELETE.
|
|
850
|
+
|
|
851
|
+
``preview`` is the unconfirmed half: it prints the same target and the same
|
|
852
|
+
disclosure, worded as what the confirmed run would do.
|
|
853
|
+
"""
|
|
793
854
|
|
|
794
855
|
import click
|
|
795
856
|
|
|
@@ -797,7 +858,8 @@ def _echo_delete_review(agent, agent_id: str) -> None:
|
|
|
797
858
|
return
|
|
798
859
|
target = _delete_target(agent, agent_id)
|
|
799
860
|
click.echo(
|
|
800
|
-
f"
|
|
861
|
+
f"{'would delete' if preview else 'deleting'} agent '{agent_id}' "
|
|
862
|
+
f"(name {target['name']!r}, type {target['type']!r}, "
|
|
801
863
|
f"status {target['status']!r}, active {target['active']}, "
|
|
802
864
|
f"config_version {target['config_version']}):",
|
|
803
865
|
err=True,
|
|
@@ -929,15 +991,7 @@ def delete_agent(dependencies: AmsDependencies, target: TenantTarget, agent_id:
|
|
|
929
991
|
|
|
930
992
|
agent_id = _agent_id(agent_id)
|
|
931
993
|
with dependencies.authenticated_sdk_factory(target) as sdk:
|
|
932
|
-
|
|
933
|
-
before = _verified_agent(sdk, target, agent_id)
|
|
934
|
-
except HealthCloudHTTPError as error:
|
|
935
|
-
if getattr(error, "status_code", 0) == 404:
|
|
936
|
-
raise AgentWriteRefused(
|
|
937
|
-
"not_found",
|
|
938
|
-
f"Agent '{agent_id}' was not found in this tenant; nothing was sent.",
|
|
939
|
-
) from None
|
|
940
|
-
raise
|
|
994
|
+
before = _delete_review_target(sdk, target, agent_id)
|
|
941
995
|
_echo_delete_review(before, agent_id)
|
|
942
996
|
try:
|
|
943
997
|
sdk.ams_admin.delete_agent(agent_id)
|
|
@@ -1046,3 +1100,74 @@ def delete_agent(dependencies: AmsDependencies, target: TenantTarget, agent_id:
|
|
|
1046
1100
|
"before doing anything else."
|
|
1047
1101
|
),
|
|
1048
1102
|
)
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
# --- previews: read the target and print the change, send nothing -----------
|
|
1106
|
+
#
|
|
1107
|
+
# A write is reviewed before it is sent, and `--confirm` acknowledges that
|
|
1108
|
+
# review. Without `--confirm` the command therefore reads the same target and
|
|
1109
|
+
# prints the same reviewed change, then refuses with exit 2. A preview is a
|
|
1110
|
+
# read, so it performs exactly one GET and never a write, a preflight write or a
|
|
1111
|
+
# second read: the confirmed run re-reads before its single write, so a preview
|
|
1112
|
+
# is never an approval token and never a TOCTOU shortcut.
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def preview_lifecycle(
|
|
1116
|
+
dependencies: AmsDependencies,
|
|
1117
|
+
target: TenantTarget,
|
|
1118
|
+
agent_id: str,
|
|
1119
|
+
*,
|
|
1120
|
+
transition: Literal["activate", "deactivate"],
|
|
1121
|
+
expected_config_version: int,
|
|
1122
|
+
) -> None:
|
|
1123
|
+
"""Print the exact lifecycle change without sending it.
|
|
1124
|
+
|
|
1125
|
+
Same one bounded ``GET``, same local pre-checks and same projected target as
|
|
1126
|
+
``set_lifecycle``; the only differences are that nothing is written and the
|
|
1127
|
+
reviewed change goes to stderr.
|
|
1128
|
+
"""
|
|
1129
|
+
|
|
1130
|
+
agent_id = _agent_id(agent_id)
|
|
1131
|
+
status, active = _lifecycle_write_pair(transition)
|
|
1132
|
+
with dependencies.authenticated_sdk_factory(target) as sdk:
|
|
1133
|
+
before = _verified_agent(sdk, target, agent_id)
|
|
1134
|
+
_lifecycle_precheck(before, agent_id, status, active, expected_config_version)
|
|
1135
|
+
_echo_preview_notice()
|
|
1136
|
+
_echo_lifecycle_review(before, agent_id, transition, status, active)
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
def preview_assignments(
|
|
1140
|
+
dependencies: AmsDependencies,
|
|
1141
|
+
target: TenantTarget,
|
|
1142
|
+
agent_id: str,
|
|
1143
|
+
changes: dict,
|
|
1144
|
+
*,
|
|
1145
|
+
expected_config_version: int,
|
|
1146
|
+
) -> None:
|
|
1147
|
+
"""Print the reviewed assignment diff without sending it.
|
|
1148
|
+
|
|
1149
|
+
Same bounded body validation, one ``GET``, the same diff and local version
|
|
1150
|
+
guard as ``set_assignments``; no PUT is sent.
|
|
1151
|
+
"""
|
|
1152
|
+
|
|
1153
|
+
from healthcloud.clients.ams_admin import prepare_assignment_update
|
|
1154
|
+
|
|
1155
|
+
agent_id = _agent_id(agent_id)
|
|
1156
|
+
try:
|
|
1157
|
+
changes = prepare_assignment_update(**changes)
|
|
1158
|
+
except ValueError as error:
|
|
1159
|
+
raise AgentWriteRefused("validation", f"{error}; nothing was sent.") from None
|
|
1160
|
+
with dependencies.authenticated_sdk_factory(target) as sdk:
|
|
1161
|
+
before, changed = _assignment_plan(sdk, target, agent_id, changes, expected_config_version)
|
|
1162
|
+
_echo_preview_notice()
|
|
1163
|
+
_echo_assignment_review(agent_id, before, changed)
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
def preview_delete(dependencies: AmsDependencies, target: TenantTarget, agent_id: str) -> None:
|
|
1167
|
+
"""Print the delete target and its disclosure without sending the DELETE."""
|
|
1168
|
+
|
|
1169
|
+
agent_id = _agent_id(agent_id)
|
|
1170
|
+
with dependencies.authenticated_sdk_factory(target) as sdk:
|
|
1171
|
+
before = _delete_review_target(sdk, target, agent_id)
|
|
1172
|
+
_echo_preview_notice()
|
|
1173
|
+
_echo_delete_review(before, agent_id, preview=True)
|
|
@@ -135,6 +135,25 @@ def emit_compact_json(value: dict, *, err: bool = False) -> None:
|
|
|
135
135
|
click.echo(json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True), err=err)
|
|
136
136
|
|
|
137
137
|
|
|
138
|
+
def run_preview(operation: str, target: dict, callback: Callable[[], Any]) -> None:
|
|
139
|
+
"""Run the read half of an unconfirmed write, printing no success envelope.
|
|
140
|
+
|
|
141
|
+
A preview is a read: a failure (not logged in, wrong tenant, 404, transport,
|
|
142
|
+
SDK missing) renders the same bounded error envelope as any other read and
|
|
143
|
+
exits nonzero, so nothing escapes as a traceback. On success the caller has
|
|
144
|
+
already printed the reviewed target to stderr, so this renders nothing: the
|
|
145
|
+
adapter then refuses with exit 2 and a message saying nothing was sent.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
mode = output_mode()
|
|
149
|
+
try:
|
|
150
|
+
callback()
|
|
151
|
+
except KeyboardInterrupt as error:
|
|
152
|
+
_bounded_failure(operation, target, error, mode, read_only=True)
|
|
153
|
+
except Exception as error:
|
|
154
|
+
_bounded_failure(operation, target, error, mode, read_only=True)
|
|
155
|
+
|
|
156
|
+
|
|
138
157
|
def _render_success(envelope: dict, mode: str) -> None:
|
|
139
158
|
if mode != "json":
|
|
140
159
|
click.echo(success_text(envelope))
|
|
@@ -169,6 +188,32 @@ def _error_message(operation: str, category: str, target: dict, error: Exception
|
|
|
169
188
|
return _message(operation, category, target)
|
|
170
189
|
|
|
171
190
|
|
|
191
|
+
def _bounded_failure(operation: str, target: dict, error: BaseException, mode: str, *, read_only: bool) -> None:
|
|
192
|
+
"""Classify ``error``, render the error envelope, and exit bounded."""
|
|
193
|
+
|
|
194
|
+
# ``error`` may be a KeyboardInterrupt (BaseException): classification
|
|
195
|
+
# maps it to transport/130 and the bounded envelope, never a traceback.
|
|
196
|
+
|
|
197
|
+
category, exit_code = _classification(operation, error)
|
|
198
|
+
# An unknown mutation outcome carries its own reconciliation envelope:
|
|
199
|
+
# the write may have committed, so it renders as ``unknown`` (never a
|
|
200
|
+
# plain failure and never success) and still exits nonzero.
|
|
201
|
+
envelope = getattr(error, "envelope", None)
|
|
202
|
+
_render_failure(
|
|
203
|
+
envelope
|
|
204
|
+
if isinstance(envelope, dict)
|
|
205
|
+
else failed(
|
|
206
|
+
operation,
|
|
207
|
+
target,
|
|
208
|
+
category,
|
|
209
|
+
_error_message(operation, category, target, error),
|
|
210
|
+
read_only=read_only,
|
|
211
|
+
),
|
|
212
|
+
mode,
|
|
213
|
+
)
|
|
214
|
+
raise SystemExit(exit_code) from None
|
|
215
|
+
|
|
216
|
+
|
|
172
217
|
def run_domain(operation: str, target: dict, callback: Callable[[], Any], *, read_only: bool = True) -> None:
|
|
173
218
|
"""Run one AMS operation and render its envelope in the active output mode.
|
|
174
219
|
|
|
@@ -180,29 +225,7 @@ def run_domain(operation: str, target: dict, callback: Callable[[], Any], *, rea
|
|
|
180
225
|
mode = output_mode()
|
|
181
226
|
|
|
182
227
|
def _fail(error: BaseException) -> None:
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
# ``error`` may be a KeyboardInterrupt (BaseException): classification
|
|
186
|
-
# maps it to transport/130 and the bounded envelope, never a traceback.
|
|
187
|
-
|
|
188
|
-
category, exit_code = _classification(operation, error)
|
|
189
|
-
# An unknown mutation outcome carries its own reconciliation envelope:
|
|
190
|
-
# the write may have committed, so it renders as ``unknown`` (never a
|
|
191
|
-
# plain failure and never success) and still exits nonzero.
|
|
192
|
-
envelope = getattr(error, "envelope", None)
|
|
193
|
-
_render_failure(
|
|
194
|
-
envelope
|
|
195
|
-
if isinstance(envelope, dict)
|
|
196
|
-
else failed(
|
|
197
|
-
operation,
|
|
198
|
-
target,
|
|
199
|
-
category,
|
|
200
|
-
_error_message(operation, category, target, error),
|
|
201
|
-
read_only=read_only,
|
|
202
|
-
),
|
|
203
|
-
mode,
|
|
204
|
-
)
|
|
205
|
-
raise SystemExit(exit_code) from None
|
|
228
|
+
_bounded_failure(operation, target, error, mode, read_only=read_only)
|
|
206
229
|
|
|
207
230
|
try:
|
|
208
231
|
envelope = callback()
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "healthcloud-cli"
|
|
7
|
-
version = "3.7.
|
|
7
|
+
version = "3.7.3"
|
|
8
8
|
description = "Command-line interface for the HealthCloud MCP gateway, generated from the shared MCP tool manifest."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.14"
|
|
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
|
{healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/data_sources/__init__.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/data_sources/commands.py
RENAMED
|
File without changes
|
|
File without changes
|
{healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/data_sources/operations.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/data_sources/projections.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/departments/operations.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/departments/projections.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/guardrails/operations.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/guardrails/projections.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/organization/__init__.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/organization/commands.py
RENAMED
|
File without changes
|
|
File without changes
|
{healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/organization/operations.py
RENAMED
|
File without changes
|
{healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/ams/organization/projections.py
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{healthcloud_cli-3.7.2 → healthcloud_cli-3.7.3}/healthcloud_cli/contracts/mcp-tools.manifest.json
RENAMED
|
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
|