singlestoredb 0.3.3__py3-none-any.whl → 1.0.3__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 -2
- singlestoredb/alchemy/__init__.py +90 -0
- singlestoredb/auth.py +6 -4
- singlestoredb/config.py +116 -16
- singlestoredb/connection.py +489 -523
- singlestoredb/converters.py +275 -26
- singlestoredb/exceptions.py +30 -4
- 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/__init__.py +0 -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/connection.py +1192 -0
- singlestoredb/management/__init__.py +3 -2
- singlestoredb/management/billing_usage.py +148 -0
- singlestoredb/management/cluster.py +19 -14
- singlestoredb/management/manager.py +100 -40
- singlestoredb/management/organization.py +188 -0
- singlestoredb/management/region.py +6 -8
- singlestoredb/management/utils.py +253 -4
- singlestoredb/management/workspace.py +1153 -35
- singlestoredb/mysql/__init__.py +177 -0
- singlestoredb/mysql/_auth.py +298 -0
- singlestoredb/mysql/charset.py +214 -0
- singlestoredb/mysql/connection.py +1814 -0
- singlestoredb/mysql/constants/CLIENT.py +38 -0
- singlestoredb/mysql/constants/COMMAND.py +32 -0
- singlestoredb/mysql/constants/CR.py +78 -0
- singlestoredb/mysql/constants/ER.py +474 -0
- singlestoredb/mysql/constants/FIELD_TYPE.py +32 -0
- singlestoredb/mysql/constants/FLAG.py +15 -0
- singlestoredb/mysql/constants/SERVER_STATUS.py +10 -0
- singlestoredb/mysql/constants/__init__.py +0 -0
- singlestoredb/mysql/converters.py +271 -0
- singlestoredb/mysql/cursors.py +713 -0
- singlestoredb/mysql/err.py +92 -0
- singlestoredb/mysql/optionfile.py +20 -0
- singlestoredb/mysql/protocol.py +388 -0
- singlestoredb/mysql/tests/__init__.py +19 -0
- singlestoredb/mysql/tests/base.py +126 -0
- singlestoredb/mysql/tests/conftest.py +37 -0
- singlestoredb/mysql/tests/test_DictCursor.py +132 -0
- singlestoredb/mysql/tests/test_SSCursor.py +141 -0
- singlestoredb/mysql/tests/test_basic.py +452 -0
- singlestoredb/mysql/tests/test_connection.py +851 -0
- singlestoredb/mysql/tests/test_converters.py +58 -0
- singlestoredb/mysql/tests/test_cursor.py +141 -0
- singlestoredb/mysql/tests/test_err.py +16 -0
- singlestoredb/mysql/tests/test_issues.py +514 -0
- singlestoredb/mysql/tests/test_load_local.py +75 -0
- singlestoredb/mysql/tests/test_nextset.py +88 -0
- singlestoredb/mysql/tests/test_optionfile.py +27 -0
- singlestoredb/mysql/tests/thirdparty/__init__.py +6 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/capabilities.py +323 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/dbapi20.py +865 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +110 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +224 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +101 -0
- singlestoredb/mysql/times.py +23 -0
- 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 -117
- singlestoredb/tests/test_config.py +13 -15
- singlestoredb/tests/test_connection.py +241 -289
- singlestoredb/tests/test_dbapi.py +27 -0
- singlestoredb/tests/test_exceptions.py +0 -2
- 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 -28
- singlestoredb/tests/test_management.py +588 -10
- singlestoredb/tests/test_plugin.py +33 -0
- singlestoredb/tests/test_results.py +11 -14
- singlestoredb/tests/test_types.py +0 -2
- singlestoredb/tests/test_udf.py +687 -0
- singlestoredb/tests/test_xdict.py +0 -2
- singlestoredb/tests/utils.py +3 -4
- singlestoredb/types.py +4 -5
- singlestoredb/utils/config.py +71 -12
- singlestoredb/utils/convert_rows.py +0 -2
- singlestoredb/utils/debug.py +13 -0
- singlestoredb/utils/mogrify.py +151 -0
- singlestoredb/utils/results.py +4 -3
- singlestoredb/utils/xdict.py +12 -12
- singlestoredb-1.0.3.dist-info/METADATA +139 -0
- singlestoredb-1.0.3.dist-info/RECORD +112 -0
- {singlestoredb-0.3.3.dist-info → singlestoredb-1.0.3.dist-info}/WHEEL +1 -1
- singlestoredb-1.0.3.dist-info/entry_points.txt +2 -0
- singlestoredb/drivers/__init__.py +0 -46
- singlestoredb/drivers/base.py +0 -200
- singlestoredb/drivers/cymysql.py +0 -40
- singlestoredb/drivers/http.py +0 -49
- singlestoredb/drivers/mariadb.py +0 -42
- singlestoredb/drivers/mysqlconnector.py +0 -51
- singlestoredb/drivers/mysqldb.py +0 -62
- singlestoredb/drivers/pymysql.py +0 -39
- singlestoredb/drivers/pyodbc.py +0 -67
- singlestoredb/http.py +0 -794
- singlestoredb-0.3.3.dist-info/METADATA +0 -105
- singlestoredb-0.3.3.dist-info/RECORD +0 -46
- {singlestoredb-0.3.3.dist-info → singlestoredb-1.0.3.dist-info}/LICENSE +0 -0
- {singlestoredb-0.3.3.dist-info → singlestoredb-1.0.3.dist-info}/top_level.txt +0 -0
singlestoredb/http.py
DELETED
|
@@ -1,794 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
"""SingleStoreDB HTTP API interface."""
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import functools
|
|
6
|
-
import json
|
|
7
|
-
import re
|
|
8
|
-
from base64 import b64decode
|
|
9
|
-
from collections.abc import Mapping
|
|
10
|
-
from collections.abc import Sequence
|
|
11
|
-
from typing import Any
|
|
12
|
-
from typing import Callable
|
|
13
|
-
from typing import Dict
|
|
14
|
-
from typing import Iterable
|
|
15
|
-
from typing import Optional
|
|
16
|
-
from typing import Tuple
|
|
17
|
-
from typing import Union
|
|
18
|
-
from urllib.parse import urljoin
|
|
19
|
-
|
|
20
|
-
import requests
|
|
21
|
-
|
|
22
|
-
from . import types
|
|
23
|
-
from .config import get_option
|
|
24
|
-
from .converters import converters
|
|
25
|
-
from .exceptions import DatabaseError # noqa: F401
|
|
26
|
-
from .exceptions import DataError
|
|
27
|
-
from .exceptions import Error
|
|
28
|
-
from .exceptions import IntegrityError
|
|
29
|
-
from .exceptions import InterfaceError
|
|
30
|
-
from .exceptions import InternalError
|
|
31
|
-
from .exceptions import NotSupportedError
|
|
32
|
-
from .exceptions import OperationalError
|
|
33
|
-
from .exceptions import ProgrammingError
|
|
34
|
-
from .exceptions import Warning # noqa: F401
|
|
35
|
-
from .utils.convert_rows import convert_rows
|
|
36
|
-
from .utils.results import Result
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# DB-API settings
|
|
40
|
-
apilevel = '2.0'
|
|
41
|
-
paramstyle = 'qmark'
|
|
42
|
-
threadsafety = 1
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
Description = Tuple[
|
|
46
|
-
str, int, Optional[int], Optional[int], Optional[int],
|
|
47
|
-
Optional[int], bool,
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
_interface_errors = set([
|
|
52
|
-
0,
|
|
53
|
-
2013, # CR_SERVER_LOST
|
|
54
|
-
2006, # CR_SERVER_GONE_ERROR
|
|
55
|
-
2012, # CR_HANDSHAKE_ERR
|
|
56
|
-
2004, # CR_IPSOCK_ERROR
|
|
57
|
-
2014, # CR_COMMANDS_OUT_OF_SYNC
|
|
58
|
-
])
|
|
59
|
-
_data_errors = set([
|
|
60
|
-
1406, # ER_DATA_TOO_LONG
|
|
61
|
-
1441, # ER_DATETIME_FUNCTION_OVERFLOW
|
|
62
|
-
1365, # ER_DIVISION_BY_ZERO
|
|
63
|
-
1230, # ER_NO_DEFAULT
|
|
64
|
-
1171, # ER_PRIMARY_CANT_HAVE_NULL
|
|
65
|
-
1264, # ER_WARN_DATA_OUT_OF_RANGE
|
|
66
|
-
1265, # ER_WARN_DATA_TRUNCATED
|
|
67
|
-
])
|
|
68
|
-
_programming_errors = set([
|
|
69
|
-
1065, # ER_EMPTY_QUERY
|
|
70
|
-
1179, # ER_CANT_DO_THIS_DURING_AN_TRANSACTION
|
|
71
|
-
1007, # ER_DB_CREATE_EXISTS
|
|
72
|
-
1110, # ER_FIELD_SPECIFIED_TWICE
|
|
73
|
-
1111, # ER_INVALID_GROUP_FUNC_USE
|
|
74
|
-
1082, # ER_NO_SUCH_INDEX
|
|
75
|
-
1741, # ER_NO_SUCH_KEY_VALUE
|
|
76
|
-
1146, # ER_NO_SUCH_TABLE
|
|
77
|
-
1449, # ER_NO_SUCH_USER
|
|
78
|
-
1064, # ER_PARSE_ERROR
|
|
79
|
-
1149, # ER_SYNTAX_ERROR
|
|
80
|
-
1113, # ER_TABLE_MUST_HAVE_COLUMNS
|
|
81
|
-
1112, # ER_UNSUPPORTED_EXTENSION
|
|
82
|
-
1102, # ER_WRONG_DB_NAME
|
|
83
|
-
1103, # ER_WRONG_TABLE_NAME
|
|
84
|
-
1049, # ER_BAD_DB_ERROR
|
|
85
|
-
1582, # ER_??? Wrong number of args
|
|
86
|
-
])
|
|
87
|
-
_integrity_errors = set([
|
|
88
|
-
1215, # ER_CANNOT_ADD_FOREIGN
|
|
89
|
-
1062, # ER_DUP_ENTRY
|
|
90
|
-
1169, # ER_DUP_UNIQUE
|
|
91
|
-
1364, # ER_NO_DEFAULT_FOR_FIELD
|
|
92
|
-
1216, # ER_NO_REFERENCED_ROW
|
|
93
|
-
1452, # ER_NO_REFERENCED_ROW_2
|
|
94
|
-
1217, # ER_ROW_IS_REFERENCED
|
|
95
|
-
1451, # ER_ROW_IS_REFERENCED_2
|
|
96
|
-
1460, # ER_XAER_OUTSIDE
|
|
97
|
-
1401, # ER_XAER_RMERR
|
|
98
|
-
1048, # ER_BAD_NULL_ERROR
|
|
99
|
-
1264, # ER_DATA_OUT_OF_RANGE
|
|
100
|
-
4025, # ER_CONSTRAINT_FAILED
|
|
101
|
-
1826, # ER_DUP_CONSTRAINT_NAME
|
|
102
|
-
])
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def get_precision_scale(type_code: str) -> tuple[Optional[int], Optional[int]]:
|
|
106
|
-
"""Parse the precision and scale from a data type."""
|
|
107
|
-
if '(' not in type_code:
|
|
108
|
-
return (None, None)
|
|
109
|
-
m = re.search(r'\(\s*(\d+)\s*,\s*(\d+)\s*\)', type_code)
|
|
110
|
-
if m:
|
|
111
|
-
return int(m.group(1)), int(m.group(2))
|
|
112
|
-
m = re.search(r'\(\s*(\d+)\s*\)', type_code)
|
|
113
|
-
if m:
|
|
114
|
-
return (int(m.group(1)), None)
|
|
115
|
-
raise ValueError(f'Unrecognized type code: {type_code}')
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def get_exc_type(code: int) -> type:
|
|
119
|
-
"""Map error code to DB-API error type."""
|
|
120
|
-
if code in _interface_errors:
|
|
121
|
-
return InterfaceError
|
|
122
|
-
if code in _data_errors:
|
|
123
|
-
return DataError
|
|
124
|
-
if code in _programming_errors:
|
|
125
|
-
return ProgrammingError
|
|
126
|
-
if code in _integrity_errors:
|
|
127
|
-
return IntegrityError
|
|
128
|
-
if code >= 1000:
|
|
129
|
-
return OperationalError
|
|
130
|
-
return InternalError
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def identity(x: Any) -> Any:
|
|
134
|
-
"""Return input value."""
|
|
135
|
-
return x
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def b64decode_converter(
|
|
139
|
-
converter: Callable[..., Any],
|
|
140
|
-
x: Optional[str],
|
|
141
|
-
encoding: str = 'utf-8',
|
|
142
|
-
) -> Optional[bytes]:
|
|
143
|
-
"""Decode value before applying converter."""
|
|
144
|
-
if x is None:
|
|
145
|
-
return None
|
|
146
|
-
if converter is None:
|
|
147
|
-
return b64decode(x)
|
|
148
|
-
return converter(b64decode(x))
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
class PyMyField(object):
|
|
152
|
-
"""Field for PyMySQL compatibility."""
|
|
153
|
-
|
|
154
|
-
def __init__(self, name: str, flags: int, charset: int) -> None:
|
|
155
|
-
self.name = name
|
|
156
|
-
self.flags = flags
|
|
157
|
-
self.charsetnr = charset
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
class PyMyResult(object):
|
|
161
|
-
"""Result for PyMySQL compatibility."""
|
|
162
|
-
|
|
163
|
-
def __init__(self) -> None:
|
|
164
|
-
self.fields: list[PyMyField] = []
|
|
165
|
-
|
|
166
|
-
def append(self, item: PyMyField) -> None:
|
|
167
|
-
self.fields.append(item)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
class Cursor(object):
|
|
171
|
-
"""
|
|
172
|
-
SingleStoreDB HTTP database cursor.
|
|
173
|
-
|
|
174
|
-
Cursor objects should not be created directly. They should come from
|
|
175
|
-
the `cursor` method on the `Connection` object.
|
|
176
|
-
|
|
177
|
-
Parameters
|
|
178
|
-
----------
|
|
179
|
-
connection : Connection
|
|
180
|
-
The HTTP Connection object the cursor belongs to
|
|
181
|
-
|
|
182
|
-
"""
|
|
183
|
-
|
|
184
|
-
def __init__(self, connection: Connection):
|
|
185
|
-
self.connection: Optional[Connection] = connection
|
|
186
|
-
self._results: list[list[tuple[Any, ...]]] = [[]]
|
|
187
|
-
self._row_idx: int = -1
|
|
188
|
-
self._result_idx: int = -1
|
|
189
|
-
self._descriptions: list[list[Description]] = []
|
|
190
|
-
self.arraysize: int = 1000
|
|
191
|
-
self.rowcount: int = 0
|
|
192
|
-
self.messages: list[tuple[int, str]] = []
|
|
193
|
-
self.lastrowid: Optional[int] = None
|
|
194
|
-
self._pymy_results: list[PyMyResult] = []
|
|
195
|
-
|
|
196
|
-
@property
|
|
197
|
-
def _result(self) -> Optional[PyMyResult]:
|
|
198
|
-
"""Return Result object for PyMySQL compatibility."""
|
|
199
|
-
if self._result_idx < 0:
|
|
200
|
-
return None
|
|
201
|
-
return self._pymy_results[self._result_idx]
|
|
202
|
-
|
|
203
|
-
@property
|
|
204
|
-
def description(self) -> Optional[list[Description]]:
|
|
205
|
-
"""Return description for current result set."""
|
|
206
|
-
if not self._descriptions:
|
|
207
|
-
return None
|
|
208
|
-
if self._result_idx >= 0 and self._result_idx < len(self._descriptions):
|
|
209
|
-
return self._descriptions[self._result_idx]
|
|
210
|
-
return None
|
|
211
|
-
|
|
212
|
-
def _post(self, path: str, *args: Any, **kwargs: Any) -> requests.Response:
|
|
213
|
-
"""
|
|
214
|
-
Invoke a POST request on the HTTP connection.
|
|
215
|
-
|
|
216
|
-
Parameters
|
|
217
|
-
----------
|
|
218
|
-
path : str
|
|
219
|
-
The path of the resource
|
|
220
|
-
*args : positional parameters, optional
|
|
221
|
-
Extra parameters to the POST request
|
|
222
|
-
**kwargs : keyword parameters, optional
|
|
223
|
-
Extra keyword parameters to the POST request
|
|
224
|
-
|
|
225
|
-
Returns
|
|
226
|
-
-------
|
|
227
|
-
requests.Response
|
|
228
|
-
|
|
229
|
-
"""
|
|
230
|
-
if self.connection is None:
|
|
231
|
-
raise InterfaceError(errno=2048, msg='Connection is closed.')
|
|
232
|
-
return self.connection._post(path, *args, **kwargs)
|
|
233
|
-
|
|
234
|
-
def callproc(
|
|
235
|
-
self, name: str,
|
|
236
|
-
params: Union[Sequence[Any], Mapping[str, Any]],
|
|
237
|
-
) -> None:
|
|
238
|
-
"""
|
|
239
|
-
Call a stored procedure.
|
|
240
|
-
|
|
241
|
-
Parameters
|
|
242
|
-
----------
|
|
243
|
-
name : str
|
|
244
|
-
Name of the stored procedure
|
|
245
|
-
params : iterable or dict, optional
|
|
246
|
-
Parameters to the stored procedure
|
|
247
|
-
|
|
248
|
-
"""
|
|
249
|
-
if self.connection is None:
|
|
250
|
-
raise InterfaceError(errno=2048, msg='Connection is closed.')
|
|
251
|
-
raise NotImplementedError
|
|
252
|
-
|
|
253
|
-
def close(self) -> None:
|
|
254
|
-
"""Close the cursor."""
|
|
255
|
-
if self.connection is not None:
|
|
256
|
-
self.connection = None
|
|
257
|
-
|
|
258
|
-
def execute(
|
|
259
|
-
self, query: str,
|
|
260
|
-
params: Optional[Union[Sequence[Any], Mapping[str, Any]]] = None,
|
|
261
|
-
) -> None:
|
|
262
|
-
"""
|
|
263
|
-
Execute a SQL statement.
|
|
264
|
-
|
|
265
|
-
Parameters
|
|
266
|
-
----------
|
|
267
|
-
oper : str
|
|
268
|
-
The SQL statement to execute
|
|
269
|
-
params : iterable or dict, optional
|
|
270
|
-
Parameters to substitute into the SQL code
|
|
271
|
-
|
|
272
|
-
"""
|
|
273
|
-
if self.connection is None:
|
|
274
|
-
raise InterfaceError(errno=2048, msg='Connection is closed.')
|
|
275
|
-
|
|
276
|
-
data: Dict[str, Any] = dict(sql=query)
|
|
277
|
-
if params is not None:
|
|
278
|
-
data['args'] = params
|
|
279
|
-
if self.connection._database:
|
|
280
|
-
data['database'] = self.connection._database
|
|
281
|
-
|
|
282
|
-
sql_type = 'exec'
|
|
283
|
-
if re.match(r'^\s*(select|show|call|echo)\s+', query, flags=re.I):
|
|
284
|
-
sql_type = 'query'
|
|
285
|
-
|
|
286
|
-
if sql_type == 'query':
|
|
287
|
-
res = self._post('query/tuples', json=data)
|
|
288
|
-
else:
|
|
289
|
-
res = self._post('exec', json=data)
|
|
290
|
-
|
|
291
|
-
if res.status_code >= 400:
|
|
292
|
-
if res.text:
|
|
293
|
-
if re.match(r'^Error\s+\d+:', res.text):
|
|
294
|
-
code, msg = res.text.split(':', 1)
|
|
295
|
-
icode = int(code.split()[-1])
|
|
296
|
-
else:
|
|
297
|
-
icode = res.status_code
|
|
298
|
-
msg = res.text
|
|
299
|
-
raise get_exc_type(icode)(icode, msg.strip())
|
|
300
|
-
raise InterfaceError(errno=res.status_code, msg='HTTP Error')
|
|
301
|
-
|
|
302
|
-
out = json.loads(res.text)
|
|
303
|
-
|
|
304
|
-
self._descriptions = []
|
|
305
|
-
self._results = []
|
|
306
|
-
self._row_idx = -1
|
|
307
|
-
self._result_idx = -1
|
|
308
|
-
self.rowcount = 0
|
|
309
|
-
|
|
310
|
-
if sql_type == 'query':
|
|
311
|
-
# description: (name, type_code, display_size, internal_size,
|
|
312
|
-
# precision, scale, null_ok, column_flags, charset)
|
|
313
|
-
|
|
314
|
-
# Remove converters for things the JSON parser already converted
|
|
315
|
-
http_converters = dict(converters)
|
|
316
|
-
http_converters.pop(4, None)
|
|
317
|
-
http_converters.pop(5, None)
|
|
318
|
-
http_converters.pop(6, None)
|
|
319
|
-
http_converters.pop(15, None)
|
|
320
|
-
http_converters.pop(245, None)
|
|
321
|
-
http_converters.pop(247, None)
|
|
322
|
-
http_converters.pop(249, None)
|
|
323
|
-
http_converters.pop(250, None)
|
|
324
|
-
http_converters.pop(251, None)
|
|
325
|
-
http_converters.pop(252, None)
|
|
326
|
-
http_converters.pop(253, None)
|
|
327
|
-
http_converters.pop(254, None)
|
|
328
|
-
|
|
329
|
-
results = out['results']
|
|
330
|
-
|
|
331
|
-
# Convert data to Python types
|
|
332
|
-
if results and results[0]:
|
|
333
|
-
self._row_idx = 0
|
|
334
|
-
self._result_idx = 0
|
|
335
|
-
|
|
336
|
-
for result in results:
|
|
337
|
-
|
|
338
|
-
pymy_res = PyMyResult()
|
|
339
|
-
convs = []
|
|
340
|
-
|
|
341
|
-
description: list[Description] = []
|
|
342
|
-
for i, col in enumerate(result.get('columns', [])):
|
|
343
|
-
charset = 0
|
|
344
|
-
flags = 0
|
|
345
|
-
data_type = col['dataType'].split('(')[0]
|
|
346
|
-
type_code = types.ColumnType.get_code(data_type)
|
|
347
|
-
prec, scale = get_precision_scale(col['dataType'])
|
|
348
|
-
converter = http_converters.get(type_code, None)
|
|
349
|
-
if 'UNSIGNED' in data_type:
|
|
350
|
-
flags = 32
|
|
351
|
-
if data_type.endswith('BLOB') or data_type.endswith('BINARY'):
|
|
352
|
-
converter = functools.partial(b64decode_converter, converter)
|
|
353
|
-
charset = 63 # BINARY
|
|
354
|
-
if type_code == 0: # DECIMAL
|
|
355
|
-
type_code = types.ColumnType.get_code('NEWDECIMAL')
|
|
356
|
-
elif type_code == 15: # VARCHAR / VARBINARY
|
|
357
|
-
type_code = types.ColumnType.get_code('VARSTRING')
|
|
358
|
-
if type_code == 246 and prec is not None: # NEWDECIMAL
|
|
359
|
-
prec += 1 # for sign
|
|
360
|
-
if scale is not None and scale > 0:
|
|
361
|
-
prec += 1 # for decimal
|
|
362
|
-
if converter is not None:
|
|
363
|
-
convs.append((i, None, converter))
|
|
364
|
-
description.append((
|
|
365
|
-
str(col['name']), type_code,
|
|
366
|
-
None, None, prec, scale,
|
|
367
|
-
col.get('nullable', False),
|
|
368
|
-
))
|
|
369
|
-
pymy_res.append(PyMyField(col['name'], flags, charset))
|
|
370
|
-
self._descriptions.append(description)
|
|
371
|
-
|
|
372
|
-
rows = convert_rows(result.get('rows', []), convs)
|
|
373
|
-
|
|
374
|
-
self._results.append(rows)
|
|
375
|
-
self._pymy_results.append(pymy_res)
|
|
376
|
-
|
|
377
|
-
self.rowcount = len(self._results[0])
|
|
378
|
-
else:
|
|
379
|
-
self.rowcount = out['rowsAffected']
|
|
380
|
-
|
|
381
|
-
def executemany(
|
|
382
|
-
self, query: str,
|
|
383
|
-
param_seq: Optional[
|
|
384
|
-
Sequence[
|
|
385
|
-
Union[
|
|
386
|
-
Sequence[Any],
|
|
387
|
-
Mapping[str, Any],
|
|
388
|
-
]
|
|
389
|
-
]
|
|
390
|
-
] = None,
|
|
391
|
-
) -> None:
|
|
392
|
-
"""
|
|
393
|
-
Execute SQL code against multiple sets of parameters.
|
|
394
|
-
|
|
395
|
-
Parameters
|
|
396
|
-
----------
|
|
397
|
-
query : str
|
|
398
|
-
The SQL statement to execute
|
|
399
|
-
params_seq : iterable of iterables or dicts, optional
|
|
400
|
-
Sets of parameters to substitute into the SQL code
|
|
401
|
-
|
|
402
|
-
"""
|
|
403
|
-
if self.connection is None:
|
|
404
|
-
raise InterfaceError(errno=2048, msg='Connection is closed.')
|
|
405
|
-
|
|
406
|
-
results = []
|
|
407
|
-
if param_seq:
|
|
408
|
-
description = []
|
|
409
|
-
for params in param_seq:
|
|
410
|
-
self.execute(query, params)
|
|
411
|
-
if self._descriptions:
|
|
412
|
-
description = self._descriptions[-1]
|
|
413
|
-
if self._rows is not None:
|
|
414
|
-
results.append(self._rows)
|
|
415
|
-
self._results = results
|
|
416
|
-
self._descriptions = [description for _ in range(len(results))]
|
|
417
|
-
if self._results:
|
|
418
|
-
self.rowcount = len(self._results[0])
|
|
419
|
-
else:
|
|
420
|
-
self.execute(query)
|
|
421
|
-
|
|
422
|
-
@property
|
|
423
|
-
def _has_row(self) -> bool:
|
|
424
|
-
"""Determine if a row is available."""
|
|
425
|
-
if self._result_idx < 0 or self._result_idx >= len(self._results):
|
|
426
|
-
return False
|
|
427
|
-
if self._row_idx < 0 or self._row_idx >= len(self._results[self._result_idx]):
|
|
428
|
-
return False
|
|
429
|
-
return True
|
|
430
|
-
|
|
431
|
-
@property
|
|
432
|
-
def _rows(self) -> list[tuple[Any, ...]]:
|
|
433
|
-
"""Return current set of rows."""
|
|
434
|
-
if not self._has_row:
|
|
435
|
-
return []
|
|
436
|
-
return self._results[self._result_idx]
|
|
437
|
-
|
|
438
|
-
def fetchone(self) -> Optional[Result]:
|
|
439
|
-
"""
|
|
440
|
-
Fetch a single row from the result set.
|
|
441
|
-
|
|
442
|
-
Returns
|
|
443
|
-
-------
|
|
444
|
-
tuple
|
|
445
|
-
Values of the returned row if there are rows remaining
|
|
446
|
-
None
|
|
447
|
-
If there are no rows left to return
|
|
448
|
-
|
|
449
|
-
"""
|
|
450
|
-
if not self._has_row:
|
|
451
|
-
return None
|
|
452
|
-
out = self._rows[self._row_idx]
|
|
453
|
-
self._row_idx += 1
|
|
454
|
-
return out
|
|
455
|
-
|
|
456
|
-
def fetchmany(
|
|
457
|
-
self,
|
|
458
|
-
size: Optional[int] = None,
|
|
459
|
-
) -> Result:
|
|
460
|
-
"""
|
|
461
|
-
Fetch `size` rows from the result.
|
|
462
|
-
|
|
463
|
-
If `size` is not specified, the `arraysize` attribute is used.
|
|
464
|
-
|
|
465
|
-
Returns
|
|
466
|
-
-------
|
|
467
|
-
list of tuples
|
|
468
|
-
Values of the returned rows if there are rows remaining
|
|
469
|
-
|
|
470
|
-
"""
|
|
471
|
-
if not self._has_row:
|
|
472
|
-
return []
|
|
473
|
-
if not size:
|
|
474
|
-
size = max(int(self.arraysize), 1)
|
|
475
|
-
else:
|
|
476
|
-
size = max(int(size), 1)
|
|
477
|
-
out = self._rows[self._row_idx:self._row_idx+size]
|
|
478
|
-
self._row_idx += size
|
|
479
|
-
return out
|
|
480
|
-
|
|
481
|
-
def fetchall(self) -> Result:
|
|
482
|
-
"""
|
|
483
|
-
Fetch all rows in the result set.
|
|
484
|
-
|
|
485
|
-
Returns
|
|
486
|
-
-------
|
|
487
|
-
list of tuples
|
|
488
|
-
Values of the returned rows if there are rows remaining
|
|
489
|
-
|
|
490
|
-
"""
|
|
491
|
-
if not self._has_row:
|
|
492
|
-
return []
|
|
493
|
-
out = list(self._rows)
|
|
494
|
-
self._row_idx = -1
|
|
495
|
-
return out
|
|
496
|
-
|
|
497
|
-
def nextset(self) -> Optional[bool]:
|
|
498
|
-
"""Skip to the next available result set."""
|
|
499
|
-
if self._result_idx < 0:
|
|
500
|
-
self._row_idx = -1
|
|
501
|
-
return False
|
|
502
|
-
|
|
503
|
-
self._result_idx += 1
|
|
504
|
-
self._row_idx = 0
|
|
505
|
-
|
|
506
|
-
if self._result_idx >= len(self._results):
|
|
507
|
-
self._result_idx = -1
|
|
508
|
-
self._row_idx = -1
|
|
509
|
-
return False
|
|
510
|
-
|
|
511
|
-
self.rowcount = len(self._results[self._result_idx])
|
|
512
|
-
|
|
513
|
-
return True
|
|
514
|
-
|
|
515
|
-
def setinputsizes(self, sizes: Sequence[int]) -> None:
|
|
516
|
-
"""Predefine memory areas for parameters."""
|
|
517
|
-
pass
|
|
518
|
-
|
|
519
|
-
def setoutputsize(self, size: int, column: Optional[str] = None) -> None:
|
|
520
|
-
"""Set a column buffer size for fetches of large columns."""
|
|
521
|
-
pass
|
|
522
|
-
|
|
523
|
-
@ property
|
|
524
|
-
def rownumber(self) -> Optional[int]:
|
|
525
|
-
"""
|
|
526
|
-
Return the zero-based index of the cursor in the result set.
|
|
527
|
-
|
|
528
|
-
Returns
|
|
529
|
-
-------
|
|
530
|
-
int
|
|
531
|
-
|
|
532
|
-
"""
|
|
533
|
-
if self._row_idx < 0:
|
|
534
|
-
return None
|
|
535
|
-
return self._row_idx
|
|
536
|
-
|
|
537
|
-
def scroll(self, value: int, mode: str = 'relative') -> None:
|
|
538
|
-
"""
|
|
539
|
-
Scroll the cursor to the position in the result set.
|
|
540
|
-
|
|
541
|
-
Parameters
|
|
542
|
-
----------
|
|
543
|
-
value : int
|
|
544
|
-
Value of the positional move
|
|
545
|
-
mode : str
|
|
546
|
-
Type of move that should be made: 'relative' or 'absolute'
|
|
547
|
-
|
|
548
|
-
"""
|
|
549
|
-
if mode == 'relative':
|
|
550
|
-
self._row_idx += value
|
|
551
|
-
elif mode == 'absolute':
|
|
552
|
-
self._row_idx = value
|
|
553
|
-
else:
|
|
554
|
-
raise ValueError(
|
|
555
|
-
f'{mode} is not a valid mode, '
|
|
556
|
-
'expecting "relative" or "absolute"',
|
|
557
|
-
)
|
|
558
|
-
|
|
559
|
-
def next(self) -> Optional[Result]:
|
|
560
|
-
"""
|
|
561
|
-
Return the next row from the result set for use in iterators.
|
|
562
|
-
|
|
563
|
-
Returns
|
|
564
|
-
-------
|
|
565
|
-
tuple
|
|
566
|
-
Values from the next result row
|
|
567
|
-
None
|
|
568
|
-
If no more rows exist
|
|
569
|
-
|
|
570
|
-
"""
|
|
571
|
-
out = self.fetchone()
|
|
572
|
-
if out is None:
|
|
573
|
-
raise StopIteration
|
|
574
|
-
return out
|
|
575
|
-
|
|
576
|
-
__next__ = next
|
|
577
|
-
|
|
578
|
-
def __iter__(self) -> Iterable[tuple[Any, ...]]:
|
|
579
|
-
"""Return result iterator."""
|
|
580
|
-
return iter(self._rows)
|
|
581
|
-
|
|
582
|
-
def __enter__(self) -> Cursor:
|
|
583
|
-
"""Enter a context."""
|
|
584
|
-
return self
|
|
585
|
-
|
|
586
|
-
def __exit__(
|
|
587
|
-
self, exc_type: Optional[object],
|
|
588
|
-
exc_value: Optional[Exception], exc_traceback: Optional[str],
|
|
589
|
-
) -> None:
|
|
590
|
-
"""Exit a context."""
|
|
591
|
-
self.close()
|
|
592
|
-
|
|
593
|
-
def is_connected(self) -> bool:
|
|
594
|
-
"""
|
|
595
|
-
Check if the cursor is still connected.
|
|
596
|
-
|
|
597
|
-
Returns
|
|
598
|
-
-------
|
|
599
|
-
bool
|
|
600
|
-
|
|
601
|
-
"""
|
|
602
|
-
if self.connection is None:
|
|
603
|
-
return False
|
|
604
|
-
return self.connection.is_connected()
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
class Connection(object):
|
|
608
|
-
"""
|
|
609
|
-
SingleStoreDB HTTP database connection.
|
|
610
|
-
|
|
611
|
-
Instances of this object are typically created through the
|
|
612
|
-
`connection` function rather than creating them directly.
|
|
613
|
-
|
|
614
|
-
See Also
|
|
615
|
-
--------
|
|
616
|
-
`connect`
|
|
617
|
-
|
|
618
|
-
"""
|
|
619
|
-
|
|
620
|
-
Warning = Warning
|
|
621
|
-
Error = Error
|
|
622
|
-
InterfaceError = InterfaceError
|
|
623
|
-
DatabaseError = DatabaseError
|
|
624
|
-
DataError = DataError
|
|
625
|
-
OperationalError = OperationalError
|
|
626
|
-
IntegrityError = IntegrityError
|
|
627
|
-
InternalError = InternalError
|
|
628
|
-
ProgrammingError = ProgrammingError
|
|
629
|
-
NotSupportedError = NotSupportedError
|
|
630
|
-
|
|
631
|
-
def __init__(self, **kwargs: Any):
|
|
632
|
-
host = kwargs.get('host', get_option('host'))
|
|
633
|
-
port = kwargs.get('port', get_option('http_port'))
|
|
634
|
-
|
|
635
|
-
self._sess: Optional[requests.Session] = requests.Session()
|
|
636
|
-
|
|
637
|
-
user = kwargs.get('user', get_option('user'))
|
|
638
|
-
password = kwargs.get('password', get_option('password'))
|
|
639
|
-
if user is not None and password is not None:
|
|
640
|
-
self._sess.auth = (user, password)
|
|
641
|
-
elif user is not None:
|
|
642
|
-
self._sess.auth = (user, '')
|
|
643
|
-
self._sess.headers.update({
|
|
644
|
-
'Content-Type': 'application/json',
|
|
645
|
-
'Accept': 'application/json',
|
|
646
|
-
'Accept-Encoding': 'compress,identity',
|
|
647
|
-
})
|
|
648
|
-
|
|
649
|
-
if kwargs.get('ssl_disabled', get_option('ssl_disabled')):
|
|
650
|
-
self._sess.verify = False
|
|
651
|
-
else:
|
|
652
|
-
ssl_key = kwargs.get('ssl_key', get_option('ssl_key'))
|
|
653
|
-
ssl_cert = kwargs.get('ssl_cert', get_option('ssl_cert'))
|
|
654
|
-
if ssl_key and ssl_cert:
|
|
655
|
-
self._sess.cert = (ssl_key, ssl_cert)
|
|
656
|
-
elif ssl_cert:
|
|
657
|
-
self._sess.cert = ssl_cert
|
|
658
|
-
|
|
659
|
-
ssl_ca = kwargs.get('ssl_ca', get_option('ssl_ca'))
|
|
660
|
-
if ssl_ca:
|
|
661
|
-
self._sess.verify = ssl_ca
|
|
662
|
-
|
|
663
|
-
version = kwargs.get('version', 'v1')
|
|
664
|
-
protocol = kwargs.get('protocol', 'https')
|
|
665
|
-
|
|
666
|
-
self._database = kwargs.get('database', get_option('database'))
|
|
667
|
-
self._url = f'{protocol}://{host}:{port}/api/{version}/'
|
|
668
|
-
self.messages: list[list[Any]] = []
|
|
669
|
-
self._autocommit: bool = True
|
|
670
|
-
|
|
671
|
-
def _post(self, path: str, *args: Any, **kwargs: Any) -> requests.Response:
|
|
672
|
-
"""
|
|
673
|
-
Invoke a POST request on the HTTP connection.
|
|
674
|
-
|
|
675
|
-
Parameters
|
|
676
|
-
----------
|
|
677
|
-
path : str
|
|
678
|
-
The path of the resource
|
|
679
|
-
*args : positional parameters, optional
|
|
680
|
-
Extra parameters to the POST request
|
|
681
|
-
**kwargs : keyword parameters, optional
|
|
682
|
-
Extra keyword parameters to the POST request
|
|
683
|
-
|
|
684
|
-
Returns
|
|
685
|
-
-------
|
|
686
|
-
requests.Response
|
|
687
|
-
|
|
688
|
-
"""
|
|
689
|
-
if self._sess is None:
|
|
690
|
-
raise InterfaceError(errno=2048, msg='Connection is closed.')
|
|
691
|
-
return self._sess.post(urljoin(self._url, path), *args, **kwargs)
|
|
692
|
-
|
|
693
|
-
def close(self) -> None:
|
|
694
|
-
"""Close the connection."""
|
|
695
|
-
self._sess = None
|
|
696
|
-
|
|
697
|
-
def autocommit(self, value: bool) -> None:
|
|
698
|
-
"""Set autocommit mode."""
|
|
699
|
-
self._autocommit = value
|
|
700
|
-
|
|
701
|
-
def commit(self) -> None:
|
|
702
|
-
"""Commit the pending transaction."""
|
|
703
|
-
if self._autocommit:
|
|
704
|
-
return
|
|
705
|
-
raise NotSupportedError(msg='operation not supported')
|
|
706
|
-
|
|
707
|
-
def rollback(self) -> None:
|
|
708
|
-
"""Rollback the pending transaction."""
|
|
709
|
-
if self._autocommit:
|
|
710
|
-
return
|
|
711
|
-
raise NotSupportedError(msg='operation not supported')
|
|
712
|
-
|
|
713
|
-
def cursor(self) -> Cursor:
|
|
714
|
-
"""
|
|
715
|
-
Create a new cursor object.
|
|
716
|
-
|
|
717
|
-
Returns
|
|
718
|
-
-------
|
|
719
|
-
Cursor
|
|
720
|
-
|
|
721
|
-
"""
|
|
722
|
-
return Cursor(self)
|
|
723
|
-
|
|
724
|
-
def __enter__(self) -> Connection:
|
|
725
|
-
"""Enter a context."""
|
|
726
|
-
return self
|
|
727
|
-
|
|
728
|
-
def __exit__(
|
|
729
|
-
self, exc_type: Optional[object],
|
|
730
|
-
exc_value: Optional[Exception], exc_traceback: Optional[str],
|
|
731
|
-
) -> None:
|
|
732
|
-
"""Exit a context."""
|
|
733
|
-
self.close()
|
|
734
|
-
|
|
735
|
-
def is_connected(self) -> bool:
|
|
736
|
-
"""
|
|
737
|
-
Check if the database is still connected.
|
|
738
|
-
|
|
739
|
-
Returns
|
|
740
|
-
-------
|
|
741
|
-
bool
|
|
742
|
-
|
|
743
|
-
"""
|
|
744
|
-
if self._sess is None:
|
|
745
|
-
return False
|
|
746
|
-
url = '/'.join(self._url.split('/')[:3]) + '/ping'
|
|
747
|
-
res = self._sess.get(url)
|
|
748
|
-
if res.status_code <= 400 and res.text == 'pong':
|
|
749
|
-
return True
|
|
750
|
-
return False
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
def connect(
|
|
754
|
-
host: Optional[str] = None, port: Optional[int] = None,
|
|
755
|
-
user: Optional[str] = None, password: Optional[str] = None,
|
|
756
|
-
database: Optional[str] = None, protocol: str = 'https', version: str = 'v1',
|
|
757
|
-
ssl_key: Optional[str] = None, ssl_cert: Optional[str] = None,
|
|
758
|
-
ssl_ca: Optional[str] = None, ssl_disabled: Optional[bool] = None,
|
|
759
|
-
) -> Connection:
|
|
760
|
-
"""
|
|
761
|
-
Connect to a SingleStoreDB using HTTP.
|
|
762
|
-
|
|
763
|
-
Parameters
|
|
764
|
-
----------
|
|
765
|
-
user : str, optional
|
|
766
|
-
Database user name
|
|
767
|
-
password : str, optional
|
|
768
|
-
Database user password
|
|
769
|
-
host : str, optional
|
|
770
|
-
Database host name or IP address
|
|
771
|
-
port : int, optional
|
|
772
|
-
Database port. This defaults to 3306 for non-HTTP connections, 80
|
|
773
|
-
for HTTP connections, and 443 for HTTPS connections.
|
|
774
|
-
database : str, optional
|
|
775
|
-
Database name
|
|
776
|
-
protocol : str, optional
|
|
777
|
-
HTTP protocol: `http` or `https`
|
|
778
|
-
version : str, optional
|
|
779
|
-
Version of the HTTP API
|
|
780
|
-
ssl_key : str, optional
|
|
781
|
-
File containing SSL key
|
|
782
|
-
ssl_cert : str, optional
|
|
783
|
-
File containing SSL certificate
|
|
784
|
-
ssl_ca : str, optional
|
|
785
|
-
File containing SSL certificate authority
|
|
786
|
-
ssl_disabled : bool, optional
|
|
787
|
-
Disable SSL usage
|
|
788
|
-
|
|
789
|
-
Returns
|
|
790
|
-
-------
|
|
791
|
-
Connection
|
|
792
|
-
|
|
793
|
-
"""
|
|
794
|
-
return Connection(**dict(locals()))
|