h2gis 2.0.0__py3-none-win_amd64.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.
- h2gis/__init__.py +114 -0
- h2gis/_buffer.py +114 -0
- h2gis/_native.py +368 -0
- h2gis/_queries.py +166 -0
- h2gis/adapt.py +350 -0
- h2gis/connection.py +384 -0
- h2gis/cursor.py +428 -0
- h2gis/dataframe.py +423 -0
- h2gis/errors.py +263 -0
- h2gis/lib/h2gis.dll +4 -0
- h2gis/rows.py +84 -0
- h2gis/sql.py +214 -0
- h2gis-2.0.0.dist-info/METADATA +149 -0
- h2gis-2.0.0.dist-info/RECORD +17 -0
- h2gis-2.0.0.dist-info/WHEEL +5 -0
- h2gis-2.0.0.dist-info/licenses/license-file.md +165 -0
- h2gis-2.0.0.dist-info/top_level.txt +1 -0
h2gis/__init__.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
* H2GIS-python is a Python wrapper to use H2GIS.
|
|
3
|
+
* <a href="http://www.h2database.com">http://www.h2database.com</a>. H2GIS-python is developed by CNRS
|
|
4
|
+
* <a href="http://www.cnrs.fr/">http://www.cnrs.fr/</a>.
|
|
5
|
+
*
|
|
6
|
+
* This code is part of the H2GIS-python project. H2GIS-python is free software;
|
|
7
|
+
* you can redistribute it and/or modify it under the terms of the GNU
|
|
8
|
+
* Lesser General Public License as published by the Free Software Foundation;
|
|
9
|
+
* version 3.0 of the License.
|
|
10
|
+
*
|
|
11
|
+
* H2GIS-python is distributed in the hope that it will be useful, but
|
|
12
|
+
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
13
|
+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
|
14
|
+
* for more details <http://www.gnu.org/licenses/>.
|
|
15
|
+
*
|
|
16
|
+
*
|
|
17
|
+
* For more information, please consult: <a href="http://www.h2gis.org/">http://www.h2gis.org/</a>
|
|
18
|
+
* or contact directly: info_at_h2gis.org
|
|
19
|
+
|
|
20
|
+
@author Maël PHILIPPE, CNRS
|
|
21
|
+
@author Erwan BOCHER, CNRS
|
|
22
|
+
|
|
23
|
+
DB-API 2.0 driver for H2GIS, with an API modelled on psycopg 3::
|
|
24
|
+
|
|
25
|
+
import h2gis
|
|
26
|
+
from h2gis.rows import dict_row
|
|
27
|
+
|
|
28
|
+
with h2gis.connect("/data/my_db", row_factory=dict_row) as conn:
|
|
29
|
+
conn.execute("INSERT INTO LIEUX(NOM, THE_GEOM) VALUES (%s, %s)", ["Nantes", point])
|
|
30
|
+
for row in conn.execute("SELECT * FROM LIEUX"):
|
|
31
|
+
print(row["NOM"], row["THE_GEOM"])
|
|
32
|
+
gdf = h2gis.read_gdf(conn, "SELECT * FROM LIEUX")
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import datetime as _dt
|
|
36
|
+
import time as _time
|
|
37
|
+
|
|
38
|
+
from . import errors, rows, sql
|
|
39
|
+
from .adapt import AdaptersMap, ColumnType, Geometry, Json, adapters
|
|
40
|
+
from .connection import Connection, Rollback, Transaction, connect
|
|
41
|
+
from .cursor import Column, Cursor
|
|
42
|
+
from .dataframe import read_df, read_gdf, to_h2gis
|
|
43
|
+
from .errors import (DatabaseError, DataError, Error, IntegrityError, InterfaceError,
|
|
44
|
+
InternalError, NotSupportedError, OperationalError, ProgrammingError,
|
|
45
|
+
Warning)
|
|
46
|
+
|
|
47
|
+
__version__ = "2.0.0"
|
|
48
|
+
|
|
49
|
+
# --- PEP 249 module interface ------------------------------------------------
|
|
50
|
+
|
|
51
|
+
apilevel = "2.0"
|
|
52
|
+
threadsafety = 2 # threads may share the module and connections (calls are serialized)
|
|
53
|
+
paramstyle = "pyformat"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DBAPITypeObject:
|
|
57
|
+
"""Compare equal to the ``type_code`` values of a family of types (PEP 249)."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, name: str, *codes: int):
|
|
60
|
+
self.name = name
|
|
61
|
+
self.codes = frozenset(codes)
|
|
62
|
+
|
|
63
|
+
def __eq__(self, other) -> bool:
|
|
64
|
+
if isinstance(other, DBAPITypeObject):
|
|
65
|
+
return self.codes == other.codes
|
|
66
|
+
return other in self.codes
|
|
67
|
+
|
|
68
|
+
def __hash__(self) -> int:
|
|
69
|
+
return hash(self.codes)
|
|
70
|
+
|
|
71
|
+
def __repr__(self) -> str:
|
|
72
|
+
return f"<DBAPITypeObject {self.name}>"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
STRING = DBAPITypeObject("STRING", ColumnType.STRING)
|
|
76
|
+
BINARY = DBAPITypeObject("BINARY", ColumnType.BINARY)
|
|
77
|
+
NUMBER = DBAPITypeObject("NUMBER", ColumnType.INT, ColumnType.LONG, ColumnType.FLOAT,
|
|
78
|
+
ColumnType.DOUBLE, ColumnType.DECIMAL)
|
|
79
|
+
DATETIME = DBAPITypeObject("DATETIME", ColumnType.TEMPORAL)
|
|
80
|
+
ROWID = DBAPITypeObject("ROWID")
|
|
81
|
+
BOOLEAN = DBAPITypeObject("BOOLEAN", ColumnType.BOOLEAN)
|
|
82
|
+
GEOMETRY = DBAPITypeObject("GEOMETRY", ColumnType.GEOMETRY)
|
|
83
|
+
JSON = DBAPITypeObject("JSON", ColumnType.JSON)
|
|
84
|
+
UUID = DBAPITypeObject("UUID", ColumnType.UUID)
|
|
85
|
+
|
|
86
|
+
Date = _dt.date
|
|
87
|
+
Time = _dt.time
|
|
88
|
+
Timestamp = _dt.datetime
|
|
89
|
+
Binary = bytes
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def DateFromTicks(ticks: float) -> _dt.date:
|
|
93
|
+
return Date(*_time.localtime(ticks)[:3])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def TimeFromTicks(ticks: float) -> _dt.time:
|
|
97
|
+
return Time(*_time.localtime(ticks)[3:6])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def TimestampFromTicks(ticks: float) -> _dt.datetime:
|
|
101
|
+
return Timestamp(*_time.localtime(ticks)[:6])
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
__all__ = [
|
|
105
|
+
"connect", "Connection", "Cursor", "Column", "Transaction", "Rollback",
|
|
106
|
+
"read_df", "read_gdf", "to_h2gis", "Geometry", "Json", "AdaptersMap", "adapters",
|
|
107
|
+
"errors", "rows", "sql",
|
|
108
|
+
"apilevel", "threadsafety", "paramstyle",
|
|
109
|
+
"Warning", "Error", "InterfaceError", "DatabaseError", "DataError", "OperationalError",
|
|
110
|
+
"IntegrityError", "InternalError", "ProgrammingError", "NotSupportedError",
|
|
111
|
+
"STRING", "BINARY", "NUMBER", "DATETIME", "ROWID", "BOOLEAN", "GEOMETRY", "JSON", "UUID",
|
|
112
|
+
"Date", "Time", "Timestamp", "Binary", "DateFromTicks", "TimeFromTicks",
|
|
113
|
+
"TimestampFromTicks",
|
|
114
|
+
]
|
h2gis/_buffer.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
H2GIS-python is a Python wrapper to use H2GIS <http://www.h2gis.org/>.
|
|
3
|
+
H2GIS-python is developed by CNRS <http://www.cnrs.fr/>.
|
|
4
|
+
|
|
5
|
+
This code is part of the H2GIS-python project. H2GIS-python is free software;
|
|
6
|
+
you can redistribute it and/or modify it under the terms of the GNU
|
|
7
|
+
Lesser General Public License as published by the Free Software Foundation;
|
|
8
|
+
version 3.0 of the License. See <http://www.gnu.org/licenses/>.
|
|
9
|
+
|
|
10
|
+
Decoder of the column oriented buffer produced by
|
|
11
|
+
``org.h2gis.graalvm.ResultSetWrapper.serialize()`` (all integers little endian)::
|
|
12
|
+
|
|
13
|
+
u32 column_count
|
|
14
|
+
u32 row_count
|
|
15
|
+
u64 column_offset[column_count] # from the start of the buffer
|
|
16
|
+
column*:
|
|
17
|
+
u32 name_length, utf-8 name
|
|
18
|
+
u32 type_code # see adapt.ColumnType, | 0x100 if a null mask follows
|
|
19
|
+
[u8 null_mask[ceil(row_count/8)]] # bit i % 8 of byte i // 8 set: row i is NULL
|
|
20
|
+
u32 values_length
|
|
21
|
+
values:
|
|
22
|
+
INT i32 | LONG i64 | FLOAT f32 | DOUBLE f64 | BOOLEAN u8
|
|
23
|
+
other types: u32 length + bytes per value
|
|
24
|
+
|
|
25
|
+
NULL values keep a placeholder in ``values``; the mask tells them apart.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import struct
|
|
31
|
+
from typing import List
|
|
32
|
+
|
|
33
|
+
from . import errors
|
|
34
|
+
from .adapt import ColumnType
|
|
35
|
+
|
|
36
|
+
NULL_LENGTH = 0xFFFFFFFF
|
|
37
|
+
FLAG_NULL_MASK = 0x100
|
|
38
|
+
|
|
39
|
+
_FIXED = {
|
|
40
|
+
ColumnType.INT: "i",
|
|
41
|
+
ColumnType.LONG: "q",
|
|
42
|
+
ColumnType.FLOAT: "f",
|
|
43
|
+
ColumnType.DOUBLE: "d",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Chunk:
|
|
48
|
+
"""A decoded buffer: column names, type codes and values by column."""
|
|
49
|
+
|
|
50
|
+
__slots__ = ("names", "type_codes", "row_count", "columns", "has_null_mask")
|
|
51
|
+
|
|
52
|
+
def __init__(self, names, type_codes, row_count, columns, has_null_mask):
|
|
53
|
+
self.names: List[str] = names
|
|
54
|
+
self.type_codes: List[int] = type_codes
|
|
55
|
+
self.row_count: int = row_count
|
|
56
|
+
self.columns: List[list] = columns
|
|
57
|
+
self.has_null_mask: List[bool] = has_null_mask
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def decode(buf: bytes) -> Chunk:
|
|
61
|
+
view = memoryview(buf)
|
|
62
|
+
try:
|
|
63
|
+
col_count, row_count = struct.unpack_from("<II", view, 0)
|
|
64
|
+
offsets = struct.unpack_from(f"<{col_count}Q", view, 8)
|
|
65
|
+
names, codes, columns, masks = [], [], [], []
|
|
66
|
+
for offset in offsets:
|
|
67
|
+
name_len = struct.unpack_from("<I", view, offset)[0]
|
|
68
|
+
pos = offset + 4
|
|
69
|
+
names.append(bytes(view[pos:pos + name_len]).decode("utf-8", "replace"))
|
|
70
|
+
pos += name_len
|
|
71
|
+
raw_code = struct.unpack_from("<I", view, pos)[0]
|
|
72
|
+
pos += 4
|
|
73
|
+
code = raw_code & ~FLAG_NULL_MASK
|
|
74
|
+
nulls = None
|
|
75
|
+
if raw_code & FLAG_NULL_MASK:
|
|
76
|
+
mask_len = (row_count + 7) // 8
|
|
77
|
+
nulls = bytes(view[pos:pos + mask_len])
|
|
78
|
+
pos += mask_len
|
|
79
|
+
values_len = struct.unpack_from("<I", view, pos)[0]
|
|
80
|
+
pos += 4
|
|
81
|
+
values = _decode_values(view[pos:pos + values_len], code, row_count)
|
|
82
|
+
if nulls is not None:
|
|
83
|
+
for i in range(row_count):
|
|
84
|
+
if nulls[i >> 3] & (1 << (i & 7)):
|
|
85
|
+
values[i] = None
|
|
86
|
+
codes.append(code)
|
|
87
|
+
columns.append(values)
|
|
88
|
+
masks.append(nulls is not None)
|
|
89
|
+
except (struct.error, IndexError, UnicodeDecodeError) as exc:
|
|
90
|
+
raise errors.InterfaceError(f"malformed result buffer: {exc}") from exc
|
|
91
|
+
return Chunk(names, codes, row_count, columns, masks)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _decode_values(view: memoryview, code: int, n: int) -> list:
|
|
95
|
+
fmt = _FIXED.get(code)
|
|
96
|
+
if fmt is not None:
|
|
97
|
+
return list(struct.unpack_from(f"<{n}{fmt}", view, 0)) if n else []
|
|
98
|
+
if code == ColumnType.BOOLEAN:
|
|
99
|
+
return [b != 0 for b in bytes(view[:n])]
|
|
100
|
+
out: list = []
|
|
101
|
+
pos = 0
|
|
102
|
+
as_text = code not in (ColumnType.GEOMETRY, ColumnType.BINARY)
|
|
103
|
+
for _ in range(n):
|
|
104
|
+
length = struct.unpack_from("<I", view, pos)[0]
|
|
105
|
+
pos += 4
|
|
106
|
+
if length == NULL_LENGTH:
|
|
107
|
+
out.append(None)
|
|
108
|
+
continue
|
|
109
|
+
data = bytes(view[pos:pos + length])
|
|
110
|
+
if len(data) != length:
|
|
111
|
+
raise errors.InterfaceError("malformed result buffer: truncated value")
|
|
112
|
+
pos += length
|
|
113
|
+
out.append(data.decode("utf-8", "replace") if as_text else data)
|
|
114
|
+
return out
|
h2gis/_native.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""
|
|
2
|
+
H2GIS-python is a Python wrapper to use H2GIS <http://www.h2gis.org/>.
|
|
3
|
+
H2GIS-python is developed by CNRS <http://www.cnrs.fr/>.
|
|
4
|
+
|
|
5
|
+
This code is part of the H2GIS-python project. H2GIS-python is free software;
|
|
6
|
+
you can redistribute it and/or modify it under the terms of the GNU
|
|
7
|
+
Lesser General Public License as published by the Free Software Foundation;
|
|
8
|
+
version 3.0 of the License. See <http://www.gnu.org/licenses/>.
|
|
9
|
+
|
|
10
|
+
Low level binding to the GraalVM native library (``h2gis.so`` / ``.dll`` /
|
|
11
|
+
``.dylib``) built from ``org.h2gis.graalvm.GraalCInterface`` (module
|
|
12
|
+
``h2gis-graalvm`` of H2GIS).
|
|
13
|
+
|
|
14
|
+
* The library must implement the C API version 2 or later
|
|
15
|
+
(``h2gis_version()``); see docs/NATIVE_API.md.
|
|
16
|
+
* One GraalVM isolate per library and per process, shared by every connection.
|
|
17
|
+
* Each Python thread gets its own isolate thread (``graal_attach_thread``),
|
|
18
|
+
detached when the thread ends: calling into an isolate with the isolate
|
|
19
|
+
thread of another OS thread is undefined behaviour, and the Java side keeps
|
|
20
|
+
the last error per thread.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import ctypes
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import platform
|
|
29
|
+
import threading
|
|
30
|
+
from typing import Dict, List, Optional
|
|
31
|
+
|
|
32
|
+
from . import errors
|
|
33
|
+
|
|
34
|
+
__all__ = ["NativeLib", "get_lib", "default_library_path", "native_platform",
|
|
35
|
+
"library_file_name", "MIN_C_API_VERSION"]
|
|
36
|
+
|
|
37
|
+
#: Oldest version of the C interface this driver works with.
|
|
38
|
+
MIN_C_API_VERSION = 2
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _GraalIsolate(ctypes.Structure):
|
|
42
|
+
_fields_: list = []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _GraalIsolateThread(ctypes.Structure):
|
|
46
|
+
_fields_: list = []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_Isolate_p = ctypes.POINTER(_GraalIsolate)
|
|
50
|
+
_Thread_p = ctypes.POINTER(_GraalIsolateThread)
|
|
51
|
+
|
|
52
|
+
# Java long is 64 bits; C long is only 32 bits on Windows (LLP64)
|
|
53
|
+
_jlong = ctypes.c_int64
|
|
54
|
+
_jint = ctypes.c_int32
|
|
55
|
+
_c_char_p = ctypes.c_char_p
|
|
56
|
+
_c_void_p = ctypes.c_void_p
|
|
57
|
+
|
|
58
|
+
# name -> (argtypes, restype). The isolate thread is always the first argument.
|
|
59
|
+
_FUNCTIONS = {
|
|
60
|
+
"h2gis_version": ([_Thread_p], _jint),
|
|
61
|
+
"h2gis_get_last_error": ([_Thread_p], _c_void_p),
|
|
62
|
+
"h2gis_free_result_buffer": ([_Thread_p, _c_void_p], None),
|
|
63
|
+
"h2gis_connect": ([_Thread_p, _c_char_p, _c_char_p, _c_char_p], _jlong),
|
|
64
|
+
"h2gis_load": ([_Thread_p, _jlong], _jlong),
|
|
65
|
+
"h2gis_close_connection": ([_Thread_p, _jlong], None),
|
|
66
|
+
"h2gis_delete_database_and_close": ([_Thread_p, _jlong], None),
|
|
67
|
+
"h2gis_execute": ([_Thread_p, _jlong, _c_char_p], _jint),
|
|
68
|
+
"h2gis_fetch": ([_Thread_p, _jlong, _c_char_p], _jlong),
|
|
69
|
+
"h2gis_prepare": ([_Thread_p, _jlong, _c_char_p], _jlong),
|
|
70
|
+
"h2gis_bind_long": ([_Thread_p, _jlong, _jint, _jlong], _jint),
|
|
71
|
+
"h2gis_bind_double": ([_Thread_p, _jlong, _jint, ctypes.c_double], _jint),
|
|
72
|
+
"h2gis_bind_string": ([_Thread_p, _jlong, _jint, _c_char_p], _jint),
|
|
73
|
+
"h2gis_bind_blob": ([_Thread_p, _jlong, _jint, _c_char_p, _jint], _jint),
|
|
74
|
+
"h2gis_bind_null": ([_Thread_p, _jlong, _jint], _jint),
|
|
75
|
+
"h2gis_execute_prepared": ([_Thread_p, _jlong], _jlong),
|
|
76
|
+
"h2gis_execute_prepared_update": ([_Thread_p, _jlong], _jint),
|
|
77
|
+
"h2gis_add_batch": ([_Thread_p, _jlong], _jint),
|
|
78
|
+
"h2gis_execute_batch": ([_Thread_p, _jlong], _jlong),
|
|
79
|
+
"h2gis_close_query": ([_Thread_p, _jlong], None),
|
|
80
|
+
"h2gis_fetch_all": ([_Thread_p, _jlong, _c_void_p], _c_void_p),
|
|
81
|
+
"h2gis_fetch_batch": ([_Thread_p, _jlong, _jint, _c_void_p], _c_void_p),
|
|
82
|
+
"h2gis_get_metadata_json": ([_Thread_p, _jlong], _c_void_p),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_LIB_NAMES = {"Windows": "h2gis.dll", "Darwin": "h2gis.dylib"}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def native_platform() -> str:
|
|
90
|
+
"""Name of the current platform in the ``native/`` folder: ``linux-x86_64``,
|
|
91
|
+
``windows-x86_64``, ``macos-arm64``..."""
|
|
92
|
+
system = {"Darwin": "macos"}.get(platform.system(), platform.system().lower())
|
|
93
|
+
machine = platform.machine().lower()
|
|
94
|
+
machine = {"amd64": "x86_64", "x64": "x86_64", "aarch64": "arm64" if system == "macos" else "aarch64"
|
|
95
|
+
}.get(machine, machine)
|
|
96
|
+
return f"{system}-{machine}"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def library_file_name() -> str:
|
|
100
|
+
"""``h2gis.so``, ``h2gis.dll`` or ``h2gis.dylib``."""
|
|
101
|
+
return _LIB_NAMES.get(platform.system(), "h2gis.so")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def default_library_path() -> str:
|
|
105
|
+
"""Path of the native library, the first of:
|
|
106
|
+
|
|
107
|
+
1. ``$H2GIS_LIB_PATH``;
|
|
108
|
+
2. ``h2gis/lib/h2gis.<ext>``, inside an installed wheel;
|
|
109
|
+
3. ``native/<platform>/h2gis.<ext>`` of the repository, when h2gis is used
|
|
110
|
+
from a clone (``pip install -e .``, tests).
|
|
111
|
+
"""
|
|
112
|
+
env = os.environ.get("H2GIS_LIB_PATH")
|
|
113
|
+
if env:
|
|
114
|
+
return env
|
|
115
|
+
package_dir = os.path.dirname(os.path.abspath(__file__))
|
|
116
|
+
name = library_file_name()
|
|
117
|
+
bundled = os.path.join(package_dir, "lib", name)
|
|
118
|
+
if os.path.exists(bundled):
|
|
119
|
+
return bundled
|
|
120
|
+
checkout = os.path.join(os.path.dirname(package_dir), "native", native_platform(), name)
|
|
121
|
+
if os.path.exists(checkout):
|
|
122
|
+
return checkout
|
|
123
|
+
return bundled # missing: NativeLib reports the expected location
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class NativeLib:
|
|
127
|
+
"""A loaded native library and its GraalVM isolate.
|
|
128
|
+
|
|
129
|
+
Don't instantiate directly: use :func:`get_lib`, which returns one shared
|
|
130
|
+
instance per library path.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(self, path: str):
|
|
134
|
+
if not os.path.exists(path):
|
|
135
|
+
raise errors.InterfaceError(
|
|
136
|
+
f"native H2GIS library not found: {path!r}. Install the h2gis "
|
|
137
|
+
"package with its 'lib' folder, set H2GIS_LIB_PATH or pass lib_path=; "
|
|
138
|
+
"from a clone: python tools/update_native.py --download --current-platform")
|
|
139
|
+
self.path = path
|
|
140
|
+
try:
|
|
141
|
+
self._lib = ctypes.CDLL(path)
|
|
142
|
+
except OSError as exc:
|
|
143
|
+
raise errors.InterfaceError(f"cannot load native library {path!r}: {exc}") from exc
|
|
144
|
+
|
|
145
|
+
if getattr(self._lib, "h2gis_version", None) is None:
|
|
146
|
+
raise errors.InterfaceError(
|
|
147
|
+
f"{path!r} implements the H2GIS C API version 1; h2gis-python 2 needs "
|
|
148
|
+
f"version {MIN_C_API_VERSION} or later (H2GIS >= 2.2.6)")
|
|
149
|
+
missing = [name for name in _FUNCTIONS if getattr(self._lib, name, None) is None]
|
|
150
|
+
if missing:
|
|
151
|
+
raise errors.InterfaceError(f"{path!r} has no entry point {', '.join(missing)}")
|
|
152
|
+
for name, (args, res) in _FUNCTIONS.items():
|
|
153
|
+
fn = getattr(self._lib, name)
|
|
154
|
+
fn.argtypes, fn.restype = args, res
|
|
155
|
+
|
|
156
|
+
self._lib.graal_create_isolate.argtypes = [
|
|
157
|
+
_c_void_p, ctypes.POINTER(_Isolate_p), ctypes.POINTER(_Thread_p)]
|
|
158
|
+
self._lib.graal_create_isolate.restype = ctypes.c_int
|
|
159
|
+
self._lib.graal_attach_thread.argtypes = [_Isolate_p, ctypes.POINTER(_Thread_p)]
|
|
160
|
+
self._lib.graal_attach_thread.restype = ctypes.c_int
|
|
161
|
+
self._lib.graal_detach_thread.argtypes = [_Thread_p]
|
|
162
|
+
self._lib.graal_detach_thread.restype = ctypes.c_int
|
|
163
|
+
|
|
164
|
+
self._isolate = _Isolate_p()
|
|
165
|
+
thread = _Thread_p()
|
|
166
|
+
rc = self._lib.graal_create_isolate(None, ctypes.byref(self._isolate), ctypes.byref(thread))
|
|
167
|
+
if rc != 0:
|
|
168
|
+
raise errors.InterfaceError(f"failed to create the GraalVM isolate (code {rc})")
|
|
169
|
+
self._local = threading.local()
|
|
170
|
+
self._local.thread = thread
|
|
171
|
+
|
|
172
|
+
self.version: int = self._lib.h2gis_version(thread)
|
|
173
|
+
if self.version < MIN_C_API_VERSION:
|
|
174
|
+
raise errors.InterfaceError(
|
|
175
|
+
f"{path!r} implements the H2GIS C API version {self.version}; "
|
|
176
|
+
f"h2gis-python 2 needs version {MIN_C_API_VERSION} or later")
|
|
177
|
+
|
|
178
|
+
# -- isolate thread -------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def thread(self):
|
|
182
|
+
thread = getattr(self._local, "thread", None)
|
|
183
|
+
if thread is None:
|
|
184
|
+
thread = _Thread_p()
|
|
185
|
+
rc = self._lib.graal_attach_thread(self._isolate, ctypes.byref(thread))
|
|
186
|
+
if rc != 0:
|
|
187
|
+
raise errors.InterfaceError(f"failed to attach thread to the isolate (code {rc})")
|
|
188
|
+
self._local.thread = thread
|
|
189
|
+
# Detach when the Python thread ends: CPython clears the thread
|
|
190
|
+
# local storage from the exiting thread itself, which is what
|
|
191
|
+
# graal_detach_thread requires. A thread that exits while still
|
|
192
|
+
# attached crashes the isolate at its next safepoint.
|
|
193
|
+
self._local.detacher = _Detacher(self._lib, thread)
|
|
194
|
+
return thread
|
|
195
|
+
|
|
196
|
+
# -- strings, errors ------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
def _take_string(self, ptr) -> str:
|
|
199
|
+
"""Read and free a string allocated by the library."""
|
|
200
|
+
if not ptr:
|
|
201
|
+
return ""
|
|
202
|
+
try:
|
|
203
|
+
return ctypes.string_at(ptr).decode("utf-8", "replace")
|
|
204
|
+
finally:
|
|
205
|
+
self._lib.h2gis_free_result_buffer(self.thread, ptr)
|
|
206
|
+
|
|
207
|
+
def last_error(self) -> str:
|
|
208
|
+
return self._take_string(self._lib.h2gis_get_last_error(self.thread))
|
|
209
|
+
|
|
210
|
+
def _raise(self, what: str, default=errors.DatabaseError):
|
|
211
|
+
message = self.last_error()
|
|
212
|
+
if not message:
|
|
213
|
+
raise default(f"{what} failed (no error message from the native library)")
|
|
214
|
+
raise errors.from_native_message(message, default)
|
|
215
|
+
|
|
216
|
+
# -- connections ----------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
def connect(self, path: str, user: str, password: str) -> int:
|
|
219
|
+
handle = self._lib.h2gis_connect(self.thread, _encode(path), _encode(user), _encode(password))
|
|
220
|
+
if not handle:
|
|
221
|
+
self._raise("connection", errors.OperationalError)
|
|
222
|
+
return handle
|
|
223
|
+
|
|
224
|
+
def close_connection(self, conn: int) -> None:
|
|
225
|
+
self._lib.h2gis_close_connection(self.thread, conn)
|
|
226
|
+
|
|
227
|
+
def delete_database_and_close(self, conn: int) -> None:
|
|
228
|
+
self._lib.h2gis_delete_database_and_close(self.thread, conn)
|
|
229
|
+
message = self.last_error()
|
|
230
|
+
if message:
|
|
231
|
+
raise errors.from_native_message(message, errors.OperationalError)
|
|
232
|
+
|
|
233
|
+
def load_functions(self, conn: int) -> None:
|
|
234
|
+
"""Register the H2GIS spatial functions (done by connect() when missing)."""
|
|
235
|
+
if not self._lib.h2gis_load(self.thread, conn):
|
|
236
|
+
self._raise("loading the H2GIS functions")
|
|
237
|
+
|
|
238
|
+
# -- plain statements -----------------------------------------------------
|
|
239
|
+
|
|
240
|
+
def execute(self, conn: int, sql: str) -> int:
|
|
241
|
+
"""Run an update statement (or a script), return the update count."""
|
|
242
|
+
count = self._lib.h2gis_execute(self.thread, conn, _encode(sql))
|
|
243
|
+
if count < 0:
|
|
244
|
+
self._raise("execute")
|
|
245
|
+
return count
|
|
246
|
+
|
|
247
|
+
def query(self, conn: int, sql: str) -> int:
|
|
248
|
+
"""Run a query, return a result set handle."""
|
|
249
|
+
handle = self._lib.h2gis_fetch(self.thread, conn, _encode(sql))
|
|
250
|
+
if not handle:
|
|
251
|
+
self._raise("query")
|
|
252
|
+
return handle
|
|
253
|
+
|
|
254
|
+
def close_query(self, handle: int) -> None:
|
|
255
|
+
"""Close a result set or statement handle (and the results of a statement)."""
|
|
256
|
+
self._lib.h2gis_close_query(self.thread, handle)
|
|
257
|
+
|
|
258
|
+
# -- prepared statements --------------------------------------------------
|
|
259
|
+
|
|
260
|
+
def prepare(self, conn: int, sql: str) -> int:
|
|
261
|
+
handle = self._lib.h2gis_prepare(self.thread, conn, _encode(sql))
|
|
262
|
+
if not handle:
|
|
263
|
+
self._raise("prepare", errors.ProgrammingError)
|
|
264
|
+
return handle
|
|
265
|
+
|
|
266
|
+
def bind(self, stmt: int, index: int, kind: str, value) -> None:
|
|
267
|
+
"""Bind one parameter (1-based ``index``) as returned by ``adapt.bind_value``."""
|
|
268
|
+
lib, th = self._lib, self.thread
|
|
269
|
+
if kind == "null":
|
|
270
|
+
rc = lib.h2gis_bind_null(th, stmt, index)
|
|
271
|
+
elif kind == "long":
|
|
272
|
+
rc = lib.h2gis_bind_long(th, stmt, index, value)
|
|
273
|
+
elif kind == "double":
|
|
274
|
+
rc = lib.h2gis_bind_double(th, stmt, index, value)
|
|
275
|
+
elif kind == "string":
|
|
276
|
+
rc = lib.h2gis_bind_string(th, stmt, index, value.encode("utf-8"))
|
|
277
|
+
elif kind == "blob":
|
|
278
|
+
data = bytes(value)
|
|
279
|
+
rc = lib.h2gis_bind_blob(th, stmt, index, data, len(data))
|
|
280
|
+
else: # pragma: no cover - guarded by adapt.bind_value
|
|
281
|
+
raise errors.InterfaceError(f"unknown bind kind {kind!r}")
|
|
282
|
+
if rc != 0:
|
|
283
|
+
self._raise(f"binding parameter {index}", errors.DataError)
|
|
284
|
+
|
|
285
|
+
def execute_prepared(self, stmt: int) -> int:
|
|
286
|
+
handle = self._lib.h2gis_execute_prepared(self.thread, stmt)
|
|
287
|
+
if not handle:
|
|
288
|
+
self._raise("prepared query")
|
|
289
|
+
return handle
|
|
290
|
+
|
|
291
|
+
def execute_prepared_update(self, stmt: int) -> int:
|
|
292
|
+
count = self._lib.h2gis_execute_prepared_update(self.thread, stmt)
|
|
293
|
+
if count < 0:
|
|
294
|
+
self._raise("prepared update")
|
|
295
|
+
return count
|
|
296
|
+
|
|
297
|
+
def add_batch(self, stmt: int) -> None:
|
|
298
|
+
if self._lib.h2gis_add_batch(self.thread, stmt) != 0:
|
|
299
|
+
self._raise("add batch")
|
|
300
|
+
|
|
301
|
+
def execute_batch(self, stmt: int) -> int:
|
|
302
|
+
count = self._lib.h2gis_execute_batch(self.thread, stmt)
|
|
303
|
+
if count < 0:
|
|
304
|
+
self._raise("execute batch")
|
|
305
|
+
return count
|
|
306
|
+
|
|
307
|
+
# -- results --------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
def _buffer(self, fn, what: str, *args) -> bytes:
|
|
310
|
+
size = ctypes.c_int64(0)
|
|
311
|
+
ptr = fn(self.thread, *args, ctypes.byref(size))
|
|
312
|
+
if not ptr:
|
|
313
|
+
self._raise(what, errors.FetchError)
|
|
314
|
+
try:
|
|
315
|
+
return ctypes.string_at(ptr, size.value)
|
|
316
|
+
finally:
|
|
317
|
+
self._lib.h2gis_free_result_buffer(self.thread, ptr)
|
|
318
|
+
|
|
319
|
+
def fetch_all(self, rs: int) -> bytes:
|
|
320
|
+
"""Serialized remaining rows (see _buffer.py)."""
|
|
321
|
+
return self._buffer(self._lib.h2gis_fetch_all, "fetch", rs)
|
|
322
|
+
|
|
323
|
+
def fetch_batch(self, rs: int, size: int) -> bytes:
|
|
324
|
+
"""Serialized next ``size`` rows; fewer rows means the end is reached."""
|
|
325
|
+
return self._buffer(self._lib.h2gis_fetch_batch, "fetch", rs, int(size))
|
|
326
|
+
|
|
327
|
+
def metadata(self, rs: int) -> List[dict]:
|
|
328
|
+
"""Column metadata: ``[{"name", "typeName", "type"}, ...]``."""
|
|
329
|
+
raw = self._take_string(self._lib.h2gis_get_metadata_json(self.thread, rs))
|
|
330
|
+
try:
|
|
331
|
+
return json.loads(raw) if raw else []
|
|
332
|
+
except ValueError:
|
|
333
|
+
return []
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class _Detacher:
|
|
337
|
+
__slots__ = ("lib", "thread")
|
|
338
|
+
|
|
339
|
+
def __init__(self, lib, thread):
|
|
340
|
+
self.lib, self.thread = lib, thread
|
|
341
|
+
|
|
342
|
+
def __del__(self):
|
|
343
|
+
try:
|
|
344
|
+
self.lib.graal_detach_thread(self.thread)
|
|
345
|
+
except Exception: # pragma: no cover - interpreter shutdown
|
|
346
|
+
pass
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _encode(text: str) -> bytes:
|
|
350
|
+
if not isinstance(text, str):
|
|
351
|
+
raise errors.InterfaceError(f"expected str, got {type(text).__name__}")
|
|
352
|
+
if "\x00" in text:
|
|
353
|
+
raise errors.DataError("NUL characters are not allowed in SQL strings")
|
|
354
|
+
return text.encode("utf-8")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
_LIBS: Dict[str, NativeLib] = {}
|
|
358
|
+
_LIBS_LOCK = threading.Lock()
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def get_lib(path: Optional[str] = None) -> NativeLib:
|
|
362
|
+
"""Return the shared :class:`NativeLib` for ``path`` (default library if None)."""
|
|
363
|
+
path = os.path.realpath(path or default_library_path())
|
|
364
|
+
with _LIBS_LOCK:
|
|
365
|
+
lib = _LIBS.get(path)
|
|
366
|
+
if lib is None:
|
|
367
|
+
lib = _LIBS[path] = NativeLib(path)
|
|
368
|
+
return lib
|