singlestoredb 1.1.0__cp38-abi3-win32.whl → 1.3.0__cp38-abi3-win32.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.

Files changed (32) hide show
  1. _singlestoredb_accel.pyd +0 -0
  2. singlestoredb/__init__.py +1 -1
  3. singlestoredb/config.py +6 -0
  4. singlestoredb/connection.py +23 -1
  5. singlestoredb/converters.py +390 -0
  6. singlestoredb/functions/ext/asgi.py +7 -1
  7. singlestoredb/fusion/handler.py +14 -8
  8. singlestoredb/fusion/handlers/stage.py +167 -84
  9. singlestoredb/fusion/handlers/workspace.py +250 -108
  10. singlestoredb/fusion/registry.py +27 -10
  11. singlestoredb/http/connection.py +18 -1
  12. singlestoredb/management/__init__.py +1 -0
  13. singlestoredb/management/organization.py +4 -0
  14. singlestoredb/management/utils.py +2 -2
  15. singlestoredb/management/workspace.py +79 -6
  16. singlestoredb/mysql/connection.py +92 -16
  17. singlestoredb/mysql/constants/EXTENDED_TYPE.py +3 -0
  18. singlestoredb/mysql/constants/FIELD_TYPE.py +16 -0
  19. singlestoredb/mysql/constants/VECTOR_TYPE.py +6 -0
  20. singlestoredb/mysql/cursors.py +13 -10
  21. singlestoredb/mysql/protocol.py +50 -1
  22. singlestoredb/notebook/__init__.py +15 -0
  23. singlestoredb/notebook/_objects.py +212 -0
  24. singlestoredb/tests/test.sql +49 -0
  25. singlestoredb/tests/test_connection.py +174 -0
  26. singlestoredb/utils/results.py +5 -1
  27. {singlestoredb-1.1.0.dist-info → singlestoredb-1.3.0.dist-info}/METADATA +1 -1
  28. {singlestoredb-1.1.0.dist-info → singlestoredb-1.3.0.dist-info}/RECORD +32 -28
  29. {singlestoredb-1.1.0.dist-info → singlestoredb-1.3.0.dist-info}/LICENSE +0 -0
  30. {singlestoredb-1.1.0.dist-info → singlestoredb-1.3.0.dist-info}/WHEEL +0 -0
  31. {singlestoredb-1.1.0.dist-info → singlestoredb-1.3.0.dist-info}/entry_points.txt +0 -0
  32. {singlestoredb-1.1.0.dist-info → singlestoredb-1.3.0.dist-info}/top_level.txt +0 -0
@@ -3,4 +3,5 @@ from .cluster import manage_cluster
3
3
  from .manager import get_organization
4
4
  from .manager import get_token
5
5
  from .workspace import get_secret
6
+ from .workspace import get_stage
6
7
  from .workspace import manage_workspaces
@@ -121,6 +121,10 @@ class Organization(object):
121
121
 
