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/runner.py
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Running one generated command: its inputs, the confirmation, the call, the wait and the output.
|
|
3
|
+
|
|
4
|
+
Order matters: every input is read and checked (flags, secrets, the project) before any request,
|
|
5
|
+
and a destructive command is confirmed before anything is sent.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import shlex
|
|
13
|
+
import sys
|
|
14
|
+
import uuid
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
|
|
20
|
+
from databay import waits
|
|
21
|
+
from databay.cli.console import Console, output_format
|
|
22
|
+
from databay.cli.output import emit
|
|
23
|
+
from databay.errors import (
|
|
24
|
+
DatabayCancelled,
|
|
25
|
+
DatabayError,
|
|
26
|
+
DatabayNotReady,
|
|
27
|
+
DatabayUsageError,
|
|
28
|
+
DatabayWaitError,
|
|
29
|
+
)
|
|
30
|
+
from databay.invoke import CallValues, Session, resolve_project
|
|
31
|
+
from databay.transport import Transport
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from collections.abc import Callable
|
|
35
|
+
|
|
36
|
+
from databay.cli.flags import Flag
|
|
37
|
+
from databay.operation import Body, Operation, Param, Wait
|
|
38
|
+
from databay.profiles import Profile
|
|
39
|
+
from databay.tokens import TokenProvider
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _usage(message: str, code: str = "usage_invalid") -> DatabayUsageError:
|
|
43
|
+
return DatabayUsageError(message, code=code)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse_json(flag: str, text: str) -> Any: # noqa: ANN401
|
|
47
|
+
try:
|
|
48
|
+
return json.loads(text)
|
|
49
|
+
except ValueError as exc:
|
|
50
|
+
raise _usage(f"{flag} takes JSON: {exc}") from None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
#: A flag with choices reads its value as text (``click.Choice``): an integer or number enum sends a number.
|
|
54
|
+
_NUMBERS: dict[str | None, type] = {"integer": int, "number": float}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _convert(param: Param, flag: str, value: Any) -> Any: # noqa: ANN401
|
|
58
|
+
if param.kind == "json":
|
|
59
|
+
return _parse_json(flag, value)
|
|
60
|
+
if param.kind == "list":
|
|
61
|
+
cast = _NUMBERS.get(param.item_kind)
|
|
62
|
+
return [cast(v) for v in value] if cast else list(value)
|
|
63
|
+
cast = _NUMBERS.get(param.kind)
|
|
64
|
+
return cast(value) if cast else value
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Stdin:
|
|
68
|
+
"""stdin, which one input at most may read."""
|
|
69
|
+
|
|
70
|
+
def __init__(self) -> None:
|
|
71
|
+
self.claimed_by: str | None = None
|
|
72
|
+
|
|
73
|
+
def read(self, flag: str) -> str:
|
|
74
|
+
if self.claimed_by is not None:
|
|
75
|
+
raise _usage(f"{flag} and {self.claimed_by} both read stdin; pass one of them another way")
|
|
76
|
+
self.claimed_by = flag
|
|
77
|
+
return sys.stdin.read()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _secret(param: Param, values: dict[str, Any], stdin: Stdin) -> Any: # noqa: ANN401
|
|
81
|
+
env_flag, stdin_flag = f"--{param.flag}-env", f"--{param.flag}-stdin"
|
|
82
|
+
env_name = values.get(f"{param.identifier}_env")
|
|
83
|
+
from_stdin = values.get(f"{param.identifier}_stdin")
|
|
84
|
+
if env_name and from_stdin:
|
|
85
|
+
raise _usage(f"pass {param.name} with {env_flag} or {stdin_flag}, not both")
|
|
86
|
+
text: str | None = None
|
|
87
|
+
if env_name:
|
|
88
|
+
text = os.environ.get(env_name, "")
|
|
89
|
+
if not text:
|
|
90
|
+
raise _usage(f"{env_flag} names ${env_name}, which is not set or empty", "secret_required")
|
|
91
|
+
elif from_stdin:
|
|
92
|
+
text = stdin.read(stdin_flag).rstrip("\r\n")
|
|
93
|
+
if not text:
|
|
94
|
+
raise _usage(f"{stdin_flag}: stdin was empty", "secret_required")
|
|
95
|
+
elif param.required:
|
|
96
|
+
if not sys.stdin.isatty():
|
|
97
|
+
raise _usage(f"{param.name} is a secret: pass {env_flag} NAME or {stdin_flag}", "secret_required")
|
|
98
|
+
text = click.prompt(param.name, hide_input=True, err=True)
|
|
99
|
+
if text is None:
|
|
100
|
+
return None
|
|
101
|
+
return _parse_json(env_flag, text) if param.kind == "json" else text
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _body_file(path: str, stdin: Stdin) -> Any: # noqa: ANN401
|
|
105
|
+
if path == "-":
|
|
106
|
+
return _parse_json("--file", stdin.read("--file -"))
|
|
107
|
+
try:
|
|
108
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
109
|
+
except OSError as exc:
|
|
110
|
+
raise _usage(f"--file {path}: {exc.strerror}") from None
|
|
111
|
+
return _parse_json(f"--file {path}", text)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _with_variant(spec: Body, variant: str, body: Any, fields: dict[str, Any]) -> dict[str, Any]: # noqa: ANN401
|
|
115
|
+
"""``body`` with the variant's ``fields`` and discriminator in place; a file's own discriminator must agree."""
|
|
116
|
+
if not isinstance(body, dict):
|
|
117
|
+
raise _usage("--file must hold a JSON object")
|
|
118
|
+
holder = body if spec.variant_field is None else body.setdefault(spec.variant_field, {})
|
|
119
|
+
if not isinstance(holder, dict):
|
|
120
|
+
raise _usage(f"--file's {spec.variant_field} must hold a JSON object")
|
|
121
|
+
assert spec.discriminator is not None
|
|
122
|
+
holder.update(fields)
|
|
123
|
+
given = holder.get(spec.discriminator, variant)
|
|
124
|
+
if given != variant:
|
|
125
|
+
raise _usage(f"--file has {spec.discriminator}={given!r}, but the command is `{variant}`")
|
|
126
|
+
holder[spec.discriminator] = variant
|
|
127
|
+
return body
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def call_values(op: Operation, variant: str | None, flags: list[Flag], values: dict[str, Any]) -> CallValues:
|
|
131
|
+
"""The command line as ``CallValues``; a missing required input is a usage error naming its flag."""
|
|
132
|
+
call = CallValues(project_flag=values.get("project"), idempotency_key=values.get("idempotency_key"))
|
|
133
|
+
location = op.project_location
|
|
134
|
+
stdin = Stdin()
|
|
135
|
+
body: dict[str, Any] = {}
|
|
136
|
+
variant_body: dict[str, Any] = {}
|
|
137
|
+
in_variant = {p.name for p in op.body.variant(variant)[1]} if op.body is not None and variant is not None else ()
|
|
138
|
+
body_flags: list[str] = []
|
|
139
|
+
missing: list[str] = []
|
|
140
|
+
for flag in flags:
|
|
141
|
+
param = flag.param
|
|
142
|
+
if param is None or flag.role == "secret_stdin":
|
|
143
|
+
continue
|
|
144
|
+
is_project = location is not None and location == (param.location, param.name)
|
|
145
|
+
if flag.role == "secret_env":
|
|
146
|
+
if values.get("file") is not None:
|
|
147
|
+
secret_flags = {
|
|
148
|
+
f"{param.identifier}_env": flag.name,
|
|
149
|
+
f"{param.identifier}_stdin": f"--{param.flag}-stdin",
|
|
150
|
+
}
|
|
151
|
+
body_flags += [name for dest, name in secret_flags.items() if values.get(dest)]
|
|
152
|
+
continue
|
|
153
|
+
value = _secret(param, values, stdin)
|
|
154
|
+
if value is not None:
|
|
155
|
+
(variant_body if param.name in in_variant else body)[param.name] = value
|
|
156
|
+
body_flags.append(flag.name)
|
|
157
|
+
elif param.required:
|
|
158
|
+
missing.append(flag.name)
|
|
159
|
+
continue
|
|
160
|
+
raw = values.get(flag.dest)
|
|
161
|
+
if raw is None or raw == ():
|
|
162
|
+
if flag.required and not (param.location == "body" and values.get("file") is not None):
|
|
163
|
+
missing.append(flag.name)
|
|
164
|
+
continue
|
|
165
|
+
if is_project:
|
|
166
|
+
call.project_explicit = str(raw)
|
|
167
|
+
if param.location == "body":
|
|
168
|
+
body_flags.append(flag.name)
|
|
169
|
+
continue
|
|
170
|
+
value = _convert(param, flag.name, raw)
|
|
171
|
+
if param.location == "body":
|
|
172
|
+
(variant_body if param.name in in_variant else body)[param.name] = value
|
|
173
|
+
body_flags.append(flag.name)
|
|
174
|
+
else:
|
|
175
|
+
{"path": call.path, "query": call.query, "header": call.headers}[param.location][param.name] = value
|
|
176
|
+
if missing:
|
|
177
|
+
raise _usage(f"`{op.command}` needs {', '.join(missing)}")
|
|
178
|
+
if op.body is None:
|
|
179
|
+
return call
|
|
180
|
+
if values.get("file") is not None:
|
|
181
|
+
if body_flags:
|
|
182
|
+
raise _usage(f"pass the body with --file or with flags ({', '.join(body_flags)}), not both")
|
|
183
|
+
body = _body_file(values["file"], stdin)
|
|
184
|
+
if location is not None and location[0] == "body" and isinstance(body, dict) and location[1] in body:
|
|
185
|
+
# The file's own project id is an explicit one: --project must agree with it. A null one is
|
|
186
|
+
# not given, as the SDK reads None.
|
|
187
|
+
named = body.pop(location[1])
|
|
188
|
+
if named is not None:
|
|
189
|
+
call.project_explicit = str(named)
|
|
190
|
+
call.body = body if variant is None else _with_variant(op.body, variant, body, variant_body)
|
|
191
|
+
call.has_body = True
|
|
192
|
+
return call
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _confirm(op: Operation, call: CallValues, project: str | None, yes: bool, console: Console) -> None:
|
|
196
|
+
if not op.destructive or yes:
|
|
197
|
+
return
|
|
198
|
+
if not (sys.stdin.isatty() and console.stdout.isatty()):
|
|
199
|
+
raise _usage(f"`{op.command}` removes or overwrites state: pass --yes to confirm", "confirmation_required")
|
|
200
|
+
target = " ".join(str(v) for v in call.path.values())
|
|
201
|
+
where = f" in project {project}" if project else ""
|
|
202
|
+
answer = click.prompt(
|
|
203
|
+
f"{op.verb} {op.noun} {target}{where}? [y/N]".replace(" ", " "),
|
|
204
|
+
default="",
|
|
205
|
+
show_default=False,
|
|
206
|
+
err=True,
|
|
207
|
+
)
|
|
208
|
+
if answer.strip().lower() not in {"y", "yes"}:
|
|
209
|
+
raise DatabayCancelled(f"{op.command} cancelled; nothing was sent")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _resume_command(
|
|
213
|
+
wait: Wait, bound: dict[str, str], timeout: float | None, profile: str | None, project: str | None
|
|
214
|
+
) -> str:
|
|
215
|
+
"""The status command that resumes ``wait``: same target, project and profile; nothing starts again."""
|
|
216
|
+
status_op = waits.operation(wait.status_operation)
|
|
217
|
+
words = ["databay", status_op.noun, status_op.verb]
|
|
218
|
+
words += [bound[p.name] for p in status_op.path_params]
|
|
219
|
+
for param in status_op.params:
|
|
220
|
+
if param.location == "query" and param.name in bound:
|
|
221
|
+
words += [f"--{param.flag}", bound[param.name]]
|
|
222
|
+
words.append("--wait")
|
|
223
|
+
if timeout is not None:
|
|
224
|
+
words += ["--timeout", f"{timeout:g}"]
|
|
225
|
+
if status_op.project is not None and project is not None:
|
|
226
|
+
words += ["--project", project]
|
|
227
|
+
if profile is not None:
|
|
228
|
+
words += ["--profile", profile]
|
|
229
|
+
return shlex.join(words)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def open_session(values: dict[str, Any], console: Console, state: dict[str, Any]) -> tuple[Session, Profile]:
|
|
233
|
+
"""A session as the profile ``--profile`` (else ``current_profile``) names, with the common flags applied."""
|
|
234
|
+
from databay.profiles import load_profile # noqa: PLC0415
|
|
235
|
+
from databay.tokens import token_provider # noqa: PLC0415
|
|
236
|
+
|
|
237
|
+
profile = load_profile(values.get("profile"))
|
|
238
|
+
return session_for(profile, token_provider(profile, state.get("http")), values, console, state), profile
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def session_for(
|
|
242
|
+
profile: Profile, tokens: TokenProvider, values: dict[str, Any], console: Console, state: dict[str, Any]
|
|
243
|
+
) -> Session:
|
|
244
|
+
"""A session as ``profile`` with ``tokens``, the common flags applied."""
|
|
245
|
+
from databay.profiles import login_command # noqa: PLC0415
|
|
246
|
+
|
|
247
|
+
transport = Transport(
|
|
248
|
+
server=profile.server,
|
|
249
|
+
tokens=tokens,
|
|
250
|
+
client_kind="cli",
|
|
251
|
+
retries=not values.get("no_retry"),
|
|
252
|
+
verbose=bool(values.get("verbose")),
|
|
253
|
+
events=console.event,
|
|
254
|
+
http=state.get("http"),
|
|
255
|
+
login_command=login_command(profile),
|
|
256
|
+
)
|
|
257
|
+
return Session(transport, active_project=profile.active_project, profile=profile.name)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def run(ctx: click.Context, op: Operation, variant: str | None, flags: list[Flag], values: dict[str, Any]) -> None:
|
|
261
|
+
"""Run ``op`` from parsed command-line ``values``; any failure leaves as a ``DatabayError``."""
|
|
262
|
+
state: dict[str, Any] = ctx.ensure_object(dict)
|
|
263
|
+
console = Console(output=output_format(values.get("output"), sys.stdout), verbose=bool(values.get("verbose")))
|
|
264
|
+
state["console"] = console
|
|
265
|
+
if values.get("timeout") is not None and not values.get("wait"):
|
|
266
|
+
raise _usage("--timeout applies to --wait only")
|
|
267
|
+
call = call_values(op, variant, flags, values)
|
|
268
|
+
if op.idempotent and call.idempotency_key is None:
|
|
269
|
+
call.idempotency_key = str(uuid.uuid4())
|
|
270
|
+
state["idempotency_key"] = call.idempotency_key
|
|
271
|
+
session, profile = open_session(values, console, state)
|
|
272
|
+
state["context"] = {"server": profile.server, "profile": profile.name, "request_id": session.transport.request_id}
|
|
273
|
+
project = resolve_project(op, call, session.active_project)
|
|
274
|
+
state["context"]["project"] = project
|
|
275
|
+
_confirm(op, call, project, bool(values.get("yes")), console)
|
|
276
|
+
explicit_profile = values.get("profile")
|
|
277
|
+
try:
|
|
278
|
+
session.ensure_compatible(op)
|
|
279
|
+
resuming = bool(values.get("wait")) and op.resumes is not None
|
|
280
|
+
# From here a write may be out: an interrupt can no longer promise that nothing changed.
|
|
281
|
+
state["sent"] = op.method != "get" and not resuming
|
|
282
|
+
state["reading"] = not state["sent"]
|
|
283
|
+
if resuming:
|
|
284
|
+
_run_resume(op, call, session, console, values, explicit_profile, project, state)
|
|
285
|
+
return
|
|
286
|
+
try:
|
|
287
|
+
result = session.call(op, call)
|
|
288
|
+
except DatabayNotReady as exc:
|
|
289
|
+
# A not-ready verdict is still an answer: print it, then exit 9.
|
|
290
|
+
emit(
|
|
291
|
+
console,
|
|
292
|
+
{"result": exc.result, **({"project": project} if project else {})},
|
|
293
|
+
op.columns,
|
|
294
|
+
op.response.rows,
|
|
295
|
+
)
|
|
296
|
+
raise
|
|
297
|
+
state["context"]["request_id"] = result.request_id
|
|
298
|
+
envelope: dict[str, Any] = {"result": result.raw}
|
|
299
|
+
if result.project is not None:
|
|
300
|
+
envelope["project"] = result.project
|
|
301
|
+
if call.idempotency_key is not None:
|
|
302
|
+
envelope["idempotency_key"] = call.idempotency_key
|
|
303
|
+
wait = op.wait
|
|
304
|
+
bound = waits.bind(wait, result.raw, result.path) if wait is not None else None
|
|
305
|
+
if wait is not None and values.get("wait"):
|
|
306
|
+
if bound is None:
|
|
307
|
+
envelope["wait"] = waits.not_started(wait).to_dict()
|
|
308
|
+
emit(console, envelope, op.columns, op.response.rows)
|
|
309
|
+
return
|
|
310
|
+
resume = _resume_command(wait, bound, values.get("timeout"), explicit_profile, project)
|
|
311
|
+
_wait(session, wait, bound, values.get("timeout"), project, console, envelope, op, resume, state)
|
|
312
|
+
return
|
|
313
|
+
if wait is not None and bound is not None:
|
|
314
|
+
envelope["next"] = _resume_command(wait, bound, None, explicit_profile, project)
|
|
315
|
+
emit(console, envelope, op.columns, op.response.rows)
|
|
316
|
+
finally:
|
|
317
|
+
session.transport.close()
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _wait(
|
|
321
|
+
session: Session,
|
|
322
|
+
wait: Wait,
|
|
323
|
+
bound: dict[str, str],
|
|
324
|
+
timeout: float | None,
|
|
325
|
+
project: str | None,
|
|
326
|
+
console: Console,
|
|
327
|
+
envelope: dict[str, Any],
|
|
328
|
+
op: Operation,
|
|
329
|
+
resume: str,
|
|
330
|
+
state: dict[str, Any],
|
|
331
|
+
*,
|
|
332
|
+
from_status: bool = False,
|
|
333
|
+
) -> None:
|
|
334
|
+
"""
|
|
335
|
+
Wait, then print the envelope with the wait's outcome, also when the wait does not succeed.
|
|
336
|
+
|
|
337
|
+
Whatever stops the wait, the operation's answer is printed. ``from_status`` makes the last status
|
|
338
|
+
answer the result: a status command run with ``--wait``.
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
def poll(on_state: Callable[[waits.WaitOutcome], None]) -> waits.WaitOutcome:
|
|
342
|
+
return waits.run(session, wait, bound, timeout_s=timeout, project=project, on_state=on_state)
|
|
343
|
+
|
|
344
|
+
report_wait(poll, console, envelope, op.columns, op.response.rows, resume, state, from_status=from_status)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def report_wait(
|
|
348
|
+
poll: Callable[[Callable[[waits.WaitOutcome], None]], waits.WaitOutcome],
|
|
349
|
+
console: Console,
|
|
350
|
+
envelope: dict[str, Any],
|
|
351
|
+
columns: tuple[str, ...],
|
|
352
|
+
rows: str | None,
|
|
353
|
+
resume: str,
|
|
354
|
+
state: dict[str, Any],
|
|
355
|
+
*,
|
|
356
|
+
from_status: bool = False,
|
|
357
|
+
then: Callable[[waits.WaitOutcome], str] | None = None,
|
|
358
|
+
) -> None:
|
|
359
|
+
"""
|
|
360
|
+
Run ``poll``, reporting each state on stderr, then print the envelope, also when the wait does not succeed.
|
|
361
|
+
|
|
362
|
+
A wait that can still end well (out of time, a transient failure, an interrupt) carries ``next``,
|
|
363
|
+
the command that resumes it without starting anything again. ``then`` names ``next`` after a success.
|
|
364
|
+
"""
|
|
365
|
+
state["next"] = resume
|
|
366
|
+
|
|
367
|
+
def on_state(outcome: waits.WaitOutcome) -> None:
|
|
368
|
+
console.progress(outcome.state, outcome.elapsed_s, outcome.status_operation)
|
|
369
|
+
|
|
370
|
+
def record(outcome: waits.WaitOutcome) -> None:
|
|
371
|
+
data = outcome.to_dict()
|
|
372
|
+
if from_status:
|
|
373
|
+
envelope["result"] = data.pop("status")
|
|
374
|
+
envelope["wait"] = data
|
|
375
|
+
|
|
376
|
+
def print_answer() -> None:
|
|
377
|
+
# A status command's only answer is the status: with none read yet there is nothing to print.
|
|
378
|
+
if envelope["result"] is not None or not from_status:
|
|
379
|
+
emit(console, envelope, columns, rows)
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
outcome = poll(on_state)
|
|
383
|
+
except DatabayError as exc:
|
|
384
|
+
if isinstance(exc, DatabayWaitError) and exc.outcome is not None:
|
|
385
|
+
record(exc.outcome)
|
|
386
|
+
if exc.recovery.action == "retry" or exc.exit_code == 3: # noqa: PLR2004 — exit 3: sign in, then resume
|
|
387
|
+
# Only a stop that a later poll can get past is worth resuming: a transient one, or a token
|
|
388
|
+
# that a new sign-in replaces.
|
|
389
|
+
envelope["next"] = resume
|
|
390
|
+
exc.next_command = resume
|
|
391
|
+
print_answer()
|
|
392
|
+
raise
|
|
393
|
+
except KeyboardInterrupt:
|
|
394
|
+
envelope["next"] = resume
|
|
395
|
+
print_answer()
|
|
396
|
+
raise
|
|
397
|
+
record(outcome)
|
|
398
|
+
if then is not None:
|
|
399
|
+
envelope["next"] = then(outcome)
|
|
400
|
+
emit(console, envelope, columns, rows)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _run_resume(
|
|
404
|
+
op: Operation,
|
|
405
|
+
call: CallValues,
|
|
406
|
+
session: Session,
|
|
407
|
+
console: Console,
|
|
408
|
+
values: dict[str, Any],
|
|
409
|
+
profile: str | None,
|
|
410
|
+
project: str | None,
|
|
411
|
+
state: dict[str, Any],
|
|
412
|
+
) -> None:
|
|
413
|
+
"""``--wait`` on a status command: poll it as the wait that started it would; the result is its last answer."""
|
|
414
|
+
assert op.resumes is not None
|
|
415
|
+
bound = {**{k: str(v) for k, v in call.path.items()}, **{k: str(v) for k, v in call.query.items()}}
|
|
416
|
+
resume = _resume_command(op.resumes, bound, values.get("timeout"), profile, project)
|
|
417
|
+
envelope: dict[str, Any] = {"result": None}
|
|
418
|
+
if project is not None:
|
|
419
|
+
envelope["project"] = project
|
|
420
|
+
_wait(
|
|
421
|
+
session,
|
|
422
|
+
op.resumes,
|
|
423
|
+
bound,
|
|
424
|
+
values.get("timeout"),
|
|
425
|
+
project,
|
|
426
|
+
console,
|
|
427
|
+
envelope,
|
|
428
|
+
op,
|
|
429
|
+
resume,
|
|
430
|
+
state,
|
|
431
|
+
from_status=True,
|
|
432
|
+
)
|
databay/cli/skill.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""``databay skill install``: copy the agent skill that ships with this CLI to where an agent loads it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from databay.agent_skill import SCOPES
|
|
10
|
+
from databay.cli.common import console_for
|
|
11
|
+
from databay.cli.handwritten import HANDWRITTEN
|
|
12
|
+
from databay.settings import DEFAULT_SKILL_SCOPE, OUTPUT_FORMATS
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group("skill", short_help=HANDWRITTEN["skill"][2])
|
|
16
|
+
def skill() -> None:
|
|
17
|
+
"""The agent skill: how an agent drives this CLI, with the command and error references."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
INSTALL_HELP = """Install the skill where the agent loads it, recording this CLI's version in its SKILL.md.
|
|
21
|
+
|
|
22
|
+
\b
|
|
23
|
+
claude: ~/.claude/skills/databay (user) or .claude/skills/databay (project)
|
|
24
|
+
codex: ~/.agents/skills/databay (user) or .agents/skills/databay (project)
|
|
25
|
+
|
|
26
|
+
A copy from another CLI version is replaced only with --force; without it the command exits 2
|
|
27
|
+
(skill_exists) and names both versions."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@skill.command("install", help=INSTALL_HELP)
|
|
31
|
+
@click.option("--target", required=True, metavar="AGENT", help="the agent to install for: claude or codex")
|
|
32
|
+
@click.option(
|
|
33
|
+
"--scope",
|
|
34
|
+
type=click.Choice(SCOPES),
|
|
35
|
+
default=DEFAULT_SKILL_SCOPE,
|
|
36
|
+
show_default=True,
|
|
37
|
+
help="user: under your home, for every project; project: under the current directory",
|
|
38
|
+
)
|
|
39
|
+
@click.option("--force", is_flag=True, default=False, help="replace a copy installed by another CLI version")
|
|
40
|
+
@click.option("--output", type=click.Choice(OUTPUT_FORMATS), default=None, help="json, table or csv")
|
|
41
|
+
@click.pass_context
|
|
42
|
+
def install(ctx: click.Context, target: str, scope: str, force: bool, output: str | None) -> None:
|
|
43
|
+
from databay.agent_skill import install as install_skill # noqa: PLC0415
|
|
44
|
+
from databay.cli.output import emit # noqa: PLC0415
|
|
45
|
+
from databay.version import CLIENT_VERSION # noqa: PLC0415
|
|
46
|
+
|
|
47
|
+
console, _ = console_for(ctx, {"output": output})
|
|
48
|
+
path, previous = install_skill(
|
|
49
|
+
target,
|
|
50
|
+
scope, # type: ignore[arg-type] — click checked the choice
|
|
51
|
+
force=force,
|
|
52
|
+
version=CLIENT_VERSION,
|
|
53
|
+
home=Path.home(),
|
|
54
|
+
cwd=Path.cwd(),
|
|
55
|
+
)
|
|
56
|
+
result = {
|
|
57
|
+
"target": target,
|
|
58
|
+
"scope": scope,
|
|
59
|
+
"path": str(path),
|
|
60
|
+
"version": CLIENT_VERSION,
|
|
61
|
+
"replaced": previous is not None,
|
|
62
|
+
"replaced_version": previous.version if previous is not None else None,
|
|
63
|
+
}
|
|
64
|
+
emit(console, {"result": result}, ())
|
databay/cli/sql.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""``databay sql``: the curated command for ``POST /api/configstore/sql/execute``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from databay.cli.common import call_once, common_options, result_envelope
|
|
12
|
+
|
|
13
|
+
SQL_HELP = """Run one read-only statement and print its rows.
|
|
14
|
+
|
|
15
|
+
\b
|
|
16
|
+
The statement is the argument, or the text of --file PATH (--file - reads
|
|
17
|
+
stdin), or stdin when neither is given and stdin is not a terminal.
|
|
18
|
+
\b
|
|
19
|
+
JSON prints the server's answer (columns, rows, row_count, truncated,
|
|
20
|
+
warnings); a table or CSV prints the rows under their column names.
|
|
21
|
+
|
|
22
|
+
\b
|
|
23
|
+
Example:
|
|
24
|
+
databay sql --db <db_uuid> "select count(*) from analytics.orders\""""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _statement(argument: str | None, path: str | None) -> str:
|
|
28
|
+
from databay.errors import DatabayUsageError # noqa: PLC0415
|
|
29
|
+
|
|
30
|
+
if argument is not None and path is not None:
|
|
31
|
+
raise DatabayUsageError("pass the statement as the argument or with --file, not both")
|
|
32
|
+
if argument is not None:
|
|
33
|
+
text = argument
|
|
34
|
+
elif path == "-" or (path is None and not sys.stdin.isatty()):
|
|
35
|
+
text = sys.stdin.read()
|
|
36
|
+
elif path is not None:
|
|
37
|
+
try:
|
|
38
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
39
|
+
except OSError as exc:
|
|
40
|
+
raise DatabayUsageError(f"--file {path}: {exc.strerror}") from None
|
|
41
|
+
else:
|
|
42
|
+
raise DatabayUsageError("pass the statement as the argument, with --file PATH, or on stdin")
|
|
43
|
+
if not text.strip():
|
|
44
|
+
raise DatabayUsageError("the statement is empty")
|
|
45
|
+
return text.strip()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@click.command("sql", help=SQL_HELP, short_help="Run one read-only SQL statement")
|
|
49
|
+
@click.argument("statement", required=False)
|
|
50
|
+
@click.option("--file", "file", default=None, metavar="PATH", help="read the statement from a file, or - for stdin")
|
|
51
|
+
@click.option(
|
|
52
|
+
"--db", "db", default=None, metavar="DB_UUID", help="the warehouse to run against (default: the org's default)"
|
|
53
|
+
)
|
|
54
|
+
@click.option("--project", default=None, help="the project to act in (default: the active project)")
|
|
55
|
+
@common_options
|
|
56
|
+
@click.pass_context
|
|
57
|
+
def sql(
|
|
58
|
+
ctx: click.Context,
|
|
59
|
+
statement: str | None,
|
|
60
|
+
file: str | None,
|
|
61
|
+
db: str | None,
|
|
62
|
+
project: str | None,
|
|
63
|
+
**values: Any, # noqa: ANN401
|
|
64
|
+
) -> None:
|
|
65
|
+
from databay.cli.output import emit_grid # noqa: PLC0415
|
|
66
|
+
from databay.invoke import CallValues # noqa: PLC0415
|
|
67
|
+
from databay.operations.sql import SQL_EXECUTE # noqa: PLC0415
|
|
68
|
+
|
|
69
|
+
query = _statement(statement, file)
|
|
70
|
+
body: dict[str, Any] = {"query": query}
|
|
71
|
+
if db is not None:
|
|
72
|
+
body["db_uuid"] = db
|
|
73
|
+
call = CallValues(body=body, has_body=True, project_flag=project)
|
|
74
|
+
console, result = call_once(ctx, values, SQL_EXECUTE, call)
|
|
75
|
+
answer = result.parsed
|
|
76
|
+
if answer.truncated:
|
|
77
|
+
console.notice(
|
|
78
|
+
"truncated",
|
|
79
|
+
f"the server returned the first {answer.row_count} rows",
|
|
80
|
+
only_on_terminal=console.output == "json",
|
|
81
|
+
)
|
|
82
|
+
for warning in answer.warnings or []:
|
|
83
|
+
console.notice("warning", f"{warning.code}: {warning.message}", only_on_terminal=console.output == "json")
|
|
84
|
+
emit_grid(console, result_envelope(result), list(answer.columns), [list(row) for row in answer.rows])
|