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.
Files changed (43) hide show
  1. {rushdb-2.6.0 → rushdb-2.10.0}/PKG-INFO +54 -1
  2. {rushdb-2.6.0 → rushdb-2.10.0}/README.md +53 -0
  3. {rushdb-2.6.0 → rushdb-2.10.0}/pyproject.toml +1 -1
  4. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/__init__.py +6 -0
  5. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/ai.py +59 -26
  6. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/records.py +40 -0
  7. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/transactions.py +2 -1
  8. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/client.py +14 -3
  9. rushdb-2.10.0/src/rushdb/common.py +33 -0
  10. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/models/__init__.py +6 -0
  11. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/models/result.py +16 -2
  12. rushdb-2.10.0/src/rushdb/models/search_query.py +93 -0
  13. rushdb-2.10.0/tests/test_ai_vector_search.py +117 -0
  14. {rushdb-2.6.0 → rushdb-2.10.0}/tests/test_create_import.py +9 -3
  15. {rushdb-2.6.0 → rushdb-2.10.0}/tests/test_search_query.py +224 -0
  16. {rushdb-2.6.0 → rushdb-2.10.0}/uv.lock +49 -49
  17. rushdb-2.6.0/src/rushdb/common.py +0 -20
  18. rushdb-2.6.0/src/rushdb/models/search_query.py +0 -37
  19. {rushdb-2.6.0 → rushdb-2.10.0}/.env.example +0 -0
  20. {rushdb-2.6.0 → rushdb-2.10.0}/.github/workflows/ci.yml +0 -0
  21. {rushdb-2.6.0 → rushdb-2.10.0}/.gitignore +0 -0
  22. {rushdb-2.6.0 → rushdb-2.10.0}/CODE_OF_CONDUCT.md +0 -0
  23. {rushdb-2.6.0 → rushdb-2.10.0}/CONTRIBUTING.md +0 -0
  24. {rushdb-2.6.0 → rushdb-2.10.0}/LICENSE +0 -0
  25. {rushdb-2.6.0 → rushdb-2.10.0}/rushdb-logo.svg +0 -0
  26. {rushdb-2.6.0 → rushdb-2.10.0}/src/__init__.py +0 -0
  27. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/__init__.py +0 -0
  28. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/base.py +0 -0
  29. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/labels.py +0 -0
  30. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/properties.py +0 -0
  31. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/query.py +0 -0
  32. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/relationship_patterns.py +0 -0
  33. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/api/relationships.py +0 -0
  34. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/models/api_response.py +0 -0
  35. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/models/property.py +0 -0
  36. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/models/record.py +0 -0
  37. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/models/relationship.py +0 -0
  38. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/models/transaction.py +0 -0
  39. {rushdb-2.6.0 → rushdb-2.10.0}/src/rushdb/utils/token_prefix.py +0 -0
  40. {rushdb-2.6.0 → rushdb-2.10.0}/tests/__init__.py +0 -0
  41. {rushdb-2.6.0 → rushdb-2.10.0}/tests/test_base_setup.py +0 -0
  42. {rushdb-2.6.0 → rushdb-2.10.0}/tests/test_relationship_patterns.py +0 -0
  43. {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.6.0
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rushdb"
3
- version = "2.6.0"
3
+ version = "2.10.0"
4
4
  description = "RushDB Python SDK — memory layer for AI agents and modern apps"
5
5
  authors = [
6
6
  {name = "RushDB Team", email = "hi@rushdb.com"}
@@ -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 schema exploration, semantic vector search,
3
+ Provides methods for AI-assisted graph exploration, smart search,
4
4
  and embedding index management.
5
5
  """
6
6
 
7
- from typing import TYPE_CHECKING, Any, Dict, Optional, Union
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({"query": "fast cars", "propertyName": "description"})
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.ai.search()``.
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(self, params: Dict[str, Any]) -> ApiResponse:
218
- """Perform semantic (vector) search over indexed record properties.
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
- **Direct vector-index mode** (default, fast): used when no ``where``
221
- filter and at most one ``labels`` entry. Queries the shared global
222
- vector index directly.
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
- **Prefilter mode** (exact, slower): activated when a ``where``
225
- filter is supplied or ``labels`` contains more than one value.
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
- params: Search parameters. Expected keys:
231
-
232
- - ``query`` (str): The natural-language query text.
233
- - ``propertyName`` (str): Property that has been embedded.
234
- - ``labels`` (list[str], optional): Scope to specific labels.
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
- ApiResponse: Response whose ``data`` is a list of semantic search
240
- result objects (each includes the matched record and a score).
248
+ RecordSearchResult: Matching records. The generated SearchQuery is
249
+ available as ``result.search_query`` and server warnings as
250
+ ``result.warnings``.
241
251
  """
242
- response = self.client._make_request("POST", "/ai/search", params)
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 ApiResponse(
275
+ return RecordSearchResult(
245
276
  data=records,
246
- success=response.get("success", True),
247
- total=response.get("total"),
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). Defaults to None.
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
- if url and "/api/" not in effective_url:
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
- error_body = json.loads(e.read().decode("utf-8"))
249
- raise RushDBError(error_body.get("message", str(e)), error_body)
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
- # Type alias for record search results
250
- RecordSearchResult = SearchResult[Record]
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]]