122
122
  """
123
123
 
124
+ id: str
125
+ name: str
126
+ firewall_ranges: List[str]
127
+
124
128
  def __init__(self, id: str, name: str, firewall_ranges: List[str]):
125
129
  """Use :attr:`WorkspaceManager.organization` instead."""
126
130
  #: Unique ID of the organization
@@ -294,7 +294,7 @@ def snake_to_camel_dict(
294
294
  return None
295
295
  out = {}
296
296
  for k, v in s.items():
297
- if isinstance(s, Mapping):
297
+ if isinstance(v, Mapping):
298
298
  out[str(snake_to_camel(k))] = snake_to_camel_dict(v, cap_first=cap_first)
299
299
  else:
300
300
  out[str(snake_to_camel(k))] = v
@@ -307,7 +307,7 @@ def camel_to_snake_dict(s: Optional[Mapping[str, Any]]) -> Optional[Dict[str, An
307
307
  return None
308
308
  out = {}
309
309
  for k, v in s.items():
310
- if isinstance(s, Mapping):
310
+ if isinstance(v, Mapping):
311
311
  out[str(camel_to_snake(k))] = camel_to_snake_dict(v)
312
312
  else:
313
313
  out[str(camel_to_snake(k))] = v
@@ -34,9 +34,53 @@ from .utils import ttl_property
34
34
  from .utils import vars_to_str
35
35
 
36
36
 
37
+ def get_organization() -> Organization:
38
+ """Get the organization."""
39
+ return manage_workspaces().organization
40
+
41
+
37
42
  def get_secret(name: str) -> str:
38
43
  """Get a secret from the organization."""
39
- return manage_workspaces().organization.get_secret(name).value
44
+ return get_organization().get_secret(name).value
45
+
46
+
47
+ def get_workspace_group(
48
+ workspace_group: Optional[Union[WorkspaceGroup, str]] = None,
49
+ ) -> WorkspaceGroup:
50
+ """Get the stage for the workspace group."""
51
+ if isinstance(workspace_group, WorkspaceGroup):
52
+ return workspace_group
53
+ elif workspace_group:
54
+ return manage_workspaces().workspace_groups[workspace_group]
55
+ elif 'SINGLESTOREDB_WORKSPACE_GROUP' in os.environ:
56
+ return manage_workspaces().workspace_groups[
57
+ os.environ['SINGLESTOREDB_WORKSPACE_GROUP']
58
+ ]
59
+ raise RuntimeError('no workspace group specified')
60
+
61
+
62
+ def get_stage(
63
+ workspace_group: Optional[Union[WorkspaceGroup, str]] = None,
64
+ ) -> Stage:
65
+ """Get the stage for the workspace group."""
66
+ return get_workspace_group(workspace_group).stage
67
+
68
+
69
+ def get_workspace(
70
+ workspace_group: Optional[Union[WorkspaceGroup, str]] = None,
71
+ workspace: Optional[Union[Workspace, str]] = None,
72
+ ) -> Workspace:
73
+ """Get the workspaces for a workspace_group."""
74
+ if isinstance(workspace, Workspace):
75
+ return workspace
76
+ wg = get_workspace_group(workspace_group)
77
+ if workspace:
78
+ return wg.workspaces[workspace]
79
+ elif 'SINGLESTOREDB_WORKSPACE' in os.environ:
80
+ return wg.workspaces[
81
+ os.environ['SINGLESTOREDB_WORKSPACE']
82
+ ]
83
+ raise RuntimeError('no workspace group specified')
40
84
 
41
85
 
42
86
  class StageObject(object):
@@ -926,6 +970,21 @@ class Workspace(object):
926
970
 
927
971
  """
928
972
 
973
+ name: str
974
+ id: str
975
+ group_id: str
976
+ size: str
977
+ state: str
978
+ created_at: Optional[datetime.datetime]
979
+ terminated_at: Optional[datetime.datetime]
980
+ endpoint: Optional[str]
981
+ auto_suspend: Optional[Dict[str, Any]]
982
+ cache_config: Optional[int]
983
+ deployment_type: Optional[str]
984
+ resume_attachments: Optional[List[Dict[str, Any]]]
985
+ scaling_progress: Optional[int]
986
+ last_resumed_at: Optional[datetime.datetime]
987
+
929
988
  def __init__(
930
989
  self,
931
990
  name: str,
@@ -939,9 +998,9 @@ class Workspace(object):
939
998
  auto_suspend: Optional[Dict[str, Any]] = None,
940
999
  cache_config: Optional[int] = None,
941
1000
  deployment_type: Optional[str] = None,
942
- resume_attachments: Optional[Dict[str, Any]] = None,
1001
+ resume_attachments: Optional[List[Dict[str, Any]]] = None,
943
1002
  scaling_progress: Optional[int] = None,
944
- last_resumed_at: Optional[str] = None,
1003
+ last_resumed_at: Optional[Union[str, datetime.datetime]] = None,
945
1004
  ):
946
1005
  #: Name of the workspace
947
1006
  self.name = name
@@ -981,7 +1040,11 @@ class Workspace(object):
981
1040
  self.deployment_type = deployment_type
982
1041
 
983
1042
  #: Database attachments
984
- self.resume_attachments = camel_to_snake_dict(resume_attachments)
1043
+ self.resume_attachments = [
1044
+ camel_to_snake_dict(x) # type: ignore
1045
+ for x in resume_attachments or []
1046
+ if x is not None
1047
+ ]
985
1048
 
986
1049
  #: Current progress percentage for scaling the workspace
