rushdb 2.6.0__tar.gz → 2.10.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.
- {rushdb-2.6.0 → rushdb-2.10.0}/PKG-INFO +54 -1
- {rushdb-2.6.0 → rushdb-2.10.0}/README.md +53 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/pyproject.toml +1 -1
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/__init__.py +6 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/ai.py +59 -26
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/records.py +40 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/transactions.py +2 -1
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/client.py +14 -3
- rushdb-2.10.0/src/rushdb/common.py +33 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/models/__init__.py +6 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/models/result.py +16 -2
- rushdb-2.10.0/src/rushdb/models/search_query.py +93 -0
- rushdb-2.10.0/tests/test_ai_vector_search.py +117 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/tests/test_create_import.py +9 -3
- {rushdb-2.6.0 → rushdb-2.10.0}/tests/test_search_query.py +224 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/uv.lock +49 -49
- rushdb-2.6.0/src/rushdb/common.py +0 -20
- rushdb-2.6.0/src/rushdb/models/search_query.py +0 -37
- {rushdb-2.6.0 → rushdb-2.10.0}/.env.example +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/.github/workflows/ci.yml +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/.gitignore +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/CODE_OF_CONDUCT.md +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/CONTRIBUTING.md +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/LICENSE +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/rushdb-logo.svg +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/__init__.py +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/__init__.py +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/base.py +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/labels.py +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/properties.py +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/query.py +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/relationship_patterns.py +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/relationships.py +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/models/api_response.py +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/models/property.py +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/models/record.py +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/models/relationship.py +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/models/transaction.py +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/utils/token_prefix.py +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/tests/__init__.py +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/tests/test_base_setup.py +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/tests/test_relationship_patterns.py +0 -0
- {rushdb-2.6.0 → rushdb-2.10.0}/tests/test_search_result.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rushdb
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.10.0
|
|
4
4
|
Summary: RushDB Python SDK — memory layer for AI agents and modern apps
|
|
5
5
|
Project-URL: Homepage, https://github.com/rush-db/rushdb-python
|
|
6
6
|
Project-URL: Repository, https://github.com/rush-db/rushdb-python
|
|
@@ -135,6 +135,28 @@ authored_posts = db.records.find({
|
|
|
135
135
|
'limit': 10,
|
|
136
136
|
})
|
|
137
137
|
|
|
138
|
+
# Multi-hop: add hops to $relation — everyone in Alice's reporting chain, up to 4 levels
|
|
139
|
+
chain = db.records.find({
|
|
140
|
+
'labels': ['EMPLOYEE'],
|
|
141
|
+
'where': {
|
|
142
|
+
'EMPLOYEE': {
|
|
143
|
+
'$relation': {'type': 'REPORTS_TO', 'direction': 'out', 'hops': {'min': 1, 'max': 4}},
|
|
144
|
+
'name': {'$contains': 'Alice'},
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
# Cycle detection: accounts on a circular transfer ring (fraud rings, circular ownership)
|
|
150
|
+
ring_members = db.records.find({
|
|
151
|
+
'labels': ['ACCOUNT'],
|
|
152
|
+
'where': {
|
|
153
|
+
'RING': { # key is a display name — the $cycle block holds only $relation
|
|
154
|
+
'$cycle': True,
|
|
155
|
+
'$relation': {'type': 'TRANSFERRED_TO', 'direction': 'out', 'hops': {'min': 2, 'max': 6}},
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
|
|
138
160
|
# Manage relationships explicitly
|
|
139
161
|
user = db.records.find_uniq({'labels': ['USER'], 'where': {'name': 'Alice'}})
|
|
140
162
|
company = db.records.find_uniq({'labels': ['COMPANY'], 'where': {'name': 'Acme Corp'}})
|
|
@@ -205,6 +227,37 @@ if result:
|
|
|
205
227
|
|
|
206
228
|
---
|
|
207
229
|
|
|
230
|
+
## Vector Search And Smart Search
|
|
231
|
+
|
|
232
|
+
Use `db.records.vector_search()` for direct semantic/vector retrieval over an
|
|
233
|
+
embedding index:
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
results = db.records.vector_search({
|
|
237
|
+
'labels': ['MEMORY'],
|
|
238
|
+
'propertyName': 'content',
|
|
239
|
+
'query': 'how agents remember things',
|
|
240
|
+
'where': {'agent_id': 'agent-42'},
|
|
241
|
+
'limit': 5,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
for record in results:
|
|
245
|
+
print(record.score, record.get('content'))
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Use `db.ai.search()` when you want RushDB to turn a natural-language request
|
|
249
|
+
into a SearchQuery and execute it:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
results = db.ai.search('Find active memories about Q4 results for agent-42')
|
|
253
|
+
print(results.search_query)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`db.ai.search({...})` still works as a deprecated vector-search alias, but new
|
|
257
|
+
code should use `db.records.vector_search({...})`.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
208
261
|
## Record API
|
|
209
262
|
|
|
210
263
|
```python
|
|
@@ -110,6 +110,28 @@ authored_posts = db.records.find({
|
|
|
110
110
|
'limit': 10,
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
+
# Multi-hop: add hops to $relation — everyone in Alice's reporting chain, up to 4 levels
|
|
114
|
+
chain = db.records.find({
|
|
115
|
+
'labels': ['EMPLOYEE'],
|
|
116
|
+
'where': {
|
|
117
|
+
'EMPLOYEE': {
|
|
118
|
+
'$relation': {'type': 'REPORTS_TO', 'direction': 'out', 'hops': {'min': 1, 'max': 4}},
|
|
119
|
+
'name': {'$contains': 'Alice'},
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
# Cycle detection: accounts on a circular transfer ring (fraud rings, circular ownership)
|
|
125
|
+
ring_members = db.records.find({
|
|
126
|
+
'labels': ['ACCOUNT'],
|
|
127
|
+
'where': {
|
|
128
|
+
'RING': { # key is a display name — the $cycle block holds only $relation
|
|
129
|
+
'$cycle': True,
|
|
130
|
+
'$relation': {'type': 'TRANSFERRED_TO', 'direction': 'out', 'hops': {'min': 2, 'max': 6}},
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
|
|
113
135
|
# Manage relationships explicitly
|
|
114
136
|
user = db.records.find_uniq({'labels': ['USER'], 'where': {'name': 'Alice'}})
|
|
115
137
|
company = db.records.find_uniq({'labels': ['COMPANY'], 'where': {'name': 'Acme Corp'}})
|
|
@@ -180,6 +202,37 @@ if result:
|
|
|
180
202
|
|
|
181
203
|
---
|
|
182
204
|
|
|
205
|
+
## Vector Search And Smart Search
|
|
206
|
+
|
|
207
|
+
Use `db.records.vector_search()` for direct semantic/vector retrieval over an
|
|
208
|
+
embedding index:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
results = db.records.vector_search({
|
|
212
|
+
'labels': ['MEMORY'],
|
|
213
|
+
'propertyName': 'content',
|
|
214
|
+
'query': 'how agents remember things',
|
|
215
|
+
'where': {'agent_id': 'agent-42'},
|
|
216
|
+
'limit': 5,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
for record in results:
|
|
220
|
+
print(record.score, record.get('content'))
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Use `db.ai.search()` when you want RushDB to turn a natural-language request
|
|
224
|
+
into a SearchQuery and execute it:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
results = db.ai.search('Find active memories about Q4 results for agent-42')
|
|
228
|
+
print(results.search_query)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`db.ai.search({...})` still works as a deprecated vector-search alias, but new
|
|
232
|
+
code should use `db.records.vector_search({...})`.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
183
236
|
## Record API
|
|
184
237
|
|
|
185
238
|
```python
|
|
@@ -24,6 +24,9 @@ from .models.search_query import (
|
|
|
24
24
|
RelationshipEndpointQuery,
|
|
25
25
|
RelationshipSearchQuery,
|
|
26
26
|
SearchQuery,
|
|
27
|
+
TraversalHops,
|
|
28
|
+
TraversalHopsRange,
|
|
29
|
+
TraversalRelationOptions,
|
|
27
30
|
)
|
|
28
31
|
from .models.transaction import Transaction
|
|
29
32
|
|
|
@@ -46,6 +49,9 @@ __all__ = [
|
|
|
46
49
|
"SearchQuery",
|
|
47
50
|
"RelationshipEndpointQuery",
|
|
48
51
|
"RelationshipSearchQuery",
|
|
52
|
+
"TraversalHops",
|
|
53
|
+
"TraversalHopsRange",
|
|
54
|
+
"TraversalRelationOptions",
|
|
49
55
|
"QueryAPI",
|
|
50
56
|
"RelationsAPI",
|
|
51
57
|
"RelationshipPatternsAPI",
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"""AI API for RushDB Python SDK.
|
|
2
2
|
|
|
3
|
-
Provides methods for graph
|
|
3
|
+
Provides methods for AI-assisted graph exploration, smart search,
|
|
4
4
|
and embedding index management.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import warnings
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Union, cast
|
|
8
9
|
|
|
9
10
|
from ..models.api_response import ApiResponse
|
|
10
11
|
from ..models.record import Record
|
|
12
|
+
from ..models.result import RecordSearchResult
|
|
13
|
+
from ..models.search_query import SearchQuery
|
|
11
14
|
from ..models.transaction import Transaction
|
|
12
15
|
from .base import BaseAPI
|
|
13
16
|
|
|
@@ -124,7 +127,12 @@ class AIAPI(BaseAPI):
|
|
|
124
127
|
|
|
125
128
|
>>> db = RushDB(api_key="...")
|
|
126
129
|
>>> schema = db.ai.get_schema()
|
|
127
|
-
>>> results = db.ai.search(
|
|
130
|
+
>>> results = db.ai.search("books about fast cars")
|
|
131
|
+
>>> vectors = db.records.vector_search({
|
|
132
|
+
... "query": "fast cars",
|
|
133
|
+
... "propertyName": "description",
|
|
134
|
+
... "labels": ["Book"],
|
|
135
|
+
... })
|
|
128
136
|
>>> db.ai.indexes.create({"propertyName": "description", "label": "Book"})
|
|
129
137
|
"""
|
|
130
138
|
|
|
@@ -145,7 +153,7 @@ class AIAPI(BaseAPI):
|
|
|
145
153
|
indexes exist for that property — each entry exposes ``id``,
|
|
146
154
|
``sourceType``, ``similarityFunction``, ``dimensions``, ``status``, and
|
|
147
155
|
``modelKey``. A non-empty ``vectorIndexes`` list means the property is
|
|
148
|
-
queryable with ``db.
|
|
156
|
+
queryable with ``db.records.vector_search()``.
|
|
149
157
|
|
|
150
158
|
Args:
|
|
151
159
|
params: Optional filter. Pass ``{"labels": ["Label1"]}`` to scope
|
|
@@ -214,35 +222,60 @@ class AIAPI(BaseAPI):
|
|
|
214
222
|
total=response.get("total"),
|
|
215
223
|
)
|
|
216
224
|
|
|
217
|
-
def search(
|
|
218
|
-
|
|
225
|
+
def search(
|
|
226
|
+
self,
|
|
227
|
+
prompt: Union[str, Dict[str, Any]],
|
|
228
|
+
current_query: Optional[SearchQuery] = None,
|
|
229
|
+
transaction: Optional[Union[Transaction, str]] = None,
|
|
230
|
+
) -> RecordSearchResult:
|
|
231
|
+
"""Perform AI-assisted smart search from natural language.
|
|
219
232
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
233
|
+
RushDB converts ``prompt`` into a SearchQuery using the project schema,
|
|
234
|
+
executes it, and returns matching records with the generated query
|
|
235
|
+
attached to ``result.search_query``.
|
|
223
236
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
Candidates are first narrowed by MATCH/WHERE, then ranked by exact
|
|
227
|
-
cosine similarity.
|
|
237
|
+
Use ``db.records.vector_search({...})`` for direct vector similarity
|
|
238
|
+
over embedding indexes.
|
|
228
239
|
|
|
229
240
|
Args:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
-
|
|
234
|
-
|
|
235
|
-
- ``where`` (dict, optional): Additional property filters.
|
|
236
|
-
- ``limit`` (int, optional): Maximum number of results.
|
|
241
|
+
prompt: Natural-language search request. Passing a dict is
|
|
242
|
+
deprecated and delegates to ``db.records.vector_search``.
|
|
243
|
+
current_query: Optional current SearchQuery context from a
|
|
244
|
+
dashboard/query-builder session.
|
|
245
|
+
transaction: Optional transaction context.
|
|
237
246
|
|
|
238
247
|
Returns:
|
|
239
|
-
|
|
240
|
-
|
|
248
|
+
RecordSearchResult: Matching records. The generated SearchQuery is
|
|
249
|
+
available as ``result.search_query`` and server warnings as
|
|
250
|
+
``result.warnings``.
|
|
241
251
|
"""
|
|
242
|
-
|
|
252
|
+
if isinstance(prompt, dict):
|
|
253
|
+
warnings.warn(
|
|
254
|
+
"db.ai.search({...}) is deprecated for vector search; "
|
|
255
|
+
"use db.records.vector_search({...}) instead.",
|
|
256
|
+
DeprecationWarning,
|
|
257
|
+
stacklevel=2,
|
|
258
|
+
)
|
|
259
|
+
return self.client.records.vector_search(prompt, transaction=transaction)
|
|
260
|
+
|
|
261
|
+
headers = Transaction._build_transaction_header(transaction)
|
|
262
|
+
generated = self.client._make_request(
|
|
263
|
+
"POST",
|
|
264
|
+
"/ai/search-query",
|
|
265
|
+
{"prompt": prompt, "currentQuery": current_query},
|
|
266
|
+
headers,
|
|
267
|
+
)
|
|
268
|
+
generated_data = generated.get("data") or {}
|
|
269
|
+
search_query = generated_data.get("searchQuery") or {}
|
|
270
|
+
|
|
271
|
+
response = self.client._make_request(
|
|
272
|
+
"POST", "/records/search", search_query, headers
|
|
273
|
+
)
|
|
243
274
|
records = [Record(self.client, item) for item in response.get("data", [])]
|
|
244
|
-
return
|
|
275
|
+
return RecordSearchResult(
|
|
245
276
|
data=records,
|
|
246
|
-
|
|
247
|
-
|
|
277
|
+
total=response.get("total", len(records)),
|
|
278
|
+
search_query=cast(SearchQuery, search_query),
|
|
279
|
+
client=self.client,
|
|
280
|
+
warnings=generated_data.get("warnings") or [],
|
|
248
281
|
)
|
|
@@ -688,6 +688,46 @@ class RecordsAPI(BaseAPI):
|
|
|
688
688
|
except Exception:
|
|
689
689
|
return RecordSearchResult(data=[], total=0, client=self.client)
|
|
690
690
|
|
|
691
|
+
def vector_search(
|
|
692
|
+
self,
|
|
693
|
+
params: Dict[str, Any],
|
|
694
|
+
transaction: Optional[Union[Transaction, str]] = None,
|
|
695
|
+
) -> RecordSearchResult:
|
|
696
|
+
"""Perform vector similarity search over indexed record properties.
|
|
697
|
+
|
|
698
|
+
This is the Python SDK counterpart to the TypeScript
|
|
699
|
+
``db.records.vectorSearch({...})`` method. RushDB narrows candidates by
|
|
700
|
+
``labels`` and optional ``where`` filters first, then ranks them by
|
|
701
|
+
vector similarity. Pass ``query`` for managed indexes or ``queryVector``
|
|
702
|
+
for external/custom vectors.
|
|
703
|
+
|
|
704
|
+
Args:
|
|
705
|
+
params: Vector search parameters. Expected keys:
|
|
706
|
+
|
|
707
|
+
- ``propertyName`` (str): Property that has an embedding index.
|
|
708
|
+
- ``labels`` (list[str]): Labels to scope the search.
|
|
709
|
+
- ``query`` (str, optional): Text query for managed indexes.
|
|
710
|
+
- ``queryVector`` (list[float], optional): Pre-computed vector.
|
|
711
|
+
- ``where`` (dict, optional): Additional property filters.
|
|
712
|
+
- ``limit`` (int, optional): Maximum number of results.
|
|
713
|
+
- ``skip`` (int, optional): Number of results to skip.
|
|
714
|
+
- ``topK`` (int, optional): Candidate count in direct vector mode.
|
|
715
|
+
transaction: Optional transaction context.
|
|
716
|
+
|
|
717
|
+
Returns:
|
|
718
|
+
RecordSearchResult: Matching records ranked by similarity. Each
|
|
719
|
+
record may include ``__score``.
|
|
720
|
+
"""
|
|
721
|
+
headers = Transaction._build_transaction_header(transaction)
|
|
722
|
+
response = self.client._make_request("POST", "/ai/search", params, headers)
|
|
723
|
+
records = [Record(self.client, item) for item in response.get("data", [])]
|
|
724
|
+
return RecordSearchResult(
|
|
725
|
+
data=records,
|
|
726
|
+
total=response.get("total", len(records)),
|
|
727
|
+
search_query=typing.cast(SearchQuery, params),
|
|
728
|
+
client=self.client,
|
|
729
|
+
)
|
|
730
|
+
|
|
691
731
|
def find_one(
|
|
692
732
|
self,
|
|
693
733
|
search_query: Optional[SearchQuery] = None,
|
|
@@ -51,7 +51,8 @@ class TransactionsAPI(BaseAPI):
|
|
|
51
51
|
Args:
|
|
52
52
|
ttl (Optional[int], optional): Time-to-live in milliseconds for the transaction.
|
|
53
53
|
After this time, the transaction will automatically expire and be rolled back.
|
|
54
|
-
If None, defaults to 5000ms (5 seconds).
|
|
54
|
+
If None, defaults to 5000ms (5 seconds). Values above the server-side cap
|
|
55
|
+
(60 seconds) are clamped to it. Defaults to None.
|
|
55
56
|
|
|
56
57
|
Returns:
|
|
57
58
|
Transaction: A Transaction object that can be used with other API operations.
|
|
@@ -144,7 +144,9 @@ class RushDB:
|
|
|
144
144
|
else None
|
|
145
145
|
)
|
|
146
146
|
effective_url = url or base_url or self.DEFAULT_BASE_URL
|
|
147
|
-
|
|
147
|
+
# Both `url` and its deprecated alias `base_url` are documented to get
|
|
148
|
+
# `/api/v1` appended when the path is absent.
|
|
149
|
+
if (url or base_url) and "/api/" not in effective_url:
|
|
148
150
|
effective_url = effective_url.rstrip("/") + "/api/v1"
|
|
149
151
|
self.base_url = effective_url.rstrip("/")
|
|
150
152
|
self.api_key = api_key
|
|
@@ -245,8 +247,17 @@ class RushDB:
|
|
|
245
247
|
with urllib.request.urlopen(request) as response:
|
|
246
248
|
return json.loads(response.read().decode("utf-8"))
|
|
247
249
|
except urllib.error.HTTPError as e:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
+
# Error bodies are usually JSON, but a gateway/proxy may return HTML or an
|
|
251
|
+
# empty body — without this guard the JSONDecodeError raised here would
|
|
252
|
+
# escape as-is instead of surfacing as a RushDBError.
|
|
253
|
+
try:
|
|
254
|
+
error_body = json.loads(e.read().decode("utf-8"))
|
|
255
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
256
|
+
error_body = {}
|
|
257
|
+
message = error_body.get("message")
|
|
258
|
+
if isinstance(message, list):
|
|
259
|
+
message = "; ".join(str(item) for item in message)
|
|
260
|
+
raise RushDBError(message or f"HTTP {e.code}", error_body, status=e.code)
|
|
250
261
|
except urllib.error.URLError as e:
|
|
251
262
|
raise RushDBError(f"Connection error: {str(e)}")
|
|
252
263
|
except json.JSONDecodeError as e:
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Dict, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RushDBError(Exception):
|
|
5
|
+
"""Custom exception for RushDB client errors.
|
|
6
|
+
|
|
7
|
+
Attributes:
|
|
8
|
+
details: Parsed error response body from the server, if any.
|
|
9
|
+
status: HTTP status code for server errors (e.g. 403 for a write attempt
|
|
10
|
+
with a read-only API key, 408 for a server-side transaction timeout).
|
|
11
|
+
``None`` for client-side errors such as connection failures.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
message: str,
|
|
17
|
+
details: Optional[Dict] = None,
|
|
18
|
+
status: Optional[int] = None,
|
|
19
|
+
):
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.details = details or {}
|
|
22
|
+
self.status = status
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NonUniqueResultError(Exception):
|
|
26
|
+
"""Raised by ``records.find_uniq()`` when more than one record matches the query."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, count: int):
|
|
29
|
+
super().__init__(
|
|
30
|
+
f"Expected at most one record but found {count}. "
|
|
31
|
+
"Refine your search query or use records.find() instead."
|
|
32
|
+
)
|
|
33
|
+
self.count = count
|
|
@@ -11,6 +11,9 @@ from .search_query import (
|
|
|
11
11
|
RelationshipEndpointQuery,
|
|
12
12
|
RelationshipSearchQuery,
|
|
13
13
|
SearchQuery,
|
|
14
|
+
TraversalHops,
|
|
15
|
+
TraversalHopsRange,
|
|
16
|
+
TraversalRelationOptions,
|
|
14
17
|
)
|
|
15
18
|
from .transaction import Transaction
|
|
16
19
|
|
|
@@ -26,5 +29,8 @@ __all__ = [
|
|
|
26
29
|
"SearchQuery",
|
|
27
30
|
"RelationshipEndpointQuery",
|
|
28
31
|
"RelationshipSearchQuery",
|
|
32
|
+
"TraversalHops",
|
|
33
|
+
"TraversalHopsRange",
|
|
34
|
+
"TraversalRelationOptions",
|
|
29
35
|
"Transaction",
|
|
30
36
|
]
|
|
@@ -30,6 +30,7 @@ class SearchResult(Generic[T]):
|
|
|
30
30
|
total: Optional[int] = None,
|
|
31
31
|
search_query: Optional[SearchQuery] = None,
|
|
32
32
|
client: Optional["RushDB"] = None,
|
|
33
|
+
warnings: Optional[List[str]] = None,
|
|
33
34
|
):
|
|
34
35
|
"""
|
|
35
36
|
Initialize search result.
|
|
@@ -39,11 +40,13 @@ class SearchResult(Generic[T]):
|
|
|
39
40
|
total: Total number of matching records (may be larger than len(data))
|
|
40
41
|
search_query: The search query used to generate this result
|
|
41
42
|
client: Optional RushDB client instance (required for delete_all, next, set_properties)
|
|
43
|
+
warnings: Optional warnings returned by AI-assisted query generation
|
|
42
44
|
"""
|
|
43
45
|
self._data = data
|
|
44
46
|
self._total = total or len(data)
|
|
45
47
|
self._search_query = search_query or {}
|
|
46
48
|
self._client = client
|
|
49
|
+
self._warnings = warnings or []
|
|
47
50
|
|
|
48
51
|
@property
|
|
49
52
|
def data(self) -> List[T]:
|
|
@@ -60,6 +63,11 @@ class SearchResult(Generic[T]):
|
|
|
60
63
|
"""Get the search query used to generate this result."""
|
|
61
64
|
return self._search_query
|
|
62
65
|
|
|
66
|
+
@property
|
|
67
|
+
def warnings(self) -> List[str]:
|
|
68
|
+
"""Get warnings returned by AI-assisted query generation."""
|
|
69
|
+
return self._warnings
|
|
70
|
+
|
|
63
71
|
@property
|
|
64
72
|
def has_more(self) -> bool:
|
|
65
73
|
"""Check if there are more records available beyond this result set."""
|
|
@@ -106,6 +114,7 @@ class SearchResult(Generic[T]):
|
|
|
106
114
|
"total": self.total,
|
|
107
115
|
"data": self.data,
|
|
108
116
|
"search_query": self.search_query,
|
|
117
|
+
"warnings": self.warnings,
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
def get_page_info(self) -> dict:
|
|
@@ -246,5 +255,10 @@ class SearchResult(Generic[T]):
|
|
|
246
255
|
return pd.DataFrame(rows)
|
|
247
256
|
|
|
248
257
|
|
|
249
|
-
|
|
250
|
-
|
|
258
|
+
class RecordSearchResult(SearchResult[Record]):
|
|
259
|
+
"""Search result specialized for ``Record`` items.
|
|
260
|
+
|
|
261
|
+
A real subclass (not a ``SearchResult[Record]`` alias) so that
|
|
262
|
+
``isinstance(result, RecordSearchResult)`` works — isinstance checks against
|
|
263
|
+
subscripted generics raise ``TypeError`` at runtime.
|
|
264
|
+
"""
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any, Dict, List, Optional, TypedDict, Union
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class OrderDirection(str, Enum):
|
|
6
|
+
ASC = "asc"
|
|
7
|
+
DESC = "desc"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TraversalHopsRange(TypedDict, total=False):
|
|
11
|
+
"""Hop range for variable-length traversal.
|
|
12
|
+
|
|
13
|
+
``min`` defaults to 1 (2 inside a ``$cycle`` block). Omitting ``max``
|
|
14
|
+
requests unbounded traversal, which is only allowed on self-hosted
|
|
15
|
+
deployments and projects with a custom Neo4j instance; the shared cloud
|
|
16
|
+
connection caps ``max`` per deployment (default 25).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
min: int
|
|
20
|
+
max: int
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
TraversalHops = Union[int, TraversalHopsRange]
|
|
24
|
+
"""Depth of a variable-length traversal: an exact hop count or a range."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TraversalRelationOptions(TypedDict, total=False):
|
|
28
|
+
"""Value for the ``$relation`` key of a traversal block in ``where``.
|
|
29
|
+
|
|
30
|
+
``type`` and ``direction`` constrain every hop; ``hops`` makes the
|
|
31
|
+
traversal variable-length. The nested label constrains only the final
|
|
32
|
+
record — intermediate records are anonymous.
|
|
33
|
+
|
|
34
|
+
Example::
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
"labels": ["EMPLOYEE"],
|
|
38
|
+
"where": {
|
|
39
|
+
"EMPLOYEE": {
|
|
40
|
+
"$alias": "$manager",
|
|
41
|
+
"$relation": {
|
|
42
|
+
"type": "REPORTS_TO",
|
|
43
|
+
"direction": "out",
|
|
44
|
+
"hops": {"min": 1, "max": 4},
|
|
45
|
+
},
|
|
46
|
+
"name": {"$contains": "Alice"},
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
type: str
|
|
53
|
+
direction: str # 'in' | 'out'
|
|
54
|
+
hops: TraversalHops
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SearchQuery(TypedDict, total=False):
|
|
58
|
+
"""TypedDict representing the query structure for finding records.
|
|
59
|
+
|
|
60
|
+
Inside ``where``, a nested key that is a label name traverses to related
|
|
61
|
+
records. Traversal blocks accept ``$alias`` (name the endpoint for
|
|
62
|
+
``select``/``groupBy``), ``$relation`` (a type string or
|
|
63
|
+
:class:`TraversalRelationOptions`, including variable-length ``hops``),
|
|
64
|
+
and ``$cycle`` (``True`` binds the traversal back to the parent record to
|
|
65
|
+
detect rings; requires ``$relation`` with ``hops`` where ``min`` >= 2 and
|
|
66
|
+
accepts no other keys).
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
where: Optional[Dict[str, Any]]
|
|
70
|
+
labels: Optional[List[str]]
|
|
71
|
+
skip: Optional[int]
|
|
72
|
+
limit: Optional[int]
|
|
73
|
+
orderBy: Optional[Union[Dict[str, OrderDirection], OrderDirection]]
|
|
74
|
+
select: Optional[Dict[str, Any]]
|
|
75
|
+
groupBy: Optional[List[str]]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class RelationshipEndpointQuery(TypedDict, total=False):
|
|
79
|
+
"""Endpoint record predicates for relationship search."""
|
|
80
|
+
|
|
81
|
+
where: Optional[Dict[str, Any]]
|
|
82
|
+
labels: Optional[List[str]]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class RelationshipSearchQuery(TypedDict, total=False):
|
|
86
|
+
"""TypedDict representing the query structure for finding relationship edges."""
|
|
87
|
+
|
|
88
|
+
where: Optional[Dict[str, Any]]
|
|
89
|
+
source: Optional[RelationshipEndpointQuery]
|
|
90
|
+
target: Optional[RelationshipEndpointQuery]
|
|
91
|
+
skip: Optional[int]
|
|
92
|
+
limit: Optional[int]
|
|
93
|
+
orderBy: Optional[Union[Dict[str, OrderDirection], OrderDirection]]
|