dataplat 0.1.0__tar.gz → 0.2.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.
Files changed (173) hide show
  1. dataplat-0.2.0/.github/workflows/ci.yml +48 -0
  2. dataplat-0.2.0/.github/workflows/release.yml +119 -0
  3. dataplat-0.2.0/CHANGELOG.md +123 -0
  4. {dataplat-0.1.0 → dataplat-0.2.0}/PKG-INFO +28 -4
  5. {dataplat-0.1.0 → dataplat-0.2.0}/README.md +25 -2
  6. dataplat-0.2.0/dataplat/__init__.py +27 -0
  7. dataplat-0.2.0/dataplat/cli/_lazy.py +108 -0
  8. dataplat-0.2.0/dataplat/cli/_missing.py +133 -0
  9. dataplat-0.2.0/dataplat/cli/_options.py +29 -0
  10. dataplat-0.2.0/dataplat/cli/_prompt.py +66 -0
  11. dataplat-0.2.0/dataplat/cli/_render.py +56 -0
  12. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/bi/superset.py +36 -38
  13. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ci/github/runner.py +42 -18
  14. dataplat-0.2.0/dataplat/cli/cloud/aws/_common.py +132 -0
  15. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/cloud/aws/rds.py +21 -23
  16. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/cloud/aws/redshift.py +18 -14
  17. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/cloud/aws/secrets.py +144 -145
  18. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/config.py +109 -37
  19. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/db/__init__.py +25 -31
  20. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/db/_common.py +12 -6
  21. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/db/_report.py +15 -1
  22. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/db/dbt_orphans.py +160 -152
  23. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/db/describe.py +86 -101
  24. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/db/long_queries.py +24 -32
  25. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/db/role.py +87 -43
  26. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/db/role_create.py +109 -78
  27. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/db/role_drop.py +65 -49
  28. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/db/role_list.py +21 -9
  29. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/db/top_tables.py +27 -34
  30. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ingest/airbyte/_common.py +5 -2
  31. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ingest/airbyte/_cursor.py +78 -33
  32. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ingest/airbyte/_resource.py +24 -20
  33. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ingest/airbyte/connections.py +195 -130
  34. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ingest/airbyte/definitions.py +36 -19
  35. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ingest/airbyte/jobs.py +24 -16
  36. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ingest/airbyte/tags.py +13 -8
  37. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ingest/airbyte/templates.py +42 -15
  38. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ingest/airbyte/tui.py +21 -12
  39. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ingest/airbyte/workspaces.py +21 -11
  40. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/open.py +6 -1
  41. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/status.py +40 -17
  42. dataplat-0.2.0/dataplat/core/deps.py +227 -0
  43. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/core/envrc.py +80 -14
  44. dataplat-0.2.0/dataplat/core/registry.py +110 -0
  45. dataplat-0.2.0/dataplat/main.py +77 -0
  46. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/airbyte/client.py +7 -5
  47. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/airbyte/connections.py +20 -9
  48. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/airbyte/definitions.py +13 -4
  49. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/airbyte/destinations.py +1 -3
  50. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/airbyte/tags.py +7 -3
  51. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/airbyte/workspaces.py +3 -1
  52. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/aws/auth.py +3 -1
  53. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/db/describe.py +64 -27
  54. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/db/long_queries.py +10 -28
  55. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/db/orphans.py +4 -6
  56. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/db/role.py +168 -96
  57. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/db/role_admin.py +63 -37
  58. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/db/role_dialects.py +104 -59
  59. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/superset/client.py +12 -4
  60. {dataplat-0.1.0 → dataplat-0.2.0}/pyproject.toml +15 -5
  61. dataplat-0.2.0/tests/cli/test_airbyte_commands.py +1554 -0
  62. {dataplat-0.1.0 → dataplat-0.2.0}/tests/cli/test_airbyte_cursor_logic.py +103 -54
  63. dataplat-0.2.0/tests/cli/test_airbyte_guards.py +310 -0
  64. dataplat-0.2.0/tests/cli/test_airbyte_tui.py +129 -0
  65. {dataplat-0.1.0 → dataplat-0.2.0}/tests/cli/test_aws_secrets.py +272 -9
  66. dataplat-0.2.0/tests/cli/test_aws_secrets_write.py +336 -0
  67. dataplat-0.2.0/tests/cli/test_cli_smoke.py +495 -0
  68. dataplat-0.2.0/tests/cli/test_config.py +279 -0
  69. {dataplat-0.1.0 → dataplat-0.2.0}/tests/cli/test_db_common.py +55 -1
  70. {dataplat-0.1.0 → dataplat-0.2.0}/tests/cli/test_db_long_queries.py +54 -0
  71. dataplat-0.2.0/tests/cli/test_db_query.py +199 -0
  72. dataplat-0.2.0/tests/cli/test_dbt_orphans.py +763 -0
  73. {dataplat-0.1.0 → dataplat-0.2.0}/tests/cli/test_describe.py +277 -21
  74. {dataplat-0.1.0 → dataplat-0.2.0}/tests/cli/test_github_runner.py +290 -7
  75. dataplat-0.2.0/tests/cli/test_missing_deps.py +277 -0
  76. {dataplat-0.1.0 → dataplat-0.2.0}/tests/cli/test_open.py +15 -0
  77. dataplat-0.2.0/tests/cli/test_prompt.py +149 -0
  78. dataplat-0.2.0/tests/cli/test_rds.py +297 -0
  79. dataplat-0.2.0/tests/cli/test_redshift.py +173 -0
  80. {dataplat-0.1.0 → dataplat-0.2.0}/tests/cli/test_regression.py +4 -4
  81. dataplat-0.2.0/tests/cli/test_render.py +95 -0
  82. dataplat-0.2.0/tests/cli/test_role.py +660 -0
  83. dataplat-0.2.0/tests/cli/test_role_create.py +457 -0
  84. dataplat-0.2.0/tests/cli/test_role_drop.py +299 -0
  85. dataplat-0.2.0/tests/cli/test_status.py +798 -0
  86. dataplat-0.2.0/tests/cli/test_superset.py +473 -0
  87. {dataplat-0.1.0 → dataplat-0.2.0}/tests/cli/test_top_tables.py +73 -18
  88. {dataplat-0.1.0 → dataplat-0.2.0}/tests/conftest.py +20 -5
  89. dataplat-0.2.0/tests/core/test_deps.py +299 -0
  90. dataplat-0.2.0/tests/core/test_envrc.py +265 -0
  91. dataplat-0.2.0/tests/core/test_registry.py +96 -0
  92. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/airbyte/test_client.py +5 -1
  93. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/airbyte/test_connections.py +7 -2
  94. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/airbyte/test_definitions.py +11 -3
  95. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/airbyte/test_destinations.py +13 -2
  96. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/airbyte/test_sources.py +13 -2
  97. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/airbyte/test_workspaces.py +1 -0
  98. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/aws/test_auth.py +6 -2
  99. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/db/test_connection.py +26 -9
  100. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/db/test_describe.py +283 -125
  101. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/db/test_long_queries.py +1 -1
  102. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/db/test_orphans.py +20 -37
  103. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/db/test_role.py +180 -141
  104. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/db/test_role_admin.py +94 -42
  105. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/db/test_role_dialects.py +39 -28
  106. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/db/test_top_tables.py +13 -14
  107. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/superset/test_client.py +11 -3
  108. {dataplat-0.1.0 → dataplat-0.2.0}/uv.lock +34 -2
  109. dataplat-0.1.0/.github/workflows/ci.yml +0 -31
  110. dataplat-0.1.0/.github/workflows/release.yml +0 -58
  111. dataplat-0.1.0/CHANGELOG.md +0 -17
  112. dataplat-0.1.0/dataplat/__init__.py +0 -3
  113. dataplat-0.1.0/dataplat/cli/_missing.py +0 -108
  114. dataplat-0.1.0/dataplat/cli/cloud/aws/_common.py +0 -77
  115. dataplat-0.1.0/dataplat/core/deps.py +0 -138
  116. dataplat-0.1.0/dataplat/main.py +0 -87
  117. dataplat-0.1.0/tests/cli/test_airbyte_commands.py +0 -555
  118. dataplat-0.1.0/tests/cli/test_airbyte_guards.py +0 -127
  119. dataplat-0.1.0/tests/cli/test_cli_smoke.py +0 -267
  120. dataplat-0.1.0/tests/cli/test_config.py +0 -152
  121. dataplat-0.1.0/tests/cli/test_db_query.py +0 -70
  122. dataplat-0.1.0/tests/cli/test_dbt_orphans.py +0 -102
  123. dataplat-0.1.0/tests/cli/test_missing_deps.py +0 -107
  124. dataplat-0.1.0/tests/cli/test_redshift.py +0 -17
  125. dataplat-0.1.0/tests/cli/test_role.py +0 -389
  126. dataplat-0.1.0/tests/cli/test_role_create.py +0 -213
  127. dataplat-0.1.0/tests/cli/test_status.py +0 -138
  128. dataplat-0.1.0/tests/core/test_deps.py +0 -117
  129. dataplat-0.1.0/tests/core/test_envrc.py +0 -134
  130. {dataplat-0.1.0 → dataplat-0.2.0}/.gitignore +0 -0
  131. {dataplat-0.1.0 → dataplat-0.2.0}/.python-version +0 -0
  132. {dataplat-0.1.0 → dataplat-0.2.0}/LICENSE +0 -0
  133. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/__init__.py +0 -0
  134. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/bi/__init__.py +0 -0
  135. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/bi/app.py +0 -0
  136. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ci/__init__.py +0 -0
  137. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ci/app.py +0 -0
  138. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ci/github/__init__.py +0 -0
  139. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ci/github/app.py +0 -0
  140. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/cloud/__init__.py +0 -0
  141. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/cloud/app.py +0 -0
  142. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/cloud/aws/__init__.py +0 -0
  143. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/cloud/aws/app.py +0 -0
  144. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ingest/__init__.py +0 -0
  145. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ingest/airbyte/__init__.py +0 -0
  146. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ingest/airbyte/app.py +0 -0
  147. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ingest/airbyte/destinations.py +0 -0
  148. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ingest/airbyte/enums.py +0 -0
  149. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ingest/airbyte/sources.py +0 -0
  150. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/cli/ingest/app.py +0 -0
  151. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/core/__init__.py +0 -0
  152. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/core/errors.py +0 -0
  153. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/__init__.py +0 -0
  154. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/airbyte/__init__.py +0 -0
  155. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/airbyte/_resource.py +0 -0
  156. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/airbyte/jobs.py +0 -0
  157. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/airbyte/sources.py +0 -0
  158. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/aws/__init__.py +0 -0
  159. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/db/__init__.py +0 -0
  160. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/db/connection.py +0 -0
  161. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/db/targets.py +0 -0
  162. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/db/top_tables.py +0 -0
  163. {dataplat-0.1.0 → dataplat-0.2.0}/dataplat/services/superset/__init__.py +0 -0
  164. {dataplat-0.1.0 → dataplat-0.2.0}/tests/__init__.py +0 -0
  165. {dataplat-0.1.0 → dataplat-0.2.0}/tests/cli/__init__.py +0 -0
  166. {dataplat-0.1.0 → dataplat-0.2.0}/tests/core/__init__.py +0 -0
  167. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/__init__.py +0 -0
  168. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/airbyte/__init__.py +0 -0
  169. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/airbyte/test_jobs.py +0 -0
  170. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/aws/__init__.py +0 -0
  171. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/db/__init__.py +0 -0
  172. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/db/test_targets.py +0 -0
  173. {dataplat-0.1.0 → dataplat-0.2.0}/tests/services/superset/__init__.py +0 -0
