traverse-http 0.6.7__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,59 @@
1
+ from .client import TraverseHttpClient
2
+ from .models import (
3
+ HealthResponse,
4
+ HealthDatabase,
5
+ LicenseInfo,
6
+ LicenseLimits,
7
+ UpdateInfo,
8
+ DatabaseInfo,
9
+ TvdbFileInfo,
10
+ UploadResult,
11
+ AliasInfo,
12
+ SchemaResponse,
13
+ SchemaProperty,
14
+ IndexInfo,
15
+ ConstraintInfo,
16
+ LabelDetail,
17
+ QueryResponse,
18
+ QueryStats,
19
+ ImportResult,
20
+ ImportError,
21
+ CsvImportResult,
22
+ CsvImportError,
23
+ CsvEdgeImportResult,
24
+ ColumnMapping,
25
+ EdgeEndpoint,
26
+ TaskInfo,
27
+ UserInfo,
28
+ )
29
+ from .exceptions import TraverseHttpException
30
+
31
+ __all__ = [
32
+ "TraverseHttpClient",
33
+ "TraverseHttpException",
34
+ "HealthResponse",
35
+ "HealthDatabase",
36
+ "LicenseInfo",
37
+ "LicenseLimits",
38
+ "UpdateInfo",
39
+ "DatabaseInfo",
40
+ "TvdbFileInfo",
41
+ "UploadResult",
42
+ "AliasInfo",
43
+ "SchemaResponse",
44
+ "SchemaProperty",
45
+ "IndexInfo",
46
+ "ConstraintInfo",
47
+ "LabelDetail",
48
+ "QueryResponse",
49
+ "QueryStats",
50
+ "ImportResult",
51
+ "ImportError",
52
+ "CsvImportResult",
53
+ "CsvImportError",
54
+ "CsvEdgeImportResult",
55
+ "ColumnMapping",
56
+ "EdgeEndpoint",
57
+ "TaskInfo",
58
+ "UserInfo",
59
+ ]
@@ -0,0 +1,490 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Any, BinaryIO
5
+ from urllib.parse import quote
6
+
7
+ import requests
8
+
9
+ from .exceptions import TraverseHttpException
10
+ from .models import (
11
+ AliasInfo,
12
+ ColumnMapping,
13
+ ConstraintInfo,
14
+ CsvEdgeImportResult,
15
+ CsvImportError,
16
+ CsvImportResult,
17
+ DatabaseInfo,
18
+ EdgeEndpoint,
19
+ HealthDatabase,
20
+ HealthResponse,
21
+ ImportError,
22
+ ImportResult,
23
+ IndexInfo,
24
+ LabelDetail,
25
+ LicenseInfo,
26
+ LicenseLimits,
27
+ QueryResponse,
28
+ QueryStats,
29
+ SchemaProperty,
30
+ SchemaResponse,
31
+ TaskInfo,
32
+ TvdbFileInfo,
33
+ UpdateInfo,
34
+ UploadResult,
35
+ UserInfo,
36
+ )
37
+
38
+
39
+ class TraverseHttpClient:
40
+ """HTTP client for the Traverse server management API."""
41
+
42
+ def __init__(
43
+ self,
44
+ base_url: str,
45
+ *,
46
+ api_key: str | None = None,
47
+ timeout: float = 100.0,
48
+ transfer_timeout: float = 3600.0,
49
+ allow_untrusted_certificate: bool = False,
50
+ ):
51
+ self._base_url = base_url.rstrip("/") + "/"
52
+ self._timeout = timeout
53
+ self._transfer_timeout = transfer_timeout
54
+ self._session = requests.Session()
55
+ if api_key:
56
+ self._session.headers["Authorization"] = f"Bearer {api_key}"
57
+ if allow_untrusted_certificate:
58
+ self._session.verify = False
59
+
60
+ def close(self) -> None:
61
+ self._session.close()
62
+
63
+ def __enter__(self) -> TraverseHttpClient:
64
+ return self
65
+
66
+ def __exit__(self, *args: Any) -> None:
67
+ self.close()
68
+
69
+ # ── Health ──────────────────────────────────────────────────────────
70
+
71
+ def health(self) -> HealthResponse:
72
+ data = self._get("api/health")
73
+ return _parse_health(data)
74
+
75
+ def ready(self) -> bool:
76
+ try:
77
+ resp = self._session.get(
78
+ self._url("api/ready"), timeout=self._timeout
79
+ )
80
+ return 200 <= resp.status_code < 300
81
+ except Exception:
82
+ return False
83
+
84
+ # ── Databases ───────────────────────────────────────────────────────
85
+
86
+ def list_databases(self) -> list[DatabaseInfo]:
87
+ data = self._get("api/databases")
88
+ return [_parse_database_info(d) for d in data]
89
+
90
+ def create_database(self, name: str) -> None:
91
+ self._post("api/databases", {"name": name})
92
+
93
+ def drop_database(self, name: str) -> None:
94
+ self._poll_task(self._delete(f"api/databases/{_encode(name)}?async=true"))
95
+
96
+ def rename_database(self, old_name: str, new_name: str) -> None:
97
+ self._post(f"api/databases/{_encode(old_name)}/rename", {"name": new_name})
98
+
99
+ def unload_database(self, name: str) -> None:
100
+ self._poll_task(
101
+ self._post_raw(
102
+ f"api/databases/{_encode(name)}/unload?async=true",
103
+ timeout=self._transfer_timeout,
104
+ )
105
+ )
106
+
107
+ def load_database(self, name: str, file: str) -> None:
108
+ self._poll_task(
109
+ self._post_raw(
110
+ "api/databases/load?async=true",
111
+ json={"name": name, "file": file},
112
+ timeout=self._transfer_timeout,
113
+ )
114
+ )
115
+
116
+ # ── Files ───────────────────────────────────────────────────────────
117
+
118
+ def list_files(self) -> list[TvdbFileInfo]:
119
+ data = self._get("api/files")
120
+ return [TvdbFileInfo(**d) for d in data]
121
+
122
+ def upload_file(self, name: str, data: bytes | BinaryIO) -> UploadResult:
123
+ if hasattr(data, "read"):
124
+ data = data.read()
125
+ resp = self._session.put(
126
+ self._url(f"api/files/{_encode(name)}"),
127
+ data=data,
128
+ headers={"Content-Type": "application/octet-stream"},
129
+ timeout=self._transfer_timeout,
130
+ )
131
+ _ensure_success(resp)
132
+ return UploadResult(**resp.json())
133
+
134
+ def download_file(self, name: str) -> bytes:
135
+ resp = self._session.get(
136
+ self._url(f"api/files/{_encode(name)}"),
137
+ timeout=self._transfer_timeout,
138
+ )
139
+ _ensure_success(resp)
140
+ return resp.content
141
+
142
+ def delete_file(self, name: str) -> None:
143
+ self._delete(f"api/files/{_encode(name)}")
144
+
145
+ # ── Aliases ─────────────────────────────────────────────────────────
146
+
147
+ def list_aliases(self) -> list[AliasInfo]:
148
+ data = self._get("api/aliases")
149
+ return [AliasInfo(**d) for d in data]
150
+
151
+ def create_alias(self, alias: str, database: str) -> None:
152
+ self._post("api/aliases", {"alias": alias, "database": database})
153
+
154
+ def alter_alias(self, alias: str, database: str) -> None:
155
+ self._put(f"api/aliases/{_encode(alias)}", {"database": database})
156
+
157
+ def drop_alias(self, alias: str) -> None:
158
+ self._delete(f"api/aliases/{_encode(alias)}")
159
+
160
+ # ── Schema ──────────────────────────────────────────────────────────
161
+
162
+ def get_schema(self, database: str | None = None) -> SchemaResponse:
163
+ path = "api/schema"
164
+ if database is not None:
165
+ path += f"?database={_encode(database)}"
166
+ data = self._get(path)
167
+ return _parse_schema(data)
168
+
169
+ # ── Query ───────────────────────────────────────────────────────────
170
+
171
+ def query(
172
+ self,
173
+ cypher: str,
174
+ parameters: dict[str, Any] | None = None,
175
+ database: str | None = None,
176
+ ) -> QueryResponse:
177
+ body: dict[str, Any] = {"query": cypher}
178
+ if parameters is not None:
179
+ body["parameters"] = parameters
180
+ if database is not None:
181
+ body["database"] = database
182
+ data = self._post("api/query", body)
183
+ return _parse_query_response(data)
184
+
185
+ # ── Import ──────────────────────────────────────────────────────────
186
+
187
+ def import_cypher(
188
+ self, cypher: str, database: str | None = None
189
+ ) -> ImportResult:
190
+ body: dict[str, str] = {"cypher": cypher}
191
+ if database is not None:
192
+ body["database"] = database
193
+ data = self._post("api/import", body)
194
+ return _parse_import_result(data)
195
+
196
+ def import_csv(
197
+ self,
198
+ csv: str,
199
+ label: str,
200
+ columns: list[ColumnMapping],
201
+ database: str | None = None,
202
+ delimiter: str | None = None,
203
+ skip_header: bool | None = None,
204
+ ) -> CsvImportResult:
205
+ body: dict[str, Any] = {
206
+ "csv": csv,
207
+ "label": label,
208
+ "columns": [c.to_dict() for c in columns],
209
+ }
210
+ if database is not None:
211
+ body["database"] = database
212
+ if delimiter is not None:
213
+ body["delimiter"] = delimiter
214
+ if skip_header is not None:
215
+ body["skip_header"] = skip_header
216
+ data = self._post("api/import/csv", body)
217
+ return _parse_csv_import_result(data)
218
+
219
+ def import_csv_edges(
220
+ self,
221
+ csv: str,
222
+ edge_type: str,
223
+ source: EdgeEndpoint,
224
+ target: EdgeEndpoint,
225
+ columns: list[ColumnMapping] | None = None,
226
+ database: str | None = None,
227
+ delimiter: str | None = None,
228
+ skip_header: bool | None = None,
229
+ ) -> CsvEdgeImportResult:
230
+ body: dict[str, Any] = {
231
+ "csv": csv,
232
+ "edge_type": edge_type,
233
+ "source": source.to_dict(),
234
+ "target": target.to_dict(),
235
+ }
236
+ if columns is not None:
237
+ body["columns"] = [c.to_dict() for c in columns]
238
+ if database is not None:
239
+ body["database"] = database
240
+ if delimiter is not None:
241
+ body["delimiter"] = delimiter
242
+ if skip_header is not None:
243
+ body["skip_header"] = skip_header
244
+ data = self._post("api/import/csv/edges", body)
245
+ return _parse_csv_edge_import_result(data)
246
+
247
+ # ── Users ───────────────────────────────────────────────────────────
248
+
249
+ def list_users(self) -> list[UserInfo]:
250
+ data = self._get("api/users")
251
+ return [UserInfo(**d) for d in data]
252
+
253
+ def create_user(self, username: str, password: str, role: str) -> None:
254
+ self._post(
255
+ "api/users",
256
+ {"username": username, "password": password, "role": role},
257
+ )
258
+
259
+ def update_user(
260
+ self,
261
+ username: str,
262
+ password: str | None = None,
263
+ role: str | None = None,
264
+ ) -> None:
265
+ body: dict[str, str] = {}
266
+ if password is not None:
267
+ body["password"] = password
268
+ if role is not None:
269
+ body["role"] = role
270
+ self._put(f"api/users/{_encode(username)}", body)
271
+
272
+ def delete_user(self, username: str) -> None:
273
+ self._delete(f"api/users/{_encode(username)}")
274
+
275
+ # ── Server operations ───────────────────────────────────────────────
276
+
277
+ def save(self) -> None:
278
+ self._poll_task(
279
+ self._post_raw("api/save?async=true", timeout=self._transfer_timeout)
280
+ )
281
+
282
+ # ── Tasks ───────────────────────────────────────────────────────────
283
+
284
+ def get_task(self, task_id: str) -> TaskInfo:
285
+ data = self._get(f"api/tasks/{_encode(task_id)}")
286
+ return _parse_task_info(data)
287
+
288
+ # ── Internal HTTP helpers ───────────────────────────────────────────
289
+
290
+ def _url(self, path: str) -> str:
291
+ return self._base_url + path
292
+
293
+ def _get(self, path: str) -> Any:
294
+ resp = self._session.get(self._url(path), timeout=self._timeout)
295
+ _ensure_success(resp)
296
+ return resp.json()
297
+
298
+ def _post(self, path: str, body: Any = None) -> Any:
299
+ resp = self._session.post(
300
+ self._url(path), json=body, timeout=self._timeout
301
+ )
302
+ _ensure_success(resp)
303
+ text = resp.text
304
+ if text:
305
+ return resp.json()
306
+ return None
307
+
308
+ def _post_raw(
309
+ self,
310
+ path: str,
311
+ json: Any = None,
312
+ timeout: float | None = None,
313
+ ) -> str:
314
+ resp = self._session.post(
315
+ self._url(path), json=json, timeout=timeout or self._timeout
316
+ )
317
+ _ensure_success(resp)
318
+ return resp.text
319
+
320
+ def _put(self, path: str, body: Any) -> None:
321
+ resp = self._session.put(
322
+ self._url(path), json=body, timeout=self._timeout
323
+ )
324
+ _ensure_success(resp)
325
+
326
+ def _delete(self, path: str) -> str:
327
+ resp = self._session.delete(self._url(path), timeout=self._timeout)
328
+ _ensure_success(resp)
329
+ return resp.text
330
+
331
+ def _poll_task(self, response_body: str) -> None:
332
+ if not response_body or not response_body.strip():
333
+ return
334
+ import json as _json
335
+
336
+ try:
337
+ data = _json.loads(response_body)
338
+ except ValueError:
339
+ return
340
+ task_id = data.get("taskId")
341
+ if not task_id:
342
+ return
343
+ delay = 0.25
344
+ while True:
345
+ time.sleep(delay)
346
+ task = self.get_task(task_id)
347
+ if task.status == "completed":
348
+ return
349
+ if task.status == "failed":
350
+ raise TraverseHttpException(task.error or "task failed")
351
+ delay = min(delay * 2, 2.0)
352
+
353
+
354
+ # ── Helpers ─────────────────────────────────────────────────────────
355
+
356
+
357
+ def _encode(s: str) -> str:
358
+ return quote(s, safe="")
359
+
360
+
361
+ def _ensure_success(resp: requests.Response) -> None:
362
+ if 200 <= resp.status_code < 300:
363
+ return
364
+ error_message = None
365
+ try:
366
+ data = resp.json()
367
+ error_message = data.get("error")
368
+ except Exception:
369
+ pass
370
+ raise TraverseHttpException(
371
+ error_message or f"HTTP {resp.status_code}",
372
+ status_code=resp.status_code,
373
+ )
374
+
375
+
376
+ # ── Model parsers ───────────────────────────────────────────────────
377
+
378
+
379
+ def _parse_health(d: dict) -> HealthResponse:
380
+ license_data = d.get("license")
381
+ license_info = None
382
+ if license_data:
383
+ limits_data = license_data.get("limits")
384
+ limits = LicenseLimits(**limits_data) if limits_data else None
385
+ license_info = LicenseInfo(
386
+ status=license_data.get("status", ""),
387
+ plan=license_data.get("plan"),
388
+ trial_days_remaining=license_data.get("trial_days_remaining"),
389
+ subscription_expires_at=license_data.get("subscription_expires_at"),
390
+ limits=limits,
391
+ )
392
+ update_data = d.get("update")
393
+ update_info = UpdateInfo(**update_data) if update_data else None
394
+ return HealthResponse(
395
+ status=d.get("status", ""),
396
+ version=d.get("version", ""),
397
+ uptime_secs=d.get("uptime_secs", 0),
398
+ memory_bytes=d.get("memory_bytes", 0),
399
+ databases=[HealthDatabase(**db) for db in d.get("databases", [])],
400
+ allow_drop_database=d.get("allow_drop_database", False),
401
+ auth_required=d.get("auth_required", False),
402
+ license=license_info,
403
+ update=update_info,
404
+ )
405
+
406
+
407
+ def _parse_database_info(d: dict) -> DatabaseInfo:
408
+ return DatabaseInfo(
409
+ name=d.get("name", ""),
410
+ default=d.get("default", False),
411
+ read_only=d.get("readOnly", False),
412
+ aliases=d.get("aliases", []),
413
+ )
414
+
415
+
416
+ def _parse_schema(d: dict) -> SchemaResponse:
417
+ return SchemaResponse(
418
+ labels=d.get("labels", []),
419
+ relationship_types=d.get("relationship_types", []),
420
+ property_keys=[SchemaProperty(**p) for p in d.get("property_keys", [])],
421
+ indexes=[IndexInfo(**i) for i in d.get("indexes", [])],
422
+ constraints=[ConstraintInfo(**c) for c in d.get("constraints", [])],
423
+ labels_detail=[_parse_label_detail(ld) for ld in d.get("labels_detail", [])],
424
+ relationship_types_detail=[
425
+ _parse_label_detail(ld) for ld in d.get("relationship_types_detail", [])
426
+ ],
427
+ )
428
+
429
+
430
+ def _parse_label_detail(d: dict) -> LabelDetail:
431
+ return LabelDetail(
432
+ name=d.get("name", ""),
433
+ properties=[SchemaProperty(**p) for p in d.get("properties", [])],
434
+ )
435
+
436
+
437
+ def _parse_query_response(d: dict) -> QueryResponse:
438
+ return QueryResponse(
439
+ columns=d.get("columns", []),
440
+ rows=d.get("rows", []),
441
+ total_rows=d.get("total_rows", 0),
442
+ truncated=d.get("truncated", False),
443
+ query_type=d.get("query_type", ""),
444
+ stats=QueryStats(**d["stats"]) if "stats" in d else QueryStats(),
445
+ time_ms=d.get("time_ms", 0.0),
446
+ parsing_time_ms=d.get("parsing_time_ms", 0.0),
447
+ planning_time_ms=d.get("planning_time_ms", 0.0),
448
+ execution_time_ms=d.get("execution_time_ms", 0.0),
449
+ )
450
+
451
+
452
+ def _parse_import_result(d: dict) -> ImportResult:
453
+ return ImportResult(
454
+ total_statements=d.get("total_statements", 0),
455
+ executed=d.get("executed", 0),
456
+ errors=[ImportError(**e) for e in d.get("errors", [])],
457
+ stats=QueryStats(**d["stats"]) if "stats" in d else QueryStats(),
458
+ time_ms=d.get("time_ms", 0.0),
459
+ )
460
+
461
+
462
+ def _parse_csv_import_result(d: dict) -> CsvImportResult:
463
+ return CsvImportResult(
464
+ nodes_created=d.get("nodes_created", 0),
465
+ properties_set=d.get("properties_set", 0),
466
+ labels_added=d.get("labels_added", 0),
467
+ errors=[CsvImportError(**e) for e in d.get("errors", [])],
468
+ time_ms=d.get("time_ms", 0.0),
469
+ )
470
+
471
+
472
+ def _parse_csv_edge_import_result(d: dict) -> CsvEdgeImportResult:
473
+ return CsvEdgeImportResult(
474
+ edges_created=d.get("edges_created", 0),
475
+ properties_set=d.get("properties_set", 0),
476
+ errors=[CsvImportError(**e) for e in d.get("errors", [])],
477
+ time_ms=d.get("time_ms", 0.0),
478
+ )
479
+
480
+
481
+ def _parse_task_info(d: dict) -> TaskInfo:
482
+ return TaskInfo(
483
+ id=d.get("id", ""),
484
+ operation=d.get("operation", ""),
485
+ status=d.get("status", ""),
486
+ result=d.get("result"),
487
+ error=d.get("error"),
488
+ created_at=d.get("createdAt", ""),
489
+ finished_at=d.get("finishedAt"),
490
+ )
@@ -0,0 +1,4 @@
1
+ class TraverseHttpException(Exception):
2
+ def __init__(self, message: str, status_code: int | None = None):
3
+ super().__init__(message)
4
+ self.status_code = status_code
@@ -0,0 +1,245 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ # ── Health ──────────────────────────────────────────────────────────
8
+
9
+ @dataclass
10
+ class LicenseLimits:
11
+ max_databases: int | None = None
12
+ max_nodes_per_db: int | None = None
13
+ max_edges_per_db: int | None = None
14
+ max_connections: int | None = None
15
+ user_management: bool = False
16
+ ai_chat: bool = False
17
+
18
+
19
+ @dataclass
20
+ class LicenseInfo:
21
+ status: str = ""
22
+ plan: str | None = None
23
+ trial_days_remaining: int | None = None
24
+ subscription_expires_at: str | None = None
25
+ limits: LicenseLimits | None = None
26
+
27
+
28
+ @dataclass
29
+ class UpdateInfo:
30
+ latest_version: str | None = None
31
+ release_notes: str | None = None
32
+ published_at: str | None = None
33
+ download_available: bool = False
34
+
35
+
36
+ @dataclass
37
+ class HealthDatabase:
38
+ name: str = ""
39
+ default: bool = False
40
+ nodes: int = 0
41
+ edges: int = 0
42
+ memory_bytes: int = 0
43
+
44
+
45
+ @dataclass
46
+ class HealthResponse:
47
+ status: str = ""
48
+ version: str = ""
49
+ uptime_secs: int = 0
50
+ memory_bytes: int = 0
51
+ databases: list[HealthDatabase] = field(default_factory=list)
52
+ allow_drop_database: bool = False
53
+ auth_required: bool = False
54
+ license: LicenseInfo | None = None
55
+ update: UpdateInfo | None = None
56
+
57
+
58
+ # ── Databases ───────────────────────────────────────────────────────
59
+
60
+ @dataclass
61
+ class DatabaseInfo:
62
+ name: str = ""
63
+ default: bool = False
64
+ read_only: bool = False
65
+ aliases: list[str] = field(default_factory=list)
66
+
67
+
68
+ # ── Files ───────────────────────────────────────────────────────────
69
+
70
+ @dataclass
71
+ class TvdbFileInfo:
72
+ name: str = ""
73
+ file: str = ""
74
+ bytes: int = 0
75
+ loaded: bool = False
76
+
77
+
78
+ @dataclass
79
+ class UploadResult:
80
+ ok: bool = False
81
+ name: str = ""
82
+ file: str = ""
83
+ bytes: int = 0
84
+
85
+
86
+ # ── Aliases ─────────────────────────────────────────────────────────
87
+
88
+ @dataclass
89
+ class AliasInfo:
90
+ alias: str = ""
91
+ database: str = ""
92
+
93
+
94
+ # ── Schema ──────────────────────────────────────────────────────────
95
+
96
+ @dataclass
97
+ class SchemaProperty:
98
+ name: str = ""
99
+ type: str = ""
100
+
101
+
102
+ @dataclass
103
+ class IndexInfo:
104
+ name: str = ""
105
+ label: str = ""
106
+ property: str = ""
107
+ entries: int = 0
108
+
109
+
110
+ @dataclass
111
+ class ConstraintInfo:
112
+ name: str = ""
113
+ type: str = ""
114
+ label: str = ""
115
+ property: str = ""
116
+
117
+
118
+ @dataclass
119
+ class LabelDetail:
120
+ name: str = ""
121
+ properties: list[SchemaProperty] = field(default_factory=list)
122
+
123
+
124
+ @dataclass
125
+ class SchemaResponse:
126
+ labels: list[str] = field(default_factory=list)
127
+ relationship_types: list[str] = field(default_factory=list)
128
+ property_keys: list[SchemaProperty] = field(default_factory=list)
129
+ indexes: list[IndexInfo] = field(default_factory=list)
130
+ constraints: list[ConstraintInfo] = field(default_factory=list)
131
+ labels_detail: list[LabelDetail] = field(default_factory=list)
132
+ relationship_types_detail: list[LabelDetail] = field(default_factory=list)
133
+
134
+
135
+ # ── Query ───────────────────────────────────────────────────────────
136
+
137
+ @dataclass
138
+ class QueryStats:
139
+ nodes_created: int = 0
140
+ nodes_deleted: int = 0
141
+ relationships_created: int = 0
142
+ relationships_deleted: int = 0
143
+ properties_set: int = 0
144
+ labels_added: int = 0
145
+ labels_removed: int = 0
146
+
147
+
148
+ @dataclass
149
+ class QueryResponse:
150
+ columns: list[str] = field(default_factory=list)
151
+ rows: list[list[Any]] = field(default_factory=list)
152
+ total_rows: int = 0
153
+ truncated: bool = False
154
+ query_type: str = ""
155
+ stats: QueryStats = field(default_factory=QueryStats)
156
+ time_ms: float = 0.0
157
+ parsing_time_ms: float = 0.0
158
+ planning_time_ms: float = 0.0
159
+ execution_time_ms: float = 0.0
160
+
161
+
162
+ # ── Import ──────────────────────────────────────────────────────────
163
+
164
+ @dataclass
165
+ class ImportError:
166
+ statement: str = ""
167
+ error: str = ""
168
+
169
+
170
+ @dataclass
171
+ class ImportResult:
172
+ total_statements: int = 0
173
+ executed: int = 0
174
+ errors: list[ImportError] = field(default_factory=list)
175
+ stats: QueryStats = field(default_factory=QueryStats)
176
+ time_ms: float = 0.0
177
+
178
+
179
+ @dataclass
180
+ class CsvImportError:
181
+ row: int = 0
182
+ error: str = ""
183
+
184
+
185
+ @dataclass
186
+ class CsvImportResult:
187
+ nodes_created: int = 0
188
+ properties_set: int = 0
189
+ labels_added: int = 0
190
+ errors: list[CsvImportError] = field(default_factory=list)
191
+ time_ms: float = 0.0
192
+
193
+
194
+ @dataclass
195
+ class CsvEdgeImportResult:
196
+ edges_created: int = 0
197
+ properties_set: int = 0
198
+ errors: list[CsvImportError] = field(default_factory=list)
199
+ time_ms: float = 0.0
200
+
201
+
202
+ @dataclass
203
+ class ColumnMapping:
204
+ name: str
205
+ property: str | None = None
206
+ type: str | None = None
207
+
208
+ def to_dict(self) -> dict[str, str]:
209
+ d: dict[str, str] = {"name": self.name}
210
+ if self.property is not None:
211
+ d["property"] = self.property
212
+ if self.type is not None:
213
+ d["type"] = self.type
214
+ return d
215
+
216
+
217
+ @dataclass
218
+ class EdgeEndpoint:
219
+ label: str
220
+ column: str
221
+ property: str
222
+
223
+ def to_dict(self) -> dict[str, str]:
224
+ return {"label": self.label, "column": self.column, "property": self.property}
225
+
226
+
227
+ # ── Tasks ───────────────────────────────────────────────────────────
228
+
229
+ @dataclass
230
+ class TaskInfo:
231
+ id: str = ""
232
+ operation: str = ""
233
+ status: str = ""
234
+ result: Any = None
235
+ error: str | None = None
236
+ created_at: str = ""
237
+ finished_at: str | None = None
238
+
239
+
240
+ # ── Users ───────────────────────────────────────────────────────────
241
+
242
+ @dataclass
243
+ class UserInfo:
244
+ username: str = ""
245
+ role: str = ""
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: traverse-http
3
+ Version: 0.6.7
4
+ Summary: Official Python HTTP client for Traverse — Truespar's in-memory graph database. Cypher queries, schema introspection, CSV import, database management.
5
+ Project-URL: Homepage, https://truespar.com/traverse/docs
6
+ Project-URL: Documentation, https://truespar.com/traverse/docs
7
+ Author: Truespar
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: cypher,database,graph,graph-database,traverse,truespar
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Database :: Front-Ends
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: requests>=2.28
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # traverse-http
27
+
28
+ Official Python HTTP client for [Traverse](https://truespar.com/traverse/docs) — Truespar's in-memory graph database.
29
+
30
+ - Execute Cypher queries with parameterized values
31
+ - Database management (create, drop, rename, load, unload)
32
+ - Schema introspection (labels, types, properties, indexes, constraints)
33
+ - CSV and Cypher import with async task polling
34
+ - File management (upload, download, delete `.tvdb` files)
35
+ - User and alias management
36
+ - Typed response models with dataclass return types
37
+ - Python 3.10+, MIT licensed
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install traverse-http
43
+ ```
44
+
45
+ ## Quick Start
46
+
47
+ ```python
48
+ from traverse_http import TraverseHttpClient
49
+
50
+ client = TraverseHttpClient("http://localhost:7691", api_key="tvs_your_key")
51
+
52
+ # Execute a query
53
+ result = client.query("MATCH (n:Person) RETURN n.name, n.age LIMIT 10")
54
+ for row in result.rows:
55
+ print(row)
56
+
57
+ # List databases
58
+ databases = client.list_databases()
59
+ for db in databases:
60
+ print(f"{db.name}: {db.node_count} nodes, {db.edge_count} edges")
61
+
62
+ # Get schema
63
+ schema = client.schema()
64
+ print(schema.labels)
65
+ ```
66
+
67
+ ## Database Management
68
+
69
+ ```python
70
+ client.create_database("mydb")
71
+ client.rename_database("mydb", "production")
72
+ client.drop_database("production") # requires --allow-drop-database on server
73
+ ```
74
+
75
+ ## CSV Import
76
+
77
+ ```python
78
+ result = client.import_csv(
79
+ file_path="people.csv",
80
+ label="Person",
81
+ database="mydb",
82
+ )
83
+ print(f"Imported {result.nodes_created} nodes")
84
+ ```
85
+
86
+ ## Documentation
87
+
88
+ - [Traverse documentation](https://truespar.com/traverse/docs)
89
+ - [Python SDK reference](https://truespar.com/traverse/docs/v1/python-sdk)
90
+
91
+ ## License
92
+
93
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,8 @@
1
+ traverse_http/__init__.py,sha256=0bSK9OK9QK22lIfm8ln2SGyeYzRD27MYSaC7Vti42DM,1196
2
+ traverse_http/client.py,sha256=SAqv6DrMbTYNvGBpEMLxR6_F5M3KBVIxGo2n8Az5yY0,17567
3
+ traverse_http/exceptions.py,sha256=LRx2nvtePTgXQzSm6G7NbdAh1-I5sCloGtA64V_ASRw,187
4
+ traverse_http/models.py,sha256=-trH4erXtzX9tC0ANFvlAxnAoPZ7rjebq63asXteyWs,6825
5
+ traverse_http-0.6.7.dist-info/METADATA,sha256=Z044XaUP5AQcAhM-DYHLBUsLqb0IIapzh3dysKcRp6c,2695
6
+ traverse_http-0.6.7.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ traverse_http-0.6.7.dist-info/licenses/LICENSE,sha256=xiUjVMIouoNYZsUw2NUed3Dv0_SQ3-C1DpgVwBxRo-s,10019
8
+ traverse_http-0.6.7.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,196 @@
1
+ TRAVERSE END-USER LICENSE AGREEMENT (EULA)
2
+
3
+ Copyright (c) 2025-2026 Truespar. All rights reserved.
4
+
5
+ IMPORTANT — READ CAREFULLY: This End-User License Agreement ("Agreement") is a
6
+ legal agreement between you (either an individual or a single entity, "Licensee")
7
+ and Truespar ("Licensor") for the Traverse software,
8
+ including all source code, binaries, documentation, and associated files
9
+ (collectively, the "Software").
10
+
11
+ By downloading, installing, copying, or otherwise using the Software, you agree
12
+ to be bound by the terms of this Agreement. If you do not agree, do not use the
13
+ Software.
14
+
15
+ 1. GRANT OF LICENSE
16
+
17
+ Subject to the terms of this Agreement, Licensor grants Licensee a limited,
18
+ non-exclusive, non-transferable, revocable license to:
19
+
20
+ (a) Use the Software for internal purposes, including evaluation, development,
21
+ and production deployment.
22
+
23
+ (b) Create applications and services that interface with the Software through
24
+ its published APIs (Bolt protocol, FFI bindings, gRPC, and CLI).
25
+
26
+ 2. RESTRICTIONS
27
+
28
+ Licensee shall NOT:
29
+
30
+ (a) Modify, adapt, alter, translate, or create derivative works of the
31
+ Software or any part thereof.
32
+
33
+ (b) Reverse engineer, disassemble, decompile, or otherwise attempt to derive
34
+ the source code, underlying ideas, algorithms, data structures, or
35
+ non-public APIs of the Software. To the extent that applicable law
36
+ (including EU Directive 2009/24/EC) grants a non-waivable right to
37
+ decompile for interoperability purposes, Licensee must first provide
38
+ thirty (30) days' prior written notice to Licensor and may exercise such
39
+ right only to the minimum extent necessary to achieve interoperability
40
+ with independently created software.
41
+
42
+ (c) Redistribute, sublicense, lease, rent, loan, sell, or otherwise transfer
43
+ the Software or any rights therein to any third party.
44
+
45
+ (d) Remove, alter, or obscure any proprietary notices, labels, or marks on
46
+ the Software.
47
+
48
+ (e) Use the Software to operate a service bureau, outsourcing service, or
49
+ managed-service offering for the benefit of third parties, or to provide
50
+ database-as-a-service offerings based on the Software, without a separate
51
+ commercial agreement with the Licensor.
52
+
53
+ (f) Use the Software to build a product or service that competes with the
54
+ Software.
55
+
56
+ (g) Publish benchmark results, performance comparisons, or similar evaluations
57
+ of the Software without prior written consent of the Licensor.
58
+
59
+ 3. INTELLECTUAL PROPERTY
60
+
61
+ The Software is the intellectual property of the Licensor and is protected by
62
+ copyright law and international treaties. Licensor retains all right, title,
63
+ and interest in and to the Software, including all intellectual property rights
64
+ therein. This Agreement does not convey any ownership interest in the Software.
65
+
66
+ 4. FEEDBACK
67
+
68
+ If Licensee provides suggestions, ideas, enhancement requests, or other
69
+ feedback regarding the Software ("Feedback"), Licensor may freely use,
70
+ incorporate, modify, license, and distribute such Feedback without restriction,
71
+ attribution, or compensation to Licensee. Licensee hereby assigns to Licensor
72
+ all right, title, and interest in and to any Feedback.
73
+
74
+ 5. CONFIDENTIALITY
75
+
76
+ (a) "Confidential Information" means the source code, object code, algorithms,
77
+ data structures, architecture, internal APIs, performance characteristics,
78
+ and any other non-public technical or business information of the Licensor
79
+ disclosed to or accessed by Licensee, whether in written, electronic, or
80
+ other form.
81
+
82
+ (b) Licensee shall hold all Confidential Information in strict confidence,
83
+ using at least the same degree of care it uses to protect its own
84
+ confidential information, but no less than reasonable care.
85
+
86
+ (c) Licensee shall not disclose Confidential Information to any third party
87
+ without the prior written consent of Licensor, except to Licensee's
88
+ employees or contractors who have a need to know and are bound by
89
+ confidentiality obligations at least as protective as those herein.
90
+
91
+ (d) The obligations in this section do not apply to information that:
92
+ (i) is or becomes publicly available through no fault of Licensee;
93
+ (ii) was rightfully known to Licensee prior to disclosure by Licensor;
94
+ (iii) is independently developed by Licensee without use of or reference
95
+ to the Confidential Information; or (iv) is required to be disclosed by
96
+ law, provided Licensee gives Licensor prompt written notice and
97
+ cooperates to limit the scope of disclosure.
98
+
99
+ (e) These confidentiality obligations survive termination of this Agreement
100
+ for a period of five (5) years, or indefinitely for trade secrets for as
101
+ long as they remain trade secrets under applicable law.
102
+
103
+ 6. THIRD-PARTY COMPONENTS
104
+
105
+ The Software incorporates open-source components licensed under permissive
106
+ licenses (MIT, Apache-2.0, BSD, and similar). These components retain their
107
+ original licenses. A list of third-party components and their licenses is
108
+ provided in the THIRD-PARTY-NOTICES file included with the Software.
109
+
110
+ Nothing in this Agreement restricts, limits, or otherwise affects any rights
111
+ Licensee may have under the licenses of such third-party components.
112
+
113
+ 7. NO WARRANTY
114
+
115
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
116
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
117
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
118
+ LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN
119
+ ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN
120
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
121
+
122
+ 8. LIMITATION OF LIABILITY
123
+
124
+ (a) IN NO EVENT SHALL LICENSOR'S TOTAL AGGREGATE LIABILITY TO LICENSEE FOR
125
+ ALL CLAIMS ARISING UNDER OR IN CONNECTION WITH THIS AGREEMENT EXCEED THE
126
+ GREATER OF: (I) THE TOTAL AMOUNT OF FEES ACTUALLY PAID BY LICENSEE TO
127
+ LICENSOR FOR THE SOFTWARE DURING THE TWELVE (12) MONTHS IMMEDIATELY
128
+ PRECEDING THE EVENT GIVING RISE TO THE CLAIM; OR (II) FIFTY US DOLLARS
129
+ (USD $50.00).
130
+
131
+ (b) IN NO EVENT SHALL LICENSOR BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
132
+ SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, INCLUDING BUT NOT LIMITED TO
133
+ LOSS OF PROFITS, DATA, BUSINESS, OR GOODWILL, REGARDLESS OF THE CAUSE OF
134
+ ACTION OR THE THEORY OF LIABILITY, EVEN IF LICENSOR HAS BEEN ADVISED OF
135
+ THE POSSIBILITY OF SUCH DAMAGES.
136
+
137
+ (c) THE LIMITATIONS IN SUBSECTIONS (a) AND (b) ABOVE SHALL NOT APPLY TO
138
+ LIABILITY ARISING FROM: (I) FRAUD OR FRAUDULENT MISREPRESENTATION;
139
+ (II) GROSS NEGLIGENCE OR WILLFUL MISCONDUCT; (III) LICENSEE'S BREACH OF
140
+ SECTIONS 2 (RESTRICTIONS) OR 5 (CONFIDENTIALITY); OR (IV) EITHER PARTY'S
141
+ INDEMNIFICATION OBLIGATIONS UNDER SECTION 9.
142
+
143
+ 9. INDEMNIFICATION
144
+
145
+ (a) Licensee shall defend, indemnify, and hold harmless Licensor and its
146
+ officers, directors, employees, and agents from and against any and all
147
+ third-party claims, damages, losses, liabilities, costs, and expenses
148
+ (including reasonable attorneys' fees) arising out of or related to:
149
+ (i) Licensee's breach of this Agreement, including but not limited to the
150
+ restrictions in Section 2 and the confidentiality obligations in
151
+ Section 5; (ii) Licensee's use of the Software in violation of applicable
152
+ law; or (iii) Licensee's gross negligence or willful misconduct.
153
+
154
+ (b) Licensor shall promptly notify Licensee of any such claim and provide
155
+ reasonable cooperation at Licensee's expense. Licensee shall have sole
156
+ control of the defense and settlement, provided that any settlement
157
+ requiring Licensor to admit liability or pay money shall require
158
+ Licensor's prior written consent.
159
+
160
+ 10. TERMINATION
161
+
162
+ (a) This Agreement is effective until terminated.
163
+
164
+ (b) Licensor may terminate this Agreement immediately upon written notice if
165
+ Licensee breaches Section 2 (Restrictions), Section 5 (Confidentiality),
166
+ or Section 9 (Indemnification).
167
+
168
+ (c) For any other material breach, the non-breaching party may terminate this
169
+ Agreement if the breaching party fails to cure such breach within thirty
170
+ (30) days of receiving written notice thereof.
171
+
172
+ (d) Upon termination, Licensee must immediately cease all use of the Software
173
+ and destroy all copies (including backups) in its possession or control
174
+ within five (5) business days. Upon Licensor's request, Licensee shall
175
+ certify such destruction in writing.
176
+
177
+ (e) Sections 2, 3, 4, 5, 7, 8, 9, 11, and 12 survive termination of this
178
+ Agreement.
179
+
180
+ 11. GOVERNING LAW
181
+
182
+ This Agreement shall be governed by and construed in accordance with the laws
183
+ of the State of Delaware, United States, without regard to its conflict-of-law
184
+ provisions. The United Nations Convention on Contracts for the International
185
+ Sale of Goods is expressly excluded. Any disputes arising under this Agreement
186
+ shall be resolved exclusively in the state or federal courts located in the
187
+ State of Delaware. Notwithstanding the foregoing, Licensor may seek injunctive
188
+ or other equitable relief in any court of competent jurisdiction to protect its
189
+ intellectual property rights or Confidential Information.
190
+
191
+ 12. ENTIRE AGREEMENT
192
+
193
+ This Agreement constitutes the entire agreement between the parties with
194
+ respect to the Software and supersedes all prior negotiations, representations,
195
+ or agreements relating thereto. No modification of this Agreement shall be
196
+ binding unless in writing and signed by both parties.