rushdb 2.0.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 (42) hide show
  1. {rushdb-2.0.0 → rushdb-2.3.0}/PKG-INFO +12 -2
  2. {rushdb-2.0.0 → rushdb-2.3.0}/README.md +11 -1
  3. {rushdb-2.0.0 → rushdb-2.3.0}/pyproject.toml +1 -3
  4. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/__init__.py +23 -2
  5. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/__init__.py +2 -0
  6. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/records.py +93 -81
  7. rushdb-2.3.0/src/rushdb/api/relationship_patterns.py +61 -0
  8. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/relationships.py +60 -28
  9. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/client.py +2 -2
  10. rushdb-2.3.0/src/rushdb/models/__init__.py +30 -0
  11. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/models/record.py +13 -16
  12. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/models/relationship.py +4 -1
  13. rushdb-2.3.0/src/rushdb/models/search_query.py +37 -0
  14. {rushdb-2.0.0 → rushdb-2.3.0}/tests/test_create_import.py +47 -0
  15. rushdb-2.3.0/tests/test_relationship_patterns.py +160 -0
  16. {rushdb-2.0.0 → rushdb-2.3.0}/uv.lock +1 -1
  17. rushdb-2.0.0/src/rushdb/models/__init__.py +0 -17
  18. rushdb-2.0.0/src/rushdb/models/search_query.py +0 -19
  19. {rushdb-2.0.0 → rushdb-2.3.0}/.env.example +0 -0
  20. {rushdb-2.0.0 → rushdb-2.3.0}/.github/workflows/ci.yml +0 -0
  21. {rushdb-2.0.0 → rushdb-2.3.0}/.gitignore +0 -0
  22. {rushdb-2.0.0 → rushdb-2.3.0}/CODE_OF_CONDUCT.md +0 -0
  23. {rushdb-2.0.0 → rushdb-2.3.0}/CONTRIBUTING.md +0 -0
  24. {rushdb-2.0.0 → rushdb-2.3.0}/LICENSE +0 -0
  25. {rushdb-2.0.0 → rushdb-2.3.0}/rushdb-logo.svg +0 -0
  26. {rushdb-2.0.0 → rushdb-2.3.0}/src/__init__.py +0 -0
  27. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/ai.py +0 -0
  28. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/base.py +0 -0
  29. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/labels.py +0 -0
  30. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/properties.py +0 -0
  31. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/query.py +0 -0
  32. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/api/transactions.py +0 -0
  33. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/common.py +0 -0
  34. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/models/api_response.py +0 -0
  35. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/models/property.py +0 -0
  36. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/models/result.py +0 -0
  37. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/models/transaction.py +0 -0
  38. {rushdb-2.0.0 → rushdb-2.3.0}/src/rushdb/utils/token_prefix.py +0 -0
  39. {rushdb-2.0.0 → rushdb-2.3.0}/tests/__init__.py +0 -0
  40. {rushdb-2.0.0 → rushdb-2.3.0}/tests/test_base_setup.py +0 -0
  41. {rushdb-2.0.0 → rushdb-2.3.0}/tests/test_search_query.py +0 -0
  42. {rushdb-2.0.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.0.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.0.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 = [
@@ -5,14 +5,26 @@ Exposes the RushDB class.
5
5
 
6
6
  from .api.ai import AIAPI
7
7
  from .api.query import QueryAPI
8
+ from .api.records import pick_record_id
9
+ from .api.relationship_patterns import RelationshipPatternsAPI
8
10
  from .api.relationships import RelationsAPI
9
11
  from .client import RushDB
10
12
  from .common import NonUniqueResultError, RushDBError
11
13
  from .models.api_response import ApiResponse
12
14
  from .models.property import Property
13
- from .models.record import Record
14
- from .models.relationship import RelationshipDetachOptions, RelationshipOptions
15
+ from .models.record import Record, RecordTarget, RelationTarget
16
+ from .models.relationship import (
17
+ Relationship,
18
+ RelationshipDetachOptions,
19
+ RelationshipDirection,
20
+ RelationshipOptions,
21
+ )
15
22
  from .models.result import RecordSearchResult, SearchResult
23
+ from .models.search_query import (
24
+ RelationshipEndpointQuery,
25
+ RelationshipSearchQuery,
26
+ SearchQuery,
27
+ )
16
28
  from .models.transaction import Transaction
17
29
 
18
30
  __all__ = [
@@ -20,14 +32,23 @@ __all__ = [
20
32
  "RushDBError",
21
33
  "NonUniqueResultError",
22
34
  "Record",
35
+ "RecordTarget",
36
+ "RelationTarget",
37
+ "pick_record_id",
23
38
  "RecordSearchResult",
24
39
  "SearchResult",
25
40
  "Transaction",
26
41
  "Property",
42
+ "Relationship",
43
+ "RelationshipDirection",
27
44
  "RelationshipOptions",
28
45
  "RelationshipDetachOptions",
46
+ "SearchQuery",
47
+ "RelationshipEndpointQuery",
48
+ "RelationshipSearchQuery",
29
49
  "QueryAPI",
30
50
  "RelationsAPI",
51
+ "RelationshipPatternsAPI",
31
52
  "ApiResponse",
32
53
  "AIAPI",
33
54
  ]
@@ -2,6 +2,7 @@ from .labels import LabelsAPI
2
2
  from .properties import PropertiesAPI
3
3
  from .query import QueryAPI
4
4
  from .records import RecordsAPI
5
+ from .relationship_patterns import RelationshipPatternsAPI
5
6
  from .relationships import RelationsAPI
6
7
  from .transactions import TransactionsAPI
7
8
 
@@ -12,4 +13,5 @@ __all__ = [
12
13
  "TransactionsAPI",
13
14
  "QueryAPI",
14
15
  "RelationsAPI",
16
+ "RelationshipPatternsAPI",
15
17
  ]
@@ -1,7 +1,7 @@
1
1
  import typing
2
2
  from typing import Any, Dict, List, Optional, Union
3
3
 
4
- from ..models.record import Record
4
+ from ..models.record import Record, RecordTarget, RelationTarget
5
5
  from ..models.relationship import RelationshipDetachOptions, RelationshipOptions
6
6
  from ..models.result import RecordSearchResult
7
7
  from ..models.search_query import SearchQuery
@@ -9,6 +9,29 @@ from ..models.transaction import Transaction
9
9
  from .base import BaseAPI
10
10
 
11
11
 
12
+ def pick_record_id(target: RecordTarget) -> Optional[str]:
13
+ """Extract a record ID string from any RecordTarget representation.
14
+
15
+ Mirrors the JavaScript SDK's ``pickRecordId`` utility in ``src/api/utils.ts``.
16
+ Accepts a UUID string, a ``Record`` instance, or a plain dict containing
17
+ an ``__id`` field — in all cases the resolved UUID string is returned.
18
+
19
+ Args:
20
+ target: A record reference as a string ID, ``Record`` object, or dict
21
+ with an ``"__id"`` key.
22
+
23
+ Returns:
24
+ The UUID string, or ``None`` if it cannot be resolved.
25
+ """
26
+ if isinstance(target, str):
27
+ return target
28
+ elif isinstance(target, Record):
29
+ return target.data.get("__id")
30
+ elif isinstance(target, dict):
31
+ return target.get("__id")
32
+ return None
33
+
34
+
12
35
  def _is_flat(obj: Any) -> bool:
13
36
  """Return True if obj is a dict with no nested dict/list values."""
14
37
  return isinstance(obj, dict) and not any(
@@ -55,7 +78,7 @@ class RecordsAPI(BaseAPI):
55
78
 
56
79
  def set(
57
80
  self,
58
- record_id: str,
81
+ target: RecordTarget,
59
82
  data: Dict[str, Any],
60
83
  label: Optional[str] = None,
61
84
  vectors: Optional[List[Dict[str, Any]]] = None,
@@ -67,7 +90,8 @@ class RecordsAPI(BaseAPI):
67
90
  removing any existing fields that are not present in the new data.
68
91
 
69
92
  Args:
70
- record_id (str): The unique identifier of the record to update.
93
+ target (RecordTarget): The record to update. Accepts a UUID string,
94
+ a ``Record`` instance, or a dict containing an ``"__id"`` field.
71
95
  data (Dict[str, Any]): The new data to replace the existing record data.
72
96
  label (Optional[str]): Optional label to re-assign to the record.
73
97
  vectors (Optional[List[Dict[str, Any]]]): Optional pre-computed embedding vectors
@@ -81,9 +105,10 @@ class RecordsAPI(BaseAPI):
81
105
  Dict[str, str]: Response from the server containing operation status.
82
106
 
83
107
  Raises:
84
- ValueError: If the record_id is invalid or empty.
108
+ ValueError: If the target cannot be resolved to a valid record ID.
85
109
  RequestError: If the server request fails.
86
110
  """
111
+ record_id = pick_record_id(target)
87
112
  headers = Transaction._build_transaction_header(transaction)
88
113
  payload: Dict[str, Any] = {"data": data}
89
114
  if label is not None:
@@ -96,7 +121,7 @@ class RecordsAPI(BaseAPI):
96
121
 
97
122
  def update(
98
123
  self,
99
- record_id: str,
124
+ target: RecordTarget,
100
125
  data: Dict[str, Any],
101
126
  transaction: Optional[Transaction] = None,
102
127
  ) -> Dict[str, str]:
@@ -106,7 +131,8 @@ class RecordsAPI(BaseAPI):
106
131
  with existing record data without removing existing fields.
107
132
 
108
133
  Args:
109
- record_id (str): The unique identifier of the record to update.
134
+ target (RecordTarget): The record to update. Accepts a UUID string,
135
+ a ``Record`` instance, or a dict containing an ``"__id"`` field.
110
136
  data (Dict[str, Any]): The data to merge with the existing record data.
111
137
  transaction (Optional[Transaction], optional): Transaction context for the operation.
112
138
  If provided, the operation will be part of the transaction. Defaults to None.
@@ -115,9 +141,10 @@ class RecordsAPI(BaseAPI):
115
141
  Dict[str, str]: Response from the server containing operation status.
116
142
 
117
143
  Raises:
118
- ValueError: If the record_id is invalid or empty.
144
+ ValueError: If the target cannot be resolved to a valid record ID.
119
145
  RequestError: If the server request fails.
120
146
  """
147
+ record_id = pick_record_id(target)
121
148
  headers = Transaction._build_transaction_header(transaction)
122
149
 
123
150
  return self.client._make_request(
@@ -383,15 +410,8 @@ class RecordsAPI(BaseAPI):
383
410
 
384
411
  def attach(
385
412
  self,
386
- source: Union[str, Dict[str, Any]],
387
- target: Union[
388
- str,
389
- List[str],
390
- Dict[str, Any],
391
- List[Dict[str, Any]],
392
- "Record",
393
- List["Record"],
394
- ],
413
+ source: RecordTarget,
414
+ target: RelationTarget,
395
415
  options: Optional[RelationshipOptions] = None,
396
416
  transaction: Optional[Transaction] = None,
397
417
  ) -> Dict[str, str]:
@@ -402,16 +422,11 @@ class RecordsAPI(BaseAPI):
402
422
  record dictionaries, or Record objects.
403
423
 
404
424
  Args:
405
- source (Union[str, Dict[str, Any]]): The source record to attach targets to.
406
- Can be a record ID string or a record dictionary containing '__id'.
407
- target (Union[str, List[str], Dict[str, Any], List[Dict[str, Any]], Record, List[Record]]):
408
- The target record(s) to attach to the source. Accepts multiple formats:
409
- - Single record ID (str)
410
- - List of record IDs (List[str])
411
- - Record dictionary with '__id' field
412
- - List of record dictionaries
413
- - Record object
414
- - List of Record objects
425
+ source (RecordTarget): The source record to attach targets to.
426
+ Accepts a UUID string, a ``Record`` instance, or a dict with ``"__id"``.
427
+ target (RelationTarget): The target record(s) to attach to the source.
428
+ Accepts a single or list of: UUID string, ``Record`` instance, or
429
+ dict with ``"__id"``.
415
430
  options (Optional[RelationshipOptions], optional): Additional options for the relationship.
416
431
  Defaults to None.
417
432
  transaction (Optional[Transaction], optional): Transaction context for the operation.
@@ -431,6 +446,17 @@ class RecordsAPI(BaseAPI):
431
446
  >>>
432
447
  >>> # Attach using Record objects
433
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
+ ... )
434
460
  """
435
461
  headers = Transaction._build_transaction_header(transaction)
436
462
 
@@ -445,15 +471,8 @@ class RecordsAPI(BaseAPI):
445
471
 
446
472
  def detach(
447
473
  self,
448
- source: Union[str, Dict[str, Any]],
449
- target: Union[
450
- str,
451
- List[str],
452
- Dict[str, Any],
453
- List[Dict[str, Any]],
454
- "Record",
455
- List["Record"],
456
- ],
474
+ source: RecordTarget,
475
+ target: RelationTarget,
457
476
  options: Optional[RelationshipDetachOptions] = None,
458
477
  transaction: Optional[Transaction] = None,
459
478
  ) -> Dict[str, str]:
@@ -464,16 +483,11 @@ class RecordsAPI(BaseAPI):
464
483
  record dictionaries, or Record objects.
465
484
 
466
485
  Args:
467
- source (Union[str, Dict[str, Any]]): The source record to detach targets from.
468
- Can be a record ID string or a record dictionary containing '__id'.
469
- target (Union[str, List[str], Dict[str, Any], List[Dict[str, Any]], Record, List[Record]]):
470
- The target record(s) to detach from the source. Accepts multiple formats:
471
- - Single record ID (str)
472
- - List of record IDs (List[str])
473
- - Record dictionary with '__id' field
474
- - List of record dictionaries
475
- - Record object
476
- - List of Record objects
486
+ source (RecordTarget): The source record to detach targets from.
487
+ Accepts a UUID string, a ``Record`` instance, or a dict with ``"__id"``.
488
+ target (RelationTarget): The target record(s) to detach from the source.
489
+ Accepts a single or list of: UUID string, ``Record`` instance, or
490
+ dict with ``"__id"``.
477
491
  options (Optional[RelationshipDetachOptions], optional): Additional options for the detach operation.
478
492
  Defaults to None.
479
493
  transaction (Optional[Transaction], optional): Transaction context for the operation.
@@ -550,7 +564,7 @@ class RecordsAPI(BaseAPI):
550
564
 
551
565
  def delete_by_id(
552
566
  self,
553
- id_or_ids: Union[str, List[str]],
567
+ target: Union[RecordTarget, List[RecordTarget]],
554
568
  transaction: Optional[Transaction] = None,
555
569
  ) -> Dict[str, str]:
556
570
  """Delete one or more records by their unique identifiers.
@@ -559,8 +573,9 @@ class RecordsAPI(BaseAPI):
559
573
  deletion and bulk deletion of multiple records.
560
574
 
561
575
  Args:
562
- id_or_ids (Union[str, List[str]]): The record identifier(s) to delete.
563
- Can be a single record ID string or a list of record ID strings.
576
+ target (Union[RecordTarget, List[RecordTarget]]): The record(s) to delete.
577
+ Accepts a UUID string, a ``Record`` instance, a dict containing
578
+ ``"__id"``, or a list of any of those forms.
564
579
  transaction (Optional[Transaction], optional): Transaction context for the operation.
565
580
  If provided, the operation will be part of the transaction. Defaults to None.
566
581
 
@@ -569,39 +584,42 @@ class RecordsAPI(BaseAPI):
569
584
  information about the deletion operation.
570
585
 
571
586
  Raises:
572
- ValueError: If any of the provided IDs are invalid or empty.
587
+ ValueError: If any target cannot be resolved to a valid record ID.
573
588
  RequestError: If the server request fails.
574
589
 
575
590
  Note:
576
- When deleting multiple records (list of IDs), the operation uses a
591
+ When deleting multiple records (list), the operation uses a
577
592
  batch delete with a limit of 1000 records. For single record deletion,
578
593
  it uses a direct DELETE request.
579
594
 
580
595
  Example:
581
596
  >>> records_api = RecordsAPI(client)
582
- >>> # Delete a single record
597
+ >>> # Delete a single record — string, Record object, or dict all work
583
598
  >>> response = records_api.delete_by_id("record_123")
599
+ >>> response = records_api.delete_by_id(my_record)
584
600
  >>>
585
601
  >>> # Delete multiple records
586
- >>> record_ids = ["record_123", "record_456", "record_789"]
587
- >>> response = records_api.delete_by_id(record_ids)
602
+ >>> response = records_api.delete_by_id(["record_123", "record_456"])
603
+ >>> response = records_api.delete_by_id([record_a, record_b])
588
604
  """
589
605
  headers = Transaction._build_transaction_header(transaction)
590
606
 
591
- if isinstance(id_or_ids, list):
607
+ if isinstance(target, list):
608
+ ids = self._extract_target_ids(target)
592
609
  return self.client._make_request(
593
610
  "POST",
594
611
  "/records/delete",
595
- {"limit": 1000, "where": {"$id": {"$in": id_or_ids}}},
612
+ {"limit": 1000, "where": {"$id": {"$in": ids}}},
596
613
  headers,
597
614
  )
615
+ record_id = pick_record_id(target)
598
616
  return self.client._make_request(
599
- "DELETE", f"/records/{id_or_ids}", None, headers
617
+ "DELETE", f"/records/{record_id}", None, headers
600
618
  )
601
619
 
602
620
  def find_by_id(
603
621
  self,
604
- id_or_ids: Union[str, List[str]],
622
+ target: Union[RecordTarget, List[RecordTarget]],
605
623
  transaction: Optional[Transaction] = None,
606
624
  ) -> Union["Record", RecordSearchResult]:
607
625
  """Retrieve one or more records by their unique identifiers.
@@ -609,17 +627,19 @@ class RecordsAPI(BaseAPI):
609
627
  Mirrors the TypeScript SDK ``records.findById`` method.
610
628
 
611
629
  Args:
612
- id_or_ids: A single record ID string or a list of ID strings.
630
+ target: A single record reference (UUID string, ``Record`` instance, or
631
+ dict with ``"__id"``), or a list of any of those forms.
613
632
  transaction: Optional transaction context for the operation.
614
633
 
615
634
  Returns:
616
- A single :class:`Record` when ``id_or_ids`` is a string, or a
617
- :class:`RecordSearchResult` when ``id_or_ids`` is a list.
635
+ A single :class:`Record` when ``target`` is a single reference, or a
636
+ :class:`RecordSearchResult` when ``target`` is a list.
618
637
  """
619
638
  headers = Transaction._build_transaction_header(transaction)
620
- if isinstance(id_or_ids, list):
639
+ if isinstance(target, list):
640
+ ids = self._extract_target_ids(target)
621
641
  response = self.client._make_request(
622
- "POST", "/records", {"ids": id_or_ids}, headers
642
+ "POST", "/records", {"ids": ids}, headers
623
643
  )
624
644
  records = [Record(self.client, r) for r in (response.get("data") or [])]
625
645
  return RecordSearchResult(
@@ -627,21 +647,25 @@ class RecordsAPI(BaseAPI):
627
647
  total=response.get("total", len(records)),
628
648
  client=self.client,
629
649
  )
650
+ record_id = pick_record_id(target)
630
651
  response = self.client._make_request(
631
- "GET", f"/records/{id_or_ids}", None, headers
652
+ "GET", f"/records/{record_id}", None, headers
632
653
  )
633
654
  return Record(self.client, response.get("data", response))
634
655
 
635
656
  def find(
636
657
  self,
637
658
  search_query: Optional[SearchQuery] = None,
638
- record_id: Optional[str] = None,
659
+ record_id: Optional[RecordTarget] = None,
639
660
  transaction: Optional[Transaction] = None,
640
661
  ) -> RecordSearchResult:
641
662
  try:
642
663
  headers = Transaction._build_transaction_header(transaction)
643
664
 
644
- path = f"/records/{record_id}/search" if record_id else "/records/search"
665
+ resolved_id = pick_record_id(record_id) if record_id is not None else None
666
+ path = (
667
+ f"/records/{resolved_id}/search" if resolved_id else "/records/search"
668
+ )
645
669
  response = self.client._make_request(
646
670
  "POST",
647
671
  path,
@@ -825,14 +849,7 @@ class RecordsAPI(BaseAPI):
825
849
 
826
850
  @staticmethod
827
851
  def _extract_target_ids(
828
- target: Union[
829
- str,
830
- List[str],
831
- Dict[str, Any],
832
- List[Dict[str, Any]],
833
- "Record",
834
- List["Record"],
835
- ],
852
+ target: Union[RecordTarget, List[RecordTarget]],
836
853
  ) -> List[str]:
837
854
  """Extract record IDs from various input types and formats.
838
855
 
@@ -841,14 +858,9 @@ class RecordsAPI(BaseAPI):
841
858
  types commonly used throughout the API for specifying target records.
842
859
 
843
860
  Args:
844
- target (Union[str, List[str], Dict[str, Any], List[Dict[str, Any]], Record, List[Record]]):
845
- The target input to extract IDs from. Supported formats:
846
- - str: Single record ID
847
- - List[str]: List of record IDs
848
- - Dict[str, Any]: Record dictionary containing '__id' field
849
- - List[Dict[str, Any]]: List of record dictionaries with '__id' fields
850
- - Record: Record object with data containing '__id'
851
- - List[Record]: List of Record objects
861
+ target (Union[RecordTarget, List[RecordTarget]]): The target input to
862
+ extract IDs from. Accepts a UUID string, a ``Record`` instance, a
863
+ dict with ``"__id"``, or a list of any of those forms.
852
864
 
853
865
  Returns:
854
866
  List[str]: List of extracted record ID strings.
@@ -0,0 +1,61 @@
1
+ """Relationship pattern suggestions API for the RushDB Python SDK."""
2
+
3
+ from typing import TYPE_CHECKING, Any, Dict
4
+
5
+ from ..models.api_response import ApiResponse
6
+ from .base import BaseAPI
7
+
8
+ if TYPE_CHECKING:
9
+ from ..client import RushDB
10
+
11
+
12
+ class RelationshipPatternsAPI(BaseAPI):
13
+ """Review and manage relationship patterns inferred from project ontology.
14
+
15
+ Accessed via ``db.relationships.patterns``.
16
+ """
17
+
18
+ def __init__(self, client: "RushDB"):
19
+ super().__init__(client)
20
+
21
+ @staticmethod
22
+ def _wrap(response: Dict[str, Any]) -> ApiResponse:
23
+ return ApiResponse(
24
+ data=response.get("data"),
25
+ success=response.get("success", True),
26
+ total=response.get("total"),
27
+ )
28
+
29
+ def list(self) -> ApiResponse:
30
+ """List inferred patterns, ontology relationships, and analysis status."""
31
+ response = self.client._make_request("GET", "/relationships/patterns")
32
+ return self._wrap(response)
33
+
34
+ def analyze(self) -> ApiResponse:
35
+ """Queue ontology analysis to generate relationship pattern suggestions."""
36
+ response = self.client._make_request(
37
+ "POST", "/relationships/patterns/analyze", {}
38
+ )
39
+ return self._wrap(response)
40
+
41
+ def approve(self, pattern_id: str) -> ApiResponse:
42
+ """Approve and apply a suggested relationship pattern."""
43
+ response = self.client._make_request(
44
+ "POST", f"/relationships/patterns/{pattern_id}/approve", {}
45
+ )
46
+ return self._wrap(response)
47
+
48
+ def ignore(self, pattern_id: str) -> ApiResponse:
49
+ """Ignore a suggested relationship pattern without applying it."""
50
+ response = self.client._make_request(
51
+ "POST", f"/relationships/patterns/{pattern_id}/ignore", {}
52
+ )
53
+ return self._wrap(response)
54
+
55
+ def delete(self, pattern_id: str, *, delete_existing: bool = False) -> ApiResponse:
56
+ """Delete a saved pattern and optionally its materialized relationships."""
57
+ params = {"deleteExisting": "true"} if delete_existing else None
58
+ response = self.client._make_request(
59
+ "DELETE", f"/relationships/patterns/{pattern_id}", params=params
60
+ )
61
+ return self._wrap(response)
@@ -1,11 +1,15 @@
1
1
  import typing
2
- from typing import 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
+ from .relationship_patterns import RelationshipPatternsAPI
10
+
11
+ if TYPE_CHECKING:
12
+ from ..client import RushDB
9
13
 
10
14
 
11
15
  class PaginationParams(TypedDict, total=False):
@@ -43,59 +47,78 @@ class RelationsAPI(BaseAPI):
43
47
  Attributes:
44
48
  client: The underlying RushDB client instance for making HTTP requests.
45
49
 
46
- Note:
47
- This API currently contains async methods. Ensure you're using it in an
48
- async context or consider updating to sync methods if needed.
49
-
50
50
  Example:
51
51
  >>> from rushdb import RushDB
52
52
  >>> client = RushDB(api_key="your_api_key")
53
53
  >>> relations_api = client.relationships
54
54
  >>>
55
55
  >>> # Find all relationships
56
- >>> 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"}})
57
60
  >>>
58
61
  >>> # Find relationships with pagination
59
- >>> pagination = {"limit": 50, "skip": 0}
60
- >>> page_1 = await relations_api.find(pagination=pagination)
62
+ >>> page_1 = relations_api.find(pagination={"limit": 50, "skip": 0})
61
63
  """
62
64
 
65
+ def __init__(self, client: "RushDB"):
66
+ super().__init__(client)
67
+ self.patterns = RelationshipPatternsAPI(client)
68
+
63
69
  def find(
64
70
  self,
65
- search_query: Optional[SearchQuery] = None,
71
+ search_query: Optional[RelationshipSearchQuery] = None,
66
72
  pagination: Optional[PaginationParams] = None,
67
73
  transaction: Optional[Union[Transaction, str]] = None,
68
- ) -> List[Relationship]:
74
+ ) -> SearchResult[Relationship]:
69
75
  """Search for and retrieve relationships matching the specified criteria.
70
76
 
71
77
  Args:
72
- 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.
73
85
  If None, returns all relationships (subject to pagination limits).
74
- 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.
75
90
  transaction: Optional transaction context.
76
91
 
77
92
  Returns:
78
- 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``.
79
97
 
80
98
  Raises:
81
99
  RushDBError: If the server request fails.
82
100
 
83
101
  Example:
84
102
  >>> relations_api = RelationsAPI(client)
85
- >>> all_rels = relations_api.find()
86
- >>> 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'
87
113
  """
88
- # Build query string for pagination
89
- 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 {})
90
117
  if pagination:
91
118
  if pagination.get("limit") is not None:
92
- query_params["limit"] = str(pagination["limit"])
119
+ payload["limit"] = pagination["limit"]
93
120
  if pagination.get("skip") is not None:
94
- query_params["skip"] = str(pagination["skip"])
95
-
96
- # Construct path with query string
97
- query_string = f"?{urlencode(query_params)}" if query_params else ""
98
- path = f"/relationships/search{query_string}"
121
+ payload["skip"] = pagination["skip"]
99
122
 
100
123
  # Build headers with transaction if present
101
124
  headers = Transaction._build_transaction_header(transaction)
@@ -103,12 +126,17 @@ class RelationsAPI(BaseAPI):
103
126
  # Make request
104
127
  response = self.client._make_request(
105
128
  method="POST",
106
- path=path,
107
- data=typing.cast(typing.Dict[str, typing.Any], search_query or {}),
129
+ path="/relationships/search",
130
+ data=payload,
108
131
  headers=headers,
109
132
  )
110
133
 
111
- 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
+ )
112
140
 
113
141
  def create_many(
114
142
  self,
@@ -117,6 +145,7 @@ class RelationsAPI(BaseAPI):
117
145
  target: dict,
118
146
  type: Optional[str] = None,
119
147
  direction: Optional[str] = None,
148
+ properties: Optional[dict] = None,
120
149
  many_to_many: Optional[bool] = None,
121
150
  transaction: Optional[Union[Transaction, str]] = None,
122
151
  ) -> dict:
@@ -131,6 +160,7 @@ class RelationsAPI(BaseAPI):
131
160
  target (dict): { label: str, key?: str, where?: dict }
132
161
  type (str, optional): Relationship type override.
133
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.
134
164
  many_to_many (bool, optional): Enable cartesian mode (requires filters, disallows keys).
135
165
  transaction (Transaction|str, optional): Transaction context.
136
166
 
@@ -143,6 +173,8 @@ class RelationsAPI(BaseAPI):
143
173
  payload["type"] = type
144
174
  if direction:
145
175
  payload["direction"] = direction
176
+ if properties is not None:
177
+ payload["properties"] = properties
146
178
  if many_to_many is not None:
147
179
  payload["manyToMany"] = many_to_many
148
180
  return self.client._make_request(
@@ -34,7 +34,7 @@ class _SettingsNamespace:
34
34
  Returns:
35
35
  Dict[str, Any]: Current project settings object.
36
36
  """
37
- response = self._client._make_request("GET", "/settings")
37
+ response = self._client._make_request("GET", "/sdk/settings")
38
38
  return response.get("data", response)
39
39
 
40
40
 
@@ -288,7 +288,7 @@ class RushDB:
288
288
  ... return client
289
289
  """
290
290
  try:
291
- self._make_request("GET", "/settings")
291
+ self._make_request("GET", "/sdk/settings")
292
292
  return True
293
293
  except RushDBError:
294
294
  return False
@@ -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
+ ]
@@ -7,6 +7,17 @@ from .transaction import Transaction
7
7
  if TYPE_CHECKING:
8
8
  from ..client import RushDB
9
9
 
10
+ # RecordTarget mirrors the JS SDK's DBRecordTarget type:
11
+ # a record can be identified by a UUID string, a dict containing "__id", or a Record instance.
12
+ RecordTarget = Union[str, Dict[str, Any], "Record"]
13
+
14
+ # RelationTarget mirrors the JS SDK's RelationTarget type:
15
+ # one or more records used as the target in attach/detach operations.
16
+ RelationTarget = Union[
17
+ "RecordTarget",
18
+ List["RecordTarget"],
19
+ ]
20
+
10
21
 
11
22
  class Record:
12
23
  """Represents a record in RushDB with methods for manipulation."""
@@ -74,14 +85,7 @@ class Record:
74
85
 
75
86
  def attach(
76
87
  self,
77
- target: Union[
78
- str,
79
- List[str],
80
- Dict[str, Any],
81
- List[Dict[str, Any]],
82
- "Record",
83
- List["Record"],
84
- ],
88
+ target: "RelationTarget",
85
89
  options: Optional[RelationshipOptions] = None,
86
90
  transaction: Optional[Transaction] = None,
87
91
  ) -> Dict[str, str]:
@@ -90,14 +94,7 @@ class Record:
90
94
 
91
95
  def detach(
92
96
  self,
93
- target: Union[
94
- str,
95
- List[str],
96
- Dict[str, Any],
97
- List[Dict[str, Any]],
98
- "Record",
99
- List["Record"],
100
- ],
97
+ target: "RelationTarget",
101
98
  options: Optional[RelationshipDetachOptions] = None,
102
99
  transaction: Optional[Transaction] = None,
103
100
  ) -> Dict[str, str]:
@@ -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
+ )
@@ -785,7 +785,7 @@ wheels = [
785
785
 
786
786
  [[package]]
787
787
  name = "rushdb"
788
- version = "2.0.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]]
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