icegraph-client 0.14.4__py3-none-any.whl
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.
- icegraph_client/__init__.py +6 -0
- icegraph_client/cli.py +81 -0
- icegraph_client/clients/graph_client.py +72 -0
- icegraph_client/clients/icegraph_client.py +42 -0
- icegraph_client/clients/snapshots_client.py +29 -0
- icegraph_client/clients/tables_client.py +15 -0
- icegraph_client/utils/http_utils.py +13 -0
- icegraph_client/utils/json_utils.py +22 -0
- icegraph_client-0.14.4.dist-info/METADATA +7 -0
- icegraph_client-0.14.4.dist-info/RECORD +13 -0
- icegraph_client-0.14.4.dist-info/WHEEL +5 -0
- icegraph_client-0.14.4.dist-info/entry_points.txt +2 -0
- icegraph_client-0.14.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from icegraph_client.clients.icegraph_client import IceGraphClient
|
|
2
|
+
from icegraph_client.clients.snapshots_client import SnapshotInfo
|
|
3
|
+
from icegraph_client.clients.graph_client import GraphResult, Issues
|
|
4
|
+
from icegraph_client.utils.json_utils import jsonify
|
|
5
|
+
|
|
6
|
+
__all__ = ["IceGraphClient", "SnapshotInfo", "GraphResult", "Issues", "jsonify"]
|
icegraph_client/cli.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import inspect
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from icegraph_client.clients.icegraph_client import IceGraphClient
|
|
9
|
+
from icegraph_client.utils.json_utils import jsonify
|
|
10
|
+
|
|
11
|
+
BASE_URL_ENV_VAR = "ICEGRAPH_BASE_URL"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _tables(client: IceGraphClient, _: argparse.Namespace):
|
|
15
|
+
print("Fetching tables...", file=sys.stderr)
|
|
16
|
+
return client.list_tables()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _snapshots(client: IceGraphClient, args: argparse.Namespace):
|
|
20
|
+
print(f"Fetching snapshots for '{args.table}'...", file=sys.stderr)
|
|
21
|
+
return client.get_snapshot_map(args.table)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _graph(client: IceGraphClient, args: argparse.Namespace):
|
|
25
|
+
print(f"Building graph for '{args.table}'...", file=sys.stderr)
|
|
26
|
+
return client.get_graph(args.table, args.start_snapshot_id, args.end_snapshot_id)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
30
|
+
parser = argparse.ArgumentParser(prog="icegraph", description="CLI for the IceGraph server API")
|
|
31
|
+
parser.add_argument("--base-url", default=None, help=f"IceGraph server URL. Falls back to the {BASE_URL_ENV_VAR} environment variable")
|
|
32
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
33
|
+
|
|
34
|
+
tables_parser = subparsers.add_parser("tables", help="List all available tables")
|
|
35
|
+
tables_parser.set_defaults(handler=_tables)
|
|
36
|
+
|
|
37
|
+
snapshots_parser = subparsers.add_parser("snapshots", help="Get the snapshot map for a table")
|
|
38
|
+
snapshots_parser.add_argument("table", help="Full table name")
|
|
39
|
+
snapshots_parser.set_defaults(handler=_snapshots)
|
|
40
|
+
|
|
41
|
+
graph_parser = subparsers.add_parser("graph", help="Build the metadata graph for a table")
|
|
42
|
+
graph_parser.add_argument("table", help="Full table name")
|
|
43
|
+
graph_parser.add_argument("-s", "--start-snapshot-id", default=None, help="Start snapshot id")
|
|
44
|
+
graph_parser.add_argument("-e", "--end-snapshot-id", default=None, help="End snapshot id")
|
|
45
|
+
graph_parser.set_defaults(handler=_graph)
|
|
46
|
+
|
|
47
|
+
return parser
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main():
|
|
51
|
+
parser = build_parser()
|
|
52
|
+
args = parser.parse_args()
|
|
53
|
+
|
|
54
|
+
base_url = args.base_url or os.environ.get(BASE_URL_ENV_VAR)
|
|
55
|
+
if not base_url:
|
|
56
|
+
print(
|
|
57
|
+
inspect.cleandoc(f"""
|
|
58
|
+
No IceGraph server address was given.
|
|
59
|
+
|
|
60
|
+
Set it with one of the following:
|
|
61
|
+
As an argument: --base-url http://<icegraph-server-host>
|
|
62
|
+
As an environment variable (bash/zsh): export {BASE_URL_ENV_VAR}=http://<icegraph-server-host>
|
|
63
|
+
As an environment variable (cmd): set {BASE_URL_ENV_VAR}=http://<icegraph-server-host>
|
|
64
|
+
"""),
|
|
65
|
+
file=sys.stderr,
|
|
66
|
+
)
|
|
67
|
+
sys.exit(2)
|
|
68
|
+
|
|
69
|
+
client = IceGraphClient(base_url)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
result = args.handler(client, args)
|
|
73
|
+
except requests.RequestException as e:
|
|
74
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
|
|
77
|
+
print(jsonify(result))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
main()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
import arrow
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from icegraph_client.utils.http_utils import raise_for_status
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Issues:
|
|
12
|
+
errors: dict
|
|
13
|
+
warnings: dict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class GraphResult:
|
|
18
|
+
nodes: list
|
|
19
|
+
metadata: dict
|
|
20
|
+
issues: Issues
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GraphClient:
|
|
24
|
+
def __init__(self, base_url: str, **requests_kwargs):
|
|
25
|
+
self.base_url = base_url
|
|
26
|
+
self.requests_kwargs = requests_kwargs
|
|
27
|
+
|
|
28
|
+
def get_graph(self, table: str, start_snapshot_id: str = None, end_snapshot_id: str = None, poll_interval_seconds: float = 1.0) -> GraphResult:
|
|
29
|
+
data = self._get_graph(table, start_snapshot_id, end_snapshot_id, poll_interval_seconds)
|
|
30
|
+
nodes = self._extract_node_details(data["nodes"])
|
|
31
|
+
metadata = data["metadata"]
|
|
32
|
+
issues = Issues(errors=data["errors"], warnings=data["warnings"])
|
|
33
|
+
|
|
34
|
+
return GraphResult(nodes=nodes, metadata=metadata, issues=issues)
|
|
35
|
+
|
|
36
|
+
def _extract_node_details(self, nodes: list) -> list:
|
|
37
|
+
details_list = []
|
|
38
|
+
for node in nodes:
|
|
39
|
+
details = node.get("details", {})
|
|
40
|
+
for key, value in details.items():
|
|
41
|
+
if value and "timestamp" in key.lower():
|
|
42
|
+
details[key] = arrow.get(value).to("local")
|
|
43
|
+
details_list.append(details)
|
|
44
|
+
|
|
45
|
+
return details_list
|
|
46
|
+
|
|
47
|
+
def _get_graph(self, table: str, start_snapshot_id: str, end_snapshot_id: str, poll_interval_seconds: float) -> dict:
|
|
48
|
+
job_id = self._submit_graph_job(table, start_snapshot_id, end_snapshot_id)
|
|
49
|
+
|
|
50
|
+
while True:
|
|
51
|
+
result = self._get_graph_job(job_id)
|
|
52
|
+
if result.get("status") != "processing":
|
|
53
|
+
return result
|
|
54
|
+
time.sleep(poll_interval_seconds)
|
|
55
|
+
|
|
56
|
+
def _submit_graph_job(self, table: str, start_snapshot_id=None, end_snapshot_id=None) -> str:
|
|
57
|
+
response = requests.post(
|
|
58
|
+
f"{self.base_url}/api/v1/graph-data",
|
|
59
|
+
data={
|
|
60
|
+
"table_name": table,
|
|
61
|
+
"start_snapshot_id": start_snapshot_id,
|
|
62
|
+
"end_snapshot_id": end_snapshot_id,
|
|
63
|
+
},
|
|
64
|
+
**self.requests_kwargs,
|
|
65
|
+
)
|
|
66
|
+
raise_for_status(response)
|
|
67
|
+
return response.json()["key"]
|
|
68
|
+
|
|
69
|
+
def _get_graph_job(self, job_id: str) -> dict:
|
|
70
|
+
response = requests.get(f"{self.base_url}/api/v1/graph-data/{job_id}", **self.requests_kwargs)
|
|
71
|
+
raise_for_status(response)
|
|
72
|
+
return response.json()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from urllib.parse import urlparse
|
|
3
|
+
|
|
4
|
+
from icegraph_client.clients.tables_client import TablesClient
|
|
5
|
+
from icegraph_client.clients.snapshots_client import SnapshotsClient
|
|
6
|
+
from icegraph_client.clients.graph_client import GraphClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IceGraphClient:
|
|
10
|
+
def __init__(self, base_url: str, **requests_kwargs):
|
|
11
|
+
if "://" not in base_url:
|
|
12
|
+
base_url = f"https://{base_url}"
|
|
13
|
+
|
|
14
|
+
parsed_base_url = urlparse(base_url)
|
|
15
|
+
self._base_url = f"{parsed_base_url.scheme}://{parsed_base_url.netloc}"
|
|
16
|
+
|
|
17
|
+
self._tables_client = TablesClient(self._base_url, **requests_kwargs)
|
|
18
|
+
self._snapshots_client = SnapshotsClient(self._base_url, **requests_kwargs)
|
|
19
|
+
self._graph_client = GraphClient(self._base_url, **requests_kwargs)
|
|
20
|
+
|
|
21
|
+
def list_tables(self):
|
|
22
|
+
return self._tables_client.list_tables()
|
|
23
|
+
|
|
24
|
+
def get_snapshot_map(self, table: str):
|
|
25
|
+
return self._snapshots_client.get_snapshot_map(table)
|
|
26
|
+
|
|
27
|
+
def get_graph(self, table: str, start_snapshot_id: str = None, end_snapshot_id: str = None, poll_interval_seconds: float = 1.0):
|
|
28
|
+
if start_snapshot_id is None and end_snapshot_id is None:
|
|
29
|
+
snapshots = self.get_snapshot_map(table)
|
|
30
|
+
|
|
31
|
+
if snapshots:
|
|
32
|
+
latest_snapshot = max(snapshots, key=lambda snapshot: snapshot.timestamp)
|
|
33
|
+
print(
|
|
34
|
+
f"No snapshot range given, selected only the latest snapshot {latest_snapshot.snapshot_id} at {latest_snapshot.timestamp.humanize()}",
|
|
35
|
+
file=sys.stderr,
|
|
36
|
+
)
|
|
37
|
+
start_snapshot_id = latest_snapshot.snapshot_id
|
|
38
|
+
|
|
39
|
+
else:
|
|
40
|
+
print(f"Table {table} has no data, both start and end snapshot remain unset", file=sys.stderr)
|
|
41
|
+
|
|
42
|
+
return self._graph_client.get_graph(table, start_snapshot_id, end_snapshot_id, poll_interval_seconds)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
import arrow
|
|
5
|
+
|
|
6
|
+
from icegraph_client.utils.http_utils import raise_for_status
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class SnapshotInfo:
|
|
11
|
+
timestamp: arrow.Arrow
|
|
12
|
+
snapshot_id: str
|
|
13
|
+
operation: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SnapshotsClient:
|
|
17
|
+
def __init__(self, base_url: str, **requests_kwargs):
|
|
18
|
+
self.base_url = base_url
|
|
19
|
+
self.requests_kwargs = requests_kwargs
|
|
20
|
+
|
|
21
|
+
def get_snapshot_map(self, table: str) -> list[SnapshotInfo]:
|
|
22
|
+
response = requests.get(f"{self.base_url}/api/v1/snapshot-map/{table}", **self.requests_kwargs)
|
|
23
|
+
raise_for_status(response)
|
|
24
|
+
data = response.json()
|
|
25
|
+
|
|
26
|
+
return [
|
|
27
|
+
SnapshotInfo(timestamp=arrow.get(timestamp).to("local"), snapshot_id=snapshot["snapshot_id"], operation=snapshot["operation"])
|
|
28
|
+
for timestamp, snapshot in data.items()
|
|
29
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
from icegraph_client.utils.http_utils import raise_for_status
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TablesClient:
|
|
7
|
+
def __init__(self, base_url: str, **requests_kwargs):
|
|
8
|
+
self.base_url = base_url
|
|
9
|
+
self.requests_kwargs = requests_kwargs
|
|
10
|
+
|
|
11
|
+
def list_tables(self) -> list[str]:
|
|
12
|
+
response = requests.get(f"{self.base_url}/api/v1/tables", **self.requests_kwargs)
|
|
13
|
+
raise_for_status(response)
|
|
14
|
+
|
|
15
|
+
return response.json()["tables"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def raise_for_status(response: requests.Response) -> None:
|
|
5
|
+
try:
|
|
6
|
+
response.raise_for_status()
|
|
7
|
+
except requests.HTTPError as e:
|
|
8
|
+
try:
|
|
9
|
+
message = response.json().get("error", response.text)
|
|
10
|
+
except ValueError:
|
|
11
|
+
message = response.text
|
|
12
|
+
|
|
13
|
+
raise requests.HTTPError(f"{response.status_code} {response.reason} for {response.url}: {message}", response=response) from e
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import asdict, is_dataclass
|
|
3
|
+
|
|
4
|
+
import arrow
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _default(obj):
|
|
8
|
+
if isinstance(obj, arrow.Arrow):
|
|
9
|
+
return obj.isoformat()
|
|
10
|
+
return str(obj)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _serializable(obj):
|
|
14
|
+
if is_dataclass(obj) and not isinstance(obj, type):
|
|
15
|
+
return asdict(obj)
|
|
16
|
+
if isinstance(obj, list):
|
|
17
|
+
return [_serializable(item) for item in obj]
|
|
18
|
+
return obj
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def jsonify(obj, **json_dumps_kwargs) -> str:
|
|
22
|
+
return json.dumps(_serializable(obj), default=_default, **json_dumps_kwargs)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
icegraph_client/__init__.py,sha256=jp9woEe38ArpTL6doi5dybvMK6lszDRL2bnlKoSGifo,337
|
|
2
|
+
icegraph_client/cli.py,sha256=Ey0e4p9lgmTd86iT0jkaIM8-sDkThb8FqdvjMtwWAH4,2866
|
|
3
|
+
icegraph_client/clients/graph_client.py,sha256=sGkmNEtTfP2JTnPEqKozomMLrYbzRE3ooJB018LYCl0,2439
|
|
4
|
+
icegraph_client/clients/icegraph_client.py,sha256=l0k_VvlNyNztMrRarbkNOOACBsjJhPaRPX1ID5qIi28,1857
|
|
5
|
+
icegraph_client/clients/snapshots_client.py,sha256=usa_8Ft_LxYDXwDNNLGIZDN-SL7MoifhlFYaBgIYGqk,846
|
|
6
|
+
icegraph_client/clients/tables_client.py,sha256=MfLZdOs-DFZkD_njDRJ-CR26NSoqVjkUiLEh_o7C7Nw,447
|
|
7
|
+
icegraph_client/utils/http_utils.py,sha256=08WQk5nIKU8q0aKBLfrC7peLEBO3DjUa-vO4wtc2IeA,435
|
|
8
|
+
icegraph_client/utils/json_utils.py,sha256=FPWtAeP8g7Gb6fDCNAs0vDAI6_ZMKO5i6Vk7s-16JUo,516
|
|
9
|
+
icegraph_client-0.14.4.dist-info/METADATA,sha256=IJJJ641JzixJ4SbCBhyzIqCRXx7ZPle2CSFrS2Gzrhg,198
|
|
10
|
+
icegraph_client-0.14.4.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
11
|
+
icegraph_client-0.14.4.dist-info/entry_points.txt,sha256=kNyT7PJlvfpA7kxYUN58lg7swfOWc21JmB4XkPoXoL4,54
|
|
12
|
+
icegraph_client-0.14.4.dist-info/top_level.txt,sha256=0lADl2qhSGgqJE-E6A5wDpCycX_9tgOAamhPyqr2Ik8,16
|
|
13
|
+
icegraph_client-0.14.4.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
icegraph_client
|