steve-cli 0.5.4__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.4 → steve_cli-0.5.6}/PKG-INFO +1 -1
  2. {steve_cli-0.5.4 → steve_cli-0.5.6}/pyproject.toml +1 -1
  3. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/decorators/lineage_job.py +5 -1
  4. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/s3.py +28 -14
  5. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/trino.py +91 -10
  6. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli.egg-info/PKG-INFO +1 -1
  7. {steve_cli-0.5.4 → steve_cli-0.5.6}/README.md +0 -0
  8. {steve_cli-0.5.4 → steve_cli-0.5.6}/setup.cfg +0 -0
  9. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/__init__.py +0 -0
  10. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/auth.py +0 -0
  11. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/cli.py +0 -0
  12. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/decorators/__init__.py +0 -0
  13. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/__init__.py +0 -0
  14. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/adapters/__init__.py +0 -0
  15. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/adapters/composite.py +0 -0
  16. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/adapters/dataproductregistry.py +0 -0
  17. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/adapters/logging.py +0 -0
  18. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/adapters/null.py +0 -0
  19. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/adapters/openlineage.py +0 -0
  20. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/collector.py +0 -0
  21. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/port.py +0 -0
  22. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/registry.py +0 -0
  23. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/lineage/storage.py +0 -0
  24. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/ontology.py +0 -0
  25. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/policies/__init__.py +0 -0
  26. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/policies/client.py +0 -0
  27. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/__init__.py +0 -0
  28. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/__init__.py +0 -0
  29. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/__init__.py +0 -0
  30. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/csv.py +0 -0
  31. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/excel.py +0 -0
  32. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/generic.py +0 -0
  33. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/json.py +0 -0
  34. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/extractors/parquet.py +0 -0
  35. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/port.py +0 -0
  36. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/metadata/registry.py +0 -0
  37. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/parquet.py +0 -0
  38. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage/protocol.py +0 -0
  39. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/storage.py +0 -0
  40. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/validation/__init__.py +0 -0
  41. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/validation/adapters/__init__.py +0 -0
  42. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/validation/adapters/great_expectations.py +0 -0
  43. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/validation/adapters/null.py +0 -0
  44. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/validation/adapters/validoopsie.py +0 -0
  45. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/validation/port.py +0 -0
  46. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/validation/registry.py +0 -0
  47. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli/vkg.py +0 -0
  48. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli.egg-info/SOURCES.txt +0 -0
  49. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli.egg-info/dependency_links.txt +0 -0
  50. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli.egg-info/entry_points.txt +0 -0
  51. {steve_cli-0.5.4 → steve_cli-0.5.6}/steve_cli.egg-info/requires.txt +0 -0
  52. {steve_cli-0.5.4 → 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.4
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.4"
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()
@@ -26,12 +26,21 @@ class TrinoStorage:
26
26
  self._workspace_id = os.getenv("WORKSPACE_ID")
27
27
  trino_endpoint = get_service_url("trino", self._workspace_id)
28
28
  if not trino_endpoint:
29
- raise EnvironmentError("Trino endpoint not found — run `steve login` to configure credentials")
29
+ from steve_cli.auth import _get_workspace_entry
30
+ entry = _get_workspace_entry(self._workspace_id)
31
+ checks = [
32
+ ("✓" if entry else "✗", "credentials found in ~/.steve/credentials.json"),
33
+ ("✓" if entry.get("token") else "✗", "token present"),
34
+ ("✓" if entry.get("base_url") else "✗", "base_url present (STEVE_BASE_URL configured on automation-kernel)"),
35
+ ("✓" if self._workspace_id else "✗", f"WORKSPACE_ID env var set (got: {self._workspace_id!r})"),
36
+ ]
37
+ detail = "\n".join(f" {mark} {desc}" for mark, desc in checks)
38
+ raise OSError(f"Trino endpoint not found:\n{detail}")
30
39
  self.catalog = os.getenv("TRINO_CATALOG", "minio")
31
40
  resolved_workspace = workspace or os.getenv("WORKSPACE_NAME")
32
41
  self.schema = _derive_schema(tier, resolved_workspace) or os.environ.get("TRINO_SCHEMA", "")
33
42
  if not self.schema:
34
- 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")
35
44
  self.user = os.getenv("TRINO_USER", "admin")
36
45
  self._base = trino_endpoint.rstrip("/")
37
46
  self._lakekeeper_endpoint = os.getenv("LAKEKEEPER_ENDPOINT")
@@ -106,8 +115,19 @@ class TrinoStorage:
106
115
  headers["Authorization"] = f"Bearer {token}"
107
116
 
108
117
  t0 = time.perf_counter()
109
- resp = requests.post(f"{self._base}/v1/statement", data=sql, headers=headers)
110
- 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
+ )
111
131
  if trace:
