singlestoredb 0.4.0__py3-none-any.whl → 1.0.4__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.
Potentially problematic release.
This version of singlestoredb might be problematic. Click here for more details.
- singlestoredb/__init__.py +33 -1
- singlestoredb/alchemy/__init__.py +90 -0
- singlestoredb/auth.py +5 -1
- singlestoredb/config.py +116 -14
- singlestoredb/connection.py +483 -516
- singlestoredb/converters.py +238 -135
- singlestoredb/exceptions.py +30 -2
- singlestoredb/functions/__init__.py +1 -0
- singlestoredb/functions/decorator.py +142 -0
- singlestoredb/functions/dtypes.py +1639 -0
- singlestoredb/functions/ext/__init__.py +2 -0
- singlestoredb/functions/ext/arrow.py +375 -0
- singlestoredb/functions/ext/asgi.py +661 -0
- singlestoredb/functions/ext/json.py +427 -0
- singlestoredb/functions/ext/mmap.py +306 -0
- singlestoredb/functions/ext/rowdat_1.py +744 -0
- singlestoredb/functions/signature.py +673 -0
- singlestoredb/fusion/__init__.py +11 -0
- singlestoredb/fusion/graphql.py +213 -0
- singlestoredb/fusion/handler.py +621 -0
- singlestoredb/fusion/handlers/stage.py +257 -0
- singlestoredb/fusion/handlers/utils.py +162 -0
- singlestoredb/fusion/handlers/workspace.py +412 -0
- singlestoredb/fusion/registry.py +164 -0
- singlestoredb/fusion/result.py +399 -0
- singlestoredb/http/__init__.py +27 -0
- singlestoredb/{http.py → http/connection.py} +555 -154
- singlestoredb/management/__init__.py +3 -0
- singlestoredb/management/billing_usage.py +148 -0
- singlestoredb/management/cluster.py +14 -6
- singlestoredb/management/manager.py +100 -38
- singlestoredb/management/organization.py +188 -0
- singlestoredb/management/region.py +5 -5
- singlestoredb/management/utils.py +281 -2
- singlestoredb/management/workspace.py +1344 -49
- singlestoredb/{clients/pymysqlsv → mysql}/__init__.py +16 -21
- singlestoredb/{clients/pymysqlsv → mysql}/_auth.py +39 -8
- singlestoredb/{clients/pymysqlsv → mysql}/charset.py +26 -23
- singlestoredb/{clients/pymysqlsv/connections.py → mysql/connection.py} +532 -165
- singlestoredb/{clients/pymysqlsv → mysql}/constants/CLIENT.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/COMMAND.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/CR.py +0 -2
- singlestoredb/{clients/pymysqlsv → mysql}/constants/ER.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/FIELD_TYPE.py +1 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/FLAG.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/SERVER_STATUS.py +0 -1
- singlestoredb/mysql/converters.py +271 -0
- singlestoredb/{clients/pymysqlsv → mysql}/cursors.py +228 -112
- singlestoredb/mysql/err.py +92 -0
- singlestoredb/{clients/pymysqlsv → mysql}/optionfile.py +5 -4
- singlestoredb/{clients/pymysqlsv → mysql}/protocol.py +49 -20
- singlestoredb/mysql/tests/__init__.py +19 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/base.py +32 -12
- singlestoredb/mysql/tests/conftest.py +37 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_DictCursor.py +11 -7
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_SSCursor.py +17 -12
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_basic.py +32 -24
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_connection.py +130 -119
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_converters.py +9 -7
- singlestoredb/mysql/tests/test_cursor.py +141 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_err.py +3 -2
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_issues.py +35 -27
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_load_local.py +13 -11
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_nextset.py +7 -3
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_optionfile.py +2 -1
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/__init__.py +1 -1
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/capabilities.py +19 -17
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/dbapi20.py +31 -22
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +3 -4
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +24 -20
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +4 -4
- singlestoredb/{clients/pymysqlsv → mysql}/times.py +3 -4
- singlestoredb/pytest.py +283 -0
- singlestoredb/tests/empty.sql +0 -0
- singlestoredb/tests/ext_funcs/__init__.py +385 -0
- singlestoredb/tests/test.sql +210 -0
- singlestoredb/tests/test2.sql +1 -0
- singlestoredb/tests/test_basics.py +482 -115
- singlestoredb/tests/test_config.py +13 -13
- singlestoredb/tests/test_connection.py +241 -305
- singlestoredb/tests/test_dbapi.py +27 -0
- singlestoredb/tests/test_ext_func.py +1193 -0
- singlestoredb/tests/test_ext_func_data.py +1101 -0
- singlestoredb/tests/test_fusion.py +465 -0
- singlestoredb/tests/test_http.py +32 -26
- singlestoredb/tests/test_management.py +588 -8
- singlestoredb/tests/test_plugin.py +33 -0
- singlestoredb/tests/test_results.py +11 -12
- singlestoredb/tests/test_udf.py +687 -0
- singlestoredb/tests/utils.py +3 -2
- singlestoredb/utils/config.py +58 -0
- singlestoredb/utils/debug.py +13 -0
- singlestoredb/utils/mogrify.py +151 -0
- singlestoredb/utils/results.py +4 -1
- singlestoredb-1.0.4.dist-info/METADATA +139 -0
- singlestoredb-1.0.4.dist-info/RECORD +112 -0
- {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/WHEEL +1 -1
- singlestoredb-1.0.4.dist-info/entry_points.txt +2 -0
- singlestoredb/clients/pymysqlsv/converters.py +0 -365
- singlestoredb/clients/pymysqlsv/err.py +0 -144
- singlestoredb/clients/pymysqlsv/tests/__init__.py +0 -19
- singlestoredb/clients/pymysqlsv/tests/test_cursor.py +0 -133
- singlestoredb/clients/pymysqlsv/tests/thirdparty/test_MySQLdb/__init__.py +0 -9
- singlestoredb/drivers/__init__.py +0 -45
- singlestoredb/drivers/base.py +0 -198
- singlestoredb/drivers/cymysql.py +0 -38
- singlestoredb/drivers/http.py +0 -47
- singlestoredb/drivers/mariadb.py +0 -40
- singlestoredb/drivers/mysqlconnector.py +0 -49
- singlestoredb/drivers/mysqldb.py +0 -60
- singlestoredb/drivers/pymysql.py +0 -37
- singlestoredb/drivers/pymysqlsv.py +0 -35
- singlestoredb/drivers/pyodbc.py +0 -65
- singlestoredb-0.4.0.dist-info/METADATA +0 -111
- singlestoredb-0.4.0.dist-info/RECORD +0 -86
- /singlestoredb/{clients → fusion/handlers}/__init__.py +0 -0
- /singlestoredb/{clients/pymysqlsv → mysql}/constants/__init__.py +0 -0
- {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/LICENSE +0 -0
- {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
"""SingleStoreDB HTTP API interface."""
|
|
3
|
+
import datetime
|
|
4
|
+
import decimal
|
|
3
5
|
import functools
|
|
4
6
|
import json
|
|
7
|
+
import math
|
|
8
|
+
import os
|
|
5
9
|
import re
|
|
10
|
+
import time
|
|
6
11
|
from base64 import b64decode
|
|
7
12
|
from typing import Any
|
|
8
13
|
from typing import Callable
|
|
@@ -10,41 +15,62 @@ from typing import Dict
|
|
|
10
15
|
from typing import Iterable
|
|
11
16
|
from typing import List
|
|
12
17
|
from typing import Optional
|
|
18
|
+
from typing import Sequence
|
|
13
19
|
from typing import Tuple
|
|
14
20
|
from typing import Union
|
|
15
21
|
from urllib.parse import urljoin
|
|
22
|
+
from urllib.parse import urlparse
|
|
16
23
|
|
|
17
24
|
import requests
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
try:
|
|
27
|
+
import numpy as np
|
|
28
|
+
has_numpy = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
has_numpy = False
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
import pygeos
|
|
34
|
+
has_pygeos = True
|
|
35
|
+
except ImportError:
|
|
36
|
+
has_pygeos = False
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
import shapely.geometry
|
|
40
|
+
import shapely.wkt
|
|
41
|
+
has_shapely = True
|
|
42
|
+
except ImportError:
|
|
43
|
+
has_shapely = False
|
|
44
|
+
|
|
45
|
+
from .. import connection
|
|
46
|
+
from .. import fusion
|
|
47
|
+
from .. import types
|
|
48
|
+
from ..config import get_option
|
|
49
|
+
from ..converters import converters
|
|
50
|
+
from ..exceptions import DatabaseError # noqa: F401
|
|
51
|
+
from ..exceptions import DataError
|
|
52
|
+
from ..exceptions import Error # noqa: F401
|
|
53
|
+
from ..exceptions import IntegrityError
|
|
54
|
+
from ..exceptions import InterfaceError
|
|
55
|
+
from ..exceptions import InternalError
|
|
56
|
+
from ..exceptions import NotSupportedError
|
|
57
|
+
from ..exceptions import OperationalError
|
|
58
|
+
from ..exceptions import ProgrammingError
|
|
59
|
+
from ..exceptions import Warning # noqa: F401
|
|
60
|
+
from ..utils.convert_rows import convert_rows
|
|
61
|
+
from ..utils.debug import log_query
|
|
62
|
+
from ..utils.mogrify import mogrify
|
|
63
|
+
from ..utils.results import Description
|
|
64
|
+
from ..utils.results import format_results
|
|
65
|
+
from ..utils.results import Result
|
|
34
66
|
|
|
35
67
|
|
|
36
68
|
# DB-API settings
|
|
37
69
|
apilevel = '2.0'
|
|
38
|
-
paramstyle = '
|
|
70
|
+
paramstyle = 'named'
|
|
39
71
|
threadsafety = 1
|
|
40
72
|
|
|
41
73
|
|
|
42
|
-
Description = Tuple[
|
|
43
|
-
str, int, Optional[int], Optional[int], Optional[int],
|
|
44
|
-
Optional[int], bool,
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
|
|
48
74
|
_interface_errors = set([
|
|
49
75
|
0,
|
|
50
76
|
2013, # CR_SERVER_LOST
|
|
@@ -145,6 +171,125 @@ def b64decode_converter(
|
|
|
145
171
|
return converter(b64decode(x))
|
|
146
172
|
|
|
147
173
|
|
|
174
|
+
def encode_timedelta(obj: datetime.timedelta) -> str:
|
|
175
|
+
"""Encode timedelta as str."""
|
|
176
|
+
seconds = int(obj.seconds) % 60
|
|
177
|
+
minutes = int(obj.seconds // 60) % 60
|
|
178
|
+
hours = int(obj.seconds // 3600) % 24 + int(obj.days) * 24
|
|
179
|
+
if obj.microseconds:
|
|
180
|
+
fmt = '{0:02d}:{1:02d}:{2:02d}.{3:06d}'
|
|
181
|
+
else:
|
|
182
|
+
fmt = '{0:02d}:{1:02d}:{2:02d}'
|
|
183
|
+
return fmt.format(hours, minutes, seconds, obj.microseconds)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def encode_time(obj: datetime.time) -> str:
|
|
187
|
+
"""Encode time as str."""
|
|
188
|
+
if obj.microsecond:
|
|
189
|
+
fmt = '{0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}'
|
|
190
|
+
else:
|
|
191
|
+
fmt = '{0.hour:02}:{0.minute:02}:{0.second:02}'
|
|
192
|
+
return fmt.format(obj)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def encode_datetime(obj: datetime.datetime) -> str:
|
|
196
|
+
"""Encode datetime as str."""
|
|
197
|
+
if obj.microsecond:
|
|
198
|
+
fmt = '{0.year:04}-{0.month:02}-{0.day:02} ' \
|
|
199
|
+
'{0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}'
|
|
200
|
+
else:
|
|
201
|
+
fmt = '{0.year:04}-{0.month:02}-{0.day:02} ' \
|
|
202
|
+
'{0.hour:02}:{0.minute:02}:{0.second:02}'
|
|
203
|
+
return fmt.format(obj)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def encode_date(obj: datetime.date) -> str:
|
|
207
|
+
"""Encode date as str."""
|
|
208
|
+
fmt = '{0.year:04}-{0.month:02}-{0.day:02}'
|
|
209
|
+
return fmt.format(obj)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def encode_struct_time(obj: time.struct_time) -> str:
|
|
213
|
+
"""Encode time struct to str."""
|
|
214
|
+
return encode_datetime(datetime.datetime(*obj[:6]))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def encode_decimal(o: decimal.Decimal) -> str:
|
|
218
|
+
"""Encode decimal to str."""
|
|
219
|
+
return format(o, 'f')
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# Most argument encoding is done by the JSON encoder, but these
|
|
223
|
+
# are exceptions to the rule.
|
|
224
|
+
encoders = {
|
|
225
|
+
datetime.datetime: encode_datetime,
|
|
226
|
+
datetime.date: encode_date,
|
|
227
|
+
datetime.time: encode_time,
|
|
228
|
+
datetime.timedelta: encode_timedelta,
|
|
229
|
+
time.struct_time: encode_struct_time,
|
|
230
|
+
decimal.Decimal: encode_decimal,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if has_shapely:
|
|
235
|
+
encoders[shapely.geometry.Point] = shapely.wkt.dumps
|
|
236
|
+
encoders[shapely.geometry.Polygon] = shapely.wkt.dumps
|
|
237
|
+
encoders[shapely.geometry.LineString] = shapely.wkt.dumps
|
|
238
|
+
|
|
239
|
+
if has_numpy:
|
|
240
|
+
|
|
241
|
+
def encode_ndarray(obj: np.ndarray) -> bytes: # type: ignore
|
|
242
|
+
"""Encode an ndarray as bytes."""
|
|
243
|
+
return obj.tobytes()
|
|
244
|
+
|
|
245
|
+
encoders[np.ndarray] = encode_ndarray
|
|
246
|
+
|
|
247
|
+
if has_pygeos:
|
|
248
|
+
encoders[pygeos.Geometry] = pygeos.io.to_wkt
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def convert_special_type(
|
|
252
|
+
arg: Any,
|
|
253
|
+
nan_as_null: bool = False,
|
|
254
|
+
inf_as_null: bool = False,
|
|
255
|
+
) -> Any:
|
|
256
|
+
"""Convert special data type objects."""
|
|
257
|
+
dtype = type(arg)
|
|
258
|
+
if dtype is float or \
|
|
259
|
+
(
|
|
260
|
+
has_numpy and dtype in (
|
|
261
|
+
np.float16, np.float32, np.float64,
|
|
262
|
+
getattr(np, 'float128', np.float64),
|
|
263
|
+
)
|
|
264
|
+
):
|
|
265
|
+
if nan_as_null and math.isnan(arg):
|
|
266
|
+
return None
|
|
267
|
+
if inf_as_null and math.isinf(arg):
|
|
268
|
+
return None
|
|
269
|
+
func = encoders.get(dtype, None)
|
|
270
|
+
if func is not None:
|
|
271
|
+
return func(arg) # type: ignore
|
|
272
|
+
return arg
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def convert_special_params(
|
|
276
|
+
params: Optional[Union[Sequence[Any], Dict[str, Any]]] = None,
|
|
277
|
+
nan_as_null: bool = False,
|
|
278
|
+
inf_as_null: bool = False,
|
|
279
|
+
) -> Optional[Union[Sequence[Any], Dict[str, Any]]]:
|
|
280
|
+
"""Convert parameters of special data types."""
|
|
281
|
+
if params is None:
|
|
282
|
+
return params
|
|
283
|
+
converter = functools.partial(
|
|
284
|
+
convert_special_type,
|
|
285
|
+
nan_as_null=nan_as_null,
|
|
286
|
+
inf_as_null=inf_as_null,
|
|
287
|
+
)
|
|
288
|
+
if isinstance(params, Dict):
|
|
289
|
+
return {k: converter(v) for k, v in params.items()}
|
|
290
|
+
return tuple(map(converter, params))
|
|
291
|
+
|
|
292
|
+
|
|
148
293
|
class PyMyField(object):
|
|
149
294
|
"""Field for PyMySQL compatibility."""
|
|
150
295
|
|
|
@@ -159,12 +304,13 @@ class PyMyResult(object):
|
|
|
159
304
|
|
|
160
305
|
def __init__(self) -> None:
|
|
161
306
|
self.fields: List[PyMyField] = []
|
|
307
|
+
self.unbuffered_active = False
|
|
162
308
|
|
|
163
309
|
def append(self, item: PyMyField) -> None:
|
|
164
310
|
self.fields.append(item)
|
|
165
311
|
|
|
166
312
|
|
|
167
|
-
class Cursor(
|
|
313
|
+
class Cursor(connection.Cursor):
|
|
168
314
|
"""
|
|
169
315
|
SingleStoreDB HTTP database cursor.
|
|
170
316
|
|
|
@@ -178,17 +324,20 @@ class Cursor(object):
|
|
|
178
324
|
|
|
179
325
|
"""
|
|
180
326
|
|
|
181
|
-
def __init__(self,
|
|
182
|
-
self
|
|
327
|
+
def __init__(self, conn: 'Connection'):
|
|
328
|
+
connection.Cursor.__init__(self, conn)
|
|
329
|
+
self._connection: Optional[Connection] = conn
|
|
183
330
|
self._results: List[List[Tuple[Any, ...]]] = [[]]
|
|
331
|
+
self._results_type: str = self._connection._results_type \
|
|
332
|
+
if self._connection is not None else 'tuples'
|
|
184
333
|
self._row_idx: int = -1
|
|
185
334
|
self._result_idx: int = -1
|
|
186
335
|
self._descriptions: List[List[Description]] = []
|
|
187
|
-
self.arraysize: int =
|
|
336
|
+
self.arraysize: int = get_option('results.arraysize')
|
|
188
337
|
self.rowcount: int = 0
|
|
189
|
-
self.messages: List[Tuple[int, str]] = []
|
|
190
338
|
self.lastrowid: Optional[int] = None
|
|
191
339
|
self._pymy_results: List[PyMyResult] = []
|
|
340
|
+
self._expect_results: bool = False
|
|
192
341
|
|
|
193
342
|
@property
|
|
194
343
|
def _result(self) -> Optional[PyMyResult]:
|
|
@@ -224,13 +373,13 @@ class Cursor(object):
|
|
|
224
373
|
requests.Response
|
|
225
374
|
|
|
226
375
|
"""
|
|
227
|
-
if self.
|
|
228
|
-
raise
|
|
229
|
-
return self.
|
|
376
|
+
if self._connection is None:
|
|
377
|
+
raise ProgrammingError(errno=2048, msg='Connection is closed.')
|
|
378
|
+
return self._connection._post(path, *args, **kwargs)
|
|
230
379
|
|
|
231
380
|
def callproc(
|
|
232
381
|
self, name: str,
|
|
233
|
-
params:
|
|
382
|
+
params: Optional[Sequence[Any]] = None,
|
|
234
383
|
) -> None:
|
|
235
384
|
"""
|
|
236
385
|
Call a stored procedure.
|
|
@@ -239,47 +388,140 @@ class Cursor(object):
|
|
|
239
388
|
----------
|
|
240
389
|
name : str
|
|
241
390
|
Name of the stored procedure
|
|
242
|
-
params :
|
|
391
|
+
params : sequence, optional
|
|
243
392
|
Parameters to the stored procedure
|
|
244
393
|
|
|
245
394
|
"""
|
|
246
|
-
if self.
|
|
247
|
-
raise
|
|
248
|
-
|
|
395
|
+
if self._connection is None:
|
|
396
|
+
raise ProgrammingError(errno=2048, msg='Connection is closed.')
|
|
397
|
+
|
|
398
|
+
name = connection._name_check(name)
|
|
399
|
+
|
|
400
|
+
if not params:
|
|
401
|
+
self._execute(f'CALL {name}();', is_callproc=True)
|
|
402
|
+
else:
|
|
403
|
+
keys = ', '.join(['%s' for i in range(len(params))])
|
|
404
|
+
self._execute(f'CALL {name}({keys});', params, is_callproc=True)
|
|
249
405
|
|
|
250
406
|
def close(self) -> None:
|
|
251
407
|
"""Close the cursor."""
|
|
252
|
-
|
|
253
|
-
self.connection = None
|
|
408
|
+
self._connection = None
|
|
254
409
|
|
|
255
410
|
def execute(
|
|
256
411
|
self, query: str,
|
|
257
|
-
|
|
258
|
-
) ->
|
|
412
|
+
args: Optional[Union[Sequence[Any], Dict[str, Any]]] = None,
|
|
413
|
+
) -> int:
|
|
259
414
|
"""
|
|
260
415
|
Execute a SQL statement.
|
|
261
416
|
|
|
262
417
|
Parameters
|
|
263
418
|
----------
|
|
264
|
-
|
|
419
|
+
query : str
|
|
265
420
|
The SQL statement to execute
|
|
266
|
-
|
|
421
|
+
args : iterable or dict, optional
|
|
267
422
|
Parameters to substitute into the SQL code
|
|
268
423
|
|
|
269
424
|
"""
|
|
270
|
-
|
|
271
|
-
raise InterfaceError(errno=2048, msg='Connection is closed.')
|
|
425
|
+
return self._execute(query, args)
|
|
272
426
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
427
|
+
def _validate_param_subs(
|
|
428
|
+
self, query: str,
|
|
429
|
+
args: Optional[Union[Sequence[Any], Dict[str, Any]]] = None,
|
|
430
|
+
) -> None:
|
|
431
|
+
"""Make sure the parameter substitions are valid."""
|
|
432
|
+
if args:
|
|
433
|
+
if isinstance(args, Sequence):
|
|
434
|
+
query = query % tuple(args)
|
|
435
|
+
else:
|
|
436
|
+
query = query % args
|
|
437
|
+
|
|
438
|
+
def _execute_fusion_query(
|
|
439
|
+
self,
|
|
440
|
+
oper: Union[str, bytes],
|
|
441
|
+
params: Optional[Union[Sequence[Any], Dict[str, Any]]] = None,
|
|
442
|
+
handler: Any = None,
|
|
443
|
+
) -> int:
|
|
444
|
+
oper = mogrify(oper, params)
|
|
445
|
+
|
|
446
|
+
if isinstance(oper, bytes):
|
|
447
|
+
oper = oper.decode('utf-8')
|
|
448
|
+
|
|
449
|
+
log_query(oper, None)
|
|
450
|
+
|
|
451
|
+
results_type = self._results_type
|
|
452
|
+
self._results_type = 'tuples'
|
|
453
|
+
try:
|
|
454
|
+
mgmt_res = fusion.execute(
|
|
455
|
+
self._connection, # type: ignore
|
|
456
|
+
oper,
|
|
457
|
+
handler=handler,
|
|
458
|
+
)
|
|
459
|
+
finally:
|
|
460
|
+
self._results_type = results_type
|
|
461
|
+
|
|
462
|
+
self._descriptions.append(list(mgmt_res.description))
|
|
463
|
+
self._results.append(list(mgmt_res.rows))
|
|
464
|
+
self.rowcount = len(self._results[-1])
|
|
465
|
+
|
|
466
|
+
pymy_res = PyMyResult()
|
|
467
|
+
for field in mgmt_res.fields:
|
|
468
|
+
pymy_res.append(
|
|
469
|
+
PyMyField(
|
|
470
|
+
field.name,
|
|
471
|
+
field.flags,
|
|
472
|
+
field.charsetnr,
|
|
473
|
+
),
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
self._pymy_results.append(pymy_res)
|
|
477
|
+
|
|
478
|
+
if self._results and self._results[0]:
|
|
479
|
+
self._row_idx = 0
|
|
480
|
+
self._result_idx = 0
|
|
481
|
+
|
|
482
|
+
return self.rowcount
|
|
483
|
+
|
|
484
|
+
def _execute(
|
|
485
|
+
self, oper: str,
|
|
486
|
+
params: Optional[Union[Sequence[Any], Dict[str, Any]]] = None,
|
|
487
|
+
is_callproc: bool = False,
|
|
488
|
+
) -> int:
|
|
489
|
+
self._descriptions = []
|
|
490
|
+
self._results = []
|
|
491
|
+
self._pymy_results = []
|
|
492
|
+
self._row_idx = -1
|
|
493
|
+
self._result_idx = -1
|
|
494
|
+
self.rowcount = 0
|
|
495
|
+
self._expect_results = False
|
|
496
|
+
|
|
497
|
+
if self._connection is None:
|
|
498
|
+
raise ProgrammingError(errno=2048, msg='Connection is closed.')
|
|
278
499
|
|
|
279
500
|
sql_type = 'exec'
|
|
280
|
-
if re.match(r'^\s*(select|show|call|echo)\s+',
|
|
501
|
+
if re.match(r'^\s*(select|show|call|echo|describe|with)\s+', oper, flags=re.I):
|
|
502
|
+
self._expect_results = True
|
|
281
503
|
sql_type = 'query'
|
|
282
504
|
|
|
505
|
+
self._validate_param_subs(oper, params)
|
|
506
|
+
|
|
507
|
+
handler = fusion.get_handler(oper)
|
|
508
|
+
if handler is not None:
|
|
509
|
+
return self._execute_fusion_query(oper, params, handler=handler)
|
|
510
|
+
|
|
511
|
+
oper, params = self._connection._convert_params(oper, params)
|
|
512
|
+
|
|
513
|
+
log_query(oper, params)
|
|
514
|
+
|
|
515
|
+
data: Dict[str, Any] = dict(sql=oper)
|
|
516
|
+
if params is not None:
|
|
517
|
+
data['args'] = convert_special_params(
|
|
518
|
+
params,
|
|
519
|
+
nan_as_null=self._connection.connection_params['nan_as_null'],
|
|
520
|
+
inf_as_null=self._connection.connection_params['inf_as_null'],
|
|
521
|
+
)
|
|
522
|
+
if self._connection._database:
|
|
523
|
+
data['database'] = self._connection._database
|
|
524
|
+
|
|
283
525
|
if sql_type == 'query':
|
|
284
526
|
res = self._post('query/tuples', json=data)
|
|
285
527
|
else:
|
|
@@ -298,18 +540,18 @@ class Cursor(object):
|
|
|
298
540
|
|
|
299
541
|
out = json.loads(res.text)
|
|
300
542
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
543
|
+
if 'error' in out:
|
|
544
|
+
raise OperationalError(
|
|
545
|
+
errno=out['error'].get('code', 0),
|
|
546
|
+
msg=out['error'].get('message', 'HTTP Error'),
|
|
547
|
+
)
|
|
306
548
|
|
|
307
549
|
if sql_type == 'query':
|
|
308
550
|
# description: (name, type_code, display_size, internal_size,
|
|
309
551
|
# precision, scale, null_ok, column_flags, charset)
|
|
310
552
|
|
|
311
553
|
# Remove converters for things the JSON parser already converted
|
|
312
|
-
http_converters = dict(
|
|
554
|
+
http_converters = dict(self._connection.decoders)
|
|
313
555
|
http_converters.pop(4, None)
|
|
314
556
|
http_converters.pop(5, None)
|
|
315
557
|
http_converters.pop(6, None)
|
|
@@ -323,6 +565,12 @@ class Cursor(object):
|
|
|
323
565
|
http_converters.pop(253, None)
|
|
324
566
|
http_converters.pop(254, None)
|
|
325
567
|
|
|
568
|
+
# Merge passed in converters
|
|
569
|
+
if self._connection._conv:
|
|
570
|
+
for k, v in self._connection._conv.items():
|
|
571
|
+
if isinstance(k, int):
|
|
572
|
+
http_converters[k] = v
|
|
573
|
+
|
|
326
574
|
results = out['results']
|
|
327
575
|
|
|
328
576
|
# Convert data to Python types
|
|
@@ -358,11 +606,14 @@ class Cursor(object):
|
|
|
358
606
|
prec += 1 # for decimal
|
|
359
607
|
if converter is not None:
|
|
360
608
|
convs.append((i, None, converter))
|
|
361
|
-
description.append(
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
609
|
+
description.append(
|
|
610
|
+
Description(
|
|
611
|
+
str(col['name']), type_code,
|
|
612
|
+
None, None, prec, scale,
|
|
613
|
+
col.get('nullable', False),
|
|
614
|
+
flags, charset,
|
|
615
|
+
),
|
|
616
|
+
)
|
|
366
617
|
pymy_res.append(PyMyField(col['name'], flags, charset))
|
|
367
618
|
self._descriptions.append(description)
|
|
368
619
|
|
|
@@ -371,21 +622,25 @@ class Cursor(object):
|
|
|
371
622
|
self._results.append(rows)
|
|
372
623
|
self._pymy_results.append(pymy_res)
|
|
373
624
|
|
|
625
|
+
# For compatibility with PyMySQL/MySQLdb
|
|
626
|
+
if is_callproc:
|
|
627
|
+
self._results.append([])
|
|
628
|
+
|
|
374
629
|
self.rowcount = len(self._results[0])
|
|
630
|
+
|
|
375
631
|
else:
|
|
632
|
+
# For compatibility with PyMySQL/MySQLdb
|
|
633
|
+
if is_callproc:
|
|
634
|
+
self._results.append([])
|
|
635
|
+
|
|
376
636
|
self.rowcount = out['rowsAffected']
|
|
377
637
|
|
|
638
|
+
return self.rowcount
|
|
639
|
+
|
|
378
640
|
def executemany(
|
|
379
641
|
self, query: str,
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
Union[
|
|
383
|
-
List[Any],
|
|
384
|
-
Dict[str, Any],
|
|
385
|
-
]
|
|
386
|
-
]
|
|
387
|
-
] = None,
|
|
388
|
-
) -> None:
|
|
642
|
+
args: Optional[Sequence[Union[Sequence[Any], Dict[str, Any]]]] = None,
|
|
643
|
+
) -> int:
|
|
389
644
|
"""
|
|
390
645
|
Execute SQL code against multiple sets of parameters.
|
|
391
646
|
|
|
@@ -393,28 +648,38 @@ class Cursor(object):
|
|
|
393
648
|
----------
|
|
394
649
|
query : str
|
|
395
650
|
The SQL statement to execute
|
|
396
|
-
|
|
651
|
+
args : iterable of iterables or dicts, optional
|
|
397
652
|
Sets of parameters to substitute into the SQL code
|
|
398
653
|
|
|
399
654
|
"""
|
|
400
|
-
if self.
|
|
401
|
-
raise
|
|
655
|
+
if self._connection is None:
|
|
656
|
+
raise ProgrammingError(errno=2048, msg='Connection is closed.')
|
|
402
657
|
|
|
403
658
|
results = []
|
|
404
|
-
|
|
659
|
+
rowcount = 0
|
|
660
|
+
if args is not None and len(args) > 0:
|
|
405
661
|
description = []
|
|
406
|
-
|
|
662
|
+
# Detect dataframes
|
|
663
|
+
if hasattr(args, 'itertuples'):
|
|
664
|
+
argiter = args.itertuples(index=False) # type: ignore
|
|
665
|
+
else:
|
|
666
|
+
argiter = iter(args)
|
|
667
|
+
for params in argiter:
|
|
407
668
|
self.execute(query, params)
|
|
408
669
|
if self._descriptions:
|
|
409
670
|
description = self._descriptions[-1]
|
|
410
671
|
if self._rows is not None:
|
|
411
672
|
results.append(self._rows)
|
|
673
|
+
rowcount += self.rowcount
|
|
412
674
|
self._results = results
|
|
413
675
|
self._descriptions = [description for _ in range(len(results))]
|
|
414
|
-
if self._results:
|
|
415
|
-
self.rowcount = len(self._results[0])
|
|
416
676
|
else:
|
|
417
677
|
self.execute(query)
|
|
678
|
+
rowcount += self.rowcount
|
|
679
|
+
|
|
680
|
+
self.rowcount = rowcount
|
|
681
|
+
|
|
682
|
+
return self.rowcount
|
|
418
683
|
|
|
419
684
|
@property
|
|
420
685
|
def _has_row(self) -> bool:
|
|
@@ -444,11 +709,19 @@ class Cursor(object):
|
|
|
444
709
|
If there are no rows left to return
|
|
445
710
|
|
|
446
711
|
"""
|
|
712
|
+
if self._connection is None:
|
|
713
|
+
raise ProgrammingError(errno=2048, msg='Connection is closed')
|
|
714
|
+
if not self._expect_results:
|
|
715
|
+
raise self._connection.ProgrammingError(msg='No query has been submitted')
|
|
447
716
|
if not self._has_row:
|
|
448
717
|
return None
|
|
449
718
|
out = self._rows[self._row_idx]
|
|
450
719
|
self._row_idx += 1
|
|
451
|
-
return
|
|
720
|
+
return format_results(
|
|
721
|
+
self._results_type,
|
|
722
|
+
self.description or [],
|
|
723
|
+
out, single=True,
|
|
724
|
+
)
|
|
452
725
|
|
|
453
726
|
def fetchmany(
|
|
454
727
|
self,
|
|
@@ -465,15 +738,21 @@ class Cursor(object):
|
|
|
465
738
|
Values of the returned rows if there are rows remaining
|
|
466
739
|
|
|
467
740
|
"""
|
|
741
|
+
if self._connection is None:
|
|
742
|
+
raise ProgrammingError(errno=2048, msg='Connection is closed')
|
|
743
|
+
if not self._expect_results:
|
|
744
|
+
raise self._connection.ProgrammingError(msg='No query has been submitted')
|
|
468
745
|
if not self._has_row:
|
|
469
|
-
|
|
746
|
+
if 'dict' in self._results_type:
|
|
747
|
+
return {}
|
|
748
|
+
return tuple()
|
|
470
749
|
if not size:
|
|
471
750
|
size = max(int(self.arraysize), 1)
|
|
472
751
|
else:
|
|
473
752
|
size = max(int(size), 1)
|
|
474
753
|
out = self._rows[self._row_idx:self._row_idx+size]
|
|
475
|
-
self._row_idx +=
|
|
476
|
-
return out
|
|
754
|
+
self._row_idx += len(out)
|
|
755
|
+
return format_results(self._results_type, self.description or [], out)
|
|
477
756
|
|
|
478
757
|
def fetchall(self) -> Result:
|
|
479
758
|
"""
|
|
@@ -485,17 +764,26 @@ class Cursor(object):
|
|
|
485
764
|
Values of the returned rows if there are rows remaining
|
|
486
765
|
|
|
487
766
|
"""
|
|
767
|
+
if self._connection is None:
|
|
768
|
+
raise ProgrammingError(errno=2048, msg='Connection is closed')
|
|
769
|
+
if not self._expect_results:
|
|
770
|
+
raise self._connection.ProgrammingError(msg='No query has been submitted')
|
|
488
771
|
if not self._has_row:
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
772
|
+
if 'dict' in self._results_type:
|
|
773
|
+
return {}
|
|
774
|
+
return tuple()
|
|
775
|
+
out = list(self._rows[self._row_idx:])
|
|
776
|
+
self._row_idx = len(out)
|
|
777
|
+
return format_results(self._results_type, self.description or [], out)
|
|
493
778
|
|
|
494
779
|
def nextset(self) -> Optional[bool]:
|
|
495
780
|
"""Skip to the next available result set."""
|
|
781
|
+
if self._connection is None:
|
|
782
|
+
raise ProgrammingError(errno=2048, msg='Connection is closed')
|
|
783
|
+
|
|
496
784
|
if self._result_idx < 0:
|
|
497
785
|
self._row_idx = -1
|
|
498
|
-
return
|
|
786
|
+
return None
|
|
499
787
|
|
|
500
788
|
self._result_idx += 1
|
|
501
789
|
self._row_idx = 0
|
|
@@ -503,13 +791,13 @@ class Cursor(object):
|
|
|
503
791
|
if self._result_idx >= len(self._results):
|
|
504
792
|
self._result_idx = -1
|
|
505
793
|
self._row_idx = -1
|
|
506
|
-
return
|
|
794
|
+
return None
|
|
507
795
|
|
|
508
796
|
self.rowcount = len(self._results[self._result_idx])
|
|
509
797
|
|
|
510
798
|
return True
|
|
511
799
|
|
|
512
|
-
def setinputsizes(self, sizes:
|
|
800
|
+
def setinputsizes(self, sizes: Sequence[int]) -> None:
|
|
513
801
|
"""Predefine memory areas for parameters."""
|
|
514
802
|
pass
|
|
515
803
|
|
|
@@ -517,7 +805,7 @@ class Cursor(object):
|
|
|
517
805
|
"""Set a column buffer size for fetches of large columns."""
|
|
518
806
|
pass
|
|
519
807
|
|
|
520
|
-
@
|
|
808
|
+
@property
|
|
521
809
|
def rownumber(self) -> Optional[int]:
|
|
522
810
|
"""
|
|
523
811
|
Return the zero-based index of the cursor in the result set.
|
|
@@ -543,6 +831,8 @@ class Cursor(object):
|
|
|
543
831
|
Type of move that should be made: 'relative' or 'absolute'
|
|
544
832
|
|
|
545
833
|
"""
|
|
834
|
+
if self._connection is None:
|
|
835
|
+
raise ProgrammingError(errno=2048, msg='Connection is closed')
|
|
546
836
|
if mode == 'relative':
|
|
547
837
|
self._row_idx += value
|
|
548
838
|
elif mode == 'absolute':
|
|
@@ -565,6 +855,8 @@ class Cursor(object):
|
|
|
565
855
|
If no more rows exist
|
|
566
856
|
|
|
567
857
|
"""
|
|
858
|
+
if self._connection is None:
|
|
859
|
+
raise InterfaceError(errno=2048, msg='Connection is closed')
|
|
568
860
|
out = self.fetchone()
|
|
569
861
|
if out is None:
|
|
570
862
|
raise StopIteration
|
|
@@ -587,6 +879,13 @@ class Cursor(object):
|
|
|
587
879
|
"""Exit a context."""
|
|
588
880
|
self.close()
|
|
589
881
|
|
|
882
|
+
@property
|
|
883
|
+
def open(self) -> bool:
|
|
884
|
+
"""Check if the cursor is still connected."""
|
|
885
|
+
if self._connection is None:
|
|
886
|
+
return False
|
|
887
|
+
return self._connection.is_connected()
|
|
888
|
+
|
|
590
889
|
def is_connected(self) -> bool:
|
|
591
890
|
"""
|
|
592
891
|
Check if the cursor is still connected.
|
|
@@ -596,12 +895,10 @@ class Cursor(object):
|
|
|
596
895
|
bool
|
|
597
896
|
|
|
598
897
|
"""
|
|
599
|
-
|
|
600
|
-
return False
|
|
601
|
-
return self.connection.is_connected()
|
|
898
|
+
return self.open
|
|
602
899
|
|
|
603
900
|
|
|
604
|
-
class Connection(
|
|
901
|
+
class Connection(connection.Connection):
|
|
605
902
|
"""
|
|
606
903
|
SingleStoreDB HTTP database connection.
|
|
607
904
|
|
|
@@ -613,19 +910,13 @@ class Connection(object):
|
|
|
613
910
|
`connect`
|
|
614
911
|
|
|
615
912
|
"""
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
Error = Error
|
|
619
|
-
InterfaceError = InterfaceError
|
|
620
|
-
DatabaseError = DatabaseError
|
|
621
|
-
DataError = DataError
|
|
622
|
-
OperationalError = OperationalError
|
|
623
|
-
IntegrityError = IntegrityError
|
|
624
|
-
InternalError = InternalError
|
|
625
|
-
ProgrammingError = ProgrammingError
|
|
626
|
-
NotSupportedError = NotSupportedError
|
|
913
|
+
driver = 'https'
|
|
914
|
+
paramstyle = 'qmark'
|
|
627
915
|
|
|
628
916
|
def __init__(self, **kwargs: Any):
|
|
917
|
+
from .. import __version__ as client_version
|
|
918
|
+
connection.Connection.__init__(self, **kwargs)
|
|
919
|
+
|
|
629
920
|
host = kwargs.get('host', get_option('host'))
|
|
630
921
|
port = kwargs.get('port', get_option('http_port'))
|
|
631
922
|
|
|
@@ -641,6 +932,7 @@ class Connection(object):
|
|
|
641
932
|
'Content-Type': 'application/json',
|
|
642
933
|
'Accept': 'application/json',
|
|
643
934
|
'Accept-Encoding': 'compress,identity',
|
|
935
|
+
'User-Agent': f'SingleStoreDB-Python/{client_version}',
|
|
644
936
|
})
|
|
645
937
|
|
|
646
938
|
if kwargs.get('ssl_disabled', get_option('ssl_disabled')):
|
|
@@ -657,13 +949,104 @@ class Connection(object):
|
|
|
657
949
|
if ssl_ca:
|
|
658
950
|
self._sess.verify = ssl_ca
|
|
659
951
|
|
|
660
|
-
|
|
661
|
-
|
|
952
|
+
ssl_verify_cert = kwargs.get('ssl_verify_cert', True)
|
|
953
|
+
if not ssl_verify_cert:
|
|
954
|
+
self._sess.verify = False
|
|
955
|
+
|
|
956
|
+
if kwargs.get('multi_statements', False):
|
|
957
|
+
raise self.InterfaceError(
|
|
958
|
+
0, 'The Data API does not allow multiple '
|
|
959
|
+
'statements within a query',
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
self._version = kwargs.get('version', 'v2')
|
|
963
|
+
self.driver = kwargs.get('driver', 'https')
|
|
964
|
+
|
|
965
|
+
self.encoders = {k: v for (k, v) in converters.items() if type(k) is not int}
|
|
966
|
+
self.decoders = {k: v for (k, v) in converters.items() if type(k) is int}
|
|
662
967
|
|
|
663
968
|
self._database = kwargs.get('database', get_option('database'))
|
|
664
|
-
self._url = f'{
|
|
665
|
-
self.
|
|
969
|
+
self._url = f'{self.driver}://{host}:{port}/api/{self._version}/'
|
|
970
|
+
self._host = host
|
|
971
|
+
self._messages: List[Tuple[int, str]] = []
|
|
666
972
|
self._autocommit: bool = True
|
|
973
|
+
self._conv = kwargs.get('conv', None)
|
|
974
|
+
self._in_sync: bool = False
|
|
975
|
+
self._track_env: bool = kwargs.get('track_env', False) \
|
|
976
|
+
or host == 'singlestore.com'
|
|
977
|
+
|
|
978
|
+
@property
|
|
979
|
+
def messages(self) -> List[Tuple[int, str]]:
|
|
980
|
+
return self._messages
|
|
981
|
+
|
|
982
|
+
def connect(self) -> 'Connection':
|
|
983
|
+
"""Connect to the server."""
|
|
984
|
+
return self
|
|
985
|
+
|
|
986
|
+
def _sync_connection(self, kwargs: Dict[str, Any]) -> None:
|
|
987
|
+
"""Synchronize connection with env variable."""
|
|
988
|
+
if self._sess is None:
|
|
989
|
+
raise InterfaceError(errno=2048, msg='Connection is closed.')
|
|
990
|
+
|
|
991
|
+
if self._in_sync:
|
|
992
|
+
return
|
|
993
|
+
|
|
994
|
+
if not self._track_env:
|
|
995
|
+
return
|
|
996
|
+
|
|
997
|
+
url = os.environ.get('SINGLESTOREDB_URL')
|
|
998
|
+
if not url:
|
|
999
|
+
if self._host == 'singlestore.com':
|
|
1000
|
+
raise InterfaceError(0, 'Connection URL has not been established')
|
|
1001
|
+
return
|
|
1002
|
+
|
|
1003
|
+
out = {}
|
|
1004
|
+
urlp = connection._parse_url(url)
|
|
1005
|
+
out.update(urlp)
|
|
1006
|
+
out = connection._cast_params(out)
|
|
1007
|
+
|
|
1008
|
+
# Set default port based on driver.
|
|
1009
|
+
if 'port' not in out or not out['port']:
|
|
1010
|
+
if out.get('driver', 'https') == 'http':
|
|
1011
|
+
out['port'] = int(get_option('port') or 80)
|
|
1012
|
+
else:
|
|
1013
|
+
out['port'] = int(get_option('port') or 443)
|
|
1014
|
+
|
|
1015
|
+
# If there is no user and the password is empty, remove the password key.
|
|
1016
|
+
if 'user' not in out and not out.get('password', None):
|
|
1017
|
+
out.pop('password', None)
|
|
1018
|
+
|
|
1019
|
+
if out['host'] == 'singlestore.com':
|
|
1020
|
+
raise InterfaceError(0, 'Connection URL has not been established')
|
|
1021
|
+
|
|
1022
|
+
# Get current connection attributes
|
|
1023
|
+
curr_url = urlparse(self._url, scheme='singlestoredb', allow_fragments=True)
|
|
1024
|
+
if self._sess.auth is not None:
|
|
1025
|
+
auth = tuple(self._sess.auth) # type: ignore
|
|
1026
|
+
else:
|
|
1027
|
+
auth = (None, None) # type: ignore
|
|
1028
|
+
|
|
1029
|
+
# If it's just a password change, we don't need to reconnect
|
|
1030
|
+
if (curr_url.hostname, curr_url.port, auth[0], self._database) == \
|
|
1031
|
+
(out['host'], out['port'], out['user'], out.get('database')):
|
|
1032
|
+
return
|
|
1033
|
+
|
|
1034
|
+
try:
|
|
1035
|
+
self._in_sync = True
|
|
1036
|
+
sess = requests.Session()
|
|
1037
|
+
sess.auth = (out['user'], out['password'])
|
|
1038
|
+
sess.headers.update(self._sess.headers)
|
|
1039
|
+
sess.verify = self._sess.verify
|
|
1040
|
+
sess.cert = self._sess.cert
|
|
1041
|
+
self._database = out.get('database')
|
|
1042
|
+
self._host = out['host']
|
|
1043
|
+
self._url = f'{out.get("driver", "https")}://{out["host"]}:{out["port"]}' \
|
|
1044
|
+
f'/api/{self._version}/'
|
|
1045
|
+
self._sess = sess
|
|
1046
|
+
if self._database:
|
|
1047
|
+
kwargs['json']['database'] = self._database
|
|
1048
|
+
finally:
|
|
1049
|
+
self._in_sync = False
|
|
667
1050
|
|
|
668
1051
|
def _post(self, path: str, *args: Any, **kwargs: Any) -> requests.Response:
|
|
669
1052
|
"""
|
|
@@ -685,24 +1068,46 @@ class Connection(object):
|
|
|
685
1068
|
"""
|
|
686
1069
|
if self._sess is None:
|
|
687
1070
|
raise InterfaceError(errno=2048, msg='Connection is closed.')
|
|
1071
|
+
|
|
1072
|
+
self._sync_connection(kwargs)
|
|
1073
|
+
|
|
1074
|
+
if 'timeout' not in kwargs:
|
|
1075
|
+
kwargs['timeout'] = get_option('connect_timeout')
|
|
1076
|
+
|
|
688
1077
|
return self._sess.post(urljoin(self._url, path), *args, **kwargs)
|
|
689
1078
|
|
|
690
1079
|
def close(self) -> None:
|
|
691
1080
|
"""Close the connection."""
|
|
1081
|
+
if self._host == 'singlestore.com':
|
|
1082
|
+
return
|
|
1083
|
+
if self._sess is None:
|
|
1084
|
+
raise Error(errno=2048, msg='Connection is closed')
|
|
692
1085
|
self._sess = None
|
|
693
1086
|
|
|
694
|
-
def autocommit(self, value: bool) -> None:
|
|
1087
|
+
def autocommit(self, value: bool = True) -> None:
|
|
695
1088
|
"""Set autocommit mode."""
|
|
1089
|
+
if self._host == 'singlestore.com':
|
|
1090
|
+
return
|
|
1091
|
+
if self._sess is None:
|
|
1092
|
+
raise InterfaceError(errno=2048, msg='Connection is closed')
|
|
696
1093
|
self._autocommit = value
|
|
697
1094
|
|
|
698
1095
|
def commit(self) -> None:
|
|
699
1096
|
"""Commit the pending transaction."""
|
|
1097
|
+
if self._host == 'singlestore.com':
|
|
1098
|
+
return
|
|
1099
|
+
if self._sess is None:
|
|
1100
|
+
raise InterfaceError(errno=2048, msg='Connection is closed')
|
|
700
1101
|
if self._autocommit:
|
|
701
1102
|
return
|
|
702
1103
|
raise NotSupportedError(msg='operation not supported')
|
|
703
1104
|
|
|
704
1105
|
def rollback(self) -> None:
|
|
705
1106
|
"""Rollback the pending transaction."""
|
|
1107
|
+
if self._host == 'singlestore.com':
|
|
1108
|
+
return
|
|
1109
|
+
if self._sess is None:
|
|
1110
|
+
raise InterfaceError(errno=2048, msg='Connection is closed')
|
|
706
1111
|
if self._autocommit:
|
|
707
1112
|
return
|
|
708
1113
|
raise NotSupportedError(msg='operation not supported')
|
|
@@ -729,6 +1134,17 @@ class Connection(object):
|
|
|
729
1134
|
"""Exit a context."""
|
|
730
1135
|
self.close()
|
|
731
1136
|
|
|
1137
|
+
@property
|
|
1138
|
+
def open(self) -> bool:
|
|
1139
|
+
"""Check if the database is still connected."""
|
|
1140
|
+
if self._sess is None:
|
|
1141
|
+
return False
|
|
1142
|
+
url = '/'.join(self._url.split('/')[:3]) + '/ping'
|
|
1143
|
+
res = self._sess.get(url)
|
|
1144
|
+
if res.status_code <= 400 and res.text == 'pong':
|
|
1145
|
+
return True
|
|
1146
|
+
return False
|
|
1147
|
+
|
|
732
1148
|
def is_connected(self) -> bool:
|
|
733
1149
|
"""
|
|
734
1150
|
Check if the database is still connected.
|
|
@@ -738,54 +1154,39 @@ class Connection(object):
|
|
|
738
1154
|
bool
|
|
739
1155
|
|
|
740
1156
|
"""
|
|
741
|
-
|
|
742
|
-
return False
|
|
743
|
-
url = '/'.join(self._url.split('/')[:3]) + '/ping'
|
|
744
|
-
res = self._sess.get(url)
|
|
745
|
-
if res.status_code <= 400 and res.text == 'pong':
|
|
746
|
-
return True
|
|
747
|
-
return False
|
|
1157
|
+
return self.open
|
|
748
1158
|
|
|
749
1159
|
|
|
750
1160
|
def connect(
|
|
751
|
-
host: Optional[str] = None,
|
|
752
|
-
user: Optional[str] = None,
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1161
|
+
host: Optional[str] = None,
|
|
1162
|
+
user: Optional[str] = None,
|
|
1163
|
+
password: Optional[str] = None,
|
|
1164
|
+
port: Optional[int] = None,
|
|
1165
|
+
database: Optional[str] = None,
|
|
1166
|
+
driver: Optional[str] = None,
|
|
1167
|
+
pure_python: Optional[bool] = None,
|
|
1168
|
+
local_infile: Optional[bool] = None,
|
|
1169
|
+
charset: Optional[str] = None,
|
|
1170
|
+
ssl_key: Optional[str] = None,
|
|
1171
|
+
ssl_cert: Optional[str] = None,
|
|
1172
|
+
ssl_ca: Optional[str] = None,
|
|
1173
|
+
ssl_disabled: Optional[bool] = None,
|
|
1174
|
+
ssl_cipher: Optional[str] = None,
|
|
1175
|
+
ssl_verify_cert: Optional[bool] = None,
|
|
1176
|
+
ssl_verify_identity: Optional[bool] = None,
|
|
1177
|
+
conv: Optional[Dict[int, Callable[..., Any]]] = None,
|
|
1178
|
+
credential_type: Optional[str] = None,
|
|
1179
|
+
autocommit: Optional[bool] = None,
|
|
1180
|
+
results_type: Optional[str] = None,
|
|
1181
|
+
buffered: Optional[bool] = None,
|
|
1182
|
+
results_format: Optional[str] = None,
|
|
1183
|
+
program_name: Optional[str] = None,
|
|
1184
|
+
conn_attrs: Optional[Dict[str, str]] = None,
|
|
1185
|
+
multi_statements: Optional[bool] = None,
|
|
1186
|
+
connect_timeout: Optional[int] = None,
|
|
1187
|
+
nan_as_null: Optional[bool] = None,
|
|
1188
|
+
inf_as_null: Optional[bool] = None,
|
|
1189
|
+
encoding_errors: Optional[str] = None,
|
|
1190
|
+
track_env: Optional[bool] = None,
|
|
756
1191
|
) -> Connection:
|
|
757
|
-
"""
|
|
758
|
-
Connect to a SingleStoreDB using HTTP.
|
|
759
|
-
|
|
760
|
-
Parameters
|
|
761
|
-
----------
|
|
762
|
-
user : str, optional
|
|
763
|
-
Database user name
|
|
764
|
-
password : str, optional
|
|
765
|
-
Database user password
|
|
766
|
-
host : str, optional
|
|
767
|
-
Database host name or IP address
|
|
768
|
-
port : int, optional
|
|
769
|
-
Database port. This defaults to 3306 for non-HTTP connections, 80
|
|
770
|
-
for HTTP connections, and 443 for HTTPS connections.
|
|
771
|
-
database : str, optional
|
|
772
|
-
Database name
|
|
773
|
-
protocol : str, optional
|
|
774
|
-
HTTP protocol: `http` or `https`
|
|
775
|
-
version : str, optional
|
|
776
|
-
Version of the HTTP API
|
|
777
|
-
ssl_key : str, optional
|
|
778
|
-
File containing SSL key
|
|
779
|
-
ssl_cert : str, optional
|
|
780
|
-
File containing SSL certificate
|
|
781
|
-
ssl_ca : str, optional
|
|
782
|
-
File containing SSL certificate authority
|
|
783
|
-
ssl_disabled : bool, optional
|
|
784
|
-
Disable SSL usage
|
|
785
|
-
|
|
786
|
-
Returns
|
|
787
|
-
-------
|
|
788
|
-
Connection
|
|
789
|
-
|
|
790
|
-
"""
|
|
791
1192
|
return Connection(**dict(locals()))
|