rushdb 2.2.0__tar.gz → 2.6.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.6.0}/PKG-INFO +14 -3
- {rushdb-2.2.0 → rushdb-2.6.0}/README.md +13 -2
- {rushdb-2.2.0 → rushdb-2.6.0}/pyproject.toml +1 -3
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/__init__.py +16 -1
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/ai.py +12 -12
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/records.py +16 -1
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/relationship_patterns.py +3 -3
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/relationships.py +52 -28
- rushdb-2.6.0/src/rushdb/models/__init__.py +30 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/models/api_response.py +1 -1
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/models/property.py +3 -2
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/models/relationship.py +4 -1
- rushdb-2.6.0/src/rushdb/models/search_query.py +37 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/tests/test_create_import.py +47 -0
- rushdb-2.6.0/tests/test_relationship_patterns.py +160 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/uv.lock +48 -48
- 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.6.0}/.env.example +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/.github/workflows/ci.yml +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/.gitignore +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/CODE_OF_CONDUCT.md +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/CONTRIBUTING.md +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/LICENSE +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/rushdb-logo.svg +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/src/__init__.py +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/__init__.py +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/base.py +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/labels.py +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/properties.py +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/query.py +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/transactions.py +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/client.py +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/common.py +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/models/record.py +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/models/result.py +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/models/transaction.py +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/utils/token_prefix.py +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/tests/__init__.py +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/tests/test_base_setup.py +0 -0
- {rushdb-2.2.0 → rushdb-2.6.0}/tests/test_search_query.py +0 -0
- {rushdb-2.2.0 → rushdb-2.6.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.6.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
|
---
|
|
@@ -151,7 +161,8 @@ csv_data = "name,email,age\nJohn,john@example.com,30\nJane,jane@example.com,25"
|
|
|
151
161
|
db.records.import_csv(
|
|
152
162
|
label='USER',
|
|
153
163
|
data=csv_data,
|
|
154
|
-
|
|
164
|
+
# skipEmptyValues: treat empty cells ("" / []) as unset instead of storing them (0/False are kept)
|
|
165
|
+
options={'returnResult': True, 'suggestTypes': True, 'skipEmptyValues': True},
|
|
155
166
|
parse_config={'header': True, 'skipEmptyLines': True, 'dynamicTyping': True},
|
|
156
167
|
)
|
|
157
168
|
```
|
|
@@ -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
|
---
|
|
@@ -126,7 +136,8 @@ csv_data = "name,email,age\nJohn,john@example.com,30\nJane,jane@example.com,25"
|
|
|
126
136
|
db.records.import_csv(
|
|
127
137
|
label='USER',
|
|
128
138
|
data=csv_data,
|
|
129
|
-
|
|
139
|
+
# skipEmptyValues: treat empty cells ("" / []) as unset instead of storing them (0/False are kept)
|
|
140
|
+
options={'returnResult': True, 'suggestTypes': True, 'skipEmptyValues': True},
|
|
130
141
|
parse_config={'header': True, 'skipEmptyLines': True, 'dynamicTyping': True},
|
|
131
142
|
)
|
|
132
143
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "rushdb"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.6.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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""AI API for RushDB Python SDK.
|
|
2
2
|
|
|
3
|
-
Provides methods for graph
|
|
3
|
+
Provides methods for graph schema exploration, semantic vector search,
|
|
4
4
|
and embedding index management.
|
|
5
5
|
"""
|
|
6
6
|
|
|
@@ -123,7 +123,7 @@ class AIAPI(BaseAPI):
|
|
|
123
123
|
Example::
|
|
124
124
|
|
|
125
125
|
>>> db = RushDB(api_key="...")
|
|
126
|
-
>>>
|
|
126
|
+
>>> schema = db.ai.get_schema()
|
|
127
127
|
>>> results = db.ai.search({"query": "fast cars", "propertyName": "description"})
|
|
128
128
|
>>> db.ai.indexes.create({"propertyName": "description", "label": "Book"})
|
|
129
129
|
"""
|
|
@@ -132,12 +132,12 @@ class AIAPI(BaseAPI):
|
|
|
132
132
|
super().__init__(client)
|
|
133
133
|
self.indexes = _AIIndexesNamespace(client)
|
|
134
134
|
|
|
135
|
-
def
|
|
135
|
+
def get_schema(
|
|
136
136
|
self,
|
|
137
137
|
params: Optional[Dict[str, Any]] = None,
|
|
138
138
|
transaction: Optional[Union[Transaction, str]] = None,
|
|
139
139
|
) -> ApiResponse:
|
|
140
|
-
"""Return the full graph
|
|
140
|
+
"""Return the full graph schema as structured JSON.
|
|
141
141
|
|
|
142
142
|
Each item contains the label name, record count, properties with value
|
|
143
143
|
ranges/samples, and cross-label relationships with direction.
|
|
@@ -149,13 +149,13 @@ class AIAPI(BaseAPI):
|
|
|
149
149
|
|
|
150
150
|
Args:
|
|
151
151
|
params: Optional filter. Pass ``{"labels": ["Label1"]}`` to scope
|
|
152
|
-
the
|
|
153
|
-
to bypass the 1-hour
|
|
152
|
+
the schema to specific labels only. Pass ``{"force": True}``
|
|
153
|
+
to bypass the 1-hour schema cache and trigger a full
|
|
154
154
|
recalculation.
|
|
155
155
|
transaction: Optional transaction context.
|
|
156
156
|
|
|
157
157
|
Returns:
|
|
158
|
-
ApiResponse: Response whose ``data`` is a list of
|
|
158
|
+
ApiResponse: Response whose ``data`` is a list of schema items.
|
|
159
159
|
"""
|
|
160
160
|
headers: Dict[str, str] = {}
|
|
161
161
|
if transaction is not None:
|
|
@@ -165,7 +165,7 @@ class AIAPI(BaseAPI):
|
|
|
165
165
|
headers["x-transaction-id"] = tx_id
|
|
166
166
|
|
|
167
167
|
response = self.client._make_request(
|
|
168
|
-
"POST", "/ai/
|
|
168
|
+
"POST", "/ai/schema", params or {}, headers=headers or None
|
|
169
169
|
)
|
|
170
170
|
return ApiResponse(
|
|
171
171
|
data=response.get("data"),
|
|
@@ -173,12 +173,12 @@ class AIAPI(BaseAPI):
|
|
|
173
173
|
total=response.get("total"),
|
|
174
174
|
)
|
|
175
175
|
|
|
176
|
-
def
|
|
176
|
+
def get_schema_markdown(
|
|
177
177
|
self,
|
|
178
178
|
params: Optional[Dict[str, Any]] = None,
|
|
179
179
|
transaction: Optional[Union[Transaction, str]] = None,
|
|
180
180
|
) -> ApiResponse:
|
|
181
|
-
"""Return the full graph
|
|
181
|
+
"""Return the full graph schema as compact Markdown tables.
|
|
182
182
|
|
|
183
183
|
Token-efficient representation intended for direct LLM consumption.
|
|
184
184
|
Includes labels with counts, properties with types and value
|
|
@@ -191,7 +191,7 @@ class AIAPI(BaseAPI):
|
|
|
191
191
|
Args:
|
|
192
192
|
params: Optional filter. Pass ``{"labels": ["Label1"]}`` to scope
|
|
193
193
|
the output to specific labels only. Pass ``{"force": True}``
|
|
194
|
-
to bypass the 1-hour
|
|
194
|
+
to bypass the 1-hour schema cache and trigger a full
|
|
195
195
|
recalculation.
|
|
196
196
|
transaction: Optional transaction context.
|
|
197
197
|
|
|
@@ -206,7 +206,7 @@ class AIAPI(BaseAPI):
|
|
|
206
206
|
headers["x-transaction-id"] = tx_id
|
|
207
207
|
|
|
208
208
|
response = self.client._make_request(
|
|
209
|
-
"POST", "/ai/
|
|
209
|
+
"POST", "/ai/schema/md", params or {}, headers=headers or None
|
|
210
210
|
)
|
|
211
211
|
return ApiResponse(
|
|
212
212
|
data=response.get("data"),
|
|
@@ -172,6 +172,8 @@ class RecordsAPI(BaseAPI):
|
|
|
172
172
|
Available options:
|
|
173
173
|
- returnResult (bool): Whether to return the created record data. Defaults to True.
|
|
174
174
|
- suggestTypes (bool): Whether to automatically suggest data types. Defaults to True.
|
|
175
|
+
- skipEmptyValues (bool): Treat empty strings ("") and empty arrays ([]) as unset —
|
|
176
|
+
no property is created. 0 and False are kept. Defaults to False.
|
|
175
177
|
vectors (Optional[List[Dict[str, Any]]]): Optional pre-computed embedding vectors
|
|
176
178
|
to write alongside the record. Each entry must contain at least
|
|
177
179
|
``propertyName`` and ``vector``; ``similarityFunction`` is required when
|
|
@@ -231,7 +233,9 @@ class RecordsAPI(BaseAPI):
|
|
|
231
233
|
record. Nested objects/arrays are not supported here — raises
|
|
232
234
|
``ValueError`` if any item contains a nested object or list.
|
|
233
235
|
options: Optional write options forwarded as-is to the server
|
|
234
|
-
(e.g. ``suggestTypes``, ``
|
|
236
|
+
(e.g. ``suggestTypes``, ``skipEmptyValues``, ``mergeBy``,
|
|
237
|
+
``mergeStrategy``, etc.). ``skipEmptyValues`` treats empty strings
|
|
238
|
+
and empty arrays as unset (``0``/``False`` are kept); defaults to False.
|
|
235
239
|
vectors: Optional per-row inline vectors for external embedding indexes.
|
|
236
240
|
``vectors[i]`` is applied to ``data[i]``. Each element is a list of
|
|
237
241
|
vector entry dicts: ``[{"propertyName": str, "vector": List[float],
|
|
@@ -446,6 +450,17 @@ class RecordsAPI(BaseAPI):
|
|
|
446
450
|
>>>
|
|
447
451
|
>>> # Attach using Record objects
|
|
448
452
|
>>> response = records_api.attach(source_record, target_records)
|
|
453
|
+
>>>
|
|
454
|
+
>>> # Attach with a typed relationship carrying edge properties
|
|
455
|
+
>>> response = records_api.attach(
|
|
456
|
+
... movie,
|
|
457
|
+
... actor,
|
|
458
|
+
... options={
|
|
459
|
+
... "type": "STARS_IN",
|
|
460
|
+
... "direction": "out",
|
|
461
|
+
... "properties": {"role": "lead", "billing": 1},
|
|
462
|
+
... },
|
|
463
|
+
... )
|
|
449
464
|
"""
|
|
450
465
|
headers = Transaction._build_transaction_header(transaction)
|
|
451
466
|
|
|
@@ -10,7 +10,7 @@ if TYPE_CHECKING:
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class RelationshipPatternsAPI(BaseAPI):
|
|
13
|
-
"""Review and manage relationship patterns inferred from project
|
|
13
|
+
"""Review and manage relationship patterns inferred from project schema.
|
|
14
14
|
|
|
15
15
|
Accessed via ``db.relationships.patterns``.
|
|
16
16
|
"""
|
|
@@ -27,12 +27,12 @@ class RelationshipPatternsAPI(BaseAPI):
|
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
def list(self) -> ApiResponse:
|
|
30
|
-
"""List inferred patterns,
|
|
30
|
+
"""List inferred patterns, schema relationships, and analysis status."""
|
|
31
31
|
response = self.client._make_request("GET", "/relationships/patterns")
|
|
32
32
|
return self._wrap(response)
|
|
33
33
|
|
|
34
34
|
def analyze(self) -> ApiResponse:
|
|
35
|
-
"""Queue
|
|
35
|
+
"""Queue schema analysis to generate relationship pattern suggestions."""
|
|
36
36
|
response = self.client._make_request(
|
|
37
37
|
"POST", "/relationships/patterns/analyze", {}
|
|
38
38
|
)
|
|
@@ -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
|
+
]
|
|
@@ -19,7 +19,7 @@ class ApiResponse(Generic[T]):
|
|
|
19
19
|
total: Optional total count (e.g. for list endpoints).
|
|
20
20
|
|
|
21
21
|
Example:
|
|
22
|
-
>>> response = db.ai.
|
|
22
|
+
>>> response = db.ai.get_schema_markdown()
|
|
23
23
|
>>> schema_md = response.data # str
|
|
24
24
|
>>> response = db.ai.indexes.find()
|
|
25
25
|
>>> indexes = response.data # list[dict]
|
|
@@ -21,8 +21,9 @@ BooleanValue = bool
|
|
|
21
21
|
NumberValue = Union[float, int]
|
|
22
22
|
StringValue = str
|
|
23
23
|
|
|
24
|
-
# Property types
|
|
25
|
-
|
|
24
|
+
# Property types. A None/null value means the field is unset; `None` is still accepted as an
|
|
25
|
+
# input value in `PropertyWithValue.value` below.
|
|
26
|
+
PropertyType = Literal["boolean", "datetime", "number", "string"]
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
class Property(TypedDict):
|
|
@@ -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
|
+
)
|
|
@@ -15,13 +15,13 @@ resolution-markers = [
|
|
|
15
15
|
"python_full_version < '3.9'",
|
|
16
16
|
]
|
|
17
17
|
dependencies = [
|
|
18
|
-
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }
|
|
19
|
-
{ name = "mypy-extensions"
|
|
20
|
-
{ name = "packaging"
|
|
21
|
-
{ name = "pathspec"
|
|
22
|
-
{ name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }
|
|
23
|
-
{ name = "tomli"
|
|
24
|
-
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }
|
|
18
|
+
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" } },
|
|
19
|
+
{ name = "mypy-extensions" },
|
|
20
|
+
{ name = "packaging" },
|
|
21
|
+
{ name = "pathspec" },
|
|
22
|
+
{ name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" } },
|
|
23
|
+
{ name = "tomli" },
|
|
24
|
+
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" } },
|
|
25
25
|
]
|
|
26
26
|
sdist = { url = "https://files.pythonhosted.org/packages/04/b0/46fb0d4e00372f4a86a6f8efa3cb193c9f64863615e39010b1477e010578/black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f", size = 644810, upload-time = "2024-08-02T17:43:18.405Z" }
|
|
27
27
|
wheels = [
|
|
@@ -57,14 +57,14 @@ resolution-markers = [
|
|
|
57
57
|
"python_full_version == '3.9.*'",
|
|
58
58
|
]
|
|
59
59
|
dependencies = [
|
|
60
|
-
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version
|
|
60
|
+
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
|
61
61
|
{ name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
|
62
|
-
{ name = "mypy-extensions"
|
|
63
|
-
{ name = "packaging"
|
|
64
|
-
{ name = "pathspec"
|
|
65
|
-
{ name = "platformdirs", version = "4.3.8", source = { registry = "https://pypi.org/simple" }
|
|
66
|
-
{ name = "tomli", marker = "python_full_version
|
|
67
|
-
{ name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version
|
|
62
|
+
{ name = "mypy-extensions" },
|
|
63
|
+
{ name = "packaging" },
|
|
64
|
+
{ name = "pathspec" },
|
|
65
|
+
{ name = "platformdirs", version = "4.3.8", source = { registry = "https://pypi.org/simple" } },
|
|
66
|
+
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
|
67
|
+
{ name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
68
68
|
]
|
|
69
69
|
sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" }
|
|
70
70
|
wheels = [
|
|
@@ -196,7 +196,7 @@ resolution-markers = [
|
|
|
196
196
|
"python_full_version < '3.9'",
|
|
197
197
|
]
|
|
198
198
|
dependencies = [
|
|
199
|
-
{ name = "colorama", marker = "
|
|
199
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
200
200
|
]
|
|
201
201
|
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
|
|
202
202
|
wheels = [
|
|
@@ -211,7 +211,7 @@ resolution-markers = [
|
|
|
211
211
|
"python_full_version >= '3.10'",
|
|
212
212
|
]
|
|
213
213
|
dependencies = [
|
|
214
|
-
{ name = "colorama", marker = "
|
|
214
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
215
215
|
]
|
|
216
216
|
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
|
|
217
217
|
wheels = [
|
|
@@ -311,7 +311,7 @@ wheels = [
|
|
|
311
311
|
|
|
312
312
|
[package.optional-dependencies]
|
|
313
313
|
toml = [
|
|
314
|
-
{ name = "tomli"
|
|
314
|
+
{ name = "tomli" },
|
|
315
315
|
]
|
|
316
316
|
|
|
317
317
|
[[package]]
|
|
@@ -394,7 +394,7 @@ wheels = [
|
|
|
394
394
|
|
|
395
395
|
[package.optional-dependencies]
|
|
396
396
|
toml = [
|
|
397
|
-
{ name = "tomli", marker = "python_full_version
|
|
397
|
+
{ name = "tomli", marker = "python_full_version <= '3.11'" },
|
|
398
398
|
]
|
|
399
399
|
|
|
400
400
|
[[package]]
|
|
@@ -402,8 +402,8 @@ name = "exceptiongroup"
|
|
|
402
402
|
version = "1.3.0"
|
|
403
403
|
source = { registry = "https://pypi.org/simple" }
|
|
404
404
|
dependencies = [
|
|
405
|
-
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
|
406
|
-
{ name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.
|
|
405
|
+
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9' or python_full_version >= '3.11'" },
|
|
406
|
+
{ name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
|
|
407
407
|
]
|
|
408
408
|
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
|
|
409
409
|
wheels = [
|
|
@@ -461,9 +461,9 @@ resolution-markers = [
|
|
|
461
461
|
"python_full_version < '3.9'",
|
|
462
462
|
]
|
|
463
463
|
dependencies = [
|
|
464
|
-
{ name = "mypy-extensions"
|
|
465
|
-
{ name = "tomli"
|
|
466
|
-
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }
|
|
464
|
+
{ name = "mypy-extensions" },
|
|
465
|
+
{ name = "tomli" },
|
|
466
|
+
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" } },
|
|
467
467
|
]
|
|
468
468
|
sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" }
|
|
469
469
|
wheels = [
|
|
@@ -515,10 +515,10 @@ resolution-markers = [
|
|
|
515
515
|
"python_full_version == '3.9.*'",
|
|
516
516
|
]
|
|
517
517
|
dependencies = [
|
|
518
|
-
{ name = "mypy-extensions"
|
|
519
|
-
{ name = "pathspec"
|
|
520
|
-
{ name = "tomli", marker = "python_full_version
|
|
521
|
-
{ name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }
|
|
518
|
+
{ name = "mypy-extensions" },
|
|
519
|
+
{ name = "pathspec" },
|
|
520
|
+
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
|
521
|
+
{ name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" } },
|
|
522
522
|
]
|
|
523
523
|
sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" }
|
|
524
524
|
wheels = [
|
|
@@ -649,12 +649,12 @@ resolution-markers = [
|
|
|
649
649
|
"python_full_version < '3.9'",
|
|
650
650
|
]
|
|
651
651
|
dependencies = [
|
|
652
|
-
{ name = "colorama", marker = "
|
|
653
|
-
{ name = "exceptiongroup"
|
|
654
|
-
{ name = "iniconfig"
|
|
655
|
-
{ name = "packaging"
|
|
656
|
-
{ name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }
|
|
657
|
-
{ name = "tomli"
|
|
652
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
653
|
+
{ name = "exceptiongroup" },
|
|
654
|
+
{ name = "iniconfig" },
|
|
655
|
+
{ name = "packaging" },
|
|
656
|
+
{ name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" } },
|
|
657
|
+
{ name = "tomli" },
|
|
658
658
|
]
|
|
659
659
|
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
|
|
660
660
|
wheels = [
|
|
@@ -670,13 +670,13 @@ resolution-markers = [
|
|
|
670
670
|
"python_full_version == '3.9.*'",
|
|
671
671
|
]
|
|
672
672
|
dependencies = [
|
|
673
|
-
{ name = "colorama", marker = "
|
|
674
|
-
{ name = "exceptiongroup", marker = "python_full_version
|
|
675
|
-
{ name = "iniconfig"
|
|
676
|
-
{ name = "packaging"
|
|
677
|
-
{ name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }
|
|
678
|
-
{ name = "pygments"
|
|
679
|
-
{ name = "tomli", marker = "python_full_version
|
|
673
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
674
|
+
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
|
675
|
+
{ name = "iniconfig" },
|
|
676
|
+
{ name = "packaging" },
|
|
677
|
+
{ name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" } },
|
|
678
|
+
{ name = "pygments" },
|
|
679
|
+
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
|
680
680
|
]
|
|
681
681
|
sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
|
|
682
682
|
wheels = [
|
|
@@ -691,8 +691,8 @@ resolution-markers = [
|
|
|
691
691
|
"python_full_version < '3.9'",
|
|
692
692
|
]
|
|
693
693
|
dependencies = [
|
|
694
|
-
{ name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"]
|
|
695
|
-
{ name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }
|
|
694
|
+
{ name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"] },
|
|
695
|
+
{ name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" } },
|
|
696
696
|
]
|
|
697
697
|
sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042, upload-time = "2024-03-24T20:16:34.856Z" }
|
|
698
698
|
wheels = [
|
|
@@ -708,9 +708,9 @@ resolution-markers = [
|
|
|
708
708
|
"python_full_version == '3.9.*'",
|
|
709
709
|
]
|
|
710
710
|
dependencies = [
|
|
711
|
-
{ name = "coverage", version = "7.9.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"]
|
|
712
|
-
{ name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }
|
|
713
|
-
{ name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }
|
|
711
|
+
{ name = "coverage", version = "7.9.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"] },
|
|
712
|
+
{ name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" } },
|
|
713
|
+
{ name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" } },
|
|
714
714
|
]
|
|
715
715
|
sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" }
|
|
716
716
|
wheels = [
|
|
@@ -785,7 +785,7 @@ wheels = [
|
|
|
785
785
|
|
|
786
786
|
[[package]]
|
|
787
787
|
name = "rushdb"
|
|
788
|
-
version = "2.
|
|
788
|
+
version = "2.6.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'" },
|
|
@@ -930,7 +930,7 @@ resolution-markers = [
|
|
|
930
930
|
"python_full_version < '3.9'",
|
|
931
931
|
]
|
|
932
932
|
dependencies = [
|
|
933
|
-
{ name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }
|
|
933
|
+
{ name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" } },
|
|
934
934
|
]
|
|
935
935
|
sdist = { url = "https://files.pythonhosted.org/packages/fa/3c/4f2a430c01a22abd49a583b6b944173e39e7d01b688190a5618bd59a2e22/types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95", size = 18065, upload-time = "2024-10-16T02:46:10.818Z" }
|
|
936
936
|
wheels = [
|
|
@@ -946,7 +946,7 @@ resolution-markers = [
|
|
|
946
946
|
"python_full_version == '3.9.*'",
|
|
947
947
|
]
|
|
948
948
|
dependencies = [
|
|
949
|
-
{ name = "urllib3", version = "2.5.0", source = { registry = "https://pypi.org/simple" }
|
|
949
|
+
{ name = "urllib3", version = "2.5.0", source = { registry = "https://pypi.org/simple" } },
|
|
950
950
|
]
|
|
951
951
|
sdist = { url = "https://files.pythonhosted.org/packages/6d/7f/73b3a04a53b0fd2a911d4ec517940ecd6600630b559e4505cc7b68beb5a0/types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826", size = 23118, upload-time = "2025-06-11T03:11:41.272Z" }
|
|
952
952
|
wheels = [
|
|
@@ -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
|