dataplat 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dataplat/__init__.py +3 -0
- dataplat/cli/__init__.py +1 -0
- dataplat/cli/_missing.py +108 -0
- dataplat/cli/bi/__init__.py +1 -0
- dataplat/cli/bi/app.py +14 -0
- dataplat/cli/bi/superset.py +558 -0
- dataplat/cli/ci/__init__.py +1 -0
- dataplat/cli/ci/app.py +14 -0
- dataplat/cli/ci/github/__init__.py +1 -0
- dataplat/cli/ci/github/app.py +10 -0
- dataplat/cli/ci/github/runner.py +399 -0
- dataplat/cli/cloud/__init__.py +1 -0
- dataplat/cli/cloud/app.py +14 -0
- dataplat/cli/cloud/aws/__init__.py +1 -0
- dataplat/cli/cloud/aws/_common.py +77 -0
- dataplat/cli/cloud/aws/app.py +12 -0
- dataplat/cli/cloud/aws/rds.py +686 -0
- dataplat/cli/cloud/aws/redshift.py +312 -0
- dataplat/cli/cloud/aws/secrets.py +875 -0
- dataplat/cli/config.py +492 -0
- dataplat/cli/db/__init__.py +342 -0
- dataplat/cli/db/_common.py +130 -0
- dataplat/cli/db/_report.py +180 -0
- dataplat/cli/db/dbt_orphans.py +842 -0
- dataplat/cli/db/describe.py +939 -0
- dataplat/cli/db/long_queries.py +299 -0
- dataplat/cli/db/role.py +434 -0
- dataplat/cli/db/role_create.py +452 -0
- dataplat/cli/db/role_drop.py +290 -0
- dataplat/cli/db/role_list.py +147 -0
- dataplat/cli/db/top_tables.py +337 -0
- dataplat/cli/ingest/__init__.py +1 -0
- dataplat/cli/ingest/airbyte/__init__.py +5 -0
- dataplat/cli/ingest/airbyte/_common.py +36 -0
- dataplat/cli/ingest/airbyte/_cursor.py +268 -0
- dataplat/cli/ingest/airbyte/_resource.py +192 -0
- dataplat/cli/ingest/airbyte/app.py +31 -0
- dataplat/cli/ingest/airbyte/connections.py +1234 -0
- dataplat/cli/ingest/airbyte/definitions.py +122 -0
- dataplat/cli/ingest/airbyte/destinations.py +26 -0
- dataplat/cli/ingest/airbyte/enums.py +45 -0
- dataplat/cli/ingest/airbyte/jobs.py +112 -0
- dataplat/cli/ingest/airbyte/sources.py +26 -0
- dataplat/cli/ingest/airbyte/tags.py +57 -0
- dataplat/cli/ingest/airbyte/templates.py +144 -0
- dataplat/cli/ingest/airbyte/tui.py +123 -0
- dataplat/cli/ingest/airbyte/workspaces.py +80 -0
- dataplat/cli/ingest/app.py +14 -0
- dataplat/cli/open.py +117 -0
- dataplat/cli/status.py +308 -0
- dataplat/core/__init__.py +1 -0
- dataplat/core/deps.py +138 -0
- dataplat/core/envrc.py +125 -0
- dataplat/core/errors.py +23 -0
- dataplat/main.py +87 -0
- dataplat/services/__init__.py +1 -0
- dataplat/services/airbyte/__init__.py +1 -0
- dataplat/services/airbyte/_resource.py +113 -0
- dataplat/services/airbyte/client.py +280 -0
- dataplat/services/airbyte/connections.py +306 -0
- dataplat/services/airbyte/definitions.py +64 -0
- dataplat/services/airbyte/destinations.py +68 -0
- dataplat/services/airbyte/jobs.py +72 -0
- dataplat/services/airbyte/sources.py +58 -0
- dataplat/services/airbyte/tags.py +120 -0
- dataplat/services/airbyte/workspaces.py +47 -0
- dataplat/services/aws/__init__.py +1 -0
- dataplat/services/aws/auth.py +80 -0
- dataplat/services/db/__init__.py +1 -0
- dataplat/services/db/connection.py +169 -0
- dataplat/services/db/describe.py +1034 -0
- dataplat/services/db/long_queries.py +378 -0
- dataplat/services/db/orphans.py +429 -0
- dataplat/services/db/role.py +812 -0
- dataplat/services/db/role_admin.py +458 -0
- dataplat/services/db/role_dialects.py +521 -0
- dataplat/services/db/targets.py +128 -0
- dataplat/services/db/top_tables.py +193 -0
- dataplat/services/superset/__init__.py +1 -0
- dataplat/services/superset/client.py +319 -0
- dataplat-0.1.0.dist-info/METADATA +326 -0
- dataplat-0.1.0.dist-info/RECORD +85 -0
- dataplat-0.1.0.dist-info/WHEEL +4 -0
- dataplat-0.1.0.dist-info/entry_points.txt +2 -0
- dataplat-0.1.0.dist-info/licenses/LICENSE +21 -0
dataplat/cli/ci/app.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""CI command area."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from dataplat.cli.ci.github.app import app as github_app
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(
|
|
10
|
+
name="ci",
|
|
11
|
+
help="CI tools (GitHub Actions runners)",
|
|
12
|
+
no_args_is_help=True,
|
|
13
|
+
)
|
|
14
|
+
app.add_typer(github_app, name="github")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""GitHub CLI groups."""
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""GitHub Actions runner management commands."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(
|
|
14
|
+
name="runner",
|
|
15
|
+
help="Manage GitHub Actions self-hosted runners",
|
|
16
|
+
no_args_is_help=True,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
DEFAULT_IMAGE = "myoung34/github-runner:2.334.0-ubuntu-noble"
|
|
22
|
+
DEFAULT_RUNNER_STATE_DIR = Path.home() / ".config" / "dataplat" / "github-runner"
|
|
23
|
+
|
|
24
|
+
# Extra DNS resolvers for the runner container. Docker Desktop does not honor
|
|
25
|
+
# macOS per-domain VPN resolver scopes, so a container may get only public DNS
|
|
26
|
+
# and fail to resolve internal hostnames; point DP_CI_RUNNER_DNS (comma-
|
|
27
|
+
# separated) at your VPN resolver when that bites.
|
|
28
|
+
DEFAULT_DNS = [
|
|
29
|
+
s.strip() for s in os.getenv("DP_CI_RUNNER_DNS", "").split(",") if s.strip()
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run_command(
|
|
34
|
+
cmd: list[str],
|
|
35
|
+
check: bool = True,
|
|
36
|
+
env: dict[str, str] | None = None,
|
|
37
|
+
) -> subprocess.CompletedProcess:
|
|
38
|
+
"""Run a shell command and return the result.
|
|
39
|
+
|
|
40
|
+
``env``, when given, is the full environment for the child process —
|
|
41
|
+
used to hand secrets to ``docker`` without putting them in argv.
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
result = subprocess.run(
|
|
45
|
+
cmd,
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
48
|
+
check=check,
|
|
49
|
+
env=env,
|
|
50
|
+
)
|
|
51
|
+
return result
|
|
52
|
+
except FileNotFoundError:
|
|
53
|
+
console.print(f"[red]Error: {cmd[0]} not found on PATH[/red]")
|
|
54
|
+
raise typer.Exit(code=1)
|
|
55
|
+
except subprocess.CalledProcessError as e:
|
|
56
|
+
console.print(f"[red]Command failed: {' '.join(cmd)}[/red]")
|
|
57
|
+
console.print(f"[red]Error: {e.stderr}[/red]")
|
|
58
|
+
raise typer.Exit(code=1)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def ensure_docker_available() -> None:
|
|
62
|
+
"""Exit with a clear message when docker is missing or the daemon is down."""
|
|
63
|
+
result = run_command(["docker", "info", "--format", "{{.ServerVersion}}"], check=False)
|
|
64
|
+
if result.returncode != 0:
|
|
65
|
+
detail = (result.stderr or "").strip().splitlines()
|
|
66
|
+
hint = detail[0] if detail else "daemon not reachable"
|
|
67
|
+
console.print(f"[red]Error: docker daemon unavailable ({hint})[/red]")
|
|
68
|
+
raise typer.Exit(code=1)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_env_var(name: str) -> str:
|
|
72
|
+
"""Get an environment variable or exit with error."""
|
|
73
|
+
value = os.getenv(name)
|
|
74
|
+
if not value:
|
|
75
|
+
console.print(f"[red]Error: {name} environment variable must be set[/red]")
|
|
76
|
+
raise typer.Exit(code=1)
|
|
77
|
+
return value
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def ensure_image_present(image: str) -> None:
|
|
81
|
+
"""Ensure ``image`` is available locally, streaming the pull if not.
|
|
82
|
+
|
|
83
|
+
``docker run`` will pull silently when the image is missing, but the
|
|
84
|
+
surrounding ``run_command`` captures stdout/stderr — so a multi-GB pull
|
|
85
|
+
looks like a hang. Stream the pull through the user's terminal instead.
|
|
86
|
+
"""
|
|
87
|
+
inspect = run_command(["docker", "image", "inspect", image], check=False)
|
|
88
|
+
if getattr(inspect, "returncode", 0) == 0:
|
|
89
|
+
return
|
|
90
|
+
console.print(f"[dim]Image {image} not present locally. Pulling…[/dim]")
|
|
91
|
+
try:
|
|
92
|
+
subprocess.run(["docker", "pull", image], check=True)
|
|
93
|
+
except subprocess.CalledProcessError:
|
|
94
|
+
console.print(f"[red]Failed to pull image: {image}[/red]")
|
|
95
|
+
raise typer.Exit(code=1)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_container_name(runner_name: str) -> str:
|
|
99
|
+
"""Generate a Docker-safe container name from the runner name."""
|
|
100
|
+
return f"gha-runner-{_slug(runner_name, fallback='runner')}"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_runner_state_dir() -> Path:
|
|
104
|
+
"""Return the local state directory for runner mount data."""
|
|
105
|
+
override = os.getenv("DP_GITHUB_RUNNER_STATE_DIR")
|
|
106
|
+
if override:
|
|
107
|
+
return Path(override).expanduser()
|
|
108
|
+
return DEFAULT_RUNNER_STATE_DIR
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _slug(value: str, fallback: str) -> str:
|
|
112
|
+
"""Normalize arbitrary text for local filesystem paths."""
|
|
113
|
+
normalized = re.sub(r"[^A-Za-z0-9._-]+", "-", value).strip("-.")
|
|
114
|
+
if normalized:
|
|
115
|
+
return normalized
|
|
116
|
+
return fallback
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _normalize_repo_ref(repo_url: str) -> str:
|
|
120
|
+
"""Normalize repository URLs to a stable reference."""
|
|
121
|
+
candidate = repo_url.strip()
|
|
122
|
+
if candidate.endswith(".git"):
|
|
123
|
+
candidate = candidate[:-4]
|
|
124
|
+
|
|
125
|
+
if "://" in candidate:
|
|
126
|
+
parsed = urlparse(candidate)
|
|
127
|
+
if parsed.netloc and parsed.path:
|
|
128
|
+
return f"{parsed.netloc}/{parsed.path.strip('/')}".lower()
|
|
129
|
+
|
|
130
|
+
if "@" in candidate and ":" in candidate.split("@", 1)[1]:
|
|
131
|
+
_, remote = candidate.split("@", 1)
|
|
132
|
+
host, path = remote.split(":", 1)
|
|
133
|
+
return f"{host}/{path.strip('/')}".lower()
|
|
134
|
+
|
|
135
|
+
return candidate.lower()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _repo_id(repo_url: str) -> str:
|
|
139
|
+
"""Build a filesystem-safe identifier for a repository URL."""
|
|
140
|
+
return _slug(_normalize_repo_ref(repo_url), fallback="repo")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_repo_mount_dir(repo_url: str) -> Path:
|
|
144
|
+
"""Return the default host mount directory for a repository."""
|
|
145
|
+
return get_runner_state_dir() / "workdirs" / _repo_id(repo_url)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_repo_mount_record_path(repo_url: str) -> Path:
|
|
149
|
+
"""Return the path of the mount-record file for a repository."""
|
|
150
|
+
return get_runner_state_dir() / "mounts" / f"{_repo_id(repo_url)}.path"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def resolve_local_workdir(repo_url: str, local_workdir: str | None) -> Path:
|
|
154
|
+
"""Resolve (and create) the local workdir for a repository mount."""
|
|
155
|
+
workdir = (
|
|
156
|
+
Path(local_workdir).expanduser()
|
|
157
|
+
if local_workdir
|
|
158
|
+
else get_repo_mount_dir(repo_url)
|
|
159
|
+
)
|
|
160
|
+
workdir.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
return workdir.resolve()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def record_repo_mount(repo_url: str, resolved_workdir: Path) -> Path:
|
|
165
|
+
"""Persist the mount record after a successful container start."""
|
|
166
|
+
mount_record_path = get_repo_mount_record_path(repo_url)
|
|
167
|
+
mount_record_path.parent.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
mount_record_path.write_text(f"{resolved_workdir}\n")
|
|
169
|
+
return mount_record_path
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@app.command()
|
|
173
|
+
def start(
|
|
174
|
+
runner_name: str = typer.Option(
|
|
175
|
+
..., "--runner-name", "-n", help="Name for the GitHub Actions runner"
|
|
176
|
+
),
|
|
177
|
+
repo_url: str = typer.Option(
|
|
178
|
+
...,
|
|
179
|
+
"--repo-url",
|
|
180
|
+
"-r",
|
|
181
|
+
help="Repository URL (e.g., https://github.com/org/repo)",
|
|
182
|
+
),
|
|
183
|
+
runner_scope: str = typer.Option(
|
|
184
|
+
"repo", "--scope", "-s", help="Runner scope (repo, org, or enterprise)"
|
|
185
|
+
),
|
|
186
|
+
runner_workdir: str = typer.Option(
|
|
187
|
+
"/tmp/.github/runner", "--workdir", "-w", help="Runner working directory"
|
|
188
|
+
),
|
|
189
|
+
debug_output: bool = typer.Option(
|
|
190
|
+
True, "--debug/--no-debug", "-d/-D", help="Enable debug output"
|
|
191
|
+
),
|
|
192
|
+
local_workdir: str | None = typer.Option(
|
|
193
|
+
None,
|
|
194
|
+
"--local-workdir",
|
|
195
|
+
"-l",
|
|
196
|
+
help=(
|
|
197
|
+
"Local directory to mount as runner workdir. "
|
|
198
|
+
"Defaults to ~/.config/dataplat/github-runner/workdirs/<repository-id>"
|
|
199
|
+
),
|
|
200
|
+
),
|
|
201
|
+
image: str = typer.Option(
|
|
202
|
+
DEFAULT_IMAGE, "--image", "-i", help="Docker image to use"
|
|
203
|
+
),
|
|
204
|
+
dns: list[str] = typer.Option(
|
|
205
|
+
DEFAULT_DNS,
|
|
206
|
+
"--dns",
|
|
207
|
+
help=(
|
|
208
|
+
"DNS server(s) for the runner container (repeatable). Defaults "
|
|
209
|
+
"to DP_CI_RUNNER_DNS (comma-separated); useful when the runner "
|
|
210
|
+
"must use a VPN resolver for internal hostnames. Pass --dns '' "
|
|
211
|
+
"for Docker's default DNS."
|
|
212
|
+
),
|
|
213
|
+
),
|
|
214
|
+
):
|
|
215
|
+
"""Start the GitHub Actions runner container."""
|
|
216
|
+
console.print("[blue]Starting GitHub Actions runner...[/blue]")
|
|
217
|
+
|
|
218
|
+
# Check for required environment variables
|
|
219
|
+
app_id = get_env_var("GHA_APP_ID")
|
|
220
|
+
app_private_key = get_env_var("GHA_APP_PRIVATE_KEY")
|
|
221
|
+
|
|
222
|
+
ensure_docker_available()
|
|
223
|
+
container_name = get_container_name(runner_name)
|
|
224
|
+
|
|
225
|
+
# Check for existing container
|
|
226
|
+
console.print("Checking for existing GitHub Actions runner...")
|
|
227
|
+
result = run_command(
|
|
228
|
+
["docker", "ps", "-a", "-q", "-f", f"name={container_name}"], check=False
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if result.stdout.strip():
|
|
232
|
+
console.print("Found existing runner container. Removing it...")
|
|
233
|
+
run_command(["docker", "stop", container_name], check=False)
|
|
234
|
+
run_command(["docker", "rm", container_name], check=False)
|
|
235
|
+
|
|
236
|
+
# Start new container
|
|
237
|
+
console.print("Starting new GitHub Actions runner container...")
|
|
238
|
+
ensure_image_present(image)
|
|
239
|
+
resolved_local_workdir = resolve_local_workdir(repo_url, local_workdir)
|
|
240
|
+
|
|
241
|
+
docker_cmd = [
|
|
242
|
+
"docker",
|
|
243
|
+
"run",
|
|
244
|
+
"-d",
|
|
245
|
+
"--restart",
|
|
246
|
+
"on-failure:5",
|
|
247
|
+
"--name",
|
|
248
|
+
container_name,
|
|
249
|
+
]
|
|
250
|
+
# Pin DNS when internal hostnames need a specific resolver (see
|
|
251
|
+
# DEFAULT_DNS). Empty entries fall back to Docker's default resolver.
|
|
252
|
+
for dns_server in dns:
|
|
253
|
+
if dns_server and dns_server.strip():
|
|
254
|
+
docker_cmd += ["--dns", dns_server.strip()]
|
|
255
|
+
# Secrets are handed to the docker CLI via its process environment and
|
|
256
|
+
# referenced by bare `-e NAME` flags, so the private key never appears
|
|
257
|
+
# in argv (`ps`) or `docker inspect`-able command lines on the host side.
|
|
258
|
+
docker_cmd += [
|
|
259
|
+
"-e",
|
|
260
|
+
f"RUNNER_NAME={runner_name}",
|
|
261
|
+
"-e",
|
|
262
|
+
"APP_ID",
|
|
263
|
+
"-e",
|
|
264
|
+
"APP_PRIVATE_KEY",
|
|
265
|
+
"-e",
|
|
266
|
+
f"RUNNER_SCOPE={runner_scope}",
|
|
267
|
+
"-e",
|
|
268
|
+
f"RUNNER_WORKDIR={runner_workdir}",
|
|
269
|
+
"-e",
|
|
270
|
+
f"DEBUG_OUTPUT={str(debug_output).lower()}",
|
|
271
|
+
"-e",
|
|
272
|
+
f"REPO_URL={repo_url}",
|
|
273
|
+
"-v",
|
|
274
|
+
"/var/run/docker.sock:/var/run/docker.sock",
|
|
275
|
+
"-v",
|
|
276
|
+
f"{resolved_local_workdir}:{runner_workdir}",
|
|
277
|
+
image,
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
docker_env = {
|
|
281
|
+
**os.environ,
|
|
282
|
+
"APP_ID": app_id,
|
|
283
|
+
"APP_PRIVATE_KEY": app_private_key,
|
|
284
|
+
}
|
|
285
|
+
run_command(docker_cmd, env=docker_env)
|
|
286
|
+
record_repo_mount(repo_url, resolved_local_workdir)
|
|
287
|
+
console.print("[green]✓ GitHub Actions runner started successfully[/green]")
|
|
288
|
+
console.print(f"[dim]Container name: {container_name}[/dim]")
|
|
289
|
+
console.print(f"[dim]Local mount: {resolved_local_workdir}[/dim]")
|
|
290
|
+
console.print(f"[dim]Mount record: {get_repo_mount_record_path(repo_url)}[/dim]")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@app.command()
|
|
294
|
+
def stop(
|
|
295
|
+
runner_name: str = typer.Option(
|
|
296
|
+
..., "--runner-name", "-n", help="Name of the GitHub Actions runner to stop"
|
|
297
|
+
),
|
|
298
|
+
):
|
|
299
|
+
"""Stop and remove the GitHub Actions runner container."""
|
|
300
|
+
console.print("[blue]Stopping GitHub Actions runner...[/blue]")
|
|
301
|
+
|
|
302
|
+
ensure_docker_available()
|
|
303
|
+
container_name = get_container_name(runner_name)
|
|
304
|
+
|
|
305
|
+
# Check if container is running
|
|
306
|
+
result = run_command(
|
|
307
|
+
["docker", "ps", "-q", "-f", f"name={container_name}"], check=False
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if result.stdout.strip():
|
|
311
|
+
run_command(["docker", "stop", container_name])
|
|
312
|
+
run_command(["docker", "rm", container_name])
|
|
313
|
+
console.print("[green]✓ GitHub Actions runner stopped and removed[/green]")
|
|
314
|
+
else:
|
|
315
|
+
console.print("[yellow]! No running GitHub Actions runner found[/yellow]")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@app.command()
|
|
319
|
+
def status(
|
|
320
|
+
runner_name: str = typer.Option(
|
|
321
|
+
..., "--runner-name", "-n", help="Name of the GitHub Actions runner to check"
|
|
322
|
+
),
|
|
323
|
+
):
|
|
324
|
+
"""Check the status of the GitHub Actions runner."""
|
|
325
|
+
console.print("[blue]GitHub Actions runner status:[/blue]\n")
|
|
326
|
+
|
|
327
|
+
ensure_docker_available()
|
|
328
|
+
container_name = get_container_name(runner_name)
|
|
329
|
+
|
|
330
|
+
# Check if container is running
|
|
331
|
+
running_result = run_command(
|
|
332
|
+
["docker", "ps", "-q", "-f", f"name={container_name}"], check=False
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if running_result.stdout.strip():
|
|
336
|
+
console.print("[green]✓ Runner is running[/green]\n")
|
|
337
|
+
|
|
338
|
+
# Get detailed status
|
|
339
|
+
result = run_command(
|
|
340
|
+
[
|
|
341
|
+
"docker",
|
|
342
|
+
"ps",
|
|
343
|
+
"-f",
|
|
344
|
+
f"name={container_name}",
|
|
345
|
+
"--format",
|
|
346
|
+
"{{.Names}}\t{{.Status}}\t{{.Ports}}",
|
|
347
|
+
],
|
|
348
|
+
check=False,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if result.stdout.strip():
|
|
352
|
+
lines = result.stdout.strip().split("\n")
|
|
353
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
354
|
+
table.add_column("Name")
|
|
355
|
+
table.add_column("Status")
|
|
356
|
+
table.add_column("Ports")
|
|
357
|
+
|
|
358
|
+
for line in lines:
|
|
359
|
+
parts = line.split("\t")
|
|
360
|
+
table.add_row(*parts)
|
|
361
|
+
|
|
362
|
+
console.print(table)
|
|
363
|
+
else:
|
|
364
|
+
# Check if container exists but is not running
|
|
365
|
+
exists_result = run_command(
|
|
366
|
+
["docker", "ps", "-a", "-q", "-f", f"name={container_name}"], check=False
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
if exists_result.stdout.strip():
|
|
370
|
+
console.print(
|
|
371
|
+
"[yellow]! Runner container exists but is not running[/yellow]\n"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
result = run_command(
|
|
375
|
+
[
|
|
376
|
+
"docker",
|
|
377
|
+
"ps",
|
|
378
|
+
"-a",
|
|
379
|
+
"-f",
|
|
380
|
+
f"name={container_name}",
|
|
381
|
+
"--format",
|
|
382
|
+
"{{.Names}}\t{{.Status}}",
|
|
383
|
+
],
|
|
384
|
+
check=False,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
if result.stdout.strip():
|
|
388
|
+
lines = result.stdout.strip().split("\n")
|
|
389
|
+
table = Table(show_header=True, header_style="bold yellow")
|
|
390
|
+
table.add_column("Name")
|
|
391
|
+
table.add_column("Status")
|
|
392
|
+
|
|
393
|
+
for line in lines:
|
|
394
|
+
parts = line.split("\t")
|
|
395
|
+
table.add_row(*parts)
|
|
396
|
+
|
|
397
|
+
console.print(table)
|
|
398
|
+
else:
|
|
399
|
+
console.print("[red]✗ No runner container found[/red]")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Cloud area — cloud-provider operations."""
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Cloud command area."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from dataplat.cli.cloud.aws.app import app as aws_app
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(
|
|
10
|
+
name="cloud",
|
|
11
|
+
help="Cloud-provider tools (AWS)",
|
|
12
|
+
no_args_is_help=True,
|
|
13
|
+
)
|
|
14
|
+
app.add_typer(aws_app, name="aws")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""AWS CLI groups."""
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Shared helpers for the aws command group."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich import box
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from dataplat.core.errors import AuthError
|
|
14
|
+
from dataplat.services.aws.auth import get_session
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def default_profile() -> str:
|
|
18
|
+
"""AWS profile used when --profile is omitted."""
|
|
19
|
+
return os.getenv("DP_AWS_PROFILE") or "default"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def default_region() -> str | None:
|
|
23
|
+
"""AWS region used when --region is omitted (None → profile default)."""
|
|
24
|
+
return (
|
|
25
|
+
os.getenv("DP_AWS_REGION")
|
|
26
|
+
or os.getenv("AWS_REGION")
|
|
27
|
+
or os.getenv("AWS_DEFAULT_REGION")
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def cli_session(console: Console, profile: str | None, region: str | None):
|
|
32
|
+
"""Return a boto3 Session, converting auth failures into a clean exit."""
|
|
33
|
+
try:
|
|
34
|
+
return get_session(
|
|
35
|
+
profile=profile or default_profile(),
|
|
36
|
+
region=region or default_region(),
|
|
37
|
+
notify=lambda msg: console.print(f"[yellow]{msg}[/yellow]"),
|
|
38
|
+
)
|
|
39
|
+
except AuthError as exc:
|
|
40
|
+
console.print(f"[red]{exc}[/red]")
|
|
41
|
+
raise typer.Exit(code=1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def make_table(title: str) -> Table:
|
|
45
|
+
"""The aws group's shared table style."""
|
|
46
|
+
return Table(
|
|
47
|
+
title=title,
|
|
48
|
+
box=box.SIMPLE_HEAVY,
|
|
49
|
+
header_style="bold bright_white",
|
|
50
|
+
title_style="bold cyan",
|
|
51
|
+
border_style="bright_black",
|
|
52
|
+
row_styles=["", "on #2a2a2a"],
|
|
53
|
+
show_lines=False,
|
|
54
|
+
pad_edge=False,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def profile_option(default: str | None = None) -> Any:
|
|
59
|
+
return typer.Option(
|
|
60
|
+
default,
|
|
61
|
+
"--profile",
|
|
62
|
+
"-p",
|
|
63
|
+
help="AWS profile name. Defaults to DP_AWS_PROFILE or 'default'.",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def region_option(default: str | None = None) -> Any:
|
|
68
|
+
return typer.Option(
|
|
69
|
+
default,
|
|
70
|
+
"--region",
|
|
71
|
+
"-r",
|
|
72
|
+
help="AWS region. Defaults to DP_AWS_REGION/AWS_REGION or the "
|
|
73
|
+
"profile's region.",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
JsonOption = typer.Option(False, "--json", help="Emit JSON instead of a table.")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""AWS command group."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from dataplat.cli.cloud.aws import rds, redshift, secrets
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(name="aws", help="AWS operations", no_args_is_help=True)
|
|
10
|
+
app.add_typer(secrets.app, name="secrets")
|
|
11
|
+
app.add_typer(rds.app, name="rds")
|
|
12
|
+
app.add_typer(redshift.app, name="redshift")
|