steve-cli 0.5.2__tar.gz → 0.5.3__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.2 → steve_cli-0.5.3}/PKG-INFO +1 -1
  2. {steve_cli-0.5.2 → steve_cli-0.5.3}/pyproject.toml +1 -1
  3. steve_cli-0.5.3/steve_cli/auth.py +75 -0
  4. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/cli.py +153 -32
  5. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/ontology.py +5 -3
  6. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/policies/client.py +20 -3
  7. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/trino.py +24 -2
  8. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli.egg-info/PKG-INFO +1 -1
  9. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli.egg-info/SOURCES.txt +1 -0
  10. {steve_cli-0.5.2 → steve_cli-0.5.3}/README.md +0 -0
  11. {steve_cli-0.5.2 → steve_cli-0.5.3}/setup.cfg +0 -0
  12. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/__init__.py +0 -0
  13. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/decorators/__init__.py +0 -0
  14. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/decorators/lineage_job.py +0 -0
  15. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/__init__.py +0 -0
  16. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/adapters/__init__.py +0 -0
  17. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/adapters/composite.py +0 -0
  18. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/adapters/dataproductregistry.py +0 -0
  19. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/adapters/logging.py +0 -0
  20. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/adapters/null.py +0 -0
  21. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/adapters/openlineage.py +0 -0
  22. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/collector.py +0 -0
  23. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/port.py +0 -0
  24. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/registry.py +0 -0
  25. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/storage.py +0 -0
  26. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/policies/__init__.py +0 -0
  27. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/__init__.py +0 -0
  28. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/__init__.py +0 -0
  29. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/extractors/__init__.py +0 -0
  30. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/extractors/csv.py +0 -0
  31. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/extractors/excel.py +0 -0
  32. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/extractors/generic.py +0 -0
  33. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/extractors/json.py +0 -0
  34. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/extractors/parquet.py +0 -0
  35. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/port.py +0 -0
  36. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/registry.py +0 -0
  37. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/parquet.py +0 -0
  38. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/protocol.py +0 -0
  39. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/s3.py +0 -0
  40. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage.py +0 -0
  41. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/validation/__init__.py +0 -0
  42. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/validation/adapters/__init__.py +0 -0
  43. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/validation/adapters/great_expectations.py +0 -0
  44. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/validation/adapters/null.py +0 -0
  45. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/validation/adapters/validoopsie.py +0 -0
  46. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/validation/port.py +0 -0
  47. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/validation/registry.py +0 -0
  48. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/vkg.py +0 -0
  49. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli.egg-info/dependency_links.txt +0 -0
  50. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli.egg-info/entry_points.txt +0 -0
  51. {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli.egg-info/requires.txt +0 -0
  52. {steve_cli-0.5.2 → steve_cli-0.5.3}/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.2
3
+ Version: 0.5.3
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.2"
8
+ version = "0.5.3"
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"}
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ _CREDENTIALS_FILE = Path.home() / ".steve" / "credentials.json"
9
+
10
+
11
+ def load_credentials() -> dict:
12
+ if _CREDENTIALS_FILE.exists():
13
+ try:
14
+ return json.loads(_CREDENTIALS_FILE.read_text())
15
+ except (json.JSONDecodeError, OSError):
16
+ return {}
17
+ return {}
18
+
19
+
20
+ def save_credentials(
21
+ workspace_id: str,
22
+ token: str,
23
+ base_url: Optional[str] = None,
24
+ workspace_name: Optional[str] = None,
25
+ ) -> None:
26
+ _CREDENTIALS_FILE.parent.mkdir(parents=True, exist_ok=True)
27
+ creds = load_credentials()
28
+ workspaces = creds.get("workspaces", {})
29
+ entry: dict = {"token": token}
30
+ if base_url:
31
+ entry["base_url"] = base_url.rstrip("/")
32
+ if workspace_name:
33
+ entry["workspace_name"] = workspace_name
34
+ workspaces[workspace_id] = entry
35
+ creds["workspaces"] = workspaces
36
+ _CREDENTIALS_FILE.write_text(json.dumps(creds, indent=2))
37
+ _CREDENTIALS_FILE.chmod(0o600)
38
+
39
+
40
+ def clear_credentials(workspace_id: Optional[str] = None) -> None:
41
+ if workspace_id is None:
42
+ if _CREDENTIALS_FILE.exists():
43
+ _CREDENTIALS_FILE.unlink()
44
+ return
45
+ creds = load_credentials()
46
+ workspaces = creds.get("workspaces", {})
47
+ workspaces.pop(workspace_id, None)
48
+ creds["workspaces"] = workspaces
49
+ _CREDENTIALS_FILE.write_text(json.dumps(creds, indent=2))
50
+ _CREDENTIALS_FILE.chmod(0o600)
51
+
52
+
53
+ def _get_workspace_entry(workspace_id: Optional[str]) -> dict:
54
+ creds = load_credentials()
55
+ workspaces = creds.get("workspaces", {})
56
+ if workspace_id and workspace_id in workspaces:
57
+ return workspaces[workspace_id]
58
+ if workspaces:
59
+ return next(iter(workspaces.values()), {})
60
+ return {}
61
+
62
+
63
+ def get_token(workspace_id: Optional[str] = None) -> Optional[str]:
64
+ env_token = os.getenv("STEVE_TOKEN")
65
+ if env_token:
66
+ return env_token
67
+ return _get_workspace_entry(workspace_id).get("token")
68
+
69
+
70
+ def get_service_url(service: str, workspace_id: Optional[str] = None) -> Optional[str]:
71
+ entry = _get_workspace_entry(workspace_id)
72
+ base_url = entry.get("base_url")
73
+ if not base_url or not workspace_id:
74
+ return None
75
+ return f"https://{service}-{workspace_id}.{base_url}"
@@ -101,9 +101,14 @@ def run_job_command(job: Dict[str, Any]) -> int:
101
101
 
102
102
 
103
103
  @click.group()
104
- def main():
104
+ @click.option('--trace', is_flag=True, default=False, help='Print timing information for Trino queries.', envvar='STEVE_TRACE')
105
+ @click.pass_context
106
+ def main(ctx: click.Context, trace: bool):
105
107
  """Steve CLI - Run jobs and manage environment setup."""
106
- pass
108
+ ctx.ensure_object(dict)
109
+ ctx.obj['trace'] = trace
110
+ if trace:
111
+ os.environ['STEVE_TRINO_TRACE'] = '1'
107
112
 
108
113
 
109
114
  @main.group(invoke_without_command=True)
@@ -803,17 +808,26 @@ def _list_trino_tables(storage_kwargs: dict, label: str) -> tuple:
803
808
  @main.command("tables")
804
809
  @click.option('--env-file', '-e', type=click.Path(path_type=Path), multiple=True,
805
810
  help='Path to .env file(s). Can be specified multiple times. Defaults to .env and .workspaces.env')
806
- def tables(env_file: tuple):
811
+ @click.option('--schema', '-s', default=None,
812
+ help='Schema to use, e.g. "default/bronze" or "myworkspace/silver". Skips interactive prompt.')
813
+ @click.option('--table', '-t', default=None,
814
+ help='Table name to open directly. Skips interactive prompt.')
815
+ @click.option('--output', '-o', default='visidata',
816
+ type=click.Choice(['visidata', 'table', 'csv', 'json']),
817
+ help='Output format: visidata (interactive), table, csv, or json.')
818
+ @click.pass_context
819
+ def tables(ctx: click.Context, env_file: tuple, schema: str | None, table: str | None, output: str):
807
820
  """List Iceberg tables via Trino and view their contents."""
808
- if not os.getenv("TRINO_ENDPOINT"):
809
- click.secho("TRINO_ENDPOINT is not set — Trino is not available.", fg="yellow")
810
- return
811
-
812
821
  cwd = Path.cwd()
813
822
  env_files = [Path(f) for f in env_file] if env_file else [cwd / ".env", cwd / ".workspaces.env"]
814
823
  for ef in env_files:
815
824
  load_dotenv(ef)
816
825
 
826
+ from steve_cli.auth import get_service_url
827
+ if not get_service_url("trino", os.getenv("WORKSPACE_ID")):
828
+ click.secho("Not logged in — run `steve login` to configure credentials.", fg="yellow")
829
+ return
830
+
817
831
  tiers = ["bronze", "silver", "gold"]
818
832
  options: List[Dict[str, Any]] = []
819
833
 
@@ -834,42 +848,85 @@ def tables(env_file: tuple):
834
848
  })
