machbaseapi 2.0.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.
- machbaseAPI/__init__.py +32 -0
- machbaseAPI/connector.py +251 -0
- machbaseAPI/conntest.py +27 -0
- machbaseAPI/constants.py +189 -0
- machbaseAPI/errors.py +38 -0
- machbaseAPI/machbaseAPI.py +318 -0
- machbaseAPI/marshal.py +225 -0
- machbaseAPI/packet.py +87 -0
- machbaseAPI/protocol.py +661 -0
- machbaseAPI/sample/ConnTest.py +55 -0
- machbaseAPI/sample/MakeData.py +14 -0
- machbaseAPI/sample/Sample1Connect.py +27 -0
- machbaseAPI/sample/Sample2Simple.py +77 -0
- machbaseAPI/sample/Sample3Append.py +70 -0
- machbaseAPI/sample/Sample4Fetch.py +137 -0
- machbaseAPI/sample/Sample5Append2.py +81 -0
- machbaseAPI/sample/Sample5ConnectEx.py +57 -0
- machbaseAPI/sample/__init__.py +1 -0
- machbaseAPI/sample/data.txt +99 -0
- machbaseAPI/types.py +52 -0
- machbaseapi-2.0.0.dist-info/METADATA +121 -0
- machbaseapi-2.0.0.dist-info/RECORD +24 -0
- machbaseapi-2.0.0.dist-info/WHEEL +5 -0
- machbaseapi-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence
|
|
6
|
+
|
|
7
|
+
from .connector import MachbaseConnection
|
|
8
|
+
from .errors import Error, ProgrammingError
|
|
9
|
+
|
|
10
|
+
gCharset = "UTF-8"
|
|
11
|
+
_SELECT_PREFIX = re.compile(r"^\s*(SELECT|WITH|DESC|DESCRIBE|SHOW)\b", re.IGNORECASE)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _stringify(value: Any) -> Any:
|
|
15
|
+
if value is None:
|
|
16
|
+
return None
|
|
17
|
+
if isinstance(value, (bytes, bytearray, memoryview)):
|
|
18
|
+
value = bytes(value)
|
|
19
|
+
return value.hex().upper()
|
|
20
|
+
if isinstance(value, float):
|
|
21
|
+
return str(int(value)) if value.is_integer() else str(value)
|
|
22
|
+
if isinstance(value, bool):
|
|
23
|
+
return "1" if value else "0"
|
|
24
|
+
return str(value)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _encode_row(row: Dict[str, Any]) -> str:
|
|
28
|
+
encoded = {str(key).lower(): _stringify(value) for key, value in row.items()}
|
|
29
|
+
if "_arrival_time" in encoded and "_ARRIVAL_TIME" not in encoded:
|
|
30
|
+
encoded["_ARRIVAL_TIME"] = encoded["_arrival_time"]
|
|
31
|
+
return json.dumps(encoded, separators=(",", ":"))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _encode_rows(rows: Sequence[Dict[str, Any]]) -> str:
|
|
35
|
+
return ",".join(_encode_row(row) for row in rows)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class machbaseAPI(object):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class machbase(object):
|
|
43
|
+
def __init__(self):
|
|
44
|
+
self._conn: Optional[MachbaseConnection] = None
|
|
45
|
+
self._opened = False
|
|
46
|
+
self._append_session = None
|
|
47
|
+
self._rows: List[Dict[str, Any]] = []
|
|
48
|
+
self._last_result: str = ""
|
|
49
|
+
self._append_types: Optional[List[int]] = None
|
|
50
|
+
|
|
51
|
+
def _set_ok(self, text: str) -> int:
|
|
52
|
+
self._last_result = json.dumps({"EXECUTE RESULT": text}, separators=(",", ":"))
|
|
53
|
+
return 1
|
|
54
|
+
|
|
55
|
+
def _set_error(self, text: str, detail: str = "") -> int:
|
|
56
|
+
payload = {"EXECUTE ERROR": text}
|
|
57
|
+
if detail:
|
|
58
|
+
payload["MACHBASE_ERROR"] = detail
|
|
59
|
+
self._last_result = json.dumps(payload, separators=(",", ":"))
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
def _ensure_conn(self) -> MachbaseConnection:
|
|
63
|
+
if self._conn is None:
|
|
64
|
+
raise RuntimeError("Connection is not open")
|
|
65
|
+
return self._conn
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def _normalize_append_payload(
|
|
69
|
+
types_or_values: Any,
|
|
70
|
+
explicit_values: Any = None,
|
|
71
|
+
) -> tuple[Optional[List[int]], List[Sequence[Any]]]:
|
|
72
|
+
if explicit_values is not None:
|
|
73
|
+
if types_or_values is None:
|
|
74
|
+
types = None
|
|
75
|
+
else:
|
|
76
|
+
types = [int(value) for value in types_or_values]
|
|
77
|
+
raw_rows = explicit_values
|
|
78
|
+
else:
|
|
79
|
+
types = None
|
|
80
|
+
raw_rows = types_or_values
|
|
81
|
+
|
|
82
|
+
if raw_rows is None:
|
|
83
|
+
return types, []
|
|
84
|
+
|
|
85
|
+
if isinstance(raw_rows, (list, tuple)):
|
|
86
|
+
if len(raw_rows) == 0:
|
|
87
|
+
return types, []
|
|
88
|
+
if isinstance(raw_rows[0], (list, tuple)):
|
|
89
|
+
rows = [list(row) for row in raw_rows]
|
|
90
|
+
else:
|
|
91
|
+
rows = [list(raw_rows)]
|
|
92
|
+
else:
|
|
93
|
+
raise ProgrammingError("Append values must be sequence of rows.")
|
|
94
|
+
|
|
95
|
+
return types, rows
|
|
96
|
+
|
|
97
|
+
def isConnected(self):
|
|
98
|
+
return 1 if self._conn and self._conn.is_connected() else 0
|
|
99
|
+
|
|
100
|
+
def isOpened(self):
|
|
101
|
+
return 1 if self._opened else 0
|
|
102
|
+
|
|
103
|
+
def open(self, aHost="127.0.0.1", aUser="SYS", aPw="MANAGER", aPort=5656):
|
|
104
|
+
try:
|
|
105
|
+
self._conn = MachbaseConnection(host=aHost, user=aUser.upper(), password=aPw, port=int(aPort))
|
|
106
|
+
self._opened = True
|
|
107
|
+
return 1
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
self._conn = None
|
|
110
|
+
self._opened = False
|
|
111
|
+
return self._set_error("Connection error.", str(exc))
|
|
112
|
+
|
|
113
|
+
def openEx(self, aHost="127.0.0.1", aUser="SYS", aPw="MANAGER", aPort=5656, aConnStr=""):
|
|
114
|
+
try:
|
|
115
|
+
self._conn = MachbaseConnection(
|
|
116
|
+
host=aHost,
|
|
117
|
+
user=aUser.upper(),
|
|
118
|
+
password=aPw,
|
|
119
|
+
port=int(aPort),
|
|
120
|
+
connection_string=aConnStr,
|
|
121
|
+
)
|
|
122
|
+
self._opened = True
|
|
123
|
+
return 1
|
|
124
|
+
except Exception as exc:
|
|
125
|
+
self._conn = None
|
|
126
|
+
self._opened = False
|
|
127
|
+
return self._set_error("Connection error.", str(exc))
|
|
128
|
+
|
|
129
|
+
def close(self):
|
|
130
|
+
if self._conn is None:
|
|
131
|
+
self._opened = False
|
|
132
|
+
return 1
|
|
133
|
+
try:
|
|
134
|
+
self._conn.close()
|
|
135
|
+
self._opened = False
|
|
136
|
+
self._conn = None
|
|
137
|
+
self._append_session = None
|
|
138
|
+
self._rows = []
|
|
139
|
+
return 1
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
return self._set_error("Connection close error.", str(exc))
|
|
142
|
+
|
|
143
|
+
def execute(self, aSql):
|
|
144
|
+
try:
|
|
145
|
+
if _SELECT_PREFIX.match(aSql):
|
|
146
|
+
if self.select(aSql) == 0:
|
|
147
|
+
return 0
|
|
148
|
+
return 1
|
|
149
|
+
self._ensure_conn()._client.exec_direct(aSql)
|
|
150
|
+
self._rows = []
|
|
151
|
+
return self._set_ok("Execute Success")
|
|
152
|
+
except Exception as exc:
|
|
153
|
+
return self._set_error("SQLPrepare error.", str(exc))
|
|
154
|
+
|
|
155
|
+
def select(self, aSql):
|
|
156
|
+
try:
|
|
157
|
+
result = self._ensure_conn()._client.query(aSql)
|
|
158
|
+
self._rows = list(result.rows)
|
|
159
|
+
self._last_result = _encode_rows(self._rows)
|
|
160
|
+
return 1
|
|
161
|
+
except Exception as exc:
|
|
162
|
+
self._rows = []
|
|
163
|
+
return self._set_error("SQLPrepare error.", str(exc))
|
|
164
|
+
|
|
165
|
+
def fetch(self):
|
|
166
|
+
if not self._rows:
|
|
167
|
+
return 0, None
|
|
168
|
+
return 1, _encode_row(self._rows.pop(0))
|
|
169
|
+
|
|
170
|
+
def selectClose(self):
|
|
171
|
+
self._rows = []
|
|
172
|
+
return 1
|
|
173
|
+
|
|
174
|
+
def column(self, aTableName):
|
|
175
|
+
return self.columns(aTableName)
|
|
176
|
+
|
|
177
|
+
def statistics(self, aTableName, aUser="SYS"):
|
|
178
|
+
sql = (
|
|
179
|
+
"select name, type, colcount from m$tables "
|
|
180
|
+
"where user_id = (select user_id from m$users where name = '%s') and name = '%s'"
|
|
181
|
+
% (aUser.upper(), aTableName.upper())
|
|
182
|
+
)
|
|
183
|
+
return self.select(sql)
|
|
184
|
+
|
|
185
|
+
def schema(self, aSql):
|
|
186
|
+
return self.execute(aSql)
|
|
187
|
+
|
|
188
|
+
def tables(self):
|
|
189
|
+
sql = (
|
|
190
|
+
"select t.NAME as name, u.NAME as username, t.COLCOUNT as colcount "
|
|
191
|
+
"from m$sys_tables t, m$sys_users u where u.user_id=t.user_id"
|
|
192
|
+
)
|
|
193
|
+
return self.select(sql)
|
|
194
|
+
|
|
195
|
+
def columns(self, aTableName):
|
|
196
|
+
if aTableName is None:
|
|
197
|
+
sql = (
|
|
198
|
+
"select c.name name, c.type type, c.length length "
|
|
199
|
+
"from m$sys_columns c, m$sys_tables t where c.table_id = t.id "
|
|
200
|
+
"and c.id not in(0,65534) order by c.id"
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
name = aTableName.upper()
|
|
204
|
+
if name.startswith("M$"):
|
|
205
|
+
sql = (
|
|
206
|
+
"select name,type,length from m$columns where table_id = "
|
|
207
|
+
"(select id from m$tables where name = '%s') and id not in(0,65534) order by id" % name
|
|
208
|
+
)
|
|
209
|
+
elif name.startswith("V$"):
|
|
210
|
+
sql = (
|
|
211
|
+
"select name,type,length from v$columns where table_id = "
|
|
212
|
+
"(select id from v$tables where name = '%s') and id not in(0,65534) order by id" % name
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
sql = (
|
|
216
|
+
"select name,type,length from m$sys_columns where table_id = "
|
|
217
|
+
"(select id from m$sys_tables where name = '%s') and name not in('_RID','_ARRIVAL_TIME') order by id"
|
|
218
|
+
% name
|
|
219
|
+
)
|
|
220
|
+
return self.select(sql)
|
|
221
|
+
|
|
222
|
+
def appendOpen(self, aTableName, aTypes=None):
|
|
223
|
+
try:
|
|
224
|
+
self._append_session = self._ensure_conn()._client.append_open(aTableName)
|
|
225
|
+
self._append_types = [int(v) for v in aTypes] if aTypes else None
|
|
226
|
+
return 1
|
|
227
|
+
except Exception as exc:
|
|
228
|
+
return self._set_error("Append open error.", str(exc))
|
|
229
|
+
|
|
230
|
+
def appendClose(self):
|
|
231
|
+
if self._append_session is None:
|
|
232
|
+
return 0
|
|
233
|
+
try:
|
|
234
|
+
self._ensure_conn()._client.append_close(self._append_session)
|
|
235
|
+
self._append_session = None
|
|
236
|
+
self._append_types = None
|
|
237
|
+
return self._set_ok("Append success")
|
|
238
|
+
except Exception as exc:
|
|
239
|
+
return self._set_error("Append close error.", str(exc))
|
|
240
|
+
|
|
241
|
+
def appendData(
|
|
242
|
+
self,
|
|
243
|
+
aTableName,
|
|
244
|
+
aTypes,
|
|
245
|
+
aValues=None,
|
|
246
|
+
aFormat="YYYY-MM-DD HH24:MI:SS",
|
|
247
|
+
on_ack: Optional[Callable[[dict], None]] = None,
|
|
248
|
+
):
|
|
249
|
+
_ = aTableName, aFormat
|
|
250
|
+
if self._append_session is None:
|
|
251
|
+
return 0
|
|
252
|
+
try:
|
|
253
|
+
types, rows = self._normalize_append_payload(aTypes, aValues)
|
|
254
|
+
self._ensure_conn()._client.append_data(
|
|
255
|
+
self._append_session,
|
|
256
|
+
rows,
|
|
257
|
+
types=types or self._append_types,
|
|
258
|
+
on_ack=on_ack,
|
|
259
|
+
)
|
|
260
|
+
return 1
|
|
261
|
+
except Exception as exc:
|
|
262
|
+
return self._set_error("Append data error.", str(exc))
|
|
263
|
+
|
|
264
|
+
def appendDataByTime(
|
|
265
|
+
self,
|
|
266
|
+
aTableName,
|
|
267
|
+
aTypes,
|
|
268
|
+
aValues,
|
|
269
|
+
aFormat="YYYY-MM-DD HH24:MI:SS",
|
|
270
|
+
aTimes=None,
|
|
271
|
+
on_ack: Optional[Callable[[dict], None]] = None,
|
|
272
|
+
):
|
|
273
|
+
_ = aTableName, aFormat
|
|
274
|
+
if self._append_session is None:
|
|
275
|
+
return 0
|
|
276
|
+
try:
|
|
277
|
+
types, rows = self._normalize_append_payload(aTypes, aValues)
|
|
278
|
+
self._ensure_conn()._client.append_data(
|
|
279
|
+
self._append_session,
|
|
280
|
+
rows,
|
|
281
|
+
types=types or self._append_types,
|
|
282
|
+
times=aTimes,
|
|
283
|
+
on_ack=on_ack,
|
|
284
|
+
)
|
|
285
|
+
return 1
|
|
286
|
+
except Exception as exc:
|
|
287
|
+
return self._set_error("Append data error.", str(exc))
|
|
288
|
+
|
|
289
|
+
def appendFlush(self):
|
|
290
|
+
return 1 if self._append_session is not None else 0
|
|
291
|
+
|
|
292
|
+
def append(self, aTableName, aTypes, aValues=None, aFormat="YYYY-MM-DD HH24:MI:SS"):
|
|
293
|
+
_ = aFormat
|
|
294
|
+
types, rows = self._normalize_append_payload(aTypes, aValues)
|
|
295
|
+
if not rows:
|
|
296
|
+
return 0
|
|
297
|
+
try:
|
|
298
|
+
count = self._ensure_conn()._client.append(aTableName, rows, types=types)
|
|
299
|
+
self._last_result = json.dumps({"EXECUTE RESULT": "Append success"}, separators=(",", ":"))
|
|
300
|
+
return 1
|
|
301
|
+
except Exception as exc:
|
|
302
|
+
return self._set_error("Append error.", str(exc))
|
|
303
|
+
|
|
304
|
+
def appendByTime(self, aTableName, aTypes, aValues=None, aFormat="YYYY-MM-DD HH24:MI:SS", aTimes=None):
|
|
305
|
+
_ = aFormat
|
|
306
|
+
if aValues is None and isinstance(aTypes, (list, tuple)) and aTimes is not None and aTypes and isinstance(aTypes[0], (list, tuple)):
|
|
307
|
+
aValues = aTypes
|
|
308
|
+
aTypes = None
|
|
309
|
+
try:
|
|
310
|
+
types, rows = self._normalize_append_payload(aTypes, aValues)
|
|
311
|
+
count = self._ensure_conn()._client.append(aTableName, rows, types=types, times=aTimes)
|
|
312
|
+
self._last_result = json.dumps({"EXECUTE RESULT": "Append success"}, separators=(",", ":"))
|
|
313
|
+
return 1
|
|
314
|
+
except Exception as exc:
|
|
315
|
+
return self._set_error("Append error.", str(exc))
|
|
316
|
+
|
|
317
|
+
def result(self):
|
|
318
|
+
return self._last_result
|
machbaseAPI/marshal.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Dict, Iterable, Iterator, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
from .constants import CMI_BINARY_TYPE, CMI_PACKET_MAX_BODY, CMI_ROWS_TYPE, CMI_STRING_TYPE
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
MarshalPrimitive = Union[int, float, bool, bytes, bytearray, str, None]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class MarshalUnit:
|
|
14
|
+
id: int
|
|
15
|
+
type: int
|
|
16
|
+
length: int
|
|
17
|
+
data: bytes
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _align8(length: int) -> int:
|
|
21
|
+
return (length + 7) & ~7
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _read_u32le(payload: bytes) -> int:
|
|
25
|
+
if len(payload) < 4:
|
|
26
|
+
return 0
|
|
27
|
+
return int.from_bytes(payload[:4], "big")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _read_fixed_size(type_id: int) -> Optional[int]:
|
|
31
|
+
if type_id in {
|
|
32
|
+
CMI_STRING_TYPE,
|
|
33
|
+
CMI_BINARY_TYPE,
|
|
34
|
+
0x0000000C,
|
|
35
|
+
0x000000F1,
|
|
36
|
+
0x000000F2,
|
|
37
|
+
0x0000000D,
|
|
38
|
+
}:
|
|
39
|
+
return None
|
|
40
|
+
if type_id in {0x00000004, 0x00000005}:
|
|
41
|
+
return 1
|
|
42
|
+
if type_id in {0x00000006, 0x00000007}: # 16-bit
|
|
43
|
+
return 2
|
|
44
|
+
if type_id in {0x00000008, 0x00000009, 0x00000018, 0x00000019}: # 32-bit
|
|
45
|
+
return 4
|
|
46
|
+
return 8
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def decode_scalar(unit: MarshalUnit) -> MarshalPrimitive:
|
|
50
|
+
if unit.type in {
|
|
51
|
+
CMI_STRING_TYPE,
|
|
52
|
+
0x0000000C,
|
|
53
|
+
0x000000F1,
|
|
54
|
+
0x000000F2,
|
|
55
|
+
0x0000000D,
|
|
56
|
+
}:
|
|
57
|
+
try:
|
|
58
|
+
return unit.data.decode("utf-8")
|
|
59
|
+
except Exception:
|
|
60
|
+
return unit.data.decode("utf-8", errors="replace")
|
|
61
|
+
if unit.type == 0x00000004:
|
|
62
|
+
return unit.data[0] if unit.data else 0
|
|
63
|
+
if unit.type == 0x00000005:
|
|
64
|
+
return unit.data[0] if unit.data else 0
|
|
65
|
+
if unit.type == 0x00000006:
|
|
66
|
+
return int.from_bytes(unit.data[:2], "little", signed=True) if len(unit.data) >= 2 else 0
|
|
67
|
+
if unit.type == 0x00000007:
|
|
68
|
+
return int.from_bytes(unit.data[:2], "little", signed=False) if len(unit.data) >= 2 else 0
|
|
69
|
+
if unit.type == 0x00000008:
|
|
70
|
+
return int.from_bytes(unit.data[:4], "little", signed=True) if len(unit.data) >= 4 else 0
|
|
71
|
+
if unit.type == 0x00000009:
|
|
72
|
+
return int.from_bytes(unit.data[:4], "little", signed=False) if len(unit.data) >= 4 else 0
|
|
73
|
+
if unit.type == 0x0000000A:
|
|
74
|
+
return int.from_bytes(unit.data[:8], "little", signed=True) if len(unit.data) >= 8 else 0
|
|
75
|
+
if unit.type == 0x0000000B:
|
|
76
|
+
return int.from_bytes(unit.data[:8], "little", signed=False) if len(unit.data) >= 8 else 0
|
|
77
|
+
return unit.data
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class MarshalWriter:
|
|
81
|
+
"""Build marshal units and split them into protocol packets."""
|
|
82
|
+
|
|
83
|
+
HEADER_SIZE = 16
|
|
84
|
+
|
|
85
|
+
def __init__(self, protocol_id: int, stmt_id: int, adds: int = 0):
|
|
86
|
+
self.protocol_id = protocol_id & 0xFF
|
|
87
|
+
self.stmt_id = stmt_id & 0xFFFFFFFF
|
|
88
|
+
self.adds = adds & 0xFFFF
|
|
89
|
+
self._units: List[bytes] = []
|
|
90
|
+
self._current_length = 0
|
|
91
|
+
self._bodies: List[bytes] = []
|
|
92
|
+
|
|
93
|
+
def add_string(self, unit_id: int, value: str) -> None:
|
|
94
|
+
payload = value.encode("utf-8")
|
|
95
|
+
self._add_variable(unit_id, CMI_STRING_TYPE, payload)
|
|
96
|
+
|
|
97
|
+
def add_binary(self, unit_id: int, value: Union[bytes, bytearray]) -> None:
|
|
98
|
+
self._add_variable(unit_id, CMI_BINARY_TYPE, bytes(value))
|
|
99
|
+
|
|
100
|
+
def add_uint32(self, unit_id: int, value: int) -> None:
|
|
101
|
+
payload = value.to_bytes(4, "little", signed=False)
|
|
102
|
+
self._add_fixed(unit_id, 0x00000009, payload)
|
|
103
|
+
|
|
104
|
+
def add_sint32(self, unit_id: int, value: int) -> None:
|
|
105
|
+
payload = value.to_bytes(4, "little", signed=True)
|
|
106
|
+
self._add_fixed(unit_id, 0x00000008, payload)
|
|
107
|
+
|
|
108
|
+
def add_uint64(self, unit_id: int, value: int) -> None:
|
|
109
|
+
payload = value.to_bytes(8, "little", signed=False)
|
|
110
|
+
self._add_fixed(unit_id, 0x0000000B, payload)
|
|
111
|
+
|
|
112
|
+
def add_sint64(self, unit_id: int, value: int) -> None:
|
|
113
|
+
payload = value.to_bytes(8, "little", signed=True)
|
|
114
|
+
self._add_fixed(unit_id, 0x0000000A, payload)
|
|
115
|
+
|
|
116
|
+
def add_boolean(self, unit_id: int, value: bool) -> None:
|
|
117
|
+
payload = b"\x01" if value else b"\x00"
|
|
118
|
+
self._add_fixed(unit_id, 0x00000026, payload)
|
|
119
|
+
|
|
120
|
+
def finalize(self) -> List[bytes]:
|
|
121
|
+
self._flush_current()
|
|
122
|
+
if not self._bodies:
|
|
123
|
+
self._bodies = [b""]
|
|
124
|
+
count = len(self._bodies)
|
|
125
|
+
result = []
|
|
126
|
+
for index, body in enumerate(self._bodies):
|
|
127
|
+
if count == 1:
|
|
128
|
+
flag = 0
|
|
129
|
+
elif index == 0:
|
|
130
|
+
flag = 1
|
|
131
|
+
elif index + 1 == count:
|
|
132
|
+
flag = 3
|
|
133
|
+
else:
|
|
134
|
+
flag = 2
|
|
135
|
+
result.append(self._build_packet(body, flag))
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
def _add_variable(self, unit_id: int, type_id: int, payload: bytes) -> None:
|
|
139
|
+
length = _align8(len(payload))
|
|
140
|
+
unit = bytearray(self.HEADER_SIZE + length)
|
|
141
|
+
unit[0:4] = int.to_bytes(unit_id & 0xFFFFFFFF, 4, "little")
|
|
142
|
+
unit[4:8] = int.to_bytes(type_id & 0xFFFFFFFF, 4, "little")
|
|
143
|
+
unit[8:16] = int.to_bytes(len(payload), 8, "little")
|
|
144
|
+
unit[16 : 16 + len(payload)] = payload
|
|
145
|
+
self._enqueue(bytes(unit))
|
|
146
|
+
|
|
147
|
+
def _add_fixed(self, unit_id: int, type_id: int, payload: bytes) -> None:
|
|
148
|
+
unit = bytearray(self.HEADER_SIZE)
|
|
149
|
+
unit[0:4] = int.to_bytes(unit_id & 0xFFFFFFFF, 4, "little")
|
|
150
|
+
unit[4:8] = int.to_bytes(type_id & 0xFFFFFFFF, 4, "little")
|
|
151
|
+
fixed = bytes(payload)
|
|
152
|
+
unit[8 : 8 + len(fixed)] = fixed
|
|
153
|
+
self._enqueue(bytes(unit))
|
|
154
|
+
|
|
155
|
+
def _enqueue(self, unit: bytes) -> None:
|
|
156
|
+
if len(unit) > CMI_PACKET_MAX_BODY:
|
|
157
|
+
self._flush_current()
|
|
158
|
+
self._bodies.append(unit)
|
|
159
|
+
return
|
|
160
|
+
if self._current_length + len(unit) > CMI_PACKET_MAX_BODY:
|
|
161
|
+
self._flush_current()
|
|
162
|
+
self._units.append(unit)
|
|
163
|
+
self._current_length += len(unit)
|
|
164
|
+
|
|
165
|
+
def _flush_current(self) -> None:
|
|
166
|
+
if not self._units:
|
|
167
|
+
self._current_length = 0
|
|
168
|
+
return
|
|
169
|
+
body = b"".join(self._units)
|
|
170
|
+
self._bodies.append(body)
|
|
171
|
+
self._units.clear()
|
|
172
|
+
self._current_length = 0
|
|
173
|
+
|
|
174
|
+
def _build_packet(self, body: bytes, flag: int) -> bytes:
|
|
175
|
+
length_with_flag = (flag << 30) | (len(body) & 0x3FFFFFFF)
|
|
176
|
+
header = bytearray(16)
|
|
177
|
+
header[0] = 0
|
|
178
|
+
header[1:3] = self.adds.to_bytes(2, "big")
|
|
179
|
+
header[3] = self.protocol_id & 0xFF
|
|
180
|
+
header[4:8] = length_with_flag.to_bytes(4, "big")
|
|
181
|
+
header[8:12] = self.stmt_id.to_bytes(4, "big")
|
|
182
|
+
return bytes(header) + body
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class MarshalReader:
|
|
186
|
+
def __init__(self, payload: bytes):
|
|
187
|
+
self._payload = payload
|
|
188
|
+
|
|
189
|
+
def __iter__(self) -> Iterator[MarshalUnit]:
|
|
190
|
+
return self._iter_units()
|
|
191
|
+
|
|
192
|
+
def _iter_units(self) -> Iterator[MarshalUnit]:
|
|
193
|
+
offset = 0
|
|
194
|
+
while offset < len(self._payload):
|
|
195
|
+
if offset + 16 > len(self._payload):
|
|
196
|
+
raise ValueError("Incomplete marshal unit header")
|
|
197
|
+
unit_id = int.from_bytes(self._payload[offset : offset + 4], "little")
|
|
198
|
+
type_id = int.from_bytes(self._payload[offset + 4 : offset + 8], "little")
|
|
199
|
+
raw_length = int.from_bytes(self._payload[offset + 8 : offset + 16], "little")
|
|
200
|
+
body_offset = offset + 8
|
|
201
|
+
|
|
202
|
+
if type_id in {
|
|
203
|
+
CMI_STRING_TYPE,
|
|
204
|
+
CMI_BINARY_TYPE,
|
|
205
|
+
0x0000000C,
|
|
206
|
+
0x000000F1,
|
|
207
|
+
0x000000F2,
|
|
208
|
+
CMI_ROWS_TYPE,
|
|
209
|
+
}:
|
|
210
|
+
aligned = _align8(raw_length)
|
|
211
|
+
body_offset = offset + 16
|
|
212
|
+
if body_offset + aligned > len(self._payload):
|
|
213
|
+
raise ValueError("Incomplete variable marshal payload")
|
|
214
|
+
data = self._payload[body_offset : body_offset + raw_length]
|
|
215
|
+
yield MarshalUnit(unit_id, type_id, raw_length, data)
|
|
216
|
+
offset = body_offset + aligned
|
|
217
|
+
else:
|
|
218
|
+
size = _read_fixed_size(type_id)
|
|
219
|
+
if size is None:
|
|
220
|
+
size = 8
|
|
221
|
+
if body_offset + size > offset + 16:
|
|
222
|
+
raise ValueError("Incomplete fixed marshal payload")
|
|
223
|
+
data = self._payload[body_offset : body_offset + size]
|
|
224
|
+
yield MarshalUnit(unit_id, type_id, size, data)
|
|
225
|
+
offset = offset + 16
|
machbaseAPI/packet.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Packet utilities for Machbase binary protocol."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import socket
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Deque, List, Optional
|
|
9
|
+
|
|
10
|
+
from collections import deque
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Packet:
|
|
15
|
+
protocol: int
|
|
16
|
+
flag: int
|
|
17
|
+
adds: int
|
|
18
|
+
stmt_id: int
|
|
19
|
+
body: bytes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PacketReader:
|
|
23
|
+
HEADER_SIZE = 16
|
|
24
|
+
|
|
25
|
+
def __init__(self, sock: socket.socket):
|
|
26
|
+
self._socket = sock
|
|
27
|
+
self._buffer = b""
|
|
28
|
+
self._queue: Deque[Packet] = deque()
|
|
29
|
+
self._closed = False
|
|
30
|
+
self._error: Optional[BaseException] = None
|
|
31
|
+
|
|
32
|
+
def next(self, timeout_ms: Optional[int] = None) -> Packet:
|
|
33
|
+
if self._error:
|
|
34
|
+
raise self._error
|
|
35
|
+
if self._queue:
|
|
36
|
+
return self._queue.popleft()
|
|
37
|
+
|
|
38
|
+
deadline = None
|
|
39
|
+
if timeout_ms is not None:
|
|
40
|
+
deadline = time.monotonic() + timeout_ms / 1000
|
|
41
|
+
|
|
42
|
+
while True:
|
|
43
|
+
if self._queue:
|
|
44
|
+
return self._queue.popleft()
|
|
45
|
+
if self._closed:
|
|
46
|
+
raise ConnectionError("socket closed")
|
|
47
|
+
now = time.monotonic()
|
|
48
|
+
if deadline is not None and now >= deadline:
|
|
49
|
+
raise TimeoutError("timed out waiting packet")
|
|
50
|
+
|
|
51
|
+
timeout = None
|
|
52
|
+
if deadline is not None:
|
|
53
|
+
timeout = max(0.0, deadline - now)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
self._socket.settimeout(timeout)
|
|
57
|
+
chunk = self._socket.recv(65536)
|
|
58
|
+
if not chunk:
|
|
59
|
+
self._closed = True
|
|
60
|
+
if not self._queue:
|
|
61
|
+
raise ConnectionError("connection closed by server")
|
|
62
|
+
return self._queue.popleft()
|
|
63
|
+
self._buffer += chunk
|
|
64
|
+
self._parse_buffer()
|
|
65
|
+
except TimeoutError as exc:
|
|
66
|
+
raise exc
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
self._error = exc
|
|
69
|
+
raise
|
|
70
|
+
|
|
71
|
+
def _parse_buffer(self) -> None:
|
|
72
|
+
while len(self._buffer) >= self.HEADER_SIZE:
|
|
73
|
+
length_field = int.from_bytes(self._buffer[4:8], "big")
|
|
74
|
+
flag = (length_field >> 30) & 0x03
|
|
75
|
+
body_length = length_field & 0x3FFFFFFF
|
|
76
|
+
total_length = self.HEADER_SIZE + body_length
|
|
77
|
+
if len(self._buffer) < total_length:
|
|
78
|
+
return
|
|
79
|
+
header = self._buffer[:self.HEADER_SIZE]
|
|
80
|
+
body = self._buffer[self.HEADER_SIZE : total_length]
|
|
81
|
+
self._buffer = self._buffer[total_length:]
|
|
82
|
+
|
|
83
|
+
protocol = header[3]
|
|
84
|
+
adds = int.from_bytes(header[1:3], "big")
|
|
85
|
+
stmt_id = int.from_bytes(header[8:12], "big")
|
|
86
|
+
self._queue.append(Packet(protocol=protocol, flag=flag, adds=adds, stmt_id=stmt_id, body=body))
|
|
87
|
+
|