dataplat 0.2.3__tar.gz → 0.3.0__tar.gz
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.
- {dataplat-0.2.3 → dataplat-0.3.0}/CHANGELOG.md +86 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/PKG-INFO +107 -3
- {dataplat-0.2.3 → dataplat-0.3.0}/README.md +104 -1
- dataplat-0.3.0/dataplat/cli/_exit.py +68 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/_lazy.py +104 -3
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/bi/superset.py +26 -27
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/cloud/aws/_common.py +48 -3
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/cloud/aws/rds.py +27 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/cloud/aws/redshift.py +36 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/cloud/aws/secrets.py +90 -5
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/config.py +95 -7
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/db/__init__.py +20 -6
- dataplat-0.3.0/dataplat/cli/db/_common.py +266 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/db/dbt_orphans.py +43 -17
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/db/long_queries.py +6 -4
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/db/top_tables.py +2 -2
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/_common.py +8 -8
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/connections.py +21 -19
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/definitions.py +6 -9
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/tags.py +6 -9
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/templates.py +5 -8
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/workspaces.py +6 -9
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/status.py +153 -42
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/core/envrc.py +81 -10
- dataplat-0.3.0/dataplat/core/errors.py +94 -0
- dataplat-0.3.0/dataplat/core/registry.py +381 -0
- dataplat-0.3.0/dataplat/core/trace.py +332 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/main.py +22 -3
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/airbyte/client.py +95 -3
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/aws/auth.py +35 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/db/describe.py +29 -1
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/db/orphans.py +20 -4
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/superset/client.py +50 -1
- {dataplat-0.2.3 → dataplat-0.3.0}/pyproject.toml +14 -2
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_airbyte_commands.py +47 -6
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_aws_secrets.py +147 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_cli_smoke.py +282 -3
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_config.py +117 -0
- dataplat-0.3.0/tests/cli/test_db_common.py +414 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_db_long_queries.py +17 -2
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_db_query.py +132 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_dbt_orphans.py +35 -2
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_describe.py +15 -0
- dataplat-0.3.0/tests/cli/test_exit.py +129 -0
- dataplat-0.3.0/tests/cli/test_rds.py +1001 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_redshift.py +71 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_status.py +254 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_superset.py +64 -4
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_top_tables.py +8 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/core/test_envrc.py +82 -0
- dataplat-0.3.0/tests/core/test_errors.py +128 -0
- dataplat-0.3.0/tests/core/test_registry.py +495 -0
- dataplat-0.3.0/tests/core/test_trace.py +373 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/integration/conftest.py +17 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/integration/test_describe_pg.py +108 -10
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/integration/test_harness.py +94 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/integration/test_orphans_pg.py +96 -1
- dataplat-0.3.0/tests/services/airbyte/test_client.py +265 -0
- dataplat-0.3.0/tests/services/aws/test_auth.py +290 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/db/test_describe.py +35 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/db/test_orphans.py +58 -0
- dataplat-0.3.0/tests/services/superset/test_client.py +144 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/uv.lock +1 -1
- dataplat-0.2.3/dataplat/cli/db/_common.py +0 -136
- dataplat-0.2.3/dataplat/core/errors.py +0 -23
- dataplat-0.2.3/dataplat/core/registry.py +0 -110
- dataplat-0.2.3/tests/cli/test_db_common.py +0 -130
- dataplat-0.2.3/tests/cli/test_rds.py +0 -297
- dataplat-0.2.3/tests/core/test_registry.py +0 -96
- dataplat-0.2.3/tests/services/airbyte/test_client.py +0 -43
- dataplat-0.2.3/tests/services/aws/test_auth.py +0 -107
- dataplat-0.2.3/tests/services/superset/test_client.py +0 -54
- {dataplat-0.2.3 → dataplat-0.3.0}/.github/workflows/ci.yml +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/.github/workflows/release.yml +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/.gitignore +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/.python-version +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/CONTRIBUTING.md +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/LICENSE +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/_missing.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/_options.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/_prompt.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/_render.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/bi/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/bi/app.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ci/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ci/app.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ci/github/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ci/github/app.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ci/github/runner.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/cloud/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/cloud/app.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/cloud/aws/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/cloud/aws/app.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/db/_report.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/db/describe.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/db/role.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/db/role_create.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/db/role_drop.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/db/role_list.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ingest/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/_cursor.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/_resource.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/app.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/destinations.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/enums.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/jobs.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/sources.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/tui.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/ingest/app.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/cli/open.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/core/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/core/deps.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/airbyte/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/airbyte/_resource.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/airbyte/connections.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/airbyte/definitions.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/airbyte/destinations.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/airbyte/jobs.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/airbyte/sources.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/airbyte/tags.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/airbyte/workspaces.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/aws/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/db/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/db/_like.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/db/connection.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/db/long_queries.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/db/role.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/db/role_admin.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/db/role_dialects.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/db/targets.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/db/top_tables.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/dataplat/services/superset/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_airbyte_cursor_logic.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_airbyte_guards.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_airbyte_tui.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_aws_secrets_write.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_github_runner.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_missing_deps.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_open.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_prompt.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_regression.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_render.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_role.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_role_create.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/cli/test_role_drop.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/conftest.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/core/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/core/test_deps.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/integration/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/integration/redshift/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/integration/redshift/conftest.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/integration/redshift/test_conformance.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/integration/redshift/test_harness.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/integration/test_long_queries_pg.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/integration/test_roles_pg.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/integration/test_top_tables_pg.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/airbyte/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/airbyte/test_connections.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/airbyte/test_definitions.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/airbyte/test_destinations.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/airbyte/test_jobs.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/airbyte/test_sources.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/airbyte/test_workspaces.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/aws/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/db/__init__.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/db/test_connection.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/db/test_long_queries.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/db/test_role.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/db/test_role_admin.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/db/test_role_dialects.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/db/test_targets.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/db/test_top_tables.py +0 -0
- {dataplat-0.2.3 → dataplat-0.3.0}/tests/services/superset/__init__.py +0 -0
|
@@ -1,5 +1,91 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Exit codes now say what went wrong.** Every failure used to be `1`, so a
|
|
8
|
+
wrapper script could not tell "your config is wrong" from "the warehouse is
|
|
9
|
+
down" — and only one of those is worth retrying. Typed failures now carry
|
|
10
|
+
their own code: `2` invalid input, `3` configuration, `4` authentication,
|
|
11
|
+
`5` external service. `0`, `1` and `2` keep their conventional meanings, and
|
|
12
|
+
`2` is deliberately shared with Click's own usage error, because
|
|
13
|
+
`--format nope` and `-t nosuchtarget` are one condition to the caller.
|
|
14
|
+
|
|
15
|
+
An unreachable warehouse exits `5`, since that is the retryable case. A bad
|
|
16
|
+
statement against a reachable server stays `1`: retrying a syntax error would
|
|
17
|
+
fail identically forever. Untyped failures and a declined confirmation also
|
|
18
|
+
stay `1`. The full table is in the README.
|
|
19
|
+
|
|
20
|
+
**Scripts that branch on a non-zero exit will see new numbers.** Anything
|
|
21
|
+
testing `== 1` for a config or auth problem needs updating.
|
|
22
|
+
|
|
23
|
+
- **`dp db dbt-orphans` is more aggressive.** Its "which models are live" query
|
|
24
|
+
interpolated the dbt project name into a `LIKE` pattern without escaping it,
|
|
25
|
+
and dbt project names are snake_case — so the `_` in `my_project` matched any
|
|
26
|
+
character and a *sibling* project's models (`my2project`) sharing the
|
|
27
|
+
`dbt_artifacts` schema were counted as live. The same applied to
|
|
28
|
+
`DP_DBT_INVOCATION_COMMAND`, where `_` is ordinary in a dbt selector.
|
|
29
|
+
|
|
30
|
+
The direction is the point: an over-large live set makes **fewer** objects look
|
|
31
|
+
orphaned. Escaping it shrinks the live set, so dbt-orphans will now rename —
|
|
32
|
+
and after the grace period drop — objects it previously left alone. Run
|
|
33
|
+
`dp db dbt-orphans` (dry-run is the default) and read the plan before you
|
|
34
|
+
`--no-dry-run` the first time after upgrading.
|
|
35
|
+
|
|
36
|
+
- **`dp status` runs its checks concurrently**: 40.7s to 10.4s with five
|
|
37
|
+
targets, one unreachable. Sections run in parallel and each database target is
|
|
38
|
+
probed in parallel within them, which is where the time actually went — a 10s
|
|
39
|
+
connect timeout per target was paid serially. Key order and section order are
|
|
40
|
+
unchanged, because both pools iterate the declared mapping rather than
|
|
41
|
+
completion order. The AWS section stays serial and last: it may hand the
|
|
42
|
+
terminal to an interactive `aws sso login`.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **`--verbose` (or `DP_VERBOSE=1`) shows what dataplat actually sent** — SQL
|
|
47
|
+
statements, HTTP requests with status and duration, and AWS service calls.
|
|
48
|
+
It writes to stderr only, so `--json` and `--format csv` stay pipeable, and
|
|
49
|
+
everything passes through a redactor: passwords, secret values, bearer tokens
|
|
50
|
+
and API keys are never traced. Parameter values and response bodies are not
|
|
51
|
+
traced either — those are the data, not the request.
|
|
52
|
+
|
|
53
|
+
- **Third-party command areas.** `dp` discovers areas declared in the
|
|
54
|
+
`dataplat.areas` entry-point group, so a package can add a command area
|
|
55
|
+
without a change here. Discovery reads only the entry-point metadata, never
|
|
56
|
+
imports the plugin, so `dp --version` and `dp --help` stay import-free and
|
|
57
|
+
fast. A plugin that fails to import warns on stderr and leaves the built-in
|
|
58
|
+
areas working; it cannot shadow a built-in area.
|
|
59
|
+
|
|
60
|
+
- `dp config doctor` warns when a loaded `.envrc` value still contains an
|
|
61
|
+
unexpanded `$VAR`. dataplat does not run a shell, so
|
|
62
|
+
`export PGHOST=$DB_HOST` loads the literal text — which then surfaces as a
|
|
63
|
+
baffling connection failure rather than as the configuration mistake it is.
|
|
64
|
+
|
|
65
|
+
- Shell completion is documented (`dp --install-completion`). It always worked;
|
|
66
|
+
the README never said so.
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- `dp db query --format json|csv` could emit output that would not parse. The
|
|
71
|
+
progress spinner painted to stdout, which was invisible while Rich only did
|
|
72
|
+
that for a real terminal — the frames are erased — but `FORCE_COLOR` makes
|
|
73
|
+
Rich treat a pipe as a terminal too, and then the escape sequences ended up in
|
|
74
|
+
the redirected file. The spinner now follows the same sink as the notices.
|
|
75
|
+
|
|
76
|
+
- `dp db describe` reported the owner's grant option two different ways
|
|
77
|
+
depending on whether you asked about a schema or a relation. PostgreSQL grants
|
|
78
|
+
an owner every grant option implicitly and never records it in the ACL, so
|
|
79
|
+
reading the ACL reported "cannot delegate" about a role that demonstrably can.
|
|
80
|
+
Schema privileges now agree with relation privileges.
|
|
81
|
+
|
|
82
|
+
- The `Operating System :: OS Independent` classifier was an overclaim and has
|
|
83
|
+
been narrowed. Four things break on Windows: `dp config init` creates a
|
|
84
|
+
symlink, the dependency auto-install re-execs through `os.execvp`, the runner
|
|
85
|
+
commands shell out to `docker` with a POSIX default workdir, and the
|
|
86
|
+
credentials file is written with a `0o600` mode Windows ignores — after which
|
|
87
|
+
dataplat reports its own file as insecurely permissioned. CI tests Linux only.
|
|
88
|
+
|
|
3
89
|
## 0.2.3
|
|
4
90
|
|
|
5
91
|
Redshift-only fixes. Nothing changes for PostgreSQL targets.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataplat
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: One command to manage any shape of data platform: databases, ingestion, BI, cloud, and CI.
|
|
5
5
|
Project-URL: Homepage, https://github.com/hanslemm/dataplat
|
|
6
6
|
Project-URL: Repository, https://github.com/hanslemm/dataplat
|
|
@@ -14,7 +14,8 @@ Classifier: Development Status :: 4 - Beta
|
|
|
14
14
|
Classifier: Environment :: Console
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
16
16
|
Classifier: Intended Audience :: System Administrators
|
|
17
|
-
Classifier: Operating System ::
|
|
17
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
21
|
Classifier: Topic :: Database
|
|
@@ -94,7 +95,9 @@ dp
|
|
|
94
95
|
|
|
95
96
|
## Installation
|
|
96
97
|
|
|
97
|
-
Requires Python 3.12 or newer.
|
|
98
|
+
Requires Python 3.12 or newer, on Linux or macOS. Windows is untested and parts
|
|
99
|
+
of it will not work: `dp config init` creates a symlink, the dependency
|
|
100
|
+
self-install re-execs the process, and `dp ci github runner` drives `docker`.
|
|
98
101
|
|
|
99
102
|
```bash
|
|
100
103
|
uv tool install "dataplat[all]" # recommended: everything
|
|
@@ -141,6 +144,23 @@ Either path only ever *adds* to your install: the command is pinned to the
|
|
|
141
144
|
tool underneath you, and it carries your existing extras along, so adding
|
|
142
145
|
`db` cannot drop an `ingest` you already had.
|
|
143
146
|
|
|
147
|
+
## Shell completion
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
dp --install-completion # detect the shell, write the script, hook it up
|
|
151
|
+
dp --show-completion # print it instead, and install it yourself
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Completion takes effect in the next shell. bash, zsh and fish are supported;
|
|
155
|
+
`--show-completion` is the escape hatch when your rc file is managed by
|
|
156
|
+
something else (Nix, chezmoi, a dotfiles repo) or when shell detection fails.
|
|
157
|
+
|
|
158
|
+
`dp <TAB>` is answered from the area names alone and imports nothing.
|
|
159
|
+
Completing *inside* an area has to import it, because the subcommands it owes
|
|
160
|
+
the shell are that area's own — so the first `dp db <TAB>` pays for psycopg, and
|
|
161
|
+
an area whose extra is not installed completes to nothing rather than offering
|
|
162
|
+
to install it mid-keystroke.
|
|
163
|
+
|
|
144
164
|
## Quick start
|
|
145
165
|
|
|
146
166
|
1. Declare your database targets — any names you like:
|
|
@@ -203,6 +223,7 @@ rely only on the global link you chose.
|
|
|
203
223
|
| --- | --- |
|
|
204
224
|
| `DP_ENVRC_PATH` | Explicit `.envrc` to load, ahead of every other candidate. |
|
|
205
225
|
| `DP_ENVRC_ALLOW_CWD` | Set to `0` to stop picking up `.envrc` from the current directory. |
|
|
226
|
+
| `DP_VERBOSE` | Set to `1` to trace every statement and request to stderr, for a whole session — same switch as `--verbose`. |
|
|
206
227
|
| `DP_TARGETS` | Comma-separated DB target names (e.g. `warehouse,lake`). |
|
|
207
228
|
| `DP_DEFAULT_TARGET` | Target used when `--target` is omitted (default: first of `DP_TARGETS`). |
|
|
208
229
|
| `<NAME>_ENGINE` | `postgresql` (default) or `redshift`, per target. |
|
|
@@ -231,6 +252,89 @@ rely only on the global link you chose.
|
|
|
231
252
|
- **`--limit/-n`** — row caps share one spelling everywhere.
|
|
232
253
|
- **Secrets stay off argv** — prefer `--value-stdin` / hidden prompts; values
|
|
233
254
|
are never echoed back.
|
|
255
|
+
- **`--verbose`** — a root flag: show what the tool actually sent, on stderr.
|
|
256
|
+
|
|
257
|
+
### Exit codes
|
|
258
|
+
|
|
259
|
+
Exit codes are a contract, not an implementation detail — a wrapper script
|
|
260
|
+
branches on them long after it has stopped reading our output:
|
|
261
|
+
|
|
262
|
+
| Code | Meaning | Retry? |
|
|
263
|
+
| --- | --- | --- |
|
|
264
|
+
| `0` | Success. | — |
|
|
265
|
+
| `1` | Unexpected or not-yet-classified failure. Also a declined confirmation: "no" is not an error, but it is not "done" either. | No — you don't know what happened. |
|
|
266
|
+
| `2` | Invalid input: an unknown flag or target, a value that cannot be parsed, a combination of arguments that cannot work. | No — the command itself is wrong. |
|
|
267
|
+
| `3` | Configuration problem: missing connection settings, an unknown engine, an unset `AIRBYTE_BASE_URL` or `DP_DBT_PROJECT`. | No — a human has to fix the config. |
|
|
268
|
+
| `4` | Authentication failure: credentials rejected, a login endpoint that would not authenticate, `aws sso login` failed. | No — a new credential is needed. |
|
|
269
|
+
| `5` | External service failure: a call to Airbyte, Superset or AWS failed, timed out or returned something unusable; a warehouse that refused the operation. | **Yes** — the only class where a retry can help. |
|
|
270
|
+
|
|
271
|
+
`0`, `1` and `2` keep their conventional meanings. `2` is Click's own code for a
|
|
272
|
+
usage error, which is why invalid input shares it: `dp db query --format nope`
|
|
273
|
+
(Click's complaint) and `-t nosuchtarget` (ours) are one condition to the
|
|
274
|
+
caller — "you passed something I cannot use" — and splitting them by who noticed
|
|
275
|
+
would be a distinction with no use.
|
|
276
|
+
|
|
277
|
+
The point of the codes above `2` is that `5` is the one worth retrying, and `3`
|
|
278
|
+
and `4` are the ones you must never retry: no amount of sleeping and trying
|
|
279
|
+
again creates a missing config file or repairs a rejected password. Cap the
|
|
280
|
+
retries anyway — `5` means "the other end failed", which covers a warehouse
|
|
281
|
+
that was restarting *and* a `DROP` the server refused because something still
|
|
282
|
+
depends on it, and only the first of those gets better on its own.
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
dp db long-queries -t warehouse --json > queries.json
|
|
286
|
+
case $? in
|
|
287
|
+
0) ;;
|
|
288
|
+
5) echo "service unavailable; will retry" >&2; exit 75 ;; # EX_TEMPFAIL
|
|
289
|
+
*) echo "not retryable; fix and re-run" >&2; exit 1 ;;
|
|
290
|
+
esac
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Treat `1` as "unknown", never as "retryable": it is the code for a failure
|
|
294
|
+
dataplat has not classified, so retrying it is a guess.
|
|
295
|
+
|
|
296
|
+
### Verbose tracing
|
|
297
|
+
|
|
298
|
+
`--verbose` (or `DP_VERBOSE=1`) answers the one question logs cannot: what did
|
|
299
|
+
`dp` actually send?
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
dp --verbose db query 'SELECT 1' # root flag, before the subcommand
|
|
303
|
+
DP_VERBOSE=1 dp db long-queries 2> trace.log # or for a whole session
|
|
304
|
+
dp --verbose db describe public 2>&1 >/dev/null | grep '\[dp:sql\]'
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Every line is prefixed with its category — `[dp:sql]` or `[dp:http]` — and
|
|
308
|
+
collapsed onto one line, so the output greps cleanly:
|
|
309
|
+
|
|
310
|
+
```text
|
|
311
|
+
[dp:sql] connect me@db.example.com:5432/analytics engine=postgresql
|
|
312
|
+
[dp:sql] SELECT 1 FROM pg_namespace WHERE nspname = %s | 1 params bound
|
|
313
|
+
[dp:http] GET https://api.airbyte.com/v1/jobs?limit=20
|
|
314
|
+
[dp:http] GET https://api.airbyte.com/v1/jobs?limit=20 -> 200 143.8ms
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
SQL is traced *before* the statement runs, which is the point: the trace you
|
|
318
|
+
need is the one for the query that never came back, and a line written afterwards
|
|
319
|
+
would never be written at all. That is also why there is no duration on it — use
|
|
320
|
+
`dp db long-queries` for how long. HTTP gets two lines for the same reason, one
|
|
321
|
+
on the way out and one on the response; a line with no `-> status` partner *is*
|
|
322
|
+
the signal that a request hung, was refused, or never connected.
|
|
323
|
+
|
|
324
|
+
**It writes to stderr and never to stdout**, so `--json` and `--format csv` stay
|
|
325
|
+
machine-readable with tracing on. Piping into `jq` is still valid, and
|
|
326
|
+
`2>/dev/null` drops the trace without touching the data:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
dp --verbose db query --format json 'SELECT 1' 2>/dev/null | jq
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Secrets are never traced.** Every message is redacted on the way out —
|
|
333
|
+
passwords (including the SQL `PASSWORD '…'` literal that role creation sends),
|
|
334
|
+
tokens, API keys, `Authorization` headers and credentials embedded in a URL all
|
|
335
|
+
become `***`. Parameter values, result rows and response bodies are not traced
|
|
336
|
+
at all: they are your warehouse's data, and a trace that scrolls the answer past
|
|
337
|
+
you has hidden the request it exists to show.
|
|
234
338
|
|
|
235
339
|
## Examples
|
|
236
340
|
|
|
@@ -49,7 +49,9 @@ dp
|
|
|
49
49
|
|
|
50
50
|
## Installation
|
|
51
51
|
|
|
52
|
-
Requires Python 3.12 or newer.
|
|
52
|
+
Requires Python 3.12 or newer, on Linux or macOS. Windows is untested and parts
|
|
53
|
+
of it will not work: `dp config init` creates a symlink, the dependency
|
|
54
|
+
self-install re-execs the process, and `dp ci github runner` drives `docker`.
|
|
53
55
|
|
|
54
56
|
```bash
|
|
55
57
|
uv tool install "dataplat[all]" # recommended: everything
|
|
@@ -96,6 +98,23 @@ Either path only ever *adds* to your install: the command is pinned to the
|
|
|
96
98
|
tool underneath you, and it carries your existing extras along, so adding
|
|
97
99
|
`db` cannot drop an `ingest` you already had.
|
|
98
100
|
|
|
101
|
+
## Shell completion
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
dp --install-completion # detect the shell, write the script, hook it up
|
|
105
|
+
dp --show-completion # print it instead, and install it yourself
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Completion takes effect in the next shell. bash, zsh and fish are supported;
|
|
109
|
+
`--show-completion` is the escape hatch when your rc file is managed by
|
|
110
|
+
something else (Nix, chezmoi, a dotfiles repo) or when shell detection fails.
|
|
111
|
+
|
|
112
|
+
`dp <TAB>` is answered from the area names alone and imports nothing.
|
|
113
|
+
Completing *inside* an area has to import it, because the subcommands it owes
|
|
114
|
+
the shell are that area's own — so the first `dp db <TAB>` pays for psycopg, and
|
|
115
|
+
an area whose extra is not installed completes to nothing rather than offering
|
|
116
|
+
to install it mid-keystroke.
|
|
117
|
+
|
|
99
118
|
## Quick start
|
|
100
119
|
|
|
101
120
|
1. Declare your database targets — any names you like:
|
|
@@ -158,6 +177,7 @@ rely only on the global link you chose.
|
|
|
158
177
|
| --- | --- |
|
|
159
178
|
| `DP_ENVRC_PATH` | Explicit `.envrc` to load, ahead of every other candidate. |
|
|
160
179
|
| `DP_ENVRC_ALLOW_CWD` | Set to `0` to stop picking up `.envrc` from the current directory. |
|
|
180
|
+
| `DP_VERBOSE` | Set to `1` to trace every statement and request to stderr, for a whole session — same switch as `--verbose`. |
|
|
161
181
|
| `DP_TARGETS` | Comma-separated DB target names (e.g. `warehouse,lake`). |
|
|
162
182
|
| `DP_DEFAULT_TARGET` | Target used when `--target` is omitted (default: first of `DP_TARGETS`). |
|
|
163
183
|
| `<NAME>_ENGINE` | `postgresql` (default) or `redshift`, per target. |
|
|
@@ -186,6 +206,89 @@ rely only on the global link you chose.
|
|
|
186
206
|
- **`--limit/-n`** — row caps share one spelling everywhere.
|
|
187
207
|
- **Secrets stay off argv** — prefer `--value-stdin` / hidden prompts; values
|
|
188
208
|
are never echoed back.
|
|
209
|
+
- **`--verbose`** — a root flag: show what the tool actually sent, on stderr.
|
|
210
|
+
|
|
211
|
+
### Exit codes
|
|
212
|
+
|
|
213
|
+
Exit codes are a contract, not an implementation detail — a wrapper script
|
|
214
|
+
branches on them long after it has stopped reading our output:
|
|
215
|
+
|
|
216
|
+
| Code | Meaning | Retry? |
|
|
217
|
+
| --- | --- | --- |
|
|
218
|
+
| `0` | Success. | — |
|
|
219
|
+
| `1` | Unexpected or not-yet-classified failure. Also a declined confirmation: "no" is not an error, but it is not "done" either. | No — you don't know what happened. |
|
|
220
|
+
| `2` | Invalid input: an unknown flag or target, a value that cannot be parsed, a combination of arguments that cannot work. | No — the command itself is wrong. |
|
|
221
|
+
| `3` | Configuration problem: missing connection settings, an unknown engine, an unset `AIRBYTE_BASE_URL` or `DP_DBT_PROJECT`. | No — a human has to fix the config. |
|
|
222
|
+
| `4` | Authentication failure: credentials rejected, a login endpoint that would not authenticate, `aws sso login` failed. | No — a new credential is needed. |
|
|
223
|
+
| `5` | External service failure: a call to Airbyte, Superset or AWS failed, timed out or returned something unusable; a warehouse that refused the operation. | **Yes** — the only class where a retry can help. |
|
|
224
|
+
|
|
225
|
+
`0`, `1` and `2` keep their conventional meanings. `2` is Click's own code for a
|
|
226
|
+
usage error, which is why invalid input shares it: `dp db query --format nope`
|
|
227
|
+
(Click's complaint) and `-t nosuchtarget` (ours) are one condition to the
|
|
228
|
+
caller — "you passed something I cannot use" — and splitting them by who noticed
|
|
229
|
+
would be a distinction with no use.
|
|
230
|
+
|
|
231
|
+
The point of the codes above `2` is that `5` is the one worth retrying, and `3`
|
|
232
|
+
and `4` are the ones you must never retry: no amount of sleeping and trying
|
|
233
|
+
again creates a missing config file or repairs a rejected password. Cap the
|
|
234
|
+
retries anyway — `5` means "the other end failed", which covers a warehouse
|
|
235
|
+
that was restarting *and* a `DROP` the server refused because something still
|
|
236
|
+
depends on it, and only the first of those gets better on its own.
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
dp db long-queries -t warehouse --json > queries.json
|
|
240
|
+
case $? in
|
|
241
|
+
0) ;;
|
|
242
|
+
5) echo "service unavailable; will retry" >&2; exit 75 ;; # EX_TEMPFAIL
|
|
243
|
+
*) echo "not retryable; fix and re-run" >&2; exit 1 ;;
|
|
244
|
+
esac
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Treat `1` as "unknown", never as "retryable": it is the code for a failure
|
|
248
|
+
dataplat has not classified, so retrying it is a guess.
|
|
249
|
+
|
|
250
|
+
### Verbose tracing
|
|
251
|
+
|
|
252
|
+
`--verbose` (or `DP_VERBOSE=1`) answers the one question logs cannot: what did
|
|
253
|
+
`dp` actually send?
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
dp --verbose db query 'SELECT 1' # root flag, before the subcommand
|
|
257
|
+
DP_VERBOSE=1 dp db long-queries 2> trace.log # or for a whole session
|
|
258
|
+
dp --verbose db describe public 2>&1 >/dev/null | grep '\[dp:sql\]'
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Every line is prefixed with its category — `[dp:sql]` or `[dp:http]` — and
|
|
262
|
+
collapsed onto one line, so the output greps cleanly:
|
|
263
|
+
|
|
264
|
+
```text
|
|
265
|
+
[dp:sql] connect me@db.example.com:5432/analytics engine=postgresql
|
|
266
|
+
[dp:sql] SELECT 1 FROM pg_namespace WHERE nspname = %s | 1 params bound
|
|
267
|
+
[dp:http] GET https://api.airbyte.com/v1/jobs?limit=20
|
|
268
|
+
[dp:http] GET https://api.airbyte.com/v1/jobs?limit=20 -> 200 143.8ms
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
SQL is traced *before* the statement runs, which is the point: the trace you
|
|
272
|
+
need is the one for the query that never came back, and a line written afterwards
|
|
273
|
+
would never be written at all. That is also why there is no duration on it — use
|
|
274
|
+
`dp db long-queries` for how long. HTTP gets two lines for the same reason, one
|
|
275
|
+
on the way out and one on the response; a line with no `-> status` partner *is*
|
|
276
|
+
the signal that a request hung, was refused, or never connected.
|
|
277
|
+
|
|
278
|
+
**It writes to stderr and never to stdout**, so `--json` and `--format csv` stay
|
|
279
|
+
machine-readable with tracing on. Piping into `jq` is still valid, and
|
|
280
|
+
`2>/dev/null` drops the trace without touching the data:
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
dp --verbose db query --format json 'SELECT 1' 2>/dev/null | jq
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Secrets are never traced.** Every message is redacted on the way out —
|
|
287
|
+
passwords (including the SQL `PASSWORD '…'` literal that role creation sends),
|
|
288
|
+
tokens, API keys, `Authorization` headers and credentials embedded in a URL all
|
|
289
|
+
become `***`. Parameter values, result rows and response bodies are not traced
|
|
290
|
+
at all: they are your warehouse's data, and a trace that scrolls the answer past
|
|
291
|
+
you has hidden the request it exists to show.
|
|
189
292
|
|
|
190
293
|
## Examples
|
|
191
294
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""The one way a command reports a typed error and stops.
|
|
2
|
+
|
|
3
|
+
Every catch site in the tree had grown the same two lines — print red with an
|
|
4
|
+
``Error: `` prefix, then ``raise typer.Exit(code=1)`` — and the code was the
|
|
5
|
+
part that was wrong: a missing config file, a rejected password and an
|
|
6
|
+
unreachable warehouse all exited 1, so a script could tell that something broke
|
|
7
|
+
and nothing else. :func:`fail` keeps the printing byte-for-byte and takes the
|
|
8
|
+
number from the exception, which is the only thing that knows what happened.
|
|
9
|
+
|
|
10
|
+
It does not branch on the error type, deliberately. A ``type -> code`` chain
|
|
11
|
+
here would be a second source of truth that a new error class could be added
|
|
12
|
+
without, and the first symptom would be a wrong exit code in someone's CI. The
|
|
13
|
+
class attribute in :mod:`dataplat.core.errors` is the whole mechanism; this
|
|
14
|
+
function is the plumbing.
|
|
15
|
+
|
|
16
|
+
Two details that are load-bearing:
|
|
17
|
+
|
|
18
|
+
- the message goes through :func:`dataplat.cli._render.esc`, because exception
|
|
19
|
+
text quotes warehouse rows and API response bodies verbatim, and a stray
|
|
20
|
+
``[/x]`` in either one raises ``MarkupError`` mid-render — turning a handled
|
|
21
|
+
error into a traceback;
|
|
22
|
+
- output goes to *stdout*, where this codebase's errors have always gone. That
|
|
23
|
+
is parity, not a claim that it is right: moving diagnostics to stderr is what
|
|
24
|
+
:mod:`dataplat.core.trace` does for tracing, and doing the same for errors
|
|
25
|
+
would change what every existing CliRunner assertion sees. If it happens, it
|
|
26
|
+
happens here, once, on purpose.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from typing import NoReturn
|
|
32
|
+
|
|
33
|
+
import typer
|
|
34
|
+
from rich.console import Console
|
|
35
|
+
|
|
36
|
+
from dataplat.cli._render import esc
|
|
37
|
+
from dataplat.core.errors import DataplatError, ExitCode
|
|
38
|
+
|
|
39
|
+
__all__ = ["exit_code_for", "fail"]
|
|
40
|
+
|
|
41
|
+
_console = Console()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def exit_code_for(exc: BaseException) -> ExitCode:
|
|
45
|
+
"""Return the exit code ``exc`` should produce, without exiting.
|
|
46
|
+
|
|
47
|
+
A :class:`~dataplat.core.errors.DataplatError` answers for itself; anything
|
|
48
|
+
else is unclassified and therefore :attr:`~dataplat.core.errors.ExitCode.
|
|
49
|
+
FAILURE`. Split out from :func:`fail` for the callers that need the number
|
|
50
|
+
but not the exit — logging it, recording it in a summary table, or exiting
|
|
51
|
+
through their own path (a TUI cannot raise ``typer.Exit`` at an arbitrary
|
|
52
|
+
point and expect it to mean anything).
|
|
53
|
+
"""
|
|
54
|
+
if isinstance(exc, DataplatError):
|
|
55
|
+
return exc.exit_code
|
|
56
|
+
return ExitCode.FAILURE
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def fail(exc: DataplatError, *, console: Console | None = None) -> NoReturn:
|
|
60
|
+
"""Print ``exc`` as an error and exit with the code it declares.
|
|
61
|
+
|
|
62
|
+
Pass ``console`` where the command already owns one, so the error lands in
|
|
63
|
+
the same stream the rest of its output did.
|
|
64
|
+
"""
|
|
65
|
+
(console or _console).print(f"[red]Error: {esc(exc)}[/red]")
|
|
66
|
+
# exit_code_for rather than exc.exit_code: identical for every typed error,
|
|
67
|
+
# and total for the untyped one a caller slips past the annotation.
|
|
68
|
+
raise typer.Exit(code=exit_code_for(exc))
|
|
@@ -16,6 +16,12 @@ leave psycopg unimported). Completing *inside* an area — ``dp db <TAB>`` — d
|
|
|
16
16
|
import it, because the subcommand list it has to offer is the area's own; that
|
|
17
17
|
is the one non-dispatch path that pays, and there is no way around it.
|
|
18
18
|
|
|
19
|
+
Plugin areas are mounted lazily too, one level up: ``main`` mounts only the
|
|
20
|
+
built-ins (their names and help text are constants), and :class:`LazyRootGroup`
|
|
21
|
+
adds a placeholder per plugin area the first time click asks for the command
|
|
22
|
+
surface. That keeps the entry-point scan off ``dp --version``, which never looks
|
|
23
|
+
at the command list, and off ``dp db …``, whose name a plugin cannot claim.
|
|
24
|
+
|
|
19
25
|
No direct click import here either — typer vendors click, so only the
|
|
20
26
|
``TyperGroup`` surface is safe to rely on (see :mod:`dataplat.cli._missing`).
|
|
21
27
|
"""
|
|
@@ -29,7 +35,17 @@ from typer.core import TyperGroup
|
|
|
29
35
|
from typer.main import get_group
|
|
30
36
|
|
|
31
37
|
from dataplat.core.deps import ready
|
|
32
|
-
from dataplat.core.
|
|
38
|
+
from dataplat.core.errors import ExitCode
|
|
39
|
+
from dataplat.core.registry import (
|
|
40
|
+
AreaMount,
|
|
41
|
+
area_by_name,
|
|
42
|
+
is_builtin,
|
|
43
|
+
load_app,
|
|
44
|
+
mount_help,
|
|
45
|
+
plugin_areas,
|
|
46
|
+
warn_plugin,
|
|
47
|
+
warn_plugin_failed,
|
|
48
|
+
)
|
|
33
49
|
|
|
34
50
|
__all__ = [
|
|
35
51
|
"AreaPlaceholderGroup",
|
|
@@ -57,6 +73,31 @@ def area_placeholder(mount: AreaMount) -> typer.Typer:
|
|
|
57
73
|
)
|
|
58
74
|
|
|
59
75
|
|
|
76
|
+
def _load_or_diagnose(mount: AreaMount) -> Any:
|
|
77
|
+
""":func:`load_app`, but a third-party area's import failure is news.
|
|
78
|
+
|
|
79
|
+
A built-in that will not import is a bug in dp, and the traceback is the bug
|
|
80
|
+
report — swallowing it would hide it behind a sentence. A plugin that raises
|
|
81
|
+
on import is a fact about the user's environment, and the useful output is
|
|
82
|
+
which area, from which target, failed how. Either way the blast radius is one
|
|
83
|
+
area: laziness means nothing else in the CLI has imported it, so ``dp
|
|
84
|
+
--help`` and every other area keep working.
|
|
85
|
+
|
|
86
|
+
``Exception``, not ``BaseException``: a plugin that calls ``sys.exit()`` or is
|
|
87
|
+
interrupted mid-import is asking to stop the process, not to be diagnosed.
|
|
88
|
+
"""
|
|
89
|
+
if is_builtin(mount):
|
|
90
|
+
# Unguarded on purpose — see above.
|
|
91
|
+
return load_app(mount)
|
|
92
|
+
try:
|
|
93
|
+
return load_app(mount)
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
warn_plugin_failed(mount, exc)
|
|
96
|
+
# Unclassified failure: a broken third-party package is not invalid
|
|
97
|
+
# input, not our configuration, and not a service that answered badly.
|
|
98
|
+
raise typer.Exit(code=ExitCode.FAILURE)
|
|
99
|
+
|
|
100
|
+
|
|
60
101
|
def area_command(mount: AreaMount) -> typer.Typer:
|
|
61
102
|
"""``mount``'s real Typer app, or the stub that offers to install its extra.
|
|
62
103
|
|
|
@@ -65,7 +106,7 @@ def area_command(mount: AreaMount) -> typer.Typer:
|
|
|
65
106
|
``mount.deps``, so an area that is not one of the built-ins still works.
|
|
66
107
|
"""
|
|
67
108
|
if mount.deps is None or ready(mount.deps):
|
|
68
|
-
return
|
|
109
|
+
return _load_or_diagnose(mount)
|
|
69
110
|
# Imported on this branch only: the stub pulls in subprocess, which costs
|
|
70
111
|
# more to import (~5 ms) than every readiness check in the CLI together.
|
|
71
112
|
from dataplat.cli._missing import build_missing_deps_app
|
|
@@ -74,7 +115,67 @@ def area_command(mount: AreaMount) -> typer.Typer:
|
|
|
74
115
|
|
|
75
116
|
|
|
76
117
|
class LazyRootGroup(TyperGroup):
|
|
77
|
-
"""Root group that resolves an area's real app the first time it is used.
|
|
118
|
+
"""Root group that resolves an area's real app the first time it is used.
|
|
119
|
+
|
|
120
|
+
It also owns *when* plugin areas appear: ``main`` mounts the built-ins, and
|
|
121
|
+
the two overrides below add the plugin placeholders the moment click needs a
|
|
122
|
+
command surface that could contain one.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
# Class-level default, set per instance on first use: an instance attribute
|
|
126
|
+
# avoids overriding TyperGroup.__init__ just to hold one bool.
|
|
127
|
+
_plugins_mounted: bool = False
|
|
128
|
+
|
|
129
|
+
def _mount_plugins(self) -> None:
|
|
130
|
+
"""Mount a placeholder for every plugin area, once per group.
|
|
131
|
+
|
|
132
|
+
The flag is set *before* discovery, not after: a scan that warned about a
|
|
133
|
+
broken plugin must not repeat itself on the next lookup, and several
|
|
134
|
+
lookups happen in one invocation.
|
|
135
|
+
"""
|
|
136
|
+
if self._plugins_mounted:
|
|
137
|
+
return
|
|
138
|
+
self._plugins_mounted = True
|
|
139
|
+
for mount in plugin_areas():
|
|
140
|
+
if mount.name in self.commands:
|
|
141
|
+
# The registry already refuses a built-in *area*'s name; what is
|
|
142
|
+
# left is the rest of the root surface (config, status, open),
|
|
143
|
+
# which only the group knows about. Refused for the same reason:
|
|
144
|
+
# `dp status` must keep meaning `dp status`.
|
|
145
|
+
warn_plugin(
|
|
146
|
+
f"ignoring plugin area {mount.name!r}: "
|
|
147
|
+
f"dp {mount.name} is already a command"
|
|
148
|
+
)
|
|
149
|
+
continue
|
|
150
|
+
# Same construction as a built-in placeholder, so plugin areas are
|
|
151
|
+
# indistinguishable downstream — including in --help, "did you mean"
|
|
152
|
+
# and completion, which read the group's commands and nothing else.
|
|
153
|
+
self.commands[mount.name] = get_group(area_placeholder(mount))
|
|
154
|
+
|
|
155
|
+
def list_commands(self, ctx: Any) -> list[str]:
|
|
156
|
+
"""Every command name, plugin areas included.
|
|
157
|
+
|
|
158
|
+
--help, "did you mean" and top-level completion all come through here, so
|
|
159
|
+
this is where the scan is genuinely owed: the answer is a list of names,
|
|
160
|
+
and a plugin area's name belongs in it.
|
|
161
|
+
"""
|
|
162
|
+
self._mount_plugins()
|
|
163
|
+
return super().list_commands(ctx)
|
|
164
|
+
|
|
165
|
+
def get_command(self, ctx: Any, name: str) -> Any:
|
|
166
|
+
"""Look up one command, discovering plugins only if nothing claims it.
|
|
167
|
+
|
|
168
|
+
A name the built-ins already answer is returned without a scan, and that
|
|
169
|
+
is sound rather than a shortcut: a plugin cannot claim a built-in area's
|
|
170
|
+
name (the registry refuses it) nor an existing root command's (above), so
|
|
171
|
+
discovery could not change this answer. It is what keeps ``dp db query``
|
|
172
|
+
as cheap as it was before plugins existed.
|
|
173
|
+
"""
|
|
174
|
+
command = super().get_command(ctx, name)
|
|
175
|
+
if command is not None:
|
|
176
|
+
return command
|
|
177
|
+
self._mount_plugins()
|
|
178
|
+
return super().get_command(ctx, name)
|
|
78
179
|
|
|
79
180
|
def resolve_command(self, ctx: Any, args: Any) -> Any:
|
|
80
181
|
"""Import the area behind a placeholder, now that it is needed.
|