rushdb 2.0.0__tar.gz → 2.3.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.0.0 → rushdb-2.3.0}/PKG-INFO +12 -2
- {rushdb-2.0.0 → rushdb-2.3.0}/README.md +11 -1
- {rushdb-2.0.0 → rushdb-2.3.0}/pyproject.toml +1 -3
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/__init__.py +23 -2
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/__init__.py +2 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/records.py +93 -81
- rushdb-2.3.0/src/rushdb/api/relationship_patterns.py +61 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/relationships.py +60 -28
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/client.py +2 -2
- rushdb-2.3.0/src/rushdb/models/__init__.py +30 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/models/record.py +13 -16
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/models/relationship.py +4 -1
- rushdb-2.3.0/src/rushdb/models/search_query.py +37 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/tests/test_create_import.py +47 -0
- rushdb-2.3.0/tests/test_relationship_patterns.py +160 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/uv.lock +1 -1
- rushdb-2.0.0/src/rushdb/models/__init__.py +0 -17
- rushdb-2.0.0/src/rushdb/models/search_query.py +0 -19
- {rushdb-2.0.0 → rushdb-2.3.0}/.env.example +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/.github/workflows/ci.yml +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/.gitignore +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/CODE_OF_CONDUCT.md +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/CONTRIBUTING.md +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/LICENSE +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/rushdb-logo.svg +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/src/__init__.py +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/ai.py +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/base.py +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/labels.py +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/properties.py +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/query.py +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/transactions.py +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/common.py +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/models/api_response.py +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/models/property.py +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/models/result.py +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/models/transaction.py +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/utils/token_prefix.py +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/tests/__init__.py +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/tests/test_base_setup.py +0 -0
- {rushdb-2.0.0 → rushdb-2.3.0}/tests/test_search_query.py +0 -0
- {rushdb-2.0.0 → rushdb-2.3.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.3.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
|
|
@@ -138,7 +138,17 @@ authored_posts = db.records.find({
|
|
|
138
138
|
# Manage relationships explicitly
|
|
139
139
|
user = db.records.find_uniq({'labels': ['USER'], 'where': {'name': 'Alice'}})
|
|
140
140
|
company = db.records.find_uniq({'labels': ['COMPANY'], 'where': {'name': 'Acme Corp'}})
|
|
141
|
-
user.attach(
|
|
141
|
+
user.attach(
|
|
142
|
+
target=company,
|
|
143
|
+
options={'type': 'WORKS_AT', 'direction': 'out', 'properties': {'source': 'profile'}},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Relationship search: where filters edge type/properties, source/target filter endpoint records
|
|
147
|
+
relationships = db.relationships.find({
|
|
148
|
+
'source': {'labels': ['USER'], 'where': {'name': 'Alice'}},
|
|
149
|
+
'target': {'labels': ['COMPANY']},
|
|
150
|
+
'where': {'type': 'WORKS_AT', 'source': 'profile'},
|
|
151
|
+
})
|
|
142
152
|
```
|
|
143
153
|
|
|
144
154
|
---
|
|
@@ -113,7 +113,17 @@ authored_posts = db.records.find({
|
|
|
113
113
|
# Manage relationships explicitly
|
|
114
114
|
user = db.records.find_uniq({'labels': ['USER'], 'where': {'name': 'Alice'}})
|
|
115
115
|
company = db.records.find_uniq({'labels': ['COMPANY'], 'where': {'name': 'Acme Corp'}})
|
|
116
|
-
user.attach(
|
|
116
|
+
user.attach(
|
|
117
|
+
target=company,
|
|
118
|
+
options={'type': 'WORKS_AT', 'direction': 'out', 'properties': {'source': 'profile'}},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Relationship search: where filters edge type/properties, source/target filter endpoint records
|
|
122
|
+
relationships = db.relationships.find({
|
|
123
|
+
'source': {'labels': ['USER'], 'where': {'name': 'Alice'}},
|
|
124
|
+
'target': {'labels': ['COMPANY']},
|
|
125
|
+
'where': {'type': 'WORKS_AT', 'source': 'profile'},
|
|
126
|
+
})
|
|
117
127
|
```
|
|
118
128
|
|
|
119
129
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "rushdb"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.3.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"}
|
|
@@ -81,8 +81,6 @@ line-length = 120
|
|
|
81
81
|
|
|
82
82
|
[tool.isort]
|
|
83
83
|
profile = "black"
|
|
84
|
-
multi_line_output = 3
|
|
85
|
-
line_length = 120
|
|
86
84
|
|
|
87
85
|
[dependency-groups]
|
|
88
86
|
dev = [
|
|
@@ -5,14 +5,26 @@ Exposes the RushDB class.
|
|
|
5
5
|
|
|
6
6
|
from .api.ai import AIAPI
|
|
7
7
|
from .api.query import QueryAPI
|
|
8
|
+
from .api.records import pick_record_id
|
|
9
|
+
from .api.relationship_patterns import RelationshipPatternsAPI
|
|
8
10
|
from .api.relationships import RelationsAPI
|
|
9
11
|
from .client import RushDB
|
|
10
12
|
from .common import NonUniqueResultError, RushDBError
|
|
11
13
|
from .models.api_response import ApiResponse
|
|
12
14
|
from .models.property import Property
|
|
13
|
-
from .models.record import Record
|
|
14
|
-
from .models.relationship import
|
|
15
|
+
from .models.record import Record, RecordTarget, RelationTarget
|
|
16
|
+
from .models.relationship import (
|
|
17
|
+
Relationship,
|
|
18
|
+
RelationshipDetachOptions,
|
|
19
|
+
RelationshipDirection,
|
|
20
|
+
RelationshipOptions,
|
|
21
|
+
)
|
|
15
22
|
from .models.result import RecordSearchResult, SearchResult
|
|
23
|
+
from .models.search_query import (
|
|
24
|
+
RelationshipEndpointQuery,
|
|
25
|
+
RelationshipSearchQuery,
|
|
26
|
+
SearchQuery,
|
|
27
|
+
)
|
|
16
28
|
from .models.transaction import Transaction
|
|
17
29
|
|
|
18
30
|
__all__ = [
|
|
@@ -20,14 +32,23 @@ __all__ = [
|
|
|
20
32
|
"RushDBError",
|
|
21
33
|
"NonUniqueResultError",
|
|
22
34
|
"Record",
|
|
35
|
+
"RecordTarget",
|
|
36
|
+
"RelationTarget",
|
|
37
|
+
"pick_record_id",
|
|
23
38
|
"RecordSearchResult",
|
|
24
39
|
"SearchResult",
|
|
25
40
|
"Transaction",
|
|
26
41
|
"Property",
|
|
42
|
+
"Relationship",
|
|
43
|
+
"RelationshipDirection",
|
|
27
44
|
"RelationshipOptions",
|
|
28
45
|
"RelationshipDetachOptions",
|
|
46
|
+
"SearchQuery",
|
|
47
|
+
"RelationshipEndpointQuery",
|
|
48
|
+
"RelationshipSearchQuery",
|
|
29
49
|
"QueryAPI",
|
|
30
50
|
"RelationsAPI",
|
|
51
|
+
"RelationshipPatternsAPI",
|
|
31
52
|
"ApiResponse",
|
|
32
53
|
"AIAPI",
|
|
33
54
|
]
|
|
@@ -2,6 +2,7 @@ from .labels import LabelsAPI
|
|
|
2
2
|
from .properties import PropertiesAPI
|
|
3
3
|
from .query import QueryAPI
|
|
4
4
|
from .records import RecordsAPI
|
|
5
|
+
from .relationship_patterns import RelationshipPatternsAPI
|
|
5
6
|
from .relationships import RelationsAPI
|
|
6
7
|
from .transactions import TransactionsAPI
|
|
7
8
|
|
|
@@ -12,4 +13,5 @@ __all__ = [
|
|
|
12
13
|
"TransactionsAPI",
|
|
13
14
|
"QueryAPI",
|
|
14
15
|
"RelationsAPI",
|
|
16
|
+
"RelationshipPatternsAPI",
|
|
15
17
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
from typing import Any, Dict, List, Optional, Union
|
|
3
3
|
|
|
4
|
-
from ..models.record import Record
|
|
4
|
+
from ..models.record import Record, RecordTarget, RelationTarget
|
|
5
5
|
from ..models.relationship import RelationshipDetachOptions, RelationshipOptions
|
|
6
6
|
from ..models.result import RecordSearchResult
|
|
7
7
|
from ..models.search_query import SearchQuery
|
|
@@ -9,6 +9,29 @@ from ..models.transaction import Transaction
|
|
|
9
9
|
from .base import BaseAPI
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
def pick_record_id(target: RecordTarget) -> Optional[str]:
|
|
13
|
+
"""Extract a record ID string from any RecordTarget representation.
|
|
14
|
+
|
|
15
|
+
Mirrors the JavaScript SDK's ``pickRecordId`` utility in ``src/api/utils.ts``.
|
|
16
|
+
Accepts a UUID string, a ``Record`` instance, or a plain dict containing
|
|
17
|
+
an ``__id`` field — in all cases the resolved UUID string is returned.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
target: A record reference as a string ID, ``Record`` object, or dict
|
|
21
|
+
with an ``"__id"`` key.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The UUID string, or ``None`` if it cannot be resolved.
|
|
25
|
+
"""
|
|
26
|
+
if isinstance(target, str):
|
|
27
|
+
return target
|
|
28
|
+
elif isinstance(target, Record):
|
|
29
|
+
return target.data.get("__id")
|
|
30
|
+
elif isinstance(target, dict):
|
|
31
|
+
return target.get("__id")
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
12
35
|
def _is_flat(obj: Any) -> bool:
|
|
13
36
|
"""Return True if obj is a dict with no nested dict/list values."""
|
|
14
37
|
return isinstance(obj, dict) and not any(
|
|
@@ -55,7 +78,7 @@ class RecordsAPI(BaseAPI):
|
|
|
55
78
|
|
|
56
79
|
def set(
|
|
57
80
|
self,
|
|
58
|
-
|
|
81
|
+
target: RecordTarget,
|
|
59
82
|
data: Dict[str, Any],
|
|
60
83
|
label: Optional[str] = None,
|
|
61
84
|
vectors: Optional[List[Dict[str, Any]]] = None,
|
|
@@ -67,7 +90,8 @@ class RecordsAPI(BaseAPI):
|
|
|
67
90
|
removing any existing fields that are not present in the new data.
|
|
68
91
|
|
|
69
92
|
Args:
|
|
70
|
-
|
|
93
|
+
target (RecordTarget): The record to update. Accepts a UUID string,
|
|
94
|
+
a ``Record`` instance, or a dict containing an ``"__id"`` field.
|
|
71
95
|
data (Dict[str, Any]): The new data to replace the existing record data.
|
|
72
96
|
label (Optional[str]): Optional label to re-assign to the record.
|
|
73
97
|
vectors (Optional[List[Dict[str, Any]]]): Optional pre-computed embedding vectors
|
|
@@ -81,9 +105,10 @@ class RecordsAPI(BaseAPI):
|
|
|
81
105
|
Dict[str, str]: Response from the server containing operation status.
|
|
82
106
|
|
|
83
107
|
Raises:
|
|
84
|
-
ValueError: If the
|
|
108
|
+
ValueError: If the target cannot be resolved to a valid record ID.
|
|
85
109
|
RequestError: If the server request fails.
|
|
86
110
|
"""
|
|
111
|
+
record_id = pick_record_id(target)
|
|
87
112
|
headers = Transaction._build_transaction_header(transaction)
|
|
88
113
|
payload: Dict[str, Any] = {"data": data}
|
|
89
114
|
if label is not None:
|
|
@@ -96,7 +121,7 @@ class RecordsAPI(BaseAPI):
|
|
|
96
121
|
|
|
97
122
|
def update(
|
|
98
123
|
self,
|
|
99
|
-
|
|
124
|
+
target: RecordTarget,
|
|
100
125
|
data: Dict[str, Any],
|
|
101
126
|
transaction: Optional[Transaction] = None,
|
|
102
127
|
) -> Dict[str, str]:
|
|
@@ -106,7 +131,8 @@ class RecordsAPI(BaseAPI):
|
|
|
106
131
|
with existing record data without removing existing fields.
|
|
107
132
|
|
|
108
133
|
Args:
|
|
109
|
-
|
|
134
|
+
target (RecordTarget): The record to update. Accepts a UUID string,
|
|
135
|
+
a ``Record`` instance, or a dict containing an ``"__id"`` field.
|
|
110
136
|
data (Dict[str, Any]): The data to merge with the existing record data.
|
|
111
137
|
transaction (Optional[Transaction], optional): Transaction context for the operation.
|
|
112
138
|
If provided, the operation will be part of the transaction. Defaults to None.
|
|
@@ -115,9 +141,10 @@ class RecordsAPI(BaseAPI):
|
|
|
115
141
|
Dict[str, str]: Response from the server containing operation status.
|
|
116
142
|
|
|
117
143
|
Raises:
|
|
118
|
-
ValueError: If the
|
|
144
|
+
ValueError: If the target cannot be resolved to a valid record ID.
|
|
119
145
|
RequestError: If the server request fails.
|
|
120
146
|
"""
|
|
147
|
+
record_id = pick_record_id(target)
|
|
121
148
|
headers = Transaction._build_transaction_header(transaction)
|
|
122
149
|
|
|
123
150
|
return self.client._make_request(
|
|
@@ -383,15 +410,8 @@ class RecordsAPI(BaseAPI):
|
|
|
383
410
|
|
|
384
411
|
def attach(
|
|
385
412
|
self,
|
|
386
|
-
source:
|
|
387
|
-
target:
|
|
388
|
-
str,
|
|
389
|
-
List[str],
|
|
390
|
-
Dict[str, Any],
|
|
391
|
-
List[Dict[str, Any]],
|
|
392
|
-
"Record",
|
|
393
|
-
List["Record"],
|
|
394
|
-
],
|
|
413
|
+
source: RecordTarget,
|
|
414
|
+
target: RelationTarget,
|
|
395
415
|
options: Optional[RelationshipOptions] = None,
|
|
396
416
|
transaction: Optional[Transaction] = None,
|
|
397
417
|
) -> Dict[str, str]:
|
|
@@ -402,16 +422,11 @@ class RecordsAPI(BaseAPI):
|
|
|
402
422
|
record dictionaries, or Record objects.
|
|
403
423
|
|
|
404
424
|
Args:
|
|
405
|
-
source (
|
|
406
|
-
|
|
407
|
-
target (
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
- List of record IDs (List[str])
|
|
411
|
-
- Record dictionary with '__id' field
|
|
412
|
-
- List of record dictionaries
|
|
413
|
-
- Record object
|
|
414
|
-
- List of Record objects
|
|
425
|
+
source (RecordTarget): The source record to attach targets to.
|
|
426
|
+
Accepts a UUID string, a ``Record`` instance, or a dict with ``"__id"``.
|
|
427
|
+
target (RelationTarget): The target record(s) to attach to the source.
|
|
428
|
+
Accepts a single or list of: UUID string, ``Record`` instance, or
|
|
429
|
+
dict with ``"__id"``.
|
|
415
430
|
options (Optional[RelationshipOptions], optional): Additional options for the relationship.
|
|
416
431
|
Defaults to None.
|
|
417
432
|
transaction (Optional[Transaction], optional): Transaction context for the operation.
|
|
@@ -431,6 +446,17 @@ class RecordsAPI(BaseAPI):
|
|
|
431
446
|
>>>
|
|
432
447
|
>>> # Attach using Record objects
|
|
433
448
|
>>> response = records_api.attach(source_record, target_records)
|
|
449
|
+
>>>
|
|
450
|
+
>>> # Attach with a typed relationship carrying edge properties
|
|
451
|
+
>>> response = records_api.attach(
|
|
452
|
+
... movie,
|
|
453
|
+
... actor,
|
|
454
|
+
... options={
|
|
455
|
+
... "type": "STARS_IN",
|
|
456
|
+
... "direction": "out",
|
|
457
|
+
... "properties": {"role": "lead", "billing": 1},
|
|
458
|
+
... },
|
|
459
|
+
... )
|
|
434
460
|
"""
|
|
435
461
|
headers = Transaction._build_transaction_header(transaction)
|
|
436
462
|
|
|
@@ -445,15 +471,8 @@ class RecordsAPI(BaseAPI):
|
|
|
445
471
|
|
|
446
472
|
def detach(
|
|
447
473
|
self,
|
|
448
|
-
source:
|
|
449
|
-
target:
|
|
450
|
-
str,
|
|
451
|
-
List[str],
|
|
452
|
-
Dict[str, Any],
|
|
453
|
-
List[Dict[str, Any]],
|
|
454
|
-
"Record",
|
|
455
|
-
List["Record"],
|
|
456
|
-
],
|
|
474
|
+
source: RecordTarget,
|
|
475
|
+
target: RelationTarget,
|
|
457
476
|
options: Optional[RelationshipDetachOptions] = None,
|
|
458
477
|
transaction: Optional[Transaction] = None,
|
|
459
478
|
) -> Dict[str, str]:
|
|
@@ -464,16 +483,11 @@ class RecordsAPI(BaseAPI):
|
|
|
464
483
|
record dictionaries, or Record objects.
|
|
465
484
|
|
|
466
485
|
Args:
|
|
467
|
-
source (
|
|
468
|
-
|
|
469
|
-
target (
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
- List of record IDs (List[str])
|
|
473
|
-
- Record dictionary with '__id' field
|
|
474
|
-
- List of record dictionaries
|
|
475
|
-
- Record object
|
|
476
|
-
- List of Record objects
|
|
486
|
+
source (RecordTarget): The source record to detach targets from.
|
|
487
|
+
Accepts a UUID string, a ``Record`` instance, or a dict with ``"__id"``.
|
|
488
|
+
target (RelationTarget): The target record(s) to detach from the source.
|
|
489
|
+
Accepts a single or list of: UUID string, ``Record`` instance, or
|
|
490
|
+
dict with ``"__id"``.
|
|
477
491
|
options (Optional[RelationshipDetachOptions], optional): Additional options for the detach operation.
|
|
478
492
|
Defaults to None.
|
|
479
493
|
transaction (Optional[Transaction], optional): Transaction context for the operation.
|
|
@@ -550,7 +564,7 @@ class RecordsAPI(BaseAPI):
|
|
|
550
564
|
|
|
551
565
|
def delete_by_id(
|
|
552
566
|
self,
|
|
553
|
-
|
|
567
|
+
target: Union[RecordTarget, List[RecordTarget]],
|
|
554
568
|
transaction: Optional[Transaction] = None,
|
|
555
569
|
) -> Dict[str, str]:
|
|
556
570
|
"""Delete one or more records by their unique identifiers.
|
|
@@ -559,8 +573,9 @@ class RecordsAPI(BaseAPI):
|
|
|
559
573
|
deletion and bulk deletion of multiple records.
|
|
560
574
|
|
|
561
575
|
Args:
|
|
562
|
-
|
|
563
|
-
|
|
576
|
+
target (Union[RecordTarget, List[RecordTarget]]): The record(s) to delete.
|
|
577
|
+
Accepts a UUID string, a ``Record`` instance, a dict containing
|
|
578
|
+
``"__id"``, or a list of any of those forms.
|
|
564
579
|
transaction (Optional[Transaction], optional): Transaction context for the operation.
|
|
565
580
|
If provided, the operation will be part of the transaction. Defaults to None.
|
|
566
581
|
|
|
@@ -569,39 +584,42 @@ class RecordsAPI(BaseAPI):
|
|
|
569
584
|
information about the deletion operation.
|
|
570
585
|
|
|
571
586
|
Raises:
|
|
572
|
-
ValueError: If any
|
|
587
|
+
ValueError: If any target cannot be resolved to a valid record ID.
|
|
573
588
|
RequestError: If the server request fails.
|
|
574
589
|
|
|
575
590
|
Note:
|
|
576
|
-
When deleting multiple records (list
|
|
591
|
+
When deleting multiple records (list), the operation uses a
|
|
577
592
|
batch delete with a limit of 1000 records. For single record deletion,
|
|
578
593
|
it uses a direct DELETE request.
|
|
579
594
|
|
|
580
595
|
Example:
|
|
581
596
|
>>> records_api = RecordsAPI(client)
|
|
582
|
-
>>> # Delete a single record
|
|
597
|
+
>>> # Delete a single record — string, Record object, or dict all work
|
|
583
598
|
>>> response = records_api.delete_by_id("record_123")
|
|
599
|
+
>>> response = records_api.delete_by_id(my_record)
|
|
584
600
|
>>>
|
|
585
601
|
>>> # Delete multiple records
|
|
586
|
-
>>>
|
|
587
|
-
>>> response = records_api.delete_by_id(
|
|
602
|
+
>>> response = records_api.delete_by_id(["record_123", "record_456"])
|
|
603
|
+
>>> response = records_api.delete_by_id([record_a, record_b])
|
|
588
604
|
"""
|
|
589
605
|
headers = Transaction._build_transaction_header(transaction)
|
|
590
606
|
|
|
591
|
-
if isinstance(
|
|
607
|
+
if isinstance(target, list):
|
|
608
|
+
ids = self._extract_target_ids(target)
|
|
592
609
|
return self.client._make_request(
|
|
593
610
|
"POST",
|
|
594
611
|
"/records/delete",
|
|
595
|
-
{"limit": 1000, "where": {"$id": {"$in":
|
|
612
|
+
{"limit": 1000, "where": {"$id": {"$in": ids}}},
|
|
596
613
|
headers,
|
|
597
614
|
)
|
|
615
|
+
record_id = pick_record_id(target)
|
|
598
616
|
return self.client._make_request(
|
|
599
|
-
"DELETE", f"/records/{
|
|
617
|
+
"DELETE", f"/records/{record_id}", None, headers
|
|
600
618
|
)
|
|
601
619
|
|
|
602
620
|
def find_by_id(
|
|
603
621
|
self,
|
|
604
|
-
|
|
622
|
+
target: Union[RecordTarget, List[RecordTarget]],
|
|
605
623
|
transaction: Optional[Transaction] = None,
|
|
606
624
|
) -> Union["Record", RecordSearchResult]:
|
|
607
625
|
"""Retrieve one or more records by their unique identifiers.
|
|
@@ -609,17 +627,19 @@ class RecordsAPI(BaseAPI):
|
|
|
609
627
|
Mirrors the TypeScript SDK ``records.findById`` method.
|
|
610
628
|
|
|
611
629
|
Args:
|
|
612
|
-
|
|
630
|
+
target: A single record reference (UUID string, ``Record`` instance, or
|
|
631
|
+
dict with ``"__id"``), or a list of any of those forms.
|
|
613
632
|
transaction: Optional transaction context for the operation.
|
|
614
633
|
|
|
615
634
|
Returns:
|
|
616
|
-
A single :class:`Record` when ``
|
|
617
|
-
:class:`RecordSearchResult` when ``
|
|
635
|
+
A single :class:`Record` when ``target`` is a single reference, or a
|
|
636
|
+
:class:`RecordSearchResult` when ``target`` is a list.
|
|
618
637
|
"""
|
|
619
638
|
headers = Transaction._build_transaction_header(transaction)
|
|
620
|
-
if isinstance(
|
|
639
|
+
if isinstance(target, list):
|
|
640
|
+
ids = self._extract_target_ids(target)
|
|
621
641
|
response = self.client._make_request(
|
|
622
|
-
"POST", "/records", {"ids":
|
|
642
|
+
"POST", "/records", {"ids": ids}, headers
|
|
623
643
|
)
|
|
624
644
|
records = [Record(self.client, r) for r in (response.get("data") or [])]
|
|
625
645
|
return RecordSearchResult(
|
|
@@ -627,21 +647,25 @@ class RecordsAPI(BaseAPI):
|
|
|
627
647
|
total=response.get("total", len(records)),
|
|
628
648
|
client=self.client,
|
|
629
649
|
)
|
|
650
|
+
record_id = pick_record_id(target)
|
|
630
651
|
response = self.client._make_request(
|
|
631
|
-
"GET", f"/records/{
|
|
652
|
+
"GET", f"/records/{record_id}", None, headers
|
|
632
653
|
)
|
|
633
654
|
return Record(self.client, response.get("data", response))
|
|
634
655
|
|
|
635
656
|
def find(
|
|
636
657
|
self,
|
|
637
658
|
search_query: Optional[SearchQuery] = None,
|
|
638
|
-
record_id: Optional[
|
|
659
|
+
record_id: Optional[RecordTarget] = None,
|
|
639
660
|
transaction: Optional[Transaction] = None,
|
|
640
661
|
) -> RecordSearchResult:
|
|
641
662
|
try:
|
|
642
663
|
headers = Transaction._build_transaction_header(transaction)
|
|
643
664
|
|
|
644
|
-
|
|
665
|
+
resolved_id = pick_record_id(record_id) if record_id is not None else None
|
|
666
|
+
path = (
|
|
667
|
+
f"/records/{resolved_id}/search" if resolved_id else "/records/search"
|
|
668
|
+
)
|
|
645
669
|
response = self.client._make_request(
|
|
646
670
|
"POST",
|
|
647
671
|
path,
|
|
@@ -825,14 +849,7 @@ class RecordsAPI(BaseAPI):
|
|
|
825
849
|
|
|
826
850
|
@staticmethod
|
|
827
851
|
def _extract_target_ids(
|
|
828
|
-
target: Union[
|
|
829
|
-
str,
|
|
830
|
-
List[str],
|
|
831
|
-
Dict[str, Any],
|
|
832
|
-
List[Dict[str, Any]],
|
|
833
|
-
"Record",
|
|
834
|
-
List["Record"],
|
|
835
|
-
],
|
|
852
|
+
target: Union[RecordTarget, List[RecordTarget]],
|
|
836
853
|
) -> List[str]:
|
|
837
854
|
"""Extract record IDs from various input types and formats.
|
|
838
855
|
|
|
@@ -841,14 +858,9 @@ class RecordsAPI(BaseAPI):
|
|
|
841
858
|
types commonly used throughout the API for specifying target records.
|
|
842
859
|
|
|
843
860
|
Args:
|
|
844
|
-
target (Union[
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
- List[str]: List of record IDs
|
|
848
|
-
- Dict[str, Any]: Record dictionary containing '__id' field
|
|
849
|
-
- List[Dict[str, Any]]: List of record dictionaries with '__id' fields
|
|
850
|
-
- Record: Record object with data containing '__id'
|
|
851
|
-
- List[Record]: List of Record objects
|
|
861
|
+
target (Union[RecordTarget, List[RecordTarget]]): The target input to
|
|
862
|
+
extract IDs from. Accepts a UUID string, a ``Record`` instance, a
|
|
863
|
+
dict with ``"__id"``, or a list of any of those forms.
|
|
852
864
|
|
|
853
865
|
Returns:
|
|
854
866
|
List[str]: List of extracted record ID strings.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Relationship pattern suggestions API for the RushDB Python SDK."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Dict
|
|
4
|
+
|
|
5
|
+
from ..models.api_response import ApiResponse
|
|
6
|
+
from .base import BaseAPI
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..client import RushDB
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RelationshipPatternsAPI(BaseAPI):
|
|
13
|
+
"""Review and manage relationship patterns inferred from project ontology.
|
|
14
|
+
|
|
15
|
+
Accessed via ``db.relationships.patterns``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, client: "RushDB"):
|
|
19
|
+
super().__init__(client)
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def _wrap(response: Dict[str, Any]) -> ApiResponse:
|
|
23
|
+
return ApiResponse(
|
|
24
|
+
data=response.get("data"),
|
|
25
|
+
success=response.get("success", True),
|
|
26
|
+
total=response.get("total"),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def list(self) -> ApiResponse:
|
|
30
|
+
"""List inferred patterns, ontology relationships, and analysis status."""
|
|
31
|
+
response = self.client._make_request("GET", "/relationships/patterns")
|
|
32
|
+
return self._wrap(response)
|
|
33
|
+
|
|
34
|
+
def analyze(self) -> ApiResponse:
|
|
35
|
+
"""Queue ontology analysis to generate relationship pattern suggestions."""
|
|
36
|
+
response = self.client._make_request(
|
|
37
|
+
"POST", "/relationships/patterns/analyze", {}
|
|
38
|
+
)
|
|
39
|
+
return self._wrap(response)
|
|
40
|
+
|
|
41
|
+
def approve(self, pattern_id: str) -> ApiResponse:
|
|
42
|
+
"""Approve and apply a suggested relationship pattern."""
|
|
43
|
+
response = self.client._make_request(
|
|
44
|
+
"POST", f"/relationships/patterns/{pattern_id}/approve", {}
|
|
45
|
+
)
|
|
46
|
+
return self._wrap(response)
|
|
47
|
+
|
|
48
|
+
def ignore(self, pattern_id: str) -> ApiResponse:
|
|
49
|
+
"""Ignore a suggested relationship pattern without applying it."""
|
|
50
|
+
response = self.client._make_request(
|
|
51
|
+
"POST", f"/relationships/patterns/{pattern_id}/ignore", {}
|
|
52
|
+
)
|
|
53
|
+
return self._wrap(response)
|
|
54
|
+
|
|
55
|
+
def delete(self, pattern_id: str, *, delete_existing: bool = False) -> ApiResponse:
|
|
56
|
+
"""Delete a saved pattern and optionally its materialized relationships."""
|
|
57
|
+
params = {"deleteExisting": "true"} if delete_existing else None
|
|
58
|
+
response = self.client._make_request(
|
|
59
|
+
"DELETE", f"/relationships/patterns/{pattern_id}", params=params
|
|
60
|
+
)
|
|
61
|
+
return self._wrap(response)
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import typing
|
|
2
|
-
from typing import
|
|
3
|
-
from urllib.parse import urlencode
|
|
2
|
+
from typing import TYPE_CHECKING, Optional, TypedDict, Union
|
|
4
3
|
|
|
5
4
|
from ..models.relationship import Relationship
|
|
6
|
-
from ..models.
|
|
5
|
+
from ..models.result import SearchResult
|
|
6
|
+
from ..models.search_query import RelationshipSearchQuery
|
|
7
7
|
from ..models.transaction import Transaction
|
|
8
8
|
from .base import BaseAPI
|
|
9
|
+
from .relationship_patterns import RelationshipPatternsAPI
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..client import RushDB
|
|
9
13
|
|
|
10
14
|
|
|
11
15
|
class PaginationParams(TypedDict, total=False):
|
|
@@ -43,59 +47,78 @@ class RelationsAPI(BaseAPI):
|
|
|
43
47
|
Attributes:
|
|
44
48
|
client: The underlying RushDB client instance for making HTTP requests.
|
|
45
49
|
|
|
46
|
-
Note:
|
|
47
|
-
This API currently contains async methods. Ensure you're using it in an
|
|
48
|
-
async context or consider updating to sync methods if needed.
|
|
49
|
-
|
|
50
50
|
Example:
|
|
51
51
|
>>> from rushdb import RushDB
|
|
52
52
|
>>> client = RushDB(api_key="your_api_key")
|
|
53
53
|
>>> relations_api = client.relationships
|
|
54
54
|
>>>
|
|
55
55
|
>>> # Find all relationships
|
|
56
|
-
>>> relationships =
|
|
56
|
+
>>> relationships = relations_api.find()
|
|
57
|
+
>>>
|
|
58
|
+
>>> # Filter by edge type and properties
|
|
59
|
+
>>> leads = relations_api.find({"where": {"type": "STARS_IN", "role": "lead"}})
|
|
57
60
|
>>>
|
|
58
61
|
>>> # Find relationships with pagination
|
|
59
|
-
>>>
|
|
60
|
-
>>> page_1 = await relations_api.find(pagination=pagination)
|
|
62
|
+
>>> page_1 = relations_api.find(pagination={"limit": 50, "skip": 0})
|
|
61
63
|
"""
|
|
62
64
|
|
|
65
|
+
def __init__(self, client: "RushDB"):
|
|
66
|
+
super().__init__(client)
|
|
67
|
+
self.patterns = RelationshipPatternsAPI(client)
|
|
68
|
+
|
|
63
69
|
def find(
|
|
64
70
|
self,
|
|
65
|
-
search_query: Optional[
|
|
71
|
+
search_query: Optional[RelationshipSearchQuery] = None,
|
|
66
72
|
pagination: Optional[PaginationParams] = None,
|
|
67
73
|
transaction: Optional[Union[Transaction, str]] = None,
|
|
68
|
-
) ->
|
|
74
|
+
) -> SearchResult[Relationship]:
|
|
69
75
|
"""Search for and retrieve relationships matching the specified criteria.
|
|
70
76
|
|
|
71
77
|
Args:
|
|
72
|
-
search_query:
|
|
78
|
+
search_query: Relationship-edge criteria. ``where`` filters the edge —
|
|
79
|
+
``type`` maps to the relationship type, ``direction`` constrains
|
|
80
|
+
direction, and every other key matches a user-defined edge property
|
|
81
|
+
(full operator set: ``$gte``, ``$contains``, ``$in``, ``$exists``,
|
|
82
|
+
logical grouping, ...). Use top-level ``source`` and ``target``
|
|
83
|
+
(``{"labels": [...], "where": {...}}``) to filter endpoint records.
|
|
84
|
+
``limit``/``skip`` inside the query control pagination.
|
|
73
85
|
If None, returns all relationships (subject to pagination limits).
|
|
74
|
-
pagination: Pagination options (``limit`` and ``skip``).
|
|
86
|
+
pagination: Pagination options (``limit`` and ``skip``). Merged into the
|
|
87
|
+
search query body, overriding any ``limit``/``skip`` already inside
|
|
88
|
+
``search_query`` — useful for paginating a stored query object
|
|
89
|
+
without mutating it.
|
|
75
90
|
transaction: Optional transaction context.
|
|
76
91
|
|
|
77
92
|
Returns:
|
|
78
|
-
|
|
93
|
+
SearchResult[Relationship]: Matching relationships with ``total`` count.
|
|
94
|
+
Iterable and indexable like a list; each item includes ``sourceId``,
|
|
95
|
+
``sourceLabel``, ``targetId``, ``targetLabel``, ``type``,
|
|
96
|
+
``direction``, and user-defined edge ``properties``.
|
|
79
97
|
|
|
80
98
|
Raises:
|
|
81
99
|
RushDBError: If the server request fails.
|
|
82
100
|
|
|
83
101
|
Example:
|
|
84
102
|
>>> relations_api = RelationsAPI(client)
|
|
85
|
-
>>>
|
|
86
|
-
|
|
103
|
+
>>> result = relations_api.find({
|
|
104
|
+
... "source": {"labels": ["MOVIE"], "where": {"title": "Inception"}},
|
|
105
|
+
... "target": {"labels": ["ACTOR"]},
|
|
106
|
+
... "where": {"type": "STARS_IN", "role": "lead"},
|
|
107
|
+
... "limit": 50,
|
|
108
|
+
... })
|
|
109
|
+
>>> result.total
|
|
110
|
+
1
|
|
111
|
+
>>> result[0]["properties"]["role"]
|
|
112
|
+
'lead'
|
|
87
113
|
"""
|
|
88
|
-
#
|
|
89
|
-
|
|
114
|
+
# Pagination lives in the body, same as records search. The `pagination`
|
|
115
|
+
# argument overrides limit/skip inside `search_query` without mutating it.
|
|
116
|
+
payload: typing.Dict[str, typing.Any] = dict(search_query or {})
|
|
90
117
|
if pagination:
|
|
91
118
|
if pagination.get("limit") is not None:
|
|
92
|
-
|
|
119
|
+
payload["limit"] = pagination["limit"]
|
|
93
120
|
if pagination.get("skip") is not None:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# Construct path with query string
|
|
97
|
-
query_string = f"?{urlencode(query_params)}" if query_params else ""
|
|
98
|
-
path = f"/relationships/search{query_string}"
|
|
121
|
+
payload["skip"] = pagination["skip"]
|
|
99
122
|
|
|
100
123
|
# Build headers with transaction if present
|
|
101
124
|
headers = Transaction._build_transaction_header(transaction)
|
|
@@ -103,12 +126,17 @@ class RelationsAPI(BaseAPI):
|
|
|
103
126
|
# Make request
|
|
104
127
|
response = self.client._make_request(
|
|
105
128
|
method="POST",
|
|
106
|
-
path=
|
|
107
|
-
data=
|
|
129
|
+
path="/relationships/search",
|
|
130
|
+
data=payload,
|
|
108
131
|
headers=headers,
|
|
109
132
|
)
|
|
110
133
|
|
|
111
|
-
|
|
134
|
+
data = response.get("data", [])
|
|
135
|
+
return SearchResult(
|
|
136
|
+
data=data,
|
|
137
|
+
total=response.get("total"),
|
|
138
|
+
search_query=typing.cast(typing.Any, payload),
|
|
139
|
+
)
|
|
112
140
|
|
|
113
141
|
def create_many(
|
|
114
142
|
self,
|
|
@@ -117,6 +145,7 @@ class RelationsAPI(BaseAPI):
|
|
|
117
145
|
target: dict,
|
|
118
146
|
type: Optional[str] = None,
|
|
119
147
|
direction: Optional[str] = None,
|
|
148
|
+
properties: Optional[dict] = None,
|
|
120
149
|
many_to_many: Optional[bool] = None,
|
|
121
150
|
transaction: Optional[Union[Transaction, str]] = None,
|
|
122
151
|
) -> dict:
|
|
@@ -131,6 +160,7 @@ class RelationsAPI(BaseAPI):
|
|
|
131
160
|
target (dict): { label: str, key?: str, where?: dict }
|
|
132
161
|
type (str, optional): Relationship type override.
|
|
133
162
|
direction (str, optional): 'in' | 'out'. Defaults to 'out' server-side when omitted.
|
|
163
|
+
properties (dict, optional): Shared user-defined properties to store on created edges.
|
|
134
164
|
many_to_many (bool, optional): Enable cartesian mode (requires filters, disallows keys).
|
|
135
165
|
transaction (Transaction|str, optional): Transaction context.
|
|
136
166
|
|
|
@@ -143,6 +173,8 @@ class RelationsAPI(BaseAPI):
|
|
|
143
173
|
payload["type"] = type
|
|
144
174
|
if direction:
|
|
145
175
|
payload["direction"] = direction
|
|
176
|
+
if properties is not None:
|
|
177
|
+
payload["properties"] = properties
|
|
146
178
|
if many_to_many is not None:
|
|
147
179
|
payload["manyToMany"] = many_to_many
|
|
148
180
|
return self.client._make_request(
|
|
@@ -34,7 +34,7 @@ class _SettingsNamespace:
|
|
|
34
34
|
Returns:
|
|
35
35
|
Dict[str, Any]: Current project settings object.
|
|
36
36
|
"""
|
|
37
|
-
response = self._client._make_request("GET", "/settings")
|
|
37
|
+
response = self._client._make_request("GET", "/sdk/settings")
|
|
38
38
|
return response.get("data", response)
|
|
39
39
|
|
|
40
40
|
|
|
@@ -288,7 +288,7 @@ class RushDB:
|
|
|
288
288
|
... return client
|
|
289
289
|
"""
|
|
290
290
|
try:
|
|
291
|
-
self._make_request("GET", "/settings")
|
|
291
|
+
self._make_request("GET", "/sdk/settings")
|
|
292
292
|
return True
|
|
293
293
|
except RushDBError:
|
|
294
294
|
return False
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from .property import Property
|
|
2
|
+
from .record import Record
|
|
3
|
+
from .relationship import (
|
|
4
|
+
Relationship,
|
|
5
|
+
RelationshipDetachOptions,
|
|
6
|
+
RelationshipDirection,
|
|
7
|
+
RelationshipOptions,
|
|
8
|
+
)
|
|
9
|
+
from .result import RecordSearchResult, SearchResult
|
|
10
|
+
from .search_query import (
|
|
11
|
+
RelationshipEndpointQuery,
|
|
12
|
+
RelationshipSearchQuery,
|
|
13
|
+
SearchQuery,
|
|
14
|
+
)
|
|
15
|
+
from .transaction import Transaction
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Property",
|
|
19
|
+
"Record",
|
|
20
|
+
"Relationship",
|
|
21
|
+
"RelationshipDetachOptions",
|
|
22
|
+
"RelationshipDirection",
|
|
23
|
+
"RelationshipOptions",
|
|
24
|
+
"RecordSearchResult",
|
|
25
|
+
"SearchResult",
|
|
26
|
+
"SearchQuery",
|
|
27
|
+
"RelationshipEndpointQuery",
|
|
28
|
+
"RelationshipSearchQuery",
|
|
29
|
+
"Transaction",
|
|
30
|
+
]
|
|
@@ -7,6 +7,17 @@ from .transaction import Transaction
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
8
|
from ..client import RushDB
|
|
9
9
|
|
|
10
|
+
# RecordTarget mirrors the JS SDK's DBRecordTarget type:
|
|
11
|
+
# a record can be identified by a UUID string, a dict containing "__id", or a Record instance.
|
|
12
|
+
RecordTarget = Union[str, Dict[str, Any], "Record"]
|
|
13
|
+
|
|
14
|
+
# RelationTarget mirrors the JS SDK's RelationTarget type:
|
|
15
|
+
# one or more records used as the target in attach/detach operations.
|
|
16
|
+
RelationTarget = Union[
|
|
17
|
+
"RecordTarget",
|
|
18
|
+
List["RecordTarget"],
|
|
19
|
+
]
|
|
20
|
+
|
|
10
21
|
|
|
11
22
|
class Record:
|
|
12
23
|
"""Represents a record in RushDB with methods for manipulation."""
|
|
@@ -74,14 +85,7 @@ class Record:
|
|
|
74
85
|
|
|
75
86
|
def attach(
|
|
76
87
|
self,
|
|
77
|
-
target:
|
|
78
|
-
str,
|
|
79
|
-
List[str],
|
|
80
|
-
Dict[str, Any],
|
|
81
|
-
List[Dict[str, Any]],
|
|
82
|
-
"Record",
|
|
83
|
-
List["Record"],
|
|
84
|
-
],
|
|
88
|
+
target: "RelationTarget",
|
|
85
89
|
options: Optional[RelationshipOptions] = None,
|
|
86
90
|
transaction: Optional[Transaction] = None,
|
|
87
91
|
) -> Dict[str, str]:
|
|
@@ -90,14 +94,7 @@ class Record:
|
|
|
90
94
|
|
|
91
95
|
def detach(
|
|
92
96
|
self,
|
|
93
|
-
target:
|
|
94
|
-
str,
|
|
95
|
-
List[str],
|
|
96
|
-
Dict[str, Any],
|
|
97
|
-
List[Dict[str, Any]],
|
|
98
|
-
"Record",
|
|
99
|
-
List["Record"],
|
|
100
|
-
],
|
|
97
|
+
target: "RelationTarget",
|
|
101
98
|
options: Optional[RelationshipDetachOptions] = None,
|
|
102
99
|
transaction: Optional[Transaction] = None,
|
|
103
100
|
) -> Dict[str, str]:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List, Literal, Optional, TypedDict, Union
|
|
1
|
+
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
|
|
2
2
|
|
|
3
3
|
RelationshipDirection = Literal["in", "out"]
|
|
4
4
|
|
|
@@ -7,8 +7,10 @@ class Relationship(TypedDict, total=False):
|
|
|
7
7
|
targetLabel: str
|
|
8
8
|
targetId: str
|
|
9
9
|
type: str
|
|
10
|
+
direction: RelationshipDirection
|
|
10
11
|
sourceId: str
|
|
11
12
|
sourceLabel: str
|
|
13
|
+
properties: Dict[str, Any]
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class RelationshipOptions(TypedDict, total=False):
|
|
@@ -16,6 +18,7 @@ class RelationshipOptions(TypedDict, total=False):
|
|
|
16
18
|
|
|
17
19
|
direction: Optional[RelationshipDirection]
|
|
18
20
|
type: Optional[str]
|
|
21
|
+
properties: Optional[Dict[str, Any]]
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
class RelationshipDetachOptions(TypedDict, total=False):
|
|
@@ -0,0 +1,37 @@
|
|
|
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 SearchQuery(TypedDict, total=False):
|
|
11
|
+
"""TypedDict representing the query structure for finding records."""
|
|
12
|
+
|
|
13
|
+
where: Optional[Dict[str, Any]]
|
|
14
|
+
labels: Optional[List[str]]
|
|
15
|
+
skip: Optional[int]
|
|
16
|
+
limit: Optional[int]
|
|
17
|
+
orderBy: Optional[Union[Dict[str, OrderDirection], OrderDirection]]
|
|
18
|
+
select: Optional[Dict[str, Any]]
|
|
19
|
+
groupBy: Optional[List[str]]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RelationshipEndpointQuery(TypedDict, total=False):
|
|
23
|
+
"""Endpoint record predicates for relationship search."""
|
|
24
|
+
|
|
25
|
+
where: Optional[Dict[str, Any]]
|
|
26
|
+
labels: Optional[List[str]]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RelationshipSearchQuery(TypedDict, total=False):
|
|
30
|
+
"""TypedDict representing the query structure for finding relationship edges."""
|
|
31
|
+
|
|
32
|
+
where: Optional[Dict[str, Any]]
|
|
33
|
+
source: Optional[RelationshipEndpointQuery]
|
|
34
|
+
target: Optional[RelationshipEndpointQuery]
|
|
35
|
+
skip: Optional[int]
|
|
36
|
+
limit: Optional[int]
|
|
37
|
+
orderBy: Optional[Union[Dict[str, OrderDirection], OrderDirection]]
|
|
@@ -153,6 +153,53 @@ class TestCreateImport(TestBase):
|
|
|
153
153
|
source=project, target=employee, options=detach_options
|
|
154
154
|
)
|
|
155
155
|
|
|
156
|
+
def test_attach_with_edge_properties_and_find(self):
|
|
157
|
+
"""Test attaching with edge properties and searching edges by them"""
|
|
158
|
+
movie = self.client.records.create("MOVIE", {"title": "Inception"})
|
|
159
|
+
actor = self.client.records.create("ACTOR", {"name": "Leonardo DiCaprio"})
|
|
160
|
+
|
|
161
|
+
self.client.records.attach(
|
|
162
|
+
source=movie,
|
|
163
|
+
target=actor,
|
|
164
|
+
options=RelationshipOptions(
|
|
165
|
+
type="STARS_IN",
|
|
166
|
+
direction="out",
|
|
167
|
+
properties={"role": "lead", "billing": 1},
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# where filters the edge (type + edge properties);
|
|
172
|
+
# source/target filter the endpoint records
|
|
173
|
+
result = self.client.relationships.find(
|
|
174
|
+
{
|
|
175
|
+
"source": {"labels": ["MOVIE"], "where": {"title": "Inception"}},
|
|
176
|
+
"target": {"labels": ["ACTOR"]},
|
|
177
|
+
"where": {"type": "STARS_IN", "role": "lead", "billing": {"$lte": 3}},
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
self.assertGreaterEqual(result.total, 1)
|
|
182
|
+
edge = next(
|
|
183
|
+
rel
|
|
184
|
+
for rel in result
|
|
185
|
+
if rel["sourceId"] == movie.id and rel["targetId"] == actor.id
|
|
186
|
+
)
|
|
187
|
+
self.assertEqual(edge["type"], "STARS_IN")
|
|
188
|
+
self.assertEqual(edge["properties"]["role"], "lead")
|
|
189
|
+
self.assertEqual(edge["properties"]["billing"], 1)
|
|
190
|
+
|
|
191
|
+
# Edge-property mismatch excludes the relationship
|
|
192
|
+
no_match = self.client.relationships.find(
|
|
193
|
+
{
|
|
194
|
+
"source": {"labels": ["MOVIE"], "where": {"title": "Inception"}},
|
|
195
|
+
"where": {"type": "STARS_IN", "role": "extra"},
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
self.assertNotIn(
|
|
199
|
+
(movie.id, actor.id),
|
|
200
|
+
[(rel["sourceId"], rel["targetId"]) for rel in no_match],
|
|
201
|
+
)
|
|
202
|
+
|
|
156
203
|
def test_create_with_nested_data(self):
|
|
157
204
|
"""Test creating records with nested data structure"""
|
|
158
205
|
data = {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from unittest.mock import Mock
|
|
2
|
+
|
|
3
|
+
from src.rushdb.api.records import RecordsAPI
|
|
4
|
+
from src.rushdb.api.relationship_patterns import RelationshipPatternsAPI
|
|
5
|
+
from src.rushdb.api.relationships import RelationsAPI
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_relationships_exposes_patterns_namespace():
|
|
9
|
+
client = Mock()
|
|
10
|
+
|
|
11
|
+
relationships = RelationsAPI(client)
|
|
12
|
+
|
|
13
|
+
assert isinstance(relationships.patterns, RelationshipPatternsAPI)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_list_relationship_patterns():
|
|
17
|
+
client = Mock()
|
|
18
|
+
client._make_request.return_value = {
|
|
19
|
+
"data": {"patterns": [], "relationships": [], "analysis": {"status": "idle"}},
|
|
20
|
+
"success": True,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
response = RelationsAPI(client).patterns.list()
|
|
24
|
+
|
|
25
|
+
client._make_request.assert_called_once_with("GET", "/relationships/patterns")
|
|
26
|
+
assert response.data["patterns"] == []
|
|
27
|
+
assert response.data["analysis"]["status"] == "idle"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_mutate_relationship_patterns():
|
|
31
|
+
client = Mock()
|
|
32
|
+
client._make_request.return_value = {"data": {"deleted": True}, "success": True}
|
|
33
|
+
patterns = RelationsAPI(client).patterns
|
|
34
|
+
|
|
35
|
+
patterns.analyze()
|
|
36
|
+
client._make_request.assert_called_with(
|
|
37
|
+
"POST", "/relationships/patterns/analyze", {}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
patterns.approve("pattern-id")
|
|
41
|
+
client._make_request.assert_called_with(
|
|
42
|
+
"POST", "/relationships/patterns/pattern-id/approve", {}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
patterns.ignore("pattern-id")
|
|
46
|
+
client._make_request.assert_called_with(
|
|
47
|
+
"POST", "/relationships/patterns/pattern-id/ignore", {}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
patterns.delete("pattern-id", delete_existing=True)
|
|
51
|
+
client._make_request.assert_called_with(
|
|
52
|
+
"DELETE",
|
|
53
|
+
"/relationships/patterns/pattern-id",
|
|
54
|
+
params={"deleteExisting": "true"},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_relationships_find_uses_edge_where_and_endpoint_filters():
|
|
59
|
+
client = Mock()
|
|
60
|
+
edge = {
|
|
61
|
+
"sourceId": "user-1",
|
|
62
|
+
"sourceLabel": "USER",
|
|
63
|
+
"targetId": "order-1",
|
|
64
|
+
"targetLabel": "ORDER",
|
|
65
|
+
"type": "ORDERED",
|
|
66
|
+
"direction": "out",
|
|
67
|
+
"properties": {"confidence": 0.9},
|
|
68
|
+
}
|
|
69
|
+
client._make_request.return_value = {"data": [edge], "total": 1}
|
|
70
|
+
|
|
71
|
+
search_query = {
|
|
72
|
+
"source": {"labels": ["USER"], "where": {"status": "active"}},
|
|
73
|
+
"target": {"labels": ["ORDER"]},
|
|
74
|
+
"where": {"type": "ORDERED", "confidence": {"$gte": 0.8}},
|
|
75
|
+
"limit": 25,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
response = RelationsAPI(client).find(search_query)
|
|
79
|
+
|
|
80
|
+
# Pagination travels in the body, same as records search.
|
|
81
|
+
client._make_request.assert_called_once_with(
|
|
82
|
+
method="POST",
|
|
83
|
+
path="/relationships/search",
|
|
84
|
+
data=search_query,
|
|
85
|
+
headers=None,
|
|
86
|
+
)
|
|
87
|
+
assert response.total == 1
|
|
88
|
+
assert list(response) == [edge]
|
|
89
|
+
assert response[0]["properties"]["confidence"] == 0.9
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_relationships_find_explicit_pagination_wins_over_query_limit():
|
|
93
|
+
client = Mock()
|
|
94
|
+
client._make_request.return_value = {"data": [], "total": 0}
|
|
95
|
+
|
|
96
|
+
search_query = {"where": {"type": "ORDERED"}, "limit": 25}
|
|
97
|
+
RelationsAPI(client).find(search_query, pagination={"limit": 10, "skip": 5})
|
|
98
|
+
|
|
99
|
+
# The pagination argument overrides limit/skip in the body, leaving the
|
|
100
|
+
# caller's search_query object untouched.
|
|
101
|
+
client._make_request.assert_called_once_with(
|
|
102
|
+
method="POST",
|
|
103
|
+
path="/relationships/search",
|
|
104
|
+
data={"where": {"type": "ORDERED"}, "limit": 10, "skip": 5},
|
|
105
|
+
headers=None,
|
|
106
|
+
)
|
|
107
|
+
assert search_query == {"where": {"type": "ORDERED"}, "limit": 25}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_relationships_create_many_forwards_edge_properties():
|
|
111
|
+
client = Mock()
|
|
112
|
+
client._make_request.return_value = {"success": True}
|
|
113
|
+
|
|
114
|
+
RelationsAPI(client).create_many(
|
|
115
|
+
source={"label": "USER", "key": "id"},
|
|
116
|
+
target={"label": "ORDER", "key": "userId"},
|
|
117
|
+
type="ORDERED",
|
|
118
|
+
direction="out",
|
|
119
|
+
properties={"source": "import", "confidence": 0.9},
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
client._make_request.assert_called_once_with(
|
|
123
|
+
"POST",
|
|
124
|
+
"/relationships/create-many",
|
|
125
|
+
{
|
|
126
|
+
"source": {"label": "USER", "key": "id"},
|
|
127
|
+
"target": {"label": "ORDER", "key": "userId"},
|
|
128
|
+
"type": "ORDERED",
|
|
129
|
+
"direction": "out",
|
|
130
|
+
"properties": {"source": "import", "confidence": 0.9},
|
|
131
|
+
},
|
|
132
|
+
None,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_records_attach_forwards_edge_properties():
|
|
137
|
+
client = Mock()
|
|
138
|
+
client._make_request.return_value = {"success": True}
|
|
139
|
+
|
|
140
|
+
RecordsAPI(client).attach(
|
|
141
|
+
source="source-id",
|
|
142
|
+
target="target-id",
|
|
143
|
+
options={
|
|
144
|
+
"type": "RELATED_TO",
|
|
145
|
+
"direction": "out",
|
|
146
|
+
"properties": {"source": "test"},
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
client._make_request.assert_called_once_with(
|
|
151
|
+
"POST",
|
|
152
|
+
"/relationships/source-id",
|
|
153
|
+
{
|
|
154
|
+
"targetIds": ["target-id"],
|
|
155
|
+
"type": "RELATED_TO",
|
|
156
|
+
"direction": "out",
|
|
157
|
+
"properties": {"source": "test"},
|
|
158
|
+
},
|
|
159
|
+
None,
|
|
160
|
+
)
|
|
@@ -785,7 +785,7 @@ wheels = [
|
|
|
785
785
|
|
|
786
786
|
[[package]]
|
|
787
787
|
name = "rushdb"
|
|
788
|
-
version = "2.
|
|
788
|
+
version = "2.3.0"
|
|
789
789
|
source = { editable = "." }
|
|
790
790
|
dependencies = [
|
|
791
791
|
{ name = "python-dotenv", version = "1.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
from .property import Property
|
|
2
|
-
from .record import Record
|
|
3
|
-
from .relationship import RelationshipDetachOptions, RelationshipOptions
|
|
4
|
-
from .result import RecordSearchResult, SearchResult
|
|
5
|
-
from .search_query import SearchQuery
|
|
6
|
-
from .transaction import Transaction
|
|
7
|
-
|
|
8
|
-
__all__ = [
|
|
9
|
-
"Property",
|
|
10
|
-
"Record",
|
|
11
|
-
"RelationshipDetachOptions",
|
|
12
|
-
"RelationshipOptions",
|
|
13
|
-
"RecordSearchResult",
|
|
14
|
-
"SearchResult",
|
|
15
|
-
"SearchQuery",
|
|
16
|
-
"Transaction",
|
|
17
|
-
]
|
|
@@ -1,19 +0,0 @@
|
|
|
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 SearchQuery(TypedDict, total=False):
|
|
11
|
-
"""TypedDict representing the query structure for finding records."""
|
|
12
|
-
|
|
13
|
-
where: Optional[Dict[str, Any]]
|
|
14
|
-
labels: Optional[List[str]]
|
|
15
|
-
skip: Optional[int]
|
|
16
|
-
limit: Optional[int]
|
|
17
|
-
orderBy: Optional[Union[Dict[str, OrderDirection], OrderDirection]]
|
|
18
|
-
select: Optional[Dict[str, Any]]
|
|
19
|
-
groupBy: Optional[List[str]]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|