singlestoredb 1.10.0__cp38-abi3-win32.whl → 1.12.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.

@@ -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=dict(
99
- databaseName=self.database,
100
- tableName=self.table,
101
- storageInfo=self.storage_info,
102
- catalogInfo=self.catalog_info,
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.shared_space`` methods.
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`` or ``FilesManager.shared_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
- raise ManagementError(
691
- msg='Operation not supported: directories are currently not allowed '
692
- 'in Files API',
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 _list_root_dir(self) -> List[str]:
911
+ def _listdir(self, path: PathLike, *, recursive: bool = False) -> List[str]:
879
912
  """
880
- Return the names of files in the root directory.
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
- if path == '' or path == '/':
909
- return self._list_root_dir()
956
+ path = re.sub(r'^(\./|/)+', r'', str(path))
957
+ path = re.sub(r'/+$', r'', path) + '/'
910
958
 
911
- raise ManagementError(
912
- msg='Operation not supported: directories are currently not allowed '
913
- 'in Files API',
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
- Path to the file
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
- raise ManagementError(
984
- msg='Operation not supported: directories are currently not allowed '
985
- 'in Files API',
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
- raise ManagementError(
1014
- msg='Operation not supported: directories are currently not allowed '
1015
- 'in Files API',
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 NAMES charset [COLLATE collation]" query.
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 NAMES {charset} COLLATE {collation}'
1016
+ query = f'SET COLLATION_SERVER={collation}'
1006
1017
  else:
1007
- query = f'SET NAMES {charset}'
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
- data = data.decode(encoding, errors=self.encoding_errors)
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