987
1050
  self.scaling_progress = scaling_progress
@@ -1248,8 +1311,18 @@ class WorkspaceGroup(object):
1248
1311
 
1249
1312
  """
1250
1313
 
1314
+ name: str
1315
+ id: str
1316
+ created_at: Optional[datetime.datetime]
1317
+ region: Optional[Region]
1318
+ firewall_ranges: List[str]
1319
+ terminated_at: Optional[datetime.datetime]
1320
+ allow_all_traffic: bool
1321
+
1251
1322
  def __init__(
1252
- self, name: str, id: str,
1323
+ self,
1324
+ name: str,
1325
+ id: str,
1253
1326
  created_at: Union[str, datetime.datetime],
1254
1327
  region: Optional[Region],
1255
1328
  firewall_ranges: List[str],
@@ -1275,7 +1348,7 @@ class WorkspaceGroup(object):
1275
1348
  self.terminated_at = to_datetime(terminated_at)
1276
1349
 
1277
1350
  #: Should all traffic be allowed?
1278
- self.allow_all_traffic = allow_all_traffic
1351
+ self.allow_all_traffic = allow_all_traffic or False
1279
1352
 
1280
1353
  self._manager: Optional[WorkspaceManager] = None
1281
1354
 
@@ -5,12 +5,15 @@
5
5
  # https://dev.mysql.com/doc/refman/5.5/en/error-handling.html
6
6
  import errno
7
7
  import functools
8
+ import io
8
9
  import os
10
+ import queue
9
11
  import socket
10
12
  import struct
11
13
  import sys
12
14
  import traceback
13
15
  import warnings
16
+ from typing import Iterable
14
17
 
15
18
  try:
16
19
  import _singlestoredb_accel
@@ -259,6 +262,8 @@ class Connection(BaseConnection):
259
262
  uploading data?
260
263
  track_env : bool, optional
261
264
  Should the connection track the SINGLESTOREDB_URL environment variable?
265
+ enable_extended_data_types : bool, optional
266
+ Should extended data types (BSON, vector) be enabled?
262
267
 
263
268
  See `Connection <https://www.python.org/dev/peps/pep-0249/#connection-objects>`_
264
269
  in the specification.
@@ -328,6 +333,7 @@ class Connection(BaseConnection):
328
333
  inf_as_null=None,
329
334
  encoding_errors='strict',
330
335
  track_env=False,
336
+ enable_extended_data_types=True,
331
337
  ):
332
338
  BaseConnection.__init__(**dict(locals()))
333
339
 
@@ -350,6 +356,7 @@ class Connection(BaseConnection):
350
356
  )
351
357
 
352
358
  self._local_infile = bool(local_infile)
359
+ self._local_infile_stream = None
353
360
  if self._local_infile:
354
361
  client_flag |= CLIENT.LOCAL_FILES
355
362
  if multi_statements:
@@ -609,6 +616,7 @@ class Connection(BaseConnection):
609
616
 
610
617
  self._in_sync = False
611
618
  self._track_env = bool(track_env) or self.host == 'singlestore.com'
619
+ self._enable_extended_data_types = enable_extended_data_types
612
620
 
613
621
  if defer_connect or self._track_env:
614
622
  self._sock = None
@@ -839,7 +847,7 @@ class Connection(BaseConnection):
839
847
  return self.cursorclass(self)
840
848
 
841
849
  # The following methods are INTERNAL USE ONLY (called from Cursor)
842
- def query(self, sql, unbuffered=False):
850
+ def query(self, sql, unbuffered=False, infile_stream=None):
843
851
  """
844
852
  Run a query on the server.
845
853
 
@@ -855,8 +863,10 @@ class Connection(BaseConnection):
855
863
  else:
856
864
  if isinstance(sql, str):
857
865
  sql = sql.encode(self.encoding, 'surrogateescape')
866
+ self._local_infile_stream = infile_stream
858
867
  self._execute_command(COMMAND.COM_QUERY, sql)
859
868
  self._affected_rows = self._read_query_result(unbuffered=unbuffered)
869
+ self._local_infile_stream = None
860
870
  return self._affected_rows
861
871
 
862
872
  def next_result(self, unbuffered=False):
