rushdb 3.1.0__tar.gz → 3.2.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 (49) hide show
  1. {rushdb-3.1.0 → rushdb-3.2.0}/PKG-INFO +9 -2
  2. {rushdb-3.1.0 → rushdb-3.2.0}/README.md +8 -1
  3. {rushdb-3.1.0 → rushdb-3.2.0}/pyproject.toml +1 -1
  4. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/api/relationships.py +56 -0
  5. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/direct/router.py +34 -0
  6. {rushdb-3.1.0 → rushdb-3.2.0}/tests/test_direct_mode.py +36 -0
  7. {rushdb-3.1.0 → rushdb-3.2.0}/tests/test_direct_mode_live.py +21 -0
  8. {rushdb-3.1.0 → rushdb-3.2.0}/uv.lock +1 -1
  9. {rushdb-3.1.0 → rushdb-3.2.0}/.env.example +0 -0
  10. {rushdb-3.1.0 → rushdb-3.2.0}/.github/workflows/ci.yml +0 -0
  11. {rushdb-3.1.0 → rushdb-3.2.0}/.gitignore +0 -0
  12. {rushdb-3.1.0 → rushdb-3.2.0}/CODE_OF_CONDUCT.md +0 -0
  13. {rushdb-3.1.0 → rushdb-3.2.0}/CONTRIBUTING.md +0 -0
  14. {rushdb-3.1.0 → rushdb-3.2.0}/LICENSE +0 -0
  15. {rushdb-3.1.0 → rushdb-3.2.0}/rushdb-logo.svg +0 -0
  16. {rushdb-3.1.0 → rushdb-3.2.0}/src/__init__.py +0 -0
  17. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/__init__.py +0 -0
  18. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/api/__init__.py +0 -0
  19. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/api/ai.py +0 -0
  20. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/api/base.py +0 -0
  21. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/api/labels.py +0 -0
  22. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/api/properties.py +0 -0
  23. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/api/query.py +0 -0
  24. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/api/records.py +0 -0
  25. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/api/relationship_patterns.py +0 -0
  26. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/api/text_indexes.py +0 -0
  27. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/api/transactions.py +0 -0
  28. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/client.py +0 -0
  29. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/common.py +0 -0
  30. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/direct/__init__.py +0 -0
  31. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/direct/capabilities.py +0 -0
  32. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/direct/engine.py +0 -0
  33. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/direct/schema.py +0 -0
  34. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/models/__init__.py +0 -0
  35. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/models/api_response.py +0 -0
  36. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/models/property.py +0 -0
  37. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/models/record.py +0 -0
  38. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/models/relationship.py +0 -0
  39. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/models/result.py +0 -0
  40. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/models/search_query.py +0 -0
  41. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/models/transaction.py +0 -0
  42. {rushdb-3.1.0 → rushdb-3.2.0}/src/rushdb/utils/token_prefix.py +0 -0
  43. {rushdb-3.1.0 → rushdb-3.2.0}/tests/__init__.py +0 -0
  44. {rushdb-3.1.0 → rushdb-3.2.0}/tests/test_ai_vector_search.py +0 -0
  45. {rushdb-3.1.0 → rushdb-3.2.0}/tests/test_base_setup.py +0 -0
  46. {rushdb-3.1.0 → rushdb-3.2.0}/tests/test_create_import.py +0 -0
  47. {rushdb-3.1.0 → rushdb-3.2.0}/tests/test_relationship_patterns.py +0 -0
  48. {rushdb-3.1.0 → rushdb-3.2.0}/tests/test_search_query.py +0 -0
  49. {rushdb-3.1.0 → rushdb-3.2.0}/tests/test_search_result.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: rushdb
3
- Version: 3.1.0
3
+ Version: 3.2.0
4
4
  Summary: RushDB SDK for Python: the client for RushDB, the database for AI (RushDB Cloud or self-hosted)
5
5
  Project-URL: Homepage, https://github.com/rush-db/rushdb-python
6
6
  Project-URL: Repository, https://github.com/rush-db/rushdb-python
@@ -93,7 +93,7 @@ What each offers:
93
93
  | Feature | Cloud | Self-hosted engine |
