antonlytics 1.0.0__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.
- antonlytics/__init__.py +119 -0
- antonlytics/_http.py +215 -0
- antonlytics/cli.py +273 -0
- antonlytics/client.py +213 -0
- antonlytics/exceptions.py +171 -0
- antonlytics/models.py +341 -0
- antonlytics/resources/__init__.py +11 -0
- antonlytics/resources/ingest.py +238 -0
- antonlytics/resources/projects.py +146 -0
- antonlytics/resources/query.py +243 -0
- antonlytics-1.0.0.dist-info/METADATA +370 -0
- antonlytics-1.0.0.dist-info/RECORD +14 -0
- antonlytics-1.0.0.dist-info/WHEEL +4 -0
- antonlytics-1.0.0.dist-info/entry_points.txt +2 -0
antonlytics/__init__.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Antonlytics Python SDK
|
|
3
|
+
======================
|
|
4
|
+
|
|
5
|
+
Official Python client for the Antonlytics Knowledge Graph API.
|
|
6
|
+
|
|
7
|
+
Quick start::
|
|
8
|
+
|
|
9
|
+
from antonlytics import Antonlytics, Triplet, EntityRef
|
|
10
|
+
|
|
11
|
+
anto = Antonlytics(api_key="anto_live_xxx")
|
|
12
|
+
|
|
13
|
+
# Ingest a relationship
|
|
14
|
+
anto.ingest.track(
|
|
15
|
+
project_id="proj_abc",
|
|
16
|
+
triplets=Triplet(
|
|
17
|
+
subject=EntityRef("Customer", id="c1", properties={"name": "Alice"}),
|
|
18
|
+
predicate="PURCHASED",
|
|
19
|
+
object=EntityRef("Product", id="p1", properties={"title": "Laptop Pro"}),
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Query the graph
|
|
24
|
+
result = (
|
|
25
|
+
anto.query.build("proj_abc")
|
|
26
|
+
.select("Customer", alias="c1")
|
|
27
|
+
.properties("name", "email", "country")
|
|
28
|
+
.eq("country", "USA")
|
|
29
|
+
.gte("age", 18)
|
|
30
|
+
.done()
|
|
31
|
+
.order_by("age", direction="desc")
|
|
32
|
+
.limit(50)
|
|
33
|
+
.run()
|
|
34
|
+
)
|
|
35
|
+
for row in result:
|
|
36
|
+
print(row)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from .client import Antonlytics, AsyncAntonlytics
|
|
40
|
+
from .exceptions import (
|
|
41
|
+
AntoError,
|
|
42
|
+
AuthenticationError,
|
|
43
|
+
IngestionFailedError,
|
|
44
|
+
InvalidConfigError,
|
|
45
|
+
NetworkError,
|
|
46
|
+
NotFoundError,
|
|
47
|
+
PermissionError,
|
|
48
|
+
PlanLimitError,
|
|
49
|
+
PollTimeoutError,
|
|
50
|
+
RateLimitError,
|
|
51
|
+
ServerError,
|
|
52
|
+
TimeoutError,
|
|
53
|
+
ValidationError,
|
|
54
|
+
)
|
|
55
|
+
from .models import (
|
|
56
|
+
ChartDataset,
|
|
57
|
+
DashboardMetrics,
|
|
58
|
+
DashboardSummary,
|
|
59
|
+
EntityRef,
|
|
60
|
+
EntitySpec,
|
|
61
|
+
EntityTypeDef,
|
|
62
|
+
GraphStats,
|
|
63
|
+
IngestResponse,
|
|
64
|
+
IngestResults,
|
|
65
|
+
IngestionEvent,
|
|
66
|
+
OntologyTree,
|
|
67
|
+
OrderBy,
|
|
68
|
+
Project,
|
|
69
|
+
PropertyDef,
|
|
70
|
+
QueryFilter,
|
|
71
|
+
QueryResult,
|
|
72
|
+
RelationshipDef,
|
|
73
|
+
RelationshipSpec,
|
|
74
|
+
Triplet,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
__version__ = "1.0.0"
|
|
78
|
+
__author__ = "Antonlytics"
|
|
79
|
+
__email__ = "sdk@antonlytics.com"
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
# Clients
|
|
83
|
+
"Antonlytics",
|
|
84
|
+
"AsyncAntonlytics",
|
|
85
|
+
# Models
|
|
86
|
+
"Triplet",
|
|
87
|
+
"EntityRef",
|
|
88
|
+
"EntitySpec",
|
|
89
|
+
"QueryFilter",
|
|
90
|
+
"OrderBy",
|
|
91
|
+
"RelationshipSpec",
|
|
92
|
+
"QueryResult",
|
|
93
|
+
"IngestResponse",
|
|
94
|
+
"IngestResults",
|
|
95
|
+
"IngestionEvent",
|
|
96
|
+
"Project",
|
|
97
|
+
"GraphStats",
|
|
98
|
+
"OntologyTree",
|
|
99
|
+
"EntityTypeDef",
|
|
100
|
+
"PropertyDef",
|
|
101
|
+
"RelationshipDef",
|
|
102
|
+
"DashboardMetrics",
|
|
103
|
+
"DashboardSummary",
|
|
104
|
+
"ChartDataset",
|
|
105
|
+
# Exceptions
|
|
106
|
+
"AntoError",
|
|
107
|
+
"AuthenticationError",
|
|
108
|
+
"PermissionError",
|
|
109
|
+
"NotFoundError",
|
|
110
|
+
"PlanLimitError",
|
|
111
|
+
"ValidationError",
|
|
112
|
+
"RateLimitError",
|
|
113
|
+
"ServerError",
|
|
114
|
+
"NetworkError",
|
|
115
|
+
"TimeoutError",
|
|
116
|
+
"IngestionFailedError",
|
|
117
|
+
"PollTimeoutError",
|
|
118
|
+
"InvalidConfigError",
|
|
119
|
+
]
|
antonlytics/_http.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Antonlytics SDK — HTTP transport layer.
|
|
3
|
+
|
|
4
|
+
Provides both synchronous (httpx.Client) and asynchronous (httpx.AsyncClient)
|
|
5
|
+
request methods with:
|
|
6
|
+
- automatic retry with exponential backoff on 5xx / network errors
|
|
7
|
+
- timeout enforcement
|
|
8
|
+
- X-Api-Key header injection
|
|
9
|
+
- structured error parsing
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from .exceptions import (
|
|
19
|
+
AntoError, NetworkError, TimeoutError as AntoTimeoutError,
|
|
20
|
+
ServerError, error_from_response,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
SDK_VERSION = "1.0.0"
|
|
24
|
+
RETRY_ON = {429, 500, 502, 503, 504}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _build_headers(api_key: str) -> dict[str, str]:
|
|
28
|
+
return {
|
|
29
|
+
"X-Api-Key": api_key,
|
|
30
|
+
"X-Sdk-Version": SDK_VERSION,
|
|
31
|
+
"X-Sdk-Language": "python",
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
"Accept": "application/json",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _parse_error(response: httpx.Response) -> AntoError:
|
|
38
|
+
try:
|
|
39
|
+
body = response.json()
|
|
40
|
+
except Exception:
|
|
41
|
+
body = {}
|
|
42
|
+
return error_from_response(response.status_code, body)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _should_retry(status: int, attempt: int, max_retries: int) -> bool:
|
|
46
|
+
return status in RETRY_ON and attempt < max_retries
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _backoff(attempt: int) -> float:
|
|
50
|
+
"""Exponential backoff: 0.3s, 0.6s, 1.2s …"""
|
|
51
|
+
return 0.3 * (2 ** attempt)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ── Synchronous client ────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
class HttpClient:
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
base_url: str,
|
|
60
|
+
api_key: str,
|
|
61
|
+
timeout: float,
|
|
62
|
+
max_retries: int,
|
|
63
|
+
debug: bool,
|
|
64
|
+
) -> None:
|
|
65
|
+
self._base = base_url.rstrip("/")
|
|
66
|
+
self._headers = _build_headers(api_key)
|
|
67
|
+
self._timeout = timeout
|
|
68
|
+
self._max_retries = max_retries
|
|
69
|
+
self._debug = debug
|
|
70
|
+
self._client = httpx.Client(
|
|
71
|
+
base_url=f"{self._base}/api/v1",
|
|
72
|
+
headers=self._headers,
|
|
73
|
+
timeout=timeout,
|
|
74
|
+
follow_redirects=True,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def close(self) -> None:
|
|
78
|
+
self._client.close()
|
|
79
|
+
|
|
80
|
+
def __enter__(self) -> "HttpClient":
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def __exit__(self, *_: Any) -> None:
|
|
84
|
+
self.close()
|
|
85
|
+
|
|
86
|
+
def request(
|
|
87
|
+
self,
|
|
88
|
+
method: str,
|
|
89
|
+
path: str,
|
|
90
|
+
*,
|
|
91
|
+
params: Optional[dict[str, Any]] = None,
|
|
92
|
+
json: Optional[Any] = None,
|
|
93
|
+
) -> Any:
|
|
94
|
+
if self._debug:
|
|
95
|
+
print(f"[Antonlytics] → {method} {path}", json or "")
|
|
96
|
+
|
|
97
|
+
attempt = 0
|
|
98
|
+
while True:
|
|
99
|
+
try:
|
|
100
|
+
response = self._client.request(method, path, params=params, json=json)
|
|
101
|
+
except httpx.TimeoutException:
|
|
102
|
+
raise AntoTimeoutError(self._timeout)
|
|
103
|
+
except httpx.RequestError as e:
|
|
104
|
+
raise NetworkError(str(e)) from e
|
|
105
|
+
|
|
106
|
+
if self._debug:
|
|
107
|
+
print(f"[Antonlytics] ← {response.status_code} {path}")
|
|
108
|
+
|
|
109
|
+
if _should_retry(response.status_code, attempt, self._max_retries):
|
|
110
|
+
time.sleep(_backoff(attempt))
|
|
111
|
+
attempt += 1
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
if not response.is_success:
|
|
115
|
+
raise _parse_error(response)
|
|
116
|
+
|
|
117
|
+
if response.status_code == 204:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
return response.json()
|
|
121
|
+
|
|
122
|
+
def get(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
|
|
123
|
+
return self.request("GET", path, params=params)
|
|
124
|
+
|
|
125
|
+
def post(self, path: str, json: Any = None) -> Any:
|
|
126
|
+
return self.request("POST", path, json=json)
|
|
127
|
+
|
|
128
|
+
def patch(self, path: str, json: Any = None) -> Any:
|
|
129
|
+
return self.request("PATCH", path, json=json)
|
|
130
|
+
|
|
131
|
+
def delete(self, path: str) -> Any:
|
|
132
|
+
return self.request("DELETE", path)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ── Asynchronous client ───────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
class AsyncHttpClient:
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
base_url: str,
|
|
141
|
+
api_key: str,
|
|
142
|
+
timeout: float,
|
|
143
|
+
max_retries: int,
|
|
144
|
+
debug: bool,
|
|
145
|
+
) -> None:
|
|
146
|
+
self._base = base_url.rstrip("/")
|
|
147
|
+
self._headers = _build_headers(api_key)
|
|
148
|
+
self._timeout = timeout
|
|
149
|
+
self._max_retries = max_retries
|
|
150
|
+
self._debug = debug
|
|
151
|
+
self._client = httpx.AsyncClient(
|
|
152
|
+
base_url=f"{self._base}/api/v1",
|
|
153
|
+
headers=self._headers,
|
|
154
|
+
timeout=timeout,
|
|
155
|
+
follow_redirects=True,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
async def aclose(self) -> None:
|
|
159
|
+
await self._client.aclose()
|
|
160
|
+
|
|
161
|
+
async def __aenter__(self) -> "AsyncHttpClient":
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
165
|
+
await self.aclose()
|
|
166
|
+
|
|
167
|
+
async def request(
|
|
168
|
+
self,
|
|
169
|
+
method: str,
|
|
170
|
+
path: str,
|
|
171
|
+
*,
|
|
172
|
+
params: Optional[dict[str, Any]] = None,
|
|
173
|
+
json: Optional[Any] = None,
|
|
174
|
+
) -> Any:
|
|
175
|
+
import asyncio
|
|
176
|
+
|
|
177
|
+
if self._debug:
|
|
178
|
+
print(f"[Antonlytics] → {method} {path}", json or "")
|
|
179
|
+
|
|
180
|
+
attempt = 0
|
|
181
|
+
while True:
|
|
182
|
+
try:
|
|
183
|
+
response = await self._client.request(method, path, params=params, json=json)
|
|
184
|
+
except httpx.TimeoutException:
|
|
185
|
+
raise AntoTimeoutError(self._timeout)
|
|
186
|
+
except httpx.RequestError as e:
|
|
187
|
+
raise NetworkError(str(e)) from e
|
|
188
|
+
|
|
189
|
+
if self._debug:
|
|
190
|
+
print(f"[Antonlytics] ← {response.status_code} {path}")
|
|
191
|
+
|
|
192
|
+
if _should_retry(response.status_code, attempt, self._max_retries):
|
|
193
|
+
await asyncio.sleep(_backoff(attempt))
|
|
194
|
+
attempt += 1
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
if not response.is_success:
|
|
198
|
+
raise _parse_error(response)
|
|
199
|
+
|
|
200
|
+
if response.status_code == 204:
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
return response.json()
|
|
204
|
+
|
|
205
|
+
async def get(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
|
|
206
|
+
return await self.request("GET", path, params=params)
|
|
207
|
+
|
|
208
|
+
async def post(self, path: str, json: Any = None) -> Any:
|
|
209
|
+
return await self.request("POST", path, json=json)
|
|
210
|
+
|
|
211
|
+
async def patch(self, path: str, json: Any = None) -> Any:
|
|
212
|
+
return await self.request("PATCH", path, json=json)
|
|
213
|
+
|
|
214
|
+
async def delete(self, path: str) -> Any:
|
|
215
|
+
return await self.request("DELETE", path)
|
antonlytics/cli.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
anto — Antonlytics CLI
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
ANTO_API_KEY=anto_live_xxx anto <command> [args]
|
|
6
|
+
|
|
7
|
+
Commands:
|
|
8
|
+
projects List all projects
|
|
9
|
+
stats <project-id> Graph statistics
|
|
10
|
+
ontology <project-id> Print ontology schema
|
|
11
|
+
ingest <project-id> <file> Ingest triplets from JSON file
|
|
12
|
+
query <project-id> <file> Execute a JSON query file
|
|
13
|
+
dashboard <project-id> Print dashboard summary
|
|
14
|
+
poll <event-id> Poll an async ingestion event
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from .client import Antonlytics
|
|
26
|
+
from .exceptions import AntoError
|
|
27
|
+
from .models import EntityRef, Triplet
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── ANSI colours (auto-disabled when not a TTY) ───────────────────────────────
|
|
31
|
+
|
|
32
|
+
_IS_TTY = sys.stdout.isatty()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _c(code: str, text: str) -> str:
|
|
36
|
+
return f"\033[{code}m{text}\033[0m" if _IS_TTY else text
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def bold(t: str) -> str: return _c("1", t)
|
|
40
|
+
def dim(t: str) -> str: return _c("2", t)
|
|
41
|
+
def amber(t: str) -> str: return _c("33", t)
|
|
42
|
+
def green(t: str) -> str: return _c("32", t)
|
|
43
|
+
def red(t: str) -> str: return _c("31", t)
|
|
44
|
+
def cyan(t: str) -> str: return _c("36", t)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
def out(msg: str = "") -> None:
|
|
50
|
+
print(msg)
|
|
51
|
+
|
|
52
|
+
def err(msg: str = "") -> None:
|
|
53
|
+
print(msg, file=sys.stderr)
|
|
54
|
+
|
|
55
|
+
def die(msg: str) -> None:
|
|
56
|
+
print(f"\n {red('✗')} {msg}\n", file=sys.stderr)
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
def hdr(title: str) -> None:
|
|
60
|
+
out()
|
|
61
|
+
out(f" {bold(title)}")
|
|
62
|
+
out(f" {'═' * max(len(title), 36)}")
|
|
63
|
+
|
|
64
|
+
def row(label: str, value: Any) -> None:
|
|
65
|
+
out(f" {dim(label.ljust(22))} {bold(str(value))}")
|
|
66
|
+
|
|
67
|
+
def need(args: list[str], idx: int, name: str) -> str:
|
|
68
|
+
if idx >= len(args):
|
|
69
|
+
die(f"Missing argument: <{name}>")
|
|
70
|
+
return args[idx]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── Commands ──────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
def cmd_projects(anto: Antonlytics) -> None:
|
|
76
|
+
projects = anto.projects.list()
|
|
77
|
+
hdr("PROJECTS")
|
|
78
|
+
if not projects:
|
|
79
|
+
out(" No projects found.")
|
|
80
|
+
return
|
|
81
|
+
for p in projects:
|
|
82
|
+
out(f" {amber(p.id[:8])}… {bold(p.name)} {dim(p.description or '')}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def cmd_stats(anto: Antonlytics, project_id: str) -> None:
|
|
86
|
+
s = anto.projects.stats(project_id)
|
|
87
|
+
hdr("GRAPH STATS")
|
|
88
|
+
row("Entity types", s.entity_types)
|
|
89
|
+
row("Relationship types", s.relationship_types)
|
|
90
|
+
row("Total entities", f"{s.total_entities:,}")
|
|
91
|
+
row("Total relationships", f"{s.total_relationships:,}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cmd_ontology(anto: Antonlytics, project_id: str) -> None:
|
|
95
|
+
tree = anto.query.ontology(project_id)
|
|
96
|
+
hdr("ONTOLOGY")
|
|
97
|
+
for type_name, defn in tree.items():
|
|
98
|
+
out(f"\n {bold(amber(type_name))}")
|
|
99
|
+
out(f" {'─' * 36}")
|
|
100
|
+
if defn.properties:
|
|
101
|
+
out(f" {dim('Properties')}")
|
|
102
|
+
for p in defn.properties:
|
|
103
|
+
out(f" {p.name.ljust(22)} {dim(p.type)}")
|
|
104
|
+
if defn.relationships:
|
|
105
|
+
out(f" {dim('Relationships')}")
|
|
106
|
+
for r in defn.relationships:
|
|
107
|
+
out(f" {green(f'─[{r.name}]→')} {r.target}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def cmd_ingest(anto: Antonlytics, project_id: str, file_path: str) -> None:
|
|
111
|
+
raw = Path(file_path).read_text()
|
|
112
|
+
data = json.loads(raw)
|
|
113
|
+
raw_triplets = data if isinstance(data, list) else [data]
|
|
114
|
+
|
|
115
|
+
hdr("INGEST")
|
|
116
|
+
row("File", file_path)
|
|
117
|
+
row("Triplets", len(raw_triplets))
|
|
118
|
+
out()
|
|
119
|
+
|
|
120
|
+
triplets = [
|
|
121
|
+
Triplet(
|
|
122
|
+
subject=EntityRef(**t["subject"]),
|
|
123
|
+
predicate=t["predicate"],
|
|
124
|
+
object=EntityRef(**t["object"]),
|
|
125
|
+
relationship_properties=t.get("relationship_properties", {}),
|
|
126
|
+
)
|
|
127
|
+
for t in raw_triplets
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
def on_status(event: Any) -> None:
|
|
131
|
+
err(f" polling… {event.status}")
|
|
132
|
+
|
|
133
|
+
result = anto.ingest.track(project_id, triplets, on_status=on_status)
|
|
134
|
+
|
|
135
|
+
if hasattr(result, "results") and result.results: # type: ignore[union-attr]
|
|
136
|
+
r = result.results # type: ignore[union-attr]
|
|
137
|
+
row("Entities created", r.created_entities)
|
|
138
|
+
row("Entities updated", r.updated_entities)
|
|
139
|
+
row("Relationships created", r.created_relationships)
|
|
140
|
+
if r.errors:
|
|
141
|
+
out(f"\n {red(f'Errors: {len(r.errors)}')}")
|
|
142
|
+
for e in r.errors[:5]:
|
|
143
|
+
out(f" [{e['index']}] {e['error']}")
|
|
144
|
+
else:
|
|
145
|
+
row("Event ID", getattr(result, "id", getattr(result, "event_id", "queued")))
|
|
146
|
+
row("Status", getattr(result, "status", "done"))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def cmd_query(anto: Antonlytics, project_id: str, file_path: str) -> None:
|
|
150
|
+
payload = json.loads(Path(file_path).read_text())
|
|
151
|
+
hdr("QUERY")
|
|
152
|
+
result = anto.query.execute(project_id, payload)
|
|
153
|
+
row("Total", result.total)
|
|
154
|
+
row("Execution", f"{result.execution_ms}ms")
|
|
155
|
+
out()
|
|
156
|
+
|
|
157
|
+
if not result.rows:
|
|
158
|
+
out(" No results.")
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
cols = result.columns or [k for k in result.rows[0] if not k.startswith("_")]
|
|
162
|
+
widths = [
|
|
163
|
+
min(28, max(len(c), max((len(str(r.get(c, ""))) for r in result.rows[:30]), default=0)))
|
|
164
|
+
for c in cols
|
|
165
|
+
]
|
|
166
|
+
out(" " + bold(" ".join(c.ljust(w) for c, w in zip(cols, widths))))
|
|
167
|
+
out(" " + " ".join("─" * w for w in widths))
|
|
168
|
+
for r in result.rows[:50]:
|
|
169
|
+
out(" " + " ".join(str(r.get(c, "")).ljust(w)[:w] for c, w in zip(cols, widths)))
|
|
170
|
+
if result.total > 50:
|
|
171
|
+
out(f"\n {dim(f'…and {result.total - 50} more rows')}")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def cmd_dashboard(anto: Antonlytics, project_id: str) -> None:
|
|
175
|
+
m = anto.dashboard.metrics(project_id)
|
|
176
|
+
hdr(f"DASHBOARD · {m.project_name}")
|
|
177
|
+
out(f"\n {dim('SUMMARY')}")
|
|
178
|
+
row("Events tracked", f"{m.summary.events_tracked:,}")
|
|
179
|
+
row("Active entities", f"{m.summary.active_entities:,}")
|
|
180
|
+
row("Relationships", f"{m.summary.total_relationships:,}")
|
|
181
|
+
row("Query usage", f"{m.summary.query_usage:,}")
|
|
182
|
+
|
|
183
|
+
if m.top_ontology_queries:
|
|
184
|
+
out(f"\n {dim('TOP QUERIES')}")
|
|
185
|
+
for q in m.top_ontology_queries[:8]:
|
|
186
|
+
out(f" {str(q['count']).rjust(6)} {q['name']}")
|
|
187
|
+
|
|
188
|
+
if m.recent_events:
|
|
189
|
+
out(f"\n {dim('RECENT EVENTS')}")
|
|
190
|
+
for e in m.recent_events:
|
|
191
|
+
col = green if e.is_done else (red if e.is_failed else amber)
|
|
192
|
+
out(f" {col(e.status.ljust(12))} {e.triplets_count} triplets {dim(e.created_at)}")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def cmd_poll(anto: Antonlytics, event_id: str) -> None:
|
|
196
|
+
hdr(f"POLLING · {event_id}")
|
|
197
|
+
|
|
198
|
+
def on_status(event: Any) -> None:
|
|
199
|
+
out(f" {dim(time.strftime('%H:%M:%S'))} {amber(event.status)}")
|
|
200
|
+
|
|
201
|
+
event = anto.ingest.poll(event_id, timeout=120.0, on_status=on_status)
|
|
202
|
+
out()
|
|
203
|
+
row("Status", event.status)
|
|
204
|
+
row("Triplets", event.triplets_count)
|
|
205
|
+
row("Finished at", event.processed_at or "—")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def cmd_help() -> None:
|
|
209
|
+
out(f"""
|
|
210
|
+
{bold('anto')} — Antonlytics CLI {dim('v1.0.0')}
|
|
211
|
+
|
|
212
|
+
{dim('Environment:')}
|
|
213
|
+
ANTO_API_KEY Your API key {dim('(required)')}
|
|
214
|
+
ANTO_BASE_URL API base URL {dim('(default: https://api.antonlytics.com)')}
|
|
215
|
+
ANTO_DEBUG=1 Log raw HTTP requests
|
|
216
|
+
|
|
217
|
+
{dim('Commands:')}
|
|
218
|
+
{amber('projects')} List all projects
|
|
219
|
+
{amber('stats')} {dim('<project-id>')} Graph statistics
|
|
220
|
+
{amber('ontology')} {dim('<project-id>')} Print ontology schema
|
|
221
|
+
{amber('ingest')} {dim('<project-id> <file>')} Ingest triplets JSON file
|
|
222
|
+
{amber('query')} {dim('<project-id> <file>')} Execute JSON query file
|
|
223
|
+
{amber('dashboard')} {dim('<project-id>')} Print dashboard summary
|
|
224
|
+
{amber('poll')} {dim('<event-id>')} Poll async ingestion event
|
|
225
|
+
|
|
226
|
+
{dim('Examples:')}
|
|
227
|
+
ANTO_API_KEY=anto_live_xxx anto projects
|
|
228
|
+
ANTO_API_KEY=anto_live_xxx anto ingest proj_abc ./triplets.json
|
|
229
|
+
ANTO_API_KEY=anto_live_xxx anto dashboard proj_abc
|
|
230
|
+
""")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
def main() -> None:
|
|
236
|
+
api_key = os.environ.get("ANTO_API_KEY", "")
|
|
237
|
+
if not api_key:
|
|
238
|
+
die("Set ANTO_API_KEY environment variable.\n export ANTO_API_KEY=anto_live_xxx")
|
|
239
|
+
|
|
240
|
+
base_url = os.environ.get("ANTO_BASE_URL", "https://api.antonlytics.com")
|
|
241
|
+
debug = os.environ.get("ANTO_DEBUG") == "1"
|
|
242
|
+
|
|
243
|
+
args = sys.argv[1:]
|
|
244
|
+
cmd = args[0] if args else ""
|
|
245
|
+
|
|
246
|
+
if cmd in ("--help", "-h", "help", ""):
|
|
247
|
+
cmd_help()
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
anto = Antonlytics(api_key=api_key, base_url=base_url, debug=debug)
|
|
252
|
+
|
|
253
|
+
if cmd == "projects": cmd_projects(anto)
|
|
254
|
+
elif cmd == "stats": cmd_stats(anto, need(args, 1, "project-id"))
|
|
255
|
+
elif cmd == "ontology": cmd_ontology(anto, need(args, 1, "project-id"))
|
|
256
|
+
elif cmd == "ingest": cmd_ingest(anto, need(args, 1, "project-id"), need(args, 2, "file"))
|
|
257
|
+
elif cmd == "query": cmd_query(anto, need(args, 1, "project-id"), need(args, 2, "file"))
|
|
258
|
+
elif cmd == "dashboard": cmd_dashboard(anto, need(args, 1, "project-id"))
|
|
259
|
+
elif cmd == "poll": cmd_poll(anto, need(args, 1, "event-id"))
|
|
260
|
+
else:
|
|
261
|
+
die(f"Unknown command: '{cmd}'. Run 'anto --help' for usage.")
|
|
262
|
+
|
|
263
|
+
anto.close()
|
|
264
|
+
|
|
265
|
+
except AntoError as e:
|
|
266
|
+
die(f"[{e.code}] {e.message}" + (f" (HTTP {e.status})" if e.status else ""))
|
|
267
|
+
except KeyboardInterrupt:
|
|
268
|
+
out()
|
|
269
|
+
sys.exit(0)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
if __name__ == "__main__":
|
|
273
|
+
main()
|