835
849
 
836
850
  if not options:
837
- click.echo("No Trino storage env variables found (expected: TRINO_ENDPOINT + BRONZE_ACCESS_KEY or {WORKSPACE}_ACCESS_KEY).")
851
+ click.echo("No Trino storage variables found (expected: BRONZE_ACCESS_KEY or {WORKSPACE}_ACCESS_KEY).")
838
852
  return
839
853
 
840
- choice = questionary.select(
841
- "Select a schema to list:",
842
- choices=[o["label"] for o in options],
843
- ).ask()
844
-
845
- if choice is None:
846
- sys.exit(0)
854
+ if schema:
855
+ parts = [p.strip() for p in schema.split("/")]
856
+ if len(parts) == 2:
857
+ ws_part, tier_part = parts
858
+ normalized = f"{ws_part} / {tier_part}"
859
+ selected = next((o for o in options if o["label"].lower() == normalized.lower()), None)
860
+ if not selected:
861
+ click.secho(f"Schema '{schema}' not found. Available: {', '.join(o['label'] for o in options)}", fg="red")
862
+ return
863
+ else:
864
+ click.secho("--schema must be in format 'workspace/tier' or 'default/tier'", fg="red")
865
+ return
866
+ else:
867
+ choice = questionary.select(
868
+ "Select a schema to list:",
869
+ choices=[o["label"] for o in options],
870
+ ).ask()
871
+ if choice is None:
872
+ sys.exit(0)
873
+ selected = next(o for o in options if o["label"] == choice)
847
874
 
