rushdb 2.2.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {rushdb-2.2.0 → rushdb-2.3.0}/PKG-INFO +12 -2
  2. {rushdb-2.2.0 → rushdb-2.3.0}/README.md +11 -1
  3. {rushdb-2.2.0 → rushdb-2.3.0}/pyproject.toml +1 -3
  4. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/__init__.py +16 -1
  5. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/records.py +11 -0
  6. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/relationships.py +52 -28
  7. rushdb-2.3.0/src/rushdb/models/__init__.py +30 -0
  8. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/models/relationship.py +4 -1
  9. rushdb-2.3.0/src/rushdb/models/search_query.py +37 -0
  10. {rushdb-2.2.0 → rushdb-2.3.0}/tests/test_create_import.py +47 -0
  11. rushdb-2.3.0/tests/test_relationship_patterns.py +160 -0
  12. {rushdb-2.2.0 → rushdb-2.3.0}/uv.lock +2 -2
  13. rushdb-2.2.0/src/rushdb/models/__init__.py +0 -17
  14. rushdb-2.2.0/src/rushdb/models/search_query.py +0 -19
  15. rushdb-2.2.0/tests/test_relationship_patterns.py +0 -54
  16. {rushdb-2.2.0 → rushdb-2.3.0}/.env.example +0 -0
  17. {rushdb-2.2.0 → rushdb-2.3.0}/.github/workflows/ci.yml +0 -0
  18. {rushdb-2.2.0 → rushdb-2.3.0}/.gitignore +0 -0
  19. {rushdb-2.2.0 → rushdb-2.3.0}/CODE_OF_CONDUCT.md +0 -0
  20. {rushdb-2.2.0 → rushdb-2.3.0}/CONTRIBUTING.md +0 -0
  21. {rushdb-2.2.0 → rushdb-2.3.0}/LICENSE +0 -0
  22. {rushdb-2.2.0 → rushdb-2.3.0}/rushdb-logo.svg +0 -0
  23. {rushdb-2.2.0 → rushdb-2.3.0}/src/__init__.py +0 -0
  24. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/__init__.py +0 -0
  25. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/ai.py +0 -0
  26. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/base.py +0 -0
  27. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/labels.py +0 -0
  28. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/properties.py +0 -0
  29. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/query.py +0 -0
  30. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/relationship_patterns.py +0 -0
  31. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/api/transactions.py +0 -0
  32. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/client.py +0 -0
  33. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/common.py +0 -0
  34. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/models/api_response.py +0 -0
  35. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/models/property.py +0 -0
  36. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/models/record.py +0 -0
  37. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/models/result.py +0 -0
  38. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/models/transaction.py +0 -0
  39. {rushdb-2.2.0 → rushdb-2.3.0}/src/rushdb/utils/token_prefix.py +0 -0
  40. {rushdb-2.2.0 → rushdb-2.3.0}/tests/__init__.py +0 -0
  41. {rushdb-2.2.0 → rushdb-2.3.0}/tests/test_base_setup.py +0 -0
  42. {rushdb-2.2.0 → rushdb-2.3.0}/tests/test_search_query.py +0 -0
  43. {rushdb-2.2.0 → rushdb-2.3.0}/tests/test_search_result.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rushdb
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: RushDB Python SDK — memory layer for AI agents and modern apps
5
5
  Project-URL: Homepage, https://github.com/rush-db/rushdb-python
6
6
  Project-URL: Repository, https://github.com/rush-db/rushdb-python
