databay-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- databay/__init__.py +5 -0
- databay/__main__.py +7 -0
- databay/agent_skill.py +184 -0
- databay/api.py +626 -0
- databay/cli/__init__.py +0 -0
- databay/cli/auth.py +377 -0
- databay/cli/build.py +89 -0
- databay/cli/commands/__init__.py +0 -0
- databay/cli/commands/alerting_webhook.py +19 -0
- databay/cli/commands/alerting_webhook_tag.py +14 -0
- databay/cli/commands/alerting_webhook_type.py +14 -0
- databay/cli/commands/alerting_webhook_undelivered.py +15 -0
- databay/cli/commands/announcement.py +17 -0
- databay/cli/commands/api_key.py +18 -0
- databay/cli/commands/api_key_project.py +16 -0
- databay/cli/commands/backfill_reason_code.py +14 -0
- databay/cli/commands/backfill_run.py +17 -0
- databay/cli/commands/canvas.py +16 -0
- databay/cli/commands/canvas_graph.py +15 -0
- databay/cli/commands/canvas_table_check.py +15 -0
- databay/cli/commands/dashboards.py +21 -0
- databay/cli/commands/dashboards_card.py +20 -0
- databay/cli/commands/dashboards_dataset.py +14 -0
- databay/cli/commands/dashboards_folder.py +16 -0
- databay/cli/commands/dashboards_home.py +17 -0
- databay/cli/commands/dashboards_placement.py +16 -0
- databay/cli/commands/db.py +19 -0
- databay/cli/commands/db_column.py +14 -0
- databay/cli/commands/docs_article.py +16 -0
- databay/cli/commands/dq_result.py +14 -0
- databay/cli/commands/dq_rule.py +16 -0
- databay/cli/commands/feature.py +14 -0
- databay/cli/commands/google_ads_source.py +14 -0
- databay/cli/commands/google_analytics_source.py +14 -0
- databay/cli/commands/instagram_account.py +14 -0
- databay/cli/commands/instagram_source.py +14 -0
- databay/cli/commands/linkedin_account.py +14 -0
- databay/cli/commands/linkedin_source.py +14 -0
- databay/cli/commands/me.py +14 -0
- databay/cli/commands/meta_ads_source.py +14 -0
- databay/cli/commands/metric_models.py +26 -0
- databay/cli/commands/metrics.py +14 -0
- databay/cli/commands/models.py +16 -0
- databay/cli/commands/nlq.py +15 -0
- databay/cli/commands/notebook_credential.py +16 -0
- databay/cli/commands/org.py +14 -0
- databay/cli/commands/org_member.py +17 -0
- databay/cli/commands/org_offboarding.py +14 -0
- databay/cli/commands/pipe.py +26 -0
- databay/cli/commands/pipe_backfill_run.py +14 -0
- databay/cli/commands/pipe_config_option.py +14 -0
- databay/cli/commands/pipe_destination.py +16 -0
- databay/cli/commands/pipe_leftover.py +16 -0
- databay/cli/commands/pipe_sink_type.py +14 -0
- databay/cli/commands/pipe_source_type.py +14 -0
- databay/cli/commands/pipe_status_bucket.py +14 -0
- databay/cli/commands/pipe_stuck_delete.py +14 -0
- databay/cli/commands/plan.py +14 -0
- databay/cli/commands/project.py +21 -0
- databay/cli/commands/project_budget.py +15 -0
- databay/cli/commands/project_member.py +18 -0
- databay/cli/commands/project_privilege.py +16 -0
- databay/cli/commands/project_share.py +16 -0
- databay/cli/commands/provision.py +14 -0
- databay/cli/commands/provisioning.py +14 -0
- databay/cli/commands/registry.py +14 -0
- databay/cli/commands/resource_home.py +14 -0
- databay/cli/commands/schema_change.py +18 -0
- databay/cli/commands/schema_history.py +17 -0
- databay/cli/commands/schema_version.py +16 -0
- databay/cli/commands/shopify_store.py +14 -0
- databay/cli/commands/sink_data_type.py +14 -0
- databay/cli/commands/source.py +24 -0
- databay/cli/commands/source_catalog.py +15 -0
- databay/cli/commands/source_check_run.py +17 -0
- databay/cli/commands/source_hidden_type.py +14 -0
- databay/cli/commands/source_name.py +14 -0
- databay/cli/commands/source_pipe.py +14 -0
- databay/cli/commands/source_publication.py +16 -0
- databay/cli/commands/source_readiness_override.py +15 -0
- databay/cli/commands/source_setup_choice.py +16 -0
- databay/cli/commands/source_stream.py +15 -0
- databay/cli/commands/source_table.py +14 -0
- databay/cli/commands/ssbi_job.py +18 -0
- databay/cli/commands/transform_type.py +14 -0
- databay/cli/commands/usage.py +14 -0
- databay/cli/commands/warehouse.py +21 -0
- databay/cli/commands/warehouse_access_assignment.py +16 -0
- databay/cli/commands/warehouse_access_role.py +22 -0
- databay/cli/commands/warehouse_access_settings.py +17 -0
- databay/cli/commands/warehouse_login.py +20 -0
- databay/cli/commands/warehouse_principal.py +17 -0
- databay/cli/commands/warehouse_service_account.py +17 -0
- databay/cli/commands/warehouse_service_account_credential.py +18 -0
- databay/cli/common.py +93 -0
- databay/cli/console.py +121 -0
- databay/cli/flags.py +183 -0
- databay/cli/handwritten.py +22 -0
- databay/cli/main.py +198 -0
- databay/cli/metrics.py +171 -0
- databay/cli/org.py +193 -0
- databay/cli/output.py +134 -0
- databay/cli/pipe_wait.py +357 -0
- databay/cli/profile.py +230 -0
- databay/cli/runner.py +432 -0
- databay/cli/skill.py +64 -0
- databay/cli/sql.py +84 -0
- databay/compat.py +161 -0
- databay/credentials.py +217 -0
- databay/error_catalog.py +226 -0
- databay/error_codes.py +3265 -0
- databay/errors.py +222 -0
- databay/feature_header.py +40 -0
- databay/invoke.py +225 -0
- databay/locks.py +41 -0
- databay/login_flow.py +245 -0
- databay/model_base.py +40 -0
- databay/models/__init__.py +0 -0
- databay/models/alerting_webhook.py +46 -0
- databay/models/alerting_webhook_tag.py +11 -0
- databay/models/alerting_webhook_type.py +11 -0
- databay/models/alerting_webhook_undelivered.py +23 -0
- databay/models/announcement.py +37 -0
- databay/models/api_key.py +81 -0
- databay/models/api_key_project.py +29 -0
- databay/models/backfill_reason_code.py +11 -0
- databay/models/backfill_run.py +41 -0
- databay/models/canvas.py +68 -0
- databay/models/canvas_graph.py +86 -0
- databay/models/canvas_table_check.py +181 -0
- databay/models/dashboards.py +79 -0
- databay/models/dashboards_card.py +37 -0
- databay/models/dashboards_dataset.py +27 -0
- databay/models/dashboards_folder.py +18 -0
- databay/models/dashboards_home.py +28 -0
- databay/models/db.py +34 -0
- databay/models/db_column.py +11 -0
- databay/models/docs_article.py +59 -0
- databay/models/dq_result.py +27 -0
- databay/models/dq_rule.py +43 -0
- databay/models/feature.py +15 -0
- databay/models/google_ads_source.py +11 -0
- databay/models/google_analytics_source.py +11 -0
- databay/models/instagram_source.py +11 -0
- databay/models/linkedin_source.py +11 -0
- databay/models/me.py +71 -0
- databay/models/meta_ads_source.py +11 -0
- databay/models/metric_models.py +274 -0
- databay/models/metrics.py +158 -0
- databay/models/models.py +73 -0
- databay/models/nlq.py +92 -0
- databay/models/notebook_credential.py +51 -0
- databay/models/org.py +44 -0
- databay/models/org_member.py +123 -0
- databay/models/org_offboarding.py +36 -0
- databay/models/pipe.py +340 -0
- databay/models/pipe_config_option.py +25 -0
- databay/models/pipe_destination.py +128 -0
- databay/models/pipe_leftover.py +51 -0
- databay/models/pipe_sink_type.py +11 -0
- databay/models/pipe_source_type.py +11 -0
- databay/models/pipe_status_bucket.py +13 -0
- databay/models/pipe_stuck_delete.py +25 -0
- databay/models/plan.py +37 -0
- databay/models/project.py +154 -0
- databay/models/project_budget.py +22 -0
- databay/models/project_member.py +67 -0
- databay/models/project_privilege.py +21 -0
- databay/models/project_share.py +76 -0
- databay/models/provision.py +12 -0
- databay/models/provisioning.py +20 -0
- databay/models/registry.py +18 -0
- databay/models/resource_home.py +35 -0
- databay/models/schema_change.py +96 -0
- databay/models/schema_history.py +32 -0
- databay/models/schema_version.py +23 -0
- databay/models/shared.py +875 -0
- databay/models/shopify_store.py +19 -0
- databay/models/sink_data_type.py +11 -0
- databay/models/source.py +263 -0
- databay/models/source_catalog.py +82 -0
- databay/models/source_check_run.py +63 -0
- databay/models/source_hidden_type.py +13 -0
- databay/models/source_name.py +18 -0
- databay/models/source_pipe.py +16 -0
- databay/models/source_publication.py +30 -0
- databay/models/source_readiness_override.py +13 -0
- databay/models/source_setup_choice.py +33 -0
- databay/models/source_stream.py +65 -0
- databay/models/source_table.py +27 -0
- databay/models/sql.py +31 -0
- databay/models/ssbi_job.py +97 -0
- databay/models/transform_type.py +40 -0
- databay/models/usage.py +112 -0
- databay/models/warehouse.py +38 -0
- databay/models/warehouse_access_assignment.py +70 -0
- databay/models/warehouse_access_role.py +148 -0
- databay/models/warehouse_access_settings.py +35 -0
- databay/models/warehouse_login.py +20 -0
- databay/models/warehouse_principal.py +49 -0
- databay/models/warehouse_service_account.py +35 -0
- databay/models/warehouse_service_account_credential.py +11 -0
- databay/oidc.py +205 -0
- databay/operation.py +191 -0
- databay/operations/__init__.py +0 -0
- databay/operations/alerting_webhook.py +317 -0
- databay/operations/alerting_webhook_tag.py +35 -0
- databay/operations/alerting_webhook_type.py +35 -0
- databay/operations/alerting_webhook_undelivered.py +74 -0
- databay/operations/announcement.py +135 -0
- databay/operations/api_key.py +198 -0
- databay/operations/api_key_project.py +130 -0
- databay/operations/backfill_reason_code.py +35 -0
- databay/operations/backfill_run.py +245 -0
- databay/operations/canvas.py +136 -0
- databay/operations/canvas_graph.py +102 -0
- databay/operations/canvas_table_check.py +101 -0
- databay/operations/dashboards.py +441 -0
- databay/operations/dashboards_card.py +406 -0
- databay/operations/dashboards_dataset.py +50 -0
- databay/operations/dashboards_folder.py +146 -0
- databay/operations/dashboards_home.py +176 -0
- databay/operations/dashboards_placement.py +152 -0
- databay/operations/db.py +337 -0
- databay/operations/db_column.py +57 -0
- databay/operations/docs_article.py +101 -0
- databay/operations/dq_result.py +43 -0
- databay/operations/dq_rule.py +155 -0
- databay/operations/feature.py +35 -0
- databay/operations/google_ads_source.py +40 -0
- databay/operations/google_analytics_source.py +40 -0
- databay/operations/instagram_account.py +47 -0
- databay/operations/instagram_source.py +40 -0
- databay/operations/linkedin_account.py +47 -0
- databay/operations/linkedin_source.py +40 -0
- databay/operations/me.py +57 -0
- databay/operations/meta_ads_source.py +40 -0
- databay/operations/metric_models.py +612 -0
- databay/operations/metrics.py +218 -0
- databay/operations/models.py +123 -0
- databay/operations/nlq.py +100 -0
- databay/operations/notebook_credential.py +95 -0
- databay/operations/org.py +79 -0
- databay/operations/org_member.py +192 -0
- databay/operations/org_offboarding.py +43 -0
- databay/operations/pipe.py +882 -0
- databay/operations/pipe_backfill_run.py +72 -0
- databay/operations/pipe_config_option.py +50 -0
- databay/operations/pipe_destination.py +178 -0
- databay/operations/pipe_leftover.py +110 -0
- databay/operations/pipe_sink_type.py +35 -0
- databay/operations/pipe_source_type.py +35 -0
- databay/operations/pipe_status_bucket.py +35 -0
- databay/operations/pipe_stuck_delete.py +52 -0
- databay/operations/plan.py +35 -0
- databay/operations/project.py +346 -0
- databay/operations/project_budget.py +135 -0
- databay/operations/project_member.py +226 -0
- databay/operations/project_privilege.py +143 -0
- databay/operations/project_share.py +160 -0
- databay/operations/provision.py +35 -0
- databay/operations/provisioning.py +35 -0
- databay/operations/registry.py +35 -0
- databay/operations/resource_home.py +80 -0
- databay/operations/schema_change.py +222 -0
- databay/operations/schema_history.py +136 -0
- databay/operations/schema_version.py +115 -0
- databay/operations/shopify_store.py +40 -0
- databay/operations/sink_data_type.py +43 -0
- databay/operations/source.py +1207 -0
- databay/operations/source_catalog.py +135 -0
- databay/operations/source_check_run.py +242 -0
- databay/operations/source_hidden_type.py +59 -0
- databay/operations/source_name.py +69 -0
- databay/operations/source_pipe.py +57 -0
- databay/operations/source_publication.py +139 -0
- databay/operations/source_readiness_override.py +96 -0
- databay/operations/source_setup_choice.py +161 -0
- databay/operations/source_stream.py +99 -0
- databay/operations/source_table.py +59 -0
- databay/operations/sql.py +54 -0
- databay/operations/ssbi_job.py +242 -0
- databay/operations/transform_type.py +35 -0
- databay/operations/usage.py +35 -0
- databay/operations/warehouse.py +343 -0
- databay/operations/warehouse_access_assignment.py +179 -0
- databay/operations/warehouse_access_role.py +473 -0
- databay/operations/warehouse_access_settings.py +172 -0
- databay/operations/warehouse_login.py +308 -0
- databay/operations/warehouse_principal.py +174 -0
- databay/operations/warehouse_service_account.py +206 -0
- databay/operations/warehouse_service_account_credential.py +285 -0
- databay/paths.py +32 -0
- databay/platform_check.py +20 -0
- databay/profiles.py +181 -0
- databay/redact.py +22 -0
- databay/reference.py +218 -0
- databay/resource.py +167 -0
- databay/resources/__init__.py +0 -0
- databay/resources/alerting_webhook.py +84 -0
- databay/resources/alerting_webhook_tag.py +21 -0
- databay/resources/alerting_webhook_type.py +21 -0
- databay/resources/alerting_webhook_undelivered.py +35 -0
- databay/resources/announcement.py +45 -0
- databay/resources/api_key.py +56 -0
- databay/resources/api_key_project.py +47 -0
- databay/resources/backfill_reason_code.py +21 -0
- databay/resources/backfill_run.py +78 -0
- databay/resources/canvas.py +43 -0
- databay/resources/canvas_graph.py +38 -0
- databay/resources/canvas_table_check.py +38 -0
- databay/resources/dashboards.py +105 -0
- databay/resources/dashboards_card.py +97 -0
- databay/resources/dashboards_dataset.py +27 -0
- databay/resources/dashboards_folder.py +47 -0
- databay/resources/dashboards_home.py +59 -0
- databay/resources/dashboards_placement.py +47 -0
- databay/resources/db.py +87 -0
- databay/resources/db_column.py +28 -0
- databay/resources/docs_article.py +37 -0
- databay/resources/dq_result.py +25 -0
- databay/resources/dq_rule.py +48 -0
- databay/resources/feature.py +21 -0
- databay/resources/google_ads_source.py +25 -0
- databay/resources/google_analytics_source.py +25 -0
- databay/resources/instagram_account.py +27 -0
- databay/resources/instagram_source.py +25 -0
- databay/resources/linkedin_account.py +27 -0
- databay/resources/linkedin_source.py +25 -0
- databay/resources/me.py +27 -0
- databay/resources/meta_ads_source.py +25 -0
- databay/resources/metric_models.py +154 -0
- databay/resources/metrics.py +57 -0
- databay/resources/models.py +42 -0
- databay/resources/nlq.py +34 -0
- databay/resources/notebook_credential.py +41 -0
- databay/resources/org.py +29 -0
- databay/resources/org_member.py +53 -0
- databay/resources/org_offboarding.py +25 -0
- databay/resources/pipe.py +208 -0
- databay/resources/pipe_backfill_run.py +28 -0
- databay/resources/pipe_config_option.py +26 -0
- databay/resources/pipe_destination.py +49 -0
- databay/resources/pipe_leftover.py +42 -0
- databay/resources/pipe_sink_type.py +21 -0
- databay/resources/pipe_source_type.py +21 -0
- databay/resources/pipe_status_bucket.py +21 -0
- databay/resources/pipe_stuck_delete.py +27 -0
- databay/resources/plan.py +21 -0
- databay/resources/project.py +92 -0
- databay/resources/project_budget.py +38 -0
- databay/resources/project_member.py +68 -0
- databay/resources/project_privilege.py +48 -0
- databay/resources/project_share.py +47 -0
- databay/resources/provision.py +21 -0
- databay/resources/provisioning.py +21 -0
- databay/resources/registry.py +21 -0
- databay/resources/resource_home.py +29 -0
- databay/resources/schema_change.py +62 -0
- databay/resources/schema_history.py +52 -0
- databay/resources/schema_version.py +43 -0
- databay/resources/shopify_store.py +25 -0
- databay/resources/sink_data_type.py +25 -0
- databay/resources/source.py +132 -0
- databay/resources/source_catalog.py +34 -0
- databay/resources/source_check_run.py +67 -0
- databay/resources/source_hidden_type.py +25 -0
- databay/resources/source_name.py +27 -0
- databay/resources/source_pipe.py +28 -0
- databay/resources/source_publication.py +47 -0
- databay/resources/source_readiness_override.py +37 -0
- databay/resources/source_setup_choice.py +50 -0
- databay/resources/source_stream.py +38 -0
- databay/resources/source_table.py +27 -0
- databay/resources/sql.py +27 -0
- databay/resources/ssbi_job.py +69 -0
- databay/resources/transform_type.py +21 -0
- databay/resources/usage.py +21 -0
- databay/resources/warehouse.py +136 -0
- databay/resources/warehouse_access_assignment.py +51 -0
- databay/resources/warehouse_access_role.py +118 -0
- databay/resources/warehouse_access_settings.py +56 -0
- databay/resources/warehouse_login.py +81 -0
- databay/resources/warehouse_principal.py +51 -0
- databay/resources/warehouse_service_account.py +61 -0
- databay/resources/warehouse_service_account_credential.py +78 -0
- databay/sdk.py +77 -0
- databay/settings.py +90 -0
- databay/skill/SKILL.md +357 -0
- databay/skill/references/commands.md +7025 -0
- databay/skill/references/errors.md +727 -0
- databay/spec.py +362 -0
- databay/stored_token.py +134 -0
- databay/tokens.py +108 -0
- databay/transport.py +357 -0
- databay/update_check.py +151 -0
- databay/version.py +6 -0
- databay/waits.py +330 -0
- databay_cli-0.1.0.dist-info/METADATA +42 -0
- databay_cli-0.1.0.dist-info/RECORD +403 -0
- databay_cli-0.1.0.dist-info/WHEEL +4 -0
- databay_cli-0.1.0.dist-info/entry_points.txt +2 -0
- databay_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
databay/cli/pipe_wait.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""
|
|
2
|
+
``databay pipe wait``: when a pipe is provisioned, and when its table is ready to query.
|
|
3
|
+
|
|
4
|
+
``active`` is the wait ``pipe status --wait`` runs. ``data`` waits for that first, then reads the
|
|
5
|
+
pipe and measures its destination each tick. It ends on rows in the table. Only a destination
|
|
6
|
+
that reports ``not_supported`` can end on a healthy freshness verdict: there, no row count exists.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import shlex
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import replace
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
|
|
18
|
+
from databay.cli.common import common_options, console_for
|
|
19
|
+
from databay.operation import Condition, Wait
|
|
20
|
+
from databay.settings import SETTINGS
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
|
|
25
|
+
from databay.cli.console import Console
|
|
26
|
+
from databay.invoke import Session
|
|
27
|
+
from databay.waits import Fetch, Reading, WaitOutcome
|
|
28
|
+
|
|
29
|
+
COMMAND = "databay pipe wait"
|
|
30
|
+
|
|
31
|
+
#: A pipe's freshness once it is active. ``not_landing_data`` fails in every freshness state and a
|
|
32
|
+
#: null value is pending: ``data_step`` applies both, because ``classify`` reads only one value.
|
|
33
|
+
#: Success ends the wait only for a table that cannot be measured; see ``_no_rows_yet``.
|
|
34
|
+
DATA_WAIT = Wait(
|
|
35
|
+
status_operation="pipe_get",
|
|
36
|
+
bind=(("pipe_uuid", "path:pipe_uuid"),),
|
|
37
|
+
field="/freshness_state",
|
|
38
|
+
pending=("never_checked", "behind"),
|
|
39
|
+
success=("fresh",),
|
|
40
|
+
failure=("stale", "not_monitored"),
|
|
41
|
+
error_field="/health_detail",
|
|
42
|
+
conditional=(
|
|
43
|
+
(
|
|
44
|
+
"unknown",
|
|
45
|
+
Condition(
|
|
46
|
+
field="/health",
|
|
47
|
+
pending=("unknown", "stale", "not_reporting", "not_monitored"),
|
|
48
|
+
success=("healthy",),
|
|
49
|
+
failure=("not_landing_data",),
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
PIPE_WAIT_HELP = """Wait until a pipe is active, or until its table is ready to query.
|
|
56
|
+
|
|
57
|
+
\b
|
|
58
|
+
--until active ends when the pipe is provisioned; data may not have arrived.
|
|
59
|
+
--until data waits for active, then ends when the table holds rows
|
|
60
|
+
(rows_landed). A warehouse that cannot report a row count ends when the
|
|
61
|
+
pipe reports healthy (healthy_unmeasured). An empty table keeps waiting.
|
|
62
|
+
\b
|
|
63
|
+
A pipe that ends in another status reports that status. A pipe that is not
|
|
64
|
+
landing data, or whose stats fail for good, exits 5. Running out of time
|
|
65
|
+
exits 4, and `next` runs the same wait again.
|
|
66
|
+
|
|
67
|
+
\b
|
|
68
|
+
Examples:
|
|
69
|
+
databay pipe wait <pipe_uuid> --until active
|
|
70
|
+
databay pipe wait <pipe_uuid> --until data --timeout 540"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _summary(
|
|
74
|
+
pipe_uuid: str, pipe_status: str, answer: dict[str, Any], destination: dict[str, Any] | None
|
|
75
|
+
) -> dict[str, Any]:
|
|
76
|
+
"""What the wait prints: the fields of the two answers that decide it, and what ``next`` is built from."""
|
|
77
|
+
measured = destination or {}
|
|
78
|
+
return {
|
|
79
|
+
"pipe_uuid": pipe_uuid,
|
|
80
|
+
"pipe_status": pipe_status,
|
|
81
|
+
"last_error": answer.get("last_error"),
|
|
82
|
+
"freshness_state": answer.get("freshness_state"),
|
|
83
|
+
"health": answer.get("health"),
|
|
84
|
+
"health_detail": answer.get("health_detail"),
|
|
85
|
+
"destination_table_exists": measured.get("destination_table_exists"),
|
|
86
|
+
"stored_rows": measured.get("stored_rows"),
|
|
87
|
+
"unavailable_code": measured.get("unavailable_code"),
|
|
88
|
+
"sink_db_uuid": answer.get("sink_db_uuid"),
|
|
89
|
+
"dest_table_name": answer.get("dest_table_name"),
|
|
90
|
+
"project_id": answer.get("project_id"),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _unavailable(destination: dict[str, Any], status: dict[str, Any]) -> Reading:
|
|
95
|
+
"""A destination that could not be measured: retried when the server says a retry may help."""
|
|
96
|
+
from databay.errors import DatabayWaitError # noqa: PLC0415
|
|
97
|
+
from databay.waits import Reading # noqa: PLC0415
|
|
98
|
+
|
|
99
|
+
code = destination.get("unavailable_code")
|
|
100
|
+
state = f"stats_unavailable ({code})"
|
|
101
|
+
retriable = destination.get("retriable")
|
|
102
|
+
if retriable is True:
|
|
103
|
+
return Reading("pending", state, status, "pipe_destination_get")
|
|
104
|
+
if retriable is None:
|
|
105
|
+
stop = DatabayWaitError(
|
|
106
|
+
f"{COMMAND}: the destination reports stats_unavailable ({code}) without saying if a retry may help",
|
|
107
|
+
code="unexpected_wait_state",
|
|
108
|
+
details={"state": state},
|
|
109
|
+
)
|
|
110
|
+
return Reading("failure", state, status, "pipe_destination_get", stop)
|
|
111
|
+
message = destination.get("unavailable_message")
|
|
112
|
+
stop = DatabayWaitError(
|
|
113
|
+
f"{COMMAND}: the destination cannot be measured ({code})" + (f": {message}" if message else ""),
|
|
114
|
+
hint=destination.get("remediation"),
|
|
115
|
+
support_ref=destination.get("support_ref"),
|
|
116
|
+
details={"state": state, "unavailable_code": code, "error": message},
|
|
117
|
+
)
|
|
118
|
+
return Reading("failure", state, status, "pipe_destination_get", stop)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _freshness(pipe: dict[str, Any], status: dict[str, Any]) -> Reading:
|
|
122
|
+
from databay.errors import DatabayWaitError # noqa: PLC0415
|
|
123
|
+
from databay.invoke import json_text # noqa: PLC0415
|
|
124
|
+
from databay.waits import Reading, read_status # noqa: PLC0415
|
|
125
|
+
|
|
126
|
+
freshness, health = pipe.get("freshness_state"), pipe.get("health")
|
|
127
|
+
if freshness is None or health is None:
|
|
128
|
+
state = f"freshness_state={json_text(freshness)} health={json_text(health)}"
|
|
129
|
+
return Reading("pending", state, status, DATA_WAIT.status_operation)
|
|
130
|
+
health_rule = DATA_WAIT.conditional[0][1]
|
|
131
|
+
if health not in (*health_rule.pending, *health_rule.success, *health_rule.failure):
|
|
132
|
+
stop = DatabayWaitError(
|
|
133
|
+
f"{COMMAND}: health is {health!r}, a state this CLI does not know",
|
|
134
|
+
code="unexpected_wait_state",
|
|
135
|
+
details={"state": health},
|
|
136
|
+
)
|
|
137
|
+
return Reading("failure", health, status, DATA_WAIT.status_operation, stop)
|
|
138
|
+
reading = read_status(DATA_WAIT, pipe, COMMAND)
|
|
139
|
+
reason = "healthy_unmeasured" if reading.outcome == "success" else None
|
|
140
|
+
return replace(reading, status=status, reason=reason)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _no_rows_yet(pipe: dict[str, Any], destination: dict[str, Any], status: dict[str, Any]) -> Reading:
|
|
144
|
+
"""A destination that can be measured and shows no rows: only a failure verdict ends the wait."""
|
|
145
|
+
from databay.invoke import json_text # noqa: PLC0415
|
|
146
|
+
from databay.waits import Reading # noqa: PLC0415
|
|
147
|
+
|
|
148
|
+
verdict = _freshness(pipe, status)
|
|
149
|
+
if verdict.outcome == "failure":
|
|
150
|
+
return verdict
|
|
151
|
+
exists = destination.get("destination_table_exists")
|
|
152
|
+
if exists is False:
|
|
153
|
+
missing = "the destination table does not exist yet"
|
|
154
|
+
elif exists and destination.get("stored_rows") == 0:
|
|
155
|
+
missing = "no rows have landed"
|
|
156
|
+
else:
|
|
157
|
+
missing = "the row count was not measured"
|
|
158
|
+
health = pipe.get("health")
|
|
159
|
+
state = f"healthy but {missing}" if health == "healthy" else f"{missing} (health={json_text(health)})"
|
|
160
|
+
return Reading("pending", state, status, "pipe_destination_get")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _measured(pipe: dict[str, Any], destination: dict[str, Any], status: dict[str, Any]) -> Reading:
|
|
164
|
+
"""What an active pipe's destination answer means."""
|
|
165
|
+
from databay.waits import Reading # noqa: PLC0415
|
|
166
|
+
|
|
167
|
+
if destination.get("stats_unavailable"):
|
|
168
|
+
if destination.get("unavailable_code") == "not_supported":
|
|
169
|
+
return _freshness(pipe, status)
|
|
170
|
+
return _unavailable(destination, status)
|
|
171
|
+
stored = destination.get("stored_rows")
|
|
172
|
+
if destination.get("destination_table_exists") and stored is not None and stored > 0:
|
|
173
|
+
return Reading("success", f"stored_rows={stored}", status, "pipe_destination_get", reason="rows_landed")
|
|
174
|
+
return _no_rows_yet(pipe, destination, status)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def data_step(pipe_uuid: str) -> Callable[[Fetch], Reading]:
|
|
178
|
+
"""One tick of ``--until data``: the status route until the pipe is active, then its two routes."""
|
|
179
|
+
from databay.errors import DatabayWaitError # noqa: PLC0415
|
|
180
|
+
from databay.invoke import CallValues, build_request # noqa: PLC0415
|
|
181
|
+
from databay.operations.pipe import PIPE_GET, PIPE_STATUS # noqa: PLC0415
|
|
182
|
+
from databay.operations.pipe_destination import PIPE_DESTINATION_GET # noqa: PLC0415
|
|
183
|
+
from databay.waits import Reading, read_status # noqa: PLC0415
|
|
184
|
+
|
|
185
|
+
active_wait = PIPE_STATUS.resumes
|
|
186
|
+
assert active_wait is not None
|
|
187
|
+
pipe_status_wait = replace(active_wait, status_operation=PIPE_GET.operation_id, field="/status")
|
|
188
|
+
values = CallValues(path={"pipe_uuid": pipe_uuid})
|
|
189
|
+
status_request = build_request(PIPE_STATUS, values, None)
|
|
190
|
+
pipe_request = build_request(PIPE_GET, values, None)
|
|
191
|
+
destination_request = build_request(PIPE_DESTINATION_GET, values, None)
|
|
192
|
+
has_been_active = False
|
|
193
|
+
|
|
194
|
+
def step(fetch: Fetch) -> Reading:
|
|
195
|
+
nonlocal has_been_active
|
|
196
|
+
if not has_been_active:
|
|
197
|
+
answer = fetch(PIPE_STATUS, status_request)
|
|
198
|
+
reading = read_status(active_wait, answer, COMMAND)
|
|
199
|
+
if reading.outcome != "success":
|
|
200
|
+
return replace(reading, status=_summary(pipe_uuid, reading.state, answer, None))
|
|
201
|
+
has_been_active = True
|
|
202
|
+
pipe = fetch(PIPE_GET, pipe_request)
|
|
203
|
+
reading = read_status(pipe_status_wait, pipe, COMMAND)
|
|
204
|
+
if reading.outcome != "success":
|
|
205
|
+
return replace(reading, status=_summary(pipe_uuid, reading.state, pipe, None))
|
|
206
|
+
if pipe.get("health") == "not_landing_data":
|
|
207
|
+
status = _summary(pipe_uuid, reading.state, pipe, None)
|
|
208
|
+
detail = pipe.get("health_detail")
|
|
209
|
+
stop = DatabayWaitError(
|
|
210
|
+
f"{COMMAND}: the pipe is not landing data" + (f": {detail}" if detail else ""),
|
|
211
|
+
details={"state": "not_landing_data", "error": detail},
|
|
212
|
+
)
|
|
213
|
+
return Reading("failure", "not_landing_data", status, PIPE_GET.operation_id, stop)
|
|
214
|
+
destination = fetch(PIPE_DESTINATION_GET, destination_request)
|
|
215
|
+
return _measured(pipe, destination, _summary(pipe_uuid, reading.state, pipe, destination))
|
|
216
|
+
|
|
217
|
+
return step
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def wait_for_data(
|
|
221
|
+
session: Session,
|
|
222
|
+
pipe_uuid: str,
|
|
223
|
+
*,
|
|
224
|
+
timeout_s: float | None,
|
|
225
|
+
on_state: Callable[[WaitOutcome], None] | None = None,
|
|
226
|
+
clock: Callable[[], float] = time.monotonic,
|
|
227
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
228
|
+
) -> WaitOutcome:
|
|
229
|
+
from databay import waits # noqa: PLC0415
|
|
230
|
+
|
|
231
|
+
return waits.poll(
|
|
232
|
+
session,
|
|
233
|
+
data_step(pipe_uuid),
|
|
234
|
+
command=COMMAND,
|
|
235
|
+
status_operation="pipe_status",
|
|
236
|
+
bound={"pipe_uuid": pipe_uuid},
|
|
237
|
+
limit=timeout_s if timeout_s is not None else session.transport.settings.wait.data_timeout_s,
|
|
238
|
+
on_state=on_state,
|
|
239
|
+
clock=clock,
|
|
240
|
+
sleep=sleep,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _profile_words(profile: str | None) -> list[str]:
|
|
245
|
+
return ["--profile", profile] if profile is not None else []
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _resume(pipe_uuid: str, until: str, timeout: float | None, profile: str | None) -> str:
|
|
249
|
+
words = ["databay", "pipe", "wait", pipe_uuid, "--until", until]
|
|
250
|
+
if timeout is not None:
|
|
251
|
+
words += ["--timeout", f"{timeout:g}"]
|
|
252
|
+
return shlex.join(words + _profile_words(profile))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _query_next(session: Session, console: Console, status: dict[str, Any], profile: str | None) -> str:
|
|
256
|
+
"""
|
|
257
|
+
The ``sql`` command counting the table's rows, on the pipe's own warehouse.
|
|
258
|
+
|
|
259
|
+
The table is already ready, so a warehouse that cannot be read changes only ``next``: it becomes
|
|
260
|
+
the command that reads the name, and a ``warehouse_unread`` event says why. One attempt, because
|
|
261
|
+
the answer is not printed until this returns and ``--timeout`` may be spent.
|
|
262
|
+
"""
|
|
263
|
+
from databay.errors import DatabayError # noqa: PLC0415
|
|
264
|
+
from databay.invoke import CallValues, build_request, decode, resolve_project # noqa: PLC0415
|
|
265
|
+
from databay.operations.warehouse import WAREHOUSE_GET # noqa: PLC0415
|
|
266
|
+
from databay.transport import error_from # noqa: PLC0415
|
|
267
|
+
|
|
268
|
+
sink = status["sink_db_uuid"]
|
|
269
|
+
project = status["project_id"]
|
|
270
|
+
flags = ["--project", project] if project is not None and project != session.active_project else []
|
|
271
|
+
flags += _profile_words(profile)
|
|
272
|
+
read_name = shlex.join(["databay", "warehouse", "get", sink, *flags])
|
|
273
|
+
call = CallValues(path={"db_uuid": sink}, project_explicit=project)
|
|
274
|
+
try:
|
|
275
|
+
request = build_request(WAREHOUSE_GET, call, resolve_project(WAREHOUSE_GET, call, session.active_project))
|
|
276
|
+
answer = session.transport.exchange(request, retries=False)
|
|
277
|
+
if answer.status >= 400: # noqa: PLR2004
|
|
278
|
+
raise error_from(answer, request)
|
|
279
|
+
database = decode(WAREHOUSE_GET, answer)[0].get("db_name")
|
|
280
|
+
except DatabayError as exc:
|
|
281
|
+
console.event("warehouse_unread", {"sink_db_uuid": sink, "code": exc.code})
|
|
282
|
+
return read_name
|
|
283
|
+
if database is None:
|
|
284
|
+
console.event("warehouse_unread", {"sink_db_uuid": sink, "code": "db_name_missing"})
|
|
285
|
+
return read_name
|
|
286
|
+
statement = f"select count(*) from {database}.{status['dest_table_name']}" # noqa: S608 — printed for the caller to run; `sql` runs it read-only
|
|
287
|
+
return shlex.join(["databay", "sql", "--db", sink, *flags, statement])
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@click.command("wait", help=PIPE_WAIT_HELP, short_help="Wait until a pipe is active or its table is ready to query")
|
|
291
|
+
@click.argument("pipe_uuid", metavar="PIPE_UUID")
|
|
292
|
+
@click.option(
|
|
293
|
+
"--until",
|
|
294
|
+
"until",
|
|
295
|
+
required=True,
|
|
296
|
+
type=click.Choice(("active", "data")),
|
|
297
|
+
help="active: provisioned; data: the table is ready to query",
|
|
298
|
+
)
|
|
299
|
+
@click.option(
|
|
300
|
+
"--timeout",
|
|
301
|
+
"timeout",
|
|
302
|
+
type=click.FloatRange(min=0, min_open=True),
|
|
303
|
+
default=None,
|
|
304
|
+
metavar="S",
|
|
305
|
+
help=(
|
|
306
|
+
"seconds to wait before exit 4, resumable "
|
|
307
|
+
f"(default: {SETTINGS.wait.default_timeout_s:g} for active, {SETTINGS.wait.data_timeout_s:g} for data)"
|
|
308
|
+
),
|
|
309
|
+
)
|
|
310
|
+
@common_options
|
|
311
|
+
@click.pass_context
|
|
312
|
+
def pipe_wait(
|
|
313
|
+
ctx: click.Context,
|
|
314
|
+
pipe_uuid: str,
|
|
315
|
+
until: str,
|
|
316
|
+
timeout: float | None,
|
|
317
|
+
**values: Any, # noqa: ANN401
|
|
318
|
+
) -> None:
|
|
319
|
+
from databay import waits # noqa: PLC0415
|
|
320
|
+
from databay.cli.runner import open_session, report_wait # noqa: PLC0415
|
|
321
|
+
from databay.operations.pipe import PIPE_GET, PIPE_STATUS # noqa: PLC0415
|
|
322
|
+
from databay.operations.pipe_destination import PIPE_DESTINATION_GET # noqa: PLC0415
|
|
323
|
+
|
|
324
|
+
console, state = console_for(ctx, values)
|
|
325
|
+
session, profile = open_session(values, console, state)
|
|
326
|
+
state["context"] = {"server": profile.server, "profile": profile.name, "request_id": session.transport.request_id}
|
|
327
|
+
state["reading"] = True
|
|
328
|
+
explicit_profile = values.get("profile")
|
|
329
|
+
resume = _resume(pipe_uuid, until, timeout, explicit_profile)
|
|
330
|
+
active_wait = PIPE_STATUS.resumes
|
|
331
|
+
assert active_wait is not None
|
|
332
|
+
|
|
333
|
+
def run_active(on_state: Callable[[WaitOutcome], None]) -> WaitOutcome:
|
|
334
|
+
return waits.run(session, active_wait, {"pipe_uuid": pipe_uuid}, timeout_s=timeout, on_state=on_state)
|
|
335
|
+
|
|
336
|
+
def run_data(on_state: Callable[[WaitOutcome], None]) -> WaitOutcome:
|
|
337
|
+
return wait_for_data(session, pipe_uuid, timeout_s=timeout, on_state=on_state)
|
|
338
|
+
|
|
339
|
+
def active_next(_: WaitOutcome) -> str:
|
|
340
|
+
return _resume(pipe_uuid, "data", timeout, explicit_profile)
|
|
341
|
+
|
|
342
|
+
def data_next(outcome: WaitOutcome) -> str:
|
|
343
|
+
return _query_next(session, console, outcome.status, explicit_profile)
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
if until == "active":
|
|
347
|
+
session.ensure_compatible(PIPE_STATUS)
|
|
348
|
+
columns, rows = PIPE_STATUS.columns, PIPE_STATUS.response.rows
|
|
349
|
+
report_wait(
|
|
350
|
+
run_active, console, {"result": None}, columns, rows, resume, state, from_status=True, then=active_next
|
|
351
|
+
)
|
|
352
|
+
return
|
|
353
|
+
for operation in (PIPE_STATUS, PIPE_GET, PIPE_DESTINATION_GET):
|
|
354
|
+
session.ensure_compatible(operation)
|
|
355
|
+
report_wait(run_data, console, {"result": None}, (), None, resume, state, from_status=True, then=data_next)
|
|
356
|
+
finally:
|
|
357
|
+
session.transport.close()
|
databay/cli/profile.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""``databay profile use|list``, ``databay status`` and ``databay project use``: local state and who you are."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from databay.cli.common import common_options, console_for
|
|
11
|
+
from databay.cli.handwritten import HANDWRITTEN
|
|
12
|
+
from databay.settings import OUTPUT_FORMATS
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from databay.agent_skill import Installed
|
|
18
|
+
from databay.cli.console import Console
|
|
19
|
+
|
|
20
|
+
PROFILE_COLUMNS = ("name", "current", "server", "auth", "user", "token_store", "active_project")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.group("profile", short_help="Switch between and list profiles")
|
|
24
|
+
def profile() -> None:
|
|
25
|
+
"""Profiles in config.toml: each is one server, its sign-in and its active project."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@profile.command("use")
|
|
29
|
+
@click.argument("name")
|
|
30
|
+
@click.option("--output", type=click.Choice(OUTPUT_FORMATS), default=None, help="json, table or csv")
|
|
31
|
+
@click.pass_context
|
|
32
|
+
def profile_use(ctx: click.Context, name: str, output: str | None) -> None:
|
|
33
|
+
"""Make NAME the current profile."""
|
|
34
|
+
from databay.cli.output import emit # noqa: PLC0415
|
|
35
|
+
from databay.errors import DatabayProfileError # noqa: PLC0415
|
|
36
|
+
from databay.paths import config_file # noqa: PLC0415
|
|
37
|
+
from databay.profiles import edit_config # noqa: PLC0415
|
|
38
|
+
|
|
39
|
+
console, _ = console_for(ctx, {"output": output})
|
|
40
|
+
|
|
41
|
+
def change(data: dict[str, Any]) -> None:
|
|
42
|
+
if name not in data["profiles"]:
|
|
43
|
+
known = ", ".join(sorted(data["profiles"])) or "none"
|
|
44
|
+
raise DatabayProfileError(f"no profile {name!r} in {config_file()} (profiles: {known})")
|
|
45
|
+
data["current_profile"] = name
|
|
46
|
+
|
|
47
|
+
edit_config(change)
|
|
48
|
+
emit(console, {"result": {"current_profile": name}}, ())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@profile.command("list")
|
|
52
|
+
@click.option("--output", type=click.Choice(OUTPUT_FORMATS), default=None, help="json, table or csv")
|
|
53
|
+
@click.pass_context
|
|
54
|
+
def profile_list(ctx: click.Context, output: str | None) -> None:
|
|
55
|
+
"""Every profile, and which one is current."""
|
|
56
|
+
from databay.cli.output import emit # noqa: PLC0415
|
|
57
|
+
from databay.credentials import key_sub # noqa: PLC0415
|
|
58
|
+
from databay.profiles import all_profiles # noqa: PLC0415
|
|
59
|
+
|
|
60
|
+
console, _ = console_for(ctx, {"output": output})
|
|
61
|
+
current, profiles = all_profiles()
|
|
62
|
+
rows = [
|
|
63
|
+
{
|
|
64
|
+
"name": p.name,
|
|
65
|
+
"current": p.name == current,
|
|
66
|
+
"server": p.server,
|
|
67
|
+
"auth": p.auth,
|
|
68
|
+
"user": key_sub(p.credential) if p.credential else None,
|
|
69
|
+
"token_store": p.token_store,
|
|
70
|
+
"active_project": p.active_project,
|
|
71
|
+
}
|
|
72
|
+
for p in profiles
|
|
73
|
+
]
|
|
74
|
+
emit(console, {"result": rows}, PROFILE_COLUMNS)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@click.command("status", short_help=HANDWRITTEN["status"][2])
|
|
78
|
+
@common_options
|
|
79
|
+
@click.pass_context
|
|
80
|
+
def status(ctx: click.Context, **values: Any) -> None: # noqa: ANN401
|
|
81
|
+
"""
|
|
82
|
+
Show the profile, the server, and who you are signed in as.
|
|
83
|
+
|
|
84
|
+
Prints the server's product and API version, the user, the org the token names, the active project,
|
|
85
|
+
which output rule applied, and every installed agent skill. It calls the server, so it also checks the
|
|
86
|
+
sign-in works. needs_org_choice is true when no org is active and you belong to several: run
|
|
87
|
+
`databay org switch`. skill.state is skill_outdated when an installed skill is from another CLI version.
|
|
88
|
+
"""
|
|
89
|
+
from databay.cli.console import output_rule # noqa: PLC0415
|
|
90
|
+
from databay.cli.output import emit # noqa: PLC0415
|
|
91
|
+
from databay.cli.runner import open_session # noqa: PLC0415
|
|
92
|
+
from databay.invoke import CallValues # noqa: PLC0415
|
|
93
|
+
from databay.operations.me import ME_GET # noqa: PLC0415
|
|
94
|
+
|
|
95
|
+
console, state = console_for(ctx, values)
|
|
96
|
+
_, rule = output_rule(values.get("output"), sys.stdout)
|
|
97
|
+
session, chosen = open_session(values, console, state)
|
|
98
|
+
state["context"] = {"server": chosen.server, "profile": chosen.name, "project": chosen.active_project}
|
|
99
|
+
try:
|
|
100
|
+
me = session.call(ME_GET, CallValues(query={"include_orgs": True})).parsed
|
|
101
|
+
assert session.server is not None
|
|
102
|
+
info, _ = session.server
|
|
103
|
+
finally:
|
|
104
|
+
session.transport.close()
|
|
105
|
+
result = {
|
|
106
|
+
"profile": chosen.name,
|
|
107
|
+
"server": chosen.server,
|
|
108
|
+
"product": info.product,
|
|
109
|
+
"api_version": info.api_version,
|
|
110
|
+
"auth": chosen.auth,
|
|
111
|
+
"user": me.preferred_username or me.email,
|
|
112
|
+
"sub": me.sub,
|
|
113
|
+
"principal": str(me.principal),
|
|
114
|
+
"org": me.organization[0] if me.organization else None,
|
|
115
|
+
"needs_org_choice": me.needs_org_choice,
|
|
116
|
+
"active_project": chosen.active_project,
|
|
117
|
+
"output": {"format": console.output, "rule": rule},
|
|
118
|
+
"skill": _skill_status(console),
|
|
119
|
+
}
|
|
120
|
+
if me.needs_org_choice:
|
|
121
|
+
console.inform(
|
|
122
|
+
"org_choice_required",
|
|
123
|
+
{"next": "databay org list"},
|
|
124
|
+
"No organization is active: run databay org list, then databay org switch <organization>.",
|
|
125
|
+
)
|
|
126
|
+
emit(console, {"result": result}, ())
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _skill_status(console: Console) -> dict[str, Any]:
|
|
130
|
+
"""
|
|
131
|
+
Every installed copy of the agent skill and whether it matches this CLI's version.
|
|
132
|
+
|
|
133
|
+
``state`` is ``current``, ``skill_outdated`` (a copy records another version, or none) or
|
|
134
|
+
``not_installed``; each outdated copy is also a ``skill_outdated`` notice on stderr.
|
|
135
|
+
"""
|
|
136
|
+
from pathlib import Path # noqa: PLC0415
|
|
137
|
+
|
|
138
|
+
from databay.agent_skill import find_installed # noqa: PLC0415
|
|
139
|
+
from databay.version import CLIENT_VERSION # noqa: PLC0415
|
|
140
|
+
|
|
141
|
+
cwd = Path.cwd()
|
|
142
|
+
copies = []
|
|
143
|
+
for copy in find_installed(home=Path.home(), cwd=cwd):
|
|
144
|
+
is_outdated = copy.version != CLIENT_VERSION
|
|
145
|
+
copies.append(
|
|
146
|
+
{
|
|
147
|
+
"target": copy.target,
|
|
148
|
+
"scope": copy.scope,
|
|
149
|
+
"path": str(copy.path),
|
|
150
|
+
"version": copy.version,
|
|
151
|
+
"problem": copy.problem,
|
|
152
|
+
"outdated": is_outdated,
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
if is_outdated:
|
|
156
|
+
command = _reinstall_command(copy, cwd)
|
|
157
|
+
console.inform(
|
|
158
|
+
"skill_outdated",
|
|
159
|
+
{
|
|
160
|
+
"path": str(copy.path),
|
|
161
|
+
"installed_version": copy.version,
|
|
162
|
+
"problem": copy.problem,
|
|
163
|
+
"cli_version": CLIENT_VERSION,
|
|
164
|
+
"next": command,
|
|
165
|
+
},
|
|
166
|
+
f"skill_outdated: {copy.path} is for CLI {copy.version or f'an unknown version ({copy.problem})'}, "
|
|
167
|
+
f"this CLI is {CLIENT_VERSION}; run {command}",
|
|
168
|
+
)
|
|
169
|
+
if not copies:
|
|
170
|
+
state = "not_installed"
|
|
171
|
+
elif any(c["outdated"] for c in copies):
|
|
172
|
+
state = "skill_outdated"
|
|
173
|
+
else:
|
|
174
|
+
state = "current"
|
|
175
|
+
return {"state": state, "cli_version": CLIENT_VERSION, "installed": copies}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _reinstall_command(copy: Installed, cwd: Path) -> str:
|
|
179
|
+
"""The command that replaces ``copy``; a project copy above ``cwd`` is reinstalled from its own folder."""
|
|
180
|
+
import shlex # noqa: PLC0415
|
|
181
|
+
|
|
182
|
+
command = f"databay skill install --target {copy.target} --scope {copy.scope} --force"
|
|
183
|
+
project = copy.path.parents[2]
|
|
184
|
+
if copy.scope == "project" and project != cwd:
|
|
185
|
+
return f"cd {shlex.quote(str(project))} && {command}"
|
|
186
|
+
return command
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@click.command("use", short_help="Set the profile's active project")
|
|
190
|
+
@click.argument("project_id")
|
|
191
|
+
@common_options
|
|
192
|
+
@click.pass_context
|
|
193
|
+
def project_use(ctx: click.Context, project_id: str, **values: Any) -> None: # noqa: ANN401
|
|
194
|
+
"""
|
|
195
|
+
Make PROJECT_ID the active project of the profile, after checking it is one you can see.
|
|
196
|
+
|
|
197
|
+
Ids only: `databay project list` shows names. A project being deleted is refused like one you cannot see.
|
|
198
|
+
"""
|
|
199
|
+
from databay.cli.output import emit # noqa: PLC0415
|
|
200
|
+
from databay.cli.runner import open_session # noqa: PLC0415
|
|
201
|
+
from databay.errors import api_error # noqa: PLC0415
|
|
202
|
+
from databay.invoke import CallValues # noqa: PLC0415
|
|
203
|
+
from databay.operations.project import PROJECT_LIST # noqa: PLC0415
|
|
204
|
+
from databay.profiles import edit_config # noqa: PLC0415
|
|
205
|
+
|
|
206
|
+
console, state = console_for(ctx, values)
|
|
207
|
+
session, chosen = open_session(values, console, state)
|
|
208
|
+
state["context"] = {"server": chosen.server, "profile": chosen.name, "project": project_id}
|
|
209
|
+
try:
|
|
210
|
+
visible = session.call(PROJECT_LIST, CallValues()).parsed
|
|
211
|
+
finally:
|
|
212
|
+
session.transport.close()
|
|
213
|
+
match = next((p for p in visible if p.id == project_id and p.status == "active"), None)
|
|
214
|
+
if match is None:
|
|
215
|
+
raise api_error(
|
|
216
|
+
"project_not_found",
|
|
217
|
+
f"project {project_id} is not an active project you can see on {chosen.server}",
|
|
218
|
+
status=404,
|
|
219
|
+
details={"project_id": project_id},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def change(data: dict[str, Any]) -> None:
|
|
223
|
+
data["profiles"][chosen.name]["active_project"] = project_id
|
|
224
|
+
|
|
225
|
+
edit_config(change)
|
|
226
|
+
emit(
|
|
227
|
+
console,
|
|
228
|
+
{"result": {"profile": chosen.name, "active_project": project_id, "name": match.name}, "project": project_id},
|
|
229
|
+
(),
|
|
230
|
+
)
|