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/errors.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The SDK's exceptions: one per error code, each with its exit code and recovery.
|
|
3
|
+
|
|
4
|
+
A server answer's ``code`` becomes the generated exception for that code (``databay.error_codes``),
|
|
5
|
+
a subclass of ``DatabayApiError``. Answers that do not fit the error contract, and failures the
|
|
6
|
+
client finds itself, are the named classes below; ``databay.error_catalog`` holds their codes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from databay.error_catalog import LOCAL_CODES, server_exit
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class Recovery:
|
|
19
|
+
action: str
|
|
20
|
+
side_effects: str
|
|
21
|
+
max_retries: int | None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DatabayError(Exception):
|
|
25
|
+
"""
|
|
26
|
+
Every error the SDK raises. ``code`` is stable; ``exit_code`` is the CLI's exit for it.
|
|
27
|
+
|
|
28
|
+
``details`` holds the extra top-level fields of a server error body (``revision``, ``quota``, …),
|
|
29
|
+
and, for a client error, the values that explain it (the version, the state).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
code: str = "internal_error"
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
message: str,
|
|
37
|
+
*,
|
|
38
|
+
code: str | None = None,
|
|
39
|
+
status: int | None = None,
|
|
40
|
+
hint: str | None = None,
|
|
41
|
+
next_command: str | None = None,
|
|
42
|
+
details: dict[str, Any] | None = None,
|
|
43
|
+
attempts: int | None = None,
|
|
44
|
+
support_ref: str | None = None,
|
|
45
|
+
docs: str | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
super().__init__(message)
|
|
48
|
+
if code is not None:
|
|
49
|
+
self.code = code
|
|
50
|
+
self.message = message
|
|
51
|
+
self.status = status
|
|
52
|
+
self._hint = hint
|
|
53
|
+
self.next_command = next_command
|
|
54
|
+
self.details = details or {}
|
|
55
|
+
self.attempts = attempts
|
|
56
|
+
self.support_ref = support_ref
|
|
57
|
+
self.docs = docs
|
|
58
|
+
#: An answer the server did give before this failure: a not-ready verdict's body, or the
|
|
59
|
+
#: answer of an operation whose wait then failed.
|
|
60
|
+
self.result: Any = None
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def exit_code(self) -> int:
|
|
64
|
+
return LOCAL_CODES[self.code].exit_code
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def recovery(self) -> Recovery:
|
|
68
|
+
local = LOCAL_CODES[self.code]
|
|
69
|
+
return Recovery(local.action, local.side_effects, local.max_retries)
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def hint(self) -> str:
|
|
73
|
+
return self._hint or LOCAL_CODES[self.code].hint
|
|
74
|
+
|
|
75
|
+
def to_dict(self) -> dict[str, Any]:
|
|
76
|
+
"""The error object the CLI prints (``{"error": ...}``)."""
|
|
77
|
+
recovery = self.recovery
|
|
78
|
+
body: dict[str, Any] = {
|
|
79
|
+
"code": self.code,
|
|
80
|
+
"message": self.message,
|
|
81
|
+
"hint": self.hint,
|
|
82
|
+
"next": self.next_command,
|
|
83
|
+
"docs": self.docs,
|
|
84
|
+
"support_ref": self.support_ref,
|
|
85
|
+
"status": self.status,
|
|
86
|
+
"attempts": self.attempts,
|
|
87
|
+
"details": self.details,
|
|
88
|
+
"recovery": {
|
|
89
|
+
"action": recovery.action,
|
|
90
|
+
"side_effects": recovery.side_effects,
|
|
91
|
+
"max_retries": recovery.max_retries,
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
return body
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class DatabayApiError(DatabayError):
|
|
98
|
+
"""A refusal the server answered with a registered ``code``; the generated classes subclass it."""
|
|
99
|
+
|
|
100
|
+
def _info(self) -> Any: # noqa: ANN401 — an ErrorInfo from the generated registry
|
|
101
|
+
from databay.error_codes import REGISTRY # noqa: PLC0415 — loaded only on an error
|
|
102
|
+
|
|
103
|
+
return REGISTRY[self.code]
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def exit_code(self) -> int:
|
|
107
|
+
return server_exit(self.code, self._info().action)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def recovery(self) -> Recovery:
|
|
111
|
+
info = self._info()
|
|
112
|
+
return Recovery(info.action, info.side_effects, info.max_retries)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def hint(self) -> str:
|
|
116
|
+
return self._hint or self._info().hint
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def api_error(code: str, message: str, **kwargs: Any) -> DatabayApiError: # noqa: ANN401
|
|
120
|
+
"""The generated exception for a registered server ``code``; a code missing from the registry is a ``KeyError``."""
|
|
121
|
+
from databay.error_codes import EXCEPTIONS # noqa: PLC0415 — loaded only on an error
|
|
122
|
+
|
|
123
|
+
return EXCEPTIONS[code](message, **kwargs)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class DatabayUsageError(DatabayError):
|
|
127
|
+
"""The command line or the SDK call is incomplete or inconsistent (exit 2)."""
|
|
128
|
+
|
|
129
|
+
code = "usage_invalid"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class DatabayProfileError(DatabayError):
|
|
133
|
+
"""No profile, a broken config file, or a token the profile names is not there (exit 3)."""
|
|
134
|
+
|
|
135
|
+
code = "no_profile"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class DatabayCancelled(DatabayError):
|
|
139
|
+
code = "cancelled"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class DatabayUncodedError(DatabayError):
|
|
143
|
+
"""A JSON error body without a ``code``: the server broke the error contract."""
|
|
144
|
+
|
|
145
|
+
code = "uncoded_error"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class DatabayFeatureDisabled(DatabayError):
|
|
149
|
+
"""A gated route's feature is off for this caller; ``feature`` and ``source`` say which and why."""
|
|
150
|
+
|
|
151
|
+
code = "feature_disabled"
|
|
152
|
+
|
|
153
|
+
def __init__(self, message: str, *, feature: str, source: str, **kwargs: Any) -> None: # noqa: ANN401
|
|
154
|
+
from databay.feature_header import FEATURE_SOURCE_HINTS # noqa: PLC0415 — loaded only on an error
|
|
155
|
+
|
|
156
|
+
super().__init__(
|
|
157
|
+
message, hint=FEATURE_SOURCE_HINTS[source], details={"feature": feature, "source": source}, **kwargs
|
|
158
|
+
)
|
|
159
|
+
self.feature = feature
|
|
160
|
+
self.source = source
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class DatabayUnknownCodeError(DatabayError):
|
|
164
|
+
"""A ``code`` this client version does not know: a newer server added it."""
|
|
165
|
+
|
|
166
|
+
code = "unknown_error_code"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class DatabayServerUnavailable(DatabayError):
|
|
170
|
+
"""A non-JSON error body: a proxy or a maintenance page answered, not Databay."""
|
|
171
|
+
|
|
172
|
+
code = "server_unavailable"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class DatabayNetworkError(DatabayError):
|
|
176
|
+
code = "network_error"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class DatabayResponseInvalid(DatabayError):
|
|
180
|
+
"""A 2xx body that does not fit the generated model; it names the field, never the body."""
|
|
181
|
+
|
|
182
|
+
code = "response_invalid"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class DatabayVersionError(DatabayError):
|
|
186
|
+
code = "server_version_mismatch"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class DatabayWaitError(DatabayError):
|
|
190
|
+
"""
|
|
191
|
+
A wait that did not succeed.
|
|
192
|
+
|
|
193
|
+
Its code is ``wait_pending`` (timed out, resumable), ``wait_failed``, ``unexpected_wait_state``,
|
|
194
|
+
``wait_target_gone`` or ``wait_feature_disabled``. ``outcome`` is the wait's last state, which the
|
|
195
|
+
CLI prints on stdout.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
code = "wait_failed"
|
|
199
|
+
|
|
200
|
+
def __init__(self, message: str, *, outcome: Any = None, **kwargs: Any) -> None: # noqa: ANN401
|
|
201
|
+
super().__init__(message, **kwargs)
|
|
202
|
+
self.outcome = outcome
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class DatabayNotReady(DatabayError):
|
|
206
|
+
"""A verdict route answered a not-ready verdict; ``result`` is the whole answer."""
|
|
207
|
+
|
|
208
|
+
code = "not_ready"
|
|
209
|
+
|
|
210
|
+
def __init__(self, message: str, *, result: Any = None, **kwargs: Any) -> None: # noqa: ANN401
|
|
211
|
+
super().__init__(message, **kwargs)
|
|
212
|
+
self.result = result
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@dataclass(frozen=True, slots=True)
|
|
216
|
+
class ErrorInfo:
|
|
217
|
+
"""One server code's registry entry (``contract/error_codes.json``)."""
|
|
218
|
+
|
|
219
|
+
hint: str
|
|
220
|
+
action: str
|
|
221
|
+
side_effects: str
|
|
222
|
+
max_retries: int | None
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The ``X-Databay-Feature`` header on the 404 a feature gate answers.
|
|
3
|
+
|
|
4
|
+
A gated feature that is off answers ``404 {"detail": "Not Found"}``, and an authenticated caller also
|
|
5
|
+
gets this header naming the feature and why it is off. Standard library only: the server's tests load
|
|
6
|
+
this file to check that the header the gate sends parses here.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from typing import NamedTuple
|
|
13
|
+
|
|
14
|
+
FEATURE_HEADER = "x-databay-feature"
|
|
15
|
+
|
|
16
|
+
#: What the caller can do, per the documented ``source``. A source not listed here is not parsed.
|
|
17
|
+
FEATURE_SOURCE_HINTS: dict[str, str] = {
|
|
18
|
+
"plan": "Your organization's plan does not include this feature. Ask whoever runs this server about changing it.",
|
|
19
|
+
"override": "This feature was turned off for your organization. Ask whoever runs this server to turn it back on.",
|
|
20
|
+
"ceiling": "This server has the feature turned off for everyone on it. Ask whoever runs the server to turn it on.",
|
|
21
|
+
"prerequisite": "This feature needs another feature that is off. Ask whoever runs this server which one.",
|
|
22
|
+
"not_ready": "This feature is still being set up on this server. Ask whoever runs it when it will be ready.",
|
|
23
|
+
"refused": "This server turned the feature off because a check it runs did not pass. Ask whoever runs the server.",
|
|
24
|
+
"legacy": "No plan covers your account. Check that it belongs to an organization, or ask whoever runs the server.",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_FEATURE_OFF = re.compile(
|
|
28
|
+
rf"disabled; feature=(?P<feature>[a-z][a-z0-9_]*); source=(?P<source>{'|'.join(FEATURE_SOURCE_HINTS)})"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FeatureOff(NamedTuple):
|
|
33
|
+
feature: str
|
|
34
|
+
source: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_feature_header(value: str | None) -> FeatureOff | None:
|
|
38
|
+
"""The feature a header names, or ``None`` when it is absent or not the documented shape."""
|
|
39
|
+
match = _FEATURE_OFF.fullmatch(value) if value is not None else None
|
|
40
|
+
return FeatureOff(match["feature"], match["source"]) if match is not None else None
|
databay/invoke.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Calling one operation: the project, the request, the answer's model, and the verdict.
|
|
3
|
+
|
|
4
|
+
``Session`` is what the SDK resources and the CLI commands share. It checks the server once
|
|
5
|
+
(``databay.compat``), places the project where the operation takes it, sends the request and
|
|
6
|
+
validates the answer against the generated model before anyone reads it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
from urllib.parse import quote
|
|
15
|
+
|
|
16
|
+
from databay import compat
|
|
17
|
+
from databay.errors import DatabayNotReady, DatabayResponseInvalid, DatabayUsageError, api_error
|
|
18
|
+
from databay.transport import IDEMPOTENCY_HEADER, PROJECT_HEADER, Request
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from databay.compat import ServerInfo
|
|
22
|
+
from databay.operation import Operation, Verdict
|
|
23
|
+
from databay.transport import Answer, Transport
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True)
|
|
27
|
+
class CallValues:
|
|
28
|
+
"""
|
|
29
|
+
The caller's inputs to one operation, by wire name.
|
|
30
|
+
|
|
31
|
+
``project_explicit`` is a project id the caller passed as the operation's own project
|
|
32
|
+
parameter; ``project_flag`` is ``--project`` (or the SDK's ``project=``).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
path: dict[str, str] = field(default_factory=dict)
|
|
36
|
+
query: dict[str, Any] = field(default_factory=dict)
|
|
37
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
38
|
+
body: Any = None
|
|
39
|
+
has_body: bool = False
|
|
40
|
+
project_explicit: str | None = None
|
|
41
|
+
project_flag: str | None = None
|
|
42
|
+
idempotency_key: str | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(slots=True)
|
|
46
|
+
class Result:
|
|
47
|
+
raw: Any
|
|
48
|
+
parsed: Any
|
|
49
|
+
project: str | None
|
|
50
|
+
request_id: str
|
|
51
|
+
#: The path values the request was sent with, which a wait may bind.
|
|
52
|
+
path: dict[str, str]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def resolve_project(op: Operation, values: CallValues, active: str | None) -> str | None:
|
|
56
|
+
"""
|
|
57
|
+
The project ``op`` is sent for.
|
|
58
|
+
|
|
59
|
+
An explicit id and ``--project`` that differ are ``project_conflict``; otherwise the explicit
|
|
60
|
+
id, then ``--project``, then the profile's active project. None of them is ``no_active_project``.
|
|
61
|
+
"""
|
|
62
|
+
if op.project is None:
|
|
63
|
+
return None
|
|
64
|
+
explicit, flag = values.project_explicit, values.project_flag
|
|
65
|
+
if explicit and flag and explicit != flag:
|
|
66
|
+
raise api_error(
|
|
67
|
+
"project_conflict",
|
|
68
|
+
f"the project id argument {explicit} and --project {flag} differ",
|
|
69
|
+
details={"argument": explicit, "flag": flag},
|
|
70
|
+
)
|
|
71
|
+
chosen = explicit or flag or active
|
|
72
|
+
if not chosen:
|
|
73
|
+
raise DatabayUsageError(
|
|
74
|
+
f"`{op.command}` acts on a project: pass --project <project_id> or set the profile's active_project",
|
|
75
|
+
code="no_active_project",
|
|
76
|
+
)
|
|
77
|
+
return chosen
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def pointer(document: Any, path: str) -> Any: # noqa: ANN401 — any JSON value
|
|
81
|
+
"""The value at a JSON pointer (``/a/b``); ``KeyError`` when a step is missing."""
|
|
82
|
+
value = document
|
|
83
|
+
for raw_step in path.lstrip("/").split("/"):
|
|
84
|
+
step = raw_step.replace("~1", "/").replace("~0", "~")
|
|
85
|
+
if isinstance(value, list):
|
|
86
|
+
value = value[int(step)]
|
|
87
|
+
elif isinstance(value, dict):
|
|
88
|
+
value = value[step]
|
|
89
|
+
else:
|
|
90
|
+
raise KeyError(path)
|
|
91
|
+
return value
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def json_text(value: Any) -> str: # noqa: ANN401
|
|
95
|
+
"""How a declaration spells a value: a string as is, anything else as JSON (``true``, ``null``, ``3``)."""
|
|
96
|
+
import json # noqa: PLC0415
|
|
97
|
+
|
|
98
|
+
return value if isinstance(value, str) else json.dumps(value)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def build_request(op: Operation, values: CallValues, project: str | None) -> Request:
|
|
102
|
+
path_values = dict(values.path)
|
|
103
|
+
query = dict(values.query)
|
|
104
|
+
headers = dict(values.headers)
|
|
105
|
+
body = values.body
|
|
106
|
+
location = op.project_location
|
|
107
|
+
if location is not None and project is not None:
|
|
108
|
+
where, name = location
|
|
109
|
+
if where == "header":
|
|
110
|
+
headers[PROJECT_HEADER] = project
|
|
111
|
+
elif where == "path":
|
|
112
|
+
path_values[name] = project
|
|
113
|
+
elif where == "query":
|
|
114
|
+
query[name] = project
|
|
115
|
+
else:
|
|
116
|
+
body = {**(body or {}), name: project}
|
|
117
|
+
missing = [p.name for p in op.path_params if p.name not in path_values]
|
|
118
|
+
if missing:
|
|
119
|
+
raise DatabayUsageError(f"`{op.command}` needs {', '.join(missing)}")
|
|
120
|
+
path = op.path
|
|
121
|
+
for name, value in path_values.items():
|
|
122
|
+
path = path.replace("{" + name + "}", quote(str(value), safe=""))
|
|
123
|
+
if values.idempotency_key is not None:
|
|
124
|
+
headers[IDEMPOTENCY_HEADER] = values.idempotency_key
|
|
125
|
+
pairs = [
|
|
126
|
+
(name, json_text(item))
|
|
127
|
+
for name, value in query.items()
|
|
128
|
+
for item in (value if isinstance(value, list) else [value])
|
|
129
|
+
]
|
|
130
|
+
return Request(
|
|
131
|
+
method=op.method.upper(),
|
|
132
|
+
path=path,
|
|
133
|
+
query=pairs,
|
|
134
|
+
headers=headers,
|
|
135
|
+
json_body=body,
|
|
136
|
+
has_body=body is not None or values.has_body,
|
|
137
|
+
secret_fields=op.secret_fields,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def model_type(reference: str) -> Any: # noqa: ANN401 — a model class or a type alias
|
|
142
|
+
module, _, name = reference.partition(":")
|
|
143
|
+
return getattr(importlib.import_module(f"databay.models.{module}"), name)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def validate(op: Operation, raw: Any, reference: str | None) -> Any: # noqa: ANN401
|
|
147
|
+
"""``raw`` as the generated model, or ``response_invalid`` naming the field, never the body."""
|
|
148
|
+
if reference is None:
|
|
149
|
+
return raw
|
|
150
|
+
from pydantic import TypeAdapter, ValidationError # noqa: PLC0415
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
return TypeAdapter(model_type(reference)).validate_python(raw)
|
|
154
|
+
except ValidationError as exc:
|
|
155
|
+
first = exc.errors(include_input=False, include_url=False)[0]
|
|
156
|
+
where = ".".join(str(part) for part in first["loc"]) or "(the body)"
|
|
157
|
+
raise DatabayResponseInvalid(
|
|
158
|
+
f"`{op.command}`: the answer does not fit {reference.partition(':')[2]} at {where} ({first['type']})",
|
|
159
|
+
details={"operation": op.operation_id, "field": where, "error": first["type"]},
|
|
160
|
+
) from None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def decode(op: Operation, answer: Answer) -> tuple[Any, Any]:
|
|
164
|
+
"""The answer as ``(raw JSON or text, validated value)``."""
|
|
165
|
+
kind = op.response.kind
|
|
166
|
+
if kind == "empty" or answer.status == 204: # noqa: PLR2004
|
|
167
|
+
return None, None
|
|
168
|
+
if kind == "text":
|
|
169
|
+
text = answer.text()
|
|
170
|
+
return text, text
|
|
171
|
+
try:
|
|
172
|
+
raw = answer.json()
|
|
173
|
+
except ValueError:
|
|
174
|
+
raise DatabayResponseInvalid(
|
|
175
|
+
f"`{op.command}`: HTTP {answer.status} answered a body that is not JSON",
|
|
176
|
+
details={"operation": op.operation_id},
|
|
177
|
+
) from None
|
|
178
|
+
return raw, validate(op, raw, op.response.model)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def check_verdict(op: Operation, verdict: Verdict, raw: Any) -> None: # noqa: ANN401
|
|
182
|
+
try:
|
|
183
|
+
value = json_text(pointer(raw, verdict.field))
|
|
184
|
+
except (KeyError, IndexError, ValueError):
|
|
185
|
+
raise DatabayResponseInvalid(
|
|
186
|
+
f"`{op.command}`: the answer has no {verdict.field}", details={"operation": op.operation_id}
|
|
187
|
+
) from None
|
|
188
|
+
if value in verdict.ok:
|
|
189
|
+
return
|
|
190
|
+
if value in verdict.not_ready:
|
|
191
|
+
raise DatabayNotReady(
|
|
192
|
+
f"`{op.command}`: {verdict.field.lstrip('/')} is {value}", result=raw, details={"value": value}
|
|
193
|
+
)
|
|
194
|
+
raise DatabayResponseInvalid(
|
|
195
|
+
f"`{op.command}`: {verdict.field.lstrip('/')} is {value!r}, which this CLI does not classify; upgrade it",
|
|
196
|
+
details={"operation": op.operation_id, "value": value},
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@dataclass
|
|
201
|
+
class Session:
|
|
202
|
+
"""One caller on one server: a transport, the profile's active project and the server's version."""
|
|
203
|
+
|
|
204
|
+
transport: Transport
|
|
205
|
+
active_project: str | None = None
|
|
206
|
+
profile: str | None = None
|
|
207
|
+
#: The server's client-config and whether it came from the cache, once read.
|
|
208
|
+
server: tuple[ServerInfo, bool] | None = None
|
|
209
|
+
|
|
210
|
+
def ensure_compatible(self, op: Operation) -> None:
|
|
211
|
+
self.server = compat.ensure_compatible(self.transport, self.server, op)
|
|
212
|
+
|
|
213
|
+
def call(self, op: Operation, values: CallValues, *, check_verdict_: bool = True) -> Result:
|
|
214
|
+
project = resolve_project(op, values, self.active_project)
|
|
215
|
+
request = build_request(op, values, project)
|
|
216
|
+
self.ensure_compatible(op)
|
|
217
|
+
answer = self.transport.send(request)
|
|
218
|
+
raw, parsed = decode(op, answer)
|
|
219
|
+
if check_verdict_ and op.verdict is not None:
|
|
220
|
+
check_verdict(op, op.verdict, raw)
|
|
221
|
+
path = {p.name: values.path[p.name] for p in op.path_params if p.name in values.path}
|
|
222
|
+
location = op.project_location
|
|
223
|
+
if location is not None and location[0] == "path" and project is not None:
|
|
224
|
+
path[location[1]] = project
|
|
225
|
+
return Result(raw=raw, parsed=parsed, project=project, request_id=answer.request_id, path=path)
|
databay/locks.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exclusive file locks between databay processes: one per credential, one for ``config.toml``.
|
|
3
|
+
|
|
4
|
+
``fcntl.flock`` on ``<config dir>/locks/<name>.lock``. The kernel drops the lock when the process
|
|
5
|
+
exits, so a killed process never leaves a credential locked.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import fcntl
|
|
11
|
+
import hashlib
|
|
12
|
+
import os
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from databay.paths import config_dir
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Iterator
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
CONFIG_LOCK = "config"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def lock_path(name: str) -> Path:
|
|
26
|
+
"""The lock file for ``name``; a credential key is hashed, since it holds a URL."""
|
|
27
|
+
stem = name if name == CONFIG_LOCK else hashlib.sha256(name.encode()).hexdigest()
|
|
28
|
+
return config_dir() / "locks" / f"{stem}.lock"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@contextmanager
|
|
32
|
+
def locked(name: str) -> Iterator[None]:
|
|
33
|
+
"""Hold the exclusive lock ``name`` for the block; waits while another process holds it."""
|
|
34
|
+
path = lock_path(name)
|
|
35
|
+
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
36
|
+
descriptor = os.open(path, os.O_RDWR | os.O_CREAT, 0o600)
|
|
37
|
+
try:
|
|
38
|
+
fcntl.flock(descriptor, fcntl.LOCK_EX)
|
|
39
|
+
yield
|
|
40
|
+
finally:
|
|
41
|
+
os.close(descriptor)
|