nerqon 1.1.0__tar.gz
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.
- nerqon-1.1.0/LICENSE +21 -0
- nerqon-1.1.0/Nerqon/__init__.py +65 -0
- nerqon-1.1.0/Nerqon/client.py +453 -0
- nerqon-1.1.0/Nerqon/exceptions.py +58 -0
- nerqon-1.1.0/Nerqon/models.py +125 -0
- nerqon-1.1.0/Nerqon.egg-info/PKG-INFO +325 -0
- nerqon-1.1.0/Nerqon.egg-info/SOURCES.txt +17 -0
- nerqon-1.1.0/Nerqon.egg-info/dependency_links.txt +1 -0
- nerqon-1.1.0/Nerqon.egg-info/requires.txt +6 -0
- nerqon-1.1.0/Nerqon.egg-info/top_level.txt +1 -0
- nerqon-1.1.0/PKG-INFO +325 -0
- nerqon-1.1.0/README.md +290 -0
- nerqon-1.1.0/pyproject.toml +54 -0
- nerqon-1.1.0/setup.cfg +4 -0
nerqon-1.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NidhiTek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Nerqon — Official Python SDK
|
|
3
|
+
==================================
|
|
4
|
+
|
|
5
|
+
Hybrid Vector + Graph Database for AI applications.
|
|
6
|
+
|
|
7
|
+
Quick Start::
|
|
8
|
+
|
|
9
|
+
from Nerqon import Nerqon
|
|
10
|
+
|
|
11
|
+
client = Nerqon(api_key="nidx_your_key")
|
|
12
|
+
|
|
13
|
+
# Add a document
|
|
14
|
+
client.add("doc-1", vector=[0.1, 0.2, ...], text="Hello world")
|
|
15
|
+
|
|
16
|
+
# Search
|
|
17
|
+
results = client.search(vector=[0.1, 0.2, ...], top_k=5)
|
|
18
|
+
for r in results.results:
|
|
19
|
+
print(f"{r.id}: {r.score:.4f}")
|
|
20
|
+
|
|
21
|
+
Full docs: https://nidhitek.com/documentation.html
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__version__ = "1.1.0"
|
|
25
|
+
|
|
26
|
+
from .client import Nerqon
|
|
27
|
+
from .exceptions import (
|
|
28
|
+
AuthenticationError,
|
|
29
|
+
ConnectionError,
|
|
30
|
+
DimensionMismatchError,
|
|
31
|
+
NerqonError,
|
|
32
|
+
NotFoundError,
|
|
33
|
+
RateLimitError,
|
|
34
|
+
ServerError,
|
|
35
|
+
ValidationError,
|
|
36
|
+
)
|
|
37
|
+
from .models import (
|
|
38
|
+
Document,
|
|
39
|
+
HealthStatus,
|
|
40
|
+
Namespace,
|
|
41
|
+
SearchResponse,
|
|
42
|
+
SearchResult,
|
|
43
|
+
Stats,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
# Client
|
|
48
|
+
"Nerqon",
|
|
49
|
+
# Exceptions
|
|
50
|
+
"NerqonError",
|
|
51
|
+
"AuthenticationError",
|
|
52
|
+
"RateLimitError",
|
|
53
|
+
"NotFoundError",
|
|
54
|
+
"ValidationError",
|
|
55
|
+
"DimensionMismatchError",
|
|
56
|
+
"ServerError",
|
|
57
|
+
"ConnectionError",
|
|
58
|
+
# Models
|
|
59
|
+
"Document",
|
|
60
|
+
"SearchResult",
|
|
61
|
+
"SearchResponse",
|
|
62
|
+
"Namespace",
|
|
63
|
+
"Stats",
|
|
64
|
+
"HealthStatus",
|
|
65
|
+
]
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Nerqon SDK — Python Client
|
|
3
|
+
================================
|
|
4
|
+
|
|
5
|
+
Official Python client for the Nerqon Hybrid Vector + Graph Database API.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from Nerqon import Nerqon
|
|
9
|
+
|
|
10
|
+
client = Nerqon(api_key="nidx_your_key")
|
|
11
|
+
client.add("doc-1", text="Hello world", vector=[0.1, 0.2, ...])
|
|
12
|
+
results = client.search(vector=[0.1, 0.2, ...], top_k=5)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import time
|
|
16
|
+
from typing import Any, Dict, List, Optional, Union
|
|
17
|
+
|
|
18
|
+
import requests
|
|
19
|
+
|
|
20
|
+
from .exceptions import (
|
|
21
|
+
AuthenticationError,
|
|
22
|
+
ConnectionError,
|
|
23
|
+
DimensionMismatchError,
|
|
24
|
+
NerqonError,
|
|
25
|
+
NotFoundError,
|
|
26
|
+
RateLimitError,
|
|
27
|
+
ServerError,
|
|
28
|
+
ValidationError,
|
|
29
|
+
)
|
|
30
|
+
from .models import (
|
|
31
|
+
Document,
|
|
32
|
+
HealthStatus,
|
|
33
|
+
Namespace,
|
|
34
|
+
SearchResponse,
|
|
35
|
+
Stats,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = ["Nerqon"]
|
|
39
|
+
|
|
40
|
+
_DEFAULT_BASE_URL = "https://api.nidhitek.com"
|
|
41
|
+
_DEFAULT_TIMEOUT = 30
|
|
42
|
+
_MAX_RETRIES = 3
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Nerqon:
|
|
46
|
+
"""
|
|
47
|
+
Official Python client for the Nerqon API.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
api_key: Your Nerqon API key (starts with ``nidx_``).
|
|
51
|
+
base_url: API base URL. Defaults to ``https://api.nidhitek.com``.
|
|
52
|
+
timeout: Request timeout in seconds. Defaults to 30.
|
|
53
|
+
max_retries: Max retries on transient failures. Defaults to 3.
|
|
54
|
+
namespace: Default namespace for all operations.
|
|
55
|
+
|
|
56
|
+
Example::
|
|
57
|
+
|
|
58
|
+
from Nerqon import Nerqon
|
|
59
|
+
|
|
60
|
+
client = Nerqon(api_key="nidx_your_key")
|
|
61
|
+
|
|
62
|
+
# Add a document
|
|
63
|
+
client.add("doc-1", text="AI is transforming search", vector=[0.1, 0.2, ...])
|
|
64
|
+
|
|
65
|
+
# Search
|
|
66
|
+
results = client.search(vector=[0.1, 0.2, ...], top_k=5)
|
|
67
|
+
for r in results.results:
|
|
68
|
+
print(f"{r.id}: {r.score:.4f} — {r.text[:80]}")
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
api_key: str,
|
|
74
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
75
|
+
timeout: int = _DEFAULT_TIMEOUT,
|
|
76
|
+
max_retries: int = _MAX_RETRIES,
|
|
77
|
+
namespace: str = "default",
|
|
78
|
+
):
|
|
79
|
+
if not api_key:
|
|
80
|
+
raise AuthenticationError("api_key is required. Get one at https://nidhitek.com/dashboard.html")
|
|
81
|
+
|
|
82
|
+
self.api_key = api_key
|
|
83
|
+
self.base_url = base_url.rstrip("/")
|
|
84
|
+
self.timeout = timeout
|
|
85
|
+
self.max_retries = max_retries
|
|
86
|
+
self.namespace = namespace
|
|
87
|
+
|
|
88
|
+
self._session = requests.Session()
|
|
89
|
+
self._session.headers.update({
|
|
90
|
+
"Authorization": f"Bearer {api_key}",
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
"User-Agent": "Nerqon-python/1.1.0",
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
# ──────────────────────────────────────────────
|
|
96
|
+
# Internal HTTP helpers
|
|
97
|
+
# ──────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
def _url(self, path: str) -> str:
|
|
100
|
+
return f"{self.base_url}{path}"
|
|
101
|
+
|
|
102
|
+
def _request(self, method: str, path: str, **kwargs) -> dict:
|
|
103
|
+
"""Make an HTTP request with retries and error handling."""
|
|
104
|
+
kwargs.setdefault("timeout", self.timeout)
|
|
105
|
+
last_error = None
|
|
106
|
+
|
|
107
|
+
for attempt in range(self.max_retries):
|
|
108
|
+
try:
|
|
109
|
+
resp = self._session.request(method, self._url(path), **kwargs)
|
|
110
|
+
return self._handle_response(resp)
|
|
111
|
+
except (requests.ConnectionError, requests.Timeout) as e:
|
|
112
|
+
last_error = ConnectionError(
|
|
113
|
+
f"Cannot connect to Nerqon at {self.base_url}: {e}"
|
|
114
|
+
)
|
|
115
|
+
if attempt < self.max_retries - 1:
|
|
116
|
+
time.sleep(min(2 ** attempt, 8))
|
|
117
|
+
continue
|
|
118
|
+
except NerqonError:
|
|
119
|
+
raise
|
|
120
|
+
except Exception as e:
|
|
121
|
+
raise NerqonError(f"Unexpected error: {e}")
|
|
122
|
+
|
|
123
|
+
raise last_error
|
|
124
|
+
|
|
125
|
+
def _handle_response(self, resp: requests.Response) -> dict:
|
|
126
|
+
"""Parse response and raise appropriate exceptions."""
|
|
127
|
+
if resp.status_code == 204:
|
|
128
|
+
return {}
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
data = resp.json()
|
|
132
|
+
except ValueError:
|
|
133
|
+
data = {"detail": resp.text}
|
|
134
|
+
|
|
135
|
+
if 200 <= resp.status_code < 300:
|
|
136
|
+
return data
|
|
137
|
+
|
|
138
|
+
detail = data.get("detail", data.get("message", str(data)))
|
|
139
|
+
|
|
140
|
+
if resp.status_code in (401, 403):
|
|
141
|
+
raise AuthenticationError(detail, status_code=resp.status_code, response=data)
|
|
142
|
+
elif resp.status_code == 404:
|
|
143
|
+
raise NotFoundError(detail, status_code=404, response=data)
|
|
144
|
+
elif resp.status_code == 422:
|
|
145
|
+
if "dimension" in str(detail).lower():
|
|
146
|
+
raise DimensionMismatchError(detail, status_code=422, response=data)
|
|
147
|
+
raise ValidationError(detail, status_code=422, response=data)
|
|
148
|
+
elif resp.status_code == 429:
|
|
149
|
+
retry_after = int(resp.headers.get("Retry-After", 60))
|
|
150
|
+
raise RateLimitError(
|
|
151
|
+
detail, retry_after=retry_after, status_code=429, response=data
|
|
152
|
+
)
|
|
153
|
+
elif resp.status_code >= 500:
|
|
154
|
+
raise ServerError(detail, status_code=resp.status_code, response=data)
|
|
155
|
+
else:
|
|
156
|
+
raise NerqonError(detail, status_code=resp.status_code, response=data)
|
|
157
|
+
|
|
158
|
+
def _ns(self, namespace: Optional[str]) -> str:
|
|
159
|
+
"""Resolve namespace — use explicit value or fall back to default."""
|
|
160
|
+
return namespace or self.namespace
|
|
161
|
+
|
|
162
|
+
# ──────────────────────────────────────────────
|
|
163
|
+
# Health & Stats
|
|
164
|
+
# ──────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
def health(self) -> HealthStatus:
|
|
167
|
+
"""Check API health. Does not require authentication.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
HealthStatus with server status, version, and uptime.
|
|
171
|
+
"""
|
|
172
|
+
resp = self._session.get(self._url("/health"), timeout=self.timeout)
|
|
173
|
+
data = resp.json() if resp.status_code == 200 else {}
|
|
174
|
+
return HealthStatus.from_dict(data)
|
|
175
|
+
|
|
176
|
+
def stats(self, namespace: str = None) -> Stats:
|
|
177
|
+
"""Get index statistics for a namespace.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
namespace: Target namespace. Uses default if not specified.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Stats object with document counts, dimension, cache hit rate, etc.
|
|
184
|
+
"""
|
|
185
|
+
data = self._request("GET", "/v1/stats", params={"namespace": self._ns(namespace)})
|
|
186
|
+
return Stats.from_dict(data)
|
|
187
|
+
|
|
188
|
+
# ──────────────────────────────────────────────
|
|
189
|
+
# Documents
|
|
190
|
+
# ──────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
def add(
|
|
193
|
+
self,
|
|
194
|
+
id: str,
|
|
195
|
+
vector: List[float],
|
|
196
|
+
text: str = "",
|
|
197
|
+
metadata: Dict[str, Any] = None,
|
|
198
|
+
namespace: str = None,
|
|
199
|
+
) -> Document:
|
|
200
|
+
"""Add a single document.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
id: Unique document identifier.
|
|
204
|
+
vector: Embedding vector (list of floats).
|
|
205
|
+
text: Document text content.
|
|
206
|
+
metadata: Optional key-value metadata.
|
|
207
|
+
namespace: Target namespace. Uses default if not specified.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Document object with the stored document details.
|
|
211
|
+
|
|
212
|
+
Raises:
|
|
213
|
+
DimensionMismatchError: If vector dimension doesn't match namespace config.
|
|
214
|
+
ValidationError: If parameters are invalid.
|
|
215
|
+
"""
|
|
216
|
+
payload = {
|
|
217
|
+
"id": id,
|
|
218
|
+
"vector": vector,
|
|
219
|
+
"text": text,
|
|
220
|
+
"metadata": metadata or {},
|
|
221
|
+
"namespace": self._ns(namespace),
|
|
222
|
+
}
|
|
223
|
+
data = self._request("POST", "/v1/documents", json=payload)
|
|
224
|
+
return Document.from_dict(data) if isinstance(data, dict) else Document(id=id)
|
|
225
|
+
|
|
226
|
+
def add_batch(
|
|
227
|
+
self,
|
|
228
|
+
documents: List[Dict[str, Any]],
|
|
229
|
+
namespace: str = None,
|
|
230
|
+
) -> dict:
|
|
231
|
+
"""Add multiple documents in a single request.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
documents: List of document dicts, each with keys:
|
|
235
|
+
``id``, ``vector``, ``text`` (optional), ``metadata`` (optional).
|
|
236
|
+
namespace: Target namespace for all documents.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
dict with batch operation results (e.g. ``{"added": 10}``).
|
|
240
|
+
|
|
241
|
+
Example::
|
|
242
|
+
|
|
243
|
+
docs = [
|
|
244
|
+
{"id": "d1", "vector": [0.1, ...], "text": "First doc"},
|
|
245
|
+
{"id": "d2", "vector": [0.2, ...], "text": "Second doc"},
|
|
246
|
+
]
|
|
247
|
+
result = client.add_batch(docs)
|
|
248
|
+
"""
|
|
249
|
+
payload = {
|
|
250
|
+
"documents": documents,
|
|
251
|
+
"namespace": self._ns(namespace),
|
|
252
|
+
}
|
|
253
|
+
return self._request("POST", "/v1/documents/batch", json=payload)
|
|
254
|
+
|
|
255
|
+
def get(self, id: str, namespace: str = None) -> Document:
|
|
256
|
+
"""Retrieve a document by ID.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
id: Document identifier.
|
|
260
|
+
namespace: Target namespace.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Document object.
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
NotFoundError: If document doesn't exist.
|
|
267
|
+
"""
|
|
268
|
+
data = self._request(
|
|
269
|
+
"GET", f"/documents/{id}",
|
|
270
|
+
params={"namespace": self._ns(namespace)},
|
|
271
|
+
)
|
|
272
|
+
return Document.from_dict(data)
|
|
273
|
+
|
|
274
|
+
def update(
|
|
275
|
+
self,
|
|
276
|
+
id: str,
|
|
277
|
+
text: str = None,
|
|
278
|
+
vector: List[float] = None,
|
|
279
|
+
metadata: Dict[str, Any] = None,
|
|
280
|
+
namespace: str = None,
|
|
281
|
+
) -> Document:
|
|
282
|
+
"""Update an existing document.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
id: Document identifier.
|
|
286
|
+
text: New text content (optional).
|
|
287
|
+
vector: New embedding vector (optional).
|
|
288
|
+
metadata: New metadata (optional).
|
|
289
|
+
namespace: Target namespace.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Updated Document object.
|
|
293
|
+
"""
|
|
294
|
+
payload = {"namespace": self._ns(namespace)}
|
|
295
|
+
if text is not None:
|
|
296
|
+
payload["text"] = text
|
|
297
|
+
if vector is not None:
|
|
298
|
+
payload["vector"] = vector
|
|
299
|
+
if metadata is not None:
|
|
300
|
+
payload["metadata"] = metadata
|
|
301
|
+
|
|
302
|
+
data = self._request("PATCH", f"/documents/{id}", json=payload)
|
|
303
|
+
return Document.from_dict(data) if isinstance(data, dict) else Document(id=id)
|
|
304
|
+
|
|
305
|
+
def delete(self, id: str, namespace: str = None) -> dict:
|
|
306
|
+
"""Delete a document by ID.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
id: Document identifier.
|
|
310
|
+
namespace: Target namespace.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
dict with deletion confirmation.
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
NotFoundError: If document doesn't exist.
|
|
317
|
+
"""
|
|
318
|
+
return self._request(
|
|
319
|
+
"DELETE", f"/documents/{id}",
|
|
320
|
+
params={"namespace": self._ns(namespace)},
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# ──────────────────────────────────────────────
|
|
324
|
+
# Search
|
|
325
|
+
# ──────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
def search(
|
|
328
|
+
self,
|
|
329
|
+
vector: List[float] = None,
|
|
330
|
+
text: str = None,
|
|
331
|
+
top_k: int = 10,
|
|
332
|
+
filters: Dict[str, Any] = None,
|
|
333
|
+
namespace: str = None,
|
|
334
|
+
mode: str = None,
|
|
335
|
+
use_graph: bool = None,
|
|
336
|
+
use_cache: bool = None,
|
|
337
|
+
) -> SearchResponse:
|
|
338
|
+
"""Search for similar documents.
|
|
339
|
+
|
|
340
|
+
Supports vector search, text search, and hybrid search modes.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
vector: Query embedding vector.
|
|
344
|
+
text: Query text (if server-side embeddings are enabled).
|
|
345
|
+
top_k: Number of results to return (default: 10).
|
|
346
|
+
filters: Metadata filters using Query DSL.
|
|
347
|
+
namespace: Target namespace.
|
|
348
|
+
mode: Search mode — ``"vector"``, ``"hybrid"``, ``"graph"``.
|
|
349
|
+
use_graph: Enable graph traversal for richer results.
|
|
350
|
+
use_cache: Enable result caching.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
SearchResponse with results, scores, and query time.
|
|
354
|
+
|
|
355
|
+
Example::
|
|
356
|
+
|
|
357
|
+
# Vector search
|
|
358
|
+
results = client.search(vector=[0.1, 0.2, ...], top_k=5)
|
|
359
|
+
|
|
360
|
+
# With metadata filters
|
|
361
|
+
results = client.search(
|
|
362
|
+
vector=[0.1, ...],
|
|
363
|
+
filters={"category": "science", "year": {"$gte": 2024}},
|
|
364
|
+
)
|
|
365
|
+
"""
|
|
366
|
+
payload = {
|
|
367
|
+
"top_k": top_k,
|
|
368
|
+
"namespace": self._ns(namespace),
|
|
369
|
+
}
|
|
370
|
+
if vector is not None:
|
|
371
|
+
payload["vector"] = vector
|
|
372
|
+
if text is not None:
|
|
373
|
+
payload["text"] = text
|
|
374
|
+
if filters:
|
|
375
|
+
payload["filters"] = filters
|
|
376
|
+
if mode:
|
|
377
|
+
payload["mode"] = mode
|
|
378
|
+
if use_graph is not None:
|
|
379
|
+
payload["use_graph"] = use_graph
|
|
380
|
+
if use_cache is not None:
|
|
381
|
+
payload["use_cache"] = use_cache
|
|
382
|
+
|
|
383
|
+
data = self._request("POST", "/v1/search", json=payload)
|
|
384
|
+
return SearchResponse.from_dict(data)
|
|
385
|
+
|
|
386
|
+
# ──────────────────────────────────────────────
|
|
387
|
+
# Namespaces
|
|
388
|
+
# ──────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
def list_namespaces(self) -> List[Namespace]:
|
|
391
|
+
"""List all namespaces.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
List of Namespace objects.
|
|
395
|
+
"""
|
|
396
|
+
data = self._request("GET", "/namespaces")
|
|
397
|
+
namespaces = data if isinstance(data, list) else data.get("namespaces", [])
|
|
398
|
+
return [Namespace.from_dict(ns) if isinstance(ns, dict) else Namespace(name=str(ns)) for ns in namespaces]
|
|
399
|
+
|
|
400
|
+
def create_namespace(self, name: str, dimension: int = None) -> dict:
|
|
401
|
+
"""Create a new namespace.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
name: Namespace name (alphanumeric + hyphens).
|
|
405
|
+
dimension: Embedding dimension for this namespace (optional).
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
dict with creation confirmation.
|
|
409
|
+
"""
|
|
410
|
+
payload = {"name": name}
|
|
411
|
+
if dimension:
|
|
412
|
+
payload["dimension"] = dimension
|
|
413
|
+
return self._request("POST", "/namespaces", json=payload)
|
|
414
|
+
|
|
415
|
+
def delete_namespace(self, name: str) -> dict:
|
|
416
|
+
"""Delete a namespace and all its data. This is irreversible.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
name: Namespace to delete.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
dict with deletion confirmation.
|
|
423
|
+
"""
|
|
424
|
+
return self._request("DELETE", f"/namespaces/{name}")
|
|
425
|
+
|
|
426
|
+
# ──────────────────────────────────────────────
|
|
427
|
+
# User Info
|
|
428
|
+
# ──────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
def me(self) -> dict:
|
|
431
|
+
"""Get current user information and usage stats.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
dict with user profile, plan, and usage data.
|
|
435
|
+
"""
|
|
436
|
+
return self._request("GET", "/v1/me")
|
|
437
|
+
|
|
438
|
+
# ──────────────────────────────────────────────
|
|
439
|
+
# Context Manager
|
|
440
|
+
# ──────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
def close(self):
|
|
443
|
+
"""Close the HTTP session."""
|
|
444
|
+
self._session.close()
|
|
445
|
+
|
|
446
|
+
def __enter__(self):
|
|
447
|
+
return self
|
|
448
|
+
|
|
449
|
+
def __exit__(self, *args):
|
|
450
|
+
self.close()
|
|
451
|
+
|
|
452
|
+
def __repr__(self):
|
|
453
|
+
return f"Nerqon(base_url={self.base_url!r}, namespace={self.namespace!r})"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Nerqon SDK — Exception classes
|
|
3
|
+
===================================
|
|
4
|
+
|
|
5
|
+
These exceptions are raised by the client when API calls fail.
|
|
6
|
+
They contain structured error information from the Nerqon API.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NerqonError(Exception):
|
|
11
|
+
"""Base exception for all Nerqon SDK errors."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, message: str, status_code: int = None, response: dict = None):
|
|
14
|
+
self.message = message
|
|
15
|
+
self.status_code = status_code
|
|
16
|
+
self.response = response or {}
|
|
17
|
+
super().__init__(self.message)
|
|
18
|
+
|
|
19
|
+
def __repr__(self):
|
|
20
|
+
return f"{self.__class__.__name__}(message={self.message!r}, status_code={self.status_code})"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuthenticationError(NerqonError):
|
|
24
|
+
"""Raised when API key is invalid or missing (HTTP 401/403)."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RateLimitError(NerqonError):
|
|
29
|
+
"""Raised when rate limit is exceeded (HTTP 429)."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, message: str, retry_after: int = None, **kwargs):
|
|
32
|
+
super().__init__(message, **kwargs)
|
|
33
|
+
self.retry_after = retry_after
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NotFoundError(NerqonError):
|
|
37
|
+
"""Raised when a resource is not found (HTTP 404)."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ValidationError(NerqonError):
|
|
42
|
+
"""Raised when request parameters are invalid (HTTP 422)."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DimensionMismatchError(ValidationError):
|
|
47
|
+
"""Raised when embedding dimensions don't match the namespace configuration."""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ServerError(NerqonError):
|
|
52
|
+
"""Raised when the Nerqon server returns a 5xx error."""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ConnectionError(NerqonError):
|
|
57
|
+
"""Raised when the SDK cannot connect to the Nerqon API."""
|
|
58
|
+
pass
|