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.
Files changed (43) hide show
  1. {rushdb-2.2.0 → rushdb-2.6.0}/PKG-INFO +14 -3
  2. {rushdb-2.2.0 → rushdb-2.6.0}/README.md +13 -2
  3. {rushdb-2.2.0 → rushdb-2.6.0}/pyproject.toml +1 -3
  4. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/__init__.py +16 -1
  5. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/ai.py +12 -12
  6. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/records.py +16 -1
  7. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/relationship_patterns.py +3 -3
  8. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/relationships.py +52 -28
  9. rushdb-2.6.0/src/rushdb/models/__init__.py +30 -0
  10. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/models/api_response.py +1 -1
  11. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/models/property.py +3 -2
  12. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/models/relationship.py +4 -1
  13. rushdb-2.6.0/src/rushdb/models/search_query.py +37 -0
  14. {rushdb-2.2.0 → rushdb-2.6.0}/tests/test_create_import.py +47 -0
  15. rushdb-2.6.0/tests/test_relationship_patterns.py +160 -0
  16. {rushdb-2.2.0 → rushdb-2.6.0}/uv.lock +48 -48
  17. rushdb-2.2.0/src/rushdb/models/__init__.py +0 -17
  18. rushdb-2.2.0/src/rushdb/models/search_query.py +0 -19
  19. rushdb-2.2.0/tests/test_relationship_patterns.py +0 -54
  20. {rushdb-2.2.0 → rushdb-2.6.0}/.env.example +0 -0
  21. {rushdb-2.2.0 → rushdb-2.6.0}/.github/workflows/ci.yml +0 -0
  22. {rushdb-2.2.0 → rushdb-2.6.0}/.gitignore +0 -0
  23. {rushdb-2.2.0 → rushdb-2.6.0}/CODE_OF_CONDUCT.md +0 -0
  24. {rushdb-2.2.0 → rushdb-2.6.0}/CONTRIBUTING.md +0 -0
  25. {rushdb-2.2.0 → rushdb-2.6.0}/LICENSE +0 -0
  26. {rushdb-2.2.0 → rushdb-2.6.0}/rushdb-logo.svg +0 -0
  27. {rushdb-2.2.0 → rushdb-2.6.0}/src/__init__.py +0 -0
  28. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/__init__.py +0 -0
  29. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/base.py +0 -0
  30. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/labels.py +0 -0
  31. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/properties.py +0 -0
  32. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/query.py +0 -0
  33. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/api/transactions.py +0 -0
  34. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/client.py +0 -0
  35. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/common.py +0 -0
  36. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/models/record.py +0 -0
  37. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/models/result.py +0 -0
  38. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/models/transaction.py +0 -0
  39. {rushdb-2.2.0 → rushdb-2.6.0}/src/rushdb/utils/token_prefix.py +0 -0
  40. {rushdb-2.2.0 → rushdb-2.6.0}/tests/__init__.py +0 -0
  41. {rushdb-2.2.0 → rushdb-2.6.0}/tests/test_base_setup.py +0 -0
  42. {rushdb-2.2.0 → rushdb-2.6.0}/tests/test_search_query.py +0 -0
  43. {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.2.0
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(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
  ---
@@ -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
- options={'returnResult': True, 'suggestTypes': True},
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(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
  ---
@@ -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
- options={'returnResult': True, 'suggestTypes': True},
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.2.0"
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 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",
@@ -1,6 +1,6 @@
1
1
  """AI API for RushDB Python SDK.
2
2
 
3
- Provides methods for graph ontology exploration, semantic vector search,
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
- >>> ontology = db.ai.get_ontology()
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 get_ontology(
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 ontology as structured JSON.
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 ontology to specific labels only. Pass ``{"force": True}``
153
- to bypass the 1-hour ontology cache and trigger a full
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 ontology items.
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/ontology", params or {}, headers=headers or None
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 get_ontology_markdown(
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 ontology as compact Markdown tables.
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 ontology cache and trigger a full
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/ontology/md", params or {}, headers=headers or None
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``, ``mergeBy``, ``mergeStrategy``, etc.).
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 ontology.
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, ontology relationships, and analysis status."""
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 ontology analysis to generate relationship pattern suggestions."""
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, 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
+ ]
@@ -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.get_ontology_markdown()
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
- PropertyType = Literal["boolean", "datetime", "null", "number", "string"]
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" }, 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'" },
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 == '3.9.*'" },
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", 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'" },
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 = "python_full_version < '3.10' and sys_platform == 'win32'" },
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 = "python_full_version >= '3.10' and sys_platform == 'win32'" },
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", marker = "python_full_version < '3.9'" },
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 >= '3.9' and python_full_version <= '3.11'" },
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.13'" },
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", 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'" },
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", 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'" },
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 = "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'" },
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 = "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'" },
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"], 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'" },
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"], 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'" },
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.2.0"
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" }, marker = "python_full_version < '3.9'" },
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" }, marker = "python_full_version >= '3.9'" },
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