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/compat.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Whether this client may call an operation on a server, from the server's ``/api/auth/client-config``.
|
|
3
|
+
|
|
4
|
+
The answer is cached per server in the state dir for ``client_config_ttl_s``. A server running
|
|
5
|
+
another product, another API major, or an older minor than the operation's ``since`` is refused
|
|
6
|
+
before the operation is sent.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
from databay.errors import DatabayResponseInvalid, DatabayVersionError
|
|
19
|
+
from databay.paths import state_dir
|
|
20
|
+
from databay.spec import API_VERSION
|
|
21
|
+
from databay.transport import Request, error_from
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from databay.operation import Operation
|
|
27
|
+
from databay.transport import Transport
|
|
28
|
+
|
|
29
|
+
CLIENT_CONFIG_PATH = "/api/auth/client-config"
|
|
30
|
+
SUPPORTED_PRODUCT = "core"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class ServerInfo:
|
|
35
|
+
product: str
|
|
36
|
+
api_version: str
|
|
37
|
+
issuer: str
|
|
38
|
+
client_id: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _version(value: str) -> tuple[int, int]:
|
|
42
|
+
major, _, minor = value.partition(".")
|
|
43
|
+
return int(major), int(minor)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse(body: Any) -> ServerInfo: # noqa: ANN401 — any JSON value
|
|
47
|
+
fields = ("product", "api_version", "issuer", "client_id")
|
|
48
|
+
if not isinstance(body, dict) or not all(isinstance(body.get(f), str) for f in fields):
|
|
49
|
+
missing = [f for f in fields if not isinstance(body, dict) or not isinstance(body.get(f), str)]
|
|
50
|
+
raise DatabayResponseInvalid(f"GET {CLIENT_CONFIG_PATH}: the answer has no string {missing}")
|
|
51
|
+
try:
|
|
52
|
+
_version(body["api_version"])
|
|
53
|
+
except ValueError as exc:
|
|
54
|
+
raise DatabayResponseInvalid(f"GET {CLIENT_CONFIG_PATH}: api_version is not major.minor") from exc
|
|
55
|
+
return ServerInfo(**{f: body[f] for f in fields})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _cache_file(server: str) -> Path:
|
|
59
|
+
return state_dir() / "client-config" / f"{hashlib.sha256(server.encode()).hexdigest()[:24]}.json"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _cached(transport: Transport) -> ServerInfo | None:
|
|
63
|
+
path = _cache_file(transport.server)
|
|
64
|
+
if not path.exists():
|
|
65
|
+
return None
|
|
66
|
+
try:
|
|
67
|
+
entry = json.loads(path.read_text(encoding="utf-8"))
|
|
68
|
+
if time.time() - float(entry["fetched_at"]) > transport.settings.client_config_ttl_s:
|
|
69
|
+
return None
|
|
70
|
+
return _parse(entry["body"])
|
|
71
|
+
except (ValueError, KeyError, TypeError, DatabayResponseInvalid) as exc:
|
|
72
|
+
transport.events("cache_invalid", {"file": str(path), "error": type(exc).__name__})
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _store(transport: Transport, body: Any) -> None: # noqa: ANN401
|
|
77
|
+
path = _cache_file(transport.server)
|
|
78
|
+
try:
|
|
79
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
tmp = path.with_suffix(f".{os.getpid()}.tmp")
|
|
81
|
+
tmp.write_text(json.dumps({"fetched_at": time.time(), "body": body}), encoding="utf-8")
|
|
82
|
+
tmp.replace(path)
|
|
83
|
+
except OSError as exc:
|
|
84
|
+
# The cache only saves a request; a read-only state dir still lets the command run.
|
|
85
|
+
transport.events("cache_write_failed", {"file": str(path), "error": type(exc).__name__})
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def server_info(transport: Transport, *, fresh: bool = False) -> tuple[ServerInfo, bool]:
|
|
89
|
+
"""The server's client-config and whether it came from the cache; ``fresh`` reads the server."""
|
|
90
|
+
cached = None if fresh else _cached(transport)
|
|
91
|
+
if cached is not None:
|
|
92
|
+
return cached, True
|
|
93
|
+
request = Request("GET", CLIENT_CONFIG_PATH)
|
|
94
|
+
answer = transport.exchange(request)
|
|
95
|
+
if answer.status == 404 and answer.is_json: # noqa: PLR2004
|
|
96
|
+
# A Databay server without client-config predates the API; a non-JSON 404 is not Databay at all.
|
|
97
|
+
raise DatabayVersionError(
|
|
98
|
+
f"the server has no {CLIENT_CONFIG_PATH}: it predates API {API_VERSION}; upgrade the server",
|
|
99
|
+
code="server_too_old",
|
|
100
|
+
details={"client_api_version": API_VERSION},
|
|
101
|
+
)
|
|
102
|
+
if answer.status >= 400: # noqa: PLR2004
|
|
103
|
+
raise error_from(answer, request)
|
|
104
|
+
try:
|
|
105
|
+
body = answer.json()
|
|
106
|
+
except ValueError as exc:
|
|
107
|
+
raise DatabayResponseInvalid(f"GET {CLIENT_CONFIG_PATH}: the answer is not JSON") from exc
|
|
108
|
+
info = _parse(body)
|
|
109
|
+
_store(transport, body)
|
|
110
|
+
return info, False
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def ensure_compatible(
|
|
114
|
+
transport: Transport, known: tuple[ServerInfo, bool] | None, op: Operation
|
|
115
|
+
) -> tuple[ServerInfo, bool]:
|
|
116
|
+
"""
|
|
117
|
+
Check ``op`` against the server; ``known`` is an earlier ``(info, from_cache)``, else client-config is read.
|
|
118
|
+
|
|
119
|
+
A refusal based on a cached answer reads the server once more first: it may have been upgraded.
|
|
120
|
+
Returns the ``(info, from_cache)`` to keep for the next operation.
|
|
121
|
+
"""
|
|
122
|
+
info, from_cache = known if known is not None else server_info(transport)
|
|
123
|
+
try:
|
|
124
|
+
check(info, op)
|
|
125
|
+
except DatabayVersionError:
|
|
126
|
+
if not from_cache:
|
|
127
|
+
raise
|
|
128
|
+
info, from_cache = server_info(transport, fresh=True)
|
|
129
|
+
check(info, op)
|
|
130
|
+
return info, from_cache
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def check(info: ServerInfo, op: Operation) -> None:
|
|
134
|
+
"""Refuse a server this client cannot call ``op`` on: product, API major, then the operation's ``since``."""
|
|
135
|
+
check_server(info)
|
|
136
|
+
server_major, server_minor = _version(info.api_version)
|
|
137
|
+
if _version(op.since) > (server_major, server_minor):
|
|
138
|
+
raise DatabayVersionError(
|
|
139
|
+
f"`{op.command}` needs server API {op.since}; the server is on {info.api_version}",
|
|
140
|
+
code="server_too_old",
|
|
141
|
+
details={"server_api_version": info.api_version, "client_api_version": API_VERSION, "since": op.since},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def check_server(info: ServerInfo) -> None:
|
|
146
|
+
"""Refuse a server this client cannot talk to at all: another product, or another API major."""
|
|
147
|
+
if info.product != SUPPORTED_PRODUCT:
|
|
148
|
+
raise DatabayVersionError(
|
|
149
|
+
f"this server runs the {info.product!r} product; the CLI supports {SUPPORTED_PRODUCT!r} only",
|
|
150
|
+
code="unsupported_product",
|
|
151
|
+
details={"product": info.product},
|
|
152
|
+
)
|
|
153
|
+
server_major, _ = _version(info.api_version)
|
|
154
|
+
client_major, _ = _version(API_VERSION)
|
|
155
|
+
versions = {"server_api_version": info.api_version, "client_api_version": API_VERSION}
|
|
156
|
+
if server_major != client_major:
|
|
157
|
+
side = "the CLI" if server_major > client_major else "the server"
|
|
158
|
+
raise DatabayVersionError(
|
|
159
|
+
f"the server speaks API {info.api_version} and this CLI speaks {API_VERSION}; upgrade {side}",
|
|
160
|
+
details=versions,
|
|
161
|
+
)
|
databay/credentials.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stored credentials: what ``databay login`` keeps for one server and one user, and where.
|
|
3
|
+
|
|
4
|
+
A credential is keyed ``databay-cli:<server>:<sub>``; profiles name it in ``credential``. It
|
|
5
|
+
holds the access token with its expiry and either a refresh token (a person's login) or an API
|
|
6
|
+
key's client secret. It lives in one of two stores, which the profile names in ``token_store``:
|
|
7
|
+
|
|
8
|
+
- ``keyring``: the OS keyring, the default. No usable keyring is ``keyring_unavailable``.
|
|
9
|
+
- ``file``: ``<config dir>/credentials/<sha256 of the key>.json``, mode 0600, chosen with
|
|
10
|
+
``--token-store file``. A file anyone else may read is ``token_file_insecure``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import stat
|
|
19
|
+
from dataclasses import asdict, dataclass, fields, replace
|
|
20
|
+
from typing import TYPE_CHECKING, Literal, Protocol
|
|
21
|
+
|
|
22
|
+
from databay.errors import DatabayProfileError
|
|
23
|
+
from databay.paths import config_dir
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
CredentialKind = Literal["login", "api_key"]
|
|
29
|
+
TokenStoreKind = Literal["keyring", "file"]
|
|
30
|
+
TOKEN_STORES: tuple[TokenStoreKind, ...] = ("keyring", "file")
|
|
31
|
+
KEY_PREFIX = "databay-cli"
|
|
32
|
+
KEYRING_SERVICE = "databay-cli"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def credential_key(server: str, sub: str) -> str:
|
|
36
|
+
return f"{KEY_PREFIX}:{server}:{sub}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def key_sub(key: str) -> str:
|
|
40
|
+
"""The ``sub`` a credential key names: what follows its last ``:`` (a URL holds colons, a sub none)."""
|
|
41
|
+
return key.rpartition(":")[2]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
#: Fields a credential saved by an earlier CLI also holds. They are dropped on read: the endpoints come
|
|
45
|
+
#: from the issuer's discovery document when they are needed, so a server that moves one never breaks
|
|
46
|
+
#: a stored login.
|
|
47
|
+
PINNED_ENDPOINT_FIELDS = frozenset({"token_endpoint", "revocation_endpoint"})
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True, slots=True)
|
|
51
|
+
class Credential:
|
|
52
|
+
kind: CredentialKind
|
|
53
|
+
server: str
|
|
54
|
+
sub: str
|
|
55
|
+
#: Where renewal and logout discover Keycloak's endpoints.
|
|
56
|
+
issuer: str
|
|
57
|
+
client_id: str
|
|
58
|
+
access_token: str
|
|
59
|
+
#: Unix time the access token expires at.
|
|
60
|
+
expires_at: float
|
|
61
|
+
#: A person's login only.
|
|
62
|
+
refresh_token: str | None = None
|
|
63
|
+
#: An API key only: the key's client secret.
|
|
64
|
+
client_secret: str | None = None
|
|
65
|
+
|
|
66
|
+
def with_tokens(self, access_token: str, expires_at: float, refresh_token: str | None) -> Credential:
|
|
67
|
+
"""A copy holding newly issued tokens; a refresh answer without a refresh token keeps the current one."""
|
|
68
|
+
return replace(
|
|
69
|
+
self,
|
|
70
|
+
access_token=access_token,
|
|
71
|
+
expires_at=expires_at,
|
|
72
|
+
refresh_token=refresh_token if refresh_token is not None else self.refresh_token,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def to_json(self) -> str:
|
|
76
|
+
return json.dumps(asdict(self))
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_json(cls, text: str, where: str) -> Credential:
|
|
80
|
+
names = {f.name for f in fields(cls)}
|
|
81
|
+
try:
|
|
82
|
+
data = json.loads(text)
|
|
83
|
+
except ValueError as exc:
|
|
84
|
+
raise _unreadable(where, "not JSON") from exc
|
|
85
|
+
if isinstance(data, dict) and set(data) == names | PINNED_ENDPOINT_FIELDS:
|
|
86
|
+
data = {name: data[name] for name in names}
|
|
87
|
+
if not isinstance(data, dict) or set(data) != names:
|
|
88
|
+
raise _unreadable(where, f"expected the fields {sorted(names)}")
|
|
89
|
+
kind = data["kind"]
|
|
90
|
+
if kind not in ("login", "api_key"):
|
|
91
|
+
raise _unreadable(where, f"unknown kind {kind!r}")
|
|
92
|
+
renews_with = "refresh_token" if kind == "login" else "client_secret"
|
|
93
|
+
if not isinstance(data[renews_with], str) or not data[renews_with]:
|
|
94
|
+
raise _unreadable(where, f"a {kind} credential without its {renews_with}")
|
|
95
|
+
return cls(**data)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _unreadable(where: str, problem: str) -> DatabayProfileError:
|
|
99
|
+
return DatabayProfileError(
|
|
100
|
+
f"the stored credential in {where} is unreadable ({problem}); sign in again with databay login",
|
|
101
|
+
code="credential_missing",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TokenStore(Protocol):
|
|
106
|
+
kind: TokenStoreKind
|
|
107
|
+
|
|
108
|
+
def ready(self) -> None:
|
|
109
|
+
"""Raise when the store cannot be used, before anyone signs in."""
|
|
110
|
+
...
|
|
111
|
+
|
|
112
|
+
def get(self, key: str) -> Credential | None: ...
|
|
113
|
+
|
|
114
|
+
def put(self, key: str, credential: Credential) -> None: ...
|
|
115
|
+
|
|
116
|
+
def delete(self, key: str) -> None: ...
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _keyring_unavailable(reason: str) -> DatabayProfileError:
|
|
120
|
+
return DatabayProfileError(
|
|
121
|
+
f"no usable OS keyring ({reason}); pass --token-store file to keep tokens in a 0600 file instead",
|
|
122
|
+
code="keyring_unavailable",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class KeyringStore:
|
|
127
|
+
kind: TokenStoreKind = "keyring"
|
|
128
|
+
|
|
129
|
+
def _backend(self): # noqa: ANN202 — a keyring backend
|
|
130
|
+
import keyring # noqa: PLC0415 — keyring scans its backends at import
|
|
131
|
+
from keyring.backends import fail # noqa: PLC0415
|
|
132
|
+
|
|
133
|
+
backend = keyring.get_keyring()
|
|
134
|
+
if isinstance(backend, fail.Keyring):
|
|
135
|
+
raise _keyring_unavailable("no keyring backend is installed or running")
|
|
136
|
+
return backend
|
|
137
|
+
|
|
138
|
+
def _call(self, action: str, *args: str, absent_ok: bool = False) -> str | None:
|
|
139
|
+
"""
|
|
140
|
+
Call the backend; a keyring error is ``keyring_unavailable``.
|
|
141
|
+
|
|
142
|
+
``absent_ok`` lets a delete of an entry that is not there succeed: ``PasswordDeleteError``
|
|
143
|
+
is a ``KeyringError`` too.
|
|
144
|
+
"""
|
|
145
|
+
from keyring.errors import KeyringError, PasswordDeleteError # noqa: PLC0415
|
|
146
|
+
|
|
147
|
+
backend = self._backend()
|
|
148
|
+
try:
|
|
149
|
+
return getattr(backend, action)(KEYRING_SERVICE, *args)
|
|
150
|
+
except PasswordDeleteError:
|
|
151
|
+
if absent_ok:
|
|
152
|
+
return None
|
|
153
|
+
raise _keyring_unavailable(f"PasswordDeleteError on {action}") from None
|
|
154
|
+
except KeyringError as exc:
|
|
155
|
+
raise _keyring_unavailable(f"{type(exc).__name__} on {action}") from exc
|
|
156
|
+
|
|
157
|
+
def ready(self) -> None:
|
|
158
|
+
self._backend()
|
|
159
|
+
|
|
160
|
+
def get(self, key: str) -> Credential | None:
|
|
161
|
+
text = self._call("get_password", key)
|
|
162
|
+
return None if text is None else Credential.from_json(text, f"the keyring entry {key}")
|
|
163
|
+
|
|
164
|
+
def put(self, key: str, credential: Credential) -> None:
|
|
165
|
+
self._call("set_password", key, credential.to_json())
|
|
166
|
+
|
|
167
|
+
def delete(self, key: str) -> None:
|
|
168
|
+
self._call("delete_password", key, absent_ok=True)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class FileStore:
|
|
172
|
+
kind: TokenStoreKind = "file"
|
|
173
|
+
|
|
174
|
+
def __init__(self, directory: Path | None = None) -> None:
|
|
175
|
+
self.directory = directory if directory is not None else config_dir() / "credentials"
|
|
176
|
+
|
|
177
|
+
def ready(self) -> None:
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
def path(self, key: str) -> Path:
|
|
181
|
+
return self.directory / f"{hashlib.sha256(key.encode()).hexdigest()}.json"
|
|
182
|
+
|
|
183
|
+
def get(self, key: str) -> Credential | None:
|
|
184
|
+
path = self.path(key)
|
|
185
|
+
try:
|
|
186
|
+
mode = path.stat().st_mode
|
|
187
|
+
except FileNotFoundError:
|
|
188
|
+
return None
|
|
189
|
+
if stat.S_IMODE(mode) & 0o077:
|
|
190
|
+
raise DatabayProfileError(
|
|
191
|
+
f"{path} holds tokens and others may read it (mode {stat.S_IMODE(mode):o}); run chmod 600 {path}",
|
|
192
|
+
code="token_file_insecure",
|
|
193
|
+
details={"path": str(path)},
|
|
194
|
+
)
|
|
195
|
+
return Credential.from_json(path.read_text(encoding="utf-8"), str(path))
|
|
196
|
+
|
|
197
|
+
def put(self, key: str, credential: Credential) -> None:
|
|
198
|
+
self.directory.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
199
|
+
path = self.path(key)
|
|
200
|
+
tmp = path.with_suffix(f".{os.getpid()}.tmp")
|
|
201
|
+
descriptor = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
202
|
+
# A temp file a crashed run left keeps its old mode through O_CREAT.
|
|
203
|
+
os.fchmod(descriptor, 0o600)
|
|
204
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
205
|
+
handle.write(credential.to_json())
|
|
206
|
+
tmp.replace(path)
|
|
207
|
+
|
|
208
|
+
def delete(self, key: str) -> None:
|
|
209
|
+
self.path(key).unlink(missing_ok=True)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def open_store(kind: str) -> TokenStore:
|
|
213
|
+
if kind == "keyring":
|
|
214
|
+
return KeyringStore()
|
|
215
|
+
if kind == "file":
|
|
216
|
+
return FileStore()
|
|
217
|
+
raise DatabayProfileError(f"token_store must be one of {list(TOKEN_STORES)}, got {kind!r}", code="profile_invalid")
|
databay/error_catalog.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exit codes, and the errors the client itself raises.
|
|
3
|
+
|
|
4
|
+
Every error the CLI ends with has one code, and every code one exit code (the table below). Server
|
|
5
|
+
codes come from the generated ``databay.error_codes`` registry; their exit is ``server_exit``. The
|
|
6
|
+
client's own codes are ``LOCAL_CODES``. ``errors.md`` is rendered from both. Standard library only:
|
|
7
|
+
``python/tools/gen_client.py`` reads it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Literal
|
|
14
|
+
|
|
15
|
+
Action = Literal["retry", "fix_and_retry", "do_first", "ask_human"]
|
|
16
|
+
SideEffects = Literal["none", "partial", "complete", "unknown"]
|
|
17
|
+
|
|
18
|
+
#: Exit code -> what it means and what to do next: one next action per exit code.
|
|
19
|
+
EXIT_CODES: dict[int, str] = {
|
|
20
|
+
0: "ok",
|
|
21
|
+
1: "the server refused; fix the input or ask a human",
|
|
22
|
+
2: "usage: the command line is incomplete or inconsistent",
|
|
23
|
+
3: "a profile or a token is needed",
|
|
24
|
+
4: "still pending: run `next`, the same wait",
|
|
25
|
+
5: "the wait ended in failure",
|
|
26
|
+
6: "transient: retry later",
|
|
27
|
+
7: "version mismatch: upgrade the side the message names",
|
|
28
|
+
8: "the client is outdated or hit a bug: upgrade it or report it",
|
|
29
|
+
9: "not ready: fix what the answer names, then check again",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class LocalCode:
|
|
35
|
+
exit_code: int
|
|
36
|
+
action: Action
|
|
37
|
+
side_effects: SideEffects
|
|
38
|
+
hint: str
|
|
39
|
+
#: How many times an agent may retry the same call before asking a human.
|
|
40
|
+
max_retries: int | None = None
|
|
41
|
+
#: The code is also a server code: its registry entry gives the hint, and this exit applies to both.
|
|
42
|
+
server_code: bool = False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
LOCAL_CODES: dict[str, LocalCode] = {
|
|
46
|
+
"usage_invalid": LocalCode(2, "fix_and_retry", "none", "Read the command's --help and pass the flags it names."),
|
|
47
|
+
"project_conflict": LocalCode(2, "fix_and_retry", "none", "", server_code=True),
|
|
48
|
+
"no_active_project": LocalCode(
|
|
49
|
+
2, "fix_and_retry", "none", "Pass --project <project_id>, or set active_project in the profile."
|
|
50
|
+
),
|
|
51
|
+
"secret_required": LocalCode(
|
|
52
|
+
2, "fix_and_retry", "none", "Pass the secret with --<field>-env NAME or --<field>-stdin; it is never a flag."
|
|
53
|
+
),
|
|
54
|
+
"confirmation_required": LocalCode(
|
|
55
|
+
2, "ask_human", "none", "This command removes or overwrites state. Pass --yes once a human has agreed."
|
|
56
|
+
),
|
|
57
|
+
"unsupported_platform": LocalCode(2, "ask_human", "none", "Run databay on Linux or macOS; on Windows, use WSL."),
|
|
58
|
+
"skill_exists": LocalCode(
|
|
59
|
+
2,
|
|
60
|
+
"ask_human",
|
|
61
|
+
"none",
|
|
62
|
+
"A skill from another CLI version is installed there. Pass --force to replace it with this version's.",
|
|
63
|
+
),
|
|
64
|
+
"skill_write_failed": LocalCode(
|
|
65
|
+
2,
|
|
66
|
+
"ask_human",
|
|
67
|
+
"none",
|
|
68
|
+
"Nothing changed. Make the folder the message names writable, or install with the other --scope.",
|
|
69
|
+
),
|
|
70
|
+
"unsupported_target": LocalCode(
|
|
71
|
+
2,
|
|
72
|
+
"fix_and_retry",
|
|
73
|
+
"none",
|
|
74
|
+
"Pass --target claude or --target codex; no other agent has a verified skill format.",
|
|
75
|
+
),
|
|
76
|
+
"org_choice_required": LocalCode(
|
|
77
|
+
2,
|
|
78
|
+
"fix_and_retry",
|
|
79
|
+
"partial",
|
|
80
|
+
"You are signed in, but you belong to several organizations and none is active. "
|
|
81
|
+
"Run databay org switch <organization>, or sign in again with --org.",
|
|
82
|
+
),
|
|
83
|
+
"org_not_in_token": LocalCode(
|
|
84
|
+
1,
|
|
85
|
+
"ask_human",
|
|
86
|
+
"complete",
|
|
87
|
+
"The server stored the organization, but the new token names another one. Run databay status; "
|
|
88
|
+
"if it persists, ask whoever runs the server.",
|
|
89
|
+
),
|
|
90
|
+
"cancelled": LocalCode(1, "ask_human", "none", "Nothing was changed. Run the command again and answer y."),
|
|
91
|
+
"feature_disabled": LocalCode(
|
|
92
|
+
1,
|
|
93
|
+
"ask_human",
|
|
94
|
+
"none",
|
|
95
|
+
"This feature is off for you or on this server; `details` names it and why. Ask whoever runs the server.",
|
|
96
|
+
),
|
|
97
|
+
"stopped": LocalCode(
|
|
98
|
+
1, "retry", "none", "Stopped while only reading; nothing changed. Run it again, or run `next` to resume a wait."
|
|
99
|
+
),
|
|
100
|
+
"interrupted": LocalCode(
|
|
101
|
+
1,
|
|
102
|
+
"ask_human",
|
|
103
|
+
"unknown",
|
|
104
|
+
"Stopped after the request was sent: check its result before running it again. "
|
|
105
|
+
"A create is safe to repeat only with the --idempotency-key it printed.",
|
|
106
|
+
),
|
|
107
|
+
"no_profile": LocalCode(3, "do_first", "none", "Sign in with databay login --server URL, or pass --profile NAME."),
|
|
108
|
+
"profile_invalid": LocalCode(3, "fix_and_retry", "none", "Fix config.toml as the message says."),
|
|
109
|
+
"token_source_conflict": LocalCode(
|
|
110
|
+
2,
|
|
111
|
+
"fix_and_retry",
|
|
112
|
+
"none",
|
|
113
|
+
"Unset DATABAY_TOKEN, or pass --profile with a profile whose auth is env; the CLI never picks one for you.",
|
|
114
|
+
),
|
|
115
|
+
"login_required": LocalCode(
|
|
116
|
+
3, "do_first", "none", "Sign in again with the command `next` names, then run this command again."
|
|
117
|
+
),
|
|
118
|
+
"credential_missing": LocalCode(
|
|
119
|
+
3, "do_first", "none", "The profile has no stored sign-in. Run the command `next` names."
|
|
120
|
+
),
|
|
121
|
+
"keyring_unavailable": LocalCode(
|
|
122
|
+
3,
|
|
123
|
+
"fix_and_retry",
|
|
124
|
+
"none",
|
|
125
|
+
"No usable OS keyring. Pass --token-store file to keep tokens in a 0600 file under the config dir.",
|
|
126
|
+
),
|
|
127
|
+
"token_file_insecure": LocalCode(
|
|
128
|
+
3, "fix_and_retry", "none", "Run chmod 600 on the file the message names; it holds tokens."
|
|
129
|
+
),
|
|
130
|
+
"offline_access_not_granted": LocalCode(
|
|
131
|
+
3,
|
|
132
|
+
"ask_human",
|
|
133
|
+
"none",
|
|
134
|
+
"Keycloak did not grant offline_access: ask whoever runs the server to give your user that role "
|
|
135
|
+
"(docs/cli.md, Sign in).",
|
|
136
|
+
),
|
|
137
|
+
"expired_token": LocalCode(
|
|
138
|
+
3, "retry", "none", "The sign-in code expired before it was approved. Run databay login again.", 1
|
|
139
|
+
),
|
|
140
|
+
"access_denied": LocalCode(
|
|
141
|
+
3, "ask_human", "none", "The sign-in was refused in the browser. Run databay login again to retry."
|
|
142
|
+
),
|
|
143
|
+
"login_timeout": LocalCode(
|
|
144
|
+
3, "retry", "none", "The browser did not come back in time. Run databay login again, or pass --device.", 1
|
|
145
|
+
),
|
|
146
|
+
"login_failed": LocalCode(
|
|
147
|
+
3, "ask_human", "none", "Keycloak refused the sign-in; the message names its OAuth error."
|
|
148
|
+
),
|
|
149
|
+
"revoke_failed": LocalCode(
|
|
150
|
+
1,
|
|
151
|
+
"ask_human",
|
|
152
|
+
"partial",
|
|
153
|
+
"The local credential is deleted, but Keycloak may still hold the session: ask whoever runs the "
|
|
154
|
+
"server to revoke it (docs/cli.md, Sign out).",
|
|
155
|
+
),
|
|
156
|
+
"token_missing": LocalCode(
|
|
157
|
+
3, "do_first", "none", "Set DATABAY_TOKEN to an access token; the profile reads it from there."
|
|
158
|
+
),
|
|
159
|
+
"wait_pending": LocalCode(
|
|
160
|
+
4, "retry", "complete", "Run `next`: it resumes the same wait and starts nothing again.", 3
|
|
161
|
+
),
|
|
162
|
+
"wait_failed": LocalCode(
|
|
163
|
+
5, "ask_human", "complete", "The operation ran and ended in a failure state; read the error it reports."
|
|
164
|
+
),
|
|
165
|
+
"wait_feature_disabled": LocalCode(
|
|
166
|
+
5,
|
|
167
|
+
"ask_human",
|
|
168
|
+
"unknown",
|
|
169
|
+
"What the wait polled is behind a feature that is off, so it cannot tell how the operation ended. "
|
|
170
|
+
"Ask whoever runs the server, then check the result before running a write again.",
|
|
171
|
+
),
|
|
172
|
+
"unexpected_wait_state": LocalCode(
|
|
173
|
+
5, "ask_human", "complete", "The server reported a state this CLI does not know. Upgrade the CLI."
|
|
174
|
+
),
|
|
175
|
+
"wait_target_gone": LocalCode(
|
|
176
|
+
5, "ask_human", "unknown", "What the wait polled no longer exists; it was deleted while waiting."
|
|
177
|
+
),
|
|
178
|
+
"non_databay_answer": LocalCode(
|
|
179
|
+
1,
|
|
180
|
+
"fix_and_retry",
|
|
181
|
+
"none",
|
|
182
|
+
"Something other than Databay answered (a proxy, a gateway or a wrong server URL): check the profile's server.",
|
|
183
|
+
),
|
|
184
|
+
"network_error": LocalCode(6, "retry", "unknown", "Check the server URL and the network, then run it again.", 3),
|
|
185
|
+
"server_unavailable": LocalCode(
|
|
186
|
+
6, "retry", "unknown", "The server answered without a Databay error body. Try again later.", 3
|
|
187
|
+
),
|
|
188
|
+
"server_version_mismatch": LocalCode(
|
|
189
|
+
7, "ask_human", "none", "The CLI and the server speak different API majors. Upgrade the older side."
|
|
190
|
+
),
|
|
191
|
+
"server_too_old": LocalCode(
|
|
192
|
+
7, "ask_human", "none", "This command needs a newer server. Upgrade the server or use an older CLI."
|
|
193
|
+
),
|
|
194
|
+
"unsupported_product": LocalCode(
|
|
195
|
+
7, "ask_human", "none", "This server runs Databay for Shopify, which the CLI does not support."
|
|
196
|
+
),
|
|
197
|
+
"unknown_error_code": LocalCode(
|
|
198
|
+
8, "ask_human", "unknown", "The server answered a code this CLI does not know. Upgrade the CLI."
|
|
199
|
+
),
|
|
200
|
+
"response_invalid": LocalCode(
|
|
201
|
+
8, "ask_human", "unknown", "The answer does not match this CLI's models. Upgrade the CLI or report it."
|
|
202
|
+
),
|
|
203
|
+
"uncoded_error": LocalCode(
|
|
204
|
+
8, "ask_human", "unknown", "The server answered an error without a code. Report it with the request id."
|
|
205
|
+
),
|
|
206
|
+
"internal_error": LocalCode(
|
|
207
|
+
8, "ask_human", "unknown", "The CLI failed unexpectedly. Report it with the crash log it names."
|
|
208
|
+
),
|
|
209
|
+
"not_ready": LocalCode(9, "fix_and_retry", "none", "Fix what the answer names, then run the check again."),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#: The hint of ``uncoded_error`` on a 404: the feature gate sends no header to a caller it could not identify.
|
|
213
|
+
UNCODED_404_HINT = (
|
|
214
|
+
"A 404 without a code can be a route this server does not have, a sign-in it no longer accepts, or a feature "
|
|
215
|
+
"that is off where the server does not say so. Sign in again, then report it with the request id."
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
#: Server codes whose exit is not the default: the caller must sign in again, or fix the command line.
|
|
219
|
+
SERVER_EXIT_OVERRIDES: dict[str, int] = {"token_invalid": 3, "token_stale": 3, "project_conflict": 2}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def server_exit(code: str, action: Action) -> int:
|
|
223
|
+
"""The exit code of a server error code: an override, else 6 when the registry says retry, else 1."""
|
|
224
|
+
if code in SERVER_EXIT_OVERRIDES:
|
|
225
|
+
return SERVER_EXIT_OVERRIDES[code]
|
|
226
|
+
return 6 if action == "retry" else 1
|