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
@@ -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
+ )