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.
@@ -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,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: icegraph-client
3
+ Version: 0.14.4
4
+ Summary: Python client and CLI for the IceGraph server API
5
+ Requires-Python: >=3.9
6
+ Requires-Dist: requests>=2.25
7
+ Requires-Dist: arrow>=1.0
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ icegraph = icegraph_client.cli:main
@@ -0,0 +1 @@
1
+ icegraph_client