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.
- {steve_cli-0.5.2 → steve_cli-0.5.3}/PKG-INFO +1 -1
- {steve_cli-0.5.2 → steve_cli-0.5.3}/pyproject.toml +1 -1
- steve_cli-0.5.3/steve_cli/auth.py +75 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/cli.py +153 -32
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/ontology.py +5 -3
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/policies/client.py +20 -3
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/trino.py +24 -2
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli.egg-info/PKG-INFO +1 -1
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli.egg-info/SOURCES.txt +1 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/README.md +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/setup.cfg +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/__init__.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/decorators/__init__.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/decorators/lineage_job.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/__init__.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/adapters/__init__.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/adapters/composite.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/adapters/dataproductregistry.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/adapters/logging.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/adapters/null.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/adapters/openlineage.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/collector.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/port.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/registry.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/lineage/storage.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/policies/__init__.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/__init__.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/__init__.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/extractors/__init__.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/extractors/csv.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/extractors/excel.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/extractors/generic.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/extractors/json.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/extractors/parquet.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/port.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/metadata/registry.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/parquet.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/protocol.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage/s3.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/storage.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/validation/__init__.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/validation/adapters/__init__.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/validation/adapters/great_expectations.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/validation/adapters/null.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/validation/adapters/validoopsie.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/validation/port.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/validation/registry.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli/vkg.py +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli.egg-info/dependency_links.txt +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli.egg-info/entry_points.txt +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli.egg-info/requires.txt +0 -0
- {steve_cli-0.5.2 → steve_cli-0.5.3}/steve_cli.egg-info/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
851
|
+
click.echo("No Trino storage variables found (expected: BRONZE_ACCESS_KEY or {WORKSPACE}_ACCESS_KEY).")
|
|
838
852
|
return
|
|
839
853
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
|
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
|