From 83c8b8cbcfd0708fedc0a2c7765133531098644f Mon Sep 17 00:00:00 2001 From: Artemiy Vereshchinskiy Date: Sun, 14 Sep 2025 11:30:32 +0700 Subject: [PATCH 1/2] Add raw query method, csv import update, create many relationships method introductions --- README.md | 22 ++++++++++++ pyproject.toml | 2 +- src/rushdb/__init__.py | 4 +++ src/rushdb/api/__init__.py | 15 ++++++++ src/rushdb/api/query.py | 43 +++++++++++++++++++++++ src/rushdb/api/records.py | 34 +++++++++++++----- src/rushdb/api/relationships.py | 61 +++++++++++++++++++++++++++++++++ src/rushdb/client.py | 4 +++ 8 files changed, 176 insertions(+), 9 deletions(-) create mode 100644 src/rushdb/api/query.py diff --git a/README.md b/README.md index 2e35a35..91fd807 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,13 @@ user.attach( target=company, options={"type": "WORKS_AT", "direction": "out"} ) + +# Run a raw Cypher query (cloud-only) +raw = db.query.raw({ + "query": "MATCH (u:USER) RETURN u LIMIT $limit", + "params": {"limit": 5} +}) +print(raw.get("data")) ``` ## Pushing Nested JSON @@ -97,6 +104,21 @@ db.records.create_many("COMPANY", { }) ``` +## Importing CSV Data with Parse Config + +```python +csv_data = """name,email,age\nJohn,john@example.com,30\nJane,jane@example.com,25""" + +response = db.records.import_csv( + label="USER", + data=csv_data, + options={"returnResult": True, "suggestTypes": True}, + parse_config={"header": True, "skipEmptyLines": True, "dynamicTyping": True} +) + +print(response.get("data")) +``` + ## SearchResult API RushDB Python SDK uses a modern `SearchResult` container that follows Python SDK best practices similar to boto3, google-cloud libraries, and other popular SDKs. diff --git a/pyproject.toml b/pyproject.toml index e67ae17..ce3bd4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "rushdb" -version = "1.10.0" +version = "1.14.0" description = "RushDB Python SDK" authors = [ {name = "RushDB Team", email = "hi@rushdb.com"} diff --git a/src/rushdb/__init__.py b/src/rushdb/__init__.py index 984137e..581346b 100644 --- a/src/rushdb/__init__.py +++ b/src/rushdb/__init__.py @@ -4,6 +4,8 @@ """ from .client import RushDB +from .api.query import QueryAPI +from .api.relationships import RelationsAPI from .common import RushDBError from .models.property import Property from .models.record import Record @@ -21,4 +23,6 @@ "Property", "RelationshipOptions", "RelationshipDetachOptions", + "QueryAPI", + "RelationsAPI", ] diff --git a/src/rushdb/api/__init__.py b/src/rushdb/api/__init__.py index e69de29..acf4777 100644 --- a/src/rushdb/api/__init__.py +++ b/src/rushdb/api/__init__.py @@ -0,0 +1,15 @@ +from .records import RecordsAPI +from .properties import PropertiesAPI +from .labels import LabelsAPI +from .transactions import TransactionsAPI +from .query import QueryAPI +from .relationships import RelationsAPI + +__all__ = [ + "RecordsAPI", + "PropertiesAPI", + "LabelsAPI", + "TransactionsAPI", + "QueryAPI", + "RelationsAPI", +] diff --git a/src/rushdb/api/query.py b/src/rushdb/api/query.py new file mode 100644 index 0000000..5975ac7 --- /dev/null +++ b/src/rushdb/api/query.py @@ -0,0 +1,43 @@ +from typing import Any, Dict, Optional + +from .base import BaseAPI +from ..models.transaction import Transaction + + +class QueryAPI(BaseAPI): + """API client for executing raw Cypher queries (cloud-only). + + This endpoint is only available when using the RushDB managed cloud + service or when your project is connected to a custom database through + RushDB Cloud. It will not function for self-hosted or local-only deployments. + + Example: + >>> from rushdb import RushDB + >>> db = RushDB("RUSHDB_API_KEY") + >>> result = db.query.raw({ + ... "query": "MATCH (n:Person) RETURN n LIMIT $limit", + ... "params": {"limit": 10} + ... }) + >>> print(result) + """ + + def raw( + self, + body: Dict[str, Any], + transaction: Optional[Transaction] = None, + ) -> Dict[str, Any]: + """Execute a raw Cypher query. + + Args: + body (Dict[str, Any]): Payload containing: + - query (str): Cypher query string + - params (Optional[Dict[str, Any]]): Parameter dict + transaction (Optional[Transaction]): Optional transaction context. + + Returns: + Dict[str, Any]: API response including raw driver result in 'data'. + """ + headers = Transaction._build_transaction_header(transaction) + return self.client._make_request( + "POST", "/query/raw", body, headers + ) diff --git a/src/rushdb/api/records.py b/src/rushdb/api/records.py index d98a90a..5bd4042 100644 --- a/src/rushdb/api/records.py +++ b/src/rushdb/api/records.py @@ -501,6 +501,7 @@ def import_csv( label: str, data: str, options: Optional[Dict[str, bool]] = None, + parse_config: Optional[Dict[str, Any]] = None, transaction: Optional[Transaction] = None, ) -> List[Dict[str, Any]]: """Import records from CSV data. @@ -511,14 +512,17 @@ def import_csv( Args: label (str): The label/type to assign to all records created from the CSV. - data (Union[str, bytes]): The CSV content to import. Can be provided - as a string. - options (Optional[Dict[str, bool]], optional): Configuration options for the import operation. - Available options: - - returnResult (bool): Whether to return the created records data. Defaults to True. - - suggestTypes (bool): Whether to automatically suggest data types for CSV columns. Defaults to True. - transaction (Optional[Transaction], optional): Transaction context for the operation. - If provided, the operation will be part of the transaction. Defaults to None. + data (str): The CSV content to import as a string. + options (Optional[Dict[str, bool]]): Import write options (see create_many for list). + parse_config (Optional[Dict[str, Any]]): CSV parsing configuration keys (subset of PapaParse): + - delimiter (str) + - header (bool) + - skipEmptyLines (bool | 'greedy') + - dynamicTyping (bool) + - quoteChar (str) + - escapeChar (str) + - newline (str) + transaction (Optional[Transaction]): Transaction context for the operation. Returns: List[Dict[str, Any]]: List of dictionaries representing the imported records, @@ -544,6 +548,20 @@ def import_csv( "data": data, "options": options or {"returnResult": True, "suggestTypes": True}, } + if parse_config: + # pass through only known parse config keys, ignore others silently + allowed_keys = { + "delimiter", + "header", + "skipEmptyLines", + "dynamicTyping", + "quoteChar", + "escapeChar", + "newline", + } + payload["parseConfig"] = { + k: v for k, v in parse_config.items() if k in allowed_keys and v is not None + } return self.client._make_request( "POST", "/records/import/csv", payload, headers diff --git a/src/rushdb/api/relationships.py b/src/rushdb/api/relationships.py index ab8494a..c49dde8 100644 --- a/src/rushdb/api/relationships.py +++ b/src/rushdb/api/relationships.py @@ -141,3 +141,64 @@ async def find( ) return response.data + + def create_many( + self, + *, + source: dict, + target: dict, + type: Optional[str] = None, + direction: Optional[str] = None, + many_to_many: Optional[bool] = None, + transaction: Optional[Union[Transaction, str]] = None, + ) -> dict: + """Bulk create relationships by matching keys or many-to-many cartesian. + + Modes: + 1. Key-match (default): requires source.key and target.key, creates relationships where source[key] = target[key]. + 2. many_to_many=True: cartesian across filtered sets (requires where filters on both source & target and omits keys). + + Args: + source (dict): { label: str, key?: str, where?: dict } + target (dict): { label: str, key?: str, where?: dict } + type (str, optional): Relationship type override. + direction (str, optional): 'in' | 'out'. Defaults to 'out' server-side when omitted. + many_to_many (bool, optional): Enable cartesian mode (requires filters, disallows keys). + transaction (Transaction|str, optional): Transaction context. + + Returns: + dict: API response payload. + """ + headers = Transaction._build_transaction_header(transaction) + payload: dict = {"source": source, "target": target} + if type: + payload["type"] = type + if direction: + payload["direction"] = direction + if many_to_many is not None: + payload["manyToMany"] = many_to_many + return self.client._make_request("POST", "/relationships/create-many", payload, headers) + + def delete_many( + self, + *, + source: dict, + target: dict, + type: Optional[str] = None, + direction: Optional[str] = None, + many_to_many: Optional[bool] = None, + transaction: Optional[Union[Transaction, str]] = None, + ) -> dict: + """Bulk delete relationships using same contract as create_many. + + See create_many for argument semantics. + """ + headers = Transaction._build_transaction_header(transaction) + payload: dict = {"source": source, "target": target} + if type: + payload["type"] = type + if direction: + payload["direction"] = direction + if many_to_many is not None: + payload["manyToMany"] = many_to_many + return self.client._make_request("POST", "/relationships/delete-many", payload, headers) diff --git a/src/rushdb/client.py b/src/rushdb/client.py index 415f575..71ad39d 100644 --- a/src/rushdb/client.py +++ b/src/rushdb/client.py @@ -15,6 +15,8 @@ from .api.properties import PropertiesAPI from .api.records import RecordsAPI from .api.transactions import TransactionsAPI +from .api.query import QueryAPI +from .api.relationships import RelationsAPI from .common import RushDBError from .utils.token_prefix import extract_mixed_properties_from_token @@ -121,6 +123,8 @@ def __init__(self, api_key: str, base_url: Optional[str] = None): self.properties = PropertiesAPI(self) self.labels = LabelsAPI(self) self.transactions = TransactionsAPI(self) + self.query = QueryAPI(self) + self.relationships = RelationsAPI(self) def _make_request( self, From 5bb874910810931594135c434e412bc668a5db41 Mon Sep 17 00:00:00 2001 From: Artemiy Vereshchinskiy Date: Sun, 14 Sep 2025 11:37:14 +0700 Subject: [PATCH 2/2] Linting, sorting --- src/rushdb/__init__.py | 2 +- src/rushdb/api/__init__.py | 18 +++++++++--------- src/rushdb/api/query.py | 6 ++---- src/rushdb/api/records.py | 4 +++- src/rushdb/api/relationships.py | 8 ++++++-- src/rushdb/client.py | 4 ++-- uv.lock | 2 +- 7 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/rushdb/__init__.py b/src/rushdb/__init__.py index 581346b..e77f08d 100644 --- a/src/rushdb/__init__.py +++ b/src/rushdb/__init__.py @@ -3,9 +3,9 @@ Exposes the RushDB class. """ -from .client import RushDB from .api.query import QueryAPI from .api.relationships import RelationsAPI +from .client import RushDB from .common import RushDBError from .models.property import Property from .models.record import Record diff --git a/src/rushdb/api/__init__.py b/src/rushdb/api/__init__.py index acf4777..c6d2fd3 100644 --- a/src/rushdb/api/__init__.py +++ b/src/rushdb/api/__init__.py @@ -1,15 +1,15 @@ -from .records import RecordsAPI -from .properties import PropertiesAPI from .labels import LabelsAPI -from .transactions import TransactionsAPI +from .properties import PropertiesAPI from .query import QueryAPI +from .records import RecordsAPI from .relationships import RelationsAPI +from .transactions import TransactionsAPI __all__ = [ - "RecordsAPI", - "PropertiesAPI", - "LabelsAPI", - "TransactionsAPI", - "QueryAPI", - "RelationsAPI", + "RecordsAPI", + "PropertiesAPI", + "LabelsAPI", + "TransactionsAPI", + "QueryAPI", + "RelationsAPI", ] diff --git a/src/rushdb/api/query.py b/src/rushdb/api/query.py index 5975ac7..f4bfa8d 100644 --- a/src/rushdb/api/query.py +++ b/src/rushdb/api/query.py @@ -1,7 +1,7 @@ from typing import Any, Dict, Optional -from .base import BaseAPI from ..models.transaction import Transaction +from .base import BaseAPI class QueryAPI(BaseAPI): @@ -38,6 +38,4 @@ def raw( Dict[str, Any]: API response including raw driver result in 'data'. """ headers = Transaction._build_transaction_header(transaction) - return self.client._make_request( - "POST", "/query/raw", body, headers - ) + return self.client._make_request("POST", "/query/raw", body, headers) diff --git a/src/rushdb/api/records.py b/src/rushdb/api/records.py index 5bd4042..aefcaf8 100644 --- a/src/rushdb/api/records.py +++ b/src/rushdb/api/records.py @@ -560,7 +560,9 @@ def import_csv( "newline", } payload["parseConfig"] = { - k: v for k, v in parse_config.items() if k in allowed_keys and v is not None + k: v + for k, v in parse_config.items() + if k in allowed_keys and v is not None } return self.client._make_request( diff --git a/src/rushdb/api/relationships.py b/src/rushdb/api/relationships.py index c49dde8..5bd2eba 100644 --- a/src/rushdb/api/relationships.py +++ b/src/rushdb/api/relationships.py @@ -177,7 +177,9 @@ def create_many( payload["direction"] = direction if many_to_many is not None: payload["manyToMany"] = many_to_many - return self.client._make_request("POST", "/relationships/create-many", payload, headers) + return self.client._make_request( + "POST", "/relationships/create-many", payload, headers + ) def delete_many( self, @@ -201,4 +203,6 @@ def delete_many( payload["direction"] = direction if many_to_many is not None: payload["manyToMany"] = many_to_many - return self.client._make_request("POST", "/relationships/delete-many", payload, headers) + return self.client._make_request( + "POST", "/relationships/delete-many", payload, headers + ) diff --git a/src/rushdb/client.py b/src/rushdb/client.py index 71ad39d..034c1a1 100644 --- a/src/rushdb/client.py +++ b/src/rushdb/client.py @@ -13,10 +13,10 @@ from .api.labels import LabelsAPI from .api.properties import PropertiesAPI -from .api.records import RecordsAPI -from .api.transactions import TransactionsAPI from .api.query import QueryAPI +from .api.records import RecordsAPI from .api.relationships import RelationsAPI +from .api.transactions import TransactionsAPI from .common import RushDBError from .utils.token_prefix import extract_mixed_properties_from_token diff --git a/uv.lock b/uv.lock index d5018c9..6e1b75c 100644 --- a/uv.lock +++ b/uv.lock @@ -785,7 +785,7 @@ wheels = [ [[package]] name = "rushdb" -version = "1.8.0" +version = "1.14.0" source = { editable = "." } dependencies = [ { name = "python-dotenv", version = "1.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },