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/metrics.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""``databay metrics describe`` and ``databay metrics query``: the curated commands of the metrics API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from databay.cli.common import call_once, common_options, result_envelope
|
|
11
|
+
from databay.models.metrics import Grain
|
|
12
|
+
|
|
13
|
+
#: Who ran the query, for the server's per-caller count: ``--caller``, ``cli`` unless an agent passes ``agent``.
|
|
14
|
+
CALLER_HEADER = "X-Databay-Caller"
|
|
15
|
+
CALLERS = ("cli", "agent")
|
|
16
|
+
#: ``--where`` operators; ``!=``, ``>=`` and ``<=`` come before ``=``, ``>`` and ``<`` so the regex reads them whole.
|
|
17
|
+
WHERE_OPS = {"!=": "notEquals", ">=": "gte", "<=": "lte", "=": "equals", ">": "gt", "<": "lt"}
|
|
18
|
+
_WHERE = re.compile(
|
|
19
|
+
r"\s*([a-z][a-z0-9_]*)\s*(" + "|".join(map(re.escape, WHERE_OPS)) + r")\s*([^=<>\s].*?)\s*", re.DOTALL
|
|
20
|
+
)
|
|
21
|
+
_ORDER = re.compile(r"([a-z][a-z0-9_]*)(?::(asc|desc))?")
|
|
22
|
+
_PROJECT_HELP = "the project to act in (default: the active project)"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def parse_order(text: str) -> dict[str, str]:
|
|
26
|
+
"""One ``--order``: ``member`` or ``member:asc|desc``."""
|
|
27
|
+
from databay.errors import DatabayUsageError # noqa: PLC0415
|
|
28
|
+
|
|
29
|
+
match = _ORDER.fullmatch(text.strip())
|
|
30
|
+
if match is None:
|
|
31
|
+
raise DatabayUsageError(f"--order {text!r}: write <member> or <member>:desc (net_sales:desc)")
|
|
32
|
+
return {"member": match[1], "direction": match[2] or "asc"}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_where(text: str) -> dict[str, Any]:
|
|
36
|
+
"""
|
|
37
|
+
One ``--where`` as a filter: ``member<op>value`` with ``=``, ``!=``, ``>``, ``>=``, ``<`` or ``<=``.
|
|
38
|
+
|
|
39
|
+
The value is the rest, without the spaces around it; commas and inner spaces stay. A value starting
|
|
40
|
+
with ``=``, ``<`` or ``>`` (``a==b``, ``a=>b``) is refused as a mistyped operator.
|
|
41
|
+
"""
|
|
42
|
+
from databay.errors import DatabayUsageError # noqa: PLC0415
|
|
43
|
+
|
|
44
|
+
match = _WHERE.fullmatch(text)
|
|
45
|
+
if match is None:
|
|
46
|
+
raise DatabayUsageError(
|
|
47
|
+
f"--where {text!r}: write <dimension><op><value>, op one of {', '.join(WHERE_OPS)} (channel=paid)"
|
|
48
|
+
)
|
|
49
|
+
member, symbol, value = match.groups()
|
|
50
|
+
return {"member": member, "op": WHERE_OPS[symbol], "values": [value]}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
DESCRIBE_HELP = """Describe one metric: its dimensions, time grains, filter operators, warehouses and state.
|
|
54
|
+
|
|
55
|
+
\b
|
|
56
|
+
Example:
|
|
57
|
+
databay metrics describe net_sales"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@click.command("describe", help=DESCRIBE_HELP, short_help="Describe one metric you can query")
|
|
61
|
+
@click.argument("metric", metavar="METRIC")
|
|
62
|
+
@click.option("--project", default=None, help=_PROJECT_HELP)
|
|
63
|
+
@common_options
|
|
64
|
+
@click.pass_context
|
|
65
|
+
def describe(ctx: click.Context, metric: str, project: str | None, **values: Any) -> None: # noqa: ANN401
|
|
66
|
+
from databay.cli.output import emit # noqa: PLC0415
|
|
67
|
+
from databay.invoke import CallValues # noqa: PLC0415
|
|
68
|
+
from databay.operations.metrics import METRICS_DESCRIBE # noqa: PLC0415
|
|
69
|
+
|
|
70
|
+
call = CallValues(path={"metric": metric}, project_flag=project)
|
|
71
|
+
console, result = call_once(ctx, values, METRICS_DESCRIBE, call)
|
|
72
|
+
emit(console, result_envelope(result), ())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
QUERY_HELP = """Query one metric, grouped by dimensions and a time grain.
|
|
76
|
+
|
|
77
|
+
\b
|
|
78
|
+
A table or CSV prints the rows under their columns; JSON prints the whole
|
|
79
|
+
answer (columns, rows, row_count, truncated, answer_path, model_version,
|
|
80
|
+
warehouse, explain). --explain adds the metric's definition, the SQL it
|
|
81
|
+
compiled to and its lineage; --dry-run answers that without running it.
|
|
82
|
+
\b
|
|
83
|
+
--where member=value filters on a dimension; it also takes !=, >, >=, <
|
|
84
|
+
and <=, and repeats (each must hold). --order member[:desc] sorts, and
|
|
85
|
+
repeats; with --limit it answers a top N.
|
|
86
|
+
\b
|
|
87
|
+
--caller agent marks a query an AI agent ran, so the server counts it
|
|
88
|
+
apart from a person's; without it the query counts as cli.
|
|
89
|
+
\b
|
|
90
|
+
Examples:
|
|
91
|
+
databay metrics query net_sales --grain day --last 30d
|
|
92
|
+
databay metrics query channel_sessions --by channel --grain week --last 90d --where channel=Direct
|
|
93
|
+
databay metrics query net_sales --grain day --last 30d --explain"""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@click.command("query", help=QUERY_HELP, short_help="Query one metric by dimensions and time grain")
|
|
97
|
+
@click.argument("metric", metavar="METRIC")
|
|
98
|
+
@click.option("--by", "by", multiple=True, metavar="DIMENSION", help="group by this dimension; repeat for more")
|
|
99
|
+
@click.option(
|
|
100
|
+
"--grain",
|
|
101
|
+
type=click.Choice([g.value for g in Grain]),
|
|
102
|
+
default=None,
|
|
103
|
+
help="group by the metric's time at this grain",
|
|
104
|
+
)
|
|
105
|
+
@click.option("--last", default=None, metavar="ND", help="the N UTC days that end today, such as 30d")
|
|
106
|
+
@click.option("--from", "start", default=None, metavar="YYYY-MM-DD", help="first day (with --to)")
|
|
107
|
+
@click.option("--to", "end", default=None, metavar="YYYY-MM-DD", help="last day, inclusive (with --from)")
|
|
108
|
+
@click.option("--where", "where", multiple=True, metavar="FILTER", help="member=value (or != > >= < <=); repeat")
|
|
109
|
+
@click.option("--order", "order", multiple=True, metavar="MEMBER[:desc]", help="sort by this member; repeat")
|
|
110
|
+
@click.option("--limit", type=click.IntRange(min=1), default=None, help="most rows to answer")
|
|
111
|
+
@click.option("--warehouse", default=None, help="the warehouse, when the metric is in several")
|
|
112
|
+
@click.option("--model-version", "model_version", type=click.IntRange(min=1), default=None, help="pin a model version")
|
|
113
|
+
@click.option("--explain", is_flag=True, default=False, help="add the definition, compiled SQL and lineage")
|
|
114
|
+
@click.option("--dry-run", "dry_run", is_flag=True, default=False, help="answer the explain without running it")
|
|
115
|
+
@click.option("--caller", type=click.Choice(CALLERS), default="cli", help="who runs the query: cli or agent")
|
|
116
|
+
@click.option("--project", default=None, help=_PROJECT_HELP)
|
|
117
|
+
@common_options
|
|
118
|
+
@click.pass_context
|
|
119
|
+
def query(
|
|
120
|
+
ctx: click.Context,
|
|
121
|
+
metric: str,
|
|
122
|
+
by: tuple[str, ...],
|
|
123
|
+
grain: str | None,
|
|
124
|
+
last: str | None,
|
|
125
|
+
start: str | None,
|
|
126
|
+
end: str | None,
|
|
127
|
+
where: tuple[str, ...],
|
|
128
|
+
order: tuple[str, ...],
|
|
129
|
+
limit: int | None,
|
|
130
|
+
warehouse: str | None,
|
|
131
|
+
model_version: int | None,
|
|
132
|
+
explain: bool,
|
|
133
|
+
dry_run: bool,
|
|
134
|
+
caller: str,
|
|
135
|
+
project: str | None,
|
|
136
|
+
**values: Any, # noqa: ANN401
|
|
137
|
+
) -> None:
|
|
138
|
+
from databay.cli.output import emit_grid # noqa: PLC0415
|
|
139
|
+
from databay.invoke import CallValues # noqa: PLC0415
|
|
140
|
+
from databay.operations.metrics import METRICS_QUERY # noqa: PLC0415
|
|
141
|
+
|
|
142
|
+
filters = [parse_where(text) for text in where]
|
|
143
|
+
ordering = [parse_order(text) for text in order]
|
|
144
|
+
time = {k: v for k, v in {"grain": grain, "last": last, "from": start, "to": end}.items() if v is not None}
|
|
145
|
+
body: dict[str, Any] = {"metric": metric, "explain": explain, "dry_run": dry_run}
|
|
146
|
+
optional = {
|
|
147
|
+
"dimensions": list(by) or None,
|
|
148
|
+
"time": time or None,
|
|
149
|
+
"filters": filters or None,
|
|
150
|
+
"order": ordering or None,
|
|
151
|
+
"limit": limit,
|
|
152
|
+
"warehouse": warehouse,
|
|
153
|
+
"model_version": model_version,
|
|
154
|
+
}
|
|
155
|
+
body.update({k: v for k, v in optional.items() if v is not None})
|
|
156
|
+
call = CallValues(body=body, has_body=True, headers={CALLER_HEADER: caller}, project_flag=project)
|
|
157
|
+
console, result = call_once(ctx, values, METRICS_QUERY, call)
|
|
158
|
+
answer = result.parsed
|
|
159
|
+
if answer.truncated:
|
|
160
|
+
console.notice(
|
|
161
|
+
"truncated",
|
|
162
|
+
f"the server returned the first {answer.row_count} rows",
|
|
163
|
+
only_on_terminal=console.output == "json",
|
|
164
|
+
)
|
|
165
|
+
emit_grid(console, result_envelope(result), [c.name for c in answer.columns], [list(row) for row in answer.rows])
|
|
166
|
+
if answer.explain is None or console.output == "json":
|
|
167
|
+
return
|
|
168
|
+
if console.output == "table":
|
|
169
|
+
console.stdout.write(f"sql:\n{answer.explain.sql.text}\n")
|
|
170
|
+
else:
|
|
171
|
+
console.notice("explain", "CSV holds only the rows; pass --output json for the explain", only_on_terminal=False)
|
databay/cli/org.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
``databay org switch``, and the organization ``databay login`` makes active.
|
|
3
|
+
|
|
4
|
+
The server stores the active organization and every token names that one. Switching is a write
|
|
5
|
+
(``PUT /api/auth/me/active-org``) followed by a token renewal; the new token is checked to name the
|
|
6
|
+
organization before the command reports it. An organization is named by its key or its display name.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from databay.cli.common import common_options, console_for
|
|
17
|
+
from databay.cli.handwritten import HANDWRITTEN
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from databay.cli.console import Console
|
|
21
|
+
from databay.invoke import Session
|
|
22
|
+
from databay.models.org import MyOrganizationOut, MyOrganizationsResponse
|
|
23
|
+
from databay.profiles import Profile
|
|
24
|
+
|
|
25
|
+
SWITCH_HELP = """Make ORGANIZATION the active organization and refresh the token.
|
|
26
|
+
|
|
27
|
+
\b
|
|
28
|
+
ORGANIZATION is a key (org-...) or a display name; `databay org list` shows
|
|
29
|
+
both. Every later command acts in that organization. The profile's active
|
|
30
|
+
project belongs to the previous organization, so it is cleared.
|
|
31
|
+
|
|
32
|
+
\b
|
|
33
|
+
Example:
|
|
34
|
+
databay org switch <organization>"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def list_orgs(session: Session) -> MyOrganizationsResponse:
|
|
38
|
+
from databay.invoke import CallValues # noqa: PLC0415
|
|
39
|
+
from databay.operations.org import ORG_LIST # noqa: PLC0415
|
|
40
|
+
|
|
41
|
+
return session.call(ORG_LIST, CallValues()).parsed
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def resolve_org(listing: MyOrganizationsResponse, wanted: str, server: str) -> MyOrganizationOut:
|
|
45
|
+
"""The organization ``wanted`` names: a key, else exactly one display name, compared without case."""
|
|
46
|
+
from databay.errors import DatabayUsageError, api_error # noqa: PLC0415
|
|
47
|
+
|
|
48
|
+
keys = [org.organization for org in listing.organizations]
|
|
49
|
+
by_key = [org for org in listing.organizations if org.organization == wanted]
|
|
50
|
+
if by_key:
|
|
51
|
+
return _usable(by_key[0])
|
|
52
|
+
by_name = [org for org in listing.organizations if org.display_name.casefold() == wanted.casefold()]
|
|
53
|
+
if len(by_name) > 1:
|
|
54
|
+
raise DatabayUsageError(
|
|
55
|
+
f"{len(by_name)} of your organizations are named {wanted!r}; pass the key instead",
|
|
56
|
+
details={"organizations": [org.organization for org in by_name]},
|
|
57
|
+
)
|
|
58
|
+
if not by_name:
|
|
59
|
+
raise api_error(
|
|
60
|
+
"org_not_member",
|
|
61
|
+
f"you belong to no organization {wanted!r} on {server}",
|
|
62
|
+
status=403,
|
|
63
|
+
details={"organization": wanted, "organizations": keys},
|
|
64
|
+
)
|
|
65
|
+
return _usable(by_name[0])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _usable(org: MyOrganizationOut) -> MyOrganizationOut:
|
|
69
|
+
"""``org``, unless it is disabled: no token can name a disabled organization."""
|
|
70
|
+
from databay.errors import api_error # noqa: PLC0415
|
|
71
|
+
|
|
72
|
+
if not org.enabled:
|
|
73
|
+
raise api_error(
|
|
74
|
+
"org_disabled",
|
|
75
|
+
f"{org.display_name} ({org.organization}) is disabled",
|
|
76
|
+
status=403,
|
|
77
|
+
details={"organization": org.organization},
|
|
78
|
+
)
|
|
79
|
+
return org
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def switch_to(
|
|
83
|
+
session: Session, profile: Profile, org: MyOrganizationOut, console: Console, state: dict[str, Any]
|
|
84
|
+
) -> dict[str, Any]:
|
|
85
|
+
"""
|
|
86
|
+
Store ``org`` as active, renew the profile's token and check it names ``org``.
|
|
87
|
+
|
|
88
|
+
A profile whose token comes from ``$DATABAY_TOKEN`` cannot renew it: the switch is stored and
|
|
89
|
+
``token_refreshed`` is false. The active project is cleared when the token named another organization.
|
|
90
|
+
"""
|
|
91
|
+
from databay.errors import DatabayError # noqa: PLC0415
|
|
92
|
+
from databay.invoke import CallValues # noqa: PLC0415
|
|
93
|
+
from databay.operations.org import ORG_SWITCH # noqa: PLC0415
|
|
94
|
+
from databay.profiles import edit_config # noqa: PLC0415
|
|
95
|
+
from databay.settings import ENV_TOKEN # noqa: PLC0415
|
|
96
|
+
from databay.stored_token import StoredToken # noqa: PLC0415
|
|
97
|
+
|
|
98
|
+
before = _token_org(session)
|
|
99
|
+
state["sent"] = True
|
|
100
|
+
session.call(ORG_SWITCH, CallValues(body={"organization": org.organization}, has_body=True))
|
|
101
|
+
tokens = session.transport.tokens
|
|
102
|
+
refreshed = isinstance(tokens, StoredToken)
|
|
103
|
+
if refreshed:
|
|
104
|
+
tokens.renewed()
|
|
105
|
+
now = _token_org(session)
|
|
106
|
+
if now != org.organization:
|
|
107
|
+
raise DatabayError(
|
|
108
|
+
f"switched to {org.organization}, but the renewed token names {now or 'no organization'}",
|
|
109
|
+
code="org_not_in_token",
|
|
110
|
+
details={"organization": org.organization, "token_organization": now},
|
|
111
|
+
next_command="databay status",
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
console.inform(
|
|
115
|
+
"token_not_refreshed",
|
|
116
|
+
{"organization": org.organization, "auth": profile.auth},
|
|
117
|
+
f"{org.organization} is now your active organization, but ${ENV_TOKEN} still holds a token "
|
|
118
|
+
"for the previous one: get a new token before the next command.",
|
|
119
|
+
)
|
|
120
|
+
cleared = None
|
|
121
|
+
if before != org.organization and profile.active_project is not None:
|
|
122
|
+
cleared = profile.active_project
|
|
123
|
+
|
|
124
|
+
def change(data: dict[str, Any]) -> None:
|
|
125
|
+
data["profiles"][profile.name].pop("active_project", None)
|
|
126
|
+
|
|
127
|
+
edit_config(change)
|
|
128
|
+
return {
|
|
129
|
+
"profile": profile.name,
|
|
130
|
+
"organization": org.organization,
|
|
131
|
+
"display_name": org.display_name,
|
|
132
|
+
"role": None if org.role is None else str(org.role),
|
|
133
|
+
"token_refreshed": refreshed,
|
|
134
|
+
"cleared_active_project": cleared,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _token_org(session: Session) -> str | None:
|
|
139
|
+
"""The organization the token now names, as the server reads it."""
|
|
140
|
+
from databay.invoke import CallValues # noqa: PLC0415
|
|
141
|
+
from databay.operations.me import ME_GET # noqa: PLC0415
|
|
142
|
+
|
|
143
|
+
me = session.call(ME_GET, CallValues()).parsed
|
|
144
|
+
return me.organization[0] if me.organization else None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def choose_interactively(listing: MyOrganizationsResponse, console: Console) -> MyOrganizationOut:
|
|
148
|
+
"""Ask on the terminal which organization to make active."""
|
|
149
|
+
usable = [org for org in listing.organizations if org.enabled]
|
|
150
|
+
lines = ["You belong to several organizations. Which one should be active?"]
|
|
151
|
+
for number, org in enumerate(usable, start=1):
|
|
152
|
+
role = f", {org.role}" if org.role is not None else ""
|
|
153
|
+
lines.append(f" {number}) {org.display_name} ({org.organization}{role})")
|
|
154
|
+
console.inform("org_choice", {}, "\n".join(lines))
|
|
155
|
+
picked = click.prompt("Organization", type=click.IntRange(1, len(usable)), err=True)
|
|
156
|
+
return usable[picked - 1]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def report_no_usable_org(listing: MyOrganizationsResponse, server: str, console: Console) -> None:
|
|
160
|
+
"""Say that no token can name an organization: every one is disabled, or there is none."""
|
|
161
|
+
console.inform(
|
|
162
|
+
"no_usable_org",
|
|
163
|
+
{"organizations": [org.organization for org in listing.organizations]},
|
|
164
|
+
f"You are signed in, but none of your organizations on {server} can be used "
|
|
165
|
+
f"({'every one is disabled' if listing.organizations else 'you belong to none'}). "
|
|
166
|
+
"Create one in the web app, or ask an organization admin to add you.",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def interactive(console: Console) -> bool:
|
|
171
|
+
return sys.stdin.isatty() and console.stderr_tty
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@click.command("switch", help=SWITCH_HELP, short_help=HANDWRITTEN["org switch"][2])
|
|
175
|
+
@click.argument("organization")
|
|
176
|
+
@common_options
|
|
177
|
+
@click.pass_context
|
|
178
|
+
def org_switch(ctx: click.Context, organization: str, **values: Any) -> None: # noqa: ANN401
|
|
179
|
+
from databay.cli.output import emit # noqa: PLC0415
|
|
180
|
+
from databay.cli.runner import open_session # noqa: PLC0415
|
|
181
|
+
from databay.errors import DatabayUsageError # noqa: PLC0415
|
|
182
|
+
|
|
183
|
+
console, state = console_for(ctx, values)
|
|
184
|
+
session, chosen = open_session(values, console, state)
|
|
185
|
+
state["context"] = {"server": chosen.server, "profile": chosen.name}
|
|
186
|
+
if chosen.auth == "api_key":
|
|
187
|
+
raise DatabayUsageError(f"profile {chosen.name} signs in as an org API key, which belongs to one organization")
|
|
188
|
+
try:
|
|
189
|
+
org = resolve_org(list_orgs(session), organization, chosen.server)
|
|
190
|
+
result = switch_to(session, chosen, org, console, state)
|
|
191
|
+
finally:
|
|
192
|
+
session.transport.close()
|
|
193
|
+
emit(console, {"result": result, "next": "databay status"}, ())
|
databay/cli/output.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Printing a command's result.
|
|
3
|
+
|
|
4
|
+
JSON is one envelope: ``{"result": <the server's answer>}``, plus ``project`` when the command
|
|
5
|
+
acted in one, ``wait`` after ``--wait``, ``idempotency_key`` on a create and ``next`` when there is
|
|
6
|
+
an obvious next command. A table or CSV prints the answer alone: a list of objects as rows (the
|
|
7
|
+
operation's declared columns, else every key), an object as one row per field.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import csv
|
|
13
|
+
import json
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
from databay.errors import DatabayResponseInvalid
|
|
17
|
+
from databay.invoke import pointer
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from databay.cli.console import Console
|
|
21
|
+
|
|
22
|
+
MAX_CELL = 80
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _text(value: Any) -> str: # noqa: ANN401
|
|
26
|
+
if value is None:
|
|
27
|
+
return ""
|
|
28
|
+
return value if isinstance(value, str) else json.dumps(value, default=str)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _cell(value: Any) -> str: # noqa: ANN401
|
|
32
|
+
"""A table cell, cut to ``MAX_CELL`` characters; CSV keeps the whole value."""
|
|
33
|
+
text = _text(value)
|
|
34
|
+
return text if len(text) <= MAX_CELL else text[: MAX_CELL - 1] + "…"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _rows(result: Any, columns: tuple[str, ...], cell: Any = _cell) -> tuple[list[str], list[list[str]]]: # noqa: ANN401
|
|
38
|
+
if isinstance(result, list) and all(isinstance(item, dict) for item in result):
|
|
39
|
+
header = list(columns)
|
|
40
|
+
if not header:
|
|
41
|
+
for item in result:
|
|
42
|
+
header += [key for key in item if key not in header]
|
|
43
|
+
return header, [[cell(item.get(key)) for key in header] for item in result]
|
|
44
|
+
if isinstance(result, dict):
|
|
45
|
+
return ["field", "value"], [[key, cell(value)] for key, value in result.items()]
|
|
46
|
+
if isinstance(result, list):
|
|
47
|
+
return ["value"], [[cell(item)] for item in result]
|
|
48
|
+
return ["value"], [[cell(result)]]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _csv_rows(result: Any, columns: tuple[str, ...]) -> tuple[list[str], list[list[str]]]: # noqa: ANN401
|
|
52
|
+
if isinstance(result, dict):
|
|
53
|
+
header = list(columns) or list(result)
|
|
54
|
+
return header, [[_text(result.get(key)) for key in header]]
|
|
55
|
+
return _rows(result, columns, _text)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def emit(console: Console, envelope: dict[str, Any], columns: tuple[str, ...], rows: str | None = None) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Print ``envelope`` in the console's format.
|
|
61
|
+
|
|
62
|
+
``rows`` is the JSON pointer of the list a table or CSV shows when the answer is an object
|
|
63
|
+
holding it (``/keys``); a table or CSV reports ``idempotency_key`` and ``next`` on stderr.
|
|
64
|
+
"""
|
|
65
|
+
_write(console, envelope, columns, rows)
|
|
66
|
+
for name in ("idempotency_key", "next"):
|
|
67
|
+
if envelope.get(name):
|
|
68
|
+
console.notice(name, envelope[name], only_on_terminal=console.output == "json")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def emit_grid(console: Console, envelope: dict[str, Any], header: list[str], rows: list[list[Any]]) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Print an answer that carries its own grid of ``header`` and ``rows`` (``sql``).
|
|
74
|
+
|
|
75
|
+
JSON prints ``envelope``; a table or CSV prints the grid as it is, so repeated column names stay apart.
|
|
76
|
+
"""
|
|
77
|
+
out = console.stdout
|
|
78
|
+
if console.output == "json":
|
|
79
|
+
out.write(json.dumps(envelope, indent=2 if out.isatty() else None, default=str) + "\n")
|
|
80
|
+
return
|
|
81
|
+
if console.output == "csv":
|
|
82
|
+
writer = csv.writer(out)
|
|
83
|
+
writer.writerow(header)
|
|
84
|
+
writer.writerows([[_text(value) for value in row] for row in rows])
|
|
85
|
+
return
|
|
86
|
+
if envelope.get("project"):
|
|
87
|
+
out.write(f"project: {envelope['project']}\n")
|
|
88
|
+
from rich.console import Console as RichConsole # noqa: PLC0415 — tables only
|
|
89
|
+
from rich.table import Table # noqa: PLC0415
|
|
90
|
+
|
|
91
|
+
table = Table(*header, show_lines=False)
|
|
92
|
+
for row in rows:
|
|
93
|
+
table.add_row(*[_cell(value) for value in row])
|
|
94
|
+
RichConsole(file=out, no_color=not console.color, soft_wrap=False, markup=False, highlight=False).print(table)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _write(console: Console, envelope: dict[str, Any], columns: tuple[str, ...], rows: str | None) -> None:
|
|
98
|
+
out = console.stdout
|
|
99
|
+
result = envelope["result"]
|
|
100
|
+
if console.output == "json":
|
|
101
|
+
indent = 2 if out.isatty() else None
|
|
102
|
+
out.write(json.dumps(envelope, indent=indent, default=str) + "\n")
|
|
103
|
+
return
|
|
104
|
+
if isinstance(result, str):
|
|
105
|
+
out.write(result if result.endswith("\n") else result + "\n")
|
|
106
|
+
return
|
|
107
|
+
if rows is not None and isinstance(result, dict):
|
|
108
|
+
try:
|
|
109
|
+
result = pointer(result, rows)
|
|
110
|
+
except (KeyError, IndexError, ValueError):
|
|
111
|
+
raise DatabayResponseInvalid(f"the answer has no {rows}, the list its columns describe") from None
|
|
112
|
+
if console.output == "csv":
|
|
113
|
+
header, cells = _csv_rows(result, columns)
|
|
114
|
+
writer = csv.writer(out)
|
|
115
|
+
writer.writerow(header)
|
|
116
|
+
writer.writerows(cells)
|
|
117
|
+
return
|
|
118
|
+
if envelope.get("project"):
|
|
119
|
+
out.write(f"project: {envelope['project']}\n")
|
|
120
|
+
if "wait" in envelope:
|
|
121
|
+
wait = envelope["wait"]
|
|
122
|
+
reason = f"{wait['reason']}, " if wait.get("reason") else ""
|
|
123
|
+
out.write(f"wait: {wait['outcome']} ({reason}{wait['state']}, {wait['elapsed_s']}s)\n")
|
|
124
|
+
if result is None:
|
|
125
|
+
out.write("done\n")
|
|
126
|
+
return
|
|
127
|
+
from rich.console import Console as RichConsole # noqa: PLC0415 — tables only
|
|
128
|
+
from rich.table import Table # noqa: PLC0415
|
|
129
|
+
|
|
130
|
+
header, cells = _rows(result, columns)
|
|
131
|
+
table = Table(*header, show_lines=False)
|
|
132
|
+
for row in cells:
|
|
133
|
+
table.add_row(*row)
|
|
134
|
+
RichConsole(file=out, no_color=not console.color, soft_wrap=False, markup=False, highlight=False).print(table)
|