dbctl 0.1.1__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.
- dbctl-0.1.1/.dbctl/connections.yaml +150 -0
- dbctl-0.1.1/.dbctl/operations.yaml +95 -0
- dbctl-0.1.1/.github/workflows/ci.yml +165 -0
- dbctl-0.1.1/.gitignore +35 -0
- dbctl-0.1.1/CHANGELOG.md +81 -0
- dbctl-0.1.1/PKG-INFO +323 -0
- dbctl-0.1.1/README.md +287 -0
- dbctl-0.1.1/dbctl/__init__.py +5 -0
- dbctl-0.1.1/dbctl/__main__.py +6 -0
- dbctl-0.1.1/dbctl/audit.py +74 -0
- dbctl-0.1.1/dbctl/cli.py +880 -0
- dbctl-0.1.1/dbctl/config.py +263 -0
- dbctl-0.1.1/dbctl/connections.py +52 -0
- dbctl-0.1.1/dbctl/db.py +117 -0
- dbctl-0.1.1/dbctl/execute.py +117 -0
- dbctl-0.1.1/dbctl/init.py +159 -0
- dbctl-0.1.1/dbctl/multi.py +53 -0
- dbctl-0.1.1/dbctl/operations.py +40 -0
- dbctl-0.1.1/dbctl/reports.py +136 -0
- dbctl-0.1.1/dbctl/runtime.py +110 -0
- dbctl-0.1.1/dbctl/tunnels/__init__.py +20 -0
- dbctl-0.1.1/dbctl/tunnels/base.py +71 -0
- dbctl-0.1.1/dbctl/tunnels/direct.py +28 -0
- dbctl-0.1.1/dbctl/tunnels/ssh.py +73 -0
- dbctl-0.1.1/dbctl/tunnels/ssm.py +142 -0
- dbctl-0.1.1/docker-compose.yml +72 -0
- dbctl-0.1.1/docs/ACTION_OUTPUT.md +99 -0
- dbctl-0.1.1/docs/DESIGN.md +231 -0
- dbctl-0.1.1/docs/SESSION_STATE.md +126 -0
- dbctl-0.1.1/docs/connections.md +233 -0
- dbctl-0.1.1/docs/operations.md +281 -0
- dbctl-0.1.1/docs/tutorial.md +690 -0
- dbctl-0.1.1/pyproject.toml +66 -0
- dbctl-0.1.1/seed/mssql.sql +66 -0
- dbctl-0.1.1/seed/mysql.sql +65 -0
- dbctl-0.1.1/seed/postgres.sql +66 -0
- dbctl-0.1.1/tests/test_bastion_tags.py +167 -0
- dbctl-0.1.1/tests/test_regressions.py +80 -0
- dbctl-0.1.1/tests/test_smoke.py +282 -0
- dbctl-0.1.1/uv.lock +981 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Sample dbctl config for the docker-compose test fleet in this repo.
|
|
2
|
+
# cp .dbctl/connections.yaml ~/.dbctl/connections.yaml
|
|
3
|
+
# cp .dbctl/operations.yaml ~/.dbctl/operations.yaml
|
|
4
|
+
# This file points at the three databases started by `docker compose up -d`.
|
|
5
|
+
|
|
6
|
+
connections:
|
|
7
|
+
pg:
|
|
8
|
+
description: "Postgres dev (docker compose)"
|
|
9
|
+
aliases: [postgres]
|
|
10
|
+
type: direct
|
|
11
|
+
driver: postgresql+psycopg
|
|
12
|
+
database: app
|
|
13
|
+
username: app_admin
|
|
14
|
+
password_env: DBCTL_PG_PASSWORD
|
|
15
|
+
direct: { host: 127.0.0.1, port: 5433 }
|
|
16
|
+
healthcheck: { query: "SELECT 1", timeout_seconds: 5 }
|
|
17
|
+
info:
|
|
18
|
+
- name: row_counts
|
|
19
|
+
description: "Top tables by row count"
|
|
20
|
+
query: |
|
|
21
|
+
SELECT relname AS table, n_live_tup AS rows
|
|
22
|
+
FROM pg_stat_user_tables
|
|
23
|
+
ORDER BY n_live_tup DESC LIMIT 10
|
|
24
|
+
- name: top_users
|
|
25
|
+
query: "SELECT name, quota_daily, is_active FROM users ORDER BY quota_daily DESC LIMIT 10"
|
|
26
|
+
safety:
|
|
27
|
+
confirm: true
|
|
28
|
+
read_only: false
|
|
29
|
+
|
|
30
|
+
my:
|
|
31
|
+
description: "MySQL dev (docker compose)"
|
|
32
|
+
aliases: [mysql]
|
|
33
|
+
type: direct
|
|
34
|
+
driver: mysql+pymysql
|
|
35
|
+
database: app
|
|
36
|
+
username: app_admin
|
|
37
|
+
password_env: DBCTL_MY_PASSWORD
|
|
38
|
+
direct: { host: 127.0.0.1, port: 3307 }
|
|
39
|
+
healthcheck: { query: "SELECT 1" }
|
|
40
|
+
info:
|
|
41
|
+
- name: top_users
|
|
42
|
+
query: "SELECT name, quota_daily, is_active FROM users ORDER BY quota_daily DESC LIMIT 10"
|
|
43
|
+
safety:
|
|
44
|
+
confirm: true
|
|
45
|
+
read_only: false
|
|
46
|
+
|
|
47
|
+
ms:
|
|
48
|
+
description: "SQL Server dev (docker compose)"
|
|
49
|
+
aliases: [mssql]
|
|
50
|
+
type: direct
|
|
51
|
+
driver: mssql+pyodbc
|
|
52
|
+
database: app
|
|
53
|
+
username: sa
|
|
54
|
+
password_env: DBCTL_MS_PASSWORD
|
|
55
|
+
# SQL Server needs a configured ODBC driver on the host. Use pyodbc's
|
|
56
|
+
# connection string by overriding connect_args in your own config:
|
|
57
|
+
# connect_args: { driver: "ODBC Driver 18 for SQL Server", TrustServerCertificate: "yes" }
|
|
58
|
+
direct: { host: 127.0.0.1, port: 1434 }
|
|
59
|
+
healthcheck: { query: "SELECT 1" }
|
|
60
|
+
info:
|
|
61
|
+
- name: top_users
|
|
62
|
+
query: "SELECT TOP 10 name, quota_daily, is_active FROM dbo.users ORDER BY quota_daily DESC"
|
|
63
|
+
safety:
|
|
64
|
+
confirm: true
|
|
65
|
+
read_only: true
|
|
66
|
+
|
|
67
|
+
# --------------------------------------------------------------------- #
|
|
68
|
+
# Reference templates for tunneled Postgres connections.
|
|
69
|
+
# These are NOT meant to connect to anything in the docker compose fleet;
|
|
70
|
+
# they are full-blown reference examples you can copy + edit in your own
|
|
71
|
+
# ~/.dbctl/connections.yaml. They are marked read_only so accidental runs
|
|
72
|
+
# are safe.
|
|
73
|
+
# --------------------------------------------------------------------- #
|
|
74
|
+
|
|
75
|
+
# AWS SSM port-forward through an EC2 bastion to a private RDS Postgres.
|
|
76
|
+
# Requires the `aws` CLI on PATH with an active SSO session in
|
|
77
|
+
# ~/.aws/cache/sso/*.json for the named `profile`.
|
|
78
|
+
pg-ssm:
|
|
79
|
+
description: "REFERENCE: Postgres via AWS SSM (edit before using)"
|
|
80
|
+
aliases: []
|
|
81
|
+
type: ssm
|
|
82
|
+
driver: postgresql+psycopg
|
|
83
|
+
database: app
|
|
84
|
+
username: app_admin
|
|
85
|
+
password_env: DBCTL_PG_SSM_PASSWORD
|
|
86
|
+
ssm:
|
|
87
|
+
region: eu-west-1
|
|
88
|
+
profile: prod # AWS SSO profile; tokens in ~/.aws/cache
|
|
89
|
+
# Resolve the bastion at tunnel-open time via aws ec2 describe-instances.
|
|
90
|
+
# Useful when the bastion is in an ASG / is replaced often — the tag
|
|
91
|
+
# set stays stable while the instance id rotates.
|
|
92
|
+
bastion_tags: { Name: bastion-prod, Env: prod }
|
|
93
|
+
# alt: hardcode the id and skip the ec2:DescribeInstances call:
|
|
94
|
+
# bastion_instance_id: i-0abcd1234ef
|
|
95
|
+
remote_host: mydb.xxxx.eu-west-1.rds.amazonaws.com
|
|
96
|
+
remote_port: 5432
|
|
97
|
+
local_port: 0 # 0 = dbctl picks a free local port
|
|
98
|
+
ssm_document: AWS-StartPortForwardingSessionToRemoteHost
|
|
99
|
+
healthcheck: { query: "SELECT 1" }
|
|
100
|
+
info:
|
|
101
|
+
- name: row_counts
|
|
102
|
+
description: "Top tables by row count"
|
|
103
|
+
query: |
|
|
104
|
+
SELECT relname AS table, n_live_tup AS rows
|
|
105
|
+
FROM pg_stat_user_tables
|
|
106
|
+
ORDER BY n_live_tup DESC LIMIT 10
|
|
107
|
+
- name: active_conns
|
|
108
|
+
description: "Active backend connections"
|
|
109
|
+
query: "SELECT count(*) FROM pg_stat_activity"
|
|
110
|
+
- name: db_size
|
|
111
|
+
description: "Database size in bytes"
|
|
112
|
+
query: "SELECT pg_database_size(current_database()) AS bytes"
|
|
113
|
+
safety:
|
|
114
|
+
confirm: true
|
|
115
|
+
read_only: true
|
|
116
|
+
allowed_operations: []
|
|
117
|
+
|
|
118
|
+
# Classic SSH port-forward through a bastion host to an internal Postgres.
|
|
119
|
+
# Requires the `ssh` CLI on PATH and a working key file at `identity`
|
|
120
|
+
# (tilde is expanded). The operator's ~/.ssh/config + agent keep working.
|
|
121
|
+
pg-ssh:
|
|
122
|
+
description: "REFERENCE: Postgres via SSH tunnel (edit before using)"
|
|
123
|
+
aliases: []
|
|
124
|
+
type: ssh
|
|
125
|
+
driver: postgresql+psycopg
|
|
126
|
+
database: app
|
|
127
|
+
username: app_admin
|
|
128
|
+
password_env: DBCTL_PG_SSH_PASSWORD
|
|
129
|
+
ssh:
|
|
130
|
+
host: bastion.example.com # reachable from your workstation
|
|
131
|
+
user: ec2-user
|
|
132
|
+
identity: ~/.ssh/id_rsa # ~ is expanded by dbctl
|
|
133
|
+
remote_host: db.internal # the database's own hostname, as the bastion sees it
|
|
134
|
+
remote_port: 5432
|
|
135
|
+
local_port: 0 # 0 = dbctl picks a free local port
|
|
136
|
+
port: 22 # bastion's SSH port
|
|
137
|
+
healthcheck: { query: "SELECT 1" }
|
|
138
|
+
info:
|
|
139
|
+
- name: row_counts
|
|
140
|
+
description: "Top tables by row count"
|
|
141
|
+
query: |
|
|
142
|
+
SELECT relname AS table, n_live_tup AS rows
|
|
143
|
+
FROM pg_stat_user_tables
|
|
144
|
+
ORDER BY n_live_tup DESC LIMIT 10
|
|
145
|
+
- name: active_conns
|
|
146
|
+
query: "SELECT count(*) FROM pg_stat_activity"
|
|
147
|
+
safety:
|
|
148
|
+
confirm: true
|
|
149
|
+
read_only: true
|
|
150
|
+
allowed_operations: []
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Sample dbctl operations.
|
|
2
|
+
# Each operation is a parameterised SQL block declaring its inputs; the CLI
|
|
3
|
+
# builds one Click option per parameter (positional + keyword).
|
|
4
|
+
|
|
5
|
+
operations:
|
|
6
|
+
add-user:
|
|
7
|
+
description: "Create or update an application user (Daily quota by default)"
|
|
8
|
+
scope: single
|
|
9
|
+
mode: execute
|
|
10
|
+
confirm: true
|
|
11
|
+
parameters:
|
|
12
|
+
- { name: name, type: string, required: true, position: 1, description: "Unique user name" }
|
|
13
|
+
- { name: quota, type: integer, required: true, position: 2, description: "Daily quota (used as daily limit)" }
|
|
14
|
+
- { name: type, type: string, default: "Daily", position: 3, description: "Account type" }
|
|
15
|
+
sql: |
|
|
16
|
+
INSERT INTO users (name, quota_daily, quota_yearly, type)
|
|
17
|
+
VALUES ($name, $quota, $quota * 365, $type)
|
|
18
|
+
ON CONFLICT (name) DO UPDATE
|
|
19
|
+
SET quota_daily = EXCLUDED.quota_daily,
|
|
20
|
+
quota_yearly = EXCLUDED.quota_yearly,
|
|
21
|
+
type = EXCLUDED.type,
|
|
22
|
+
updated_at = NOW()
|
|
23
|
+
|
|
24
|
+
list-users:
|
|
25
|
+
description: "List users (top N)"
|
|
26
|
+
scope: single
|
|
27
|
+
mode: fetch
|
|
28
|
+
output: table
|
|
29
|
+
parameters:
|
|
30
|
+
- { name: limit, type: integer, default: 10, position: 1 }
|
|
31
|
+
sql: |
|
|
32
|
+
SELECT name, quota_daily, quota_yearly, type, is_active
|
|
33
|
+
FROM users
|
|
34
|
+
ORDER BY quota_daily DESC
|
|
35
|
+
LIMIT $limit
|
|
36
|
+
|
|
37
|
+
find-user:
|
|
38
|
+
description: "Find a user by name prefix"
|
|
39
|
+
scope: single
|
|
40
|
+
mode: fetch
|
|
41
|
+
parameters:
|
|
42
|
+
- { name: prefix, type: string, required: true, position: 1 }
|
|
43
|
+
sql: |
|
|
44
|
+
SELECT name, quota_daily, is_active FROM users WHERE name ILIKE $prefix || '%'
|
|
45
|
+
|
|
46
|
+
report-logs:
|
|
47
|
+
description: "Summarise logs by level between two dates"
|
|
48
|
+
scope: single
|
|
49
|
+
mode: fetch
|
|
50
|
+
output: table
|
|
51
|
+
parameters:
|
|
52
|
+
- { name: since, type: string, required: true, position: 1, description: "ISO date (e.g. 2025-01-01)" }
|
|
53
|
+
- { name: until, type: string, required: true, position: 2, description: "ISO date" }
|
|
54
|
+
sql: |
|
|
55
|
+
SELECT level, COUNT(*) AS events, MIN(created_at) AS first_seen, MAX(created_at) AS last_seen
|
|
56
|
+
FROM logs
|
|
57
|
+
WHERE created_at >= $since::timestamp
|
|
58
|
+
AND created_at < $until::timestamp
|
|
59
|
+
GROUP BY level
|
|
60
|
+
ORDER BY events DESC
|
|
61
|
+
|
|
62
|
+
# --------------------------------------------------------------------------- #
|
|
63
|
+
# multi-database operations - invoked as `dbctl diff A B --op <name>`
|
|
64
|
+
# --------------------------------------------------------------------------- #
|
|
65
|
+
user-count:
|
|
66
|
+
description: "Compare user counts between two databases"
|
|
67
|
+
scope: multi
|
|
68
|
+
mode: diff
|
|
69
|
+
roles: [src, trg]
|
|
70
|
+
queries:
|
|
71
|
+
src: "SELECT 'users' AS t, COUNT(*) AS n FROM users"
|
|
72
|
+
trg: "SELECT 'users' AS t, COUNT(*) AS n FROM users"
|
|
73
|
+
diff:
|
|
74
|
+
key: [t]
|
|
75
|
+
show: [n]
|
|
76
|
+
|
|
77
|
+
compare-quotas:
|
|
78
|
+
description: "Side-by-side quota summary across two databases"
|
|
79
|
+
scope: multi
|
|
80
|
+
mode: diff
|
|
81
|
+
roles: [src, trg]
|
|
82
|
+
parameters:
|
|
83
|
+
- { name: period, type: string, default: "Daily", position: 1 }
|
|
84
|
+
queries:
|
|
85
|
+
src: |
|
|
86
|
+
SELECT period AS t, COUNT(*) AS n_rows, SUM(limit_value) AS total_limit
|
|
87
|
+
FROM quotas JOIN users ON quotas.user_id = users.id
|
|
88
|
+
WHERE period = $period GROUP BY period
|
|
89
|
+
trg: |
|
|
90
|
+
SELECT period AS t, COUNT(*) AS n_rows, SUM(limit_value) AS total_limit
|
|
91
|
+
FROM quotas JOIN users ON quotas.user_id = users.id
|
|
92
|
+
WHERE period = $period GROUP BY period
|
|
93
|
+
diff:
|
|
94
|
+
key: [t]
|
|
95
|
+
show: [n_rows, total_limit]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
# Run on every push to main and on PRs touching python / packaging /
|
|
4
|
+
# workflow files. Use path filters so doc-only edits don't burn CI minutes.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
pull_request:
|
|
10
|
+
branches: [main]
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
# Cancel any in-progress run when a new commit lands on the same ref —
|
|
17
|
+
# saves CI minutes on rapid force-pushes and PR update churn.
|
|
18
|
+
concurrency:
|
|
19
|
+
group: ci-${{ github.ref }}
|
|
20
|
+
cancel-in-progress: true
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
# ----------------------------------------------------------------------- #
|
|
24
|
+
# lint + typecheck — fast, no project deps installed
|
|
25
|
+
# ----------------------------------------------------------------------- #
|
|
26
|
+
lint:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
timeout-minutes: 5
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
- name: Install uv
|
|
32
|
+
uses: astral-sh/setup-uv@v3
|
|
33
|
+
with:
|
|
34
|
+
enable-cache: true
|
|
35
|
+
cache-dependency-glob: "uv.lock"
|
|
36
|
+
- name: Set up Python
|
|
37
|
+
run: uv python install 3.12
|
|
38
|
+
- name: Install dev deps
|
|
39
|
+
# --frozen keeps CI honest: if uv.lock is out of date the job
|
|
40
|
+
# fails rather than silently re-resolving.
|
|
41
|
+
run: uv sync --frozen --extra dev
|
|
42
|
+
- name: Ruff (annotations PR-native)
|
|
43
|
+
# --output-format=github emits ::error/::warning annotations that
|
|
44
|
+
# show up directly on PR file diffs and the run summary.
|
|
45
|
+
run: uv run ruff check --output-format=github dbctl tests
|
|
46
|
+
- name: Ruff format check
|
|
47
|
+
run: uv run ruff format --check dbctl tests
|
|
48
|
+
- name: Mypy
|
|
49
|
+
# Non-blocking: the codebase has pre-existing annotation debt that
|
|
50
|
+
# is being paid down incrementally. Remove `continue-on-error`
|
|
51
|
+
# once the count reaches zero.
|
|
52
|
+
run: uv run mypy dbctl
|
|
53
|
+
continue-on-error: true
|
|
54
|
+
|
|
55
|
+
# ----------------------------------------------------------------------- #
|
|
56
|
+
# test — matrix across supported Python versions + OSes
|
|
57
|
+
# ----------------------------------------------------------------------- #
|
|
58
|
+
test:
|
|
59
|
+
needs: lint
|
|
60
|
+
runs-on: ${{ matrix.os }}
|
|
61
|
+
timeout-minutes: 15
|
|
62
|
+
strategy:
|
|
63
|
+
fail-fast: false
|
|
64
|
+
matrix:
|
|
65
|
+
# requires-python = ">=3.11" in pyproject.toml
|
|
66
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
67
|
+
os: [ubuntu-latest, macos-latest]
|
|
68
|
+
exclude:
|
|
69
|
+
# Trim the matrix: macos + 3.11 + 3.13 stay, but skip 3.13 on
|
|
70
|
+
# mac to keep minutes in check (Linux covers the linux/3.13 case).
|
|
71
|
+
- os: macos-latest
|
|
72
|
+
python-version: "3.11"
|
|
73
|
+
steps:
|
|
74
|
+
- uses: actions/checkout@v4
|
|
75
|
+
- name: Install uv
|
|
76
|
+
uses: astral-sh/setup-uv@v3
|
|
77
|
+
with:
|
|
78
|
+
enable-cache: true
|
|
79
|
+
cache-dependency-glob: "uv.lock"
|
|
80
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
81
|
+
run: uv python install ${{ matrix.python-version }}
|
|
82
|
+
- name: Install deps
|
|
83
|
+
# mssql extra needs ODBC at the system level (libodbc.so), which
|
|
84
|
+
# isn't on these runners by default; tests don't need it.
|
|
85
|
+
run: uv sync --frozen --extra dev --extra postgres --extra mysql
|
|
86
|
+
- name: Run pytest with coverage
|
|
87
|
+
# --junitxml so failed runs still produce a report artifact;
|
|
88
|
+
# --cov + --cov-report=xml for coverage upload.
|
|
89
|
+
run: |
|
|
90
|
+
uv run pytest tests/ \
|
|
91
|
+
--junitxml=reports/junit-${{ matrix.os }}-${{ matrix.python-version }}.xml \
|
|
92
|
+
--cov=dbctl \
|
|
93
|
+
--cov-report=xml:reports/coverage-${{ matrix.os }}-${{ matrix.python-version }}.xml \
|
|
94
|
+
--cov-report=term
|
|
95
|
+
- name: Upload test results
|
|
96
|
+
if: always() # upload even on failure so flaky-test data survives
|
|
97
|
+
uses: actions/upload-artifact@v4
|
|
98
|
+
with:
|
|
99
|
+
name: test-results-${{ matrix.os }}-${{ matrix.python-version }}
|
|
100
|
+
path: reports/
|
|
101
|
+
if-no-files-found: error
|
|
102
|
+
retention-days: 14
|
|
103
|
+
- name: Upload coverage to Codecov
|
|
104
|
+
# Codecov works tokenless on public repos via OIDC. On private
|
|
105
|
+
# repos add a CODECOV_TOKEN secret and uncomment `token:`.
|
|
106
|
+
if: always()
|
|
107
|
+
uses: codecov/codecov-action@v4
|
|
108
|
+
with:
|
|
109
|
+
files: reports/coverage-${{ matrix.os }}-${{ matrix.python-version }}.xml
|
|
110
|
+
disable_search: true
|
|
111
|
+
fail_ci_if_error: false
|
|
112
|
+
# token: ${{ secrets.CODECOV_TOKEN }}
|
|
113
|
+
|
|
114
|
+
# ----------------------------------------------------------------------- #
|
|
115
|
+
# build — wheel + sdist; upload artifact for inspection on every run
|
|
116
|
+
# ----------------------------------------------------------------------- #
|
|
117
|
+
build:
|
|
118
|
+
needs: test
|
|
119
|
+
runs-on: ubuntu-latest
|
|
120
|
+
timeout-minutes: 5
|
|
121
|
+
steps:
|
|
122
|
+
- uses: actions/checkout@v4
|
|
123
|
+
- name: Install uv
|
|
124
|
+
uses: astral-sh/setup-uv@v3
|
|
125
|
+
with:
|
|
126
|
+
enable-cache: true
|
|
127
|
+
cache-dependency-glob: "uv.lock"
|
|
128
|
+
- name: Set up Python
|
|
129
|
+
run: uv python install 3.12
|
|
130
|
+
- name: Build distributions
|
|
131
|
+
run: uv build
|
|
132
|
+
- name: Verify metadata
|
|
133
|
+
run: uv run --with twine twine check dist/*
|
|
134
|
+
- name: Upload artifacts
|
|
135
|
+
uses: actions/upload-artifact@v4
|
|
136
|
+
with:
|
|
137
|
+
name: dist
|
|
138
|
+
path: dist/
|
|
139
|
+
if-no-files-found: error
|
|
140
|
+
|
|
141
|
+
# ----------------------------------------------------------------------- #
|
|
142
|
+
# publish — only on a pushed v* tag, gated on test + build
|
|
143
|
+
# ----------------------------------------------------------------------- #
|
|
144
|
+
publish:
|
|
145
|
+
needs: [test, build]
|
|
146
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
147
|
+
runs-on: ubuntu-latest
|
|
148
|
+
timeout-minutes: 5
|
|
149
|
+
# Dedicated environment — configure "pypi" under repo settings →
|
|
150
|
+
# Environments with required reviewers/wait-for-approval before the
|
|
151
|
+
# job can publish. PyPI trusted publishing needs id-token: write.
|
|
152
|
+
environment:
|
|
153
|
+
name: pypi
|
|
154
|
+
url: https://pypi.org/p/dbctl
|
|
155
|
+
permissions:
|
|
156
|
+
contents: read
|
|
157
|
+
id-token: write # OIDC trusted publishing
|
|
158
|
+
steps:
|
|
159
|
+
- name: Download artifacts
|
|
160
|
+
uses: actions/download-artifact@v4
|
|
161
|
+
with:
|
|
162
|
+
name: dist
|
|
163
|
+
path: dist/
|
|
164
|
+
- name: Publish to PyPI
|
|
165
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
dbctl-0.1.1/.gitignore
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
*.egg
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
.eggs/
|
|
10
|
+
|
|
11
|
+
# virtual envs
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
env/
|
|
15
|
+
|
|
16
|
+
# test / lint caches
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.ruff_cache/
|
|
19
|
+
.mypy_cache/
|
|
20
|
+
.coverage
|
|
21
|
+
htmlcov/
|
|
22
|
+
.tox/
|
|
23
|
+
|
|
24
|
+
# editors / OS
|
|
25
|
+
.idea/
|
|
26
|
+
.vscode/
|
|
27
|
+
.DS_Store
|
|
28
|
+
*.swp
|
|
29
|
+
*.swo
|
|
30
|
+
*~
|
|
31
|
+
|
|
32
|
+
# local dbctl runtime state (sample configs live under .dbctl/ in the repo;
|
|
33
|
+
# ignore the per-user history/profile data that dbctl writes at runtime)
|
|
34
|
+
.dbctl/history.jsonl
|
|
35
|
+
.dbctl/profiles/
|
dbctl-0.1.1/CHANGELOG.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.1] — 2026-08-01
|
|
9
|
+
|
|
10
|
+
Retag of the initial public release (v0.1.0 publish failed; the trusted
|
|
11
|
+
publisher had not yet been registered on PyPI). No code changes — only
|
|
12
|
+
the version bump.
|
|
13
|
+
|
|
14
|
+
## [0.1.0] — 2026-08-01
|
|
15
|
+
|
|
16
|
+
Initial public release.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **Connection registry** (`~/.dbctl/connections.yaml`) validated with pydantic
|
|
21
|
+
v2, supporting three tunnel types:
|
|
22
|
+
- `ssm` — AWS SSM port-forward through an EC2 bastion, shelled out to the
|
|
23
|
+
`aws` CLI. Supports `bastion_instance_id` or `bastion_tags` (resolved via
|
|
24
|
+
`aws ec2 describe-instances`).
|
|
25
|
+
- `ssh` — classic `ssh -N -L` port-forward, shelled out to the `ssh` CLI
|
|
26
|
+
with `ExitOnForwardFailure=yes` and `StrictHostKeyChecking=accept-new`.
|
|
27
|
+
- `direct` — no tunnel, connect to upstream host:port.
|
|
28
|
+
- **Operations registry** (`~/.dbctl/operations.yaml`) with two scopes:
|
|
29
|
+
- `single` — `dbctl <conn> <op> ...` with `execute` / `fetch` / `fetch_one` /
|
|
30
|
+
`script` / `upsert` modes.
|
|
31
|
+
- `multi` — `dbctl diff <op> <src> <trg> ...` with `diff` mode (side-by-side
|
|
32
|
+
join on a `key`); `compare` / `sync` modes reserved for v2.
|
|
33
|
+
- **Dynamic CLI** built from the registries: one Click subcommand per
|
|
34
|
+
connection, one per declared operation. Positional params via `position:`,
|
|
35
|
+
keyword params via `--flag`, all generated from the YAML declaration.
|
|
36
|
+
- **`$name` placeholders** rewritten to SQLAlchemy bind-params (`:name`) —
|
|
37
|
+
values are always parameterised, never string-interpolated.
|
|
38
|
+
- **Safety model**:
|
|
39
|
+
- `safety.confirm: true` makes DML **dry-run by default**; `--apply`
|
|
40
|
+
commits, `--yes` skips the prompt. Confirm happens *before* the
|
|
41
|
+
transaction opens so `N` leaves the DB untouched.
|
|
42
|
+
- `safety.read_only: true` blocks every DML op.
|
|
43
|
+
- `safety.allowed_operations: [...]` whitelists op names.
|
|
44
|
+
- **Audit log** at `~/.dbctl/history.jsonl` — one JSON event per run; secret
|
|
45
|
+
parameters redacted. `dbctl history list` / `dbctl <conn> history` /
|
|
46
|
+
`dbctl <conn> again` (re-run last).
|
|
47
|
+
- **Dashboard** — `dbctl` bare shows connections table; `dbctl <conn>` shows
|
|
48
|
+
a connection page with health, info queries, and available ops.
|
|
49
|
+
- **`dbctl doctor`** — healthcheck every connection.
|
|
50
|
+
- **`dbctl init`** — interactive wizard that writes/merges a new connection
|
|
51
|
+
and tests the tunnel + healthcheck before saving.
|
|
52
|
+
- **Shell completion** via `dbctl --install-completion bash|zsh|fish`.
|
|
53
|
+
- **`--profile <name>`** — swap config dir to `~/.dbctl/profiles/<name>/`.
|
|
54
|
+
- **Connection aliases** — `prod` resolves to `db1`, etc.
|
|
55
|
+
- **Bundled test fleet** — `docker-compose.yml` brings up postgres on
|
|
56
|
+
`:5433`, mysql on `:3307`, mssql on `:1434` with the same four-table
|
|
57
|
+
schema (`users`, `quotas`, `usage`, `logs`) and slightly different sample
|
|
58
|
+
data for diff testing.
|
|
59
|
+
- **15 unit tests** against in-memory SQLite covering placeholder rewriting,
|
|
60
|
+
parameter binding/coercion, mode routing, side-by-side diff, and audit
|
|
61
|
+
redaction.
|
|
62
|
+
|
|
63
|
+
### Documentation
|
|
64
|
+
|
|
65
|
+
- `README.md` — usage, install, quick start, config layout, safety model.
|
|
66
|
+
- `docs/connections.md` — full `connections.yaml` reference with examples.
|
|
67
|
+
- `docs/operations.md` — full `operations.yaml` reference with the safety
|
|
68
|
+
check matrix.
|
|
69
|
+
- `docs/DESIGN.md` — architecture, layering, dynamic CLI, placeholder
|
|
70
|
+
semantics, confirm-before-commit invariant, exit codes.
|
|
71
|
+
|
|
72
|
+
### Known limitations (v1)
|
|
73
|
+
|
|
74
|
+
- `mode: script` runs only the first statement in v1; multi-statement
|
|
75
|
+
scripts are v2.
|
|
76
|
+
- `mode: upsert` is reserved — the autoload/dialect-aware conflict logic is
|
|
77
|
+
v2.
|
|
78
|
+
- Operations are dialect-specific (no per-connection overrides).
|
|
79
|
+
- `${var}` identifier interpolation is intentionally absent (would be a SQL
|
|
80
|
+
injection vector with naive `str.replace`); use one op per table for now.
|
|
81
|
+
- Multi-DB tunnels open sequentially (parallel is v2).
|