rushdb 2.2.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.2.0 → rushdb-2.3.0}/PKG-INFO +12 -2
- {rushdb-2.2.0 → rushdb-2.3.0}/README.md +11 -1
- {rushdb-2.2.0 → rushdb-2.3.0}/pyproject.toml +1 -3
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/__init__.py +16 -1
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/records.py +11 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/relationships.py +52 -28
- rushdb-2.3.0/src/rushdb/models/__init__.py +30 -0
- {rushdb-2.2.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.2.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.2.0 → rushdb-2.3.0}/uv.lock +2 -2
- rushdb-2.2.0/src/rushdb/models/__init__.py +0 -17
- rushdb-2.2.0/src/rushdb/models/search_query.py +0 -19
- rushdb-2.2.0/tests/test_relationship_patterns.py +0 -54
- {rushdb-2.2.0 → rushdb-2.3.0}/.env.example +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/.github/workflows/ci.yml +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/.gitignore +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/CODE_OF_CONDUCT.md +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/CONTRIBUTING.md +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/LICENSE +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/rushdb-logo.svg +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/__init__.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/__init__.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/ai.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/base.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/labels.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/properties.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/query.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/relationship_patterns.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/transactions.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/client.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/common.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/models/api_response.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/models/property.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/models/record.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/models/result.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/models/transaction.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/utils/token_prefix.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/tests/__init__.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/tests/test_base_setup.py +0 -0
- {rushdb-2.2.0 → rushdb-2.3.0}/tests/test_search_query.py +0 -0
- {rushdb-2.2.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 = [
|
|
@@ -13,8 +13,18 @@ from .common import NonUniqueResultError, RushDBError
|
|
|
13
13
|
from .models.api_response import ApiResponse
|
|
14
14
|
from .models.property import Property
|
|
15
15
|
from .models.record import Record, RecordTarget, RelationTarget
|
|
16
|
-
from .models.relationship import
|
|
16
|
+
from .models.relationship import (
|
|
17
|
+
Relationship,
|
|
18
|
+
RelationshipDetachOptions,
|
|
19
|
+
RelationshipDirection,
|
|
20
|
+
RelationshipOptions,
|
|
21
|
+
)
|
|
17
22
|
from .models.result import RecordSearchResult, SearchResult
|
|
23
|
+
from .models.search_query import (
|
|
24
|
+
RelationshipEndpointQuery,
|
|
25
|
+
RelationshipSearchQuery,
|
|
26
|
+
SearchQuery,
|
|
27
|
+
)
|
|
18
28
|
from .models.transaction import Transaction
|
|
19
29
|
|
|
20
30
|
__all__ = [
|
|
@@ -29,8 +39,13 @@ __all__ = [
|
|
|
29
39
|
"SearchResult",
|
|
30
40
|
"Transaction",
|
|
31
41
|
"Property",
|
|
42
|
+
"Relationship",
|
|
43
|
+
"RelationshipDirection",
|
|
32
44
|
"RelationshipOptions",
|
|
33
45
|
"RelationshipDetachOptions",
|
|
46
|
+
"SearchQuery",
|
|
47
|
+
"RelationshipEndpointQuery",
|
|
48
|
+
"RelationshipSearchQuery",
|
|
34
49
|
"QueryAPI",
|
|
35
50
|
"RelationsAPI",
|
|
36
51
|
"RelationshipPatternsAPI",
|
|
@@ -446,6 +446,17 @@ class RecordsAPI(BaseAPI):
|
|
|
446
446
|
>>>
|
|
447
447
|
>>> # Attach using Record objects
|
|
448
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
|
+
... )
|
|
449
460
|
"""
|
|
450
461
|
headers = Transaction._build_transaction_header(transaction)
|
|
451
462
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import typing
|
|
2
|
-
from typing import TYPE_CHECKING,
|
|
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
9
|
from .relationship_patterns import RelationshipPatternsAPI
|
|
@@ -47,21 +47,19 @@ class RelationsAPI(BaseAPI):
|
|
|
47
47
|
Attributes:
|
|
48
48
|
client: The underlying RushDB client instance for making HTTP requests.
|
|
49
49
|
|
|
50
|
-
Note:
|
|
51
|
-
This API currently contains async methods. Ensure you're using it in an
|
|
52
|
-
async context or consider updating to sync methods if needed.
|
|
53
|
-
|
|
54
50
|
Example:
|
|
55
51
|
>>> from rushdb import RushDB
|
|
56
52
|
>>> client = RushDB(api_key="your_api_key")
|
|
57
53
|
>>> relations_api = client.relationships
|
|
58
54
|
>>>
|
|
59
55
|
>>> # Find all relationships
|
|
60
|
-
>>> 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"}})
|
|
61
60
|
>>>
|
|
62
61
|
>>> # Find relationships with pagination
|
|
63
|
-
>>>
|
|
64
|
-
>>> page_1 = await relations_api.find(pagination=pagination)
|
|
62
|
+
>>> page_1 = relations_api.find(pagination={"limit": 50, "skip": 0})
|
|
65
63
|
"""
|
|
66
64
|
|
|
67
65
|
def __init__(self, client: "RushDB"):
|
|
@@ -70,40 +68,57 @@ class RelationsAPI(BaseAPI):
|
|
|
70
68
|
|
|
71
69
|
def find(
|
|
72
70
|
self,
|
|
73
|
-
search_query: Optional[
|
|
71
|
+
search_query: Optional[RelationshipSearchQuery] = None,
|
|
74
72
|
pagination: Optional[PaginationParams] = None,
|
|
75
73
|
transaction: Optional[Union[Transaction, str]] = None,
|
|
76
|
-
) ->
|
|
74
|
+
) -> SearchResult[Relationship]:
|
|
77
75
|
"""Search for and retrieve relationships matching the specified criteria.
|
|
78
76
|
|
|
79
77
|
Args:
|
|
80
|
-
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.
|
|
81
85
|
If None, returns all relationships (subject to pagination limits).
|
|
82
|
-
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.
|
|
83
90
|
transaction: Optional transaction context.
|
|
84
91
|
|
|
85
92
|
Returns:
|
|
86
|
-
|
|
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``.
|
|
87
97
|
|
|
88
98
|
Raises:
|
|
89
99
|
RushDBError: If the server request fails.
|
|
90
100
|
|
|
91
101
|
Example:
|
|
92
102
|
>>> relations_api = RelationsAPI(client)
|
|
93
|
-
>>>
|
|
94
|
-
|
|
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'
|
|
95
113
|
"""
|
|
96
|
-
#
|
|
97
|
-
|
|
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 {})
|
|
98
117
|
if pagination:
|
|
99
118
|
if pagination.get("limit") is not None:
|
|
100
|
-
|
|
119
|
+
payload["limit"] = pagination["limit"]
|
|
101
120
|
if pagination.get("skip") is not None:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
# Construct path with query string
|
|
105
|
-
query_string = f"?{urlencode(query_params)}" if query_params else ""
|
|
106
|
-
path = f"/relationships/search{query_string}"
|
|
121
|
+
payload["skip"] = pagination["skip"]
|
|
107
122
|
|
|
108
123
|
# Build headers with transaction if present
|
|
109
124
|
headers = Transaction._build_transaction_header(transaction)
|
|
@@ -111,12 +126,17 @@ class RelationsAPI(BaseAPI):
|
|
|
111
126
|
# Make request
|
|
112
127
|
response = self.client._make_request(
|
|
113
128
|
method="POST",
|
|
114
|
-
path=
|
|
115
|
-
data=
|
|
129
|
+
path="/relationships/search",
|
|
130
|
+
data=payload,
|
|
116
131
|
headers=headers,
|
|
117
132
|
)
|
|
118
133
|
|
|
119
|
-
|
|
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
|
+
)
|
|
120
140
|
|
|
121
141
|
def create_many(
|
|
122
142
|
self,
|
|
@@ -125,6 +145,7 @@ class RelationsAPI(BaseAPI):
|
|
|
125
145
|
target: dict,
|
|
126
146
|
type: Optional[str] = None,
|
|
127
147
|
direction: Optional[str] = None,
|
|
148
|
+
properties: Optional[dict] = None,
|
|
128
149
|
many_to_many: Optional[bool] = None,
|
|
129
150
|
transaction: Optional[Union[Transaction, str]] = None,
|
|
130
151
|
) -> dict:
|
|
@@ -139,6 +160,7 @@ class RelationsAPI(BaseAPI):
|
|
|
139
160
|
target (dict): { label: str, key?: str, where?: dict }
|
|
140
161
|
type (str, optional): Relationship type override.
|
|
141
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.
|
|
142
164
|
many_to_many (bool, optional): Enable cartesian mode (requires filters, disallows keys).
|
|
143
165
|
transaction (Transaction|str, optional): Transaction context.
|
|
144
166
|
|
|
@@ -151,6 +173,8 @@ class RelationsAPI(BaseAPI):
|
|
|
151
173
|
payload["type"] = type
|
|
152
174
|
if direction:
|
|
153
175
|
payload["direction"] = direction
|
|
176
|
+
if properties is not None:
|
|
177
|
+
payload["properties"] = properties
|
|
154
178
|
if many_to_many is not None:
|
|
155
179
|
payload["manyToMany"] = many_to_many
|
|
156
180
|
return self.client._make_request(
|
|
@@ -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
|
+
]
|
|
@@ -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
|
+
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
version = 1
|
|
2
|
-
revision =
|
|
2
|
+
revision = 2
|
|
3
3
|
requires-python = ">=3.8"
|
|
4
4
|
resolution-markers = [
|
|
5
5
|
"python_full_version >= '3.10'",
|
|
@@ -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]]
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
from unittest.mock import Mock
|
|
2
|
-
|
|
3
|
-
from src.rushdb.api.relationship_patterns import RelationshipPatternsAPI
|
|
4
|
-
from src.rushdb.api.relationships import RelationsAPI
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_relationships_exposes_patterns_namespace():
|
|
8
|
-
client = Mock()
|
|
9
|
-
|
|
10
|
-
relationships = RelationsAPI(client)
|
|
11
|
-
|
|
12
|
-
assert isinstance(relationships.patterns, RelationshipPatternsAPI)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_list_relationship_patterns():
|
|
16
|
-
client = Mock()
|
|
17
|
-
client._make_request.return_value = {
|
|
18
|
-
"data": {"patterns": [], "relationships": [], "analysis": {"status": "idle"}},
|
|
19
|
-
"success": True,
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
response = RelationsAPI(client).patterns.list()
|
|
23
|
-
|
|
24
|
-
client._make_request.assert_called_once_with("GET", "/relationships/patterns")
|
|
25
|
-
assert response.data["patterns"] == []
|
|
26
|
-
assert response.data["analysis"]["status"] == "idle"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def test_mutate_relationship_patterns():
|
|
30
|
-
client = Mock()
|
|
31
|
-
client._make_request.return_value = {"data": {"deleted": True}, "success": True}
|
|
32
|
-
patterns = RelationsAPI(client).patterns
|
|
33
|
-
|
|
34
|
-
patterns.analyze()
|
|
35
|
-
client._make_request.assert_called_with(
|
|
36
|
-
"POST", "/relationships/patterns/analyze", {}
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
patterns.approve("pattern-id")
|
|
40
|
-
client._make_request.assert_called_with(
|
|
41
|
-
"POST", "/relationships/patterns/pattern-id/approve", {}
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
patterns.ignore("pattern-id")
|
|
45
|
-
client._make_request.assert_called_with(
|
|
46
|
-
"POST", "/relationships/patterns/pattern-id/ignore", {}
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
patterns.delete("pattern-id", delete_existing=True)
|
|
50
|
-
client._make_request.assert_called_with(
|
|
51
|
-
"DELETE",
|
|
52
|
-
"/relationships/patterns/pattern-id",
|
|
53
|
-
params={"deleteExisting": "true"},
|
|
54
|
-
)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|