94
94
  | --- | :---: | :---: |
95
95
  | Records: create, read, update, set, upsert, delete, `find` with `where`, `orderBy`, `select`, `groupBy` | ✓ | ✓ |
96
- | Relationships: `attach`/`detach`, `relationships.create_many`/`delete_many`/`find` | ✓ | ✓ |
96
+ | Relationships: `attach`/`detach`, `relationships.create_many`/`delete_many`/`find`/`update` | ✓ | ✓ |
97
97
  | Labels, properties, property values | ✓ | ✓ |
98
98
  | Import JSON and CSV, export CSV | ✓ | ✓ |
99
99
  | External vector indexes; vector search with a `queryVector` you supply | ✓ | ✓ |
@@ -227,6 +227,13 @@ relationships = db.relationships.find({
227
227
  'target': {'labels': ['COMPANY']},
228
228
  'where': {'type': 'WORKS_AT', 'source': 'profile'},
229
229
  })
230
+
231
+ # Change properties on existing relationships (RushDB 3.0.4+): set writes, unset removes
232
+ db.relationships.update(user, company, type='WORKS_AT', set={'source': 'hr'}, unset=['legacy'])
233
+ # types marks values JSON can't type: 'datetime' stores an ISO string as a datetime
234
+ db.relationships.update(
235
+ user, company, type='WORKS_AT', set={'since': '2024-03-01'}, types={'since': 'datetime'}
236
+ )
230
237
  ```
231
238
 
232
239
  ---
@@ -79,7 +79,7 @@ What each offers:
79
79
  | Feature | Cloud | Self-hosted engine |
80
80
  | --- | :---: | :---: |
81
81
  | Records: create, read, update, set, upsert, delete, `find` with `where`, `orderBy`, `select`, `groupBy` | ✓ | ✓ |
82
- | Relationships: `attach`/`detach`, `relationships.create_many`/`delete_many`/`find` | ✓ | ✓ |
82
+ | Relationships: `attach`/`detach`, `relationships.create_many`/`delete_many`/`find`/`update` | ✓ | ✓ |
83
83
  | Labels, properties, property values | ✓ | ✓ |
84
84
  | Import JSON and CSV, export CSV | ✓ | ✓ |
85
85
  | External vector indexes; vector search with a `queryVector` you supply | ✓ | ✓ |
@@ -213,6 +213,13 @@ relationships = db.relationships.find({
213
213
  'target': {'labels': ['COMPANY']},
214
214
  'where': {'type': 'WORKS_AT', 'source': 'profile'},
215
215
  })
216
+
217
+ # Change properties on existing relationships (RushDB 3.0.4+): set writes, unset removes
218
+ db.relationships.update(user, company, type='WORKS_AT', set={'source': 'hr'}, unset=['legacy'])
219
+ # types marks values JSON can't type: 'datetime' stores an ISO string as a datetime
220
+ db.relationships.update(
221
+ user, company, type='WORKS_AT', set={'since': '2024-03-01'}, types={'since': 'datetime'}
222
+ )
216
223
  ```
217
224
 
218
225
  ---
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rushdb"
3
- version = "3.1.0"
3
+ version = "3.2.0"
4
4
  description = "RushDB SDK for Python: the client for RushDB, the database for AI (RushDB Cloud or self-hosted)"
5
5
  authors = [
6
6
  {name = "RushDB Team", email = "hi@rushdb.com"}
@@ -10,6 +10,7 @@ from .relationship_patterns import RelationshipPatternsAPI
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from ..client import RushDB
13
+ from ..models.record import RecordTarget, RelationTarget
13
14
 
14
15
 
15
16
  class PaginationParams(TypedDict, total=False):
@@ -181,6 +182,61 @@ class RelationsAPI(BaseAPI):
181
182
  "POST", "/relationships/create-many", payload, headers
182
183
  )
183
184
 
