datalevin 0.10.16__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.
- datalevin/__init__.py +37 -0
- datalevin/_convert.py +192 -0
- datalevin/_interop.py +197 -0
- datalevin/_java.py +84 -0
- datalevin/_jvm.py +143 -0
- datalevin/_raw.py +120 -0
- datalevin/_resource.py +51 -0
- datalevin/client.py +169 -0
- datalevin/connection.py +77 -0
- datalevin/errors.py +55 -0
- datalevin/jars/datalevin-runtime-0.10.16.jar +0 -0
- datalevin/kv.py +87 -0
- datalevin/py.typed +1 -0
- datalevin/udf.py +61 -0
- datalevin-0.10.16.dist-info/METADATA +232 -0
- datalevin-0.10.16.dist-info/RECORD +18 -0
- datalevin-0.10.16.dist-info/WHEEL +5 -0
- datalevin-0.10.16.dist-info/top_level.txt +1 -0
datalevin/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Datalevin Python bindings over the JVM interop bridge."""
|
|
2
|
+
|
|
3
|
+
from ._interop import api_info, connect, exec_json, new_client, open_kv
|
|
4
|
+
from ._jvm import jvm_started, start_jvm
|
|
5
|
+
from ._raw import interop
|
|
6
|
+
from .client import Client
|
|
7
|
+
from .connection import Connection
|
|
8
|
+
from .errors import (
|
|
9
|
+
DatalevinConfigurationError,
|
|
10
|
+
DatalevinError,
|
|
11
|
+
DatalevinJavaError,
|
|
12
|
+
DatalevinJvmError,
|
|
13
|
+
)
|
|
14
|
+
from .kv import KV
|
|
15
|
+
from .udf import UdfRegistry, create_udf_registry
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Client",
|
|
19
|
+
"Connection",
|
|
20
|
+
"DatalevinConfigurationError",
|
|
21
|
+
"DatalevinError",
|
|
22
|
+
"DatalevinJavaError",
|
|
23
|
+
"DatalevinJvmError",
|
|
24
|
+
"KV",
|
|
25
|
+
"UdfRegistry",
|
|
26
|
+
"api_info",
|
|
27
|
+
"connect",
|
|
28
|
+
"create_udf_registry",
|
|
29
|
+
"exec_json",
|
|
30
|
+
"interop",
|
|
31
|
+
"jvm_started",
|
|
32
|
+
"new_client",
|
|
33
|
+
"open_kv",
|
|
34
|
+
"start_jvm",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
__version__ = "0.10.16"
|
datalevin/_convert.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Python <-> Java conversion helpers for Datalevin interop."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
import uuid as py_uuid
|
|
9
|
+
|
|
10
|
+
import jpype
|
|
11
|
+
from jpype.types import JByte, JLong
|
|
12
|
+
|
|
13
|
+
from ._java import call_java, classes, is_java_object
|
|
14
|
+
|
|
15
|
+
INT64_MIN = -(2**63)
|
|
16
|
+
INT64_MAX = 2**63 - 1
|
|
17
|
+
FORM_SYMBOL_STRINGS = {"*", "...", ".", "$", "%", "_"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def to_java(value):
|
|
21
|
+
"""Recursively convert Python values into Java/JVM-friendly values."""
|
|
22
|
+
|
|
23
|
+
if value is None or isinstance(value, (bool, float, str)):
|
|
24
|
+
return value
|
|
25
|
+
if is_java_object(value):
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
raw_handle = getattr(value, "raw_handle", None)
|
|
29
|
+
if callable(raw_handle):
|
|
30
|
+
return raw_handle()
|
|
31
|
+
|
|
32
|
+
if isinstance(value, int):
|
|
33
|
+
if INT64_MIN <= value <= INT64_MAX:
|
|
34
|
+
return JLong(value)
|
|
35
|
+
return classes().big_integer(str(value))
|
|
36
|
+
|
|
37
|
+
if isinstance(value, Decimal):
|
|
38
|
+
return classes().big_decimal(str(value))
|
|
39
|
+
|
|
40
|
+
if isinstance(value, py_uuid.UUID):
|
|
41
|
+
return classes().uuid.fromString(str(value))
|
|
42
|
+
|
|
43
|
+
if isinstance(value, datetime):
|
|
44
|
+
if value.tzinfo is None:
|
|
45
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
46
|
+
iso = value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
47
|
+
return classes().instant.parse(iso)
|
|
48
|
+
|
|
49
|
+
if isinstance(value, (bytes, bytearray, memoryview)):
|
|
50
|
+
return jpype.JArray(JByte)(bytes(value))
|
|
51
|
+
|
|
52
|
+
if isinstance(value, Mapping):
|
|
53
|
+
jmap = classes().linked_hash_map()
|
|
54
|
+
for key, item in value.items():
|
|
55
|
+
jmap.put(to_java(key), to_java(item))
|
|
56
|
+
return jmap
|
|
57
|
+
|
|
58
|
+
if isinstance(value, (set, frozenset)):
|
|
59
|
+
jset = classes().linked_hash_set()
|
|
60
|
+
for item in value:
|
|
61
|
+
jset.add(to_java(item))
|
|
62
|
+
return jset
|
|
63
|
+
|
|
64
|
+
if isinstance(value, (list, tuple)):
|
|
65
|
+
jlist = classes().array_list()
|
|
66
|
+
for item in value:
|
|
67
|
+
jlist.add(to_java(item))
|
|
68
|
+
return jlist
|
|
69
|
+
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def to_python(value):
|
|
74
|
+
"""Recursively convert Java results into Python-native values."""
|
|
75
|
+
|
|
76
|
+
if value is None or isinstance(value, (bool, int, float, str, bytes)):
|
|
77
|
+
return value
|
|
78
|
+
|
|
79
|
+
if not is_java_object(value):
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
cls = classes()
|
|
83
|
+
byte_array_type = jpype.JArray(JByte)
|
|
84
|
+
|
|
85
|
+
if isinstance(value, byte_array_type):
|
|
86
|
+
return bytes(value)
|
|
87
|
+
|
|
88
|
+
if isinstance(value, cls.uuid):
|
|
89
|
+
return py_uuid.UUID(str(value))
|
|
90
|
+
|
|
91
|
+
if isinstance(value, cls.instant):
|
|
92
|
+
return datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
93
|
+
|
|
94
|
+
if isinstance(value, cls.date):
|
|
95
|
+
return datetime.fromtimestamp(value.getTime() / 1000.0, tz=timezone.utc)
|
|
96
|
+
|
|
97
|
+
if isinstance(value, cls.big_integer):
|
|
98
|
+
return int(str(value))
|
|
99
|
+
|
|
100
|
+
if isinstance(value, cls.big_decimal):
|
|
101
|
+
return Decimal(str(value))
|
|
102
|
+
|
|
103
|
+
if isinstance(value, (cls.keyword_type, cls.symbol_type)):
|
|
104
|
+
return str(value)
|
|
105
|
+
|
|
106
|
+
if isinstance(value, cls.map_type) or hasattr(value, "entrySet"):
|
|
107
|
+
result = {}
|
|
108
|
+
for entry in value.entrySet():
|
|
109
|
+
result[to_python(entry.getKey())] = to_python(entry.getValue())
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
if isinstance(value, cls.set_type):
|
|
113
|
+
return {to_python(item) for item in value}
|
|
114
|
+
|
|
115
|
+
if isinstance(value, (cls.list_type, cls.collection_type)) or hasattr(value, "iterator"):
|
|
116
|
+
return [to_python(item) for item in value]
|
|
117
|
+
|
|
118
|
+
return value
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def to_edn_form(value):
|
|
122
|
+
"""Convert Python form structures into EDN-friendly JVM values."""
|
|
123
|
+
|
|
124
|
+
if value is None or is_java_object(value):
|
|
125
|
+
return value
|
|
126
|
+
|
|
127
|
+
raw_handle = getattr(value, "raw_handle", None)
|
|
128
|
+
if callable(raw_handle):
|
|
129
|
+
return raw_handle()
|
|
130
|
+
|
|
131
|
+
if isinstance(value, str):
|
|
132
|
+
if value.startswith(":"):
|
|
133
|
+
return call_java(classes().interop.keyword, value)
|
|
134
|
+
if value.startswith("?") or value in FORM_SYMBOL_STRINGS:
|
|
135
|
+
return call_java(classes().interop.symbol, value)
|
|
136
|
+
return value
|
|
137
|
+
|
|
138
|
+
if isinstance(value, Mapping):
|
|
139
|
+
jmap = classes().linked_hash_map()
|
|
140
|
+
for key, item in value.items():
|
|
141
|
+
jmap.put(to_edn_form(key), to_edn_form(item))
|
|
142
|
+
return jmap
|
|
143
|
+
|
|
144
|
+
if isinstance(value, (set, frozenset)):
|
|
145
|
+
jset = classes().linked_hash_set()
|
|
146
|
+
for item in value:
|
|
147
|
+
jset.add(to_edn_form(item))
|
|
148
|
+
return jset
|
|
149
|
+
|
|
150
|
+
if isinstance(value, (list, tuple)):
|
|
151
|
+
jlist = classes().array_list()
|
|
152
|
+
for item in value:
|
|
153
|
+
jlist.add(to_edn_form(item))
|
|
154
|
+
return jlist
|
|
155
|
+
|
|
156
|
+
return to_java(value)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def to_query_input(value):
|
|
160
|
+
"""Convert Python query inputs while preserving ordinary string literals."""
|
|
161
|
+
|
|
162
|
+
if value is None or is_java_object(value):
|
|
163
|
+
return value
|
|
164
|
+
|
|
165
|
+
raw_handle = getattr(value, "raw_handle", None)
|
|
166
|
+
if callable(raw_handle):
|
|
167
|
+
return raw_handle()
|
|
168
|
+
|
|
169
|
+
if isinstance(value, str):
|
|
170
|
+
if value.startswith(":"):
|
|
171
|
+
return call_java(classes().interop.keyword, value)
|
|
172
|
+
return value
|
|
173
|
+
|
|
174
|
+
if isinstance(value, Mapping):
|
|
175
|
+
jmap = classes().linked_hash_map()
|
|
176
|
+
for key, item in value.items():
|
|
177
|
+
jmap.put(to_query_input(key), to_query_input(item))
|
|
178
|
+
return jmap
|
|
179
|
+
|
|
180
|
+
if isinstance(value, (set, frozenset)):
|
|
181
|
+
jset = classes().linked_hash_set()
|
|
182
|
+
for item in value:
|
|
183
|
+
jset.add(to_query_input(item))
|
|
184
|
+
return jset
|
|
185
|
+
|
|
186
|
+
if isinstance(value, (list, tuple)):
|
|
187
|
+
jlist = classes().array_list()
|
|
188
|
+
for item in value:
|
|
189
|
+
jlist.add(to_query_input(item))
|
|
190
|
+
return jlist
|
|
191
|
+
|
|
192
|
+
return to_java(value)
|
datalevin/_interop.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Internal interop bindings and public constructors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from ._convert import to_java, to_python
|
|
8
|
+
from ._java import call_java, classes
|
|
9
|
+
from ._jvm import jvm_started, start_jvm
|
|
10
|
+
from .errors import DatalevinError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InteropBindings:
|
|
14
|
+
"""Thin wrapper around the Datalevin JVM bridge."""
|
|
15
|
+
|
|
16
|
+
def api_info_raw(self):
|
|
17
|
+
return call_java(classes().datalevin.apiInfo)
|
|
18
|
+
|
|
19
|
+
def exec_json_raw(self, request_json: str) -> str:
|
|
20
|
+
json_api_exec = getattr(classes().json_api, "exec_", None)
|
|
21
|
+
if json_api_exec is None:
|
|
22
|
+
json_api_exec = getattr(classes().json_api, "exec")
|
|
23
|
+
return str(call_java(json_api_exec, request_json))
|
|
24
|
+
|
|
25
|
+
def core_invoke(self, function: str, args=None):
|
|
26
|
+
return call_java(classes().interop.coreInvoke, function, to_java(list(args or ())))
|
|
27
|
+
|
|
28
|
+
def client_invoke(self, function: str, args=None):
|
|
29
|
+
return call_java(classes().interop.clientInvoke, function, to_java(list(args or ())))
|
|
30
|
+
|
|
31
|
+
def create_connection(self, dir=None, schema=None, opts=None, *, shared: bool = False):
|
|
32
|
+
target = classes().interop.getConnection if shared else classes().interop.createConnection
|
|
33
|
+
return call_java(target, dir, to_java(schema), to_java(opts))
|
|
34
|
+
|
|
35
|
+
def close_connection(self, handle) -> None:
|
|
36
|
+
call_java(classes().interop.closeConnection, handle)
|
|
37
|
+
|
|
38
|
+
def connection_closed(self, handle) -> bool:
|
|
39
|
+
return bool(call_java(classes().interop.connectionClosed, handle))
|
|
40
|
+
|
|
41
|
+
def connection_db(self, handle):
|
|
42
|
+
return call_java(classes().interop.connectionDb, handle)
|
|
43
|
+
|
|
44
|
+
def open_key_value(self, dir, opts=None):
|
|
45
|
+
return call_java(classes().interop.openKeyValue, dir, to_java(opts))
|
|
46
|
+
|
|
47
|
+
def close_key_value(self, handle) -> None:
|
|
48
|
+
call_java(classes().interop.closeKeyValue, handle)
|
|
49
|
+
|
|
50
|
+
def key_value_closed(self, handle) -> bool:
|
|
51
|
+
return bool(call_java(classes().interop.keyValueClosed, handle))
|
|
52
|
+
|
|
53
|
+
def new_client(self, uri, opts=None):
|
|
54
|
+
return call_java(classes().interop.newClient, uri, to_java(opts))
|
|
55
|
+
|
|
56
|
+
def close_client(self, handle) -> None:
|
|
57
|
+
call_java(classes().interop.closeClient, handle)
|
|
58
|
+
|
|
59
|
+
def client_disconnected(self, handle) -> bool:
|
|
60
|
+
return bool(call_java(classes().interop.clientDisconnected, handle))
|
|
61
|
+
|
|
62
|
+
def read_edn(self, edn: str):
|
|
63
|
+
return call_java(classes().interop.readEdn, edn)
|
|
64
|
+
|
|
65
|
+
def keyword(self, value: str):
|
|
66
|
+
return call_java(classes().interop.keyword, value)
|
|
67
|
+
|
|
68
|
+
def symbol(self, value: str):
|
|
69
|
+
return call_java(classes().interop.symbol, value)
|
|
70
|
+
|
|
71
|
+
def schema(self, schema):
|
|
72
|
+
if schema is None:
|
|
73
|
+
return None
|
|
74
|
+
return call_java(classes().interop.schema, to_java(schema))
|
|
75
|
+
|
|
76
|
+
def options(self, opts):
|
|
77
|
+
if opts is None:
|
|
78
|
+
return None
|
|
79
|
+
return call_java(classes().interop.options, to_java(opts))
|
|
80
|
+
|
|
81
|
+
def udf_descriptor(self, descriptor):
|
|
82
|
+
if descriptor is None:
|
|
83
|
+
return None
|
|
84
|
+
return call_java(classes().interop.udfDescriptor, to_java(descriptor))
|
|
85
|
+
|
|
86
|
+
def create_udf_registry(self):
|
|
87
|
+
return call_java(classes().interop.createUdfRegistry)
|
|
88
|
+
|
|
89
|
+
def register_udf(self, registry, descriptor, fn):
|
|
90
|
+
return call_java(classes().interop.registerUdf, registry, to_java(descriptor), fn)
|
|
91
|
+
|
|
92
|
+
def unregister_udf(self, registry, descriptor):
|
|
93
|
+
return call_java(classes().interop.unregisterUdf, registry, to_java(descriptor))
|
|
94
|
+
|
|
95
|
+
def registered_udf(self, registry, descriptor) -> bool:
|
|
96
|
+
return bool(call_java(classes().interop.registeredUdf, registry, to_java(descriptor)))
|
|
97
|
+
|
|
98
|
+
def rename_map(self, rename_map):
|
|
99
|
+
if rename_map is None:
|
|
100
|
+
return None
|
|
101
|
+
return call_java(classes().interop.renameMap, to_java(rename_map))
|
|
102
|
+
|
|
103
|
+
def delete_attrs(self, attrs):
|
|
104
|
+
if attrs is None:
|
|
105
|
+
return None
|
|
106
|
+
return call_java(classes().interop.deleteAttrs, to_java(list(attrs or ())))
|
|
107
|
+
|
|
108
|
+
def lookup_ref(self, value):
|
|
109
|
+
if value is None:
|
|
110
|
+
return None
|
|
111
|
+
return call_java(classes().interop.lookupRef, to_java(value))
|
|
112
|
+
|
|
113
|
+
def tx_data(self, tx_data):
|
|
114
|
+
if tx_data is None:
|
|
115
|
+
return None
|
|
116
|
+
return call_java(classes().interop.txData, to_java(tx_data))
|
|
117
|
+
|
|
118
|
+
def kv_txs(self, txs):
|
|
119
|
+
if txs is None:
|
|
120
|
+
return None
|
|
121
|
+
return call_java(classes().interop.kvTxs, to_java(txs))
|
|
122
|
+
|
|
123
|
+
def kv_type(self, value):
|
|
124
|
+
if value is None:
|
|
125
|
+
return None
|
|
126
|
+
return call_java(classes().interop.kvType, to_java(value))
|
|
127
|
+
|
|
128
|
+
def database_type(self, value: str):
|
|
129
|
+
return call_java(classes().interop.databaseType, value)
|
|
130
|
+
|
|
131
|
+
def role(self, role: str):
|
|
132
|
+
return call_java(classes().interop.role, role)
|
|
133
|
+
|
|
134
|
+
def permission_keyword(self, value: str):
|
|
135
|
+
return call_java(classes().interop.permissionKeyword, value)
|
|
136
|
+
|
|
137
|
+
def permission_target(self, object_type: str, target):
|
|
138
|
+
return call_java(classes().interop.permissionTarget, object_type, to_java(target))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
_BINDINGS = InteropBindings()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def api_info():
|
|
145
|
+
"""Return Datalevin JSON/API metadata as a Python dictionary."""
|
|
146
|
+
|
|
147
|
+
return to_python(_BINDINGS.api_info_raw())
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def exec_json(op: str, args=None):
|
|
151
|
+
"""Execute a raw JSON API operation."""
|
|
152
|
+
|
|
153
|
+
request = json.dumps({"op": op, "args": args or {}})
|
|
154
|
+
envelope = json.loads(_BINDINGS.exec_json_raw(request))
|
|
155
|
+
if envelope.get("ok"):
|
|
156
|
+
return envelope.get("result")
|
|
157
|
+
raise DatalevinError(
|
|
158
|
+
envelope.get("error") or "Datalevin JSON API request failed.",
|
|
159
|
+
type_name=envelope.get("type"),
|
|
160
|
+
data=envelope.get("data"),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def connect(dir=None, schema=None, opts=None, *, shared: bool = False) -> Connection:
|
|
165
|
+
"""Create or open a Datalevin Datalog connection."""
|
|
166
|
+
|
|
167
|
+
from .connection import Connection
|
|
168
|
+
|
|
169
|
+
return Connection(_BINDINGS.create_connection(dir, schema, opts, shared=shared))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def open_kv(dir, opts=None) -> KV:
|
|
173
|
+
"""Open a Datalevin KV store."""
|
|
174
|
+
|
|
175
|
+
from .kv import KV
|
|
176
|
+
|
|
177
|
+
return KV(_BINDINGS.open_key_value(dir, opts))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def new_client(uri, opts=None) -> Client:
|
|
181
|
+
"""Open a remote Datalevin admin client."""
|
|
182
|
+
|
|
183
|
+
from .client import Client
|
|
184
|
+
|
|
185
|
+
return Client(_BINDINGS.new_client(uri, opts))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
__all__ = [
|
|
189
|
+
"_BINDINGS",
|
|
190
|
+
"api_info",
|
|
191
|
+
"connect",
|
|
192
|
+
"exec_json",
|
|
193
|
+
"jvm_started",
|
|
194
|
+
"new_client",
|
|
195
|
+
"open_kv",
|
|
196
|
+
"start_jvm",
|
|
197
|
+
]
|
datalevin/_java.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""JPype class loading and Java call helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
|
|
8
|
+
import jpype
|
|
9
|
+
|
|
10
|
+
from ._jvm import start_jvm
|
|
11
|
+
from .errors import DatalevinJavaError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class JavaClasses:
|
|
16
|
+
"""Cached Java classes used by the Python wrapper."""
|
|
17
|
+
|
|
18
|
+
datalevin: object
|
|
19
|
+
interop: object
|
|
20
|
+
json_api: object
|
|
21
|
+
udf_function: object
|
|
22
|
+
linked_hash_map: object
|
|
23
|
+
linked_hash_set: object
|
|
24
|
+
array_list: object
|
|
25
|
+
uuid: object
|
|
26
|
+
instant: object
|
|
27
|
+
date: object
|
|
28
|
+
big_integer: object
|
|
29
|
+
big_decimal: object
|
|
30
|
+
map_type: object
|
|
31
|
+
list_type: object
|
|
32
|
+
set_type: object
|
|
33
|
+
collection_type: object
|
|
34
|
+
keyword_type: object
|
|
35
|
+
symbol_type: object
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@lru_cache(maxsize=1)
|
|
39
|
+
def classes() -> JavaClasses:
|
|
40
|
+
"""Load and cache the Java classes used by the binding."""
|
|
41
|
+
|
|
42
|
+
start_jvm()
|
|
43
|
+
return JavaClasses(
|
|
44
|
+
datalevin=jpype.JClass("datalevin.Datalevin"),
|
|
45
|
+
interop=jpype.JClass("datalevin.DatalevinInterop"),
|
|
46
|
+
json_api=jpype.JClass("datalevin.JSONApi"),
|
|
47
|
+
udf_function=jpype.JClass("datalevin.UdfFunction"),
|
|
48
|
+
linked_hash_map=jpype.JClass("java.util.LinkedHashMap"),
|
|
49
|
+
linked_hash_set=jpype.JClass("java.util.LinkedHashSet"),
|
|
50
|
+
array_list=jpype.JClass("java.util.ArrayList"),
|
|
51
|
+
uuid=jpype.JClass("java.util.UUID"),
|
|
52
|
+
instant=jpype.JClass("java.time.Instant"),
|
|
53
|
+
date=jpype.JClass("java.util.Date"),
|
|
54
|
+
big_integer=jpype.JClass("java.math.BigInteger"),
|
|
55
|
+
big_decimal=jpype.JClass("java.math.BigDecimal"),
|
|
56
|
+
map_type=jpype.JClass("java.util.Map"),
|
|
57
|
+
list_type=jpype.JClass("java.util.List"),
|
|
58
|
+
set_type=jpype.JClass("java.util.Set"),
|
|
59
|
+
collection_type=jpype.JClass("java.util.Collection"),
|
|
60
|
+
keyword_type=jpype.JClass("clojure.lang.Keyword"),
|
|
61
|
+
symbol_type=jpype.JClass("clojure.lang.Symbol"),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def call_java(target, *args):
|
|
66
|
+
"""Call a Java method and wrap JPype Java exceptions."""
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
return target(*args)
|
|
70
|
+
except jpype.JException as exc:
|
|
71
|
+
raise DatalevinJavaError.from_java(exc) from exc
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def is_java_object(value) -> bool:
|
|
75
|
+
"""Best-effort detection for live Java objects exposed through JPype."""
|
|
76
|
+
|
|
77
|
+
if value is None or not jpype.isJVMStarted():
|
|
78
|
+
return False
|
|
79
|
+
module = type(value).__module__
|
|
80
|
+
return (
|
|
81
|
+
module.startswith("jpype")
|
|
82
|
+
or module.startswith("_jpype")
|
|
83
|
+
or str(type(value)).startswith("<java ")
|
|
84
|
+
)
|
datalevin/_jvm.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""JVM bootstrap and runtime jar resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.resources as resources
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import shlex
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import jpype
|
|
12
|
+
|
|
13
|
+
from .errors import DatalevinConfigurationError, DatalevinJvmError
|
|
14
|
+
|
|
15
|
+
DATALEVIN_JAR_ENV = "DATALEVIN_JAR"
|
|
16
|
+
DATALEVIN_CLASSPATH_ENV = "DATALEVIN_CLASSPATH"
|
|
17
|
+
DATALEVIN_JVM_ARGS_ENV = "DATALEVIN_JVM_ARGS"
|
|
18
|
+
DATALEVIN_JAVACPP_CACHEDIR_ENV = "DATALEVIN_JAVACPP_CACHEDIR"
|
|
19
|
+
PACKAGE_NAME = "datalevin"
|
|
20
|
+
TARGET_JAR_PATTERNS = (
|
|
21
|
+
"datalevin-runtime-*.jar",
|
|
22
|
+
"datalevin-python-runtime-*.jar",
|
|
23
|
+
"datalevin-java-*.jar",
|
|
24
|
+
)
|
|
25
|
+
DEFAULT_JAVACPP_CACHEDIR = Path("/tmp/datalevin-javacpp-cache")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def jvm_started() -> bool:
|
|
29
|
+
"""Return whether the current Python process already has a JVM."""
|
|
30
|
+
|
|
31
|
+
return jpype.isJVMStarted()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def resolve_classpath(classpath: list[str] | tuple[str, ...] | None = None) -> list[str]:
|
|
35
|
+
"""Resolve the JVM classpath for the Datalevin runtime."""
|
|
36
|
+
|
|
37
|
+
if classpath:
|
|
38
|
+
return [str(Path(entry).expanduser()) for entry in classpath]
|
|
39
|
+
|
|
40
|
+
env_classpath = os.environ.get(DATALEVIN_CLASSPATH_ENV)
|
|
41
|
+
if env_classpath:
|
|
42
|
+
return [entry for entry in env_classpath.split(os.pathsep) if entry]
|
|
43
|
+
|
|
44
|
+
env_jar = os.environ.get(DATALEVIN_JAR_ENV)
|
|
45
|
+
if env_jar:
|
|
46
|
+
return [str(Path(env_jar).expanduser())]
|
|
47
|
+
|
|
48
|
+
vendored = _vendored_jars()
|
|
49
|
+
if vendored:
|
|
50
|
+
return vendored
|
|
51
|
+
|
|
52
|
+
repo_local = _repo_local_jars()
|
|
53
|
+
if repo_local:
|
|
54
|
+
return repo_local
|
|
55
|
+
|
|
56
|
+
raise DatalevinConfigurationError(
|
|
57
|
+
"Unable to resolve a Datalevin runtime jar. "
|
|
58
|
+
f"Set {DATALEVIN_JAR_ENV}, set {DATALEVIN_CLASSPATH_ENV}, "
|
|
59
|
+
"or vendor a jar under datalevin/jars/."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def start_jvm(
|
|
64
|
+
*,
|
|
65
|
+
classpath: list[str] | tuple[str, ...] | None = None,
|
|
66
|
+
jvm_args: list[str] | tuple[str, ...] | None = None,
|
|
67
|
+
convert_strings: bool = False,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Start the JVM if it is not already running."""
|
|
70
|
+
|
|
71
|
+
if jpype.isJVMStarted():
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
resolved_classpath = resolve_classpath(classpath)
|
|
75
|
+
resolved_jvm_args = list(jvm_args or ())
|
|
76
|
+
if not resolved_jvm_args:
|
|
77
|
+
env_args = os.environ.get(DATALEVIN_JVM_ARGS_ENV)
|
|
78
|
+
if env_args:
|
|
79
|
+
resolved_jvm_args = shlex.split(env_args)
|
|
80
|
+
_ensure_javacpp_cachedir_arg(resolved_jvm_args)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
jpype.startJVM(
|
|
84
|
+
*resolved_jvm_args,
|
|
85
|
+
classpath=resolved_classpath,
|
|
86
|
+
convertStrings=convert_strings,
|
|
87
|
+
)
|
|
88
|
+
except OSError as exc:
|
|
89
|
+
raise DatalevinJvmError(
|
|
90
|
+
"Failed to start the JVM for Datalevin. "
|
|
91
|
+
"Check that Java 21+ is installed and that the runtime jar is valid.",
|
|
92
|
+
cause=exc,
|
|
93
|
+
) from exc
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _vendored_jars() -> list[str]:
|
|
97
|
+
try:
|
|
98
|
+
jar_root = resources.files(PACKAGE_NAME).joinpath("jars")
|
|
99
|
+
except (FileNotFoundError, ModuleNotFoundError):
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
if not jar_root.is_dir():
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
jar = _preferred_runtime_jar(Path(str(jar_root)))
|
|
106
|
+
return [] if jar is None else [str(jar)]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _repo_local_jars() -> list[str]:
|
|
110
|
+
repo_root = Path(__file__).resolve().parents[4]
|
|
111
|
+
target_dir = repo_root / "target"
|
|
112
|
+
if not target_dir.exists():
|
|
113
|
+
return []
|
|
114
|
+
jar = _preferred_runtime_jar(target_dir)
|
|
115
|
+
return [] if jar is None else [str(jar)]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _preferred_runtime_jar(dir_path: Path) -> Path | None:
|
|
119
|
+
for pattern in TARGET_JAR_PATTERNS:
|
|
120
|
+
matches = sorted(dir_path.glob(pattern), key=_jar_sort_key)
|
|
121
|
+
if matches:
|
|
122
|
+
return matches[-1]
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _jar_sort_key(path: Path):
|
|
127
|
+
stem = path.stem
|
|
128
|
+
version = stem.rsplit("-", 1)[-1]
|
|
129
|
+
parts = re.split(r"([0-9]+)", version)
|
|
130
|
+
return [int(part) if part.isdigit() else part for part in parts]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _ensure_javacpp_cachedir_arg(jvm_args: list[str]) -> None:
|
|
134
|
+
for arg in jvm_args:
|
|
135
|
+
if (
|
|
136
|
+
arg.startswith("-Dorg.bytedeco.javacpp.cachedir=")
|
|
137
|
+
or arg.startswith("-Dorg.bytedeco.javacpp.cacheDir=")
|
|
138
|
+
):
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
cache_dir = Path(os.environ.get(DATALEVIN_JAVACPP_CACHEDIR_ENV, DEFAULT_JAVACPP_CACHEDIR))
|
|
142
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
jvm_args.append(f"-Dorg.bytedeco.javacpp.cachedir={cache_dir}")
|