singlestoredb 1.10.0__py3-none-any.whl → 1.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of singlestoredb might be problematic. Click here for more details.
- singlestoredb/__init__.py +1 -1
- singlestoredb/config.py +6 -0
- singlestoredb/connection.py +7 -0
- singlestoredb/converters.py +5 -5
- singlestoredb/functions/__init__.py +1 -0
- singlestoredb/functions/decorator.py +258 -69
- singlestoredb/functions/ext/asgi.py +121 -27
- singlestoredb/functions/signature.py +100 -9
- singlestoredb/fusion/handlers/export.py +58 -2
- singlestoredb/fusion/handlers/files.py +6 -6
- singlestoredb/fusion/handlers/models.py +250 -0
- singlestoredb/fusion/handlers/utils.py +5 -5
- singlestoredb/fusion/result.py +1 -1
- singlestoredb/http/connection.py +4 -0
- singlestoredb/management/export.py +30 -7
- singlestoredb/management/files.py +89 -26
- singlestoredb/mysql/connection.py +25 -19
- singlestoredb/server/__init__.py +0 -0
- singlestoredb/server/docker.py +455 -0
- singlestoredb/server/free_tier.py +267 -0
- singlestoredb/tests/test_udf.py +84 -32
- singlestoredb/utils/events.py +16 -0
- {singlestoredb-1.10.0.dist-info → singlestoredb-1.12.0.dist-info}/METADATA +3 -1
- {singlestoredb-1.10.0.dist-info → singlestoredb-1.12.0.dist-info}/RECORD +28 -24
- {singlestoredb-1.10.0.dist-info → singlestoredb-1.12.0.dist-info}/LICENSE +0 -0
- {singlestoredb-1.10.0.dist-info → singlestoredb-1.12.0.dist-info}/WHEEL +0 -0
- {singlestoredb-1.10.0.dist-info → singlestoredb-1.12.0.dist-info}/entry_points.txt +0 -0
- {singlestoredb-1.10.0.dist-info → singlestoredb-1.12.0.dist-info}/top_level.txt +0 -0
|
@@ -24,6 +24,9 @@ class ExportService(object):
|
|
|
24
24
|
catalog_info: Dict[str, Any]
|
|
25
25
|
storage_info: Dict[str, Any]
|
|
26
26
|
columns: Optional[List[str]]
|
|
27
|
+
partition_by: Optional[List[Dict[str, str]]]
|
|
28
|
+
order_by: Optional[List[Dict[str, Dict[str, str]]]]
|
|
29
|
+
properties: Optional[Dict[str, Any]]
|
|
27
30
|
|
|
28
31
|
def __init__(
|
|
29
32
|
self,
|
|
@@ -32,7 +35,10 @@ class ExportService(object):
|
|
|
32
35
|
table: str,
|
|
33
36
|
catalog_info: Union[str, Dict[str, Any]],
|
|
34
37
|
storage_info: Union[str, Dict[str, Any]],
|
|
35
|
-
columns: Optional[List[str]],
|
|
38
|
+
columns: Optional[List[str]] = None,
|
|
39
|
+
partition_by: Optional[List[Dict[str, str]]] = None,
|
|
40
|
+
order_by: Optional[List[Dict[str, Dict[str, str]]]] = None,
|
|
41
|
+
properties: Optional[Dict[str, Any]] = None,
|
|
36
42
|
):
|
|
37
43
|
#: Workspace group
|
|
38
44
|
self.workspace_group = workspace_group
|
|
@@ -58,6 +64,10 @@ class ExportService(object):
|
|
|
58
64
|
else:
|
|
59
65
|
self.storage_info = copy.copy(storage_info)
|
|
60
66
|
|
|
67
|
+
self.partition_by = partition_by or None
|
|
68
|
+
self.order_by = order_by or None
|
|
69
|
+
self.properties = properties or None
|
|
70
|
+
|
|
61
71
|
self._manager: Optional[WorkspaceManager] = workspace_group._manager
|
|
62
72
|
|
|
63
73
|
def __str__(self) -> str:
|
|
@@ -93,14 +103,27 @@ class ExportService(object):
|
|
|
93
103
|
msg='No workspace manager is associated with this object.',
|
|
94
104
|
)
|
|
95
105
|
|
|
106
|
+
partition_spec = None
|
|
107
|
+
if self.partition_by:
|
|
108
|
+
partition_spec = dict(partitions=self.partition_by)
|
|
109
|
+
|
|
110
|
+
sort_order_spec = None
|
|
111
|
+
if self.order_by:
|
|
112
|
+
sort_order_spec = dict(keys=self.order_by)
|
|
113
|
+
|
|
96
114
|
out = self._manager._post(
|
|
97
115
|
f'workspaceGroups/{self.workspace_group.id}/egress/startTableEgress',
|
|
98
|
-
json=
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
116
|
+
json={
|
|
117
|
+
k: v for k, v in dict(
|
|
118
|
+
databaseName=self.database,
|
|
119
|
+
tableName=self.table,
|
|
120
|
+
storageInfo=self.storage_info,
|
|
121
|
+
catalogInfo=self.catalog_info,
|
|
122
|
+
partitionSpec=partition_spec,
|
|
123
|
+
sortOrderSpec=sort_order_spec,
|
|
124
|
+
properties=self.properties,
|
|
125
|
+
).items() if v is not None
|
|
126
|
+
},
|
|
104
127
|
)
|
|
105
128
|
|
|
106
129
|
return ExportStatus(out.json()['egressID'], self.workspace_group)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import datetime
|
|
6
|
+
import glob
|
|
6
7
|
import io
|
|
7
8
|
import os
|
|
8
9
|
import re
|
|
@@ -23,9 +24,9 @@ from .utils import PathLike
|
|
|
23
24
|
from .utils import to_datetime
|
|
24
25
|
from .utils import vars_to_str
|
|
25
26
|
|
|
26
|
-
|
|
27
27
|
PERSONAL_SPACE = 'personal'
|
|
28
28
|
SHARED_SPACE = 'shared'
|
|
29
|
+
MODELS_SPACE = 'models'
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
class FilesObject(object):
|
|
@@ -35,8 +36,8 @@ class FilesObject(object):
|
|
|
35
36
|
It can belong to either a workspace stage or personal/shared space.
|
|
36
37
|
|
|
37
38
|
This object is not instantiated directly. It is used in the results
|
|
38
|
-
of various operations in ``WorkspaceGroup.stage``, ``FilesManager.personal_space
|
|
39
|
-
and ``FilesManager.
|
|
39
|
+
of various operations in ``WorkspaceGroup.stage``, ``FilesManager.personal_space``,
|
|
40
|
+
``FilesManager.shared_space`` and ``FilesManager.models_space`` methods.
|
|
40
41
|
|
|
41
42
|
"""
|
|
42
43
|
|
|
@@ -513,6 +514,11 @@ class FilesManager(Manager):
|
|
|
513
514
|
"""Return the shared file space."""
|
|
514
515
|
return FileSpace(SHARED_SPACE, self)
|
|
515
516
|
|
|
517
|
+
@property
|
|
518
|
+
def models_space(self) -> FileSpace:
|
|
519
|
+
"""Return the models file space."""
|
|
520
|
+
return FileSpace(MODELS_SPACE, self)
|
|
521
|
+
|
|
516
522
|
|
|
517
523
|
def manage_files(
|
|
518
524
|
access_token: Optional[str] = None,
|
|
@@ -551,7 +557,8 @@ class FileSpace(FileLocation):
|
|
|
551
557
|
FileSpace manager.
|
|
552
558
|
|
|
553
559
|
This object is not instantiated directly.
|
|
554
|
-
It is returned by ``FilesManager.personal_space
|
|
560
|
+
It is returned by ``FilesManager.personal_space``, ``FilesManager.shared_space``
|
|
561
|
+
or ``FileManger.models_space``.
|
|
555
562
|
|
|
556
563
|
"""
|
|
557
564
|
|
|
@@ -687,10 +694,36 @@ class FileSpace(FileLocation):
|
|
|
687
694
|
ignore all '*.pyc' files in the directory tree
|
|
688
695
|
|
|
689
696
|
"""
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
697
|
+
if not os.path.isdir(local_path):
|
|
698
|
+
raise NotADirectoryError(f'local path is not a directory: {local_path}')
|
|
699
|
+
|
|
700
|
+
if not path:
|
|
701
|
+
path = local_path
|
|
702
|
+
|
|
703
|
+
ignore_files = set()
|
|
704
|
+
if ignore:
|
|
705
|
+
if isinstance(ignore, list):
|
|
706
|
+
for item in ignore:
|
|
707
|
+
ignore_files.update(glob.glob(str(item), recursive=recursive))
|
|
708
|
+
else:
|
|
709
|
+
ignore_files.update(glob.glob(str(ignore), recursive=recursive))
|
|
710
|
+
|
|
711
|
+
for dir_path, _, files in os.walk(str(local_path)):
|
|
712
|
+
for fname in files:
|
|
713
|
+
if ignore_files and fname in ignore_files:
|
|
714
|
+
continue
|
|
715
|
+
|
|
716
|
+
local_file_path = os.path.join(dir_path, fname)
|
|
717
|
+
remote_path = os.path.join(
|
|
718
|
+
path,
|
|
719
|
+
local_file_path.lstrip(str(local_path)),
|
|
720
|
+
)
|
|
721
|
+
self.upload_file(
|
|
722
|
+
local_path=local_file_path,
|
|
723
|
+
path=remote_path,
|
|
724
|
+
overwrite=overwrite,
|
|
725
|
+
)
|
|
726
|
+
return self.info(path)
|
|
694
727
|
|
|
695
728
|
def _upload(
|
|
696
729
|
self,
|
|
@@ -875,15 +908,30 @@ class FileSpace(FileLocation):
|
|
|
875
908
|
return False
|
|
876
909
|
raise
|
|
877
910
|
|
|
878
|
-
def
|
|
911
|
+
def _listdir(self, path: PathLike, *, recursive: bool = False) -> List[str]:
|
|
879
912
|
"""
|
|
880
|
-
Return the names of files in
|
|
913
|
+
Return the names of files in a directory.
|
|
914
|
+
|
|
881
915
|
Parameters
|
|
882
916
|
----------
|
|
917
|
+
path : Path or str
|
|
918
|
+
Path to the folder
|
|
919
|
+
recursive : bool, optional
|
|
920
|
+
Should folders be listed recursively?
|
|
921
|
+
|
|
883
922
|
"""
|
|
884
923
|
res = self._manager._get(
|
|
885
|
-
f'files/fs/{self._location}',
|
|
924
|
+
f'files/fs/{self._location}/{path}',
|
|
886
925
|
).json()
|
|
926
|
+
|
|
927
|
+
if recursive:
|
|
928
|
+
out = []
|
|
929
|
+
for item in res['content'] or []:
|
|
930
|
+
out.append(item['path'])
|
|
931
|
+
if item['type'] == 'directory':
|
|
932
|
+
out.extend(self._listdir(item['path'], recursive=recursive))
|
|
933
|
+
return out
|
|
934
|
+
|
|
887
935
|
return [x['path'] for x in res['content'] or []]
|
|
888
936
|
|
|
889
937
|
def listdir(
|
|
@@ -905,13 +953,17 @@ class FileSpace(FileLocation):
|
|
|
905
953
|
List[str]
|
|
906
954
|
|
|
907
955
|
"""
|
|
908
|
-
|
|
909
|
-
|
|
956
|
+
path = re.sub(r'^(\./|/)+', r'', str(path))
|
|
957
|
+
path = re.sub(r'/+$', r'', path) + '/'
|
|
910
958
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
)
|
|
959
|
+
if not self.is_dir(path):
|
|
960
|
+
raise NotADirectoryError(f'path is not a directory: {path}')
|
|
961
|
+
|
|
962
|
+
out = self._listdir(path, recursive=recursive)
|
|
963
|
+
if path != '/':
|
|
964
|
+
path_n = len(path.split('/')) - 1
|
|
965
|
+
out = ['/'.join(x.split('/')[path_n:]) for x in out]
|
|
966
|
+
return out
|
|
915
967
|
|
|
916
968
|
def download_file(
|
|
917
969
|
self,
|
|
@@ -973,17 +1025,28 @@ class FileSpace(FileLocation):
|
|
|
973
1025
|
Parameters
|
|
974
1026
|
----------
|
|
975
1027
|
path : Path or str
|
|
976
|
-
|
|
1028
|
+
Directory path
|
|
977
1029
|
local_path : Path or str
|
|
978
1030
|
Path to local directory target location
|
|
979
1031
|
overwrite : bool, optional
|
|
980
1032
|
Should an existing directory / files be overwritten if they exist?
|
|
981
1033
|
|
|
982
1034
|
"""
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1035
|
+
|
|
1036
|
+
if local_path is not None and not overwrite and os.path.exists(local_path):
|
|
1037
|
+
raise OSError('target path already exists; use overwrite=True to replace')
|
|
1038
|
+
|
|
1039
|
+
if not self.is_dir(path):
|
|
1040
|
+
raise NotADirectoryError(f'path is not a directory: {path}')
|
|
1041
|
+
|
|
1042
|
+
files = self.listdir(path, recursive=True)
|
|
1043
|
+
for f in files:
|
|
1044
|
+
remote_path = os.path.join(path, f)
|
|
1045
|
+
if self.is_dir(remote_path):
|
|
1046
|
+
continue
|
|
1047
|
+
target = os.path.normpath(os.path.join(local_path, f))
|
|
1048
|
+
os.makedirs(os.path.dirname(target), exist_ok=True)
|
|
1049
|
+
self.download_file(remote_path, target, overwrite=overwrite)
|
|
987
1050
|
|
|
988
1051
|
def remove(self, path: PathLike) -> None:
|
|
989
1052
|
"""
|
|
@@ -1010,10 +1073,10 @@ class FileSpace(FileLocation):
|
|
|
1010
1073
|
Path to the file location
|
|
1011
1074
|
|
|
1012
1075
|
"""
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
)
|
|
1076
|
+
if not self.is_dir(path):
|
|
1077
|
+
raise NotADirectoryError('path is not a directory')
|
|
1078
|
+
|
|
1079
|
+
self._manager._delete(f'files/fs/{self._location}/{path}')
|
|
1017
1080
|
|
|
1018
1081
|
def rmdir(self, path: PathLike) -> None:
|
|
1019
1082
|
"""
|
|
@@ -625,6 +625,9 @@ class Connection(BaseConnection):
|
|
|
625
625
|
|
|
626
626
|
from .. import __version__ as VERSION_STRING
|
|
627
627
|
|
|
628
|
+
if 'SINGLESTOREDB_WORKLOAD_TYPE' in os.environ:
|
|
629
|
+
VERSION_STRING += '+' + os.environ['SINGLESTOREDB_WORKLOAD_TYPE']
|
|
630
|
+
|
|
628
631
|
self._connect_attrs = {
|
|
629
632
|
'_os': str(sys.platform),
|
|
630
633
|
'_pid': str(os.getpid()),
|
|
@@ -729,6 +732,7 @@ class Connection(BaseConnection):
|
|
|
729
732
|
return
|
|
730
733
|
if self._closed:
|
|
731
734
|
raise err.Error('Already closed')
|
|
735
|
+
events.unsubscribe(self._handle_event)
|
|
732
736
|
self._closed = True
|
|
733
737
|
if self._sock is None:
|
|
734
738
|
return
|
|
@@ -985,11 +989,18 @@ class Connection(BaseConnection):
|
|
|
985
989
|
|
|
986
990
|
def set_character_set(self, charset, collation=None):
|
|
987
991
|
"""
|
|
988
|
-
Set charaset (and collation) on the server.
|
|
992
|
+
Set session charaset (and collation) on the server.
|
|
989
993
|
|
|
990
|
-
Send "SET
|
|
994
|
+
Send "SET [COLLATION|CHARACTER_SET]_SERVER = [collation|charset]" query.
|
|
991
995
|
Update Connection.encoding based on charset.
|
|
992
996
|
|
|
997
|
+
If charset/collation are being set to utf8mb4, the corresponding global
|
|
998
|
+
variables (COLLATION_SERVER and CHARACTER_SET_SERVER) must be also set
|
|
999
|
+
to utf8mb4. This is true by default for SingleStore 8.7+. For previuous
|
|
1000
|
+
versions or non-default setting user must manully run the query
|
|
1001
|
+
`SET global collation_connection = utf8mb4_general_ci`
|
|
1002
|
+
replacing utf8mb4_general_ci with {collation}.
|
|
1003
|
+
|
|
993
1004
|
Parameters
|
|
994
1005
|
----------
|
|
995
1006
|
charset : str
|
|
@@ -1002,9 +1013,9 @@ class Connection(BaseConnection):
|
|
|
1002
1013
|
encoding = charset_by_name(charset).encoding
|
|
1003
1014
|
|
|
1004
1015
|
if collation:
|
|
1005
|
-
query = f'SET
|
|
1016
|
+
query = f'SET COLLATION_SERVER={collation}'
|
|
1006
1017
|
else:
|
|
1007
|
-
query = f'SET
|
|
1018
|
+
query = f'SET CHARACTER_SET_SERVER={charset}'
|
|
1008
1019
|
self._execute_command(COMMAND.COM_QUERY, query)
|
|
1009
1020
|
self._read_packet()
|
|
1010
1021
|
self.charset = charset
|
|
@@ -1108,19 +1119,6 @@ class Connection(BaseConnection):
|
|
|
1108
1119
|
self._get_server_information()
|
|
1109
1120
|
self._request_authentication()
|
|
1110
1121
|
|
|
1111
|
-
# Send "SET NAMES" query on init for:
|
|
1112
|
-
# - Ensure charaset (and collation) is set to the server.
|
|
1113
|
-
# - collation_id in handshake packet may be ignored.
|
|
1114
|
-
# - If collation is not specified, we don't know what is server's
|
|
1115
|
-
# default collation for the charset. For example, default collation
|
|
1116
|
-
# of utf8mb4 is:
|
|
1117
|
-
# - MySQL 5.7, MariaDB 10.x: utf8mb4_general_ci
|
|
1118
|
-
# - MySQL 8.0: utf8mb4_0900_ai_ci
|
|
1119
|
-
#
|
|
1120
|
-
# Reference:
|
|
1121
|
-
# - https://github.com/PyMySQL/PyMySQL/issues/1092
|
|
1122
|
-
# - https://github.com/wagtail/wagtail/issues/9477
|
|
1123
|
-
# - https://zenn.dev/methane/articles/2023-mysql-collation (Japanese)
|
|
1124
1122
|
self.set_character_set(self.charset, self.collation)
|
|
1125
1123
|
|
|
1126
1124
|
if self.sql_mode is not None:
|
|
@@ -1846,7 +1844,7 @@ class MySQLResult:
|
|
|
1846
1844
|
|
|
1847
1845
|
def _read_row_from_packet(self, packet):
|
|
1848
1846
|
row = []
|
|
1849
|
-
for encoding, converter in self.converters:
|
|
1847
|
+
for i, (encoding, converter) in enumerate(self.converters):
|
|
1850
1848
|
try:
|
|
1851
1849
|
data = packet.read_length_coded_string()
|
|
1852
1850
|
except IndexError:
|
|
@@ -1855,7 +1853,15 @@ class MySQLResult:
|
|
|
1855
1853
|
break
|
|
1856
1854
|
if data is not None:
|
|
1857
1855
|
if encoding is not None:
|
|
1858
|
-
|
|
1856
|
+
try:
|
|
1857
|
+
data = data.decode(encoding, errors=self.encoding_errors)
|
|
1858
|
+
except UnicodeDecodeError:
|
|
1859
|
+
raise UnicodeDecodeError(
|
|
1860
|
+
'failed to decode string value in column '
|
|
1861
|
+
f"'{self.fields[i].name}' using encoding '{encoding}'; " +
|
|
1862
|
+
"use the 'encoding_errors' option on the connection " +
|
|
1863
|
+
'to specify how to handle this error',
|
|
1864
|
+
)
|
|
1859
1865
|
if DEBUG:
|
|
1860
1866
|
print('DEBUG: DATA = ', data)
|
|
1861
1867
|
if converter is not None:
|
|
File without changes
|