@@ -0,0 +1,48 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["**"]
6
+ # A tag push matches no branch, so without this a release tag would run no
7
+ # checks at all before release.yml publishes it.
8
+ tags: ["[0-9]*.[0-9]*.[0-9]*"]
9
+ pull_request:
10
+
11
+ jobs:
12
+ checks:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ # The floor and the ceiling break for different reasons; report both
16
+ # rather than hiding one behind the other.
17
+ fail-fast: false
18
+ matrix:
19
+ # Lowest version requires-python allows, plus the dev pin.
20
+ python-version: ["3.12", "3.13"]
21
+ env:
22
+ # Overrides .python-version (3.13). Without it every uv command in the
23
+ # 3.12 leg would still pick the pin and we would test 3.13 twice.
24
+ UV_PYTHON: ${{ matrix.python-version }}
25
+ steps:
26
+ - name: Checkout
27
+ uses: actions/checkout@v4
28
+
29
+ - name: Setup uv
30
+ uses: astral-sh/setup-uv@v5
31
+
32
+ - name: Setup Python
33
+ run: uv python install ${{ matrix.python-version }}
34
+
35
+ - name: Install dependencies
36
+ run: uv sync --group dev --all-extras
37
+
38
+ - name: Ruff
39
+ run: uv run ruff check .
40
+
41
+ - name: Ruff format
42
+ run: uv run ruff format --check .
43
+
44
+ - name: MyPy
45
+ run: uv run mypy dataplat
46
+
47
+ - name: Pytest
48
+ run: uv run pytest
@@ -0,0 +1,119 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["[0-9]*.[0-9]*.[0-9]*"]
6
+
7
+ jobs:
8
+ # Nothing downstream re-checks the tree, so this job is the only thing
9
+ # standing between an untested commit and PyPI. Publishing is irreversible:
10
+ # a version can be yanked but never replaced.
11
+ verify:
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ # Must mirror ci.yml: the wheel advertises >=3.12, so the floor has to
17
+ # pass before we publish, not just the dev pin.
18
+ python-version: ["3.12", "3.13"]
19
+ env:
20
+ UV_PYTHON: ${{ matrix.python-version }}
21
+ steps:
22
+ - name: Checkout
23
+ uses: actions/checkout@v4
24
+
25
+ - name: Setup uv
26
+ uses: astral-sh/setup-uv@v5
27
+
28
+ - name: Setup Python
29
+ run: uv python install ${{ matrix.python-version }}
30
+
31
+ # Before the expensive sync: a mismatch means the tag would either
32
+ # publish the wrong version number or die at upload against an already
33
+ # occupied filename.
34
+ - name: Assert tag matches project version
35
+ run: |
36
+ uv run --no-project python - <<'PY'
37
+ import os
38
+ import sys
39
+ import tomllib
40
+
41
+ tag = os.environ["GITHUB_REF_NAME"]
42
+ with open("pyproject.toml", "rb") as handle:
43
+ version = tomllib.load(handle)["project"]["version"]
44
+ if tag != version:
45
+ sys.exit(
46
+ f"Tag '{tag}' does not match [project].version '{version}' in "
47
+ "pyproject.toml. Bump the version, commit, and re-tag; "
48
+ "refusing to publish a mismatched artifact."
49
+ )
50
+ print(f"Tag '{tag}' matches [project].version.")
51
+ PY
52
+
53
+ - name: Install dependencies
54
+ run: uv sync --group dev --all-extras
55
+
56
+ - name: Ruff
57
+ run: uv run ruff check .
58
+
59
+ - name: Ruff format
60
+ run: uv run ruff format --check .
61
+
62
+ - name: MyPy
63
+ run: uv run mypy dataplat
64
+
65
+ - name: Pytest
66
+ run: uv run pytest
67
+
68
+ build:
69
+ needs: verify
70
+ runs-on: ubuntu-latest
71
+ steps:
72
+ - name: Checkout
73
+ uses: actions/checkout@v4
74
+
75
+ - name: Setup uv
76
+ uses: astral-sh/setup-uv@v5
77
+
78
+ - name: Build sdist and wheel
79
+ run: uv build
80
+
81
+ - name: Upload artifacts
82
+ uses: actions/upload-artifact@v4
83
+ with:
84
+ name: dist
85
+ path: dist/
86
+
87
+ publish:
88
+ needs: build
89
+ runs-on: ubuntu-latest
90
+ environment: pypi
91
+ permissions:
92
+ id-token: write # PyPI Trusted Publishing (OIDC)
93
+ steps:
94
+ - name: Download artifacts
95
+ uses: actions/download-artifact@v4
96
+ with:
97
+ name: dist
98
+ path: dist/
99
+
100
+ - name: Publish to PyPI
101
+ uses: pypa/gh-action-pypi-publish@release/v1
102
+
103
+ github-release:
104
+ needs: publish
105
+ runs-on: ubuntu-latest
106
+ permissions:
107
+ contents: write
108
+ steps:
109
+ - name: Download artifacts
110
+ uses: actions/download-artifact@v4
111
+ with:
112
+ name: dist
113
+ path: dist/
114
+
115
+ - name: Create GitHub release
116
+ uses: softprops/action-gh-release@v2
117
+ with:
118
+ files: dist/*
119
+ generate_release_notes: true
@@ -0,0 +1,123 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ ### Fixed
6
+
7
+ - Data containing square brackets no longer breaks output. Rich reads `[...]`
8
+ as markup in every string it renders, so a warehouse value, identifier,
9
+ comment, secret, API payload, or driver message containing `[/x]` aborted the
10
+ command with a `MarkupError` traceback, and one containing a style name like
11
+ `[bold]` was silently swallowed — the output quietly misreported the data.
12
+ Every renderer now emits external values as literal text, table *headers*
13
+ included: `dp ingest airbyte connections list --all-columns` takes its column
14
+ labels straight from Airbyte's JSON keys, and the Textual TUI passes the same
15
+ labels to a `DataTable`, so both had the identical failure.
16
+
17
+ - `dp db dbt-orphans revert` and `purge --older-than` could not find their own
18
+ audit logs if the log directory's path contained a bracket — a home directory
19
+ named `[work]` was enough. The discovery glob interpolated the directory
20
+ without escaping it, so the brackets read as a character class and matched
21
+ nothing: `revert` reported no history, and `purge --older-than` treated every
22
+ object as having no recorded rename.
23
+
24
+ - Installing a missing extra no longer changes anything else about your
25
+ install. It was resolved unpinned, so hitting a stubbed area could upgrade
26
+ `dataplat` itself and then re-exec your command against the new code; and
27
+ because `--force` rebuilds the environment from the spec it is given,
28
+ installing `db` silently removed an `ingest` you already had. The command is
29
+ now pinned to the running version and unions the extras already present.
30
+
31
+ - `dp ci github runner start|stop|status` identified containers with an
32
+ unanchored `docker --filter name=`, which is a substring match: a runner
33
+ named `foo` matched `gha-runner-foobar`, so `status` could report a
34
+ different runner's state as yours. All filters are anchored.
35
+
36
+ - `dp status --json` emitted valid JSON only while nothing needed saying. An
37
+ expired SSO token wrote its notice to stdout, mixed into the document.
38
+ Spinners and notices now go to stderr; stdout carries only the payload.
39
+
40
+ - Pressing TAB inside an area whose extra is missing misbehaved badly. Shell
41
+ completion walks the command tree with click's resilient parsing, which calls
42
+ the same resolution hook the missing-extra stub overrode, so completion ran
43
+ the interactive installer: the offer text landed on the stream the shell
44
+ evaluates (zsh even re-invoked `dp` from the backticks in the message), a
45
+ full traceback went to the terminal, and under bash the install itself
46
+ executed from a keypress. Completion now falls through silently.
47
+
48
+ - Installing an extra inside a virtualenv that has no `pip` — the default for
49
+ `uv venv` — prescribed `python -m pip install` and could only fail, reporting
50
+ just a bare exit code. It now uses `uv pip install --python <env>` when pip is
51
+ absent, and says so plainly when neither installer is reachable.
52
+
53
+ - The `Will run:` line is shell-quoted. It prints a command the user may want
54
+ to run themselves, but the requirement spec's brackets (and now its `==`
55
+ pin) meant pasting it into zsh failed on globbing.
56
+
57
+ ### Added
58
+
59
+ - `DP_ENVRC_ALLOW_CWD=0` stops `.envrc` being picked up from the current
60
+ directory. Loading it made every command sensitive to where you stood, with
61
+ no way to see that had happened: `dp config show` and `dp config doctor` now
62
+ name the active file and which candidate produced it, and warn when it came
63
+ from the working directory.
64
+
65
+ - `DP_AWS_PROFILE_ALIASES` now works across the whole `dp cloud aws` group.
66
+ Only `secrets` resolved aliases, so `dp cloud aws rds metrics -p prod` passed
67
+ the literal `prod` to boto3 and failed with a profile-not-found error while
68
+ the same flag worked one command over. `rds list` and `redshift metrics` also
69
+ print the profile they actually used rather than the shorthand you typed.
70
+
71
+ - Python 3.12 support. `requires-python` was 3.13 while the only construct
72
+ above 3.11 in the package is one PEP 695 generic, so 3.12 users hit an
73
+ install failure for nothing. CI now tests the advertised floor as well as
74
+ the dev pin.
75
+
76
+ - The release workflow verifies that the pushed tag matches
77
+ `[project].version` and runs the full check matrix before building. CI also
78
+ runs on tag pushes — a tag matched no branch filter, so releases were
79
+ published without any checks having run.
80
+
81
+ ### Changed
82
+
83
+ - Areas mount lazily, cutting `dp --version` from 360 ms to 100 ms. Every
84
+ area's dependencies — psycopg, textual, httpx, boto3 — were imported at
85
+ startup regardless of the command; now nothing heavy loads until the area
86
+ that needs it is actually used. Areas resolve through the registry
87
+ (`dataplat/core/registry.py`) and carry their own dependency contract, so a
88
+ future third-party area is no longer tied to a lookup in a built-in dict.
89
+
90
+ - One confirmation gate covers every destructive command. Three idioms had
91
+ drifted apart and only one told you what to do when stdin was not a TTY;
92
+ non-interactive runs now consistently name the flag that would have worked
93
+ and exit 1 rather than aborting with no explanation.
94
+
95
+ **Behaviour change worth knowing before you upgrade:** piping a confirmation
96
+ into a destructive command no longer authorizes it. `echo y | dp db role drop
97
+ old_user` used to be accepted by the eight gates that had no TTY check, and
98
+ now exits 1 naming `--yes`. Data-destroying work should not proceed on a `y`
99
+ that arrived from a pipe rather than a person, and `--yes` has always been
100
+ the documented scriptable path — but any script relying on the old behaviour
101
+ fails loudly and needs `--yes` instead. Sites that already refused a piped
102
+ answer (`dp db query`'s write guard, the AWS secrets writes) are unchanged.
103
+
104
+ - `line-length = 88` is enforced. It was configured but unchecked, because
105
+ pycodestyle was not among the selected rule sets, and the tree had drifted
106
+ to 164 violations across 71 unformatted files. The repo is now
107
+ `ruff format`-clean and CI keeps it that way.
108
+
109
+ ## 0.1.0
110
+
111
+ Initial public release.
112
+
113
+ - Optional per-area dependency extras (`dataplat[db,ingest,bi,cloud,all]`)
114
+ with auto-detection: `dp config sync` installs what your enabled areas
115
+ need, and hitting a stubbed area offers to install its extra and re-run
116
+ your command.
117
+
118
+ - `dp` command with areas per component type: `db`, `ingest` (Airbyte),
119
+ `bi` (Superset), `cloud` (AWS), `ci` (GitHub runners), plus `status`,
120
+ `open`, and `config`.
121
+ - Fully config-driven database targets via `DP_TARGETS` and per-target
122
+ `<NAME>_*` environment variables (Postgres and Redshift engines).
123
+ - `.envrc` loading with a global config link (`dp config init`).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataplat
3
- Version: 0.1.0
3
+ Version: 0.2.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
@@ -15,11 +15,12 @@ Classifier: Environment :: Console
15
15
  Classifier: Intended Audience :: Developers
16
16
  Classifier: Intended Audience :: System Administrators
17
17
  Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3.12
18
19
  Classifier: Programming Language :: Python :: 3.13
19
20
  Classifier: Topic :: Database
20
21
  Classifier: Topic :: System :: Systems Administration
21
22
  Classifier: Topic :: Utilities
22
- Requires-Python: >=3.13
23
+ Requires-Python: >=3.12
23
24
  Requires-Dist: rich>=14.2.0
24
25
  Requires-Dist: typer>=0.21.1
25
26
  Provides-Extra: all
@@ -93,6 +94,8 @@ dp
93
94
 
94
95
  ## Installation
95
96
 
97
+ Requires Python 3.12 or newer.
98
+
96
99
  ```bash
97
100
  uv tool install "dataplat[all]" # recommended: everything
98
101
  # or
@@ -133,6 +136,11 @@ and re-runs your original command. Non-interactive sessions never install
133
136
  silently — they print the command and exit instead. `dp config doctor`
134
137
  also reports per-area dependency status.
135
138
 
139
+ Either path only ever *adds* to your install: the command is pinned to the
140
+ `dataplat` version you already run, so installing an extra never upgrades the
141
+ tool underneath you, and it carries your existing extras along, so adding
142
+ `db` cannot drop an `ingest` you already had.
143
+
136
144
  ## Quick start
137
145
 
138
146
  1. Declare your database targets — any names you like:
@@ -180,11 +188,21 @@ already set in your shell. Lookup order:
180
188
  1. `DP_ENVRC_PATH`
181
189
  2. `~/.config/dataplat/.envrc` (the global link — set it with `dp config init`)
182
190
  3. `.envrc` in the current directory
191
+ 4. the `.envrc` beside a development checkout of dataplat itself
192
+
193
+ Candidate 3 makes every command sensitive to where you run it: standing in a
194
+ cloned repo points `dp` at whatever host and credentials that repo's `.envrc`
195
+ exports. `dp config show` and `dp config doctor` always name the active file
196
+ *and* which candidate produced it, and warn when it came from the current
197
+ directory. Set `DP_ENVRC_ALLOW_CWD=0` to drop that candidate entirely and
198
+ rely only on the global link you chose.
183
199
 
184
200
  ## Configuration reference
185
201
 
186
202
  | Variable | Purpose |
187
203
  | --- | --- |
204
+ | `DP_ENVRC_PATH` | Explicit `.envrc` to load, ahead of every other candidate. |
205
+ | `DP_ENVRC_ALLOW_CWD` | Set to `0` to stop picking up `.envrc` from the current directory. |
188
206
  | `DP_TARGETS` | Comma-separated DB target names (e.g. `warehouse,lake`). |
189
207
  | `DP_DEFAULT_TARGET` | Target used when `--target` is omitted (default: first of `DP_TARGETS`). |
190
208
  | `<NAME>_ENGINE` | `postgresql` (default) or `redshift`, per target. |
@@ -312,13 +330,19 @@ cd dataplat
312
330
  uv sync --group dev --all-extras
313
331
  uv run pytest
314
332
  uv run ruff check .
333
+ uv run ruff format --check .
315
334
  uv run mypy dataplat
316
335
  ```
317
336
 
337
+ CI runs those four across Python 3.12 and 3.13 — the floor the wheel
338
+ advertises as well as the pinned dev version.
339
+
318
340
  ## Releasing
319
341
 
320
- Tag `X.Y.Z` (bare semver, no `v` prefix) on `main`; GitHub Actions builds
321
- and publishes to PyPI via Trusted Publishing. Commits follow
342
+ Bump `[project].version`, then tag `X.Y.Z` (bare semver, no `v` prefix) on
343
+ `main`. The release workflow refuses to publish unless the tag matches that
344
+ version, and runs the full check matrix first; only then does GitHub Actions
345
+ build and publish to PyPI via Trusted Publishing. Commits follow
322
346
  [Conventional Commits](https://www.conventionalcommits.org/).
323
347
 
324
348
  ## License
@@ -49,6 +49,8 @@ dp
49
49
 
50
50
  ## Installation
51
51
 
52
+ Requires Python 3.12 or newer.
53
+
52
54
  ```bash
53
55
  uv tool install "dataplat[all]" # recommended: everything
54
56
  # or
@@ -89,6 +91,11 @@ and re-runs your original command. Non-interactive sessions never install
89
91
  silently — they print the command and exit instead. `dp config doctor`
90
92
  also reports per-area dependency status.
91
93
 
94
+ Either path only ever *adds* to your install: the command is pinned to the
95
+ `dataplat` version you already run, so installing an extra never upgrades the
96
+ tool underneath you, and it carries your existing extras along, so adding
97
+ `db` cannot drop an `ingest` you already had.
98
+
92
99
  ## Quick start
93
100
 
94
101
  1. Declare your database targets — any names you like:
@@ -136,11 +143,21 @@ already set in your shell. Lookup order:
136
143
  1. `DP_ENVRC_PATH`
137
144
  2. `~/.config/dataplat/.envrc` (the global link — set it with `dp config init`)
138
145
  3. `.envrc` in the current directory
146
+ 4. the `.envrc` beside a development checkout of dataplat itself
147
+
148
+ Candidate 3 makes every command sensitive to where you run it: standing in a
149
+ cloned repo points `dp` at whatever host and credentials that repo's `.envrc`
150
+ exports. `dp config show` and `dp config doctor` always name the active file
151
+ *and* which candidate produced it, and warn when it came from the current
152
+ directory. Set `DP_ENVRC_ALLOW_CWD=0` to drop that candidate entirely and
153
+ rely only on the global link you chose.
139
154
 
140
155
  ## Configuration reference
141
156
 
142
157
  | Variable | Purpose |
143
158
  | --- | --- |
159
+ | `DP_ENVRC_PATH` | Explicit `.envrc` to load, ahead of every other candidate. |
160
+ | `DP_ENVRC_ALLOW_CWD` | Set to `0` to stop picking up `.envrc` from the current directory. |
144
161
  | `DP_TARGETS` | Comma-separated DB target names (e.g. `warehouse,lake`). |
145
162
  | `DP_DEFAULT_TARGET` | Target used when `--target` is omitted (default: first of `DP_TARGETS`). |
146
163
  | `<NAME>_ENGINE` | `postgresql` (default) or `redshift`, per target. |
@@ -268,13 +285,19 @@ cd dataplat
268
285
  uv sync --group dev --all-extras
269
286
  uv run pytest
270
287
  uv run ruff check .
288
+ uv run ruff format --check .
271
289
  uv run mypy dataplat
272
290
  ```
273
291
 
292
+ CI runs those four across Python 3.12 and 3.13 — the floor the wheel
293
+ advertises as well as the pinned dev version.
294
+
274
295
  ## Releasing
275
296
 
276
- Tag `X.Y.Z` (bare semver, no `v` prefix) on `main`; GitHub Actions builds
277
- and publishes to PyPI via Trusted Publishing. Commits follow
297
+ Bump `[project].version`, then tag `X.Y.Z` (bare semver, no `v` prefix) on
298
+ `main`. The release workflow refuses to publish unless the tag matches that
299
+ version, and runs the full check matrix first; only then does GitHub Actions
300
+ build and publish to PyPI via Trusted Publishing. Commits follow
278
301
  [Conventional Commits](https://www.conventionalcommits.org/).
279
302
 
280
303
  ## License
@@ -0,0 +1,27 @@
1
+ """dataplat — one command to manage any shape of data platform."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ __all__ = ["__version__"]
8
+
9
+
10
+ def __getattr__(name: str) -> Any:
11
+ """Serve ``__version__`` from the installed distribution, on demand.
12
+
13
+ A literal here would be a second place to edit at release time, and the
14
+ kind that goes stale silently: nothing in the package reads it, so a
15
+ mismatch with ``[project].version`` would never surface. Reading the
16
+ metadata keeps pyproject the only source of truth, and doing it in
17
+ ``__getattr__`` (PEP 562) keeps ``importlib.metadata`` off the import path
18
+ of every ``dp`` invocation that never asks.
19
+ """
20
+ if name == "__version__":
21
+ from importlib.metadata import PackageNotFoundError, version
22
+
23
+ try:
24
+ return version("dataplat")
25
+ except PackageNotFoundError: # running from a bare source tree
26
+ return "unknown"
27
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,108 @@
1
+ """Mount areas by name; import one only when it is actually run.
2
+
3
+ ``main`` used to call :func:`~dataplat.core.registry.load_app` for every area
4
+ at import time, so ``dp --version`` paid for psycopg, httpx and croniter
5
+ (~250 ms of a ~280 ms run) just to print one line. Here an area is mounted as
6
+ an empty placeholder group carrying nothing but the name and help text the
7
+ registry already knows, and :class:`LazyRootGroup` swaps in the real Typer app
8
+ — or the missing-deps stub — when click resolves that area, and only then.
9
+
10
+ Placeholders are mounted through ``add_typer`` rather than injected into the
11
+ click group, so command order, ``--help``, "did you mean" suggestions and
12
+ top-level completion keep coming out of Typer's own machinery instead of a
13
+ reimplementation of it, and nothing else in the CLI has to know that areas are
14
+ lazy. Those paths are all import-free (verified: ``dp --help`` and ``dp <TAB>``
15
+ leave psycopg unimported). Completing *inside* an area — ``dp db <TAB>`` — does
16
+ import it, because the subcommand list it has to offer is the area's own; that
17
+ is the one non-dispatch path that pays, and there is no way around it.
18
+
19
+ No direct click import here either — typer vendors click, so only the
20
+ ``TyperGroup`` surface is safe to rely on (see :mod:`dataplat.cli._missing`).
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import Any
26
+
27
+ import typer
28
+ from typer.core import TyperGroup
29
+ from typer.main import get_group
30
+
31
+ from dataplat.core.deps import ready
32
+ from dataplat.core.registry import AreaMount, area_by_name, load_app, mount_help
33
+
34
+ __all__ = [
35
+ "AreaPlaceholderGroup",
36
+ "LazyRootGroup",
37
+ "area_command",
38
+ "area_placeholder",
39
+ ]
40
+
41
+
42
+ class AreaPlaceholderGroup(TyperGroup):
43
+ """Stands in for an area nobody has imported yet.
44
+
45
+ Deliberately empty: it is only ever asked to describe itself (name, help
46
+ text, hidden), and :meth:`LazyRootGroup.resolve_command` replaces it before
47
+ anything can be invoked through it.
48
+ """
49
+
50
+
51
+ def area_placeholder(mount: AreaMount) -> typer.Typer:
52
+ """A command-less Typer app holding just ``mount``'s name and help line."""
53
+ return typer.Typer(
54
+ name=mount.name,
55
+ help=mount_help(mount),
56
+ cls=AreaPlaceholderGroup,
57
+ )
58
+
59
+
60
+ def area_command(mount: AreaMount) -> typer.Typer:
61
+ """``mount``'s real Typer app, or the stub that offers to install its extra.
62
+
63
+ Readiness is decided here rather than at mount time, so an area whose extra
64
+ is missing costs nothing until someone runs it — and it is decided from
65
+ ``mount.deps``, so an area that is not one of the built-ins still works.
66
+ """
67
+ if mount.deps is None or ready(mount.deps):
68
+ return load_app(mount)
69
+ # Imported on this branch only: the stub pulls in subprocess, which costs
70
+ # more to import (~5 ms) than every readiness check in the CLI together.
71
+ from dataplat.cli._missing import build_missing_deps_app
72
+
73
+ return build_missing_deps_app(mount)
74
+
75
+
76
+ class LazyRootGroup(TyperGroup):
77
+ """Root group that resolves an area's real app the first time it is used."""
78
+
79
+ def resolve_command(self, ctx: Any, args: Any) -> Any:
80
+ """Import the area behind a placeholder, now that it is needed.
81
+
82
+ Resolution is what "the user is reaching into this area" looks like to
83
+ click. ``--help``, typo suggestions and top-level completion never come
84
+ through here — they read the placeholder and stay import-free. Shell
85
+ completion *descending* into an area does come through here, with
86
+ ``ctx.resilient_parsing`` set, and must still import: the completions it
87
+ owes the shell are the area's own subcommands.
88
+ """
89
+ name, cmd, rest = super().resolve_command(ctx, args)
90
+ if isinstance(cmd, AreaPlaceholderGroup) and name is not None:
91
+ cmd = self._load_area(name)
92
+ return name, cmd, rest
93
+
94
+ def _load_area(self, name: str) -> Any:
95
+ mount = area_by_name(name)
96
+ # The placeholder exists because a mount was registered under this name.
97
+ assert mount is not None, f"no registered area named {name!r}"
98
+ # get_group, not get_command: the latter bolts the completion options
99
+ # onto whatever it builds, which add_typer never did for an area.
100
+ group = get_group(area_command(mount))
101
+ # add_typer(name=...) used to name the mounted group; get_group takes
102
+ # the name off the area's own Typer, so keep the mount authoritative —
103
+ # a third-party area may well name its app something else.
104
+ group.name = name
105
+ # Cached in place: replacing the placeholder keeps command order intact
106
+ # and means one process imports an area at most once.
107
+ self.commands[name] = group
108
+ return group