commitdb 1.2.0__py3-none-any.whl

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.
commitdb/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ """
2
+ CommitDB Python Driver
3
+
4
+ A Python client for connecting to CommitDB SQL Server.
5
+
6
+ Usage:
7
+ from commitdb import CommitDB
8
+
9
+ db = CommitDB('localhost', 3306)
10
+ db.connect()
11
+
12
+ # Create database and table
13
+ db.execute('CREATE DATABASE mydb')
14
+ db.execute('CREATE TABLE mydb.users (id INT PRIMARY KEY, name STRING)')
15
+
16
+ # Insert data
17
+ db.execute("INSERT INTO mydb.users (id, name) VALUES (1, 'Alice')")
18
+
19
+ # Query data
20
+ result = db.query('SELECT * FROM mydb.users')
21
+ for row in result:
22
+ print(row)
23
+
24
+ db.close()
25
+
26
+ Embedded mode (requires libcommitdb):
27
+ from commitdb import CommitDBLocal
28
+
29
+ with CommitDBLocal('/path/to/data') as db:
30
+ db.execute('CREATE DATABASE mydb')
31
+ """
32
+
33
+ from .client import CommitDB, CommitDBLocal, QueryResult, CommitResult, CommitDBError
34
+
35
+ __version__ = '0.1.0'
36
+ __all__ = ['CommitDB', 'CommitDBLocal', 'QueryResult', 'CommitResult', 'CommitDBError']
commitdb/binding.py ADDED
@@ -0,0 +1,135 @@
1
+ """
2
+ Python ctypes bindings for libcommitdb shared library.
3
+ """
4
+
5
+ import ctypes
6
+ import json
7
+ import os
8
+ import platform
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+
13
+ def _find_library() -> Optional[str]:
14
+ """Find the libcommitdb shared library."""
15
+ system = platform.system()
16
+
17
+ if system == 'Darwin':
18
+ lib_names = ['libcommitdb.dylib', 'libcommitdb-darwin-arm64.dylib', 'libcommitdb-darwin-amd64.dylib']
19
+ elif system == 'Linux':
20
+ lib_names = ['libcommitdb.so', 'libcommitdb-linux-amd64.so', 'libcommitdb-linux-arm64.so']
21
+ elif system == 'Windows':
22
+ lib_names = ['libcommitdb.dll']
23
+ else:
24
+ lib_names = ['libcommitdb.so', 'libcommitdb.dylib']
25
+
26
+ # Search paths
27
+ search_paths = [
28
+ Path(__file__).parent / 'lib', # Package lib directory (for pip installed)
29
+ Path(__file__).parent, # Same directory as this file
30
+ Path(__file__).parent.parent / 'lib', # drivers/python/lib
31
+ Path(__file__).parent.parent.parent.parent / 'lib', # CommitDB/lib
32
+ Path.cwd() / 'lib', # ./lib
33
+ Path.cwd(), # Current directory
34
+ ]
35
+
36
+ for path in search_paths:
37
+ for lib_name in lib_names:
38
+ lib_path = path / lib_name
39
+ if lib_path.exists():
40
+ return str(lib_path)
41
+
42
+ return None
43
+
44
+
45
+ class CommitDBBinding:
46
+ """Low-level ctypes bindings for libcommitdb."""
47
+
48
+ _lib: Optional[ctypes.CDLL] = None
49
+ _lib_path: Optional[str] = None
50
+
51
+ @classmethod
52
+ def load(cls, lib_path: Optional[str] = None) -> 'CommitDBBinding':
53
+ """Load the shared library."""
54
+ if lib_path is None:
55
+ lib_path = _find_library()
56
+
57
+ if lib_path is None:
58
+ raise RuntimeError(
59
+ "Could not find libcommitdb shared library. "
60
+ "Build it with: make lib"
61
+ )
62
+
63
+ cls._lib_path = lib_path
64
+ cls._lib = ctypes.CDLL(lib_path)
65
+
66
+ # Define function signatures
67
+ cls._lib.commitdb_open_memory.argtypes = []
68
+ cls._lib.commitdb_open_memory.restype = ctypes.c_int
69
+
70
+ cls._lib.commitdb_open_file.argtypes = [ctypes.c_char_p]
71
+ cls._lib.commitdb_open_file.restype = ctypes.c_int
72
+
73
+ cls._lib.commitdb_close.argtypes = [ctypes.c_int]
74
+ cls._lib.commitdb_close.restype = None
75
+
76
+ # Use c_void_p for the result pointer to avoid automatic conversion
77
+ cls._lib.commitdb_execute.argtypes = [ctypes.c_int, ctypes.c_char_p]
78
+ cls._lib.commitdb_execute.restype = ctypes.c_void_p
79
+
80
+ cls._lib.commitdb_free.argtypes = [ctypes.c_void_p]
81
+ cls._lib.commitdb_free.restype = None
82
+
83
+ return cls()
84
+
85
+ @classmethod
86
+ def is_loaded(cls) -> bool:
87
+ """Check if the library is loaded."""
88
+ return cls._lib is not None
89
+
90
+ @classmethod
91
+ def open_memory(cls) -> int:
92
+ """Open an in-memory database."""
93
+ if not cls.is_loaded():
94
+ cls.load()
95
+ handle = cls._lib.commitdb_open_memory()
96
+ if handle < 0:
97
+ raise RuntimeError("Failed to open in-memory database")
98
+ return handle
99
+
100
+ @classmethod
101
+ def open_file(cls, path: str) -> int:
102
+ """Open a file-based database."""
103
+ if not cls.is_loaded():
104
+ cls.load()
105
+ handle = cls._lib.commitdb_open_file(path.encode('utf-8'))
106
+ if handle < 0:
107
+ raise RuntimeError(f"Failed to open database at {path}")
108
+ return handle
109
+
110
+ @classmethod
111
+ def close(cls, handle: int) -> None:
112
+ """Close a database handle."""
113
+ if cls.is_loaded():
114
+ cls._lib.commitdb_close(handle)
115
+
116
+ @classmethod
117
+ def execute(cls, handle: int, query: str) -> dict:
118
+ """Execute a query and return the JSON response."""
119
+ if not cls.is_loaded():
120
+ raise RuntimeError("Library not loaded")
121
+
122
+ result_ptr = cls._lib.commitdb_execute(handle, query.encode('utf-8'))
123
+ if result_ptr is None or result_ptr == 0:
124
+ raise RuntimeError("Query execution failed")
125
+
126
+ try:
127
+ # Cast void pointer to char pointer and read the string
128
+ result_str = ctypes.cast(result_ptr, ctypes.c_char_p).value
129
+ if result_str is None:
130
+ raise RuntimeError("Empty response from query")
131
+ result_json = result_str.decode('utf-8')
132
+ return json.loads(result_json)
133
+ finally:
134
+ # Free the allocated memory using the void pointer
135
+ cls._lib.commitdb_free(result_ptr)
commitdb/client.py ADDED
@@ -0,0 +1,379 @@
1
+ """
2
+ CommitDB Client - Python driver for CommitDB SQL Server.
3
+ """
4
+
5
+ import json
6
+ import socket
7
+ from dataclasses import dataclass, field
8
+ from typing import Iterator, Optional
9
+
10
+
11
+ class CommitDBError(Exception):
12
+ """Exception raised for CommitDB errors."""
13
+ pass
14
+
15
+
16
+ @dataclass
17
+ class QueryResult:
18
+ """Result from a SELECT query."""
19
+ columns: list[str]
20
+ data: list[list[str]]
21
+ records_read: int
22
+ time_ms: float
23
+
24
+ def __iter__(self) -> Iterator[dict[str, str]]:
25
+ """Iterate over rows as dictionaries."""
26
+ for row in self.data:
27
+ yield dict(zip(self.columns, row))
28
+
29
+ def __len__(self) -> int:
30
+ return len(self.data)
31
+
32
+ def __getitem__(self, index: int) -> dict[str, str]:
33
+ return dict(zip(self.columns, self.data[index]))
34
+
35
+
36
+ @dataclass
37
+ class CommitResult:
38
+ """Result from a mutation operation (INSERT, UPDATE, DELETE, CREATE, DROP)."""
39
+ databases_created: int = 0
40
+ databases_deleted: int = 0
41
+ tables_created: int = 0
42
+ tables_deleted: int = 0
43
+ records_written: int = 0
44
+ records_deleted: int = 0
45
+ time_ms: float = 0.0
46
+
47
+ @property
48
+ def affected_rows(self) -> int:
49
+ """Total number of affected rows/objects."""
50
+ return (self.databases_created + self.databases_deleted +
51
+ self.tables_created + self.tables_deleted +
52
+ self.records_written + self.records_deleted)
53
+
54
+
55
+ class CommitDB:
56
+ """
57
+ CommitDB Python client.
58
+
59
+ Example:
60
+ db = CommitDB('localhost', 3306)
61
+ db.connect()
62
+ result = db.query('SELECT * FROM mydb.users')
63
+ db.close()
64
+ """
65
+
66
+ def __init__(self, host: str = 'localhost', port: int = 3306):
67
+ """
68
+ Initialize CommitDB client.
69
+
70
+ Args:
71
+ host: Server hostname
72
+ port: Server port (default 3306)
73
+ """
74
+ self.host = host
75
+ self.port = port
76
+ self._socket: Optional[socket.socket] = None
77
+ self._buffer = b''
78
+
79
+ def connect(self, timeout: float = 10.0) -> 'CommitDB':
80
+ """
81
+ Connect to the CommitDB server.
82
+
83
+ Args:
84
+ timeout: Connection timeout in seconds
85
+
86
+ Returns:
87
+ self for method chaining
88
+ """
89
+ self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
90
+ self._socket.settimeout(timeout)
91
+ self._socket.connect((self.host, self.port))
92
+ return self
93
+
94
+ def close(self) -> None:
95
+ """Close the connection."""
96
+ if self._socket:
97
+ try:
98
+ self._socket.send(b'quit\n')
99
+ except Exception:
100
+ pass
101
+ self._socket.close()
102
+ self._socket = None
103
+
104
+ def __enter__(self) -> 'CommitDB':
105
+ return self.connect()
106
+
107
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
108
+ self.close()
109
+
110
+ def _send(self, query: str) -> dict:
111
+ """Send a query and receive the response."""
112
+ if not self._socket:
113
+ raise CommitDBError("Not connected. Call connect() first.")
114
+
115
+ # Send query with newline
116
+ self._socket.send((query + '\n').encode('utf-8'))
117
+
118
+ # Read response until newline
119
+ while b'\n' not in self._buffer:
120
+ chunk = self._socket.recv(4096)
121
+ if not chunk:
122
+ raise CommitDBError("Connection closed by server")
123
+ self._buffer += chunk
124
+
125
+ # Split at first newline
126
+ line, self._buffer = self._buffer.split(b'\n', 1)
127
+
128
+ # Parse JSON response
129
+ try:
130
+ return json.loads(line.decode('utf-8'))
131
+ except json.JSONDecodeError as e:
132
+ raise CommitDBError(f"Invalid response from server: {e}")
133
+
134
+ def execute(self, query: str) -> CommitResult | QueryResult:
135
+ """
136
+ Execute a SQL query.
137
+
138
+ Args:
139
+ query: SQL query to execute
140
+
141
+ Returns:
142
+ QueryResult for SELECT queries, CommitResult for mutations
143
+ """
144
+ response = self._send(query)
145
+
146
+ if not response.get('success'):
147
+ raise CommitDBError(response.get('error', 'Unknown error'))
148
+
149
+ result_type = response.get('type')
150
+ result_data = response.get('result', {})
151
+
152
+ if result_type == 'query':
153
+ return QueryResult(
154
+ columns=result_data.get('columns', []),
155
+ data=result_data.get('data', []),
156
+ records_read=result_data.get('records_read', 0),
157
+ time_ms=result_data.get('time_ms', 0.0)
158
+ )
159
+ elif result_type == 'commit':
160
+ return CommitResult(
161
+ databases_created=result_data.get('databases_created', 0),
162
+ databases_deleted=result_data.get('databases_deleted', 0),
163
+ tables_created=result_data.get('tables_created', 0),
164
+ tables_deleted=result_data.get('tables_deleted', 0),
165
+ records_written=result_data.get('records_written', 0),
166
+ records_deleted=result_data.get('records_deleted', 0),
167
+ time_ms=result_data.get('time_ms', 0.0)
168
+ )
169
+ else:
170
+ # Unknown type, return empty commit result
171
+ return CommitResult()
172
+
173
+ def query(self, sql: str) -> QueryResult:
174
+ """
175
+ Execute a SELECT query and return results.
176
+
177
+ Args:
178
+ sql: SELECT query
179
+
180
+ Returns:
181
+ QueryResult with columns and data
182
+ """
183
+ result = self.execute(sql)
184
+ if not isinstance(result, QueryResult):
185
+ raise CommitDBError("Expected query result, got commit result")
186
+ return result
187
+
188
+ def create_database(self, name: str) -> CommitResult:
189
+ """Create a database."""
190
+ result = self.execute(f'CREATE DATABASE {name}')
191
+ if not isinstance(result, CommitResult):
192
+ raise CommitDBError("Expected commit result")
193
+ return result
194
+
195
+ def drop_database(self, name: str) -> CommitResult:
196
+ """Drop a database."""
197
+ result = self.execute(f'DROP DATABASE {name}')
198
+ if not isinstance(result, CommitResult):
199
+ raise CommitDBError("Expected commit result")
200
+ return result
201
+
202
+ def create_table(self, database: str, table: str, columns: str) -> CommitResult:
203
+ """
204
+ Create a table.
205
+
206
+ Args:
207
+ database: Database name
208
+ table: Table name
209
+ columns: Column definitions, e.g. "id INT PRIMARY KEY, name STRING"
210
+ """
211
+ result = self.execute(f'CREATE TABLE {database}.{table} ({columns})')
212
+ if not isinstance(result, CommitResult):
213
+ raise CommitDBError("Expected commit result")
214
+ return result
215
+
216
+ def insert(self, database: str, table: str, columns: list[str], values: list) -> CommitResult:
217
+ """
218
+ Insert a row.
219
+
220
+ Args:
221
+ database: Database name
222
+ table: Table name
223
+ columns: List of column names
224
+ values: List of values (strings will be quoted)
225
+ """
226
+ cols = ', '.join(columns)
227
+ vals = ', '.join(
228
+ f"'{v}'" if isinstance(v, str) else str(v)
229
+ for v in values
230
+ )
231
+ result = self.execute(f'INSERT INTO {database}.{table} ({cols}) VALUES ({vals})')
232
+ if not isinstance(result, CommitResult):
233
+ raise CommitDBError("Expected commit result")
234
+ return result
235
+
236
+ def show_databases(self) -> list[str]:
237
+ """List all databases."""
238
+ result = self.query('SHOW DATABASES')
239
+ return [row[0] for row in result.data] if result.data else []
240
+
241
+ def show_tables(self, database: str) -> list[str]:
242
+ """List all tables in a database."""
243
+ result = self.query(f'SHOW TABLES IN {database}')
244
+ return [row[0] for row in result.data] if result.data else []
245
+
246
+
247
+ class CommitDBLocal:
248
+ """
249
+ CommitDB embedded client using Go bindings.
250
+
251
+ This mode runs the database engine directly in-process without
252
+ requiring a separate server.
253
+
254
+ Example:
255
+ # In-memory database
256
+ db = CommitDBLocal()
257
+
258
+ # File-based database
259
+ db = CommitDBLocal('/path/to/data')
260
+
261
+ db.execute('CREATE DATABASE mydb')
262
+ result = db.query('SELECT * FROM mydb.users')
263
+ db.close()
264
+ """
265
+
266
+ def __init__(self, path: Optional[str] = None, lib_path: Optional[str] = None):
267
+ """
268
+ Initialize CommitDB embedded client.
269
+
270
+ Args:
271
+ path: Path for file-based persistence. If None, uses in-memory storage.
272
+ lib_path: Optional path to libcommitdb shared library.
273
+ """
274
+ from .binding import CommitDBBinding
275
+
276
+ self._binding = CommitDBBinding
277
+ if lib_path:
278
+ self._binding.load(lib_path)
279
+
280
+ self._path = path
281
+ self._handle: Optional[int] = None
282
+
283
+ def open(self) -> 'CommitDBLocal':
284
+ """Open the database."""
285
+ if self._path:
286
+ self._handle = self._binding.open_file(self._path)
287
+ else:
288
+ self._handle = self._binding.open_memory()
289
+ return self
290
+
291
+ def close(self) -> None:
292
+ """Close the database."""
293
+ if self._handle is not None:
294
+ self._binding.close(self._handle)
295
+ self._handle = None
296
+
297
+ def __enter__(self) -> 'CommitDBLocal':
298
+ return self.open()
299
+
300
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
301
+ self.close()
302
+
303
+ def _parse_response(self, response: dict) -> CommitResult | QueryResult:
304
+ """Parse a response dict into result objects."""
305
+ if not response.get('success'):
306
+ raise CommitDBError(response.get('error', 'Unknown error'))
307
+
308
+ result_type = response.get('type')
309
+ result_data = response.get('result', {})
310
+
311
+ if result_type == 'query':
312
+ return QueryResult(
313
+ columns=result_data.get('columns', []),
314
+ data=result_data.get('data', []),
315
+ records_read=result_data.get('records_read', 0),
316
+ time_ms=result_data.get('time_ms', 0.0)
317
+ )
318
+ elif result_type == 'commit':
319
+ return CommitResult(
320
+ databases_created=result_data.get('databases_created', 0),
321
+ databases_deleted=result_data.get('databases_deleted', 0),
322
+ tables_created=result_data.get('tables_created', 0),
323
+ tables_deleted=result_data.get('tables_deleted', 0),
324
+ records_written=result_data.get('records_written', 0),
325
+ records_deleted=result_data.get('records_deleted', 0),
326
+ time_ms=result_data.get('time_ms', 0.0)
327
+ )
328
+ else:
329
+ return CommitResult()
330
+
331
+ def execute(self, query: str) -> CommitResult | QueryResult:
332
+ """
333
+ Execute a SQL query.
334
+
335
+ Args:
336
+ query: SQL query to execute
337
+
338
+ Returns:
339
+ QueryResult for SELECT queries, CommitResult for mutations
340
+ """
341
+ if self._handle is None:
342
+ raise CommitDBError("Database not open. Call open() first.")
343
+
344
+ response = self._binding.execute(self._handle, query)
345
+ return self._parse_response(response)
346
+
347
+ def query(self, sql: str) -> QueryResult:
348
+ """Execute a SELECT query and return results."""
349
+ result = self.execute(sql)
350
+ if not isinstance(result, QueryResult):
351
+ raise CommitDBError("Expected query result, got commit result")
352
+ return result
353
+
354
+ def create_database(self, name: str) -> CommitResult:
355
+ """Create a database."""
356
+ result = self.execute(f'CREATE DATABASE {name}')
357
+ if not isinstance(result, CommitResult):
358
+ raise CommitDBError("Expected commit result")
359
+ return result
360
+
361
+ def create_table(self, database: str, table: str, columns: str) -> CommitResult:
362
+ """Create a table."""
363
+ result = self.execute(f'CREATE TABLE {database}.{table} ({columns})')
364
+ if not isinstance(result, CommitResult):
365
+ raise CommitDBError("Expected commit result")
366
+ return result
367
+
368
+ def insert(self, database: str, table: str, columns: list[str], values: list) -> CommitResult:
369
+ """Insert a row."""
370
+ cols = ', '.join(columns)
371
+ vals = ', '.join(
372
+ f"'{v}'" if isinstance(v, str) else str(v)
373
+ for v in values
374
+ )
375
+ result = self.execute(f'INSERT INTO {database}.{table} ({cols}) VALUES ({vals})')
376
+ if not isinstance(result, CommitResult):
377
+ raise CommitDBError("Expected commit result")
378
+ return result
379
+
Binary file
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: commitdb
3
+ Version: 1.2.0
4
+ Summary: Python driver for CommitDB SQL Server
5
+ Author: CommitDB Contributors
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/nickyhof/CommitDB
8
+ Project-URL: Documentation, https://github.com/nickyhof/CommitDB#readme
9
+ Project-URL: Repository, https://github.com/nickyhof/CommitDB
10
+ Keywords: database,sql,git,commitdb
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7.0; extra == "dev"
22
+
23
+ # CommitDB Python Driver
24
+
25
+ Python client for connecting to CommitDB SQL Server.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install -e drivers/python
31
+ ```
32
+
33
+ ## Quick Start
34
+
35
+ ```python
36
+ from commitdb import CommitDB
37
+
38
+ # Connect to server
39
+ db = CommitDB('localhost', 3306)
40
+ db.connect()
41
+
42
+ # Or use context manager
43
+ with CommitDB('localhost', 3306) as db:
44
+ # Create database and table
45
+ db.execute('CREATE DATABASE mydb')
46
+ db.execute('CREATE TABLE mydb.users (id INT PRIMARY KEY, name STRING)')
47
+
48
+ # Insert data
49
+ db.execute("INSERT INTO mydb.users (id, name) VALUES (1, 'Alice')")
50
+ db.execute("INSERT INTO mydb.users (id, name) VALUES (2, 'Bob')")
51
+
52
+ # Query data
53
+ result = db.query('SELECT * FROM mydb.users')
54
+ print(f"Columns: {result.columns}")
55
+ print(f"Rows: {len(result)}")
56
+
57
+ # Iterate over rows as dictionaries
58
+ for row in result:
59
+ print(f" {row['id']}: {row['name']}")
60
+
61
+ # Use convenience methods
62
+ db.insert('mydb', 'users', ['id', 'name'], [3, 'Charlie'])
63
+ tables = db.show_tables('mydb')
64
+ ```
65
+
66
+ ## API Reference
67
+
68
+ ### CommitDB
69
+
70
+ ```python
71
+ CommitDB(host='localhost', port=3306)
72
+ ```
73
+
74
+ **Methods:**
75
+
76
+ - `connect(timeout=10.0)` - Connect to server
77
+ - `close()` - Close connection
78
+ - `execute(sql)` - Execute any SQL query
79
+ - `query(sql)` - Execute SELECT query, returns QueryResult
80
+ - `create_database(name)` - Create a database
81
+ - `drop_database(name)` - Drop a database
82
+ - `create_table(database, table, columns)` - Create a table
83
+ - `insert(database, table, columns, values)` - Insert a row
84
+ - `show_databases()` - List databases
85
+ - `show_tables(database)` - List tables
86
+
87
+ ### QueryResult
88
+
89
+ ```python
90
+ result = db.query('SELECT * FROM mydb.users')
91
+ result.columns # ['id', 'name']
92
+ result.data # [['1', 'Alice'], ['2', 'Bob']]
93
+ len(result) # 2
94
+ result[0] # {'id': '1', 'name': 'Alice'}
95
+ for row in result:
96
+ print(row) # {'id': '1', 'name': 'Alice'}
97
+ ```
98
+
99
+ ### CommitResult
100
+
101
+ ```python
102
+ result = db.execute('INSERT INTO mydb.users ...')
103
+ result.records_written # 1
104
+ result.affected_rows # 1
105
+ result.time_ms # 1.5
106
+ ```
107
+
108
+ ## Embedded Mode (CommitDBLocal)
109
+
110
+ Run CommitDB directly in-process without a separate server. Requires the `libcommitdb` shared library.
111
+
112
+ ### Quick Start
113
+
114
+ ```python
115
+ from commitdb import CommitDBLocal
116
+
117
+ # In-memory database (no persistence)
118
+ with CommitDBLocal() as db:
119
+ db.execute('CREATE DATABASE mydb')
120
+ db.execute('CREATE TABLE mydb.users (id INT PRIMARY KEY, name STRING)')
121
+ db.execute("INSERT INTO mydb.users (id, name) VALUES (1, 'Alice')")
122
+ result = db.query('SELECT * FROM mydb.users')
123
+
124
+ # File-based persistence
125
+ with CommitDBLocal('/path/to/data') as db:
126
+ db.execute('CREATE DATABASE mydb')
127
+ # Data persists between sessions
128
+ ```
129
+
130
+ ### Building the Shared Library
131
+
132
+ If the library isn't bundled with your pip install:
133
+
134
+ ```bash
135
+ # From CommitDB root
136
+ make lib # Creates lib/libcommitdb.dylib (macOS) or .so (Linux)
137
+ ```
138
+
139
+ ### CommitDBLocal API
140
+
141
+ ```python
142
+ CommitDBLocal(path=None, lib_path=None)
143
+ ```
144
+
145
+ **Parameters:**
146
+ - `path` - Directory for file-based persistence. If `None`, uses in-memory storage.
147
+ - `lib_path` - Optional path to `libcommitdb` shared library.
148
+
149
+ **Methods:**
150
+ - `open()` - Open the database
151
+ - `close()` - Close the database
152
+ - `execute(sql)` - Execute any SQL query
153
+ - `query(sql)` - Execute SELECT query
154
+ - `create_database(name)` - Create a database
155
+ - `create_table(database, table, columns)` - Create a table
156
+ - `insert(database, table, columns, values)` - Insert a row
157
+
158
+ ### Example: Full CRUD
159
+
160
+ ```python
161
+ from commitdb import CommitDBLocal
162
+
163
+ with CommitDBLocal() as db:
164
+ # Create
165
+ db.create_database('app')
166
+ db.create_table('app', 'users', 'id INT PRIMARY KEY, name STRING, email STRING')
167
+
168
+ # Insert
169
+ db.insert('app', 'users', ['id', 'name', 'email'], [1, 'Alice', 'alice@example.com'])
170
+ db.insert('app', 'users', ['id', 'name', 'email'], [2, 'Bob', 'bob@example.com'])
171
+
172
+ # Read
173
+ result = db.query('SELECT * FROM app.users')
174
+ for row in result:
175
+ print(f"{row['name']}: {row['email']}")
176
+
177
+ # Update
178
+ db.execute("UPDATE app.users SET email = 'alice@new.com' WHERE id = 1")
179
+
180
+ # Delete
181
+ db.execute('DELETE FROM app.users WHERE id = 2')
182
+ ```
183
+
@@ -0,0 +1,10 @@
1
+ commitdb/__init__.py,sha256=A0vyiCY3du55mA9914aRAd9PoAQMM3SrcBf_Ek3aNQs,916
2
+ commitdb/binding.py,sha256=_PaafsoHxGgpZSOd27FkymM4x6BhwK07XZPXVxjvALU,4585
3
+ commitdb/client.py,sha256=PZvPAnKUEefzJmGJhoG-qYy9g91kPYQB-G1PGQ7_a3s,12465
4
+ commitdb/lib/libcommitdb-linux-amd64.so,sha256=AOblBS3zyXqMLyjxYFzYcyH00xc-z6-T1o4hjNgeIz8,14729328
5
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ tests/test_client.py,sha256=c4L72UbKP5sBg2AHerJ5jJchWL7jE89JBga4MlQVfDQ,7385
7
+ commitdb-1.2.0.dist-info/METADATA,sha256=8_tiO1o0IL4suMHzvSVh5O7ACqKnDnMsqdCJb4HbnHo,5041
8
+ commitdb-1.2.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
9
+ commitdb-1.2.0.dist-info/top_level.txt,sha256=hxbEo9CCoDzsT0VaAiV1QViidkmyaBJtUDtCFOBH-t8,15
10
+ commitdb-1.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ commitdb
2
+ tests
tests/__init__.py ADDED
File without changes
tests/test_client.py ADDED
@@ -0,0 +1,221 @@
1
+ """
2
+ Tests for CommitDB Python driver.
3
+
4
+ To run with a live server:
5
+ 1. Start the server: go run ./cmd/server
6
+ 2. Run tests: pytest drivers/python/tests/
7
+ """
8
+
9
+ import pytest
10
+ from commitdb import CommitDB, QueryResult, CommitResult, CommitDBError
11
+
12
+
13
+ class TestQueryResult:
14
+ """Tests for QueryResult class."""
15
+
16
+ def test_iteration(self):
17
+ result = QueryResult(
18
+ columns=['id', 'name'],
19
+ data=[['1', 'Alice'], ['2', 'Bob']],
20
+ records_read=2,
21
+ time_ms=1.0
22
+ )
23
+
24
+ rows = list(result)
25
+ assert rows == [
26
+ {'id': '1', 'name': 'Alice'},
27
+ {'id': '2', 'name': 'Bob'}
28
+ ]
29
+
30
+ def test_len(self):
31
+ result = QueryResult(
32
+ columns=['id'],
33
+ data=[['1'], ['2'], ['3']],
34
+ records_read=3,
35
+ time_ms=1.0
36
+ )
37
+ assert len(result) == 3
38
+
39
+ def test_getitem(self):
40
+ result = QueryResult(
41
+ columns=['id', 'name'],
42
+ data=[['1', 'Alice'], ['2', 'Bob']],
43
+ records_read=2,
44
+ time_ms=1.0
45
+ )
46
+ assert result[0] == {'id': '1', 'name': 'Alice'}
47
+ assert result[1] == {'id': '2', 'name': 'Bob'}
48
+
49
+
50
+ class TestCommitResult:
51
+ """Tests for CommitResult class."""
52
+
53
+ def test_affected_rows(self):
54
+ result = CommitResult(
55
+ databases_created=1,
56
+ tables_created=2,
57
+ records_written=3
58
+ )
59
+ assert result.affected_rows == 6
60
+
61
+ def test_defaults(self):
62
+ result = CommitResult()
63
+ assert result.affected_rows == 0
64
+ assert result.time_ms == 0.0
65
+
66
+
67
+ class TestCommitDBUnit:
68
+ """Unit tests for CommitDB client (no server required)."""
69
+
70
+ def test_init(self):
71
+ db = CommitDB('localhost', 3306)
72
+ assert db.host == 'localhost'
73
+ assert db.port == 3306
74
+
75
+ def test_not_connected_error(self):
76
+ db = CommitDB('localhost', 3306)
77
+ with pytest.raises(CommitDBError, match="Not connected"):
78
+ db.execute("SELECT 1")
79
+
80
+
81
+ # Integration tests require a running server
82
+ # These run automatically in CI where the server is started
83
+
84
+ import os
85
+ SKIP_INTEGRATION = os.environ.get('COMMITDB_SERVER_URL') is None and os.environ.get('CI') is None
86
+
87
+
88
+ @pytest.mark.skipif(SKIP_INTEGRATION, reason="Server not running - set COMMITDB_SERVER_URL or CI env var")
89
+ class TestCommitDBIntegration:
90
+ """Integration tests (requires running server)."""
91
+
92
+ @pytest.fixture
93
+ def db(self):
94
+ host = os.environ.get('COMMITDB_HOST', 'localhost')
95
+ port = int(os.environ.get('COMMITDB_PORT', '3306'))
96
+ db = CommitDB(host, port)
97
+ db.connect()
98
+ yield db
99
+ db.close()
100
+
101
+ def test_create_database(self, db):
102
+ result = db.execute('CREATE DATABASE pytest_int_test1')
103
+ assert isinstance(result, CommitResult)
104
+ assert result.databases_created == 1
105
+
106
+ def test_create_table(self, db):
107
+ db.execute('CREATE DATABASE pytest_int_test2')
108
+ result = db.execute('CREATE TABLE pytest_int_test2.users (id INT PRIMARY KEY, name STRING)')
109
+ assert isinstance(result, CommitResult)
110
+ assert result.tables_created == 1
111
+
112
+ def test_insert_and_query(self, db):
113
+ db.execute('CREATE DATABASE pytest_int_test3')
114
+ db.execute('CREATE TABLE pytest_int_test3.items (id INT PRIMARY KEY, value STRING)')
115
+ db.execute("INSERT INTO pytest_int_test3.items (id, value) VALUES (1, 'hello')")
116
+
117
+ result = db.query('SELECT * FROM pytest_int_test3.items')
118
+ assert isinstance(result, QueryResult)
119
+ assert len(result) == 1
120
+ assert result[0] == {'id': '1', 'value': 'hello'}
121
+
122
+
123
+
124
+
125
+ # Embedded mode tests (require libcommitdb shared library)
126
+ # Run with: make lib && pytest drivers/python/tests/ -v
127
+
128
+ import os
129
+ from pathlib import Path
130
+
131
+ # Try to find the shared library
132
+ def _find_lib():
133
+ lib_paths = [
134
+ Path(__file__).parent.parent.parent.parent.parent / 'lib' / 'libcommitdb.dylib',
135
+ Path(__file__).parent.parent.parent.parent.parent / 'lib' / 'libcommitdb.so',
136
+ ]
137
+ for p in lib_paths:
138
+ if p.exists():
139
+ return str(p)
140
+ return None
141
+
142
+ LIB_PATH = _find_lib()
143
+
144
+
145
+ @pytest.mark.skipif(LIB_PATH is None, reason="libcommitdb not found - run 'make lib' first")
146
+ class TestCommitDBLocal:
147
+ """Tests for embedded mode using Go bindings."""
148
+
149
+ @pytest.fixture
150
+ def db(self):
151
+ from commitdb import CommitDBLocal
152
+ db = CommitDBLocal(lib_path=LIB_PATH)
153
+ db.open()
154
+ yield db
155
+ db.close()
156
+
157
+ def test_create_database(self, db):
158
+ result = db.execute('CREATE DATABASE local_test1')
159
+ assert isinstance(result, CommitResult)
160
+ assert result.databases_created == 1
161
+
162
+ def test_create_table(self, db):
163
+ db.execute('CREATE DATABASE local_test2')
164
+ result = db.execute('CREATE TABLE local_test2.users (id INT PRIMARY KEY, name STRING)')
165
+ assert isinstance(result, CommitResult)
166
+ assert result.tables_created == 1
167
+
168
+ def test_insert_and_query(self, db):
169
+ db.execute('CREATE DATABASE local_test3')
170
+ db.execute('CREATE TABLE local_test3.items (id INT PRIMARY KEY, value STRING)')
171
+ db.execute("INSERT INTO local_test3.items (id, value) VALUES (1, 'hello')")
172
+ db.execute("INSERT INTO local_test3.items (id, value) VALUES (2, 'world')")
173
+
174
+ result = db.query('SELECT * FROM local_test3.items')
175
+ assert isinstance(result, QueryResult)
176
+ assert len(result) == 2
177
+ assert result[0] == {'id': '1', 'value': 'hello'}
178
+ assert result[1] == {'id': '2', 'value': 'world'}
179
+
180
+ def test_update(self, db):
181
+ db.execute('CREATE DATABASE local_test4')
182
+ db.execute('CREATE TABLE local_test4.data (id INT PRIMARY KEY, val STRING)')
183
+ db.execute("INSERT INTO local_test4.data (id, val) VALUES (1, 'old')")
184
+
185
+ result = db.execute("UPDATE local_test4.data SET val = 'new' WHERE id = 1")
186
+ assert isinstance(result, CommitResult)
187
+
188
+ result = db.query('SELECT * FROM local_test4.data WHERE id = 1')
189
+ assert result[0]['val'] == 'new'
190
+
191
+ def test_delete(self, db):
192
+ db.execute('CREATE DATABASE local_test5')
193
+ db.execute('CREATE TABLE local_test5.data (id INT PRIMARY KEY)')
194
+ db.execute('INSERT INTO local_test5.data (id) VALUES (1)')
195
+ db.execute('INSERT INTO local_test5.data (id) VALUES (2)')
196
+
197
+ db.execute('DELETE FROM local_test5.data WHERE id = 1')
198
+
199
+ result = db.query('SELECT * FROM local_test5.data')
200
+ assert len(result) == 1
201
+ assert result[0]['id'] == '2'
202
+
203
+ def test_context_manager(self):
204
+ from commitdb import CommitDBLocal
205
+ with CommitDBLocal(lib_path=LIB_PATH) as db:
206
+ result = db.execute('CREATE DATABASE local_test6')
207
+ assert result.databases_created == 1
208
+
209
+ def test_convenience_methods(self, db):
210
+ db.create_database('local_test7')
211
+ db.create_table('local_test7', 'users', 'id INT PRIMARY KEY, name STRING')
212
+ db.insert('local_test7', 'users', ['id', 'name'], [1, 'Alice'])
213
+
214
+ result = db.query('SELECT * FROM local_test7.users')
215
+ assert len(result) == 1
216
+ assert result[0] == {'id': '1', 'name': 'Alice'}
217
+
218
+ def test_error_handling(self, db):
219
+ with pytest.raises(CommitDBError):
220
+ db.query('SELECT * FROM nonexistent.table')
221
+