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.
@@ -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()