185
+ def update(
186
+ self,
187
+ source: "RecordTarget",
188
+ target: "RelationTarget",
189
+ *,
190
+ type: str,
191
+ direction: Optional[str] = None,
192
+ set: Optional[dict] = None,
193
+ unset: Optional[typing.List[str]] = None,
194
+ types: Optional[typing.Dict[str, str]] = None,
195
+ ) -> dict:
196
+ """Update the properties of existing relationships of one type.
197
+
198
+ Changes the ``type`` relationships from ``source`` to each ``target`` (or the
199
+ reverse with ``direction='in'``). Pairs without such a relationship are skipped.
200
+ Not available inside a transaction. Needs RushDB 3.0.4 or newer.
201
+
202
+ Args:
203
+ source: The source record (id, ``Record`` or dict with ``__id``).
204
+ target: The target record(s).
205
+ type (str): The relationship type to update.
206
+ direction (str, optional): ``'out'`` (default) or ``'in'``.
207
+ set (dict, optional): Properties to write; ``None`` values store an explicit null.
208
+ unset (list[str], optional): Names of properties to remove.
209
+ types (dict, optional): Type of ``set`` properties JSON can't express, one of
210
+ ``'string'``, ``'number'``, ``'boolean'``, ``'datetime'``;
211
+ ``{"since": "datetime"}`` stores an ISO string as a datetime.
212
+
213
+ Returns:
214
+ dict: API response; ``data["updated"]`` counts the relationships changed.
215
+
216
+ Example:
217
+ >>> db.relationships.update(
218
+ ... customer, order, type="PLACES", set={"channel": "store"}, unset=["legacyCode"]
219
+ ... )
220
+ """
221
+ from .records import RecordsAPI
222
+
223
+ source_id = RecordsAPI._extract_target_ids(source)[0]
224
+ payload: dict = {
225
+ "targetIds": RecordsAPI._extract_target_ids(target),
226
+ "type": type,
227
+ }
228
+ if direction:
229
+ payload["direction"] = direction
230
+ if set is not None:
231
+ payload["set"] = set
232
+ if unset is not None:
233
+ payload["unset"] = unset
234
+ if types:
235
+ payload["types"] = types
236
+ return self.client._make_request(
237
+ "PATCH", f"/relationships/{source_id}", payload
238
+ )
239
+
184
240
  def delete_many(
185
241
  self,
186
242
  *,
@@ -753,6 +753,39 @@ def _detach(request: _Request) -> Body:
753
753
  )
754
754
 
755
755
 
756
+ def _update_relationships(request: _Request) -> Body:
757
+ body, (source_id,) = request.body, request.params
758
+ _not_in_transaction(request.tx_id, "relationships.update")
759
+ result = request.engine.execute(
760
+ {
761
+ "kind": "mutate",
762
+ "mutation": {
763
+ "kind": "updateRelationshipsById",
764
+ "command": {
765
+ "relationType": body.get("type"),
766
+ "direction": (
767
+ "in" if _direction(body.get("direction")) == "in" else "out"
768
+ ),
769
+ "sourceIds": [source_id],
770
+ "targetIds": _as_list(body.get("targetIds")),
771
+ "set": body.get("set") or {},
772
+ "unset": body.get("unset") or [],
773
+ # Sent only when used, so untyped updates work on engines without it.
774
+ **({"types": body["types"]} if body.get("types") else {}),
775
+ },
776
+ },
777
+ }
778
+ )
779
+ updated = int((result or {}).get("affected") or 0)
780
+ plural = "" if updated == 1 else "s"
781
+ return _ok(
782
+ {
783
+ "message": f"{updated} relationship{plural} of Record {source_id} updated",
784
+ "updated": updated,
785
+ }
786
+ )
787
+
788
+
756
789
  def _link_many(kind: str, message: str, what: str) -> Callable[[_Request], Body]:
757
790
  def handle(request: _Request) -> Body:
758
791
  _not_in_transaction(request.tx_id, what)