112
132
  logger.warning("[trino] POST /v1/statement: %.3fs", time.perf_counter() - t0)
113
133
 
@@ -125,8 +145,18 @@ class TrinoStorage:
125
145
  time.sleep(0.1)
126
146
  next_uri = next_uri.replace("http://trino:8080", self._base).replace("https://trino:8080", self._base)
127
147
  t_poll = time.perf_counter()
128
- resp = requests.get(next_uri, headers=headers)
129
- 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
+ )
130
160
  poll += 1
131
161
  if trace:
132
162
  logger.warning("[trino] poll #%d: %.3fs (total %.3fs, %d rows so far)",
@@ -153,7 +183,15 @@ class TrinoStorage:
153
183
  return self.list()
154
184
 
155
185
  def get_bytes(self, path: str) -> bytes:
156
- 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
157
195
  if not rows:
158
196
  return b""
159
197
  arrow_table = pa.Table.from_pylist(rows)
@@ -167,11 +205,24 @@ class TrinoStorage:
167
205
 
168
206
  def _ensure_namespace(self) -> None:
169
207
  from pyiceberg.exceptions import NamespaceAlreadyExistsError
208
+ lakekeeper_base = self._lakekeeper_endpoint or "http://lakekeeper:8181/catalog"
170
209
  try:
171
210
  self._iceberg_catalog.create_namespace(self.schema)
172
211
  logger.info("Created namespace %s", self.schema)
173
212
  except NamespaceAlreadyExistsError:
174
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
175
226
 
176
227
  def put_bytes(self, data: bytes, path: str) -> None:
177
228
  from pyiceberg.exceptions import NoSuchTableError
@@ -179,12 +230,29 @@ class TrinoStorage:
179
230
  arrow_table = pq.read_table(io.BytesIO(data))
180
231
  table_id = f"{self.schema}.{self._table_name(path)}"
181
232
  catalog = self._iceberg_catalog
233
+ lakekeeper_base = self._lakekeeper_endpoint or "http://lakekeeper:8181/catalog"
182
234
 
183
235
  self._ensure_namespace()
184
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
+
185
253
  try:
186
254
  iceberg_table = catalog.load_table(table_id)
187
- iceberg_table.append(arrow_table)
255
+ _append(iceberg_table, arrow_table, table_id)
188
256
  except NoSuchTableError:
189
257
  from pyiceberg.schema import Schema
190
258
  from pyiceberg.types import (
@@ -221,8 +289,21 @@ class TrinoStorage:
221
289
  fields.append(NestedField(field_id=i + 1, name=field.name, field_type=iceberg_type, required=not field.nullable))
222
290
 
223
291
  iceberg_schema = Schema(*fields)
224
- iceberg_table = catalog.create_table(table_id, schema=iceberg_schema)
225
- 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)
226
307
 
227
308
  logger.info("Written %d rows to %s", len(arrow_table), table_id)
228
309
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steve-cli
3
- Version: 0.5.4
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