From 4c230a82b3d382d7a79b2f561a8fb8ca9a99497f Mon Sep 17 00:00:00 2001 From: Nikhil Isukapalli Date: Wed, 19 Nov 2025 12:49:59 -0800 Subject: [PATCH 1/9] Initialized python-sdk/ directory with examples/basic_usage.py, src/__init__.py, src/error.py, and src/connection.py. Opening a database works (#1 in basic usage). --- python-sdk/examples/basic_usage.py | 145 +++++++++++++++++++++++++++++ python-sdk/src/__init__.py | 88 +++++++++++++++++ python-sdk/src/connection.py | 134 ++++++++++++++++++++++++++ python-sdk/src/error.py | 103 ++++++++++++++++++++ 4 files changed, 470 insertions(+) create mode 100644 python-sdk/examples/basic_usage.py create mode 100644 python-sdk/src/__init__.py create mode 100644 python-sdk/src/connection.py create mode 100644 python-sdk/src/error.py diff --git a/python-sdk/examples/basic_usage.py b/python-sdk/examples/basic_usage.py new file mode 100644 index 0000000..66787ac --- /dev/null +++ b/python-sdk/examples/basic_usage.py @@ -0,0 +1,145 @@ +""" +GraphLite Python SDK - Basic Usage Example + +This example demonstrates the core features of the GraphLite Python SDK: +- Opening a database +- Creating sessions +- Executing queries +- Using transactions +- Query builder API +- Typed result deserialization + +Run with: python3 examples/basic_usage.py +""" + +from dataclasses import dataclass +from typing import Optional +import tempfile +import shutil +import sys +from pathlib import Path + +# Add parent directory to path so we can import src +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from src.connection import GraphLite +from src.error import GraphLiteError + + +@dataclass +class Person: + """Person entity for typed deserialization""" + name: str + age: float + + +def main() -> int: + """Run the basic usage example""" + print("=== GraphLite SDK Basic Usage Example ===\n") + + # Use temporary directory for demo + db_path = tempfile.mkdtemp(prefix="graphlite_sdk_example_") + + try: + # 1. Open a database + print("1. Opening database...") + db = GraphLite.open(db_path) + print(f" ✓ Database opened at {db_path}\n") + + # 2. Create a session + print("2. Creating session...") + session = db.session("admin") + print(" ✓ Session created for user 'admin'\n") + + # # 3. Execute DDL statements + # print("3. Creating schema and graph...") + # session.execute("CREATE SCHEMA IF NOT EXISTS /example") + # session.execute("SESSION SET SCHEMA /example") + # session.execute("CREATE GRAPH IF NOT EXISTS social") + # session.execute("SESSION SET GRAPH social") + # print(" ✓ Schema and graph created\n") + + # # 4. Insert data using transactions + # print("4. Inserting data with transaction...") + # with session.transaction() as tx: + # tx.execute("CREATE (p:Person {name: 'Alice', age: 30})") + # tx.execute("CREATE (p:Person {name: 'Bob', age: 25})") + # tx.execute("CREATE (p:Person {name: 'Charlie', age: 35})") + # tx.commit() + # print(" ✓ Inserted 3 persons\n") + + # # 5. Query data directly + # print("5. Querying data...") + # result = session.query("MATCH (p:Person) RETURN p.name as name, p.age as age") + # print(f" Found {len(result.rows)} persons:") + # for row in result.rows: + # name = row.get_value("name") + # age = row.get_value("age") + # if name is not None and age is not None: + # print(f" - Name: {name}, Age: {age}") + # print() + + # # 6. Use query builder + # print("6. Using query builder...") + # result = (session.query_builder() + # .match_pattern("(p:Person)") + # .where_clause("p.age > 25") + # .return_clause("p.name as name, p.age as age") + # .order_by("p.age DESC") + # .execute()) + # print(f" Found {len(result.rows)} persons over 25:") + # for row in result.rows: + # name = row.get_value("name") + # age = row.get_value("age") + # if name is not None and age is not None: + # print(f" - Name: {name}, Age: {age}") + # print() + + # # 7. Typed deserialization + # print("7. Using typed deserialization...") + # result = session.query("MATCH (p:Person) RETURN p.name as name, p.age as age") + # typed_result = result.to_typed() + # people = typed_result.deserialize_rows(Person) + # print(f" Deserialized {len(people)} persons:") + # for person in people: + # print(f" - {person}") + # print() + + # # 8. Transaction with rollback + # print("8. Demonstrating transaction rollback...") + # try: + # with session.transaction() as tx: + # tx.execute("CREATE (p:Person {name: 'David', age: 40})") + # print(" Created person 'David' in transaction") + # # Transaction is NOT committed - will auto-rollback + # # (by not calling tx.commit()) + # except Exception: + # pass # Expected - rollback on exception + # print(" Transaction rolled back (David not persisted)\n") + + # # 9. Verify rollback + # result = session.query("MATCH (p:Person) RETURN count(p) as count") + # if result.rows: + # count = result.rows[0].get_value("count") + # print(f" Person count after rollback: {count}\n") + + # print("=== Example completed successfully ===") + # return 0 + + except GraphLiteError as e: + print(f"\n❌ GraphLite Error: {e}") + return 1 + + except Exception as e: + print(f"\n❌ Unexpected error: {e}") + import traceback + traceback.print_exc() + return 1 + + finally: + # Cleanup temporary directory + shutil.rmtree(db_path, ignore_errors=True) + + +if __name__ == "__main__": + exit(main()) diff --git a/python-sdk/src/__init__.py b/python-sdk/src/__init__.py new file mode 100644 index 0000000..1f6f87b --- /dev/null +++ b/python-sdk/src/__init__.py @@ -0,0 +1,88 @@ +""" +GraphLite SDK - High-level Python API for GraphLite + +This package provides a high-level, developer-friendly SDK on top of GraphLite's core API. +It offers ergonomic patterns, type safety, session management, query builders, and +transaction support - everything needed to build robust graph-based applications in Python. + +Quick Start +----------- + +```python +from graphlite_sdk import GraphLite + +# Open database +db = GraphLite.open("./mydb") + +# Create session +session = db.session("admin") + +# Execute query +result = session.query("MATCH (p:Person) RETURN p.name") + +# Use transactions +with session.transaction() as tx: + tx.execute("CREATE (p:Person {name: 'Alice'})") + tx.commit() +``` + +Architecture +----------- + +``` +Your Application + │ + ▼ +┌─────────────────────────────────────────┐ +│ GraphLite SDK (this package) │ +│ - GraphLite (main API) │ +│ - Session (session management) │ +│ - Transaction (ACID support) │ +│ - QueryBuilder (fluent queries) │ +│ - TypedResult (deserialization) │ +└─────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ GraphLite FFI Bindings │ +│ (Low-level ctypes wrapper) │ +└─────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ GraphLite Core (Rust) │ +│ - QueryCoordinator │ +│ - Storage Engine │ +│ - Catalog Manager │ +└─────────────────────────────────────────┘ +``` +""" + +from .error import GraphLiteError +from .connection import GraphLite, Session +# from .transaction import Transaction +# from .query import QueryBuilder +# from .result import TypedResult + +__version__ = "0.1.0" + +__all__ = [ + # Main API + "GraphLite", + "Session", + + # Transaction support + # "Transaction", + + # Query building + # "QueryBuilder", + + # Result handling + # "TypedResult", + + # Errors + "GraphLiteError", + + # Version + "__version__", +] diff --git a/python-sdk/src/connection.py b/python-sdk/src/connection.py new file mode 100644 index 0000000..be886c0 --- /dev/null +++ b/python-sdk/src/connection.py @@ -0,0 +1,134 @@ +""" +Database connection and session management +This module provides the main entry points for working with GraphLite databases. +""" + +import sys +from pathlib import Path + +# Add bindings/python to path so we can import the low-level bindings +bindings_path = Path(__file__).parent.parent.parent / "bindings" / "python" +if str(bindings_path) not in sys.path: + sys.path.insert(0, str(bindings_path)) + +# Low-level GraphLite binding (like QueryCoordinator in Rust) +from graphlite import GraphLite as _GraphLiteBinding, QueryResult +from .error import ConnectionError, SessionError, QueryError + + +class GraphLite: + """ + GraphLite database connection + + High-level wrapper around the FFI bindings, similar to the Rust SDK. + """ + + def __init__(self, db: _GraphLiteBinding): + """ + Internal constructor - use GraphLite.open() instead + + Store instance of the low-level GraphLite binding + """ + self._db = db + + @classmethod + def open(cls, path: str): + """ + Open a GraphLite database at the given path + + Args: + path: Path to the database directory + + Returns: + GraphLite instance + + Raises: + ConnectionError: If database cannot be opened + """ + try: + db = _GraphLiteBinding(path) + return cls(db) + except Exception as e: + raise ConnectionError(f"Failed to open database: {e}") + + def session(self, username: str): + """ + Create a new session for the given user + + Args: + username: Username for the session + + Returns: + Session instance + + Raises: + SessionError: If session creation fails + """ + try: + session_id = self._db.create_session(username) + return Session(session_id, self._db, username) + except Exception as e: + raise SessionError(f"Failed to create session: {e}") + + def close(self): + """Close the database connection""" + if self._db: + self._db.close() + + +class Session: + """ + GraphLite database session + + Provides user context for executing queries. + """ + + def __init__(self, session_id: str, db: _GraphLiteBinding, username: str): + """Internal constructor - use db.session() instead""" + self._session_id = session_id + self._db = db + self._username = username + + def id(self) -> str: + """Get the session ID""" + return self._session_id + + def username(self) -> str: + """Get the username for this session""" + return self._username + + def query(self, query: str) -> QueryResult: + """ + Execute a GQL query + + Args: + query: GQL query string + + Returns: + QueryResult with rows and metadata + + Raises: + QueryError: If query execution fails + """ + try: + return self._db.query(self._session_id, query) + except Exception as e: + raise QueryError(f"Query failed: {e}") + + def execute(self, statement: str): + """ + Execute a statement without returning results + + Args: + statement: GQL statement to execute + + Raises: + QueryError: If execution fails + """ + try: + self._db.execute(self._session_id, statement) + except Exception as e: + raise QueryError(f"Execute failed: {e}") + + +__all__ = ['GraphLite', 'Session', 'QueryResult'] diff --git a/python-sdk/src/error.py b/python-sdk/src/error.py new file mode 100644 index 0000000..1c66e92 --- /dev/null +++ b/python-sdk/src/error.py @@ -0,0 +1,103 @@ +""" +Error types for the GraphLite SDK +""" + +from typing import Union +import json + +class GraphLiteError(Exception): + """ + Base exception for all GraphLite SDK errors. + """ + def __init__(self, message: str): + self.message = message + super().__init__(message) + +class GraphLiteCoreError(GraphLiteError): + """Error from the core GraphLite library""" + def __init__(self, message: str): + super().__init__(f"GraphLite error: {message}") + +class SessionError(GraphLiteError): + """Session-related errors""" + def __init__(self, message: str): + super().__init__(f"Session error: {message}") + +class QueryError(GraphLiteError): + """Query execution errors""" + def __init__(self, message: str): + super().__init__(f"Query error: {message}") + +class TransactionError(GraphLiteError): + """Transaction errors""" + def __init__(self, message: str): + super().__init__(f"Transaction error: {message}") + +class SerializationError(GraphLiteError): + """Serialization/deserialization errors""" + def __init__(self, message: str): + super().__init__(f"Serialization error: {message}") + + @classmethod + def from_json_error(cls, error: json.JSONDecodeError) -> "SerializationError": + """Create SerializationError from json.JSONDecodeError""" + return cls(f"JSON error: {error.msg} at line {error.lineno}, column {error.colno}") + +class TypeConversionError(GraphLiteError): + """Type conversion errors""" + def __init__(self, message: str): + super().__init__(f"Type conversion error: {message}") + +class InvalidOperationError(GraphLiteError): + """Invalid operation errors""" + def __init__(self, message: str): + super().__init__(f"Invalid operation: {message}") + +class NotFoundError(GraphLiteError): + """Resource not found errors""" + def __init__(self, message: str): + super().__init__(f"Not found: {message}") + +class ConnectionError(GraphLiteError): + """Connection errors""" + def __init__(self, message: str): + super().__init__(f"Connection error: {message}") + +class IoError(GraphLiteError): + """I/O errors""" + def __init__(self, message: str): + super().__init__(f"I/O error: {message}") + + @classmethod + def from_io_error(cls, error: OSError) -> "IoError": + """Create IoError from Python OSError (IOError in Python 3)""" + filename = getattr(error, 'filename', None) or '' + strerror = getattr(error, 'strerror', None) or str(error) + return cls(f"{strerror}: {filename}" if filename else strerror) + + +# ============================================================================ +# Error Conversion Helpers +# ============================================================================ + +def from_string(message: Union[str, bytes]) -> GraphLiteCoreError: + """ + Convert a string to GraphLiteCoreError. + """ + if isinstance(message, bytes): + message = message.decode('utf-8', errors='replace') + return GraphLiteCoreError(message) + + +def from_json_error(error: json.JSONDecodeError) -> SerializationError: + """ + Convert json.JSONDecodeError to SerializationError. + """ + return SerializationError.from_json_error(error) + + +def from_io_error(error: OSError) -> IoError: + """ + Convert OSError to IoError. + """ + return IoError.from_io_error(error) From 859faf7bf3cd68f0a33d7552f23c03b35882d901 Mon Sep 17 00:00:00 2001 From: Nikhil Isukapalli Date: Wed, 19 Nov 2025 16:22:09 -0800 Subject: [PATCH 2/9] Implemented Transaction class --- python-sdk/examples/basic_usage.py | 54 ++++---- python-sdk/src/__init__.py | 4 +- python-sdk/src/connection.py | 23 ++++ python-sdk/src/query.py | 0 python-sdk/src/transaction.py | 205 +++++++++++++++++++++++++++++ 5 files changed, 257 insertions(+), 29 deletions(-) create mode 100644 python-sdk/src/query.py create mode 100644 python-sdk/src/transaction.py diff --git a/python-sdk/examples/basic_usage.py b/python-sdk/examples/basic_usage.py index 66787ac..8f86117 100644 --- a/python-sdk/examples/basic_usage.py +++ b/python-sdk/examples/basic_usage.py @@ -51,33 +51,33 @@ def main() -> int: session = db.session("admin") print(" ✓ Session created for user 'admin'\n") - # # 3. Execute DDL statements - # print("3. Creating schema and graph...") - # session.execute("CREATE SCHEMA IF NOT EXISTS /example") - # session.execute("SESSION SET SCHEMA /example") - # session.execute("CREATE GRAPH IF NOT EXISTS social") - # session.execute("SESSION SET GRAPH social") - # print(" ✓ Schema and graph created\n") - - # # 4. Insert data using transactions - # print("4. Inserting data with transaction...") - # with session.transaction() as tx: - # tx.execute("CREATE (p:Person {name: 'Alice', age: 30})") - # tx.execute("CREATE (p:Person {name: 'Bob', age: 25})") - # tx.execute("CREATE (p:Person {name: 'Charlie', age: 35})") - # tx.commit() - # print(" ✓ Inserted 3 persons\n") - - # # 5. Query data directly - # print("5. Querying data...") - # result = session.query("MATCH (p:Person) RETURN p.name as name, p.age as age") - # print(f" Found {len(result.rows)} persons:") - # for row in result.rows: - # name = row.get_value("name") - # age = row.get_value("age") - # if name is not None and age is not None: - # print(f" - Name: {name}, Age: {age}") - # print() + # 3. Execute DDL statements + print("3. Creating schema and graph...") + session.execute("CREATE SCHEMA IF NOT EXISTS /example") + session.execute("SESSION SET SCHEMA /example") + session.execute("CREATE GRAPH IF NOT EXISTS social") + session.execute("SESSION SET GRAPH social") + print(" ✓ Schema and graph created\n") + + # 4. Insert data using transactions + print("4. Inserting data with transaction...") + with session.transaction() as tx: + tx.execute("CREATE (p:Person {name: 'Alice', age: 30})") + tx.execute("CREATE (p:Person {name: 'Bob', age: 25})") + tx.execute("CREATE (p:Person {name: 'Charlie', age: 35})") + tx.commit() + print(" ✓ Inserted 3 persons\n") + + # 5. Query data directly + print("5. Querying data...") + result = session.query("MATCH (p:Person) RETURN p.name as name, p.age as age") + print(f" Found {len(result.rows)} persons:") + for row in result.rows: + name = row.get_value("name") + age = row.get_value("age") + if name is not None and age is not None: + print(f" - Name: {name}, Age: {age}") + print() # # 6. Use query builder # print("6. Using query builder...") diff --git a/python-sdk/src/__init__.py b/python-sdk/src/__init__.py index 1f6f87b..9e39c91 100644 --- a/python-sdk/src/__init__.py +++ b/python-sdk/src/__init__.py @@ -60,7 +60,7 @@ from .error import GraphLiteError from .connection import GraphLite, Session -# from .transaction import Transaction +from .transaction import Transaction # from .query import QueryBuilder # from .result import TypedResult @@ -72,7 +72,7 @@ "Session", # Transaction support - # "Transaction", + "Transaction", # Query building # "QueryBuilder", diff --git a/python-sdk/src/connection.py b/python-sdk/src/connection.py index be886c0..bc9887b 100644 --- a/python-sdk/src/connection.py +++ b/python-sdk/src/connection.py @@ -130,5 +130,28 @@ def execute(self, statement: str): except Exception as e: raise QueryError(f"Execute failed: {e}") + def transaction(self): + """ + Begin a new transaction + + Returns a Transaction object that should be used as a context manager. + The transaction will automatically roll back when the context exits + unless commit() is explicitly called. + + Returns: + Transaction instance + + Raises: + TransactionError: If transaction cannot be started + + Examples: + >>> with session.transaction() as tx: + ... tx.execute("CREATE (p:Person {name: 'Alice'})") + ... tx.execute("CREATE (p:Person {name: 'Bob'})") + ... tx.commit() # Must commit to persist changes + """ + from .transaction import Transaction + return Transaction(self) + __all__ = ['GraphLite', 'Session', 'QueryResult'] diff --git a/python-sdk/src/query.py b/python-sdk/src/query.py new file mode 100644 index 0000000..e69de29 diff --git a/python-sdk/src/transaction.py b/python-sdk/src/transaction.py new file mode 100644 index 0000000..ddf596c --- /dev/null +++ b/python-sdk/src/transaction.py @@ -0,0 +1,205 @@ +""" +Transaction support with ACID guarantees + +This module provides transaction support following the rusqlite pattern: +- Transactions automatically roll back when context exits (unless committed) +- Explicit commit() required to persist changes +- Can be used as a context manager for automatic cleanup +""" + +from typing import TYPE_CHECKING +from .error import TransactionError + +if TYPE_CHECKING: + from .connection import Session + +import sys +from pathlib import Path +bindings_path = Path(__file__).parent.parent.parent / "bindings" / "python" +if str(bindings_path) not in sys.path: + sys.path.insert(0, str(bindings_path)) + +# Import QueryResult for type hints +from graphlite import QueryResult + + +class Transaction: + """ + Represents an active database transaction + + Transactions provide ACID guarantees for multi-statement operations. + Following the rusqlite pattern: + - Transactions automatically roll back when context exits + - Must explicitly call commit() to persist changes + - This prevents accidentally forgetting to commit or rollback + + Examples: + >>> db = GraphLite.open("./mydb") + >>> session = db.session("admin") + >>> + >>> # Using as context manager (recommended) + >>> with session.transaction() as tx: + ... tx.execute("CREATE (p:Person {name: 'Alice'})") + ... tx.execute("CREATE (p:Person {name: 'Bob'})") + ... tx.commit() # Changes are persisted + >>> + >>> # Transaction that rolls back (no commit) + >>> with session.transaction() as tx: + ... tx.execute("CREATE (p:Person {name: 'Charlie'})") + ... # Automatically rolled back on context exit + """ + + def __init__(self, session: 'Session'): + """ + Internal constructor - use session.transaction() instead + + Begin a new transaction + """ + self._session = session + self._committed = False + self._rolled_back = False + + # Execute BEGIN TRANSACTION + try: + self._session._db.execute(self._session._session_id, "BEGIN TRANSACTION") + except Exception as e: + raise TransactionError(f"Failed to begin transaction: {e}") + + def execute(self, statement: str) -> None: + """ + Execute a GQL statement within this transaction + + Args: + statement: GQL statement to execute + + Raises: + TransactionError: If transaction is already finished or execution fails + + Examples: + >>> with session.transaction() as tx: + ... tx.execute("CREATE (p:Person {name: 'Alice'})") + ... tx.execute("CREATE (p:Person {name: 'Bob'})") + ... tx.commit() + """ + if self._committed: + raise TransactionError("Transaction already committed") + if self._rolled_back: + raise TransactionError("Transaction already rolled back") + + try: + self._session._db.execute(self._session._session_id, statement) + except Exception as e: + raise TransactionError(f"Execute failed: {e}") + + def query(self, query: str) -> QueryResult: + """ + Execute a query within this transaction and return results + + Args: + query: GQL query to execute + + Returns: + QueryResult with rows and metadata + + Raises: + TransactionError: If transaction is already finished or query fails + + Examples: + >>> with session.transaction() as tx: + ... tx.execute("CREATE (p:Person {name: 'Alice', age: 30})") + ... result = tx.query("MATCH (p:Person) RETURN p") + ... tx.commit() + """ + if self._committed: + raise TransactionError("Transaction already committed") + if self._rolled_back: + raise TransactionError("Transaction already rolled back") + + try: + return self._session._db.query(self._session._session_id, query) + except Exception as e: + raise TransactionError(f"Query failed: {e}") + + def commit(self) -> None: + """ + Commit the transaction + + Persists all changes made within this transaction. After calling commit(), + the transaction cannot be used further. + + Raises: + TransactionError: If transaction is already finished or commit fails + + Examples: + >>> with session.transaction() as tx: + ... tx.execute("CREATE (p:Person {name: 'Alice'})") + ... tx.commit() # Changes are now persistent + """ + if self._committed: + raise TransactionError("Transaction already committed") + if self._rolled_back: + raise TransactionError("Transaction already rolled back") + + try: + self._session._db.execute(self._session._session_id, "COMMIT") + self._committed = True + except Exception as e: + raise TransactionError(f"Failed to commit: {e}") + + def rollback(self) -> None: + """ + Rollback the transaction + + Discards all changes made within this transaction. This is called + automatically when the transaction context exits, so explicit rollback + is rarely needed. + + Raises: + TransactionError: If rollback fails + + Examples: + >>> with session.transaction() as tx: + ... tx.execute("CREATE (p:Person {name: 'Alice'})") + ... tx.rollback() # Explicit rollback (optional, automatic on exit) + """ + if self._committed: + return # Already committed, nothing to rollback + if self._rolled_back: + return # Already rolled back + + try: + self._session._db.execute(self._session._session_id, "ROLLBACK") + self._rolled_back = True + except Exception as e: + raise TransactionError(f"Failed to rollback: {e}") + + def __enter__(self): + """Context manager entry""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """ + Context manager exit - automatically rollback if not committed + + If an exception occurred, always rollback. + If no exception, rollback unless commit() was called. + """ + if exc_type is not None: + # Exception occurred, always rollback + if not self._committed and not self._rolled_back: + try: + self.rollback() + except Exception: + pass # Ignore rollback errors during exception handling + else: + # No exception - rollback if not committed + if not self._committed and not self._rolled_back: + try: + self.rollback() + except Exception: + pass # Ignore rollback errors + + return False # Don't suppress exceptions + + +__all__ = ['Transaction'] From ebc22b9a983cf6f5dfb8715e3a8eebc71c77e772 Mon Sep 17 00:00:00 2001 From: Nikhil Isukapalli Date: Wed, 19 Nov 2025 17:26:43 -0800 Subject: [PATCH 3/9] Implemented QueryBuilder class. Steps 1-6 working on basic_usage.py but order_by DESC not working. --- python-sdk/examples/basic_usage.py | 40 ++++---- python-sdk/src/__init__.py | 16 +-- python-sdk/src/connection.py | 17 ++++ python-sdk/src/query.py | 156 +++++++++++++++++++++++++++++ 4 files changed, 195 insertions(+), 34 deletions(-) diff --git a/python-sdk/examples/basic_usage.py b/python-sdk/examples/basic_usage.py index 8f86117..d4e7164 100644 --- a/python-sdk/examples/basic_usage.py +++ b/python-sdk/examples/basic_usage.py @@ -73,27 +73,27 @@ def main() -> int: result = session.query("MATCH (p:Person) RETURN p.name as name, p.age as age") print(f" Found {len(result.rows)} persons:") for row in result.rows: - name = row.get_value("name") - age = row.get_value("age") + name = row.get("name") + age = row.get("age") if name is not None and age is not None: print(f" - Name: {name}, Age: {age}") print() - # # 6. Use query builder - # print("6. Using query builder...") - # result = (session.query_builder() - # .match_pattern("(p:Person)") - # .where_clause("p.age > 25") - # .return_clause("p.name as name, p.age as age") - # .order_by("p.age DESC") - # .execute()) - # print(f" Found {len(result.rows)} persons over 25:") - # for row in result.rows: - # name = row.get_value("name") - # age = row.get_value("age") - # if name is not None and age is not None: - # print(f" - Name: {name}, Age: {age}") - # print() + # 6. Use query builder + print("6. Using query builder...") + result = (session.query_builder() + .match_pattern("(p:Person)") + .where_clause("p.age > 25") + .return_clause("p.name as name, p.age as age") + .order_by("p.age DESC") + .execute()) + print(f" Found {len(result.rows)} persons over 25:") + for row in result.rows: + name = row.get("name") + age = row.get("age") + if name is not None and age is not None: + print(f" - Name: {name}, Age: {age}") + print() # # 7. Typed deserialization # print("7. Using typed deserialization...") @@ -120,11 +120,11 @@ def main() -> int: # # 9. Verify rollback # result = session.query("MATCH (p:Person) RETURN count(p) as count") # if result.rows: - # count = result.rows[0].get_value("count") + # count = result.rows[0].get("count") # print(f" Person count after rollback: {count}\n") - # print("=== Example completed successfully ===") - # return 0 + print("=== Example completed successfully ===") + return 0 except GraphLiteError as e: print(f"\n❌ GraphLite Error: {e}") diff --git a/python-sdk/src/__init__.py b/python-sdk/src/__init__.py index 9e39c91..2a36e0e 100644 --- a/python-sdk/src/__init__.py +++ b/python-sdk/src/__init__.py @@ -61,28 +61,16 @@ from .error import GraphLiteError from .connection import GraphLite, Session from .transaction import Transaction -# from .query import QueryBuilder +from .query import QueryBuilder # from .result import TypedResult __version__ = "0.1.0" __all__ = [ - # Main API "GraphLite", "Session", - - # Transaction support "Transaction", - - # Query building - # "QueryBuilder", - - # Result handling + "QueryBuilder", # "TypedResult", - - # Errors "GraphLiteError", - - # Version - "__version__", ] diff --git a/python-sdk/src/connection.py b/python-sdk/src/connection.py index bc9887b..2bcd8aa 100644 --- a/python-sdk/src/connection.py +++ b/python-sdk/src/connection.py @@ -153,5 +153,22 @@ def transaction(self): from .transaction import Transaction return Transaction(self) + def query_builder(self): + """ + Create a new query builder for fluent query construction + + Returns: + QueryBuilder instance + + Examples: + >>> builder = session.query_builder() + >>> result = builder.match_pattern("(p:Person)") \\ + ... .where_clause("p.age > 25") \\ + ... .return_clause("p.name, p.age") \\ + ... .execute() + """ + from .query import QueryBuilder + return QueryBuilder(self) + __all__ = ['GraphLite', 'Session', 'QueryResult'] diff --git a/python-sdk/src/query.py b/python-sdk/src/query.py index e69de29..43c8f88 100644 --- a/python-sdk/src/query.py +++ b/python-sdk/src/query.py @@ -0,0 +1,156 @@ +''' +Query builder for fluent GQL query construction + +This module provides a builder API for constructing GQL queries in a +type-safe and ergonomic way. +''' + +from typing import Optional, List, TYPE_CHECKING +from .error import QueryError + +if TYPE_CHECKING: + from .connection import Session + +import sys +from pathlib import Path +bindings_path = Path(__file__).parent.parent.parent / "bindings" / "python" +if str(bindings_path) not in sys.path: + sys.path.insert(0, str(bindings_path)) + +class QueryBuilder: + ''' + Fluent API for building GQL queries + + QueryBuilder provides a convenient way to construct complex GQL queries + without manually concatenating strings. + ''' + + def __init__(self, session: 'Session'): + ''' + Initialize the query builder + ''' + self._session = session + self._match_patterns = [] + self._where_clauses = [] + self._with_clauses = [] + self._return_clause = None + self._order_by = None + self._skip = None + self._limit = None + + def match_pattern(self, pattern: str) -> 'QueryBuilder': + ''' + Add a MATCH pattern to the query + Can be called multiple times to add multiple MATCH patterns. + ''' + self._match_patterns.append(pattern) + return self + + def where_clause(self, condition: str) -> 'QueryBuilder': + ''' + Add a WHERE clause to the query + Can be called multiple times - conditions are AND'ed together. + ''' + self._where_clauses.append(condition) + return self + + def with_clause(self, clause: str) -> 'QueryBuilder': + ''' + Add a WITH clause to the query + WITH clauses are used for query chaining and intermediate results. + ''' + self._with_clauses.append(clause) + return self + + def return_clause(self, clause: str) -> 'QueryBuilder': + ''' + Set the RETURN clause to the query + Specifies what to return from the query. Required for MATCH queries. + ''' + self._return_clause = clause + return self + + def order_by(self, clause: str) -> 'QueryBuilder': + ''' + Set the ORDER BY clause to the query + ''' + self._order_by = clause + return self + + def skip(self, n: int) -> 'QueryBuilder': + ''' + Set the SKIP value to the query + Skips the first N results. + ''' + self._skip = n + return self + + def limit(self, n: int) -> 'QueryBuilder': + ''' + Set the LIMIT value to the query + Limits the number of results returned. + ''' + self._limit = n + return self + + def build(self) -> str: + ''' + Build the query string without executing + Returns the constructed GQL query as a string. + ''' + query = "" + + # MATCH clauses + for pattern in self._match_patterns: + if query: + query += " " + query += "MATCH " + query += pattern + + # WHERE clauses + if self._where_clauses: + query += " WHERE " + query += " AND ".join(self._where_clauses) + + # WITH clauses + for clause in self._with_clauses: + query += " WITH " + query += clause + + # RETURN clause + if self._return_clause: + query += " RETURN " + query += self._return_clause + + # ORDER BY clause + if self._order_by: + query += " ORDER BY " + query += self._order_by + + # SKIP clause + if self._skip is not None: + query += " SKIP " + query += str(self._skip) + + # LIMIT clause + if self._limit is not None: + query += " LIMIT " + query += str(self._limit) + + return query.strip() + + def execute(self): + ''' + Execute the query and return results + + Returns: + QueryResult with rows and metadata + + Raises: + QueryError: If query execution fails + ''' + query = self.build() + return self._session.query(query) + + +__all__ = ['QueryBuilder'] \ No newline at end of file From 0d36569c9f392153b99506734db54d2d9afedbb8 Mon Sep 17 00:00:00 2001 From: Nikhil Isukapalli Date: Fri, 21 Nov 2025 19:13:48 -0800 Subject: [PATCH 4/9] Implemented TypedResult class. All core features tested in basic_usage.py working. --- python-sdk/examples/basic_usage.py | 64 +++++----- python-sdk/src/connection.py | 13 -- python-sdk/src/result.py | 195 +++++++++++++++++++++++++++++ 3 files changed, 228 insertions(+), 44 deletions(-) create mode 100644 python-sdk/src/result.py diff --git a/python-sdk/examples/basic_usage.py b/python-sdk/examples/basic_usage.py index d4e7164..1e3bdde 100644 --- a/python-sdk/examples/basic_usage.py +++ b/python-sdk/examples/basic_usage.py @@ -23,14 +23,14 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) from src.connection import GraphLite -from src.error import GraphLiteError - +from src.error import GraphLiteError, SerializationError, QueryError, TransactionError +from src.result import TypedResult @dataclass class Person: """Person entity for typed deserialization""" name: str - age: float + age: int def main() -> int: @@ -65,8 +65,11 @@ def main() -> int: tx.execute("CREATE (p:Person {name: 'Alice', age: 30})") tx.execute("CREATE (p:Person {name: 'Bob', age: 25})") tx.execute("CREATE (p:Person {name: 'Charlie', age: 35})") + tx.execute("CREATE (p:Person {name: 'David', age: 28})") + tx.execute("CREATE (p:Person {name: 'Eve', age: 23})") + tx.execute("CREATE (p:Person {name: 'Frank', age: 40})") tx.commit() - print(" ✓ Inserted 3 persons\n") + print(" ✓ Inserted 6 persons\n") # 5. Query data directly print("5. Querying data...") @@ -95,34 +98,33 @@ def main() -> int: print(f" - Name: {name}, Age: {age}") print() - # # 7. Typed deserialization - # print("7. Using typed deserialization...") - # result = session.query("MATCH (p:Person) RETURN p.name as name, p.age as age") - # typed_result = result.to_typed() - # people = typed_result.deserialize_rows(Person) - # print(f" Deserialized {len(people)} persons:") - # for person in people: - # print(f" - {person}") - # print() - - # # 8. Transaction with rollback - # print("8. Demonstrating transaction rollback...") - # try: - # with session.transaction() as tx: - # tx.execute("CREATE (p:Person {name: 'David', age: 40})") - # print(" Created person 'David' in transaction") - # # Transaction is NOT committed - will auto-rollback - # # (by not calling tx.commit()) - # except Exception: - # pass # Expected - rollback on exception - # print(" Transaction rolled back (David not persisted)\n") - - # # 9. Verify rollback - # result = session.query("MATCH (p:Person) RETURN count(p) as count") - # if result.rows: - # count = result.rows[0].get("count") - # print(f" Person count after rollback: {count}\n") + # 7. Typed deserialization + print("7. Using typed deserialization...") + result = session.query("MATCH (p:Person) RETURN p.name as name, p.age as age") + typed = TypedResult(result) + people = typed.deserialize_rows(Person) + print(f" Deserialized {len(people)} persons:") + for person in people: + print(f" - {person}") + print() + # 8. Transaction with rollback + print("8. Demonstrating transaction rollback...") + try: + with session.transaction() as tx: + tx.execute("CREATE (p:Person {name: 'George', age: 50})") + print(" Created person 'George' in transaction") + # Transaction is NOT committed - will auto-rollback + # (by not calling tx.commit()) + except Exception: + pass # Expected - rollback on exception + print(" Transaction rolled back (George not persisted)\n") + + # 9. Verify rollback + result = session.query("MATCH (p:Person) RETURN count(p) as count") + if result.rows: + count = result.rows[0].get("count") + print(f" Person count after rollback: {count}\n") print("=== Example completed successfully ===") return 0 diff --git a/python-sdk/src/connection.py b/python-sdk/src/connection.py index 2bcd8aa..e43afb6 100644 --- a/python-sdk/src/connection.py +++ b/python-sdk/src/connection.py @@ -143,12 +143,6 @@ def transaction(self): Raises: TransactionError: If transaction cannot be started - - Examples: - >>> with session.transaction() as tx: - ... tx.execute("CREATE (p:Person {name: 'Alice'})") - ... tx.execute("CREATE (p:Person {name: 'Bob'})") - ... tx.commit() # Must commit to persist changes """ from .transaction import Transaction return Transaction(self) @@ -159,13 +153,6 @@ def query_builder(self): Returns: QueryBuilder instance - - Examples: - >>> builder = session.query_builder() - >>> result = builder.match_pattern("(p:Person)") \\ - ... .where_clause("p.age > 25") \\ - ... .return_clause("p.name, p.age") \\ - ... .execute() """ from .query import QueryBuilder return QueryBuilder(self) diff --git a/python-sdk/src/result.py b/python-sdk/src/result.py new file mode 100644 index 0000000..6eca5d9 --- /dev/null +++ b/python-sdk/src/result.py @@ -0,0 +1,195 @@ +''' +Result handling and typed deserialization + +This module provides utilities for working with query results, including +type-safe deserialization into Rust structs. +''' + +from typing import Optional, List, Any, Dict, TypeVar, Type, TYPE_CHECKING +from .error import SerializationError + +if TYPE_CHECKING: + from .connection import Session + +import sys +from pathlib import Path + +# Add bindings/python to path so we can import the low-level bindings +bindings_path = Path(__file__).parent.parent.parent / "bindings" / "python" +if str(bindings_path) not in sys.path: + sys.path.insert(0, str(bindings_path)) + +from graphlite import QueryResult + +T = TypeVar('T') + +class TypedResult: + ''' + Wrapper around QueryResult with additional type-safe methods + + TypedResult provides convenient methods for deserializing query results + into Python types. + ''' + + def __init__(self, result: QueryResult): + ''' + Create a new TypedResult from a QueryResult + ''' + self.result = result + + def row_count(self) -> int: + ''' + Get the number of rows in the result + ''' + return len(self.result.rows) + + def column_names(self) -> List[str]: + ''' + Get the column names (variables) in the result + ''' + return self.result.variables + + def get_row(self, index: int) -> Optional[Dict[str, Any]]: + ''' + Get a specific row by index + ''' + if 0 <= index < len(self.result.rows): + return self.result.rows[index] + return None + + def deserialize_rows(self, target_type: Type[T]) -> List[T]: + ''' + Deserialize all rows into instances of the target type + + Args: + target_type: A class/type to deserialize into (e.g., a dataclass) + + Returns: + List of deserialized objects + + Raises: + SerializationError: If deserialization fails + + Examples: + >>> @dataclass + >>> class Person: + ... name: str + ... age: int + >>> + >>> result = session.query("MATCH (p:Person) RETURN p.name as name, p.age as age") + >>> typed = TypedResult(result) + >>> people = typed.deserialize_rows(Person) + ''' + try: + objects = [] + for row in self.result.rows: + obj = self.deserialize_row(row, target_type) + objects.append(obj) + return objects + except Exception as e: + raise SerializationError(f"Failed to deserialize rows: {e}") + + def deserialize_row(self, row: Dict[str, Any], target_type: Type[T]) -> T: + ''' + Deserialize a single row dict into an instance of the target type + + Args: + row: Row dictionary to deserialize + target_type: A class/type to deserialize into + + Returns: + Deserialized object + + Raises: + SerializationError: If deserialization fails + + Examples: + >>> row = result.rows[0] + >>> first_person = typed.deserialize_row(row, Person) + ''' + try: + return target_type(**row) + except Exception as e: + raise SerializationError(f"Failed to deserialize row: {e}") + + def first(self, target_type: Type[T]) -> T: + ''' + Get the first row as the given type + + Convenience method for queries that return a single row. + + Args: + target_type: A class/type to deserialize into + + Returns: + Deserialized object from the first row + + Raises: + SerializationError: If no rows exist or deserialization fails + + Examples: + >>> @dataclass + >>> class Count: + ... count: int + >>> + >>> result = session.query("MATCH (p:Person) RETURN count(p) as count") + >>> typed = TypedResult(result) + >>> count_obj = typed.first(Count) + ''' + if self.is_empty(): + raise SerializationError("No rows returned") + + return self.deserialize_row(self.result.rows[0], target_type) + + def scalar(self) -> Any: + ''' + Get a single value from the first row and first column + + Useful for queries that return a single scalar value. + + Returns: + The scalar value from the first row, first column + + Raises: + SerializationError: If no rows or columns exist + + Examples: + >>> result = session.query("MATCH (p:Person) RETURN count(p)") + >>> typed = TypedResult(result) + >>> count = typed.scalar() # Returns the count value directly + ''' + if self.is_empty(): + raise SerializationError("No rows returned") + + columns = self.result.variables + if not columns: + raise SerializationError("No columns returned") + + first_row = self.result.rows[0] + first_column = columns[0] + + if first_column not in first_row: + raise SerializationError(f"Column '{first_column}' not found in row") + + return first_row[first_column] + + def is_empty(self) -> bool: + ''' + Check if the result is empty (no rows) + + Returns: + True if result has no rows, False otherwise + ''' + return len(self.result.rows) == 0 + + def rows(self) -> List[Dict[str, Any]]: + ''' + Get all rows + + Returns: + List of row dictionaries + ''' + return self.result.rows + + +__all__ = ['TypedResult'] \ No newline at end of file From d6ba0f36db4dd4c5b9d44a2df3f5d4476d8af5e8 Mon Sep 17 00:00:00 2001 From: Nikhil Isukapalli Date: Fri, 21 Nov 2025 19:17:07 -0800 Subject: [PATCH 5/9] Added README.md for Python SDK --- python-sdk/README.md | 490 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 490 insertions(+) create mode 100644 python-sdk/README.md diff --git a/python-sdk/README.md b/python-sdk/README.md new file mode 100644 index 0000000..adf2e88 --- /dev/null +++ b/python-sdk/README.md @@ -0,0 +1,490 @@ +# GraphLite Python SDK + +High-level, ergonomic Python SDK for GraphLite - the fast, embedded graph database. + +## Overview + +The GraphLite Python SDK provides a developer-friendly API for working with GraphLite databases in Python applications. It wraps the low-level FFI bindings with a clean, intuitive interface following patterns from popular embedded databases like SQLite. + +## Features + +- ✅ **Simple API** - Clean, Pythonic interface following SQLite conventions +- ✅ **Session Management** - User context and permissions support +- ✅ **Transactions** - ACID guarantees with automatic rollback (context managers) +- ✅ **Query Builder** - Fluent API for constructing GQL queries +- ✅ **Typed Results** - Deserialize query results into Python dataclasses +- ✅ **Zero External Dependencies** - Fully embedded, no server required +- ✅ **Type Hints** - Full type annotation support for better IDE integration + +## Installation + +```bash +# Install from source (for now) +cd python-sdk +pip install -e . +``` + +## Quick Start + +Basic usage: + +```python +from graphlite_sdk import GraphLite + +# Open database +db = GraphLite.open("./mydb") + +# Create session +session = db.session("admin") + +# Execute query +result = session.query("MATCH (p:Person) RETURN p.name") + +for row in result.rows: + print(row) +``` + +## Core Concepts + +### Opening a Database + +GraphLite is an embedded database - no server required. Just open a directory: + +```python +from graphlite_sdk import GraphLite + +db = GraphLite.open("./mydb") +``` + +This creates or opens a database at the specified path. + +### Sessions + +Unlike SQLite, GraphLite uses sessions for user context and permissions: + +```python +session = db.session("username") +``` + +Sessions provide: +- User authentication and authorization +- Transaction isolation +- Audit logging + +### Executing Queries + +Simple query execution: + +```python +result = session.query("MATCH (n:Person) RETURN n") +``` + +Or for statements that don't return results: + +```python +session.execute("CREATE (p:Person {name: 'Alice'})") +``` + +### Transactions + +Transactions use Python context managers with automatic rollback: + +```python +# Transaction with explicit commit +with session.transaction() as tx: + tx.execute("CREATE (p:Person {name: 'Alice'})") + tx.execute("CREATE (p:Person {name: 'Bob'})") + tx.commit() # Persist changes + +# Transaction with automatic rollback +with session.transaction() as tx: + tx.execute("CREATE (p:Person {name: 'Charlie'})") + # tx exits without commit - changes are automatically rolled back +``` + +### Query Builder + +Build queries fluently: + +```python +result = (session.query_builder() + .match_pattern("(p:Person)") + .where_clause("p.age > 25") + .return_clause("p.name, p.age") + .order_by("p.age DESC") + .limit(10) + .execute()) +``` + +### Typed Results + +Deserialize results into Python dataclasses: + +```python +from dataclasses import dataclass +from graphlite_sdk import TypedResult + +@dataclass +class Person: + name: str + age: int + +result = session.query("MATCH (p:Person) RETURN p.name as name, p.age as age") +typed = TypedResult(result) +people = typed.deserialize_rows(Person) + +for person in people: + print(f"{person.name} is {person.age} years old") +``` + +## Examples + +### Basic CRUD Operations + +```python +from graphlite_sdk import GraphLite + +db = GraphLite.open("./mydb") +session = db.session("admin") + +# Create schema and graph +session.execute("CREATE SCHEMA IF NOT EXISTS /example") +session.execute("SESSION SET SCHEMA /example") +session.execute("CREATE GRAPH IF NOT EXISTS social") +session.execute("SESSION SET GRAPH social") + +# Create nodes +session.execute("CREATE (p:Person {name: 'Alice', age: 30})") +session.execute("CREATE (p:Person {name: 'Bob', age: 25})") + +# Create relationships +session.execute(""" + MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'}) + CREATE (a)-[:KNOWS]->(b) +""") + +# Query +result = session.query(""" + MATCH (p:Person)-[:KNOWS]->(f:Person) + RETURN p.name as person, f.name as friend +""") + +for row in result.rows: + print(f"{row['person']} knows {row['friend']}") +``` + +### Transaction Example + +```python +from graphlite_sdk import GraphLite + +db = GraphLite.open("./mydb") +session = db.session("admin") + +with session.transaction() as tx: + # Delete old relationship + tx.execute("MATCH (a)-[r:FOLLOWS]->(b) WHERE a.name = 'Alice' DELETE r") + + # Create new relationship + tx.execute(""" + MATCH (a {name: 'Alice'}), (c {name: 'Charlie'}) + CREATE (a)-[:FOLLOWS]->(c) + """) + + tx.commit() +``` + +### Query Builder Example + +```python +from graphlite_sdk import GraphLite + +db = GraphLite.open("./mydb") +session = db.session("admin") + +result = (session.query_builder() + .match_pattern("(u:User)") + .where_clause("u.status = 'active'") + .where_clause("u.lastLogin > '2024-01-01'") + .return_clause("u.name, u.email") + .order_by("u.lastLogin DESC") + .limit(20) + .execute()) + +for row in result.rows: + print(f"{row['name']}: {row['email']}") +``` + +### Typed Deserialization Example + +```python +from dataclasses import dataclass +from graphlite_sdk import GraphLite, TypedResult + +@dataclass +class User: + name: str + email: str + age: int + +db = GraphLite.open("./mydb") +session = db.session("admin") + +result = session.query( + "MATCH (u:User) RETURN u.name as name, u.email as email, u.age as age" +) + +typed = TypedResult(result) +users = typed.deserialize_rows(User) + +for user in users: + print(f"{user.name} ({user.email}): {user.age}") +``` + +### Scalar and First Methods + +```python +from dataclasses import dataclass +from graphlite_sdk import TypedResult + +# Get a scalar value (single value from first row, first column) +result = session.query("MATCH (p:Person) RETURN count(p)") +typed = TypedResult(result) +count = typed.scalar() +print(f"Total persons: {count}") + +# Get first row as typed object +@dataclass +class Count: + count: int + +result = session.query("MATCH (p:Person) RETURN count(p) as count") +typed = TypedResult(result) +count_obj = typed.first(Count) +print(f"Total persons: {count_obj.count}") +``` + +## API Comparison with SQLite + +GraphLite SDK follows similar patterns to Python's sqlite3 but adapted for graph databases: + +| Operation | sqlite3 (SQLite) | graphlite-sdk (GraphLite) | +|-----------|------------------|---------------------------| +| Open DB | `sqlite3.connect()` | `GraphLite.open()` | +| Execute | `conn.execute()` | `session.execute()` | +| Query | `conn.execute()` | `session.query()` | +| Transaction | `with conn:` | `with session.transaction():` | +| Commit | `conn.commit()` | `tx.commit()` | +| Rollback | `conn.rollback()` | `tx.rollback()` or auto | + +**Key Differences:** +- GraphLite uses **sessions** for user context (SQLite doesn't have sessions) +- GraphLite uses **GQL** (Graph Query Language) instead of SQL +- GraphLite is optimized for **graph data** (nodes, edges, paths) + +## Architecture + +```text +Your Application + │ + ▼ +┌─────────────────────────────────────────┐ +│ GraphLite SDK (this package) │ +│ - GraphLite (main API) │ ← You are here +│ - Session (session management) │ +│ - Transaction (ACID support) │ +│ - QueryBuilder (fluent queries) │ +│ - TypedResult (deserialization) │ +└─────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ GraphLite FFI Bindings │ +│ (Low-level ctypes wrapper) │ +└─────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ GraphLite Core (Rust) │ +│ - QueryCoordinator │ +│ - Storage Engine │ +│ - Catalog Manager │ +└─────────────────────────────────────────┘ +``` + +## Language Bindings + +The GraphLite Python SDK is specifically for **Python applications**. For other languages: + +- **Rust** - Use `graphlite-sdk/` (native Rust SDK) +- **Java** - Use `bindings/java/` (via JNI) +- **JavaScript/Node.js** - Use `bindings/javascript/` (via FFI/WASM) +- **Kotlin** - Use `bindings/kotlin/` (via JNI) + +See the main [MULTI_LANGUAGE_BINDINGS_DESIGN.md](../MULTI_LANGUAGE_BINDINGS_DESIGN.md) for details. + +## Performance + +GraphLite Python SDK provides good performance via FFI: +- Direct ctypes FFI calls (minimal overhead) +- JSON serialization only for query results +- Python-native data structures + +Benchmark comparison: +- **Rust SDK**: ~100% of native performance +- **Python SDK** (via FFI): ~80-90% of native +- **JavaScript bindings** (via WASM): ~70-80% of native + +## Error Handling + +The SDK uses a comprehensive error hierarchy: + +```python +from graphlite_sdk import ( + GraphLiteError, # Base error class + ConnectionError, # Database connection errors + SessionError, # Session management errors + QueryError, # Query execution errors + TransactionError, # Transaction errors + SerializationError, # Deserialization errors +) + +try: + db = GraphLite.open("./mydb") + session = db.session("admin") + result = session.query("MATCH (n) RETURN n") +except ConnectionError as e: + print(f"Failed to connect: {e}") +except QueryError as e: + print(f"Query failed: {e}") +except GraphLiteError as e: + print(f"GraphLite error: {e}") +``` + +## Examples + +Run the examples: + +```bash +# Basic usage example +python3 examples/basic_usage.py + +# More examples coming soon +``` + +## Development + +To work on the SDK: + +```bash +# Install in development mode +cd python-sdk +pip install -e . + +# Run tests (coming soon) +pytest + +# Type checking +mypy src/ +``` + +## API Reference + +### GraphLite + +```python +class GraphLite: + @classmethod + def open(cls, path: str) -> GraphLite + + def session(self, username: str) -> Session + + def close(self) -> None +``` + +### Session + +```python +class Session: + def query(self, query: str) -> QueryResult + + def execute(self, statement: str) -> None + + def transaction(self) -> Transaction + + def query_builder(self) -> QueryBuilder +``` + +### Transaction + +```python +class Transaction: + def execute(self, statement: str) -> None + + def query(self, query: str) -> QueryResult + + def commit(self) -> None + + def rollback(self) -> None +``` + +### QueryBuilder + +```python +class QueryBuilder: + def match_pattern(self, pattern: str) -> QueryBuilder + + def where_clause(self, condition: str) -> QueryBuilder + + def with_clause(self, clause: str) -> QueryBuilder + + def return_clause(self, clause: str) -> QueryBuilder + + def order_by(self, clause: str) -> QueryBuilder + + def skip(self, n: int) -> QueryBuilder + + def limit(self, n: int) -> QueryBuilder + + def build(self) -> str + + def execute(self) -> QueryResult +``` + +### TypedResult + +```python +class TypedResult: + def row_count(self) -> int + + def column_names(self) -> List[str] + + def get_row(self, index: int) -> Optional[Dict[str, Any]] + + def deserialize_rows(self, target_type: Type[T]) -> List[T] + + def deserialize_row(self, row: Dict[str, Any], target_type: Type[T]) -> T + + def first(self, target_type: Type[T]) -> T + + def scalar(self) -> Any + + def is_empty(self) -> bool + + def rows(self) -> List[Dict[str, Any]] +``` + +## Contributing + +Contributions welcome! Areas where help is needed: + +- **ORM Features** - Decorators for mapping classes to graph nodes +- **Query Validation** - Runtime query validation +- **Async Support** - asyncio integration +- **Connection Pooling** - Multi-threaded access patterns +- **Graph Algorithms** - Built-in graph algorithms (shortest path, centrality, etc.) +- **Documentation** - More examples and tutorials + +## License + +Apache-2.0 - See [LICENSE](../LICENSE) for details. From d74dc3d6736afa285bb09d216243823a6247841a Mon Sep 17 00:00:00 2001 From: Nikhil Isukapalli Date: Fri, 21 Nov 2025 19:38:58 -0800 Subject: [PATCH 6/9] Changed CREATE to INSERT --- python-sdk/examples/basic_usage.py | 16 ++++++++-------- python-sdk/src/__init__.py | 2 +- python-sdk/src/transaction.py | 16 ++++++++-------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/python-sdk/examples/basic_usage.py b/python-sdk/examples/basic_usage.py index 1e3bdde..946fd92 100644 --- a/python-sdk/examples/basic_usage.py +++ b/python-sdk/examples/basic_usage.py @@ -62,12 +62,12 @@ def main() -> int: # 4. Insert data using transactions print("4. Inserting data with transaction...") with session.transaction() as tx: - tx.execute("CREATE (p:Person {name: 'Alice', age: 30})") - tx.execute("CREATE (p:Person {name: 'Bob', age: 25})") - tx.execute("CREATE (p:Person {name: 'Charlie', age: 35})") - tx.execute("CREATE (p:Person {name: 'David', age: 28})") - tx.execute("CREATE (p:Person {name: 'Eve', age: 23})") - tx.execute("CREATE (p:Person {name: 'Frank', age: 40})") + tx.execute("INSERT (p:Person {name: 'Alice', age: 30})") + tx.execute("INSERT (p:Person {name: 'Bob', age: 25})") + tx.execute("INSERT (p:Person {name: 'Charlie', age: 35})") + tx.execute("INSERT (p:Person {name: 'David', age: 28})") + tx.execute("INSERT (p:Person {name: 'Eve', age: 23})") + tx.execute("INSERT (p:Person {name: 'Frank', age: 40})") tx.commit() print(" ✓ Inserted 6 persons\n") @@ -112,8 +112,8 @@ def main() -> int: print("8. Demonstrating transaction rollback...") try: with session.transaction() as tx: - tx.execute("CREATE (p:Person {name: 'George', age: 50})") - print(" Created person 'George' in transaction") + tx.execute("INSERT (p:Person {name: 'George', age: 50})") + print(" Inserted person 'George' in transaction") # Transaction is NOT committed - will auto-rollback # (by not calling tx.commit()) except Exception: diff --git a/python-sdk/src/__init__.py b/python-sdk/src/__init__.py index 2a36e0e..f2b3e93 100644 --- a/python-sdk/src/__init__.py +++ b/python-sdk/src/__init__.py @@ -22,7 +22,7 @@ # Use transactions with session.transaction() as tx: - tx.execute("CREATE (p:Person {name: 'Alice'})") + tx.execute("INSERT (p:Person {name: 'Alice'})") tx.commit() ``` diff --git a/python-sdk/src/transaction.py b/python-sdk/src/transaction.py index ddf596c..bf630f7 100644 --- a/python-sdk/src/transaction.py +++ b/python-sdk/src/transaction.py @@ -39,13 +39,13 @@ class Transaction: >>> >>> # Using as context manager (recommended) >>> with session.transaction() as tx: - ... tx.execute("CREATE (p:Person {name: 'Alice'})") - ... tx.execute("CREATE (p:Person {name: 'Bob'})") + ... tx.execute("INSERT (p:Person {name: 'Alice'})") + ... tx.execute("INSERT (p:Person {name: 'Bob'})") ... tx.commit() # Changes are persisted >>> >>> # Transaction that rolls back (no commit) >>> with session.transaction() as tx: - ... tx.execute("CREATE (p:Person {name: 'Charlie'})") + ... tx.execute("INSERT (p:Person {name: 'Charlie'})") ... # Automatically rolled back on context exit """ @@ -77,8 +77,8 @@ def execute(self, statement: str) -> None: Examples: >>> with session.transaction() as tx: - ... tx.execute("CREATE (p:Person {name: 'Alice'})") - ... tx.execute("CREATE (p:Person {name: 'Bob'})") + ... tx.execute("INSERT (p:Person {name: 'Alice'})") + ... tx.execute("INSERT (p:Person {name: 'Bob'})") ... tx.commit() """ if self._committed: @@ -106,7 +106,7 @@ def query(self, query: str) -> QueryResult: Examples: >>> with session.transaction() as tx: - ... tx.execute("CREATE (p:Person {name: 'Alice', age: 30})") + ... tx.execute("INSERT (p:Person {name: 'Alice', age: 30})") ... result = tx.query("MATCH (p:Person) RETURN p") ... tx.commit() """ @@ -132,7 +132,7 @@ def commit(self) -> None: Examples: >>> with session.transaction() as tx: - ... tx.execute("CREATE (p:Person {name: 'Alice'})") + ... tx.execute("INSERT (p:Person {name: 'Alice'})") ... tx.commit() # Changes are now persistent """ if self._committed: @@ -159,7 +159,7 @@ def rollback(self) -> None: Examples: >>> with session.transaction() as tx: - ... tx.execute("CREATE (p:Person {name: 'Alice'})") + ... tx.execute("INSERT (p:Person {name: 'Alice'})") ... tx.rollback() # Explicit rollback (optional, automatic on exit) """ if self._committed: From 9531e8d421fbdee589c827f9ca060a1694412b00 Mon Sep 17 00:00:00 2001 From: Nikhil Isukapalli Date: Fri, 21 Nov 2025 19:39:47 -0800 Subject: [PATCH 7/9] Deleted emojis from README --- python-sdk/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/python-sdk/README.md b/python-sdk/README.md index adf2e88..bc0395f 100644 --- a/python-sdk/README.md +++ b/python-sdk/README.md @@ -8,13 +8,13 @@ The GraphLite Python SDK provides a developer-friendly API for working with Grap ## Features -- ✅ **Simple API** - Clean, Pythonic interface following SQLite conventions -- ✅ **Session Management** - User context and permissions support -- ✅ **Transactions** - ACID guarantees with automatic rollback (context managers) -- ✅ **Query Builder** - Fluent API for constructing GQL queries -- ✅ **Typed Results** - Deserialize query results into Python dataclasses -- ✅ **Zero External Dependencies** - Fully embedded, no server required -- ✅ **Type Hints** - Full type annotation support for better IDE integration +- **Simple API** - Clean, Pythonic interface following SQLite conventions +- **Session Management** - User context and permissions support +- **Transactions** - ACID guarantees with automatic rollback (context managers) +- **Query Builder** - Fluent API for constructing GQL queries +- **Typed Results** - Deserialize query results into Python dataclasses +- **Zero External Dependencies** - Fully embedded, no server required +- **Type Hints** - Full type annotation support for better IDE integration ## Installation From 01aa2cc5e3e60982c194c331f63afe405220a604 Mon Sep 17 00:00:00 2001 From: Nikhil Isukapalli Date: Fri, 21 Nov 2025 19:48:46 -0800 Subject: [PATCH 8/9] Added TypedResult to __all__ in __init__.py --- python-sdk/src/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-sdk/src/__init__.py b/python-sdk/src/__init__.py index f2b3e93..e47eb70 100644 --- a/python-sdk/src/__init__.py +++ b/python-sdk/src/__init__.py @@ -71,6 +71,6 @@ "Session", "Transaction", "QueryBuilder", - # "TypedResult", + "TypedResult", "GraphLiteError", ] From b291a39ec25dc10fb377fc6a2e4c81aa28db2e5a Mon Sep 17 00:00:00 2001 From: Nikhil Isukapalli Date: Fri, 21 Nov 2025 19:54:04 -0800 Subject: [PATCH 9/9] Changed CREATE to INSERT in python-sdk/README.md --- python-sdk/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/python-sdk/README.md b/python-sdk/README.md index bc0395f..d3190fd 100644 --- a/python-sdk/README.md +++ b/python-sdk/README.md @@ -82,7 +82,7 @@ result = session.query("MATCH (n:Person) RETURN n") Or for statements that don't return results: ```python -session.execute("CREATE (p:Person {name: 'Alice'})") +session.execute("INSERT (p:Person {name: 'Alice'})") ``` ### Transactions @@ -92,13 +92,13 @@ Transactions use Python context managers with automatic rollback: ```python # Transaction with explicit commit with session.transaction() as tx: - tx.execute("CREATE (p:Person {name: 'Alice'})") - tx.execute("CREATE (p:Person {name: 'Bob'})") + tx.execute("INSERT (p:Person {name: 'Alice'})") + tx.execute("INSERT (p:Person {name: 'Bob'})") tx.commit() # Persist changes # Transaction with automatic rollback with session.transaction() as tx: - tx.execute("CREATE (p:Person {name: 'Charlie'})") + tx.execute("INSERT (p:Person {name: 'Charlie'})") # tx exits without commit - changes are automatically rolled back ``` @@ -154,13 +154,13 @@ session.execute("CREATE GRAPH IF NOT EXISTS social") session.execute("SESSION SET GRAPH social") # Create nodes -session.execute("CREATE (p:Person {name: 'Alice', age: 30})") -session.execute("CREATE (p:Person {name: 'Bob', age: 25})") +session.execute("INSERT (p:Person {name: 'Alice', age: 30})") +session.execute("INSERT (p:Person {name: 'Bob', age: 25})") # Create relationships session.execute(""" MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'}) - CREATE (a)-[:KNOWS]->(b) + INSERT (a)-[:KNOWS]->(b) """) # Query @@ -188,7 +188,7 @@ with session.transaction() as tx: # Create new relationship tx.execute(""" MATCH (a {name: 'Alice'}), (c {name: 'Charlie'}) - CREATE (a)-[:FOLLOWS]->(c) + INSERT (a)-[:FOLLOWS]->(c) """) tx.commit()