848
- selected = next(o for o in options if o["label"] == choice)
849
875
  storage, table_names = _list_trino_tables(selected["kwargs"], selected["label"])
850
876
 
851
877
  if not storage or not table_names:
852
878
  return
853
879
 
854
- table_choice = questionary.select(
855
- "View a table (or press Esc to exit):",
856
- choices=["(done)"] + table_names,
857
- ).ask()
858
-
859
- if not table_choice or table_choice == "(done)":
860
- return
880
+ if table:
881
+ if table not in table_names:
882
+ click.secho(f"Table '{table}' not found. Available: {', '.join(table_names)}", fg="red")
883
+ return
884
+ table_choice = table
885
+ else:
886
+ table_choice = questionary.select(
887
+ "View a table (or press Esc to exit):",
888
+ choices=["(done)"] + table_names,
889
+ ).ask()
890
+ if not table_choice or table_choice == "(done)":
891
+ return
861
892
 
862
893
  click.echo(f"\n📊 {click.style(table_choice, fg='cyan')}\n")
863
894
  try:
864
- import tempfile
865
- data = storage.get_bytes(table_choice)
866
- with tempfile.NamedTemporaryFile(suffix=".parquet", delete=False) as tmp:
867
- tmp.write(data)
868
- tmp_path = tmp.name
869
- if not shutil.which("vd"):
870
- click.secho("visidata not found. Install it with: uv pip install 'steve-cli[visidata]'", fg="yellow")
871
- return
872
- subprocess.call(["vd", tmp_path])
895
+ if output == 'visidata':
896
+ import tempfile
897
+ data = storage.get_bytes(table_choice)
898
+ with tempfile.NamedTemporaryFile(suffix=".parquet", delete=False) as tmp:
899
+ tmp.write(data)
900
+ tmp_path = tmp.name
901
+ if not shutil.which("vd"):
902
+ click.secho("visidata not found. Install it with: uv pip install 'steve-cli[visidata]'", fg="yellow")
903
+ return
904
+ subprocess.call(["vd", tmp_path])
905
+ else:
906
+ rows = storage._execute(f"SELECT * FROM {storage.catalog}.{storage.schema}.{storage._table_name(table_choice)}")
907
+ if not rows:
908
+ click.echo("(no rows)")
909
+ return
910
+ if output == 'json':
911
+ import json
912
+ click.echo(json.dumps(rows, indent=2, default=str))
913
+ elif output == 'csv':
914
+ import csv
915
+ import io as _io
916
+ buf = _io.StringIO()
917
+ writer = csv.DictWriter(buf, fieldnames=list(rows[0].keys()))
918
+ writer.writeheader()
919
+ writer.writerows(rows)
920
+ click.echo(buf.getvalue(), nl=False)
921
+ elif output == 'table':
922
+ cols = list(rows[0].keys())
923
+ widths = [max(len(c), max(len(str(r.get(c, ''))) for r in rows)) for c in cols]
924
+ header = ' '.join(c.ljust(w) for c, w in zip(cols, widths))
925
+ sep = ' '.join('-' * w for w in widths)
926
+ click.echo(header)
927
+ click.echo(sep)
928
+ for row in rows:
929
+ click.echo(' '.join(str(row.get(c, '')).ljust(w) for c, w in zip(cols, widths)))
873
930
  except Exception as e:
874
931
  click.secho(f"❌ Could not open table: {e}", fg="red", err=True)
875
932
 
@@ -901,6 +958,70 @@ def policies_apply(file: Path | None):
901
958
  sys.exit(1)
902
959
 
903
960
 
961
+ @main.command("login")
962
+ @click.option("--token", default=None, help="Service principal token (stp_...) to save")
963
+ @click.option("--workspace", "workspace_id", default=None, help="Workspace ID (defaults to WORKSPACE_ID from .env)")
964
+ @click.option("--url", "base_url", default=None, help="Platform root host, e.g. jds-dev.internal.jambit.io")
965
+ @click.option("--workspace-name", "workspace_name_opt", default=None, help="Workspace name")
966
+ @click.option('--env-file', '-e', type=click.Path(path_type=Path), multiple=True,
967
+ help='Path to .env file(s). Defaults to .env and .workspaces.env')
968
+ def login(token: str | None, workspace_id: str | None, base_url: str | None, workspace_name_opt: str | None, env_file: tuple):
969
+ """Print CLI access URLs or save a service principal token."""
970
+ cwd = Path.cwd()
971
+ env_files = [Path(f) for f in env_file] if env_file else [cwd / ".env", cwd / ".workspaces.env"]
972
+ for ef in env_files:
973
+ load_dotenv(ef)
974
+
975
+ resolved_workspace = workspace_id or os.getenv("WORKSPACE_ID")
976
+ workspace_name = workspace_name_opt or os.getenv("WORKSPACE_NAME") or resolved_workspace
977
+
978
+ if token:
979
+ from steve_cli.auth import save_credentials
980
+ if not token.startswith("stp_"):
981
+ click.secho("Token must start with 'stp_'. Create one in the workspace CLI Access tab.", fg="red", err=True)
982
+ raise SystemExit(1)
983
+ if not resolved_workspace:
984
+ click.secho("Could not resolve workspace ID. Set WORKSPACE_ID in .env or pass --workspace.", fg="red", err=True)
985
+ raise SystemExit(1)
986
+ click.echo(f"\nYou are about to log in to workspace {click.style(workspace_name, fg='cyan', bold=True)} ({resolved_workspace})")
987
+ save_credentials(resolved_workspace, token, base_url=base_url, workspace_name=workspace_name)
988
+ if base_url:
989
+ click.echo(f"Platform: {base_url}")
990
+ click.secho(f"Logged in. Credentials saved to ~/.steve/credentials.json", fg="green")
991
+ return
992
+
993
+ if not resolved_workspace:
994
+ click.secho("WORKSPACE_ID is not set. Source your .env file or pass --workspace.", fg="yellow")
995
+ raise SystemExit(1)
996
+
997
+ dev_url = f"https://datameshx.jds-dev.internal.jambit.io/workspaces/{resolved_workspace}?tab=cli"
998
+ prod_url = f"https://datameshx.jds.internal.jambit.io/workspaces/{resolved_workspace}?tab=cli"
999
+
1000
+ click.echo(f"\nLogging in to workspace {click.style(workspace_name, fg='cyan', bold=True)}:\n")
1001
+ click.echo(f" Dev: {click.style(dev_url, fg='blue', underline=True)}")
1002
+ click.echo(f" Prod: {click.style(prod_url, fg='blue', underline=True)}")
1003
+ click.echo(f"\nOpen the URL, copy the token, then run:")
1004
+ click.echo(f" {click.style('steve login --token stp_...', fg='green')}\n")
1005
+
1006
+
1007
+ @main.command("logout")
1008
+ @click.option("--workspace", "workspace_id", default=None, help="Workspace ID to log out of (defaults to all)")
1009
+ @click.option('--env-file', '-e', type=click.Path(path_type=Path), multiple=True)
1010
+ def logout(workspace_id: str | None, env_file: tuple):
1011
+ """Remove saved CLI credentials."""
1012
+ from steve_cli.auth import clear_credentials
1013
+ cwd = Path.cwd()
1014
+ env_files = [Path(f) for f in env_file] if env_file else [cwd / ".env", cwd / ".workspaces.env"]
1015
+ for ef in env_files:
1016
+ load_dotenv(ef)
1017
+ resolved_workspace = workspace_id or os.getenv("WORKSPACE_ID")
1018
+ clear_credentials(resolved_workspace)
1019
+ if resolved_workspace:
1020
+ click.secho(f"Credentials removed for workspace {resolved_workspace}.", fg="green")
1021
+ else:
1022
+ click.secho("All credentials removed.", fg="green")
1023
+
1024
+
904
1025
  @main.command("upgrade")
905
1026
  def upgrade():
906
1027
  """Upgrade steve-cli to the latest version."""
@@ -421,7 +421,8 @@ def build_cmd(manifest_file: str, base_prefix: str | None, output_dir: Path | No
421
421
  if output_dir is None:
422
422
  output_dir = Path("vkg_modules") / ".build" / Path(manifest_file).stem
423
423
 
424
- trino_endpoint = os.getenv("TRINO_ENDPOINT")
424
+ from steve_cli.auth import get_service_url
425
+ trino_endpoint = get_service_url("trino", os.getenv("WORKSPACE_ID"))
425
426
 
426
427
  click.echo(f"Building from {manifest_file} …")
427
428
  artifact, tables = _compile_local(manifest, cwd)
@@ -1548,7 +1549,8 @@ def push_cmd(
1548
1549
  click.secho("✓ Lock is current (--frozen)", fg="green")
1549
1550
  return
1550
1551
 
1551
- trino_endpoint = os.getenv("TRINO_ENDPOINT")
1552
+ from steve_cli.auth import get_service_url
1553
+ trino_endpoint = get_service_url("trino", os.getenv("WORKSPACE_ID"))
1552
1554
  click.echo(f"Pushing workspace from {chosen} …")
1553
1555
  artifact, tables = _compile_local(manifest, cwd)
1554
1556
  snapshots = _capture_snapshots(tables, trino_endpoint) if trino_endpoint and tables else {}
@@ -1578,7 +1580,7 @@ def push_cmd(
1578
1580
  _BIND_OPTIONS = [
1579
1581
  click.option("-f", "--file", "manifest_file", default="ontology.yaml", show_default=True),
1580
1582
  click.option("-b", "--binding", "binding_file", default=None, help="Binding YAML path (default: first entry in manifest bindings:)"),
1581
- click.option("--trino-endpoint", envvar="TRINO_ENDPOINT", default=None),
1583
+ click.option("--trino-endpoint", default=None),
1582
1584
  ]
1583
1585
 
1584
1586
 
@@ -15,9 +15,25 @@ class PolicyClient:
15
15
  self,
16
16
  endpoint: str | None = None,
17
17
  token: str | None = None,
18
+ workspace_id: str | None = None,
18
19
  ):
19
- self.endpoint = (endpoint or os.environ.get("PCP_ENDPOINT", "http://policy-control-plane:3010")).rstrip("/")
20
- self.token = token or os.environ.get("PCP_TOKEN", "")
20
+ from steve_cli.auth import get_service_url, get_token
21
+ resolved_via_proxy = False
22
+ if endpoint is None:
23
+ pcp_url = get_service_url("pcp", workspace_id)
24
+ endpoint = (
25
+ os.environ.get("PCP_ENDPOINT")
26
+ or pcp_url
27
+ or "http://policy-control-plane:3010"
28
+ )
29
+ resolved_via_proxy = bool(pcp_url and not os.environ.get("PCP_ENDPOINT"))
30
+ self.endpoint = endpoint.rstrip("/")
31
+ if token:
32
+ self.token = token
33
+ elif resolved_via_proxy:
34
+ self.token = get_token(workspace_id) or os.environ.get("PCP_TOKEN", "")
35
+ else:
36
+ self.token = os.environ.get("PCP_TOKEN", "")
21
37
 
22
38
  def __trpc(self, method: str, path: str, payload: dict | None = None) -> dict:
23
39
  url = f"{self.endpoint}/api/trpc/{path}"
@@ -30,7 +46,8 @@ class PolicyClient:
30
46
  data = resp.json()
31
47
  if "error" in data:
32
48
  raise RuntimeError(f"tRPC error on {path}: {data['error']}")
33
- return data.get("result", {}).get("data", data.get("result", {}))
49
+ result = data.get("result", {})
50
+ return result.get("data", {}).get("json", result.get("data", result))
34
51
 
35
52
  def _trpc(self, method: str, path: str, payload: dict | None = None) -> dict:
36
53
  try:
@@ -22,9 +22,11 @@ def _derive_schema(tier: str, workspace: str | None) -> str | None:
22
22
 
23
23
  class TrinoStorage:
24
24
  def __init__(self, tier: str = "bronze", workspace: str | None = None):
25
- trino_endpoint = os.getenv("TRINO_ENDPOINT")
25
+ from steve_cli.auth import get_service_url
26
+ self._workspace_id = os.getenv("WORKSPACE_ID")
27
+ trino_endpoint = get_service_url("trino", self._workspace_id)
26
28
  if not trino_endpoint:
27
- raise EnvironmentError("TRINO_ENDPOINT is not set — Trino is not available in this environment")
29
+ raise EnvironmentError("Trino endpoint not found — run `steve login` to configure credentials")
28
30
  self.catalog = os.getenv("TRINO_CATALOG", "minio")
29
31
  resolved_workspace = workspace or os.getenv("WORKSPACE_NAME")
30
32
  self.schema = _derive_schema(tier, resolved_workspace) or os.environ.get("TRINO_SCHEMA", "")
@@ -92,15 +94,26 @@ class TrinoStorage:
92
94
  return self.__iceberg_catalog
93
95
 
94
96
  def _execute(self, sql: str) -> list[dict]:
97
+ from steve_cli.auth import get_token
98
+ trace = os.getenv("STEVE_TRINO_TRACE") == "1"
95
99
  headers = {
96
100
  "X-Trino-User": self.user,
97
101
  "X-Trino-Catalog": self.catalog,
98
102
  "X-Trino-Schema": self.schema,
99
103
  }
104
+ token = get_token(self._workspace_id)
105
+ if token:
106
+ headers["Authorization"] = f"Bearer {token}"
107
+
108
+ t0 = time.perf_counter()
100
109
  resp = requests.post(f"{self._base}/v1/statement", data=sql, headers=headers)
101
110
  resp.raise_for_status()
111
+ if trace:
112
+ logger.warning("[trino] POST /v1/statement: %.3fs", time.perf_counter() - t0)
113
+
102
114
  data = resp.json()
103
115
  rows: list[dict] = []
116
+ poll = 0
104
117
  while True:
105
118
  if "data" in data and "columns" in data:
106
119
  col_names = [c["name"] for c in data["columns"]]
@@ -110,9 +123,18 @@ class TrinoStorage:
110
123
  if not next_uri:
111
124
  break
112
125
  time.sleep(0.1)
126
+ next_uri = next_uri.replace("http://trino:8080", self._base).replace("https://trino:8080", self._base)
127
+ t_poll = time.perf_counter()
113
128
  resp = requests.get(next_uri, headers=headers)
114
129
  resp.raise_for_status()
130
+ poll += 1
131
+ if trace:
132
+ logger.warning("[trino] poll #%d: %.3fs (total %.3fs, %d rows so far)",
133
+ poll, time.perf_counter() - t_poll, time.perf_counter() - t0, len(rows))
115
134
  data = resp.json()
135
+
136
+ if trace:
137
+ logger.warning("[trino] query done: %.3fs total, %d rows, %d polls", time.perf_counter() - t0, len(rows), poll)
116
138
  return rows
117
139
 
118
140
  @staticmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steve-cli
3
- Version: 0.5.2
3
+ Version: 0.5.3
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
@@ -1,6 +1,7 @@
1
1
  README.md
2
2
  pyproject.toml
3
3
  steve_cli/__init__.py
4
+ steve_cli/auth.py
4
5
  steve_cli/cli.py
5
6
  steve_cli/ontology.py
6
7
  steve_cli/storage.py
File without changes
File without changes
File without changes