dataplat 0.2.2__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.2 → dataplat-0.3.0}/CHANGELOG.md +123 -0
- dataplat-0.3.0/CONTRIBUTING.md +177 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/PKG-INFO +117 -3
- {dataplat-0.2.2 → dataplat-0.3.0}/README.md +114 -1
- dataplat-0.3.0/dataplat/cli/_exit.py +68 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/_lazy.py +104 -3
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/bi/superset.py +26 -27
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/cloud/aws/_common.py +48 -3
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/cloud/aws/rds.py +27 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/cloud/aws/redshift.py +36 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/cloud/aws/secrets.py +90 -5
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/config.py +95 -7
- {dataplat-0.2.2 → 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.2 → dataplat-0.3.0}/dataplat/cli/db/dbt_orphans.py +43 -17
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/db/long_queries.py +6 -4
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/db/role.py +19 -9
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/db/top_tables.py +2 -2
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/_common.py +8 -8
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/connections.py +21 -19
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/definitions.py +6 -9
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/tags.py +6 -9
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/templates.py +5 -8
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/workspaces.py +6 -9
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/status.py +153 -42
- {dataplat-0.2.2 → 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.2 → dataplat-0.3.0}/dataplat/main.py +22 -3
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/airbyte/client.py +95 -3
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/aws/auth.py +35 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/db/describe.py +51 -8
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/db/orphans.py +20 -4
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/db/role.py +10 -2
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/superset/client.py +50 -1
- {dataplat-0.2.2 → dataplat-0.3.0}/pyproject.toml +21 -2
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_airbyte_commands.py +47 -6
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_aws_secrets.py +147 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_cli_smoke.py +282 -3
- {dataplat-0.2.2 → 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.2 → dataplat-0.3.0}/tests/cli/test_db_long_queries.py +17 -2
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_db_query.py +132 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_dbt_orphans.py +35 -2
- {dataplat-0.2.2 → 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.2 → dataplat-0.3.0}/tests/cli/test_redshift.py +71 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_status.py +254 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_superset.py +64 -4
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_top_tables.py +8 -0
- {dataplat-0.2.2 → 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.2 → dataplat-0.3.0}/tests/integration/conftest.py +17 -0
- dataplat-0.3.0/tests/integration/redshift/__init__.py +19 -0
- dataplat-0.3.0/tests/integration/redshift/conftest.py +1191 -0
- dataplat-0.3.0/tests/integration/redshift/test_conformance.py +281 -0
- dataplat-0.3.0/tests/integration/redshift/test_harness.py +415 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/integration/test_describe_pg.py +108 -10
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/integration/test_harness.py +94 -0
- {dataplat-0.2.2 → 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.2 → dataplat-0.3.0}/tests/services/db/test_describe.py +71 -3
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/db/test_orphans.py +58 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/db/test_role.py +38 -1
- dataplat-0.3.0/tests/services/superset/test_client.py +144 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/uv.lock +1 -1
- dataplat-0.2.2/dataplat/cli/db/_common.py +0 -136
- dataplat-0.2.2/dataplat/core/errors.py +0 -23
- dataplat-0.2.2/dataplat/core/registry.py +0 -110
- dataplat-0.2.2/tests/cli/test_db_common.py +0 -130
- dataplat-0.2.2/tests/cli/test_rds.py +0 -297
- dataplat-0.2.2/tests/core/test_registry.py +0 -96
- dataplat-0.2.2/tests/services/airbyte/test_client.py +0 -43
- dataplat-0.2.2/tests/services/aws/test_auth.py +0 -107
- dataplat-0.2.2/tests/services/superset/test_client.py +0 -54
- {dataplat-0.2.2 → dataplat-0.3.0}/.github/workflows/ci.yml +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/.github/workflows/release.yml +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/.gitignore +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/.python-version +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/LICENSE +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/_missing.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/_options.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/_prompt.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/_render.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/bi/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/bi/app.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ci/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ci/app.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ci/github/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ci/github/app.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ci/github/runner.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/cloud/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/cloud/app.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/cloud/aws/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/cloud/aws/app.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/db/_report.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/db/describe.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/db/role_create.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/db/role_drop.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/db/role_list.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ingest/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/_cursor.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/_resource.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/app.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/destinations.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/enums.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/jobs.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/sources.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ingest/airbyte/tui.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/ingest/app.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/cli/open.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/core/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/core/deps.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/airbyte/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/airbyte/_resource.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/airbyte/connections.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/airbyte/definitions.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/airbyte/destinations.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/airbyte/jobs.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/airbyte/sources.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/airbyte/tags.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/airbyte/workspaces.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/aws/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/db/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/db/_like.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/db/connection.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/db/long_queries.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/db/role_admin.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/db/role_dialects.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/db/targets.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/db/top_tables.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/dataplat/services/superset/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_airbyte_cursor_logic.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_airbyte_guards.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_airbyte_tui.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_aws_secrets_write.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_github_runner.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_missing_deps.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_open.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_prompt.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_regression.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_render.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_role.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_role_create.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/cli/test_role_drop.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/conftest.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/core/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/core/test_deps.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/integration/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/integration/test_long_queries_pg.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/integration/test_roles_pg.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/integration/test_top_tables_pg.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/airbyte/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/airbyte/test_connections.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/airbyte/test_definitions.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/airbyte/test_destinations.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/airbyte/test_jobs.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/airbyte/test_sources.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/airbyte/test_workspaces.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/aws/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/db/__init__.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/db/test_connection.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/db/test_long_queries.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/db/test_role_admin.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/db/test_role_dialects.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/db/test_targets.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/db/test_top_tables.py +0 -0
- {dataplat-0.2.2 → dataplat-0.3.0}/tests/services/superset/__init__.py +0 -0
|
@@ -1,5 +1,128 @@
|
|
|
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
|
+
|
|
89
|
+
## 0.2.3
|
|
90
|
+
|
|
91
|
+
Redshift-only fixes. Nothing changes for PostgreSQL targets.
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
- `dp db role show` no longer claims every Redshift user has no password. The
|
|
96
|
+
attribute query reported `password_set=False` unconditionally, but
|
|
97
|
+
`pg_user.passwd` is masked to `'********'` there just as `pg_roles.rolpassword`
|
|
98
|
+
is on PostgreSQL — so it asserted "this login has no password" for every user,
|
|
99
|
+
the same falsehood 0.2.2 fixed on the PostgreSQL side. It now reports
|
|
100
|
+
`unknown`, with the reason. A Redshift *group* still reports `no`, because a
|
|
101
|
+
group has no password to hold.
|
|
102
|
+
|
|
103
|
+
- `dp db describe <schema>` now reports `USAGE` grants on Redshift. The query
|
|
104
|
+
read `information_schema.usage_privileges` filtered to `object_type = 'SCHEMA'`,
|
|
105
|
+
which the SQL standard defines over domains, collations and sequences — never
|
|
106
|
+
schemas — so it returned nothing on every server. It now scans
|
|
107
|
+
`has_schema_privilege`, mirroring how the same query has always reported
|
|
108
|
+
`CREATE` on that path. As with `CREATE`, a privilege scan cannot report a
|
|
109
|
+
grantor or a grant option, so both stay empty; the PostgreSQL path reads the
|
|
110
|
+
ACL and does better on both counts.
|
|
111
|
+
|
|
112
|
+
Both fixes rest on documented behaviour and internal precedent rather than a
|
|
113
|
+
live cluster — Redshift cannot be containerized, so CI cannot cover it. See
|
|
114
|
+
below.
|
|
115
|
+
|
|
116
|
+
### Added
|
|
117
|
+
|
|
118
|
+
- A Redshift conformance harness (`tests/integration/redshift/`) for anyone who
|
|
119
|
+
runs dataplat against a real cluster. The read-only tier is safe to point at a
|
|
120
|
+
warehouse in use — a guard refuses anything that is not plainly a read before
|
|
121
|
+
it reaches the server — and it interrogates the assumptions the two fixes above
|
|
122
|
+
depend on, printing what your cluster answered. `CONTRIBUTING.md` documents
|
|
123
|
+
both tiers and the evidence rules for changing SQL that runs on a dialect CI
|
|
124
|
+
cannot reach.
|
|
125
|
+
|
|
3
126
|
## 0.2.2
|
|
4
127
|
|
|
5
128
|
Closes the six defects 0.2.1's integration suite found and pinned as expected
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Contributing to dataplat
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
git clone https://github.com/hanslemm/dataplat
|
|
7
|
+
cd dataplat
|
|
8
|
+
uv sync --group dev --all-extras
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Checks
|
|
12
|
+
|
|
13
|
+
The four gates CI runs, across Python 3.12 and 3.13:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv run pytest
|
|
17
|
+
uv run ruff check .
|
|
18
|
+
uv run ruff format --check .
|
|
19
|
+
uv run mypy dataplat
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`uv run pytest` is green without Docker: the database-backed tests skip. To run
|
|
23
|
+
them, start a server and point the suite at it:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
docker run -d --name dp-pg-test \
|
|
27
|
+
-e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=dataplat_test \
|
|
28
|
+
-p 55432:5432 postgres:16 -c shared_preload_libraries=pg_stat_statements
|
|
29
|
+
docker exec dp-pg-test psql -U postgres -d dataplat_test \
|
|
30
|
+
-c 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements'
|
|
31
|
+
|
|
32
|
+
DP_TEST_PG_REQUIRED=1 uv run pytest # everything
|
|
33
|
+
uv run pytest -m "not integration" # skip the database half
|
|
34
|
+
docker rm -f -v dp-pg-test # -v, or the volume dangles
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`DP_TEST_PG_REQUIRED=1` turns an unreachable server into an error instead of a
|
|
38
|
+
skip. CI sets it; without it a broken database would make the whole suite skip
|
|
39
|
+
and still report success.
|
|
40
|
+
|
|
41
|
+
Commits follow [Conventional Commits](https://www.conventionalcommits.org/).
|
|
42
|
+
|
|
43
|
+
## Testing against a real Redshift cluster
|
|
44
|
+
|
|
45
|
+
Redshift is a managed service, so there is no container and CI cannot cover it.
|
|
46
|
+
If you have a cluster, you can. The suite is in `tests/integration/redshift/` and
|
|
47
|
+
is split into two tiers, because they need different permission to run:
|
|
48
|
+
|
|
49
|
+
| Marker | Mutates? | Needs |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| `redshift` | no — read-only, safe against a warehouse in use | a reachable cluster |
|
|
52
|
+
| `redshift_ddl` | **yes** | a cluster you can throw away |
|
|
53
|
+
|
|
54
|
+
Credentials come from an ordinary dataplat target, so they stay in your own
|
|
55
|
+
`.envrc` and never reach the repo:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
export DP_TARGETS=warehouse WAREHOUSE_ENGINE=redshift \
|
|
59
|
+
WAREHOUSE_HOST=... WAREHOUSE_USER=... WAREHOUSE_DATABASE=... \
|
|
60
|
+
WAREHOUSE_PASSWORD=...
|
|
61
|
+
export DP_TEST_RS_TARGET=warehouse # or DP_TEST_RS_DSN=... as an escape hatch
|
|
62
|
+
|
|
63
|
+
DP_TEST_RS_REQUIRED=1 uv run pytest -m redshift # read-only tier
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Variable | Effect |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `DP_TEST_RS_TARGET` | a dataplat target name, resolved by the tool's own config |
|
|
69
|
+
| `DP_TEST_RS_DSN` | a raw libpq URL, if you would rather not declare a target |
|
|
70
|
+
| `DP_TEST_RS_REQUIRED` | an unreachable cluster becomes an error instead of a skip |
|
|
71
|
+
| `DP_TEST_RS_DISPOSABLE` | **required** before any `redshift_ddl` test will run |
|
|
72
|
+
| `DP_TEST_RS_SCHEMA` | a schema the read-only tier may inspect (otherwise discovered) |
|
|
73
|
+
|
|
74
|
+
A plain `uv run pytest` is unaffected: with nothing configured, both tiers skip.
|
|
75
|
+
|
|
76
|
+
### Why there is a client-side read-only guard
|
|
77
|
+
|
|
78
|
+
`rs_cursor` refuses anything that is not plainly a read *before it is sent*, on
|
|
79
|
+
top of the server-side `READ ONLY` transaction. Two layers, because the cluster
|
|
80
|
+
may be production: Redshift roles are cluster-wide, and its transactional-DDL
|
|
81
|
+
semantics differ from PostgreSQL's, so the rollback-per-test isolation the
|
|
82
|
+
PostgreSQL harness relies on cannot be assumed to clean up a mistake. A
|
|
83
|
+
server-side check would refuse the statement too — but only after it crossed the
|
|
84
|
+
network to a warehouse someone depends on.
|
|
85
|
+
|
|
86
|
+
It denies by default: only `SELECT`, `WITH … SELECT`, `EXPLAIN` and `SHOW` pass.
|
|
87
|
+
It is not fooled by a leading comment, case, a stray semicolon, a second
|
|
88
|
+
statement smuggled after a `SELECT`, a data-modifying CTE, `SELECT … INTO`, or a
|
|
89
|
+
side-effecting builtin such as `pg_terminate_backend`. The statement splitter is
|
|
90
|
+
hand-written rather than regex-based because a regex that ignores quoting can
|
|
91
|
+
*hide* a statement — naive comment stripping turns `SELECT '--' ; DROP TABLE t`
|
|
92
|
+
into a harmless-looking fragment plus a `DROP` the server will happily run. The
|
|
93
|
+
one hole it cannot close is an unlisted side-effecting UDF; that is what the
|
|
94
|
+
server-side layer is for, and `assert_read_only`'s docstring says so.
|
|
95
|
+
|
|
96
|
+
### What a run does and does not prove
|
|
97
|
+
|
|
98
|
+
A green read-only run proves dataplat's `SELECT`s are **valid Redshift SQL
|
|
99
|
+
against a real server, returning results that unpack** — which is precisely the
|
|
100
|
+
class of defect the PostgreSQL suite found repeatedly in these same functions
|
|
101
|
+
(an empty `pg_partition_tree`, a masked column, a view that does not cover
|
|
102
|
+
schemas). It cannot tell you anything about `GRANT`, `DROP`, `RENAME` or session
|
|
103
|
+
termination; those need the DDL tier and a disposable cluster.
|
|
104
|
+
|
|
105
|
+
The run also prints a conformance table of what the cluster answered, because
|
|
106
|
+
the point is learning what the engine does — a green run that recorded nothing
|
|
107
|
+
has taught nobody anything.
|
|
108
|
+
|
|
109
|
+
## Dialect changes: what counts as evidence
|
|
110
|
+
|
|
111
|
+
`dataplat/services/db` targets PostgreSQL and Redshift. PostgreSQL has a real
|
|
112
|
+
integration suite behind it. Redshift has none and cannot get one cheaply — it
|
|
113
|
+
is a managed service, so there is no container to run in CI.
|
|
114
|
+
|
|
115
|
+
For a while the rule was simply "don't touch SQL that runs on Redshift, because
|
|
116
|
+
you can't test it." That is a good instinct and a bad rule. Applied literally it
|
|
117
|
+
blocked seven known defects, and when they were finally looked at one at a time,
|
|
118
|
+
six were fixable and only one genuinely needed Redshift-specific SQL. Five did
|
|
119
|
+
not touch Redshift SQL at all, and the sixth turned out to use a construct the
|
|
120
|
+
codebase was already shipping to Redshift elsewhere.
|
|
121
|
+
|
|
122
|
+
So the question is not "can I test this?" but **"what evidence do I have?"** A
|
|
123
|
+
change affecting the Redshift path needs at least one of the following, in
|
|
124
|
+
descending order of strength:
|
|
125
|
+
|
|
126
|
+
0. **A conformance run confirmed it against a real cluster.** Strongest, and the
|
|
127
|
+
only one that is evidence rather than inference — see the section above. A fix
|
|
128
|
+
currently resting on class 2 should be upgraded to class 0 when someone runs
|
|
129
|
+
the suite, and revisited if the run *refutes* it. `test_conformance.py` names
|
|
130
|
+
the assumptions each shipped fix depends on for exactly this reason.
|
|
131
|
+
|
|
132
|
+
1. **It changes no Redshift SQL.** The fix is pure Python, or touches only a
|
|
133
|
+
`_*_SQL_POSTGRES` constant. Dialect risk is zero by construction — verify
|
|
134
|
+
that claim honestly, then go ahead. Aggregation bugs, error handling, and
|
|
135
|
+
return-value shaping usually land here.
|
|
136
|
+
|
|
137
|
+
2. **The construct is already in production on the Redshift path.** Cite the
|
|
138
|
+
file and line. `ESCAPE '\'` was safe to add to `orphans.py` because
|
|
139
|
+
`top_tables.py` had always sent it to both engines;
|
|
140
|
+
`has_schema_privilege(...)` is safe in `describe.py`'s Redshift branch
|
|
141
|
+
because that branch already calls it. Internal precedent beats
|
|
142
|
+
documentation: it is the same server, the same driver, and code someone is
|
|
143
|
+
already running.
|
|
144
|
+
|
|
145
|
+
3. **It withdraws a claim rather than making one.** Replacing a confidently
|
|
146
|
+
wrong value with "unknown" cannot be more wrong than what it replaced. See
|
|
147
|
+
the standing rule below.
|
|
148
|
+
|
|
149
|
+
4. **Documented Redshift behaviour, cited, plus a fake-cursor test** pinning the
|
|
150
|
+
SQL the Redshift branch emits. Weakest of the four, because documentation and
|
|
151
|
+
deployed reality drift. Use it when the change is worth the residual risk,
|
|
152
|
+
and say so in the commit.
|
|
153
|
+
|
|
154
|
+
If none of the four applies, **do not guess.** Leave the defect, and record it
|
|
155
|
+
in a comment next to the code it affects — not in a tracker nobody reads. The
|
|
156
|
+
comment is what lets the next person re-evaluate instead of rediscovering.
|
|
157
|
+
|
|
158
|
+
### Requirements either way
|
|
159
|
+
|
|
160
|
+
- **Keep the engine constants split.** Never edit a `_*_SQL_REDSHIFT` constant
|
|
161
|
+
to fix a PostgreSQL bug. If a shared statement needs to diverge, split it and
|
|
162
|
+
leave the Redshift half byte-for-byte as it was.
|
|
163
|
+
- **Add a fake-cursor test** asserting what the Redshift branch emits. It is the
|
|
164
|
+
only mechanism that covers that path at all, and it catches the common
|
|
165
|
+
accident of "fixed both branches when I meant one".
|
|
166
|
+
- **Record the evidence in the code**, not just the commit message. A future
|
|
167
|
+
reader deciding whether they may touch the line needs to see why it is the way
|
|
168
|
+
it is.
|
|
169
|
+
|
|
170
|
+
### Standing rule: prefer "unknown" to a confident falsehood
|
|
171
|
+
|
|
172
|
+
`dp db role show` used to print `Password set: yes` for every role, including
|
|
173
|
+
passwordless ones, because the column it read is masked to `'********'` and can
|
|
174
|
+
never be NULL. A report that states something false is worse than one that
|
|
175
|
+
admits a gap — especially a report someone is using for an audit. When the
|
|
176
|
+
server will not tell you, say so, and say why in the same breath: a bare
|
|
177
|
+
"unknown" reads as a tool defect.
|
|
@@ -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
|
|
|
@@ -324,6 +428,9 @@ dp ingest airbyte connections set-cursor -c <connection-id> --xmin 0 --yes
|
|
|
324
428
|
|
|
325
429
|
## Development
|
|
326
430
|
|
|
431
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the integration suite and the rules
|
|
432
|
+
for changing SQL that runs on Redshift.
|
|
433
|
+
|
|
327
434
|
```bash
|
|
328
435
|
git clone https://github.com/hanslemm/dataplat
|
|
329
436
|
cd dataplat
|
|
@@ -337,6 +444,13 @@ uv run mypy dataplat
|
|
|
337
444
|
CI runs those four across Python 3.12 and 3.13 — the floor the wheel
|
|
338
445
|
advertises as well as the pinned dev version.
|
|
339
446
|
|
|
447
|
+
Redshift cannot be containerized, so CI cannot cover it. If you run dataplat
|
|
448
|
+
against a Redshift cluster, you can verify your own deployment: point
|
|
449
|
+
`DP_TEST_RS_TARGET` at one of your targets and run the read-only tier
|
|
450
|
+
(`uv run pytest -m redshift`). It only issues `SELECT`s — a guard refuses
|
|
451
|
+
anything else before it reaches the server — and prints what your cluster
|
|
452
|
+
answered. See [CONTRIBUTING.md](CONTRIBUTING.md#testing-against-a-real-redshift-cluster).
|
|
453
|
+
|
|
340
454
|
### Integration tests against a real PostgreSQL
|
|
341
455
|
|
|
342
456
|
Most of the suite drives a fake database cursor. That proves a code path
|
|
@@ -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
|
|
|
@@ -279,6 +382,9 @@ dp ingest airbyte connections set-cursor -c <connection-id> --xmin 0 --yes
|
|
|
279
382
|
|
|
280
383
|
## Development
|
|
281
384
|
|
|
385
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the integration suite and the rules
|
|
386
|
+
for changing SQL that runs on Redshift.
|
|
387
|
+
|
|
282
388
|
```bash
|
|
283
389
|
git clone https://github.com/hanslemm/dataplat
|
|
284
390
|
cd dataplat
|
|
@@ -292,6 +398,13 @@ uv run mypy dataplat
|
|
|
292
398
|
CI runs those four across Python 3.12 and 3.13 — the floor the wheel
|
|
293
399
|
advertises as well as the pinned dev version.
|
|
294
400
|
|
|
401
|
+
Redshift cannot be containerized, so CI cannot cover it. If you run dataplat
|
|
402
|
+
against a Redshift cluster, you can verify your own deployment: point
|
|
403
|
+
`DP_TEST_RS_TARGET` at one of your targets and run the read-only tier
|
|
404
|
+
(`uv run pytest -m redshift`). It only issues `SELECT`s — a guard refuses
|
|
405
|
+
anything else before it reaches the server — and prints what your cluster
|
|
406
|
+
answered. See [CONTRIBUTING.md](CONTRIBUTING.md#testing-against-a-real-redshift-cluster).
|
|
407
|
+
|
|
295
408
|
### Integration tests against a real PostgreSQL
|
|
296
409
|
|
|
297
410
|
Most of the suite drives a fake database cursor. That proves a code path
|