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.
Files changed (85) hide show
  1. dataplat/__init__.py +3 -0
  2. dataplat/cli/__init__.py +1 -0
  3. dataplat/cli/_missing.py +108 -0
  4. dataplat/cli/bi/__init__.py +1 -0
  5. dataplat/cli/bi/app.py +14 -0
  6. dataplat/cli/bi/superset.py +558 -0
  7. dataplat/cli/ci/__init__.py +1 -0
  8. dataplat/cli/ci/app.py +14 -0
  9. dataplat/cli/ci/github/__init__.py +1 -0
  10. dataplat/cli/ci/github/app.py +10 -0
  11. dataplat/cli/ci/github/runner.py +399 -0
  12. dataplat/cli/cloud/__init__.py +1 -0
  13. dataplat/cli/cloud/app.py +14 -0
  14. dataplat/cli/cloud/aws/__init__.py +1 -0
  15. dataplat/cli/cloud/aws/_common.py +77 -0
  16. dataplat/cli/cloud/aws/app.py +12 -0
  17. dataplat/cli/cloud/aws/rds.py +686 -0
  18. dataplat/cli/cloud/aws/redshift.py +312 -0
  19. dataplat/cli/cloud/aws/secrets.py +875 -0
  20. dataplat/cli/config.py +492 -0
  21. dataplat/cli/db/__init__.py +342 -0
  22. dataplat/cli/db/_common.py +130 -0
  23. dataplat/cli/db/_report.py +180 -0
  24. dataplat/cli/db/dbt_orphans.py +842 -0
  25. dataplat/cli/db/describe.py +939 -0
  26. dataplat/cli/db/long_queries.py +299 -0
  27. dataplat/cli/db/role.py +434 -0
  28. dataplat/cli/db/role_create.py +452 -0
  29. dataplat/cli/db/role_drop.py +290 -0
  30. dataplat/cli/db/role_list.py +147 -0
  31. dataplat/cli/db/top_tables.py +337 -0
  32. dataplat/cli/ingest/__init__.py +1 -0
  33. dataplat/cli/ingest/airbyte/__init__.py +5 -0
  34. dataplat/cli/ingest/airbyte/_common.py +36 -0
  35. dataplat/cli/ingest/airbyte/_cursor.py +268 -0
  36. dataplat/cli/ingest/airbyte/_resource.py +192 -0
  37. dataplat/cli/ingest/airbyte/app.py +31 -0
  38. dataplat/cli/ingest/airbyte/connections.py +1234 -0
  39. dataplat/cli/ingest/airbyte/definitions.py +122 -0
  40. dataplat/cli/ingest/airbyte/destinations.py +26 -0
  41. dataplat/cli/ingest/airbyte/enums.py +45 -0
  42. dataplat/cli/ingest/airbyte/jobs.py +112 -0
  43. dataplat/cli/ingest/airbyte/sources.py +26 -0
  44. dataplat/cli/ingest/airbyte/tags.py +57 -0
  45. dataplat/cli/ingest/airbyte/templates.py +144 -0
  46. dataplat/cli/ingest/airbyte/tui.py +123 -0
  47. dataplat/cli/ingest/airbyte/workspaces.py +80 -0
  48. dataplat/cli/ingest/app.py +14 -0
  49. dataplat/cli/open.py +117 -0
  50. dataplat/cli/status.py +308 -0
  51. dataplat/core/__init__.py +1 -0
  52. dataplat/core/deps.py +138 -0
  53. dataplat/core/envrc.py +125 -0
  54. dataplat/core/errors.py +23 -0
  55. dataplat/main.py +87 -0
  56. dataplat/services/__init__.py +1 -0
  57. dataplat/services/airbyte/__init__.py +1 -0
  58. dataplat/services/airbyte/_resource.py +113 -0
  59. dataplat/services/airbyte/client.py +280 -0
  60. dataplat/services/airbyte/connections.py +306 -0
  61. dataplat/services/airbyte/definitions.py +64 -0
  62. dataplat/services/airbyte/destinations.py +68 -0
  63. dataplat/services/airbyte/jobs.py +72 -0
  64. dataplat/services/airbyte/sources.py +58 -0
  65. dataplat/services/airbyte/tags.py +120 -0
  66. dataplat/services/airbyte/workspaces.py +47 -0
  67. dataplat/services/aws/__init__.py +1 -0
  68. dataplat/services/aws/auth.py +80 -0
  69. dataplat/services/db/__init__.py +1 -0
  70. dataplat/services/db/connection.py +169 -0
  71. dataplat/services/db/describe.py +1034 -0
  72. dataplat/services/db/long_queries.py +378 -0
  73. dataplat/services/db/orphans.py +429 -0
  74. dataplat/services/db/role.py +812 -0
  75. dataplat/services/db/role_admin.py +458 -0
  76. dataplat/services/db/role_dialects.py +521 -0
  77. dataplat/services/db/targets.py +128 -0
  78. dataplat/services/db/top_tables.py +193 -0
  79. dataplat/services/superset/__init__.py +1 -0
  80. dataplat/services/superset/client.py +319 -0
  81. dataplat-0.1.0.dist-info/METADATA +326 -0
  82. dataplat-0.1.0.dist-info/RECORD +85 -0
  83. dataplat-0.1.0.dist-info/WHEEL +4 -0
  84. dataplat-0.1.0.dist-info/entry_points.txt +2 -0
  85. 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,10 @@
1
+ """GitHub command group."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from dataplat.cli.ci.github import runner
8
+
9
+ app = typer.Typer(name="github", help="GitHub operations", no_args_is_help=True)
10
+ app.add_typer(runner.app, name="runner")
@@ -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")