@@ -138,7 +138,17 @@ authored_posts = db.records.find({
138
138
  # Manage relationships explicitly
139
139
  user = db.records.find_uniq({'labels': ['USER'], 'where': {'name': 'Alice'}})
140
140
  company = db.records.find_uniq({'labels': ['COMPANY'], 'where': {'name': 'Acme Corp'}})
141
- user.attach(target=company, options={'type': 'WORKS_AT', 'direction': 'out'})
141
+ user.attach(
142
+ target=company,
143
+ options={'type': 'WORKS_AT', 'direction': 'out', 'properties': {'source': 'profile'}},
144
+ )
145
+
146
+ # Relationship search: where filters edge type/properties, source/target filter endpoint records
147
+ relationships = db.relationships.find({
148
+ 'source': {'labels': ['USER'], 'where': {'name': 'Alice'}},
149
+ 'target': {'labels': ['COMPANY']},
150
+ 'where': {'type': 'WORKS_AT', 'source': 'profile'},
151
+ })
142
152
  ```
143
153
 
144
154
  ---
@@ -113,7 +113,17 @@ authored_posts = db.records.find({
113
113
  # Manage relationships explicitly
114
114
  user = db.records.find_uniq({'labels': ['USER'], 'where': {'name': 'Alice'}})
115
115
  company = db.records.find_uniq({'labels': ['COMPANY'], 'where': {'name': 'Acme Corp'}})
116
- user.attach(target=company, options={'type': 'WORKS_AT', 'direction': 'out'})
116
+ user.attach(
117
+ target=company,
118
+ options={'type': 'WORKS_AT', 'direction': 'out', 'properties': {'source': 'profile'}},
119
+ )
120
+
121
+ # Relationship search: where filters edge type/properties, source/target filter endpoint records
122
+ relationships = db.relationships.find({
123
+ 'source': {'labels': ['USER'], 'where': {'name': 'Alice'}},
124
+ 'target': {'labels': ['COMPANY']},
125
+ 'where': {'type': 'WORKS_AT', 'source': 'profile'},
126
+ })
117
127
  ```
118
128
 
119
129
  ---
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rushdb"
3
- version = "2.2.0"
3
+ version = "2.3.0"
4
4
  description = "RushDB Python SDK — memory layer for AI agents and modern apps"
5
5
  authors = [
6
6
  {name = "RushDB Team", email = "hi@rushdb.com"}
@@ -81,8 +81,6 @@ line-length = 120
81
81
 
82
82
  [tool.isort]
83
83
  profile = "black"
84
- multi_line_output = 3
85
- line_length = 120
86
84
 
87
85
  [dependency-groups]
88
86
  dev = [
@@ -13,8 +13,18 @@ from .common import NonUniqueResultError, RushDBError
13
13
  from .models.api_response import ApiResponse
14
14
  from .models.property import Property
15
15
  from .models.record import Record, RecordTarget, RelationTarget
16
- from .models.relationship import RelationshipDetachOptions, RelationshipOptions
16
+ from .models.relationship import (
17
+ Relationship,
18
+ RelationshipDetachOptions,
19
+ RelationshipDirection,
20
+ RelationshipOptions,
21
+ )
17
22
  from .models.result import RecordSearchResult, SearchResult
23
+ from .models.search_query import (
24
+ RelationshipEndpointQuery,
25
+ RelationshipSearchQuery,
26
+ SearchQuery,
27
+ )
18
28
  from .models.transaction import Transaction
19
29
 
20
30
  __all__ = [
@@ -29,8 +39,13 @@ __all__ = [
29
39
  "SearchResult",
30
40
  "Transaction",
31
41
  "Property",
42
+ "Relationship",
43
+ "RelationshipDirection",
32
44
  "RelationshipOptions",
33
45
  "RelationshipDetachOptions",
46
+ "SearchQuery",
47
+ "RelationshipEndpointQuery",
48
+ "RelationshipSearchQuery",
34
49
  "QueryAPI",
35
50
  "RelationsAPI",
36
51
  "RelationshipPatternsAPI",
@@ -446,6 +446,17 @@ class RecordsAPI(BaseAPI):
446
446
  >>>
447
447
  >>> # Attach using Record objects
448
448
  >>> response = records_api.attach(source_record, target_records)
449
+ >>>
450
+ >>> # Attach with a typed relationship carrying edge properties
451
+ >>> response = records_api.attach(
452
+ ... movie,
453
+ ... actor,
454
+ ... options={
455
+ ... "type": "STARS_IN",
456
+ ... "direction": "out",
457
+ ... "properties": {"role": "lead", "billing": 1},
458
+ ... },
459
+ ... )
449
460
  """
450
461
  headers = Transaction._build_transaction_header(transaction)
451
462
 
@@ -1,9 +1,9 @@
1
1
  import typing
2
- from typing import TYPE_CHECKING, List, Optional, TypedDict, Union
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.search_query import SearchQuery
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 = await relations_api.find()
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
- >>> pagination = {"limit": 50, "skip": 0}
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[SearchQuery] = None,
71
+ search_query: Optional[RelationshipSearchQuery] = None,
74
72
  pagination: Optional[PaginationParams] = None,
75
73
  transaction: Optional[Union[Transaction, str]] = None,
76
- ) -> List[Relationship]:
74
+ ) -> SearchResult[Relationship]:
77
75
  """Search for and retrieve relationships matching the specified criteria.
78
76
 
79
77
  Args:
80
- search_query: The search criteria to filter relationships.
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
- List[Relationship]: Relationships matching the search criteria.
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
- >>> all_rels = relations_api.find()
94
- >>> page = relations_api.find(pagination={"limit": 50, "skip": 0})
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
- # Build query string for pagination
97
- query_params = {}
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
- query_params["limit"] = str(pagination["limit"])
119
+ payload["limit"] = pagination["limit"]
101
120
  if pagination.get("skip") is not None:
102
- query_params["skip"] = str(pagination["skip"])
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=path,
115
- data=typing.cast(typing.Dict[str, typing.Any], search_query or {}),
129
+ path="/relationships/search",
130
+ data=payload,
116
131
  headers=headers,
117
132
  )
118
133
 
119
- return response.get("data", [])
134
+ data = response.get("data", [])
135
+ return SearchResult(
136
+ data=data,
137
+ total=response.get("total"),
138
+ search_query=typing.cast(typing.Any, payload),
139
+ )
120
140
 
121
141
  def create_many(
122
142
  self,
@@ -125,6 +145,7 @@ class RelationsAPI(BaseAPI):
125
145
  target: dict,
126
146
  type: Optional[str] = None,
127
147
  direction: Optional[str] = None,
148
+ properties: Optional[dict] = None,
128
149
  many_to_many: Optional[bool] = None,
129
150
  transaction: Optional[Union[Transaction, str]] = None,
130
151
  ) -> dict:
@@ -139,6 +160,7 @@ class RelationsAPI(BaseAPI):
139
160
  target (dict): { label: str, key?: str, where?: dict }
140
161
  type (str, optional): Relationship type override.
141
162
  direction (str, optional): 'in' | 'out'. Defaults to 'out' server-side when omitted.
163
+ properties (dict, optional): Shared user-defined properties to store on created edges.
142
164
  many_to_many (bool, optional): Enable cartesian mode (requires filters, disallows keys).
143
165
  transaction (Transaction|str, optional): Transaction context.
144
166
 
@@ -151,6 +173,8 @@ class RelationsAPI(BaseAPI):
151
173
  payload["type"] = type
152
174
  if direction:
153
175
  payload["direction"] = direction
176
+ if properties is not None:
177
+ payload["properties"] = properties
154
178
  if many_to_many is not None:
155
179
  payload["manyToMany"] = many_to_many
156
180
  return self.client._make_request(
@@ -0,0 +1,30 @@
1
+ from .property import Property
2
+ from .record import Record
3
+ from .relationship import (
4
+ Relationship,
5
+ RelationshipDetachOptions,
6
+ RelationshipDirection,
7
+ RelationshipOptions,
8
+ )
9
+ from .result import RecordSearchResult, SearchResult
10
+ from .search_query import (
11
+ RelationshipEndpointQuery,
12
+ RelationshipSearchQuery,
13
+ SearchQuery,
14
+ )
15
+ from .transaction import Transaction
16
+
17
+ __all__ = [
18
+ "Property",
19
+ "Record",
20
+ "Relationship",
21
+ "RelationshipDetachOptions",
22
+ "RelationshipDirection",
23
+ "RelationshipOptions",
24
+ "RecordSearchResult",
25
+ "SearchResult",
26
+ "SearchQuery",
27
+ "RelationshipEndpointQuery",
28
+ "RelationshipSearchQuery",
29
+ "Transaction",
30
+ ]
@@ -1,4 +1,4 @@
1
- from typing import List, Literal, Optional, TypedDict, Union
1
+ from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
2
2
 
3
3
  RelationshipDirection = Literal["in", "out"]
4
4
 
@@ -7,8 +7,10 @@ class Relationship(TypedDict, total=False):
7
7
  targetLabel: str
8
8
  targetId: str
9
9
  type: str
10
+ direction: RelationshipDirection
10
11
  sourceId: str
11
12
  sourceLabel: str
13
+ properties: Dict[str, Any]
12
14
 
13
15
 
14
16
  class RelationshipOptions(TypedDict, total=False):
@@ -16,6 +18,7 @@ class RelationshipOptions(TypedDict, total=False):
16
18
 
17
19
  direction: Optional[RelationshipDirection]
18
20
  type: Optional[str]
21
+ properties: Optional[Dict[str, Any]]
19
22
 
20
23
 
21
24
  class RelationshipDetachOptions(TypedDict, total=False):
@@ -0,0 +1,37 @@
1
+ from enum import Enum
2
+ from typing import Any, Dict, List, Optional, TypedDict, Union
3
+
4
+
5
+ class OrderDirection(str, Enum):
6
+ ASC = "asc"
7
+ DESC = "desc"
8
+
9
+
10
+ class SearchQuery(TypedDict, total=False):
11
+ """TypedDict representing the query structure for finding records."""
12
+
13
+ where: Optional[Dict[str, Any]]
14
+ labels: Optional[List[str]]
15
+ skip: Optional[int]
16
+ limit: Optional[int]
17
+ orderBy: Optional[Union[Dict[str, OrderDirection], OrderDirection]]
18
+ select: Optional[Dict[str, Any]]
19
+ groupBy: Optional[List[str]]
20
+
21
+
22
+ class RelationshipEndpointQuery(TypedDict, total=False):
23
+ """Endpoint record predicates for relationship search."""
24
+
25
+ where: Optional[Dict[str, Any]]
26
+ labels: Optional[List[str]]
27
+
28
+
29
+ class RelationshipSearchQuery(TypedDict, total=False):
30
+ """TypedDict representing the query structure for finding relationship edges."""
31
+
32
+ where: Optional[Dict[str, Any]]
33
+ source: Optional[RelationshipEndpointQuery]
34
+ target: Optional[RelationshipEndpointQuery]
35
+ skip: Optional[int]
36
+ limit: Optional[int]
37
+ orderBy: Optional[Union[Dict[str, OrderDirection], OrderDirection]]
@@ -153,6 +153,53 @@ class TestCreateImport(TestBase):
153
153
  source=project, target=employee, options=detach_options
154
154
  )
155
155
 
156
+ def test_attach_with_edge_properties_and_find(self):
157
+ """Test attaching with edge properties and searching edges by them"""
158
+ movie = self.client.records.create("MOVIE", {"title": "Inception"})
159
+ actor = self.client.records.create("ACTOR", {"name": "Leonardo DiCaprio"})
160
+
161
+ self.client.records.attach(
162
+ source=movie,
163
+ target=actor,
164
+ options=RelationshipOptions(
165
+ type="STARS_IN",
166
+ direction="out",
167
+ properties={"role": "lead", "billing": 1},
168
+ ),
169
+ )
170
+
171
+ # where filters the edge (type + edge properties);
172
+ # source/target filter the endpoint records
173
+ result = self.client.relationships.find(
174
+ {
175
+ "source": {"labels": ["MOVIE"], "where": {"title": "Inception"}},
176
+ "target": {"labels": ["ACTOR"]},
177
+ "where": {"type": "STARS_IN", "role": "lead", "billing": {"$lte": 3}},
178
+ }
179
+ )
180
+
181
+ self.assertGreaterEqual(result.total, 1)
182
+ edge = next(
183
+ rel
184
+ for rel in result
185
+ if rel["sourceId"] == movie.id and rel["targetId"] == actor.id
186
+ )
187
+ self.assertEqual(edge["type"], "STARS_IN")
188
+ self.assertEqual(edge["properties"]["role"], "lead")
189
+ self.assertEqual(edge["properties"]["billing"], 1)
190
+
191
+ # Edge-property mismatch excludes the relationship
192
+ no_match = self.client.relationships.find(
193
+ {
194
+ "source": {"labels": ["MOVIE"], "where": {"title": "Inception"}},
195
+ "where": {"type": "STARS_IN", "role": "extra"},
196
+ }
197
+ )
198
+ self.assertNotIn(
199
+ (movie.id, actor.id),
200
+ [(rel["sourceId"], rel["targetId"]) for rel in no_match],
201
+ )
202
+
156
203
  def test_create_with_nested_data(self):
157
204
  """Test creating records with nested data structure"""
158
205
  data = {
@@ -0,0 +1,160 @@
1
+ from unittest.mock import Mock
2
+
3
+ from src.rushdb.api.records import RecordsAPI
4
+ from src.rushdb.api.relationship_patterns import RelationshipPatternsAPI
5
+ from src.rushdb.api.relationships import RelationsAPI
6
+
7
+
8
+ def test_relationships_exposes_patterns_namespace():
9
+ client = Mock()
10
+
11
+ relationships = RelationsAPI(client)
12
+
13
+ assert isinstance(relationships.patterns, RelationshipPatternsAPI)
14
+
15
+
16
+ def test_list_relationship_patterns():
17
+ client = Mock()
18
+ client._make_request.return_value = {
19
+ "data": {"patterns": [], "relationships": [], "analysis": {"status": "idle"}},
20
+ "success": True,
21
+ }
22
+
23
+ response = RelationsAPI(client).patterns.list()
24
+
25
+ client._make_request.assert_called_once_with("GET", "/relationships/patterns")
26
+ assert response.data["patterns"] == []
27
+ assert response.data["analysis"]["status"] == "idle"
28
+
29
+
30
+ def test_mutate_relationship_patterns():
31
+ client = Mock()
32
+ client._make_request.return_value = {"data": {"deleted": True}, "success": True}
33
+ patterns = RelationsAPI(client).patterns
34
+
35
+ patterns.analyze()
36
+ client._make_request.assert_called_with(
37
+ "POST", "/relationships/patterns/analyze", {}
38
+ )
39
+
40
+ patterns.approve("pattern-id")
41
+ client._make_request.assert_called_with(
42
+ "POST", "/relationships/patterns/pattern-id/approve", {}
43
+ )
44
+
45
+ patterns.ignore("pattern-id")
46
+ client._make_request.assert_called_with(
47
+ "POST", "/relationships/patterns/pattern-id/ignore", {}
48
+ )
49
+
50
+ patterns.delete("pattern-id", delete_existing=True)
51
+ client._make_request.assert_called_with(
52
+ "DELETE",
53
+ "/relationships/patterns/pattern-id",
54
+ params={"deleteExisting": "true"},
55
+ )
56
+
57
+
58
+ def test_relationships_find_uses_edge_where_and_endpoint_filters():
59
+ client = Mock()
60
+ edge = {
61
+ "sourceId": "user-1",
62
+ "sourceLabel": "USER",
63
+ "targetId": "order-1",
64
+ "targetLabel": "ORDER",
65
+ "type": "ORDERED",
66
+ "direction": "out",
67
+ "properties": {"confidence": 0.9},
68
+ }
69
+ client._make_request.return_value = {"data": [edge], "total": 1}
70
+
71
+ search_query = {
72
+ "source": {"labels": ["USER"], "where": {"status": "active"}},
73
+ "target": {"labels": ["ORDER"]},
74
+ "where": {"type": "ORDERED", "confidence": {"$gte": 0.8}},
75
+ "limit": 25,
76
+ }
77
+
78
+ response = RelationsAPI(client).find(search_query)
79
+
80
+ # Pagination travels in the body, same as records search.
81
+ client._make_request.assert_called_once_with(
82
+ method="POST",
83
+ path="/relationships/search",
84
+ data=search_query,
85
+ headers=None,
86
+ )
87
+ assert response.total == 1
88
+ assert list(response) == [edge]
89
+ assert response[0]["properties"]["confidence"] == 0.9
90
+
91
+
92
+ def test_relationships_find_explicit_pagination_wins_over_query_limit():
93
+ client = Mock()
94
+ client._make_request.return_value = {"data": [], "total": 0}
95
+
96
+ search_query = {"where": {"type": "ORDERED"}, "limit": 25}
97
+ RelationsAPI(client).find(search_query, pagination={"limit": 10, "skip": 5})
98
+
99
+ # The pagination argument overrides limit/skip in the body, leaving the
100
+ # caller's search_query object untouched.
101
+ client._make_request.assert_called_once_with(
102
+ method="POST",
103
+ path="/relationships/search",
104
+ data={"where": {"type": "ORDERED"}, "limit": 10, "skip": 5},
105
+ headers=None,
106
+ )
107
+ assert search_query == {"where": {"type": "ORDERED"}, "limit": 25}
108
+
109
+
110
+ def test_relationships_create_many_forwards_edge_properties():
111
+ client = Mock()
112
+ client._make_request.return_value = {"success": True}
113
+
114
+ RelationsAPI(client).create_many(
115
+ source={"label": "USER", "key": "id"},
116
+ target={"label": "ORDER", "key": "userId"},
117
+ type="ORDERED",
118
+ direction="out",
119
+ properties={"source": "import", "confidence": 0.9},
120
+ )
121
+
122
+ client._make_request.assert_called_once_with(
123
+ "POST",
124
+ "/relationships/create-many",
125
+ {
126
+ "source": {"label": "USER", "key": "id"},
127
+ "target": {"label": "ORDER", "key": "userId"},
128
+ "type": "ORDERED",
129
+ "direction": "out",
130
+ "properties": {"source": "import", "confidence": 0.9},
131
+ },
132
+ None,
133
+ )
134
+
135
+
136
+ def test_records_attach_forwards_edge_properties():
137
+ client = Mock()
138
+ client._make_request.return_value = {"success": True}
139
+
140
+ RecordsAPI(client).attach(
141
+ source="source-id",
142
+ target="target-id",
143
+ options={
144
+ "type": "RELATED_TO",
145
+ "direction": "out",
146
+ "properties": {"source": "test"},
147
+ },
148
+ )
149
+
150
+ client._make_request.assert_called_once_with(
151
+ "POST",
152
+ "/relationships/source-id",
153
+ {
154
+ "targetIds": ["target-id"],
155
+ "type": "RELATED_TO",
156
+ "direction": "out",
157
+ "properties": {"source": "test"},
158
+ },
159
+ None,
160
+ )
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 3
2
+ revision = 2
3
3
  requires-python = ">=3.8"
4
4
  resolution-markers = [
5
5
  "python_full_version >= '3.10'",
@@ -785,7 +785,7 @@ wheels = [
785
785
 
786
786
  [[package]]
787
787
  name = "rushdb"
788
- version = "2.2.0"
788
+ version = "2.3.0"
789
789
  source = { editable = "." }
790
790
  dependencies = [
791
791
  { name = "python-dotenv", version = "1.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
@@ -1,17 +0,0 @@
1
- from .property import Property
2
- from .record import Record
3
- from .relationship import RelationshipDetachOptions, RelationshipOptions
4
- from .result import RecordSearchResult, SearchResult
5
- from .search_query import SearchQuery
6
- from .transaction import Transaction
7
-
8
- __all__ = [
9
- "Property",
10
- "Record",
11
- "RelationshipDetachOptions",
12
- "RelationshipOptions",
13
- "RecordSearchResult",
14
- "SearchResult",
15
- "SearchQuery",
16
- "Transaction",
17
- ]
@@ -1,19 +0,0 @@
1
- from enum import Enum
2
- from typing import Any, Dict, List, Optional, TypedDict, Union
3
-
4
-
5
- class OrderDirection(str, Enum):
6
- ASC = "asc"
7
- DESC = "desc"
8
-
9
-
10
- class SearchQuery(TypedDict, total=False):
11
- """TypedDict representing the query structure for finding records."""
12
-
13
- where: Optional[Dict[str, Any]]
14
- labels: Optional[List[str]]
15
- skip: Optional[int]
16
- limit: Optional[int]
17
- orderBy: Optional[Union[Dict[str, OrderDirection], OrderDirection]]
18
- select: Optional[Dict[str, Any]]
19
- groupBy: Optional[List[str]]
@@ -1,54 +0,0 @@
1
- from unittest.mock import Mock
2
-
3
- from src.rushdb.api.relationship_patterns import RelationshipPatternsAPI
4
- from src.rushdb.api.relationships import RelationsAPI
5
-
6
-
7
- def test_relationships_exposes_patterns_namespace():
8
- client = Mock()
9
-
10
- relationships = RelationsAPI(client)
11
-
12
- assert isinstance(relationships.patterns, RelationshipPatternsAPI)
13
-
14
-
15
- def test_list_relationship_patterns():
16
- client = Mock()
17
- client._make_request.return_value = {
18
- "data": {"patterns": [], "relationships": [], "analysis": {"status": "idle"}},
19
- "success": True,
20
- }
21
-
22
- response = RelationsAPI(client).patterns.list()
23
-
24
- client._make_request.assert_called_once_with("GET", "/relationships/patterns")
25
- assert response.data["patterns"] == []
26
- assert response.data["analysis"]["status"] == "idle"
27
-
28
-
29
- def test_mutate_relationship_patterns():
30
- client = Mock()
31
- client._make_request.return_value = {"data": {"deleted": True}, "success": True}
32
- patterns = RelationsAPI(client).patterns
33
-
34
- patterns.analyze()
35
- client._make_request.assert_called_with(
36
- "POST", "/relationships/patterns/analyze", {}
37
- )
38
-
39
- patterns.approve("pattern-id")
40
- client._make_request.assert_called_with(
41
- "POST", "/relationships/patterns/pattern-id/approve", {}
42
- )
43
-
44
- patterns.ignore("pattern-id")
45
- client._make_request.assert_called_with(
46
- "POST", "/relationships/patterns/pattern-id/ignore", {}
47
- )
48
-
49
- patterns.delete("pattern-id", delete_existing=True)
50
- client._make_request.assert_called_with(
51
- "DELETE",
52
- "/relationships/patterns/pattern-id",
53
- params={"deleteExisting": "true"},
54
- )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes