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/common.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""What every hand-written command shares with the generated ones: the common flags and the console."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from dataclasses import replace
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
from databay.cli.build import click_parameter
|
|
11
|
+
from databay.cli.flags import common_flags
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Callable, Iterator
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from databay.cli.console import Console
|
|
20
|
+
from databay.invoke import CallValues, Result
|
|
21
|
+
from databay.operation import Operation
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def common_options(command: Callable[..., Any], *, profile_help: str | None = None) -> Callable[..., Any]:
|
|
25
|
+
"""
|
|
26
|
+
Add ``--output``, ``--profile``, ``--verbose`` and ``--no-retry``, built as the generated commands build them.
|
|
27
|
+
|
|
28
|
+
``profile_help`` replaces the help of ``--profile`` for a command that writes the profile (``login``).
|
|
29
|
+
"""
|
|
30
|
+
flags = [replace(f, help=profile_help) if profile_help and f.role == "profile" else f for f in common_flags()]
|
|
31
|
+
params = getattr(command, "__click_params__", [])
|
|
32
|
+
params.extend(click_parameter(flag) for flag in reversed(flags))
|
|
33
|
+
command.__click_params__ = params # type: ignore[attr-defined]
|
|
34
|
+
return command
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def login_options(command: Callable[..., Any]) -> Callable[..., Any]:
|
|
38
|
+
from databay.settings import DEFAULT_PROFILE # noqa: PLC0415
|
|
39
|
+
|
|
40
|
+
return common_options(command, profile_help=f"the profile to create or update (default: {DEFAULT_PROFILE})")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def console_for(ctx: click.Context, values: dict[str, Any]) -> tuple[Console, dict[str, Any]]:
|
|
44
|
+
"""The command's console, registered in the CLI state so a failure is printed through it."""
|
|
45
|
+
from databay.cli.console import Console, output_format # noqa: PLC0415
|
|
46
|
+
|
|
47
|
+
state: dict[str, Any] = ctx.ensure_object(dict)
|
|
48
|
+
console = Console(output=output_format(values.get("output"), sys.stdout), verbose=bool(values.get("verbose")))
|
|
49
|
+
state["console"] = console
|
|
50
|
+
return console, state
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@contextmanager
|
|
54
|
+
def http_client(state: dict[str, Any]) -> Iterator[httpx.Client]:
|
|
55
|
+
"""The httpx client requests go through: the one ``main`` was handed (tests), else a new one closed after."""
|
|
56
|
+
if state.get("http") is not None:
|
|
57
|
+
yield state["http"]
|
|
58
|
+
return
|
|
59
|
+
from databay.settings import SETTINGS # noqa: PLC0415
|
|
60
|
+
from databay.transport import new_http_client # noqa: PLC0415
|
|
61
|
+
|
|
62
|
+
with new_http_client(SETTINGS) as client:
|
|
63
|
+
yield client
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def call_once(ctx: click.Context, values: dict[str, Any], op: Operation, call: CallValues) -> tuple[Console, Result]:
|
|
67
|
+
"""Send ``op`` once as a curated read (``sql``, ``metrics query``), recording the context a failure prints."""
|
|
68
|
+
from databay.cli.runner import open_session # noqa: PLC0415
|
|
69
|
+
from databay.invoke import resolve_project # noqa: PLC0415
|
|
70
|
+
|
|
71
|
+
console, state = console_for(ctx, values)
|
|
72
|
+
session, chosen = open_session(values, console, state)
|
|
73
|
+
try:
|
|
74
|
+
state["context"] = {
|
|
75
|
+
"server": chosen.server,
|
|
76
|
+
"profile": chosen.name,
|
|
77
|
+
"project": resolve_project(op, call, session.active_project),
|
|
78
|
+
"request_id": session.transport.request_id,
|
|
79
|
+
}
|
|
80
|
+
state["reading"] = True
|
|
81
|
+
result = session.call(op, call)
|
|
82
|
+
finally:
|
|
83
|
+
session.transport.close()
|
|
84
|
+
state["context"]["request_id"] = result.request_id
|
|
85
|
+
return console, result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def result_envelope(result: Result) -> dict[str, Any]:
|
|
89
|
+
"""``{"result": ..., "project": ...}``: the JSON a curated command prints."""
|
|
90
|
+
envelope: dict[str, Any] = {"result": result.raw}
|
|
91
|
+
if result.project is not None:
|
|
92
|
+
envelope["project"] = result.project
|
|
93
|
+
return envelope
|
databay/cli/console.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Where the CLI writes: the result on stdout, everything else on stderr.
|
|
3
|
+
|
|
4
|
+
stdout carries only the result, so piping it never mixes in progress or hints. On stderr, a
|
|
5
|
+
terminal gets short human lines and captured output gets one JSON object per line.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any, TextIO
|
|
15
|
+
|
|
16
|
+
from databay.errors import DatabayUsageError
|
|
17
|
+
from databay.settings import ENV_NO_COLOR, ENV_OUTPUT, OUTPUT_FORMATS, OutputFormat
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def output_format(flag: str | None, stdout: TextIO) -> OutputFormat:
|
|
21
|
+
"""``--output``, else ``$DATABAY_OUTPUT``, else a table on a terminal and JSON when captured."""
|
|
22
|
+
return output_rule(flag, stdout)[0]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def output_rule(flag: str | None, stdout: TextIO) -> tuple[OutputFormat, str]:
|
|
26
|
+
"""The output format and which part of the output rule chose it (``databay status`` prints it)."""
|
|
27
|
+
if flag is not None:
|
|
28
|
+
return flag, "--output" # type: ignore[return-value] — click already checked the choice
|
|
29
|
+
env = os.environ.get(ENV_OUTPUT)
|
|
30
|
+
if env:
|
|
31
|
+
if env not in OUTPUT_FORMATS:
|
|
32
|
+
raise DatabayUsageError(f"{ENV_OUTPUT}={env!r} is not one of {', '.join(OUTPUT_FORMATS)}")
|
|
33
|
+
return env, ENV_OUTPUT # type: ignore[return-value]
|
|
34
|
+
if stdout.isatty():
|
|
35
|
+
return "table", "stdout is a terminal"
|
|
36
|
+
return "json", "stdout is captured"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Console:
|
|
41
|
+
output: OutputFormat = "json"
|
|
42
|
+
verbose: bool = False
|
|
43
|
+
stdout: TextIO = field(default_factory=lambda: sys.stdout)
|
|
44
|
+
stderr: TextIO = field(default_factory=lambda: sys.stderr)
|
|
45
|
+
_live_line: bool = False
|
|
46
|
+
#: What ``notice`` already showed on a terminal, so the error lines do not repeat it.
|
|
47
|
+
_noticed: dict[str, str] = field(default_factory=dict)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def stderr_tty(self) -> bool:
|
|
51
|
+
return self.stderr.isatty()
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def color(self) -> bool:
|
|
55
|
+
return self.stdout.isatty() and not os.environ.get(ENV_NO_COLOR)
|
|
56
|
+
|
|
57
|
+
def _end_live_line(self) -> None:
|
|
58
|
+
if self._live_line:
|
|
59
|
+
self.stderr.write("\n")
|
|
60
|
+
self._live_line = False
|
|
61
|
+
|
|
62
|
+
def event(self, name: str, fields: dict[str, Any]) -> None:
|
|
63
|
+
"""One stderr event; ``request`` and ``response`` events only under ``--verbose``."""
|
|
64
|
+
if name in {"request", "response", "cache_invalid", "cache_write_failed"} and not self.verbose:
|
|
65
|
+
return
|
|
66
|
+
if not self.stderr_tty:
|
|
67
|
+
self.stderr.write(json.dumps({"event": name, **fields}, default=str) + "\n")
|
|
68
|
+
self.stderr.flush()
|
|
69
|
+
return
|
|
70
|
+
self._end_live_line()
|
|
71
|
+
details = " ".join(f"{key}={value}" for key, value in fields.items() if value is not None)
|
|
72
|
+
self.stderr.write(f"{name}: {details}\n")
|
|
73
|
+
self.stderr.flush()
|
|
74
|
+
|
|
75
|
+
def progress(self, state: str | None, elapsed_s: float, operation: str) -> None:
|
|
76
|
+
"""A wait's state: a live line on a terminal, one ``wait`` event per state change when captured."""
|
|
77
|
+
if not self.stderr_tty:
|
|
78
|
+
self.event("wait", {"state": state, "elapsed_s": round(elapsed_s, 1), "status_operation": operation})
|
|
79
|
+
return
|
|
80
|
+
self.stderr.write(f"\rwaiting: {state} ({round(elapsed_s)}s)\033[K")
|
|
81
|
+
self.stderr.flush()
|
|
82
|
+
self._live_line = True
|
|
83
|
+
|
|
84
|
+
def notice(self, name: str, value: str, *, only_on_terminal: bool) -> None:
|
|
85
|
+
"""
|
|
86
|
+
A value the result does not carry, such as ``next``: ``name: value`` on a terminal, an event when captured.
|
|
87
|
+
|
|
88
|
+
``only_on_terminal`` is for JSON output, whose envelope already holds the value.
|
|
89
|
+
"""
|
|
90
|
+
if self.stderr_tty:
|
|
91
|
+
self._end_live_line()
|
|
92
|
+
self.stderr.write(f"{name}: {value}\n")
|
|
93
|
+
self._noticed[name] = value
|
|
94
|
+
return
|
|
95
|
+
if not only_on_terminal:
|
|
96
|
+
self.event(name, {"value": value})
|
|
97
|
+
|
|
98
|
+
def inform(self, name: str, fields: dict[str, Any], text: str) -> None:
|
|
99
|
+
"""Something the user must act on: ``text`` on a terminal, the ``name`` event with ``fields`` when captured."""
|
|
100
|
+
if not self.stderr_tty:
|
|
101
|
+
self.event(name, fields)
|
|
102
|
+
return
|
|
103
|
+
self._end_live_line()
|
|
104
|
+
self.stderr.write(text + "\n")
|
|
105
|
+
self.stderr.flush()
|
|
106
|
+
|
|
107
|
+
def error(self, body: dict[str, Any]) -> None:
|
|
108
|
+
self._end_live_line()
|
|
109
|
+
if not self.stderr_tty:
|
|
110
|
+
self.stderr.write(json.dumps({"error": body}, default=str) + "\n")
|
|
111
|
+
self.stderr.flush()
|
|
112
|
+
return
|
|
113
|
+
self.stderr.write(f"error: {body['code']}: {body['message']}\n")
|
|
114
|
+
if body.get("hint"):
|
|
115
|
+
self.stderr.write(f"hint: {body['hint']}\n")
|
|
116
|
+
if body.get("next") and self._noticed.get("next") != body["next"]:
|
|
117
|
+
self.stderr.write(f"next: {body['next']}\n")
|
|
118
|
+
trace_id = (body.get("details") or {}).get("trace_id")
|
|
119
|
+
if trace_id and (body.get("status") or 0) >= 500: # noqa: PLR2004
|
|
120
|
+
self.stderr.write(f"trace_id: {trace_id}\n")
|
|
121
|
+
self.stderr.flush()
|
databay/cli/flags.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The arguments and flags of one generated command, as plain data.
|
|
3
|
+
|
|
4
|
+
``databay.cli.build`` turns them into click parameters and ``databay.reference`` renders them into
|
|
5
|
+
``commands.md``, so ``--help`` and the reference list the same flags with the same text. Standard
|
|
6
|
+
library only: ``python/tools/gen_client.py`` reads it.
|
|
7
|
+
|
|
8
|
+
- A path parameter is a positional argument, in path order. The project's own path parameter is
|
|
9
|
+
optional when nothing required follows it.
|
|
10
|
+
- A query or header parameter, or a body field, is ``--<name in kebab case>``. A list repeats the
|
|
11
|
+
flag; an object takes JSON text.
|
|
12
|
+
- A secret field (``format: password``) is never a value flag: ``--<name>-env NAME`` names a
|
|
13
|
+
variable that holds it and ``--<name>-stdin`` reads it from stdin.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import TYPE_CHECKING, Literal
|
|
20
|
+
|
|
21
|
+
from databay.settings import OUTPUT_FORMATS
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from databay.operation import Operation, Param
|
|
25
|
+
|
|
26
|
+
Role = Literal[
|
|
27
|
+
"argument",
|
|
28
|
+
"option",
|
|
29
|
+
"secret_env",
|
|
30
|
+
"secret_stdin",
|
|
31
|
+
"project",
|
|
32
|
+
"file",
|
|
33
|
+
"yes",
|
|
34
|
+
"wait",
|
|
35
|
+
"timeout",
|
|
36
|
+
"idempotency_key",
|
|
37
|
+
"output",
|
|
38
|
+
"profile",
|
|
39
|
+
"verbose",
|
|
40
|
+
"no_retry",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
#: Flags the CLI adds itself; a parameter or body field that would take one of these names is a
|
|
44
|
+
#: generator error.
|
|
45
|
+
RESERVED_FLAGS = frozenset(
|
|
46
|
+
{"project", "file", "yes", "wait", "timeout", "idempotency-key", "output", "profile", "verbose", "no-retry", "help"}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True, slots=True)
|
|
51
|
+
class Flag:
|
|
52
|
+
role: Role
|
|
53
|
+
#: The flag (``--source-id``) or the argument's metavar (``PIPE_UUID``).
|
|
54
|
+
name: str
|
|
55
|
+
dest: str
|
|
56
|
+
help: str
|
|
57
|
+
required: bool = False
|
|
58
|
+
param: Param | None = None
|
|
59
|
+
#: ``value``: takes a value; ``switch``: a bare flag; ``bool``: ``--x`` sends true and ``--no-x``
|
|
60
|
+
#: false; ``multiple``: repeated, one item each.
|
|
61
|
+
takes: Literal["value", "switch", "bool", "multiple", "argument"] = "value"
|
|
62
|
+
choices: tuple[str, ...] = ()
|
|
63
|
+
metavar: str | None = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _value_help(param: Param) -> str:
|
|
67
|
+
text = param.description.strip().splitlines()[0] if param.description.strip() else ""
|
|
68
|
+
if param.kind == "json":
|
|
69
|
+
text = f"{text} (JSON)".strip()
|
|
70
|
+
return text
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _argument(param: Param, *, required: bool) -> Flag:
|
|
74
|
+
return Flag(
|
|
75
|
+
"argument",
|
|
76
|
+
param.name.upper(),
|
|
77
|
+
param.identifier,
|
|
78
|
+
_value_help(param),
|
|
79
|
+
required=required,
|
|
80
|
+
param=param,
|
|
81
|
+
takes="argument",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _option(param: Param, *, role: Role = "option") -> list[Flag]:
|
|
86
|
+
if param.secret:
|
|
87
|
+
return [
|
|
88
|
+
Flag(
|
|
89
|
+
"secret_env",
|
|
90
|
+
f"--{param.flag}-env",
|
|
91
|
+
f"{param.identifier}_env",
|
|
92
|
+
f"name of the environment variable that holds {param.name} (secret)",
|
|
93
|
+
param=param,
|
|
94
|
+
metavar="NAME",
|
|
95
|
+
),
|
|
96
|
+
Flag(
|
|
97
|
+
"secret_stdin",
|
|
98
|
+
f"--{param.flag}-stdin",
|
|
99
|
+
f"{param.identifier}_stdin",
|
|
100
|
+
f"read {param.name} (secret) from stdin",
|
|
101
|
+
param=param,
|
|
102
|
+
takes="switch",
|
|
103
|
+
),
|
|
104
|
+
]
|
|
105
|
+
takes = "multiple" if param.kind == "list" else "bool" if param.kind == "boolean" else "value"
|
|
106
|
+
return [
|
|
107
|
+
Flag(
|
|
108
|
+
role,
|
|
109
|
+
f"--{param.flag}",
|
|
110
|
+
param.identifier,
|
|
111
|
+
_value_help(param),
|
|
112
|
+
required=param.required,
|
|
113
|
+
param=param,
|
|
114
|
+
takes=takes,
|
|
115
|
+
choices=param.choices,
|
|
116
|
+
)
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _project_path_optional(op: Operation) -> bool:
|
|
121
|
+
location = op.project_location
|
|
122
|
+
if location is None or location[0] != "path":
|
|
123
|
+
return False
|
|
124
|
+
names = [p.name for p in op.path_params]
|
|
125
|
+
return names[-1] == location[1]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def command_flags(op: Operation, variant: str | None = None) -> list[Flag]:
|
|
129
|
+
"""Every argument and flag of ``op`` (of its body variant ``variant``), in the order ``--help`` shows them."""
|
|
130
|
+
location = op.project_location
|
|
131
|
+
flags: list[Flag] = []
|
|
132
|
+
optional_last = _project_path_optional(op)
|
|
133
|
+
for index, param in enumerate(op.path_params):
|
|
134
|
+
is_project = location is not None and location == ("path", param.name)
|
|
135
|
+
last = index == len(op.path_params) - 1
|
|
136
|
+
flags.append(_argument(param, required=not (is_project and optional_last and last)))
|
|
137
|
+
for param in op.params:
|
|
138
|
+
if param.location == "path":
|
|
139
|
+
continue
|
|
140
|
+
if location is not None and location == (param.location, param.name):
|
|
141
|
+
flags.append(
|
|
142
|
+
Flag("option", f"--{param.flag}", param.identifier, _value_help(param), param=param, takes="value")
|
|
143
|
+
)
|
|
144
|
+
continue
|
|
145
|
+
flags += _option(param)
|
|
146
|
+
if op.body is not None:
|
|
147
|
+
for param in op.body.fields_of(variant):
|
|
148
|
+
if location is not None and location == ("body", param.name):
|
|
149
|
+
flags.append(
|
|
150
|
+
Flag("option", f"--{param.flag}", param.identifier, _value_help(param), param=param, takes="value")
|
|
151
|
+
)
|
|
152
|
+
continue
|
|
153
|
+
flags += _option(param)
|
|
154
|
+
flags.append(Flag("file", "--file", "file", "read the whole body as JSON from a file, or - for stdin"))
|
|
155
|
+
if op.project is not None:
|
|
156
|
+
flags.append(Flag("project", "--project", "project", "the project to act in (default: the active project)"))
|
|
157
|
+
if op.idempotent:
|
|
158
|
+
flags.append(
|
|
159
|
+
Flag(
|
|
160
|
+
"idempotency_key",
|
|
161
|
+
"--idempotency-key",
|
|
162
|
+
"idempotency_key",
|
|
163
|
+
"repeat an earlier call: the server answers it again and creates nothing (default: a new key)",
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
if op.destructive:
|
|
167
|
+
flags.append(Flag("yes", "--yes", "yes", "confirm: this removes or overwrites state", takes="switch"))
|
|
168
|
+
if op.offers_wait:
|
|
169
|
+
flags.append(Flag("wait", "--wait", "wait", "wait until it is done, polling its status", takes="switch"))
|
|
170
|
+
flags.append(
|
|
171
|
+
Flag("timeout", "--timeout", "timeout", "seconds --wait may take before exit 4 (resumable)", metavar="S")
|
|
172
|
+
)
|
|
173
|
+
flags += common_flags()
|
|
174
|
+
return flags
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def common_flags() -> list[Flag]:
|
|
178
|
+
return [
|
|
179
|
+
Flag("output", "--output", "output", "json, table or csv", choices=OUTPUT_FORMATS),
|
|
180
|
+
Flag("profile", "--profile", "profile", "the profile to use (default: current_profile)"),
|
|
181
|
+
Flag("verbose", "--verbose", "verbose", "log each request on stderr, secrets redacted", takes="switch"),
|
|
182
|
+
Flag("no_retry", "--no-retry", "no_retry", "send each request once", takes="switch"),
|
|
183
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Commands written by hand, for work no single operation does: local state, sign-in, curated flows.
|
|
3
|
+
|
|
4
|
+
Each entry is ``command -> (module, attribute, summary)``. A one-word command sits beside the
|
|
5
|
+
generated nouns; ``"<noun> <verb>"`` adds a verb to that generated noun (``project use``). The root
|
|
6
|
+
group imports the module only when the command, or its noun, runs. An operation declared
|
|
7
|
+
``agent_shape="curated:<command>"`` gets no generated command; its command belongs here.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
HANDWRITTEN: dict[str, tuple[str, str, str]] = {
|
|
11
|
+
"login": ("databay.cli.auth", "login", "Sign in and save a profile"),
|
|
12
|
+
"logout": ("databay.cli.auth", "logout", "Sign out: revoke and delete the profile's credential"),
|
|
13
|
+
"profile": ("databay.cli.profile", "profile", "Switch between and list profiles"),
|
|
14
|
+
"status": ("databay.cli.profile", "status", "Who and where: profile, server, user, org, project, skill"),
|
|
15
|
+
"skill": ("databay.cli.skill", "skill", "Install the agent skill for Claude Code or Codex"),
|
|
16
|
+
"org switch": ("databay.cli.org", "org_switch", "Make another organization active and refresh the token"),
|
|
17
|
+
"project use": ("databay.cli.profile", "project_use", "Set the profile's active project"),
|
|
18
|
+
"pipe wait": ("databay.cli.pipe_wait", "pipe_wait", "Wait until a pipe is active or its table is ready to query"),
|
|
19
|
+
"metrics describe": ("databay.cli.metrics", "describe", "Describe one metric you can query"),
|
|
20
|
+
"metrics query": ("databay.cli.metrics", "query", "Query one metric by dimensions and time grain"),
|
|
21
|
+
"sql": ("databay.cli.sql", "sql", "Run one read-only SQL statement"),
|
|
22
|
+
}
|
databay/cli/main.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The ``databay`` entry point.
|
|
3
|
+
|
|
4
|
+
Each noun's commands live in their own generated module, imported only when that noun runs, so
|
|
5
|
+
``databay --help`` and ``databay pipe get --help`` load almost nothing. Every failure leaves through
|
|
6
|
+
``fail``: one error object on stderr and the exit code its code names.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
import traceback
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
|
|
19
|
+
from databay.cli.handwritten import HANDWRITTEN
|
|
20
|
+
from databay.errors import DatabayError
|
|
21
|
+
from databay.platform_check import ensure_supported
|
|
22
|
+
from databay.settings import (
|
|
23
|
+
ENV_CONFIG_DIR,
|
|
24
|
+
ENV_NO_UPDATE_CHECK,
|
|
25
|
+
ENV_OUTPUT,
|
|
26
|
+
ENV_STATE_DIR,
|
|
27
|
+
ENV_TOKEN,
|
|
28
|
+
OUTPUT_RULE,
|
|
29
|
+
)
|
|
30
|
+
from databay.spec import API_VERSION, NOUNS
|
|
31
|
+
from databay.version import CLIENT_VERSION
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from collections.abc import Sequence
|
|
35
|
+
|
|
36
|
+
import httpx
|
|
37
|
+
|
|
38
|
+
from databay.update_check import UpdateCheck
|
|
39
|
+
|
|
40
|
+
ROOT_HELP = f"""Databay from the command line: databay <noun> <verb> [ARGS] [OPTIONS].
|
|
41
|
+
|
|
42
|
+
{OUTPUT_RULE} stdout carries only the result; progress, hints and errors go to stderr.
|
|
43
|
+
|
|
44
|
+
\b
|
|
45
|
+
Environment:
|
|
46
|
+
{ENV_TOKEN} the access token, for a profile whose auth is "env";
|
|
47
|
+
set under a login or api_key profile it is an error
|
|
48
|
+
{ENV_OUTPUT} json, table or csv
|
|
49
|
+
{ENV_CONFIG_DIR} where config.toml lives (default: the platform's config dir)
|
|
50
|
+
{ENV_STATE_DIR} where caches and crash logs go (default: the platform's state dir)
|
|
51
|
+
{ENV_NO_UPDATE_CHECK}=1 no daily check for a newer release (also off under CI)
|
|
52
|
+
NO_COLOR no colors in tables
|
|
53
|
+
|
|
54
|
+
\b
|
|
55
|
+
Exit codes: 0 ok, 1 refused, 2 usage, 3 profile or token, 4 still pending,
|
|
56
|
+
5 wait failed, 6 transient, 7 version mismatch, 8 client outdated or bug,
|
|
57
|
+
9 not ready. A failure prints {{"error": {{"code", "message", "hint", "next",
|
|
58
|
+
"recovery", "context"}}}} on stderr when captured."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RootGroup(click.Group):
|
|
62
|
+
"""Lists every noun from the generated index; imports a noun's module only to run it."""
|
|
63
|
+
|
|
64
|
+
def list_commands(self, ctx: click.Context) -> list[str]: # noqa: ARG002 — click's signature
|
|
65
|
+
return sorted(set(NOUNS) | {name for name in HANDWRITTEN if " " not in name})
|
|
66
|
+
|
|
67
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None: # noqa: ARG002
|
|
68
|
+
if cmd_name in HANDWRITTEN:
|
|
69
|
+
return _handwritten(cmd_name)
|
|
70
|
+
if cmd_name not in NOUNS:
|
|
71
|
+
return None
|
|
72
|
+
module, _ = NOUNS[cmd_name]
|
|
73
|
+
group: click.Group = importlib.import_module(f"databay.cli.commands.{module}").group
|
|
74
|
+
for verb in _handwritten_verbs(cmd_name):
|
|
75
|
+
group.add_command(_handwritten(f"{cmd_name} {verb}"), verb)
|
|
76
|
+
return group
|
|
77
|
+
|
|
78
|
+
def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None: # noqa: ARG002
|
|
79
|
+
rows = [(noun, ", ".join(sorted([*verbs, *_handwritten_verbs(noun)]))) for noun, (_, verbs) in NOUNS.items()]
|
|
80
|
+
rows += [(name, summary) for name, (_, _, summary) in HANDWRITTEN.items() if " " not in name]
|
|
81
|
+
with formatter.section("Commands"):
|
|
82
|
+
formatter.write_dl(sorted(rows))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _handwritten(command: str) -> click.Command:
|
|
86
|
+
module, attribute, _ = HANDWRITTEN[command]
|
|
87
|
+
return getattr(importlib.import_module(module), attribute)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _handwritten_verbs(noun: str) -> list[str]:
|
|
91
|
+
"""The hand-written verbs of a generated noun (``project use``)."""
|
|
92
|
+
return [verb for name in HANDWRITTEN for head, _, verb in [name.partition(" ")] if verb and head == noun]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@click.group(cls=RootGroup, help=ROOT_HELP, context_settings={"max_content_width": 110})
|
|
96
|
+
@click.version_option(f"{CLIENT_VERSION} (API {API_VERSION})", prog_name="databay", message="%(prog)s %(version)s")
|
|
97
|
+
def root() -> None:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _crash_log(exc: BaseException) -> str | None:
|
|
102
|
+
from databay.paths import state_dir # noqa: PLC0415
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
directory = state_dir()
|
|
106
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
path = directory / f"crash-{time.strftime('%Y%m%dT%H%M%S')}.log"
|
|
108
|
+
path.write_text("".join(traceback.format_exception(exc)), encoding="utf-8")
|
|
109
|
+
except OSError:
|
|
110
|
+
return None
|
|
111
|
+
return str(path)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def fail(error: DatabayError, state: dict[str, Any]) -> int:
|
|
115
|
+
from databay.cli.console import Console # noqa: PLC0415
|
|
116
|
+
|
|
117
|
+
console: Console = state.get("console") or Console()
|
|
118
|
+
body = error.to_dict()
|
|
119
|
+
body["context"] = {
|
|
120
|
+
"cli_version": CLIENT_VERSION,
|
|
121
|
+
"api_version": API_VERSION,
|
|
122
|
+
**dict.fromkeys(("server", "profile", "project", "request_id")),
|
|
123
|
+
**state.get("context", {}),
|
|
124
|
+
}
|
|
125
|
+
console.error(body)
|
|
126
|
+
return error.exit_code
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def main(argv: Sequence[str] | None = None, *, http: httpx.Client | None = None) -> int:
|
|
130
|
+
"""
|
|
131
|
+
Run the CLI and return its exit code; the console script exits with it.
|
|
132
|
+
|
|
133
|
+
``http`` is the httpx client every request goes through; tests pass one on ``httpx.MockTransport``.
|
|
134
|
+
"""
|
|
135
|
+
state: dict[str, Any] = {"http": http}
|
|
136
|
+
update = _start_update_check()
|
|
137
|
+
try:
|
|
138
|
+
ensure_supported()
|
|
139
|
+
root.main(args=list(argv) if argv is not None else None, prog_name="databay", standalone_mode=False, obj=state)
|
|
140
|
+
except DatabayError as exc:
|
|
141
|
+
code = fail(exc, state)
|
|
142
|
+
except click.exceptions.NoArgsIsHelpError as exc:
|
|
143
|
+
click.echo(exc.ctx.get_help() if exc.ctx is not None else exc.format_message())
|
|
144
|
+
code = 0
|
|
145
|
+
except click.exceptions.Abort:
|
|
146
|
+
from databay.errors import DatabayCancelled # noqa: PLC0415
|
|
147
|
+
|
|
148
|
+
if state.get("sent"):
|
|
149
|
+
key = state.get("idempotency_key")
|
|
150
|
+
error = DatabayCancelled(
|
|
151
|
+
"interrupted after the request was sent",
|
|
152
|
+
code="interrupted",
|
|
153
|
+
details={"idempotency_key": key} if key else {},
|
|
154
|
+
next_command=state.get("next"),
|
|
155
|
+
)
|
|
156
|
+
elif state.get("reading"):
|
|
157
|
+
error = DatabayCancelled(
|
|
158
|
+
"stopped while reading; nothing was changed", code="stopped", next_command=state.get("next")
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
error = DatabayCancelled("cancelled; nothing was sent")
|
|
162
|
+
code = fail(error, state)
|
|
163
|
+
except click.UsageError as exc:
|
|
164
|
+
from databay.errors import DatabayUsageError # noqa: PLC0415
|
|
165
|
+
|
|
166
|
+
hint = exc.ctx.get_usage() if exc.ctx is not None else None
|
|
167
|
+
code = fail(DatabayUsageError(exc.format_message(), hint=hint), state)
|
|
168
|
+
except click.ClickException as exc:
|
|
169
|
+
from databay.errors import DatabayUsageError # noqa: PLC0415
|
|
170
|
+
|
|
171
|
+
code = fail(DatabayUsageError(exc.format_message()), state)
|
|
172
|
+
except Exception as exc: # noqa: BLE001 — the last line: every crash leaves as internal_error
|
|
173
|
+
path = _crash_log(exc)
|
|
174
|
+
if state.get("console") is not None and state["console"].verbose:
|
|
175
|
+
traceback.print_exception(exc, file=sys.stderr)
|
|
176
|
+
error = DatabayError(
|
|
177
|
+
f"databay crashed: {type(exc).__name__}" + (f"; the trace is in {path}" if path else ""),
|
|
178
|
+
details={"crash_log": path},
|
|
179
|
+
)
|
|
180
|
+
code = fail(error, state)
|
|
181
|
+
else:
|
|
182
|
+
code = 0
|
|
183
|
+
if update is not None:
|
|
184
|
+
console = state.get("console")
|
|
185
|
+
update.finish(sys.stderr, console.event if console is not None and console.verbose else None)
|
|
186
|
+
return code
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _start_update_check() -> UpdateCheck | None:
|
|
190
|
+
import os # noqa: PLC0415
|
|
191
|
+
|
|
192
|
+
from databay import update_check # noqa: PLC0415
|
|
193
|
+
|
|
194
|
+
if not update_check.enabled(dict(os.environ), sys.stdout, sys.stderr):
|
|
195
|
+
return None
|
|
196
|
+
check = update_check.UpdateCheck()
|
|
197
|
+
check.start()
|
|
198
|
+
return check
|