rushdb 2.6.0__tar.gz → 2.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rushdb-2.6.0 → rushdb-2.9.0}/PKG-INFO +23 -1
- {rushdb-2.6.0 → rushdb-2.9.0}/README.md +22 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/pyproject.toml +1 -1
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/__init__.py +6 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/api/transactions.py +2 -1
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/client.py +14 -3
- rushdb-2.9.0/src/rushdb/common.py +33 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/models/__init__.py +6 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/models/result.py +7 -2
- rushdb-2.9.0/src/rushdb/models/search_query.py +93 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/tests/test_create_import.py +9 -3
- {rushdb-2.6.0 → rushdb-2.9.0}/tests/test_search_query.py +224 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/uv.lock +49 -49
- rushdb-2.6.0/src/rushdb/common.py +0 -20
- rushdb-2.6.0/src/rushdb/models/search_query.py +0 -37
- {rushdb-2.6.0 → rushdb-2.9.0}/.env.example +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/.github/workflows/ci.yml +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/.gitignore +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/CODE_OF_CONDUCT.md +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/CONTRIBUTING.md +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/LICENSE +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/rushdb-logo.svg +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/__init__.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/api/__init__.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/api/ai.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/api/base.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/api/labels.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/api/properties.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/api/query.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/api/records.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/api/relationship_patterns.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/api/relationships.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/models/api_response.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/models/property.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/models/record.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/models/relationship.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/models/transaction.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/src/rushdb/utils/token_prefix.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/tests/__init__.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/tests/test_base_setup.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.0}/tests/test_relationship_patterns.py +0 -0
- {rushdb-2.6.0 → rushdb-2.9.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.9.0
|
|
4
4
|
Summary: RushDB Python SDK — memory layer for AI agents and modern apps
|
|
5
5
|
Project-URL: Homepage, https://github.com/rush-db/rushdb-python
|
|
6
6
|
Project-URL: Repository, https://github.com/rush-db/rushdb-python
|
|
@@ -135,6 +135,28 @@ authored_posts = db.records.find({
|
|
|
135
135
|
'limit': 10,
|
|
136
136
|
})
|
|
137
137
|
|
|
138
|
+
# Multi-hop: add hops to $relation — everyone in Alice's reporting chain, up to 4 levels
|
|
139
|
+
chain = db.records.find({
|
|
140
|
+
'labels': ['EMPLOYEE'],
|
|
141
|
+
'where': {
|
|
142
|
+
'EMPLOYEE': {
|
|
143
|
+
'$relation': {'type': 'REPORTS_TO', 'direction': 'out', 'hops': {'min': 1, 'max': 4}},
|
|
144
|
+
'name': {'$contains': 'Alice'},
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
# Cycle detection: accounts on a circular transfer ring (fraud rings, circular ownership)
|
|
150
|
+
ring_members = db.records.find({
|
|
151
|
+
'labels': ['ACCOUNT'],
|
|
152
|
+
'where': {
|
|
153
|
+
'RING': { # key is a display name — the $cycle block holds only $relation
|
|
154
|
+
'$cycle': True,
|
|
155
|
+
'$relation': {'type': 'TRANSFERRED_TO', 'direction': 'out', 'hops': {'min': 2, 'max': 6}},
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
|
|
138
160
|
# Manage relationships explicitly
|
|
139
161
|
user = db.records.find_uniq({'labels': ['USER'], 'where': {'name': 'Alice'}})
|
|
140
162
|
company = db.records.find_uniq({'labels': ['COMPANY'], 'where': {'name': 'Acme Corp'}})
|
|
@@ -110,6 +110,28 @@ authored_posts = db.records.find({
|
|
|
110
110
|
'limit': 10,
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
+
# Multi-hop: add hops to $relation — everyone in Alice's reporting chain, up to 4 levels
|
|
114
|
+
chain = db.records.find({
|
|
115
|
+
'labels': ['EMPLOYEE'],
|
|
116
|
+
'where': {
|
|
117
|
+
'EMPLOYEE': {
|
|
118
|
+
'$relation': {'type': 'REPORTS_TO', 'direction': 'out', 'hops': {'min': 1, 'max': 4}},
|
|
119
|
+
'name': {'$contains': 'Alice'},
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
# Cycle detection: accounts on a circular transfer ring (fraud rings, circular ownership)
|
|
125
|
+
ring_members = db.records.find({
|
|
126
|
+
'labels': ['ACCOUNT'],
|
|
127
|
+
'where': {
|
|
128
|
+
'RING': { # key is a display name — the $cycle block holds only $relation
|
|
129
|
+
'$cycle': True,
|
|
130
|
+
'$relation': {'type': 'TRANSFERRED_TO', 'direction': 'out', 'hops': {'min': 2, 'max': 6}},
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
|
|
113
135
|
# Manage relationships explicitly
|
|
114
136
|
user = db.records.find_uniq({'labels': ['USER'], 'where': {'name': 'Alice'}})
|
|
115
137
|
company = db.records.find_uniq({'labels': ['COMPANY'], 'where': {'name': 'Acme Corp'}})
|
|
@@ -24,6 +24,9 @@ from .models.search_query import (
|
|
|
24
24
|
RelationshipEndpointQuery,
|
|
25
25
|
RelationshipSearchQuery,
|
|
26
26
|
SearchQuery,
|
|
27
|
+
TraversalHops,
|
|
28
|
+
TraversalHopsRange,
|
|
29
|
+
TraversalRelationOptions,
|
|
27
30
|
)
|
|
28
31
|
from .models.transaction import Transaction
|
|
29
32
|
|
|
@@ -46,6 +49,9 @@ __all__ = [
|
|
|
46
49
|
"SearchQuery",
|
|
47
50
|
"RelationshipEndpointQuery",
|
|
48
51
|
"RelationshipSearchQuery",
|
|
52
|
+
"TraversalHops",
|
|
53
|
+
"TraversalHopsRange",
|
|
54
|
+
"TraversalRelationOptions",
|
|
49
55
|
"QueryAPI",
|
|
50
56
|
"RelationsAPI",
|
|
51
57
|
"RelationshipPatternsAPI",
|
|
@@ -51,7 +51,8 @@ class TransactionsAPI(BaseAPI):
|
|
|
51
51
|
Args:
|
|
52
52
|
ttl (Optional[int], optional): Time-to-live in milliseconds for the transaction.
|
|
53
53
|
After this time, the transaction will automatically expire and be rolled back.
|
|
54
|
-
If None, defaults to 5000ms (5 seconds).
|
|
54
|
+
If None, defaults to 5000ms (5 seconds). Values above the server-side cap
|
|
55
|
+
(60 seconds) are clamped to it. Defaults to None.
|
|
55
56
|
|
|
56
57
|
Returns:
|
|
57
58
|
Transaction: A Transaction object that can be used with other API operations.
|
|
@@ -144,7 +144,9 @@ class RushDB:
|
|
|
144
144
|
else None
|
|
145
145
|
)
|
|
146
146
|
effective_url = url or base_url or self.DEFAULT_BASE_URL
|
|
147
|
-
|
|
147
|
+
# Both `url` and its deprecated alias `base_url` are documented to get
|
|
148
|
+
# `/api/v1` appended when the path is absent.
|
|
149
|
+
if (url or base_url) and "/api/" not in effective_url:
|
|
148
150
|
effective_url = effective_url.rstrip("/") + "/api/v1"
|
|
149
151
|
self.base_url = effective_url.rstrip("/")
|
|
150
152
|
self.api_key = api_key
|
|
@@ -245,8 +247,17 @@ class RushDB:
|
|
|
245
247
|
with urllib.request.urlopen(request) as response:
|
|
246
248
|
return json.loads(response.read().decode("utf-8"))
|
|
247
249
|
except urllib.error.HTTPError as e:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
+
# Error bodies are usually JSON, but a gateway/proxy may return HTML or an
|
|
251
|
+
# empty body — without this guard the JSONDecodeError raised here would
|
|
252
|
+
# escape as-is instead of surfacing as a RushDBError.
|
|
253
|
+
try:
|
|
254
|
+
error_body = json.loads(e.read().decode("utf-8"))
|
|
255
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
256
|
+
error_body = {}
|
|
257
|
+
message = error_body.get("message")
|
|
258
|
+
if isinstance(message, list):
|
|
259
|
+
message = "; ".join(str(item) for item in message)
|
|
260
|
+
raise RushDBError(message or f"HTTP {e.code}", error_body, status=e.code)
|
|
250
261
|
except urllib.error.URLError as e:
|
|
251
262
|
raise RushDBError(f"Connection error: {str(e)}")
|
|
252
263
|
except json.JSONDecodeError as e:
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Dict, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RushDBError(Exception):
|
|
5
|
+
"""Custom exception for RushDB client errors.
|
|
6
|
+
|
|
7
|
+
Attributes:
|
|
8
|
+
details: Parsed error response body from the server, if any.
|
|
9
|
+
status: HTTP status code for server errors (e.g. 403 for a write attempt
|
|
10
|
+
with a read-only API key, 408 for a server-side transaction timeout).
|
|
11
|
+
``None`` for client-side errors such as connection failures.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
message: str,
|
|
17
|
+
details: Optional[Dict] = None,
|
|
18
|
+
status: Optional[int] = None,
|
|
19
|
+
):
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.details = details or {}
|
|
22
|
+
self.status = status
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NonUniqueResultError(Exception):
|
|
26
|
+
"""Raised by ``records.find_uniq()`` when more than one record matches the query."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, count: int):
|
|
29
|
+
super().__init__(
|
|
30
|
+
f"Expected at most one record but found {count}. "
|
|
31
|
+
"Refine your search query or use records.find() instead."
|
|
32
|
+
)
|
|
33
|
+
self.count = count
|
|
@@ -11,6 +11,9 @@ from .search_query import (
|
|
|
11
11
|
RelationshipEndpointQuery,
|
|
12
12
|
RelationshipSearchQuery,
|
|
13
13
|
SearchQuery,
|
|
14
|
+
TraversalHops,
|
|
15
|
+
TraversalHopsRange,
|
|
16
|
+
TraversalRelationOptions,
|
|
14
17
|
)
|
|
15
18
|
from .transaction import Transaction
|
|
16
19
|
|
|
@@ -26,5 +29,8 @@ __all__ = [
|
|
|
26
29
|
"SearchQuery",
|
|
27
30
|
"RelationshipEndpointQuery",
|
|
28
31
|
"RelationshipSearchQuery",
|
|
32
|
+
"TraversalHops",
|
|
33
|
+
"TraversalHopsRange",
|
|
34
|
+
"TraversalRelationOptions",
|
|
29
35
|
"Transaction",
|
|
30
36
|
]
|
|
@@ -246,5 +246,10 @@ class SearchResult(Generic[T]):
|
|
|
246
246
|
return pd.DataFrame(rows)
|
|
247
247
|
|
|
248
248
|
|
|
249
|
-
|
|
250
|
-
|
|
249
|
+
class RecordSearchResult(SearchResult[Record]):
|
|
250
|
+
"""Search result specialized for ``Record`` items.
|
|
251
|
+
|
|
252
|
+
A real subclass (not a ``SearchResult[Record]`` alias) so that
|
|
253
|
+
``isinstance(result, RecordSearchResult)`` works — isinstance checks against
|
|
254
|
+
subscripted generics raise ``TypeError`` at runtime.
|
|
255
|
+
"""
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any, Dict, List, Optional, TypedDict, Union
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class OrderDirection(str, Enum):
|
|
6
|
+
ASC = "asc"
|
|
7
|
+
DESC = "desc"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TraversalHopsRange(TypedDict, total=False):
|
|
11
|
+
"""Hop range for variable-length traversal.
|
|
12
|
+
|
|
13
|
+
``min`` defaults to 1 (2 inside a ``$cycle`` block). Omitting ``max``
|
|
14
|
+
requests unbounded traversal, which is only allowed on self-hosted
|
|
15
|
+
deployments and projects with a custom Neo4j instance; the shared cloud
|
|
16
|
+
connection caps ``max`` per deployment (default 25).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
min: int
|
|
20
|
+
max: int
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
TraversalHops = Union[int, TraversalHopsRange]
|
|
24
|
+
"""Depth of a variable-length traversal: an exact hop count or a range."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TraversalRelationOptions(TypedDict, total=False):
|
|
28
|
+
"""Value for the ``$relation`` key of a traversal block in ``where``.
|
|
29
|
+
|
|
30
|
+
``type`` and ``direction`` constrain every hop; ``hops`` makes the
|
|
31
|
+
traversal variable-length. The nested label constrains only the final
|
|
32
|
+
record — intermediate records are anonymous.
|
|
33
|
+
|
|
34
|
+
Example::
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
"labels": ["EMPLOYEE"],
|
|
38
|
+
"where": {
|
|
39
|
+
"EMPLOYEE": {
|
|
40
|
+
"$alias": "$manager",
|
|
41
|
+
"$relation": {
|
|
42
|
+
"type": "REPORTS_TO",
|
|
43
|
+
"direction": "out",
|
|
44
|
+
"hops": {"min": 1, "max": 4},
|
|
45
|
+
},
|
|
46
|
+
"name": {"$contains": "Alice"},
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
type: str
|
|
53
|
+
direction: str # 'in' | 'out'
|
|
54
|
+
hops: TraversalHops
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SearchQuery(TypedDict, total=False):
|
|
58
|
+
"""TypedDict representing the query structure for finding records.
|
|
59
|
+
|
|
60
|
+
Inside ``where``, a nested key that is a label name traverses to related
|
|
61
|
+
records. Traversal blocks accept ``$alias`` (name the endpoint for
|
|
62
|
+
``select``/``groupBy``), ``$relation`` (a type string or
|
|
63
|
+
:class:`TraversalRelationOptions`, including variable-length ``hops``),
|
|
64
|
+
and ``$cycle`` (``True`` binds the traversal back to the parent record to
|
|
65
|
+
detect rings; requires ``$relation`` with ``hops`` where ``min`` >= 2 and
|
|
66
|
+
accepts no other keys).
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
where: Optional[Dict[str, Any]]
|
|
70
|
+
labels: Optional[List[str]]
|
|
71
|
+
skip: Optional[int]
|
|
72
|
+
limit: Optional[int]
|
|
73
|
+
orderBy: Optional[Union[Dict[str, OrderDirection], OrderDirection]]
|
|
74
|
+
select: Optional[Dict[str, Any]]
|
|
75
|
+
groupBy: Optional[List[str]]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class RelationshipEndpointQuery(TypedDict, total=False):
|
|
79
|
+
"""Endpoint record predicates for relationship search."""
|
|
80
|
+
|
|
81
|
+
where: Optional[Dict[str, Any]]
|
|
82
|
+
labels: Optional[List[str]]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class RelationshipSearchQuery(TypedDict, total=False):
|
|
86
|
+
"""TypedDict representing the query structure for finding relationship edges."""
|
|
87
|
+
|
|
88
|
+
where: Optional[Dict[str, Any]]
|
|
89
|
+
source: Optional[RelationshipEndpointQuery]
|
|
90
|
+
target: Optional[RelationshipEndpointQuery]
|
|
91
|
+
skip: Optional[int]
|
|
92
|
+
limit: Optional[int]
|
|
93
|
+
orderBy: Optional[Union[Dict[str, OrderDirection], OrderDirection]]
|
|
@@ -40,7 +40,7 @@ class TestCreateImport(TestBase):
|
|
|
40
40
|
# Test new functionality
|
|
41
41
|
self.assertEqual(record.get("name"), "Google LLC")
|
|
42
42
|
self.assertEqual(record.get("nonexistent", "default"), "default")
|
|
43
|
-
self.assertTrue(record.exists
|
|
43
|
+
self.assertTrue(record.exists)
|
|
44
44
|
|
|
45
45
|
def test_record_methods(self):
|
|
46
46
|
"""Test Record class methods"""
|
|
@@ -201,7 +201,11 @@ class TestCreateImport(TestBase):
|
|
|
201
201
|
)
|
|
202
202
|
|
|
203
203
|
def test_create_with_nested_data(self):
|
|
204
|
-
"""Test
|
|
204
|
+
"""Test importing records with nested data structure.
|
|
205
|
+
|
|
206
|
+
``create_many`` accepts flat rows only (it raises ``ValueError`` for
|
|
207
|
+
nested payloads); nested JSON goes through ``import_json``.
|
|
208
|
+
"""
|
|
205
209
|
data = {
|
|
206
210
|
"name": "Meta Platforms Inc",
|
|
207
211
|
"rating": 4.6,
|
|
@@ -220,7 +224,9 @@ class TestCreateImport(TestBase):
|
|
|
220
224
|
}
|
|
221
225
|
],
|
|
222
226
|
}
|
|
223
|
-
self.
|
|
227
|
+
with self.assertRaises(ValueError):
|
|
228
|
+
self.client.records.create_many("COMPANY", data)
|
|
229
|
+
self.client.records.import_json(data=data, label="COMPANY")
|
|
224
230
|
|
|
225
231
|
def test_transaction_rollback(self):
|
|
226
232
|
"""Test transaction rollback"""
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"""Test cases for RushDB search query functionality."""
|
|
2
2
|
|
|
3
|
+
import time
|
|
3
4
|
import unittest
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from src.rushdb import RushDB, RushDBError
|
|
4
8
|
|
|
5
9
|
from .test_base_setup import TestBase
|
|
6
10
|
|
|
@@ -389,5 +393,225 @@ class TestSearchQuery(TestBase):
|
|
|
389
393
|
self.client.records.find(query)
|
|
390
394
|
|
|
391
395
|
|
|
396
|
+
class TestMultihopAndCycles(TestBase):
|
|
397
|
+
"""Variable-length traversal ($relation.hops) and cycle detection ($cycle).
|
|
398
|
+
|
|
399
|
+
Seeds its own graph, isolated by a unique tenantId:
|
|
400
|
+
|
|
401
|
+
Reporting chain (MHEmployee, REPORTS_TO, directed "up"):
|
|
402
|
+
E1 -> E2 -> E3 -> E4
|
|
403
|
+
|
|
404
|
+
Transfer ring + linear chain (MHAccount, TRANSFERRED_TO):
|
|
405
|
+
A -> B -> C -> A (3-hop directed ring)
|
|
406
|
+
X -> Y -> Z (no cycle)
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
tenant: str
|
|
410
|
+
|
|
411
|
+
@classmethod
|
|
412
|
+
def setUpClass(cls):
|
|
413
|
+
super().setUpClass()
|
|
414
|
+
cls.tenant = f"multihop-{uuid.uuid4().hex[:8]}"
|
|
415
|
+
# TestBase only creates a client per-test (setUp); seeding needs one here.
|
|
416
|
+
cls.client = RushDB(cls.token, base_url=cls.base_url)
|
|
417
|
+
|
|
418
|
+
# TestBase skips per-test in setUp when the server is unreachable; this
|
|
419
|
+
# class seeds data in setUpClass, so it must skip here for the same
|
|
420
|
+
# reason (e.g. CI without a running RushDB) instead of erroring.
|
|
421
|
+
try:
|
|
422
|
+
if not cls.client.ping():
|
|
423
|
+
raise unittest.SkipTest(
|
|
424
|
+
f"Could not connect to RushDB at {cls.base_url}"
|
|
425
|
+
)
|
|
426
|
+
except RushDBError as e:
|
|
427
|
+
raise unittest.SkipTest(f"RushDB connection error: {str(e)}")
|
|
428
|
+
|
|
429
|
+
cls.client.records.create_many(
|
|
430
|
+
"MHEmployee",
|
|
431
|
+
[
|
|
432
|
+
{"name": "E1", "managerName": "E2", "tenantId": cls.tenant},
|
|
433
|
+
{"name": "E2", "managerName": "E3", "tenantId": cls.tenant},
|
|
434
|
+
{"name": "E3", "managerName": "E4", "tenantId": cls.tenant},
|
|
435
|
+
{"name": "E4", "tenantId": cls.tenant},
|
|
436
|
+
],
|
|
437
|
+
)
|
|
438
|
+
cls.client.relationships.create_many(
|
|
439
|
+
source={
|
|
440
|
+
"label": "MHEmployee",
|
|
441
|
+
"key": "managerName",
|
|
442
|
+
"where": {"tenantId": cls.tenant},
|
|
443
|
+
},
|
|
444
|
+
target={
|
|
445
|
+
"label": "MHEmployee",
|
|
446
|
+
"key": "name",
|
|
447
|
+
"where": {"tenantId": cls.tenant},
|
|
448
|
+
},
|
|
449
|
+
type="REPORTS_TO",
|
|
450
|
+
direction="out",
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
cls.client.records.create_many(
|
|
454
|
+
"MHAccount",
|
|
455
|
+
[
|
|
456
|
+
{"name": "A", "sendsTo": "B", "tenantId": cls.tenant},
|
|
457
|
+
{"name": "B", "sendsTo": "C", "tenantId": cls.tenant},
|
|
458
|
+
{"name": "C", "sendsTo": "A", "tenantId": cls.tenant},
|
|
459
|
+
{"name": "X", "sendsTo": "Y", "tenantId": cls.tenant},
|
|
460
|
+
{"name": "Y", "sendsTo": "Z", "tenantId": cls.tenant},
|
|
461
|
+
{"name": "Z", "tenantId": cls.tenant},
|
|
462
|
+
],
|
|
463
|
+
)
|
|
464
|
+
cls.client.relationships.create_many(
|
|
465
|
+
source={
|
|
466
|
+
"label": "MHAccount",
|
|
467
|
+
"key": "sendsTo",
|
|
468
|
+
"where": {"tenantId": cls.tenant},
|
|
469
|
+
},
|
|
470
|
+
target={
|
|
471
|
+
"label": "MHAccount",
|
|
472
|
+
"key": "name",
|
|
473
|
+
"where": {"tenantId": cls.tenant},
|
|
474
|
+
},
|
|
475
|
+
type="TRANSFERRED_TO",
|
|
476
|
+
direction="out", # sender -> receiver
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# relationships.create_many is applied via apoc.periodic.iterate — poll
|
|
480
|
+
# until both relationship sets are visible.
|
|
481
|
+
for _ in range(10):
|
|
482
|
+
employee_rels = cls.client.relationships.find(
|
|
483
|
+
{
|
|
484
|
+
"source": {
|
|
485
|
+
"labels": ["MHEmployee"],
|
|
486
|
+
"where": {"tenantId": cls.tenant},
|
|
487
|
+
},
|
|
488
|
+
"limit": 100,
|
|
489
|
+
}
|
|
490
|
+
)
|
|
491
|
+
account_rels = cls.client.relationships.find(
|
|
492
|
+
{
|
|
493
|
+
"source": {
|
|
494
|
+
"labels": ["MHAccount"],
|
|
495
|
+
"where": {"tenantId": cls.tenant},
|
|
496
|
+
},
|
|
497
|
+
"limit": 100,
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
if len(employee_rels.data) >= 3 and len(account_rels.data) >= 5:
|
|
501
|
+
break
|
|
502
|
+
time.sleep(1)
|
|
503
|
+
|
|
504
|
+
@classmethod
|
|
505
|
+
def tearDownClass(cls):
|
|
506
|
+
cls.client.records.delete({"where": {"tenantId": cls.tenant}})
|
|
507
|
+
super().tearDownClass()
|
|
508
|
+
|
|
509
|
+
def _names(self, result):
|
|
510
|
+
return sorted(record.get("name") for record in result)
|
|
511
|
+
|
|
512
|
+
def test_hops_range_reaches_chain_top(self):
|
|
513
|
+
"""hops {max: 3} reaches E4 from every subordinate"""
|
|
514
|
+
result = self.client.records.find(
|
|
515
|
+
{
|
|
516
|
+
"labels": ["MHEmployee"],
|
|
517
|
+
"where": {
|
|
518
|
+
"tenantId": self.tenant,
|
|
519
|
+
"MHEmployee": {
|
|
520
|
+
"$relation": {
|
|
521
|
+
"type": "REPORTS_TO",
|
|
522
|
+
"direction": "out",
|
|
523
|
+
"hops": {"max": 3},
|
|
524
|
+
},
|
|
525
|
+
"name": "E4",
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
}
|
|
529
|
+
)
|
|
530
|
+
self.assertEqual(self._names(result), ["E1", "E2", "E3"])
|
|
531
|
+
|
|
532
|
+
def test_hops_range_is_bounded(self):
|
|
533
|
+
"""hops {max: 2} does not reach 3 hops away"""
|
|
534
|
+
result = self.client.records.find(
|
|
535
|
+
{
|
|
536
|
+
"labels": ["MHEmployee"],
|
|
537
|
+
"where": {
|
|
538
|
+
"tenantId": self.tenant,
|
|
539
|
+
"MHEmployee": {
|
|
540
|
+
"$relation": {
|
|
541
|
+
"type": "REPORTS_TO",
|
|
542
|
+
"direction": "out",
|
|
543
|
+
"hops": {"max": 2},
|
|
544
|
+
},
|
|
545
|
+
"name": "E4",
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
}
|
|
549
|
+
)
|
|
550
|
+
self.assertEqual(self._names(result), ["E2", "E3"])
|
|
551
|
+
|
|
552
|
+
def test_hops_exact_count(self):
|
|
553
|
+
"""hops: 3 matches exactly 3 hops"""
|
|
554
|
+
result = self.client.records.find(
|
|
555
|
+
{
|
|
556
|
+
"labels": ["MHEmployee"],
|
|
557
|
+
"where": {
|
|
558
|
+
"tenantId": self.tenant,
|
|
559
|
+
"MHEmployee": {
|
|
560
|
+
"$relation": {
|
|
561
|
+
"type": "REPORTS_TO",
|
|
562
|
+
"direction": "out",
|
|
563
|
+
"hops": 3,
|
|
564
|
+
},
|
|
565
|
+
"name": "E4",
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
}
|
|
569
|
+
)
|
|
570
|
+
self.assertEqual(self._names(result), ["E1"])
|
|
571
|
+
|
|
572
|
+
def test_cycle_flags_ring_members(self):
|
|
573
|
+
"""$cycle returns exactly the ring participants with a deduplicated total"""
|
|
574
|
+
result = self.client.records.find(
|
|
575
|
+
{
|
|
576
|
+
"labels": ["MHAccount"],
|
|
577
|
+
"where": {
|
|
578
|
+
"tenantId": self.tenant,
|
|
579
|
+
"RING": {
|
|
580
|
+
"$cycle": True,
|
|
581
|
+
"$relation": {
|
|
582
|
+
"type": "TRANSFERRED_TO",
|
|
583
|
+
"direction": "out",
|
|
584
|
+
"hops": {"min": 2, "max": 6},
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
}
|
|
589
|
+
)
|
|
590
|
+
self.assertEqual(self._names(result), ["A", "B", "C"])
|
|
591
|
+
self.assertEqual(result.total, 3)
|
|
592
|
+
|
|
593
|
+
def test_not_cycle_excludes_ring_members(self):
|
|
594
|
+
"""$not around a $cycle block finds acyclic accounts"""
|
|
595
|
+
result = self.client.records.find(
|
|
596
|
+
{
|
|
597
|
+
"labels": ["MHAccount"],
|
|
598
|
+
"where": {
|
|
599
|
+
"tenantId": self.tenant,
|
|
600
|
+
"$not": {
|
|
601
|
+
"RING": {
|
|
602
|
+
"$cycle": True,
|
|
603
|
+
"$relation": {
|
|
604
|
+
"type": "TRANSFERRED_TO",
|
|
605
|
+
"direction": "out",
|
|
606
|
+
"hops": {"min": 2, "max": 6},
|
|
607
|
+
},
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
}
|
|
612
|
+
)
|
|
613
|
+
self.assertEqual(self._names(result), ["X", "Y", "Z"])
|
|
614
|
+
|
|
615
|
+
|
|
392
616
|
if __name__ == "__main__":
|
|
393
617
|
unittest.main()
|
|
@@ -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'",
|
|
@@ -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" }, marker = "python_full_version < '3.9'" },
|
|
19
|
+
{ name = "mypy-extensions", marker = "python_full_version < '3.9'" },
|
|
20
|
+
{ name = "packaging", marker = "python_full_version < '3.9'" },
|
|
21
|
+
{ name = "pathspec", marker = "python_full_version < '3.9'" },
|
|
22
|
+
{ name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
|
23
|
+
{ name = "tomli", marker = "python_full_version < '3.9'" },
|
|
24
|
+
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
|
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.9.*'" },
|
|
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 < '3.11'" },
|
|
67
|
-
{ name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
62
|
+
{ name = "mypy-extensions", marker = "python_full_version >= '3.9'" },
|
|
63
|
+
{ name = "packaging", marker = "python_full_version >= '3.9'" },
|
|
64
|
+
{ name = "pathspec", marker = "python_full_version >= '3.9'" },
|
|
65
|
+
{ name = "platformdirs", version = "4.3.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
|
66
|
+
{ name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
|
|
67
|
+
{ 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'" },
|
|
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 = "sys_platform == 'win32'" },
|
|
199
|
+
{ name = "colorama", marker = "python_full_version < '3.10' and 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 = "sys_platform == 'win32'" },
|
|
214
|
+
{ name = "colorama", marker = "python_full_version >= '3.10' and 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", marker = "python_full_version < '3.9'" },
|
|
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 <= '3.11'" },
|
|
397
|
+
{ name = "tomli", marker = "python_full_version >= '3.9' and 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'" },
|
|
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.13'" },
|
|
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", marker = "python_full_version < '3.9'" },
|
|
465
|
+
{ name = "tomli", marker = "python_full_version < '3.9'" },
|
|
466
|
+
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
|
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 < '3.11'" },
|
|
521
|
-
{ name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" } },
|
|
518
|
+
{ name = "mypy-extensions", marker = "python_full_version >= '3.9'" },
|
|
519
|
+
{ name = "pathspec", marker = "python_full_version >= '3.9'" },
|
|
520
|
+
{ name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
|
|
521
|
+
{ name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
|
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 = "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" },
|
|
652
|
+
{ name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" },
|
|
653
|
+
{ name = "exceptiongroup", marker = "python_full_version < '3.9'" },
|
|
654
|
+
{ name = "iniconfig", marker = "python_full_version < '3.9'" },
|
|
655
|
+
{ name = "packaging", marker = "python_full_version < '3.9'" },
|
|
656
|
+
{ name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
|
657
|
+
{ name = "tomli", marker = "python_full_version < '3.9'" },
|
|
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 = "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'" },
|
|
673
|
+
{ name = "colorama", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" },
|
|
674
|
+
{ name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
|
|
675
|
+
{ name = "iniconfig", marker = "python_full_version >= '3.9'" },
|
|
676
|
+
{ name = "packaging", marker = "python_full_version >= '3.9'" },
|
|
677
|
+
{ name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
|
678
|
+
{ name = "pygments", marker = "python_full_version >= '3.9'" },
|
|
679
|
+
{ name = "tomli", marker = "python_full_version >= '3.9' and 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"], marker = "python_full_version < '3.9'" },
|
|
695
|
+
{ name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
|
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"], marker = "python_full_version >= '3.9'" },
|
|
712
|
+
{ name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
|
713
|
+
{ name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
|
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.9.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" }, marker = "python_full_version < '3.9'" },
|
|
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" }, marker = "python_full_version >= '3.9'" },
|
|
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,20 +0,0 @@
|
|
|
1
|
-
from typing import Dict, Optional
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class RushDBError(Exception):
|
|
5
|
-
"""Custom exception for RushDB client errors."""
|
|
6
|
-
|
|
7
|
-
def __init__(self, message: str, details: Optional[Dict] = None):
|
|
8
|
-
super().__init__(message)
|
|
9
|
-
self.details = details or {}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class NonUniqueResultError(Exception):
|
|
13
|
-
"""Raised by ``records.find_uniq()`` when more than one record matches the query."""
|
|
14
|
-
|
|
15
|
-
def __init__(self, count: int):
|
|
16
|
-
super().__init__(
|
|
17
|
-
f"Expected at most one record but found {count}. "
|
|
18
|
-
"Refine your search query or use records.find() instead."
|
|
19
|
-
)
|
|
20
|
-
self.count = count
|
|
@@ -1,37 +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]]
|
|
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]]
|
|
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
|