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.
Files changed (52) hide show
  1. {steve_cli-0.5.5 → steve_cli-0.5.6}/PKG-INFO +1 -1
  2. {steve_cli-0.5.5 → steve_cli-0.5.6}/pyproject.toml +1 -1
  3. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/decorators/lineage_job.py +5 -1
  4. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/s3.py +28 -14
  5. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/trino.py +82 -10
  6. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli.egg-info/PKG-INFO +1 -1
  7. {steve_cli-0.5.5 → steve_cli-0.5.6}/README.md +0 -0
  8. {steve_cli-0.5.5 → steve_cli-0.5.6}/setup.cfg +0 -0
  9. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/__init__.py +0 -0
  10. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/auth.py +0 -0
  11. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/cli.py +0 -0
  12. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/decorators/__init__.py +0 -0
  13. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/__init__.py +0 -0
  14. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/adapters/__init__.py +0 -0
  15. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/adapters/composite.py +0 -0
  16. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/adapters/dataproductregistry.py +0 -0
  17. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/adapters/logging.py +0 -0
  18. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/adapters/null.py +0 -0
  19. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/adapters/openlineage.py +0 -0
  20. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/collector.py +0 -0
  21. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/port.py +0 -0
  22. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/registry.py +0 -0
  23. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/lineage/storage.py +0 -0
  24. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/ontology.py +0 -0
  25. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/policies/__init__.py +0 -0
  26. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/policies/client.py +0 -0
  27. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/__init__.py +0 -0
  28. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/__init__.py +0 -0
  29. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/__init__.py +0 -0
  30. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/csv.py +0 -0
  31. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/excel.py +0 -0
  32. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/generic.py +0 -0
  33. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/json.py +0 -0
  34. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/parquet.py +0 -0
  35. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/port.py +0 -0
  36. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/metadata/registry.py +0 -0
  37. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/parquet.py +0 -0
  38. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage/protocol.py +0 -0
  39. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/storage.py +0 -0
  40. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/validation/__init__.py +0 -0
  41. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/validation/adapters/__init__.py +0 -0
  42. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/validation/adapters/great_expectations.py +0 -0
  43. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/validation/adapters/null.py +0 -0
  44. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/validation/adapters/validoopsie.py +0 -0
  45. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/validation/port.py +0 -0
  46. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/validation/registry.py +0 -0
  47. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli/vkg.py +0 -0
  48. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli.egg-info/SOURCES.txt +0 -0
  49. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli.egg-info/dependency_links.txt +0 -0
  50. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli.egg-info/entry_points.txt +0 -0
  51. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli.egg-info/requires.txt +0 -0
  52. {steve_cli-0.5.5 → steve_cli-0.5.6}/steve_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steve-cli
3
- Version: 0.5.5
3
+ Version: 0.5.6
4
4
  Summary: A simple CLI tool to run jobs from jobs.yaml with proper environment setup
5
5
  Author: Frank
6
6
  License: MIT
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "steve-cli"
8
- version = "0.5.5"
8
+ version = "0.5.6"
9
9
  description = "A simple CLI tool to run jobs from jobs.yaml with proper environment setup"
10
10
  readme = "README.md"
11
11
  license = {text = "MIT"}
@@ -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 EnvironmentError(
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 denied when %s: %s (BUCKET=%s)", operation, path, self.bucket)
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("Bucket not found: %s", self.bucket)
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("File not found: %s while %s on BUCKET=%s", path, operation, self.bucket)
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("Storage error during %s: %s (BUCKET=%s)", operation, error, self.bucket)
89
- raise EnvironmentError(f"S3 {operation} failed for {path}: {error.response['Error']['Code']}")
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 EnvironmentError(f"Cannot connect to storage at {self.endpoint}")
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 EnvironmentError(f"Cannot connect to storage at {self.endpoint}")
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 EnvironmentError(f"Cannot connect to storage at {self.endpoint}")
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 EnvironmentError(f"Cannot connect to storage at {self.endpoint}")
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 = "") -> List[str]:
130
- keys: List[str] = []
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) -> List[str]:
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 EnvironmentError(f"Trino endpoint not found:\n{detail}")
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 EnvironmentError("TRINO_SCHEMA is not set and no WORKSPACE_NAME or workspace was provided")
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
- resp = requests.post(f"{self._base}/v1/statement", data=sql, headers=headers)
119
- resp.raise_for_status()
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
- resp = requests.get(next_uri, headers=headers)
138
- resp.raise_for_status()
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
- rows = self._execute(f"SELECT * FROM {self.catalog}.{self.schema}.{self._table_name(path)}")
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.append(arrow_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
- iceberg_table = catalog.create_table(table_id, schema=iceberg_schema)
234
- iceberg_table.append(arrow_table)
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steve-cli
3
- Version: 0.5.5
3
+ Version: 0.5.6
4
4
  Summary: A simple CLI tool to run jobs from jobs.yaml with proper environment setup
5
5
  Author: Frank
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes