steve-cli 0.5.5__tar.gz → 0.5.6__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.
- {steve_cli-0.5.5 → steve_cli-0.5.6}/PKG-INFO +1 -1
- {steve_cli-0.5.5 → steve_cli-0.5.6}/pyproject.toml +1 -1
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/decorators/lineage_job.py +5 -1
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/s3.py +28 -14
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/trino.py +82 -10
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli.egg-info/PKG-INFO +1 -1
- {steve_cli-0.5.5 → steve_cli-0.5.6}/README.md +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/setup.cfg +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/__init__.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/auth.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/cli.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/decorators/__init__.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/__init__.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/adapters/__init__.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/adapters/composite.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/adapters/dataproductregistry.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/adapters/logging.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/adapters/null.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/adapters/openlineage.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/collector.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/port.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/registry.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/storage.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/ontology.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/policies/__init__.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/policies/client.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/__init__.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/__init__.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/__init__.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/csv.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/excel.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/generic.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/json.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/parquet.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/port.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/registry.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/parquet.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/protocol.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/validation/__init__.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/validation/adapters/__init__.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/validation/adapters/great_expectations.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/validation/adapters/null.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/validation/adapters/validoopsie.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/validation/port.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/validation/registry.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/vkg.py +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli.egg-info/SOURCES.txt +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli.egg-info/dependency_links.txt +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli.egg-info/entry_points.txt +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli.egg-info/requires.txt +0 -0
- {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli.egg-info/top_level.txt +0 -0
|
@@ -67,8 +67,12 @@ def lineage_job(
|
|
|
67
67
|
import click
|
|
68
68
|
from steve_cli.validation.port import DataQualityError
|
|
69
69
|
if isinstance(exc, (DataQualityError, EnvironmentError)):
|
|
70
|
-
click.secho(f"ERROR {exc}", fg="red", bold=True, err=True)
|
|
70
|
+
click.secho(f"ERROR [{job_name}] {exc}", fg="red", bold=True, err=True)
|
|
71
71
|
sys.exit(1)
|
|
72
|
+
click.secho(
|
|
73
|
+
f"ERROR [{job_name}] {type(exc).__name__}: {exc}",
|
|
74
|
+
fg="red", bold=True, err=True,
|
|
75
|
+
)
|
|
72
76
|
raise
|
|
73
77
|
|
|
74
78
|
session.complete()
|
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
5
|
from pathlib import Path, PurePosixPath
|
|
6
|
-
from typing import List
|
|
7
6
|
|
|
8
7
|
import boto3
|
|
9
8
|
from botocore.client import Config
|
|
@@ -21,7 +20,7 @@ class S3Storage:
|
|
|
21
20
|
|
|
22
21
|
missing = [v for v in required_vars if not os.getenv(v)]
|
|
23
22
|
if missing:
|
|
24
|
-
raise
|
|
23
|
+
raise OSError(
|
|
25
24
|
f"Missing required environment variables for {self.tier} tier: "
|
|
26
25
|
f"{', '.join(missing)}"
|
|
27
26
|
)
|
|
@@ -78,22 +77,37 @@ class S3Storage:
|
|
|
78
77
|
|
|
79
78
|
def _handle_client_error(self, error: ClientError, path: str, operation: str) -> None:
|
|
80
79
|
code = error.response["Error"]["Code"]
|
|
80
|
+
key = self._key(path)
|
|
81
|
+
tier_upper = self.tier.upper()
|
|
82
|
+
location = f"s3://{self.bucket}/{key} at {self.endpoint}"
|
|
81
83
|
if code == "AccessDenied":
|
|
82
|
-
logger.error("Access
|
|
84
|
+
logger.error("S3Storage: Access Denied when %s [%s]", operation, location)
|
|
85
|
+
raise OSError(
|
|
86
|
+
f"S3Storage: {operation} failed — Access Denied [{location}] "
|
|
87
|
+
f"(check {tier_upper}_ACCESS_KEY / {tier_upper}_SECRET_KEY have permission)"
|
|
88
|
+
)
|
|
83
89
|
elif code == "NoSuchBucket":
|
|
84
|
-
logger.error("
|
|
90
|
+
logger.error("S3Storage: bucket not found [bucket=%s, endpoint=%s]", self.bucket, self.endpoint)
|
|
91
|
+
raise OSError(
|
|
92
|
+
f"S3Storage: {operation} failed — bucket '{self.bucket}' not found at {self.endpoint}"
|
|
93
|
+
)
|
|
85
94
|
elif code == "NoSuchKey":
|
|
86
|
-
logger.warning("
|
|
95
|
+
logger.warning("S3Storage: key not found when %s [%s]", operation, location)
|
|
96
|
+
raise OSError(
|
|
97
|
+
f"S3Storage: {operation} failed — key not found [s3://{self.bucket}/{key}]"
|
|
98
|
+
)
|
|
87
99
|
else:
|
|
88
|
-
logger.error("
|
|
89
|
-
|
|
100
|
+
logger.error("S3Storage: %s error when %s [%s]: %s", code, operation, location, error)
|
|
101
|
+
raise OSError(
|
|
102
|
+
f"S3Storage: {operation} failed — {code} [s3://{self.bucket}/{key} at {self.endpoint}]"
|
|
103
|
+
)
|
|
90
104
|
|
|
91
105
|
def put_file(self, local_path: str, path: str) -> None:
|
|
92
106
|
try:
|
|
93
107
|
self.client.upload_file(local_path, self.bucket, self._key(path))
|
|
94
108
|
logger.info("Uploaded %s", self._object_location(path))
|
|
95
109
|
except EndpointConnectionError:
|
|
96
|
-
raise
|
|
110
|
+
raise OSError(f"S3Storage: cannot connect to S3 endpoint at {self.endpoint} (uploading {path})")
|
|
97
111
|
except ClientError as e:
|
|
98
112
|
self._handle_client_error(e, path, "uploading")
|
|
99
113
|
|
|
@@ -103,7 +117,7 @@ class S3Storage:
|
|
|
103
117
|
self.client.download_file(self.bucket, self._key(path), local_path)
|
|
104
118
|
logger.info("Downloaded %s", self._object_location(path))
|
|
105
119
|
except EndpointConnectionError:
|
|
106
|
-
raise
|
|
120
|
+
raise OSError(f"S3Storage: cannot connect to S3 endpoint at {self.endpoint} (downloading {path})")
|
|
107
121
|
except ClientError as e:
|
|
108
122
|
self._handle_client_error(e, path, "downloading")
|
|
109
123
|
|
|
@@ -112,7 +126,7 @@ class S3Storage:
|
|
|
112
126
|
self.client.put_object(Bucket=self.bucket, Key=self._key(path), Body=data)
|
|
113
127
|
logger.info("Stored %s", self._object_location(path))
|
|
114
128
|
except EndpointConnectionError:
|
|
115
|
-
raise
|
|
129
|
+
raise OSError(f"S3Storage: cannot connect to S3 endpoint at {self.endpoint} (writing {path})")
|
|
116
130
|
except ClientError as e:
|
|
117
131
|
self._handle_client_error(e, path, "writing")
|
|
118
132
|
|
|
@@ -122,16 +136,16 @@ class S3Storage:
|
|
|
122
136
|
logger.info("Retrieved %s", self._object_location(path))
|
|
123
137
|
return obj["Body"].read()
|
|
124
138
|
except EndpointConnectionError:
|
|
125
|
-
raise
|
|
139
|
+
raise OSError(f"S3Storage: cannot connect to S3 endpoint at {self.endpoint} (reading {path})")
|
|
126
140
|
except ClientError as e:
|
|
127
141
|
self._handle_client_error(e, path, "reading")
|
|
128
142
|
|
|
129
|
-
def list(self, prefix: str = "") ->
|
|
130
|
-
keys:
|
|
143
|
+
def list(self, prefix: str = "") -> list[str]:
|
|
144
|
+
keys: list[str] = []
|
|
131
145
|
paginator = self.client.get_paginator("list_objects_v2")
|
|
132
146
|
for page in paginator.paginate(Bucket=self.bucket, Prefix=self._key(prefix)):
|
|
133
147
|
keys.extend(obj["Key"] for obj in page.get("Contents", []))
|
|
134
148
|
return keys
|
|
135
149
|
|
|
136
|
-
def list_all(self) ->
|
|
150
|
+
def list_all(self) -> list[str]:
|
|
137
151
|
return self.list()
|
|
@@ -35,12 +35,12 @@ class TrinoStorage:
|
|
|
35
35
|
("✓" if self._workspace_id else "✗", f"WORKSPACE_ID env var set (got: {self._workspace_id!r})"),
|
|
36
36
|
]
|
|
37
37
|
detail = "\n".join(f" {mark} {desc}" for mark, desc in checks)
|
|
38
|
-
raise
|
|
38
|
+
raise OSError(f"Trino endpoint not found:\n{detail}")
|
|
39
39
|
self.catalog = os.getenv("TRINO_CATALOG", "minio")
|
|
40
40
|
resolved_workspace = workspace or os.getenv("WORKSPACE_NAME")
|
|
41
41
|
self.schema = _derive_schema(tier, resolved_workspace) or os.environ.get("TRINO_SCHEMA", "")
|
|
42
42
|
if not self.schema:
|
|
43
|
-
raise
|
|
43
|
+
raise OSError("TRINO_SCHEMA is not set and no WORKSPACE_NAME or workspace was provided")
|
|
44
44
|
self.user = os.getenv("TRINO_USER", "admin")
|
|
45
45
|
self._base = trino_endpoint.rstrip("/")
|
|
46
46
|
self._lakekeeper_endpoint = os.getenv("LAKEKEEPER_ENDPOINT")
|
|
@@ -115,8 +115,19 @@ class TrinoStorage:
|
|
|
115
115
|
headers["Authorization"] = f"Bearer {token}"
|
|
116
116
|
|
|
117
117
|
t0 = time.perf_counter()
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
try:
|
|
119
|
+
resp = requests.post(f"{self._base}/v1/statement", data=sql, headers=headers)
|
|
120
|
+
except requests.exceptions.ConnectionError as exc:
|
|
121
|
+
raise OSError(
|
|
122
|
+
f"TrinoStorage: cannot connect to Trino at {self._base} — "
|
|
123
|
+
f"check WORKSPACE_ID env var and credentials in ~/.steve/credentials.json"
|
|
124
|
+
) from exc
|
|
125
|
+
if not resp.ok:
|
|
126
|
+
raise OSError(
|
|
127
|
+
f"TrinoStorage: POST /v1/statement failed with HTTP {resp.status_code} "
|
|
128
|
+
f"[url={self._base}] — auth proxy may not be routing to Trino, "
|
|
129
|
+
f"or the workspace is not accessible (sql: {sql[:100]!r})"
|
|
130
|
+
)
|
|
120
131
|
if trace:
|
|
121
132
|
logger.warning("[trino] POST /v1/statement: %.3fs", time.perf_counter() - t0)
|
|
122
133
|
|
|
@@ -134,8 +145,18 @@ class TrinoStorage:
|
|
|
134
145
|
time.sleep(0.1)
|
|
135
146
|
next_uri = next_uri.replace("http://trino:8080", self._base).replace("https://trino:8080", self._base)
|
|
136
147
|
t_poll = time.perf_counter()
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
try:
|
|
149
|
+
resp = requests.get(next_uri, headers=headers)
|
|
150
|
+
except requests.exceptions.ConnectionError as exc:
|
|
151
|
+
raise OSError(
|
|
152
|
+
f"TrinoStorage: connection lost while polling Trino result [url={next_uri}] — "
|
|
153
|
+
f"sql: {sql[:100]!r}"
|
|
154
|
+
) from exc
|
|
155
|
+
if not resp.ok:
|
|
156
|
+
raise OSError(
|
|
157
|
+
f"TrinoStorage: result poll failed with HTTP {resp.status_code} "
|
|
158
|
+
f"[url={next_uri}] — Trino rejected query (sql: {sql[:100]!r})"
|
|
159
|
+
)
|
|
139
160
|
poll += 1
|
|
140
161
|
if trace:
|
|
141
162
|
logger.warning("[trino] poll #%d: %.3fs (total %.3fs, %d rows so far)",
|
|
@@ -162,7 +183,15 @@ class TrinoStorage:
|
|
|
162
183
|
return self.list()
|
|
163
184
|
|
|
164
185
|
def get_bytes(self, path: str) -> bytes:
|
|
165
|
-
|
|
186
|
+
table_ref = f"{self.catalog}.{self.schema}.{self._table_name(path)}"
|
|
187
|
+
try:
|
|
188
|
+
rows = self._execute(f"SELECT * FROM {table_ref}")
|
|
189
|
+
except OSError:
|
|
190
|
+
raise
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
raise OSError(
|
|
193
|
+
f"TrinoStorage: read from '{table_ref}' failed — {type(exc).__name__}: {exc}"
|
|
194
|
+
) from exc
|
|
166
195
|
if not rows:
|
|
167
196
|
return b""
|
|
168
197
|
arrow_table = pa.Table.from_pylist(rows)
|
|
@@ -176,11 +205,24 @@ class TrinoStorage:
|
|
|
176
205
|
|
|
177
206
|
def _ensure_namespace(self) -> None:
|
|
178
207
|
from pyiceberg.exceptions import NamespaceAlreadyExistsError
|
|
208
|
+
lakekeeper_base = self._lakekeeper_endpoint or "http://lakekeeper:8181/catalog"
|
|
179
209
|
try:
|
|
180
210
|
self._iceberg_catalog.create_namespace(self.schema)
|
|
181
211
|
logger.info("Created namespace %s", self.schema)
|
|
182
212
|
except NamespaceAlreadyExistsError:
|
|
183
213
|
pass
|
|
214
|
+
except Exception as exc:
|
|
215
|
+
msg = str(exc)
|
|
216
|
+
if "403" in msg or "Access Denied" in msg or "Forbidden" in msg:
|
|
217
|
+
raise OSError(
|
|
218
|
+
f"TrinoStorage: namespace '{self.schema}' creation denied at Lakekeeper "
|
|
219
|
+
f"[{lakekeeper_base}] — check that the token has write access to warehouse "
|
|
220
|
+
f"'{self._lakekeeper_warehouse}'"
|
|
221
|
+
) from exc
|
|
222
|
+
raise OSError(
|
|
223
|
+
f"TrinoStorage: namespace '{self.schema}' creation failed at Lakekeeper "
|
|
224
|
+
f"[{lakekeeper_base}] — {type(exc).__name__}: {exc}"
|
|
225
|
+
) from exc
|
|
184
226
|
|
|
185
227
|
def put_bytes(self, data: bytes, path: str) -> None:
|
|
186
228
|
from pyiceberg.exceptions import NoSuchTableError
|
|
@@ -188,12 +230,29 @@ class TrinoStorage:
|
|
|
188
230
|
arrow_table = pq.read_table(io.BytesIO(data))
|
|
189
231
|
table_id = f"{self.schema}.{self._table_name(path)}"
|
|
190
232
|
catalog = self._iceberg_catalog
|
|
233
|
+
lakekeeper_base = self._lakekeeper_endpoint or "http://lakekeeper:8181/catalog"
|
|
191
234
|
|
|
192
235
|
self._ensure_namespace()
|
|
193
236
|
|
|
237
|
+
def _append(iceberg_table, arrow_table, table_id):
|
|
238
|
+
try:
|
|
239
|
+
iceberg_table.append(arrow_table)
|
|
240
|
+
except Exception as exc:
|
|
241
|
+
msg = str(exc)
|
|
242
|
+
if "403" in msg or "Access Denied" in msg or "Forbidden" in msg:
|
|
243
|
+
raise OSError(
|
|
244
|
+
f"TrinoStorage: write to '{table_id}' denied via Lakekeeper [{lakekeeper_base}] — "
|
|
245
|
+
f"Access Denied on warehouse '{self._lakekeeper_warehouse}' "
|
|
246
|
+
f"(check S3 bucket policy and token permissions)"
|
|
247
|
+
) from exc
|
|
248
|
+
raise OSError(
|
|
249
|
+
f"TrinoStorage: write to '{table_id}' failed via Lakekeeper [{lakekeeper_base}] — "
|
|
250
|
+
f"{type(exc).__name__}: {exc}"
|
|
251
|
+
) from exc
|
|
252
|
+
|
|
194
253
|
try:
|
|
195
254
|
iceberg_table = catalog.load_table(table_id)
|
|
196
|
-
iceberg_table
|
|
255
|
+
_append(iceberg_table, arrow_table, table_id)
|
|
197
256
|
except NoSuchTableError:
|
|
198
257
|
from pyiceberg.schema import Schema
|
|
199
258
|
from pyiceberg.types import (
|
|
@@ -230,8 +289,21 @@ class TrinoStorage:
|
|
|
230
289
|
fields.append(NestedField(field_id=i + 1, name=field.name, field_type=iceberg_type, required=not field.nullable))
|
|
231
290
|
|
|
232
291
|
iceberg_schema = Schema(*fields)
|
|
233
|
-
|
|
234
|
-
|
|
292
|
+
try:
|
|
293
|
+
iceberg_table = catalog.create_table(table_id, schema=iceberg_schema)
|
|
294
|
+
except Exception as exc:
|
|
295
|
+
msg = str(exc)
|
|
296
|
+
if "403" in msg or "Access Denied" in msg or "Forbidden" in msg:
|
|
297
|
+
raise OSError(
|
|
298
|
+
f"TrinoStorage: create table '{table_id}' denied via Lakekeeper [{lakekeeper_base}] — "
|
|
299
|
+
f"Access Denied on warehouse '{self._lakekeeper_warehouse}' "
|
|
300
|
+
f"(check S3 bucket policy and token permissions)"
|
|
301
|
+
) from exc
|
|
302
|
+
raise OSError(
|
|
303
|
+
f"TrinoStorage: create table '{table_id}' failed via Lakekeeper [{lakekeeper_base}] — "
|
|
304
|
+
f"{type(exc).__name__}: {exc}"
|
|
305
|
+
) from exc
|
|
306
|
+
_append(iceberg_table, arrow_table, table_id)
|
|
235
307
|
|
|
236
308
|
logger.info("Written %d rows to %s", len(arrow_table), table_id)
|
|
237
309
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|