pyrosql 0.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.
pyrosql/__init__.py ADDED
@@ -0,0 +1,357 @@
1
+ """PyroSQL Python driver — pure-Python ctypes binding to libpyrosql_ffi_pwire."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ctypes
6
+ import ctypes.util
7
+ import json
8
+ import os
9
+ import platform
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Any, List, Optional, Sequence
13
+
14
+
15
+ __version__ = "0.2.0"
16
+ __all__ = ["connect", "Connection", "Result", "PyroSQLError"]
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Exceptions
21
+ # ---------------------------------------------------------------------------
22
+
23
+ class PyroSQLError(Exception):
24
+ """Base exception for all PyroSQL driver errors."""
25
+
26
+
27
+ class ConnectionError(PyroSQLError): # noqa: A001 — shadows builtin intentionally
28
+ """Raised when a connection cannot be established or has been lost."""
29
+
30
+
31
+ class QueryError(PyroSQLError):
32
+ """Raised when a query or execute call fails."""
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # FFI library loading
37
+ # ---------------------------------------------------------------------------
38
+
39
+ def _lib_name() -> str:
40
+ """Return the platform-specific shared library file name."""
41
+ system = platform.system()
42
+ if system == "Linux":
43
+ return "libpyrosql_ffi_pwire.so"
44
+ elif system == "Darwin":
45
+ return "libpyrosql_ffi_pwire.dylib"
46
+ elif system == "Windows":
47
+ return "pyrosql_ffi_pwire.dll"
48
+ else:
49
+ return "libpyrosql_ffi_pwire.so"
50
+
51
+
52
+ def _find_library() -> str:
53
+ """Locate the FFI shared library.
54
+
55
+ Search order:
56
+ 1. ``PYROSQL_FFI_LIB`` environment variable (explicit path).
57
+ 2. Sibling ``ffi-pwire/target/release/`` directory (development layout).
58
+ 3. Same directory as this Python file.
59
+ 4. System library search via ctypes.util.find_library.
60
+ """
61
+ # 1. Explicit env var
62
+ env_path = os.environ.get("PYROSQL_FFI_LIB")
63
+ if env_path:
64
+ if os.path.isfile(env_path):
65
+ return env_path
66
+ raise PyroSQLError(
67
+ f"PYROSQL_FFI_LIB points to '{env_path}' which does not exist"
68
+ )
69
+
70
+ lib_name = _lib_name()
71
+
72
+ # 2. Development layout: python/ is next to ffi-pwire/
73
+ dev_path = (
74
+ Path(__file__).resolve().parent.parent.parent
75
+ / "ffi-pwire"
76
+ / "target"
77
+ / "release"
78
+ / lib_name
79
+ )
80
+ if dev_path.is_file():
81
+ return str(dev_path)
82
+
83
+ # Also check debug build
84
+ dev_debug = (
85
+ Path(__file__).resolve().parent.parent.parent
86
+ / "ffi-pwire"
87
+ / "target"
88
+ / "debug"
89
+ / lib_name
90
+ )
91
+ if dev_debug.is_file():
92
+ return str(dev_debug)
93
+
94
+ # 3. Same directory as this file (bundled wheel)
95
+ local_path = Path(__file__).resolve().parent / lib_name
96
+ if local_path.is_file():
97
+ return str(local_path)
98
+
99
+ # 4. System search
100
+ found = ctypes.util.find_library("pyrosql_ffi_pwire")
101
+ if found:
102
+ return found
103
+
104
+ raise PyroSQLError(
105
+ f"Cannot find {lib_name}. Set the PYROSQL_FFI_LIB environment "
106
+ f"variable to the full path of the shared library, or place it "
107
+ f"next to this package."
108
+ )
109
+
110
+
111
+ def _load_lib() -> ctypes.CDLL:
112
+ """Load the shared library and declare C function signatures."""
113
+ path = _find_library()
114
+ lib = ctypes.CDLL(path)
115
+
116
+ # void pyro_pwire_init(void);
117
+ lib.pyro_pwire_init.argtypes = []
118
+ lib.pyro_pwire_init.restype = None
119
+
120
+ # void* pyro_pwire_connect(const char* host, uint16_t port);
121
+ lib.pyro_pwire_connect.argtypes = [ctypes.c_char_p, ctypes.c_uint16]
122
+ lib.pyro_pwire_connect.restype = ctypes.c_void_p
123
+
124
+ # char* pyro_pwire_query(void* conn, const char* sql);
125
+ lib.pyro_pwire_query.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
126
+ lib.pyro_pwire_query.restype = ctypes.c_char_p
127
+
128
+ # int64_t pyro_pwire_execute(void* conn, const char* sql);
129
+ lib.pyro_pwire_execute.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
130
+ lib.pyro_pwire_execute.restype = ctypes.c_int64
131
+
132
+ # void pyro_pwire_free_string(char* ptr);
133
+ lib.pyro_pwire_free_string.argtypes = [ctypes.c_char_p]
134
+ lib.pyro_pwire_free_string.restype = None
135
+
136
+ # void pyro_pwire_close(void* conn);
137
+ lib.pyro_pwire_close.argtypes = [ctypes.c_void_p]
138
+ lib.pyro_pwire_close.restype = None
139
+
140
+ lib.pyro_pwire_init()
141
+ return lib
142
+
143
+
144
+ # Module-level singleton — loaded on first use.
145
+ _lib: Optional[ctypes.CDLL] = None
146
+
147
+
148
+ def _get_lib() -> ctypes.CDLL:
149
+ global _lib
150
+ if _lib is None:
151
+ _lib = _load_lib()
152
+ return _lib
153
+
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # Result
157
+ # ---------------------------------------------------------------------------
158
+
159
+ class Result:
160
+ """Query result containing columns, rows, and rows_affected.
161
+
162
+ Attributes:
163
+ columns: List of column name strings.
164
+ rows: List of rows, where each row is a list of string values
165
+ (or ``None`` for SQL NULL).
166
+ rows_affected: Number of rows affected (for DML statements).
167
+ """
168
+
169
+ __slots__ = ("columns", "rows", "rows_affected")
170
+
171
+ def __init__(
172
+ self,
173
+ columns: List[str],
174
+ rows: List[List[Any]],
175
+ rows_affected: int,
176
+ ) -> None:
177
+ self.columns = columns
178
+ self.rows = rows
179
+ self.rows_affected = rows_affected
180
+
181
+ def __len__(self) -> int:
182
+ return len(self.rows)
183
+
184
+ def __iter__(self):
185
+ """Iterate over rows as dicts keyed by column name."""
186
+ for row in self.rows:
187
+ yield dict(zip(self.columns, row))
188
+
189
+ def __repr__(self) -> str:
190
+ return (
191
+ f"Result(columns={self.columns!r}, "
192
+ f"rows={len(self.rows)}, "
193
+ f"rows_affected={self.rows_affected})"
194
+ )
195
+
196
+
197
+ # ---------------------------------------------------------------------------
198
+ # Connection
199
+ # ---------------------------------------------------------------------------
200
+
201
+ class Connection:
202
+ """A connection to a PyroSQL server via the PWire protocol.
203
+
204
+ Use :func:`connect` to create instances. Supports the context manager
205
+ protocol (``with`` statement) for automatic cleanup.
206
+
207
+ Example::
208
+
209
+ with pyrosql.connect("127.0.0.1", 12520) as conn:
210
+ result = conn.query("SELECT id, name FROM users")
211
+ for row in result:
212
+ print(row["name"])
213
+ """
214
+
215
+ def __init__(self, handle: int, lib: ctypes.CDLL) -> None:
216
+ self._handle = handle
217
+ self._lib = lib
218
+ self._closed = False
219
+
220
+ # -- public API --------------------------------------------------------
221
+
222
+ def query(self, sql: str) -> Result:
223
+ """Execute a SQL query and return the result set.
224
+
225
+ Args:
226
+ sql: SQL query string (e.g. ``"SELECT * FROM users"``).
227
+
228
+ Returns:
229
+ A :class:`Result` with columns, rows, and rows_affected.
230
+
231
+ Raises:
232
+ QueryError: If the server returns an error.
233
+ ConnectionError: If the connection is closed.
234
+ """
235
+ self._check_open()
236
+ sql_bytes = sql.encode("utf-8")
237
+ raw = self._lib.pyro_pwire_query(self._handle, sql_bytes)
238
+ if raw is None:
239
+ raise QueryError("pyro_pwire_query returned NULL")
240
+ try:
241
+ json_str = raw.decode("utf-8")
242
+ except Exception:
243
+ raise QueryError("failed to decode query response as UTF-8")
244
+
245
+ # Check for error responses (the FFI layer may return JSON with an
246
+ # "error" key, or a bare error string).
247
+ if json_str.startswith('{"error"'):
248
+ try:
249
+ err = json.loads(json_str)
250
+ raise QueryError(err.get("error", json_str))
251
+ except (json.JSONDecodeError, QueryError):
252
+ raise
253
+ except Exception:
254
+ raise QueryError(json_str)
255
+
256
+ try:
257
+ data = json.loads(json_str)
258
+ except json.JSONDecodeError as exc:
259
+ raise QueryError(f"invalid JSON response: {exc}") from exc
260
+ finally:
261
+ # Free the C-allocated string.
262
+ self._lib.pyro_pwire_free_string(raw)
263
+
264
+ return Result(
265
+ columns=data.get("columns", []),
266
+ rows=data.get("rows", []),
267
+ rows_affected=data.get("rows_affected", 0),
268
+ )
269
+
270
+ def execute(self, sql: str) -> int:
271
+ """Execute a DML statement (INSERT, UPDATE, DELETE, etc.).
272
+
273
+ Args:
274
+ sql: SQL statement string.
275
+
276
+ Returns:
277
+ The number of rows affected.
278
+
279
+ Raises:
280
+ QueryError: If the server returns an error (rows_affected == -1).
281
+ ConnectionError: If the connection is closed.
282
+ """
283
+ self._check_open()
284
+ sql_bytes = sql.encode("utf-8")
285
+ result = self._lib.pyro_pwire_execute(self._handle, sql_bytes)
286
+ if result < 0:
287
+ raise QueryError(
288
+ f"execute failed (returned {result}); "
289
+ f"use query() to see the error message"
290
+ )
291
+ return result
292
+
293
+ def close(self) -> None:
294
+ """Close the connection and free resources.
295
+
296
+ Safe to call multiple times.
297
+ """
298
+ if not self._closed and self._handle:
299
+ self._lib.pyro_pwire_close(self._handle)
300
+ self._closed = True
301
+ self._handle = None
302
+
303
+ @property
304
+ def closed(self) -> bool:
305
+ """Whether this connection has been closed."""
306
+ return self._closed
307
+
308
+ # -- context manager ---------------------------------------------------
309
+
310
+ def __enter__(self) -> "Connection":
311
+ return self
312
+
313
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
314
+ self.close()
315
+
316
+ # -- destructor --------------------------------------------------------
317
+
318
+ def __del__(self) -> None:
319
+ try:
320
+ self.close()
321
+ except Exception:
322
+ pass
323
+
324
+ # -- internals ---------------------------------------------------------
325
+
326
+ def _check_open(self) -> None:
327
+ if self._closed:
328
+ raise ConnectionError("connection is closed")
329
+
330
+ def __repr__(self) -> str:
331
+ state = "closed" if self._closed else "open"
332
+ return f"<pyrosql.Connection [{state}]>"
333
+
334
+
335
+ # ---------------------------------------------------------------------------
336
+ # Module-level connect function
337
+ # ---------------------------------------------------------------------------
338
+
339
+ def connect(host: str, port: int = 12520) -> Connection:
340
+ """Connect to a PyroSQL server.
341
+
342
+ Args:
343
+ host: Hostname or IP address (e.g. ``"127.0.0.1"``).
344
+ port: TCP port number (default: 12520).
345
+
346
+ Returns:
347
+ A :class:`Connection` instance.
348
+
349
+ Raises:
350
+ ConnectionError: If the connection cannot be established.
351
+ """
352
+ lib = _get_lib()
353
+ host_bytes = host.encode("utf-8")
354
+ handle = lib.pyro_pwire_connect(host_bytes, port)
355
+ if handle is None or handle == 0:
356
+ raise ConnectionError(f"failed to connect to {host}:{port}")
357
+ return Connection(handle, lib)
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrosql
3
+ Version: 0.2.0
4
+ Summary: PyroSQL Python driver — pure-Python ctypes binding to libpyrosql_ffi_pwire
5
+ Project-URL: Repository, https://github.com/pyrosql/pyrosql-driver
6
+ License: BSL-1.1
7
+ Keywords: database,pyrosql,sql
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.8
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Database
18
+ Classifier: Topic :: Database :: Front-Ends
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.8
@@ -0,0 +1,4 @@
1
+ pyrosql/__init__.py,sha256=dz9h8QC6jXj0t4oKMRP4FdWmEslE8l-Op7EAeVyNQ3A,10853
2
+ pyrosql-0.2.0.dist-info/METADATA,sha256=ZAhuDBYBmj-9ap-7HNLtqNU1CBiTK17NKA3ng7KxPGo,816
3
+ pyrosql-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
4
+ pyrosql-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any