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
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""AWS Redshift Serverless monitoring commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import UTC, datetime, timedelta
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from botocore.exceptions import ClientError
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from dataplat.cli.cloud.aws._common import (
|
|
13
|
+
JsonOption,
|
|
14
|
+
cli_session,
|
|
15
|
+
make_table,
|
|
16
|
+
profile_option,
|
|
17
|
+
region_option,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(
|
|
21
|
+
name="redshift",
|
|
22
|
+
help="Monitor AWS Redshift Serverless metrics",
|
|
23
|
+
no_args_is_help=True,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
console = Console()
|
|
27
|
+
|
|
28
|
+
def _get_session(profile: str | None, region: str | None):
|
|
29
|
+
"""Return a boto3 Session with shared AWS auth handling."""
|
|
30
|
+
return cli_session(console, profile, region)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _human_value(value: float | None, unit: str | None, metric_name: str) -> str:
|
|
34
|
+
if value is None:
|
|
35
|
+
return "—"
|
|
36
|
+
if unit in {"Percent"}:
|
|
37
|
+
return f"{value:.2f}%"
|
|
38
|
+
if unit in {"Bytes"}:
|
|
39
|
+
gb = value / 1024**3
|
|
40
|
+
return f"{gb:.2f} GB"
|
|
41
|
+
if unit in {"Megabytes"}:
|
|
42
|
+
gb = value / 1024
|
|
43
|
+
return f"{gb:.2f} GB"
|
|
44
|
+
if unit in {"Milliseconds", "Microseconds"}:
|
|
45
|
+
return f"{value:.2f} {unit}"
|
|
46
|
+
if unit in {"Seconds"}:
|
|
47
|
+
return f"{value:.2f}s"
|
|
48
|
+
if metric_name.endswith("Count"):
|
|
49
|
+
return str(int(round(value)))
|
|
50
|
+
return f"{value:.2f}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
_make_table = make_table
|
|
54
|
+
|
|
55
|
+
# CloudWatch units per metric (get_metric_data does not echo units back).
|
|
56
|
+
_METRIC_UNITS: dict[str, str] = {
|
|
57
|
+
"ComputeSeconds": "Seconds",
|
|
58
|
+
"QueryDuration": "Microseconds",
|
|
59
|
+
"DataStorage": "Megabytes",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _list_workgroups(client, explicit_workgroup: str | None) -> list[dict]:
|
|
64
|
+
groups: list[dict] = []
|
|
65
|
+
token: str | None = None
|
|
66
|
+
while True:
|
|
67
|
+
kwargs: dict[str, object] = {"maxResults": 100}
|
|
68
|
+
if token:
|
|
69
|
+
kwargs["nextToken"] = token
|
|
70
|
+
resp = client.list_workgroups(**kwargs)
|
|
71
|
+
groups.extend(resp.get("workgroups", []))
|
|
72
|
+
token = resp.get("nextToken")
|
|
73
|
+
if not token:
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
if explicit_workgroup:
|
|
77
|
+
groups = [g for g in groups if g.get("workgroupName") == explicit_workgroup]
|
|
78
|
+
return groups
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _discover_metric_dims(
|
|
82
|
+
cw, metric_name: str, base_dims: list[dict]
|
|
83
|
+
) -> list[dict] | None:
|
|
84
|
+
"""Find a dimension set for a metric that matches the requested scope."""
|
|
85
|
+
paginator = cw.get_paginator("list_metrics")
|
|
86
|
+
candidates: list[list[dict]] = []
|
|
87
|
+
|
|
88
|
+
for page in paginator.paginate(
|
|
89
|
+
Namespace="AWS/Redshift-Serverless",
|
|
90
|
+
MetricName=metric_name,
|
|
91
|
+
Dimensions=base_dims,
|
|
92
|
+
):
|
|
93
|
+
for metric in page.get("Metrics", []):
|
|
94
|
+
dims = metric.get("Dimensions", [])
|
|
95
|
+
dim_map = {d["Name"]: d["Value"] for d in dims}
|
|
96
|
+
matches = all(dim_map.get(d["Name"]) == d["Value"] for d in base_dims)
|
|
97
|
+
if matches:
|
|
98
|
+
candidates.append(dims)
|
|
99
|
+
|
|
100
|
+
if not candidates:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
# Prefer the least-specific dimension set; it usually yields stable series.
|
|
104
|
+
candidates.sort(key=len)
|
|
105
|
+
return candidates[0]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _batch_metric_summaries(
|
|
109
|
+
cw,
|
|
110
|
+
scoped: list[tuple[str, str, list[dict]]],
|
|
111
|
+
start: datetime,
|
|
112
|
+
end: datetime,
|
|
113
|
+
period: int,
|
|
114
|
+
) -> list[tuple[str, str, dict[str, float] | None]]:
|
|
115
|
+
"""One get_metric_data call for every ``(scope, metric, dims)`` triple."""
|
|
116
|
+
queries: list[dict] = []
|
|
117
|
+
stats = (("avg", "Average"), ("min", "Minimum"), ("max", "Maximum"))
|
|
118
|
+
for i, (_scope, metric_name, dims) in enumerate(scoped):
|
|
119
|
+
for stat_key, stat in stats:
|
|
120
|
+
queries.append(
|
|
121
|
+
{
|
|
122
|
+
"Id": f"m{i}_{stat_key}",
|
|
123
|
+
"MetricStat": {
|
|
124
|
+
"Metric": {
|
|
125
|
+
"Namespace": "AWS/Redshift-Serverless",
|
|
126
|
+
"MetricName": metric_name,
|
|
127
|
+
"Dimensions": dims,
|
|
128
|
+
},
|
|
129
|
+
"Period": period,
|
|
130
|
+
"Stat": stat,
|
|
131
|
+
},
|
|
132
|
+
"ReturnData": True,
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
if not queries:
|
|
136
|
+
return []
|
|
137
|
+
resp = cw.get_metric_data(
|
|
138
|
+
MetricDataQueries=queries,
|
|
139
|
+
StartTime=start,
|
|
140
|
+
EndTime=end,
|
|
141
|
+
ScanBy="TimestampAscending",
|
|
142
|
+
)
|
|
143
|
+
by_id: dict[str, list[float]] = {
|
|
144
|
+
r["Id"]: r.get("Values", []) for r in resp.get("MetricDataResults", [])
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
out: list[tuple[str, str, dict[str, float] | None]] = []
|
|
148
|
+
for i, (scope, metric_name, _dims) in enumerate(scoped):
|
|
149
|
+
avgs = by_id.get(f"m{i}_avg", [])
|
|
150
|
+
if not avgs:
|
|
151
|
+
out.append((scope, metric_name, None))
|
|
152
|
+
continue
|
|
153
|
+
mins = by_id.get(f"m{i}_min", []) or avgs
|
|
154
|
+
maxs = by_id.get(f"m{i}_max", []) or avgs
|
|
155
|
+
out.append(
|
|
156
|
+
(
|
|
157
|
+
scope,
|
|
158
|
+
metric_name,
|
|
159
|
+
{
|
|
160
|
+
"latest": avgs[-1],
|
|
161
|
+
"average": sum(avgs) / len(avgs),
|
|
162
|
+
"min": min(mins),
|
|
163
|
+
"max": max(maxs),
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
return out
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# shared CLI options
|
|
171
|
+
ProfileOption = profile_option()
|
|
172
|
+
RegionOption = region_option()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# commands
|
|
176
|
+
@app.command("metrics")
|
|
177
|
+
def metrics(
|
|
178
|
+
profile: str | None = ProfileOption,
|
|
179
|
+
region: str | None = RegionOption,
|
|
180
|
+
workgroup: str | None = typer.Option(
|
|
181
|
+
None,
|
|
182
|
+
"--workgroup",
|
|
183
|
+
"-w",
|
|
184
|
+
help="Redshift Serverless workgroup name. Defaults to all workgroups.",
|
|
185
|
+
),
|
|
186
|
+
hours: float = typer.Option(
|
|
187
|
+
1.0,
|
|
188
|
+
"--hours",
|
|
189
|
+
help="Look-back window in hours.",
|
|
190
|
+
),
|
|
191
|
+
period: int = typer.Option(
|
|
192
|
+
300,
|
|
193
|
+
"--period",
|
|
194
|
+
help="CloudWatch aggregation period in seconds.",
|
|
195
|
+
),
|
|
196
|
+
as_json: bool = JsonOption,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Show key Redshift Serverless CloudWatch metrics."""
|
|
199
|
+
# Workgroup + namespace focused metrics with stable operational value.
|
|
200
|
+
workgroup_metrics = [
|
|
201
|
+
"ComputeCapacity",
|
|
202
|
+
"ComputeSeconds",
|
|
203
|
+
"DatabaseConnections",
|
|
204
|
+
"QueriesQueued",
|
|
205
|
+
"QueriesRunning",
|
|
206
|
+
"QueriesCompletedPerSecond",
|
|
207
|
+
"QueryDuration",
|
|
208
|
+
]
|
|
209
|
+
namespace_metrics = [
|
|
210
|
+
"DataStorage",
|
|
211
|
+
"TotalTableCount",
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
session = _get_session(profile, region)
|
|
215
|
+
rs = session.client("redshift-serverless")
|
|
216
|
+
cw = session.client("cloudwatch")
|
|
217
|
+
|
|
218
|
+
now = datetime.now(UTC)
|
|
219
|
+
start = now - timedelta(hours=hours)
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
with console.status(
|
|
223
|
+
"[bold blue]Fetching Redshift Serverless metrics…[/bold blue]"
|
|
224
|
+
):
|
|
225
|
+
workgroups = _list_workgroups(rs, workgroup)
|
|
226
|
+
|
|
227
|
+
if not workgroups:
|
|
228
|
+
message = (
|
|
229
|
+
f"No workgroup named '{workgroup}' found."
|
|
230
|
+
if workgroup
|
|
231
|
+
else "No Redshift Serverless workgroups found."
|
|
232
|
+
)
|
|
233
|
+
console.print(f"[yellow]{message}[/yellow]")
|
|
234
|
+
raise typer.Exit(code=1 if workgroup else 0)
|
|
235
|
+
|
|
236
|
+
# Discover dimension sets per metric, then batch every summary in a
|
|
237
|
+
# single get_metric_data call.
|
|
238
|
+
scoped: list[tuple[str, str, list[dict]]] = []
|
|
239
|
+
with console.status(
|
|
240
|
+
"[bold blue]Collecting CloudWatch datapoints…[/bold blue]"
|
|
241
|
+
):
|
|
242
|
+
for wg in sorted(workgroups, key=lambda g: g.get("workgroupName", "")):
|
|
243
|
+
wg_name = wg.get("workgroupName")
|
|
244
|
+
ns_name = wg.get("namespaceName")
|
|
245
|
+
if not wg_name:
|
|
246
|
+
continue
|
|
247
|
+
for metric_name in workgroup_metrics:
|
|
248
|
+
dims = _discover_metric_dims(
|
|
249
|
+
cw, metric_name, [{"Name": "Workgroup", "Value": wg_name}]
|
|
250
|
+
)
|
|
251
|
+
if dims:
|
|
252
|
+
scoped.append((f"workgroup:{wg_name}", metric_name, dims))
|
|
253
|
+
if ns_name:
|
|
254
|
+
for metric_name in namespace_metrics:
|
|
255
|
+
dims = _discover_metric_dims(
|
|
256
|
+
cw,
|
|
257
|
+
metric_name,
|
|
258
|
+
[{"Name": "Namespace", "Value": ns_name}],
|
|
259
|
+
)
|
|
260
|
+
if dims:
|
|
261
|
+
scoped.append(
|
|
262
|
+
(f"namespace:{ns_name}", metric_name, dims)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
summaries = _batch_metric_summaries(cw, scoped, start, now, period)
|
|
266
|
+
except ClientError as exc:
|
|
267
|
+
code = exc.response.get("Error", {}).get("Code", "ClientError")
|
|
268
|
+
msg = exc.response.get("Error", {}).get("Message", str(exc))
|
|
269
|
+
console.print(f"[red]{code} while fetching metrics: {msg}[/red]")
|
|
270
|
+
raise typer.Exit(code=1)
|
|
271
|
+
|
|
272
|
+
rows = [(scope, name, s) for scope, name, s in summaries if s is not None]
|
|
273
|
+
|
|
274
|
+
if as_json:
|
|
275
|
+
payload = [
|
|
276
|
+
{"scope": scope, "metric": name, **summary}
|
|
277
|
+
for scope, name, summary in rows
|
|
278
|
+
]
|
|
279
|
+
typer.echo(json.dumps(payload, indent=2))
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
if not rows:
|
|
283
|
+
console.print(
|
|
284
|
+
"[yellow]No datapoints found for selected Redshift Serverless metrics.[/yellow]"
|
|
285
|
+
)
|
|
286
|
+
raise typer.Exit()
|
|
287
|
+
|
|
288
|
+
table = _make_table("Redshift Serverless Metrics")
|
|
289
|
+
table.add_column("Scope", style="bold cyan", min_width=24, no_wrap=True)
|
|
290
|
+
table.add_column("Metric", style="magenta", min_width=22)
|
|
291
|
+
table.add_column("Latest", justify="right", style="bright_white")
|
|
292
|
+
table.add_column("Average", justify="right", style="bright_white")
|
|
293
|
+
table.add_column("Min", justify="right", style="bright_white")
|
|
294
|
+
table.add_column("Max", justify="right", style="bright_white")
|
|
295
|
+
|
|
296
|
+
for scope, metric_name, summary in rows:
|
|
297
|
+
unit = _METRIC_UNITS.get(metric_name)
|
|
298
|
+
table.add_row(
|
|
299
|
+
scope,
|
|
300
|
+
metric_name,
|
|
301
|
+
_human_value(summary["latest"], unit, metric_name),
|
|
302
|
+
_human_value(summary["average"], unit, metric_name),
|
|
303
|
+
_human_value(summary["min"], unit, metric_name),
|
|
304
|
+
_human_value(summary["max"], unit, metric_name),
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
console.print()
|
|
308
|
+
console.print(table)
|
|
309
|
+
console.print(
|
|
310
|
+
f"\n[dim]Window: last {hours}h · Period: {period}s · "
|
|
311
|
+
f"Profile: {profile} · Region: {region}[/dim]"
|
|
312
|
+
)
|