@@ -1070,6 +1080,14 @@ class Connection(BaseConnection):
1070
1080
  c.execute('SET sql_mode=%s', (self.sql_mode,))
1071
1081
  c.close()
1072
1082
 
1083
+ if self._enable_extended_data_types:
1084
+ c = self.cursor()
1085
+ try:
1086
+ c.execute('SET @@SESSION.enable_extended_types_metadata=on')
1087
+ except self.OperationalError:
1088
+ pass
1089
+ c.close()
1090
+
1073
1091
  if self.init_command is not None:
1074
1092
  c = self.cursor()
1075
1093
  c.execute(self.init_command)
@@ -1859,24 +1877,82 @@ class LoadLocalFile:
1859
1877
  def send_data(self):
1860
1878
  """Send data packets from the local file to the server"""
1861
1879
  if not self.connection._sock:
1862
- raise err.InterfaceError(0, '')
1880
+ raise err.InterfaceError(0, 'Connection is closed')
1881
+
1863
1882
  conn = self.connection
1883
+ infile = conn._local_infile_stream
1884
+
1885
+ # 16KB is efficient enough
1886
+ packet_size = min(conn.max_allowed_packet, 16 * 1024)
1864
1887
 
1865
1888
  try:
1866
- with open(self.filename, 'rb') as open_file:
1867
- packet_size = min(
1868
- conn.max_allowed_packet, 16 * 1024,
1869
- ) # 16KB is efficient enough
1870
- while True:
1871
- chunk = open_file.read(packet_size)
1872
- if not chunk:
1873
- break
1874
- conn.write_packet(chunk)
1875
- except OSError:
1876
- raise err.OperationalError(
1877
- ER.FILE_NOT_FOUND,
1878
- f"Can't find file '{self.filename}'",
1879
- )
1889
+
1890
+ if self.filename in [':stream:', b':stream:']:
1891
+
1892
+ if infile is None:
1893
+ raise err.OperationalError(
1894
+ ER.FILE_NOT_FOUND,
1895
+ ':stream: specified for LOCAL INFILE, but no stream was supplied',
1896
+ )
1897
+
1898
+ # Binary IO
1899
+ elif isinstance(infile, io.RawIOBase):
1900
+ while True:
1901
+ chunk = infile.read(packet_size)
1902
+ if not chunk:
1903
+ break
1904
+ conn.write_packet(chunk)
1905
+
1906
+ # Text IO
1907
+ elif isinstance(infile, io.TextIOBase):
1908
+ while True:
1909
+ chunk = infile.read(packet_size)
1910
+ if not chunk:
1911
+ break
1912
+ conn.write_packet(chunk.encode('utf8'))
1913
+
1914
+ # Iterable of bytes or str
1915
+ elif isinstance(infile, Iterable):
1916
+ for chunk in infile:
1917
+ if not chunk:
1918
+ continue
1919
+ if isinstance(chunk, str):
1920
+ conn.write_packet(chunk.encode('utf8'))
1921
+ else:
1922
+ conn.write_packet(chunk)
1923
+
1924
+ # Queue (empty value ends the iteration)
1925
+ elif isinstance(infile, queue.Queue):
1926
+ while True:
1927
+ chunk = infile.get()
1928
+ if not chunk:
1929
+ break
1930
+ if isinstance(chunk, str):
1931
+ conn.write_packet(chunk.encode('utf8'))
1932
+ else:
1933
+ conn.write_packet(chunk)
1934
+
1935
+ else:
1936
+ raise err.OperationalError(
1937
+ ER.FILE_NOT_FOUND,
1938
+ ':stream: specified for LOCAL INFILE, ' +
1939
+ f'but stream type is unrecognized: {infile}',
1940
+ )
1941
+
1942
+ else:
1943
+ try:
1944
+ with open(self.filename, 'rb') as open_file:
1945
+ while True:
1946
+ chunk = open_file.read(packet_size)
1947
+ if not chunk:
1948
+ break
1949
+ conn.write_packet(chunk)
1950
+ except OSError:
1951
+ raise err.OperationalError(
1952
+ ER.FILE_NOT_FOUND,
1953
+ f"Can't find file '{self.filename!s}'",
1954
+ )
1955
+
1880
1956
  finally:
1881
1957
  if not conn._closed:
