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 +357 -0
- pyrosql-0.2.0.dist-info/METADATA +20 -0
- pyrosql-0.2.0.dist-info/RECORD +4 -0
- pyrosql-0.2.0.dist-info/WHEEL +4 -0
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,,
|