@@ -1283,6 +1316,7 @@ _ROUTES: List[Tuple[str, "re.Pattern[str]", Callable[[_Request], Body]]] = [
1283
1316
  ("POST", re.compile(r"^/records/export/csv$"), _export_csv),
1284
1317
  ("POST", re.compile(rf"^/relationships/{_ID}$"), _attach),
1285
1318
  ("PUT", re.compile(rf"^/relationships/{_ID}$"), _detach),
1319
+ ("PATCH", re.compile(rf"^/relationships/{_ID}$"), _update_relationships),
1286
1320
  (
1287
1321
  "POST",
1288
1322
  re.compile(r"^/relationships/create-many$"),
@@ -191,6 +191,9 @@ EVERY_METHOD = {
191
191
  "records.detach": lambda db: db.records.detach(
192
192
  RECORD["id"], RECORD["id"], options={"typeOrTypes": "LIKES"}
193
193
  ),
194
+ "relationships.update": lambda db: db.relationships.update(
195
+ RECORD["id"], RECORD["id"], type="LIKES", set={"weight": 1}
196
+ ),
194
197
  "records.create": lambda db: db.records.create("Book", {"title": "Dune"}),
195
198
  "records.create_many": lambda db: db.records.create_many(
196
199
  "Book", [{"title": "Dune"}]
@@ -531,6 +534,39 @@ def test_detach_without_type_or_direction_removes_every_link(engine_calls):
531
534
  ]
532
535
 
533
536
 
537
+ def test_relationship_properties_are_updated_by_record_ids(engine_calls):
538
+ result = direct_db().relationships.update(
539
+ RECORD["id"],
540
+ ["t1", "t2"],
541
+ type="PLACES",
542
+ direction="in",
543
+ set={"channel": "web", "note": None, "since": "2026-11-06T10:00:00.000Z"},
544
+ unset=["old"],
545
+ types={"since": "datetime"},
546
+ )
547
+ assert result["data"]["updated"] == 1
548
+ commands = [
549
+ op["mutation"]["command"]
550
+ for op in operations(engine_calls)
551
+ if op.get("mutation", {}).get("kind") == "updateRelationshipsById"
552
+ ]
553
+ assert commands == [
554
+ {
555
+ "relationType": "PLACES",
556
+ "direction": "in",
557
+ "sourceIds": [RECORD["id"]],
558
+ "targetIds": ["t1", "t2"],
559
+ "set": {
560
+ "channel": "web",
561
+ "note": None,
562
+ "since": "2026-11-06T10:00:00.000Z",
563
+ },
564
+ "unset": ["old"],
565
+ "types": {"since": "datetime"},
566
+ }
567
+ ]
568
+
569
+
534
570
  def test_keyword_search_uses_the_labels_text_index(engine_calls):
535
571
  result = direct_db().records.text_search({"query": "dune", "labels": ["Book"]})
536
572
  hybrid = [op for op in operations(engine_calls) if op.get("kind") == "query"][0]
@@ -85,6 +85,27 @@ def test_relationships(db):
85
85
  )
86
86
  assert found.total == 1
87
87
 
88
+ edge = found.data[0]
89
+ result = db.relationships.update(
90
+ edge["sourceId"],
91
+ edge["targetId"],
92
+ type="AUTHORED",
93
+ set={"year": 1965, "role": "author", "since": "1965-08-01T10:30:00.000Z"},
94
+ types={"since": "datetime"},
95
+ )
96
+ assert result["data"]["updated"] == 1
97
+ db.relationships.update(
98
+ edge["sourceId"], edge["targetId"], type="AUTHORED", unset=["role"]
99
+ )
100
+ updated = db.relationships.find(
101
+ {"where": {"type": "AUTHORED"}, "source": {"labels": [AUTHOR]}}
102
+ )
103
+ assert updated.data[0].get("properties") == {
104
+ "year": 1965,
105
+ "since": "1965-08-01T10:30:00.000Z",
106
+ }
107
+ assert updated.data[0].get("propertyTypes", {}).get("since") == "datetime"
108
+
88
109
 
89
110
  def test_labels_properties_schema(db):
90
111
  assert db.labels.find({})["data"][BOOK] >= 3
@@ -710,7 +710,7 @@ wheels = [
710
710
 
711
711
  [[package]]
712
712
  name = "rushdb"
713
- version = "3.1.0"
713
+ version = "3.2.0"
714
714
  source = { editable = "." }
715
715
 
716
716
  [package.dev-dependencies]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes