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/profiles.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Profiles: which server a command talks to, how it gets its token, and its active project.
|
|
3
|
+
|
|
4
|
+
``config.toml`` in the config dir::
|
|
5
|
+
|
|
6
|
+
current_profile = "staging"
|
|
7
|
+
|
|
8
|
+
[profiles.staging]
|
|
9
|
+
server = "https://databay.example.com"
|
|
10
|
+
auth = "login" # login, api_key or env
|
|
11
|
+
credential = "databay-cli:https://databay.example.com:<sub>"
|
|
12
|
+
token_store = "keyring" # keyring or file
|
|
13
|
+
active_project = "<project id>" # optional
|
|
14
|
+
|
|
15
|
+
The profile alone picks the token: ``login`` and ``api_key`` read the stored ``credential``
|
|
16
|
+
(written by ``databay login``), ``env`` reads ``$DATABAY_TOKEN``. A command uses ``--profile NAME``,
|
|
17
|
+
else ``current_profile``; with neither it fails with ``no_profile``. A key this version does not
|
|
18
|
+
know is an error, never ignored. Writers hold the config lock and replace the file whole.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import tomllib
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from typing import TYPE_CHECKING, Any
|
|
29
|
+
|
|
30
|
+
from databay.errors import DatabayProfileError, DatabayUsageError
|
|
31
|
+
from databay.locks import CONFIG_LOCK, locked
|
|
32
|
+
from databay.paths import config_file
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from collections.abc import Callable
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
PROFILE_KEYS = frozenset({"server", "auth", "credential", "token_store", "active_project"})
|
|
39
|
+
TOP_LEVEL_KEYS = frozenset({"current_profile", "profiles"})
|
|
40
|
+
#: The order a written profile lists its keys in.
|
|
41
|
+
KEY_ORDER = ("server", "auth", "credential", "token_store", "active_project")
|
|
42
|
+
#: The token sources that read a stored credential.
|
|
43
|
+
STORED_AUTH = frozenset({"login", "api_key"})
|
|
44
|
+
PROFILE_NAME = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class Profile:
|
|
49
|
+
name: str
|
|
50
|
+
server: str
|
|
51
|
+
auth: str
|
|
52
|
+
active_project: str | None = None
|
|
53
|
+
credential: str | None = None
|
|
54
|
+
token_store: str | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _invalid(path: Path, problem: str) -> DatabayProfileError:
|
|
58
|
+
return DatabayProfileError(f"{path}: {problem}", code="profile_invalid")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _read(path: Path) -> dict[str, Any]:
|
|
62
|
+
if not path.exists():
|
|
63
|
+
return {}
|
|
64
|
+
try:
|
|
65
|
+
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
66
|
+
except tomllib.TOMLDecodeError as exc:
|
|
67
|
+
raise _invalid(path, f"not valid TOML ({exc})") from exc
|
|
68
|
+
unknown = sorted(set(data) - TOP_LEVEL_KEYS)
|
|
69
|
+
if unknown:
|
|
70
|
+
raise _invalid(path, f"unknown keys {unknown}; allowed: {sorted(TOP_LEVEL_KEYS)}")
|
|
71
|
+
if not isinstance(data.get("profiles", {}), dict):
|
|
72
|
+
raise _invalid(path, "profiles is not a table")
|
|
73
|
+
return data
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _optional_text(path: Path, name: str, entry: dict[str, Any], key: str) -> str | None:
|
|
77
|
+
value = entry.get(key)
|
|
78
|
+
if value is not None and (not isinstance(value, str) or not value):
|
|
79
|
+
raise _invalid(path, f"profiles.{name}.{key} must be a non-empty string")
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _profile(path: Path, name: str, entry: Any) -> Profile: # noqa: ANN401 — a parsed TOML value
|
|
84
|
+
from databay.credentials import TOKEN_STORES # noqa: PLC0415
|
|
85
|
+
from databay.tokens import TOKEN_SOURCES # noqa: PLC0415 — tokens imports this module's types
|
|
86
|
+
|
|
87
|
+
if not isinstance(entry, dict):
|
|
88
|
+
raise _invalid(path, f"profiles.{name} is not a table")
|
|
89
|
+
unknown = sorted(set(entry) - PROFILE_KEYS)
|
|
90
|
+
if unknown:
|
|
91
|
+
raise _invalid(path, f"profiles.{name} has unknown keys {unknown}; allowed: {sorted(PROFILE_KEYS)}")
|
|
92
|
+
server = entry.get("server")
|
|
93
|
+
if not isinstance(server, str) or not server.startswith(("https://", "http://")):
|
|
94
|
+
raise _invalid(path, f"profiles.{name}.server must be an http(s) URL, got {server!r}")
|
|
95
|
+
auth = entry.get("auth")
|
|
96
|
+
if auth not in TOKEN_SOURCES:
|
|
97
|
+
raise _invalid(path, f"profiles.{name}.auth must be one of {sorted(TOKEN_SOURCES)}, got {auth!r}")
|
|
98
|
+
project = _optional_text(path, name, entry, "active_project")
|
|
99
|
+
credential = _optional_text(path, name, entry, "credential")
|
|
100
|
+
store = _optional_text(path, name, entry, "token_store")
|
|
101
|
+
if auth not in STORED_AUTH and (credential or store):
|
|
102
|
+
raise _invalid(
|
|
103
|
+
path, f"profiles.{name} has auth = {auth!r}, which stores nothing: remove credential and token_store"
|
|
104
|
+
)
|
|
105
|
+
if (credential is not None or store is not None) and store not in TOKEN_STORES:
|
|
106
|
+
raise _invalid(path, f"profiles.{name}.token_store must be one of {list(TOKEN_STORES)}, got {store!r}")
|
|
107
|
+
return Profile(
|
|
108
|
+
name=name,
|
|
109
|
+
server=server.rstrip("/"),
|
|
110
|
+
auth=auth,
|
|
111
|
+
active_project=project,
|
|
112
|
+
credential=credential,
|
|
113
|
+
token_store=store,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def load_profile(name: str | None) -> Profile:
|
|
118
|
+
"""The profile ``name``, else the config's ``current_profile``; ``no_profile`` when there is neither."""
|
|
119
|
+
path = config_file()
|
|
120
|
+
data = _read(path)
|
|
121
|
+
chosen = name or data.get("current_profile")
|
|
122
|
+
if chosen is None:
|
|
123
|
+
raise DatabayProfileError(
|
|
124
|
+
f"no profile: pass --profile NAME or sign in with databay login --server URL (config: {path})"
|
|
125
|
+
)
|
|
126
|
+
profiles = data.get("profiles", {})
|
|
127
|
+
if chosen not in profiles:
|
|
128
|
+
known = ", ".join(sorted(profiles)) or "none"
|
|
129
|
+
raise DatabayProfileError(f"no profile {chosen!r} in {path} (profiles: {known})")
|
|
130
|
+
return _profile(path, chosen, profiles[chosen])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def all_profiles() -> tuple[str | None, list[Profile]]:
|
|
134
|
+
"""``current_profile`` and every profile, by name."""
|
|
135
|
+
path = config_file()
|
|
136
|
+
data = _read(path)
|
|
137
|
+
profiles = data.get("profiles", {})
|
|
138
|
+
return data.get("current_profile"), [_profile(path, name, profiles[name]) for name in sorted(profiles)]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _toml(data: dict[str, Any]) -> str:
|
|
142
|
+
"""``config.toml`` text: strings only, every value JSON-quoted (a JSON string is a TOML basic string)."""
|
|
143
|
+
lines = []
|
|
144
|
+
if data.get("current_profile") is not None:
|
|
145
|
+
lines += [f"current_profile = {json.dumps(data['current_profile'])}", ""]
|
|
146
|
+
for name, entry in sorted(data.get("profiles", {}).items()):
|
|
147
|
+
lines.append(f"[profiles.{json.dumps(name)}]")
|
|
148
|
+
lines += [f"{key} = {json.dumps(entry[key])}" for key in KEY_ORDER if entry.get(key) is not None]
|
|
149
|
+
lines.append("")
|
|
150
|
+
return "\n".join(lines)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def edit_config(change: Callable[[dict[str, Any]], Any]) -> Any: # noqa: ANN401 — what ``change`` returns
|
|
154
|
+
"""
|
|
155
|
+
Apply ``change`` to the parsed config under the config lock, then write it; returns what ``change`` returns.
|
|
156
|
+
|
|
157
|
+
The result is validated before it is written, so a change never leaves a config this version cannot read.
|
|
158
|
+
"""
|
|
159
|
+
path = config_file()
|
|
160
|
+
with locked(CONFIG_LOCK):
|
|
161
|
+
data = _read(path)
|
|
162
|
+
data.setdefault("profiles", {})
|
|
163
|
+
outcome = change(data)
|
|
164
|
+
for name, entry in data["profiles"].items():
|
|
165
|
+
_profile(path, name, entry)
|
|
166
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
tmp = path.with_suffix(f".{os.getpid()}.tmp")
|
|
168
|
+
tmp.write_text(_toml(data), encoding="utf-8")
|
|
169
|
+
tmp.replace(path)
|
|
170
|
+
return outcome
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def login_command(profile: Profile) -> str:
|
|
174
|
+
"""The command that signs ``profile`` in again."""
|
|
175
|
+
return f"databay login --server {profile.server} --profile {profile.name}"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def check_name(name: str) -> str:
|
|
179
|
+
if not PROFILE_NAME.match(name):
|
|
180
|
+
raise DatabayUsageError(f"profile name {name!r}: use 1 to 64 letters, digits, - or _")
|
|
181
|
+
return name
|
databay/redact.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""What ``--verbose`` may print of a request: secret headers and secret body fields are replaced."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
REDACTED = "<redacted>"
|
|
8
|
+
#: Lower case: header names are case-insensitive.
|
|
9
|
+
SECRET_HEADERS = frozenset({"authorization", "cookie", "set-cookie", "proxy-authorization"})
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def headers(values: dict[str, str]) -> dict[str, str]:
|
|
13
|
+
return {name: REDACTED if name.lower() in SECRET_HEADERS else value for name, value in values.items()}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def body(value: Any, secret_fields: frozenset[str]) -> Any: # noqa: ANN401 — any JSON value
|
|
17
|
+
"""``value`` with every field named in ``secret_fields`` replaced, at any depth."""
|
|
18
|
+
if isinstance(value, dict):
|
|
19
|
+
return {k: REDACTED if k in secret_fields else body(v, secret_fields) for k, v in value.items()}
|
|
20
|
+
if isinstance(value, list):
|
|
21
|
+
return [body(item, secret_fields) for item in value]
|
|
22
|
+
return value
|
databay/reference.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The text of each command's ``--help`` and of the generated ``commands.md`` and ``errors.md``.
|
|
3
|
+
|
|
4
|
+
``--help`` and the references are rendered by the same functions from the same ``Operation``
|
|
5
|
+
data, so they cannot drift. Standard library only: ``python/tools/gen_client.py`` renders the
|
|
6
|
+
references with it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import inspect
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from databay.cli.flags import Flag, command_flags
|
|
15
|
+
from databay.error_catalog import EXIT_CODES, LOCAL_CODES, server_exit
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Iterable
|
|
19
|
+
|
|
20
|
+
from databay.operation import Operation
|
|
21
|
+
|
|
22
|
+
GENERATED_NOTE = "Generated by python/tools/gen_client.py from contract/core.json; do not edit."
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _description(op: Operation) -> str:
|
|
26
|
+
text = inspect.cleandoc(op.description) if op.description.strip() else ""
|
|
27
|
+
return text or op.summary
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def help_paragraphs(op: Operation, variant: str | None = None) -> list[tuple[str, bool]]:
|
|
31
|
+
"""``(text, preformatted)`` paragraphs of a command's help; examples keep their lines."""
|
|
32
|
+
paragraphs: list[tuple[str, bool]] = [(part, False) for part in _description(op).split("\n\n") if part.strip()]
|
|
33
|
+
location = op.project_location
|
|
34
|
+
if location is not None:
|
|
35
|
+
where = "the X-Databay-Project-ID header" if location[0] == "header" else f"{location[0]} {location[1]}"
|
|
36
|
+
paragraphs.append((f"Acts in a project, sent as {where}: --project, else the profile's active project.", False))
|
|
37
|
+
if op.destructive:
|
|
38
|
+
paragraphs.append(
|
|
39
|
+
("Removes or overwrites state: asks on a terminal, and needs --yes when output is captured.", False)
|
|
40
|
+
)
|
|
41
|
+
wait = op.wait or op.resumes
|
|
42
|
+
if wait is not None and not op.offers_wait:
|
|
43
|
+
paragraphs.append(("Answers at once: `next` names the command that waits for it.", False))
|
|
44
|
+
elif wait is not None:
|
|
45
|
+
text = (
|
|
46
|
+
f"--wait polls {wait.status_operation} until {wait.field.lstrip('/')} is "
|
|
47
|
+
f"{' or '.join(wait.success)}; it fails on {', '.join(wait.failure) or 'nothing'}."
|
|
48
|
+
)
|
|
49
|
+
paragraphs.append(
|
|
50
|
+
(
|
|
51
|
+
text,
|
|
52
|
+
False,
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
if op.verdict is not None:
|
|
56
|
+
paragraphs.append(
|
|
57
|
+
(f"Exits 9 (not_ready) when {op.verdict.field.lstrip('/')} is {' or '.join(op.verdict.not_ready)}.", False)
|
|
58
|
+
)
|
|
59
|
+
examples = [e for e in op.examples if variant is None or variant in positional_words(e)]
|
|
60
|
+
if examples:
|
|
61
|
+
paragraphs.append(("Examples:\n" + "\n".join(f" {e}" for e in examples), True))
|
|
62
|
+
return paragraphs
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def positional_words(example: str) -> list[str]:
|
|
66
|
+
"""The words of an example after ``databay <noun> <verb>`` that are neither a flag nor a flag's value."""
|
|
67
|
+
import shlex # noqa: PLC0415
|
|
68
|
+
|
|
69
|
+
words = shlex.split(example)[3:]
|
|
70
|
+
found: list[str] = []
|
|
71
|
+
index = 0
|
|
72
|
+
while index < len(words):
|
|
73
|
+
word = words[index]
|
|
74
|
+
if not word.startswith("--"):
|
|
75
|
+
found.append(word)
|
|
76
|
+
index += 1
|
|
77
|
+
continue
|
|
78
|
+
following = words[index + 1] if index + 1 < len(words) else None
|
|
79
|
+
index += 1 if following is None or following.startswith("--") else 2
|
|
80
|
+
return found
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def click_help(op: Operation, variant: str | None = None) -> str:
|
|
84
|
+
# ``\b`` keeps click from rewrapping a paragraph's lines.
|
|
85
|
+
return "\n\n".join(f"\b\n{text}" if pre else text for text, pre in help_paragraphs(op, variant))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _usage(op: Operation, flags: list[Flag], variant: str | None) -> str:
|
|
89
|
+
words = [op.command] + ([variant] if variant else [])
|
|
90
|
+
words += [flag.name if flag.required else f"[{flag.name}]" for flag in flags if flag.role == "argument"]
|
|
91
|
+
words.append("[OPTIONS]")
|
|
92
|
+
return " ".join(words)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _flag_cell(flag: Flag) -> str:
|
|
96
|
+
if flag.takes == "argument":
|
|
97
|
+
return f"`{flag.name}`"
|
|
98
|
+
if flag.takes == "bool":
|
|
99
|
+
return f"`{flag.name}` / `--no-{flag.name.removeprefix('--')}`"
|
|
100
|
+
if flag.takes == "switch":
|
|
101
|
+
return f"`{flag.name}`"
|
|
102
|
+
metavar = flag.metavar or (flag.param.kind.upper() if flag.param else "TEXT")
|
|
103
|
+
suffix = " (repeat)" if flag.takes == "multiple" else ""
|
|
104
|
+
return f"`{flag.name} {metavar}`{suffix}"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _cell(text: str) -> str:
|
|
108
|
+
return text.replace("|", "\\|").replace("\n", " ")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _command_section(op: Operation, variant: str | None) -> list[str]:
|
|
112
|
+
title = f"{op.command} {variant}" if variant else op.command
|
|
113
|
+
lines = [f"### {title}", ""]
|
|
114
|
+
for text, pre in help_paragraphs(op, variant):
|
|
115
|
+
lines += (["```", text, "```"] if pre else [text]) + [""]
|
|
116
|
+
flags = command_flags(op, variant)
|
|
117
|
+
lines += [f"Usage: `{_usage(op, flags, variant)}`", "", "| Argument or flag | Required | Description |"]
|
|
118
|
+
lines.append("|---|---|---|")
|
|
119
|
+
for flag in flags:
|
|
120
|
+
text = flag.help
|
|
121
|
+
if flag.choices:
|
|
122
|
+
text = (f"{text.rstrip('.')}. " if text else "") + f"One of: {', '.join(flag.choices)}."
|
|
123
|
+
required = "yes" if flag.required else ""
|
|
124
|
+
lines.append(f"| {_flag_cell(flag)} | {required} | {_cell(text)} |")
|
|
125
|
+
lines.append("")
|
|
126
|
+
return lines
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def commands_markdown(
|
|
130
|
+
operations: Iterable[Operation], curated: dict[str, str], handwritten: dict[str, tuple[str, str, str]]
|
|
131
|
+
) -> str:
|
|
132
|
+
"""
|
|
133
|
+
``commands.md``: every generated command, the operations a hand-written command replaces, then those commands.
|
|
134
|
+
|
|
135
|
+
``curated`` maps an operation id to the hand-written command that replaces it; ``handwritten``
|
|
136
|
+
holds the hand-written commands this CLI has, so one it lacks is listed as not available.
|
|
137
|
+
"""
|
|
138
|
+
available = set(handwritten)
|
|
139
|
+
by_noun: dict[str, list[Operation]] = {}
|
|
140
|
+
for op in operations:
|
|
141
|
+
by_noun.setdefault(op.noun, []).append(op)
|
|
142
|
+
lines = ["# databay commands", "", GENERATED_NOTE, ""]
|
|
143
|
+
lines += ["Every command also takes the flags `--output`, `--profile`, `--verbose` and `--no-retry`.", ""]
|
|
144
|
+
replaced: list[tuple[Operation, str]] = []
|
|
145
|
+
for noun in sorted(by_noun):
|
|
146
|
+
generated = [op for op in sorted(by_noun[noun], key=lambda o: o.verb) if op.operation_id not in curated]
|
|
147
|
+
replaced += [(op, curated[op.operation_id]) for op in by_noun[noun] if op.operation_id in curated]
|
|
148
|
+
if not generated:
|
|
149
|
+
continue
|
|
150
|
+
lines += [f"## {noun}", ""]
|
|
151
|
+
for op in generated:
|
|
152
|
+
if op.body is not None and op.body.discriminator is not None:
|
|
153
|
+
for key, _, _ in op.body.variants:
|
|
154
|
+
lines += _command_section(op, key)
|
|
155
|
+
else:
|
|
156
|
+
lines += _command_section(op, None)
|
|
157
|
+
if replaced:
|
|
158
|
+
lines += ["## Operations with a hand-written command", "", "| Operation | Command |", "|---|---|"]
|
|
159
|
+
for op, command in replaced:
|
|
160
|
+
status = "" if command.split()[0] in available else " (not in this CLI version)"
|
|
161
|
+
lines.append(f"| `{op.method.upper()} {op.path}` | `databay {command}`{status} |")
|
|
162
|
+
lines.append("")
|
|
163
|
+
lines += ["## Hand-written commands", "", "`databay <command> --help` lists the flags of each.", ""]
|
|
164
|
+
lines += ["| Command | What it does |", "|---|---|"]
|
|
165
|
+
lines += [f"| `databay {name}` | {_cell(entry[2])} |" for name, entry in sorted(handwritten.items())]
|
|
166
|
+
lines.append("")
|
|
167
|
+
return "\n".join(lines)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def errors_markdown(server_codes: dict[str, dict[str, Any]], pipe_codes: dict[str, dict[str, Any]]) -> str:
|
|
171
|
+
"""``errors.md``: the exit codes, the client's own codes, the server's codes and the pipe failure codes."""
|
|
172
|
+
lines = ["# databay errors", "", GENERATED_NOTE, ""]
|
|
173
|
+
lines += [
|
|
174
|
+
"When output is captured, a failed command prints one JSON object on stderr:",
|
|
175
|
+
'`{"error": {"code", "message", "hint", "next", "recovery", "context", ...}}`.',
|
|
176
|
+
"Read `recovery.action` first; the exit code is the coarse class.",
|
|
177
|
+
"",
|
|
178
|
+
"## Exit codes",
|
|
179
|
+
"",
|
|
180
|
+
"| Exit | Meaning |",
|
|
181
|
+
"|---|---|",
|
|
182
|
+
]
|
|
183
|
+
lines += [f"| {code} | {_cell(text)} |" for code, text in EXIT_CODES.items()]
|
|
184
|
+
lines += ["", "## Client codes", "", "| Code | Exit | Action | Side effects | Hint |", "|---|---|---|---|---|"]
|
|
185
|
+
for code, local in sorted(LOCAL_CODES.items()):
|
|
186
|
+
hint = server_codes[code]["hint"] if local.server_code else local.hint
|
|
187
|
+
lines.append(f"| `{code}` | {local.exit_code} | {local.action} | {local.side_effects} | {_cell(hint)} |")
|
|
188
|
+
lines += [
|
|
189
|
+
"",
|
|
190
|
+
"## Server codes",
|
|
191
|
+
"",
|
|
192
|
+
"| Code | Exit | Action | Side effects | Max retries | Hint |",
|
|
193
|
+
"|---|---|---|---|---|---|",
|
|
194
|
+
]
|
|
195
|
+
for code, info in sorted(server_codes.items()):
|
|
196
|
+
exit_code = LOCAL_CODES[code].exit_code if code in LOCAL_CODES else server_exit(code, info["action"])
|
|
197
|
+
lines.append(
|
|
198
|
+
f"| `{code}` | {exit_code} | {info['action']} | {info['side_effects']} | {_retries(info['max_retries'])} "
|
|
199
|
+
f"| {_cell(info['hint'])} |"
|
|
200
|
+
)
|
|
201
|
+
lines += [
|
|
202
|
+
"",
|
|
203
|
+
"## Pipe failure codes",
|
|
204
|
+
"",
|
|
205
|
+
"A pipe that fails records one of these in `error_code`; a wait that ends on the failure reports it.",
|
|
206
|
+
"",
|
|
207
|
+
"| Code | Action | Side effects | Max retries | Hint |",
|
|
208
|
+
"|---|---|---|---|---|",
|
|
209
|
+
]
|
|
210
|
+
for code, info in sorted(pipe_codes.items()):
|
|
211
|
+
retries = _retries(info["max_retries"])
|
|
212
|
+
lines.append(f"| `{code}` | {info['action']} | {info['side_effects']} | {retries} | {_cell(info['hint'])} |")
|
|
213
|
+
lines.append("")
|
|
214
|
+
return "\n".join(lines)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _retries(value: int | None) -> str:
|
|
218
|
+
return "" if value is None else str(value)
|
databay/resource.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
What every generated SDK resource (``databay.resources.<noun>``) does with a method call.
|
|
3
|
+
|
|
4
|
+
A method passes its arguments by wire name; an argument left at ``UNSET`` is not sent, so the
|
|
5
|
+
server's own default applies. A ``SecretStr`` is revealed only into the request body, and a
|
|
6
|
+
generated model is sent as its JSON.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import uuid
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
14
|
+
|
|
15
|
+
from databay import waits
|
|
16
|
+
from databay.errors import DatabayError, DatabayUsageError
|
|
17
|
+
from databay.invoke import CallValues, resolve_project, validate
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from databay.invoke import Session
|
|
21
|
+
from databay.operation import Operation
|
|
22
|
+
from databay.waits import WaitOutcome
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Unset:
|
|
26
|
+
"""The value of an argument the caller did not pass."""
|
|
27
|
+
|
|
28
|
+
_instance: Unset | None = None
|
|
29
|
+
|
|
30
|
+
def __new__(cls) -> Self:
|
|
31
|
+
if cls._instance is None:
|
|
32
|
+
cls._instance = super().__new__(cls)
|
|
33
|
+
return cls._instance # type: ignore[return-value]
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str:
|
|
36
|
+
return "UNSET"
|
|
37
|
+
|
|
38
|
+
def __bool__(self) -> bool:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
UNSET = Unset()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(slots=True)
|
|
46
|
+
class Waited[T]:
|
|
47
|
+
"""An answer and the wait that followed it."""
|
|
48
|
+
|
|
49
|
+
result: T
|
|
50
|
+
wait: WaitOutcome
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def wire_value(value: Any) -> Any: # noqa: ANN401 — any argument
|
|
54
|
+
"""An argument as JSON: a model as its fields, a ``SecretStr`` revealed, lists and dicts element-wise."""
|
|
55
|
+
from pydantic import BaseModel, SecretStr # noqa: PLC0415
|
|
56
|
+
|
|
57
|
+
if isinstance(value, SecretStr):
|
|
58
|
+
return value.get_secret_value()
|
|
59
|
+
if isinstance(value, BaseModel):
|
|
60
|
+
return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
|
|
61
|
+
if isinstance(value, list | tuple):
|
|
62
|
+
return [wire_value(item) for item in value]
|
|
63
|
+
if isinstance(value, dict):
|
|
64
|
+
return {key: wire_value(item) for key, item in value.items()}
|
|
65
|
+
return value
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _variant_body(op: Operation, body: Any) -> dict[str, Any]: # noqa: ANN401
|
|
69
|
+
assert op.body is not None
|
|
70
|
+
assert op.body.discriminator is not None
|
|
71
|
+
discriminator = op.body.discriminator
|
|
72
|
+
data = wire_value(body)
|
|
73
|
+
from pydantic import BaseModel # noqa: PLC0415
|
|
74
|
+
|
|
75
|
+
if isinstance(body, BaseModel):
|
|
76
|
+
data[discriminator] = wire_value(getattr(body, discriminator))
|
|
77
|
+
keys = [key for key, _, _ in op.body.variants]
|
|
78
|
+
if not isinstance(data, dict) or data.get(discriminator) not in keys:
|
|
79
|
+
holder = op.body.variant_field or "body"
|
|
80
|
+
raise DatabayUsageError(f"`{op.command}` needs a {holder} whose {discriminator} is one of {keys}")
|
|
81
|
+
return data
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def call_values(
|
|
85
|
+
op: Operation,
|
|
86
|
+
args: dict[str, Any],
|
|
87
|
+
*,
|
|
88
|
+
project: str | None,
|
|
89
|
+
idempotency_key: str | None,
|
|
90
|
+
body: Any = UNSET, # noqa: ANN401 — a variant model or its dict: the whole body, or its ``variant_field``
|
|
91
|
+
) -> CallValues:
|
|
92
|
+
"""The ``CallValues`` of an SDK call: each argument placed where its operation takes it."""
|
|
93
|
+
values = CallValues(project_flag=project, idempotency_key=idempotency_key)
|
|
94
|
+
project_name = op.project_location[1] if op.project_location else None
|
|
95
|
+
for param in op.params:
|
|
96
|
+
value = args.get(param.name, UNSET)
|
|
97
|
+
if value is UNSET or value is None:
|
|
98
|
+
continue
|
|
99
|
+
if param.name == project_name:
|
|
100
|
+
values.project_explicit = str(value)
|
|
101
|
+
continue
|
|
102
|
+
target = {"path": values.path, "query": values.query, "header": values.headers}[param.location]
|
|
103
|
+
target[param.name] = wire_value(value)
|
|
104
|
+
if op.body is None:
|
|
105
|
+
return values
|
|
106
|
+
if op.body.discriminator is not None and op.body.variant_field is None:
|
|
107
|
+
values.body = _variant_body(op, body)
|
|
108
|
+
values.has_body = True
|
|
109
|
+
return values
|
|
110
|
+
payload: dict[str, Any] = {}
|
|
111
|
+
for param in op.body.fields:
|
|
112
|
+
value = args.get(param.name, UNSET)
|
|
113
|
+
if value is UNSET:
|
|
114
|
+
continue
|
|
115
|
+
if param.name == project_name and value is not None:
|
|
116
|
+
values.project_explicit = str(value)
|
|
117
|
+
continue
|
|
118
|
+
payload[param.name] = wire_value(value)
|
|
119
|
+
if op.body.variant_field is not None:
|
|
120
|
+
payload[op.body.variant_field] = _variant_body(op, body)
|
|
121
|
+
values.body = payload
|
|
122
|
+
values.has_body = True
|
|
123
|
+
return values
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class Resource:
|
|
127
|
+
"""One noun's operations on a ``Session``; the generated subclasses add one method per verb."""
|
|
128
|
+
|
|
129
|
+
def __init__(self, session: Session) -> None:
|
|
130
|
+
self._session = session
|
|
131
|
+
|
|
132
|
+
def _call(
|
|
133
|
+
self,
|
|
134
|
+
op: Operation,
|
|
135
|
+
args: dict[str, Any],
|
|
136
|
+
*,
|
|
137
|
+
project: str | None = None,
|
|
138
|
+
idempotency_key: str | None = None,
|
|
139
|
+
body: Any = UNSET, # noqa: ANN401
|
|
140
|
+
wait: bool = False,
|
|
141
|
+
timeout: float | None = None,
|
|
142
|
+
) -> Any: # noqa: ANN401 — the operation's model, or a Waited of it
|
|
143
|
+
if op.idempotent and idempotency_key is None:
|
|
144
|
+
# One key per call, so this call's own retries never create twice.
|
|
145
|
+
idempotency_key = str(uuid.uuid4())
|
|
146
|
+
values = call_values(op, args, project=project, idempotency_key=idempotency_key, body=body)
|
|
147
|
+
if wait and op.resumes is not None:
|
|
148
|
+
project = resolve_project(op, values, self._session.active_project)
|
|
149
|
+
self._session.ensure_compatible(op)
|
|
150
|
+
bound = {**values.path, **values.query}
|
|
151
|
+
outcome = waits.run(self._session, op.resumes, bound, timeout_s=timeout, project=project)
|
|
152
|
+
return Waited(validate(op, outcome.status, op.response.model), outcome)
|
|
153
|
+
result = self._session.call(op, values)
|
|
154
|
+
if not wait:
|
|
155
|
+
return result.parsed
|
|
156
|
+
if op.wait is None:
|
|
157
|
+
raise DatabayUsageError(f"`{op.command}` has nothing to wait for")
|
|
158
|
+
bound = waits.bind(op.wait, result.raw, result.path)
|
|
159
|
+
if bound is None:
|
|
160
|
+
return Waited(result.parsed, waits.not_started(op.wait))
|
|
161
|
+
try:
|
|
162
|
+
outcome = waits.run(self._session, op.wait, bound, timeout_s=timeout, project=result.project)
|
|
163
|
+
except DatabayError as exc:
|
|
164
|
+
# The operation itself succeeded: the caller keeps its answer to resume or report.
|
|
165
|
+
exc.result = result.parsed
|
|
166
|
+
raise
|
|
167
|
+
return Waited(result.parsed, outcome)
|
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""``client.alerting_webhook``: the ``alerting-webhook`` operations.
|
|
2
|
+
|
|
3
|
+
Generated by python/tools/gen_client.py from contract/core.json; do not edit.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from databay.operations import alerting_webhook as _ops
|
|
11
|
+
from databay.resource import Resource, UNSET, Unset
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pydantic import SecretStr
|
|
15
|
+
|
|
16
|
+
from databay.models.alerting_webhook import AlertingWebhookListResponse, AlertingWebhookResponse
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AlertingWebhookResource(Resource):
|
|
20
|
+
|
|
21
|
+
def create(
|
|
22
|
+
self,
|
|
23
|
+
*,
|
|
24
|
+
name: str,
|
|
25
|
+
type: str,
|
|
26
|
+
url: SecretStr | str,
|
|
27
|
+
scope: str | None | Unset = UNSET,
|
|
28
|
+
tags: list[str] | None | Unset = UNSET,
|
|
29
|
+
project: str | None = None,
|
|
30
|
+
idempotency_key: str | None = None,
|
|
31
|
+
) -> AlertingWebhookResponse:
|
|
32
|
+
"""Create Alerting Webhook. ``databay alerting-webhook create``."""
|
|
33
|
+
return self._call(_ops.ALERTING_WEBHOOK_CREATE, {'name': name, 'scope': scope, 'tags': tags, 'type': type, 'url': url}, project=project, idempotency_key=idempotency_key)
|
|
34
|
+
|
|
35
|
+
def delete(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
webhook_uuid: str,
|
|
39
|
+
project: str | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Delete Alerting Webhook. ``databay alerting-webhook delete``."""
|
|
42
|
+
return self._call(_ops.ALERTING_WEBHOOK_DELETE, {'webhook_uuid': webhook_uuid}, project=project)
|
|
43
|
+
|
|
44
|
+
def get(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
webhook_uuid: str,
|
|
48
|
+
project: str | None = None,
|
|
49
|
+
) -> AlertingWebhookResponse:
|
|
50
|
+
"""Get Alerting Webhook. ``databay alerting-webhook get``."""
|
|
51
|
+
return self._call(_ops.ALERTING_WEBHOOK_GET, {'webhook_uuid': webhook_uuid}, project=project)
|
|
52
|
+
|
|
53
|
+
def list(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
project: str | None = None,
|
|
57
|
+
) -> AlertingWebhookListResponse:
|
|
58
|
+
"""Get Alerting Webhooks. ``databay alerting-webhook list``."""
|
|
59
|
+
return self._call(_ops.ALERTING_WEBHOOK_LIST, {}, project=project)
|
|
60
|
+
|
|
61
|
+
def notify(
|
|
62
|
+
self,
|
|
63
|
+
*,
|
|
64
|
+
body: str,
|
|
65
|
+
title: str,
|
|
66
|
+
tags: list[str] | Unset = UNSET,
|
|
67
|
+
project: str | None = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Notify. ``databay alerting-webhook notify``."""
|
|
70
|
+
return self._call(_ops.ALERTING_WEBHOOK_NOTIFY, {'tags': tags, 'body': body, 'title': title}, project=project)
|
|
71
|
+
|
|
72
|
+
def update(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
webhook_uuid: str,
|
|
76
|
+
name: str,
|
|
77
|
+
type: str,
|
|
78
|
+
url: SecretStr | str,
|
|
79
|
+
scope: str | None | Unset = UNSET,
|
|
80
|
+
tags: list[str] | None | Unset = UNSET,
|
|
81
|
+
project: str | None = None,
|
|
82
|
+
) -> AlertingWebhookResponse:
|
|
83
|
+
"""Update Alerting Webhook. ``databay alerting-webhook update``."""
|
|
84
|
+
return self._call(_ops.ALERTING_WEBHOOK_UPDATE, {'webhook_uuid': webhook_uuid, 'name': name, 'scope': scope, 'tags': tags, 'type': type, 'url': url}, project=project)
|