1882
1958
  # send the empty packet to signify we are done sending data
@@ -0,0 +1,3 @@
1
+ NONE = 0
2
+ BSON = 1
3
+ VECTOR = 2
@@ -30,3 +30,19 @@ GEOMETRY = 255
30
30
  CHAR = TINY
31
31
  INTERVAL = ENUM
32
32
  BOOL = TINY
33
+
34
+ # SingleStoreDB-specific.
35
+ # Only enabled when enable_extended_types_metadata=1 in the server.
36
+ BSON = 1001
37
+ FLOAT32_VECTOR_JSON = 2001
38
+ FLOAT64_VECTOR_JSON = 2002
39
+ INT8_VECTOR_JSON = 2003
40
+ INT16_VECTOR_JSON = 2004
41
+ INT32_VECTOR_JSON = 2005
42
+ INT64_VECTOR_JSON = 2006
43
+ FLOAT32_VECTOR = 3001
44
+ FLOAT64_VECTOR = 3002
45
+ INT8_VECTOR = 3003
46
+ INT16_VECTOR = 3004
47
+ INT32_VECTOR = 3005
48
+ INT64_VECTOR = 3006
@@ -0,0 +1,6 @@
1
+ FLOAT32 = 1
2
+ FLOAT64 = 2
3
+ INT8 = 3
4
+ INT16 = 4
5
+ INT32 = 5
6
+ INT64 = 6
@@ -178,7 +178,7 @@ class Cursor(BaseCursor):
178
178
 
179
179
  return query
180
180
 
181
- def execute(self, query, args=None):
181
+ def execute(self, query, args=None, infile_stream=None):
182
182
  """
183
183
  Execute a query.
184
184
 
@@ -192,6 +192,8 @@ class Cursor(BaseCursor):
192
192
  Query to execute.
193
193
  args : Sequence[Any] or Dict[str, Any] or Any, optional
194
194
  Parameters used with query. (optional)
195
+ infile_stream : io.BytesIO or Iterator[bytes], optional
196
+ Data stream for ``LOCAL INFILE`` statements
195
197
 
196
198
  Returns
197
199
  -------
@@ -205,7 +207,7 @@ class Cursor(BaseCursor):
205
207
 
206
208
  query = self.mogrify(query, args)
207
209
 
208
- result = self._query(query)
210
+ result = self._query(query, infile_stream=infile_stream)
209
211
  self._executed = query
210
212
  return result
211
213
 
@@ -387,10 +389,10 @@ class Cursor(BaseCursor):
387
389
  raise IndexError('out of range')
388
390
  self._rownumber = r
389
391
 
390
- def _query(self, q):
392
+ def _query(self, q, infile_stream=None):
391
393
  conn = self._get_db()
392
394
  self._clear_result()
393
- conn.query(q)
395
+ conn.query(q, infile_stream=infile_stream)
394
396
  self._do_get_result()
395
397
  return self.rowcount
396
398
 
@@ -417,10 +419,11 @@ class Cursor(BaseCursor):
417
419
  if self.rowcount == 18446744073709551615:
418
420
  self.rowcount = -1
419
421
  self._description = result.description
420
- self._format_schema = get_schema(
421
- self.connection._results_type,
422
- result.description,
423
- )
422
+ if self._description:
423
+ self._format_schema = get_schema(
424
+ self.connection._results_type,
425
+ result.description,
426
+ )
424
427
  self.lastrowid = result.insert_id
425
428
  self._rows = result.rows
426
429
 
@@ -679,10 +682,10 @@ class SSCursor(Cursor):
679
682
 
680
683
  __del__ = close
681
684
 
682
- def _query(self, q):
685
+ def _query(self, q, infile_stream=None):
683
686
  conn = self._get_db()
684
687
  self._clear_result()
685
- conn.query(q, unbuffered=True)
688
+ conn.query(q, unbuffered=True, infile_stream=infile_stream)
686
689
  self._do_get_result()
687
690
  return self.rowcount
688
691
 
@@ -8,8 +8,10 @@ from . import err
8
8
  from ..config import get_option
9
9
  from ..utils.results import Description
10
10
  from .charset import MBLENGTH
11
+ from .constants import EXTENDED_TYPE
11
12
  from .constants import FIELD_TYPE
12
13
  from .constants import SERVER_STATUS
14
+ from .constants import VECTOR_TYPE
13
15
 
14
16
 
15
17
  DEBUG = get_option('debug.connection')
@@ -264,16 +266,63 @@ class FieldDescriptorPacket(MysqlPacket):
264
266
  self.org_table = self.read_length_coded_string().decode(encoding)
265
267
  self.name = self.read_length_coded_string().decode(encoding)
266
268
  self.org_name = self.read_length_coded_string().decode(encoding)
269
+ n_bytes = 0
267
270
  (
271
+ n_bytes,
268
272
  self.charsetnr,
269
273
  self.length,
270
274
  self.type_code,
271
275
  self.flags,
272
276
  self.scale,
273
- ) = self.read_struct('<xHIBHBxx')
277
+ ) = self.read_struct('<BHIBHBxx')
278
+
274
279
  # 'default' is a length coded binary and is still in the buffer?
275
280
  # not used for normal result sets...
276
281
 
282
+ # Extended types
283
+ if n_bytes > 12:
284
+ ext_type_code = self.read_uint8()
285
+ if ext_type_code == EXTENDED_TYPE.NONE:
286
+ pass
287
+ elif ext_type_code == EXTENDED_TYPE.BSON:
288
+ self.type_code = FIELD_TYPE.BSON
289
+ elif ext_type_code == EXTENDED_TYPE.VECTOR:
290
+ (self.length, vec_type) = self.read_struct('<IB')
291
+ if vec_type == VECTOR_TYPE.FLOAT32:
292
+ if self.charsetnr == 63:
293
+ self.type_code = FIELD_TYPE.FLOAT32_VECTOR
294
+ else:
295
+ self.type_code = FIELD_TYPE.FLOAT32_VECTOR_JSON
296
+ elif vec_type == VECTOR_TYPE.FLOAT64:
297
+ if self.charsetnr == 63:
298
+ self.type_code = FIELD_TYPE.FLOAT64_VECTOR
299
+ else:
300
+ self.type_code = FIELD_TYPE.FLOAT64_VECTOR_JSON
301
+ elif vec_type == VECTOR_TYPE.INT8:
302
+ if self.charsetnr == 63:
303
+ self.type_code = FIELD_TYPE.INT8_VECTOR
304
+ else:
305
+ self.type_code = FIELD_TYPE.INT8_VECTOR_JSON
306
+ elif vec_type == VECTOR_TYPE.INT16:
307
+ if self.charsetnr == 63:
308
+ self.type_code = FIELD_TYPE.INT16_VECTOR
309
+ else:
310
+ self.type_code = FIELD_TYPE.INT16_VECTOR_JSON
311
+ elif vec_type == VECTOR_TYPE.INT32:
312
+ if self.charsetnr == 63:
313
+ self.type_code = FIELD_TYPE.INT32_VECTOR
314
+ else:
315
+ self.type_code = FIELD_TYPE.INT32_VECTOR_JSON
316
+ elif vec_type == VECTOR_TYPE.INT64:
317
+ if self.charsetnr == 63:
318
+ self.type_code = FIELD_TYPE.INT64_VECTOR
319
+ else:
320
+ self.type_code = FIELD_TYPE.INT64_VECTOR_JSON
321
+ else:
322
+ raise TypeError(f'unrecognized vector data type: {vec_type}')
323
+ else:
324
+ raise TypeError(f'unrecognized extended data type: {ext_type_code}')
325
+
277
326
  def description(self):
278
327
  """Provides a 7-item tuple compatible with the Python PEP249 DB Spec."""
279
328
  return Description(
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env python
2
+ import os as _os
3
+ import warnings as _warnings
4
+
5
+ from ._objects import organization # noqa: F401
6
+ from ._objects import secrets # noqa: F401
7
+ from ._objects import stage # noqa: F401
8
+ from ._objects import workspace # noqa: F401
9
+ from ._objects import workspace_group # noqa: F401
10
+
11
+ if 'SINGLESTOREDB_ORGANIZATION' not in _os.environ:
12
+ _warnings.warn(
13
+ 'This package is intended for use in the SingleStoreDB notebook environment',
14
+ RuntimeWarning,
15
+ )