singlestoredb 1.11.0__cp38-abi3-win_amd64.whl → 1.12.1__cp38-abi3-win_amd64.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.

@@ -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()),
@@ -986,11 +989,18 @@ class Connection(BaseConnection):
986
989
 
987
990
  def set_character_set(self, charset, collation=None):
988
991
  """
989
- Set charaset (and collation) on the server.
992
+ Set session charaset (and collation) on the server.
990
993
 
991
- Send "SET NAMES charset [COLLATE collation]" query.
994
+ Send "SET [COLLATION|CHARACTER_SET]_SERVER = [collation|charset]" query.
992
995
  Update Connection.encoding based on charset.
993
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
+
994
1004
  Parameters
995
1005
  ----------
996
1006
  charset : str
@@ -1003,9 +1013,9 @@ class Connection(BaseConnection):
1003
1013
  encoding = charset_by_name(charset).encoding
1004
1014
 
1005
1015
  if collation:
1006
- query = f'SET NAMES {charset} COLLATE {collation}'
1016
+ query = f'SET COLLATION_SERVER={collation}'
1007
1017
  else:
1008
- query = f'SET NAMES {charset}'
1018
+ query = f'SET CHARACTER_SET_SERVER={charset}'
1009
1019
  self._execute_command(COMMAND.COM_QUERY, query)
1010
1020
  self._read_packet()
1011
1021
  self.charset = charset
@@ -1109,19 +1119,6 @@ class Connection(BaseConnection):
1109
1119
  self._get_server_information()
1110
1120
  self._request_authentication()
1111
1121
 
1112
- # Send "SET NAMES" query on init for:
1113
- # - Ensure charaset (and collation) is set to the server.
1114
- # - collation_id in handshake packet may be ignored.
1115
- # - If collation is not specified, we don't know what is server's
1116
- # default collation for the charset. For example, default collation
1117
- # of utf8mb4 is:
1118
- # - MySQL 5.7, MariaDB 10.x: utf8mb4_general_ci
1119
- # - MySQL 8.0: utf8mb4_0900_ai_ci
1120
- #
1121
- # Reference:
1122
- # - https://github.com/PyMySQL/PyMySQL/issues/1092
1123
- # - https://github.com/wagtail/wagtail/issues/9477
1124
- # - https://zenn.dev/methane/articles/2023-mysql-collation (Japanese)
1125
1122
  self.set_character_set(self.charset, self.collation)
1126
1123
 
1127
1124
  if self.sql_mode is not None:
@@ -1847,7 +1844,7 @@ class MySQLResult:
1847
1844
 
1848
1845
  def _read_row_from_packet(self, packet):
1849
1846
  row = []
1850
- for encoding, converter in self.converters:
1847
+ for i, (encoding, converter) in enumerate(self.converters):
1851
1848
  try:
1852
1849
  data = packet.read_length_coded_string()
1853
1850
  except IndexError:
@@ -1856,7 +1853,15 @@ class MySQLResult:
1856
1853
  break
1857
1854
  if data is not None:
1858
1855
  if encoding is not None:
1859
- 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
+ )
1860
1865
  if DEBUG:
1861
1866
  print('DEBUG: DATA = ', data)
1862
1867
  if converter is not None:
File without changes
@@ -0,0 +1,455 @@
1
+ #!/usr/bin/env python
2
+ """Utilities for running singlestoredb-dev Docker image."""
3
+ from __future__ import annotations
4
+
5
+ import atexit
6
+ import os
7
+ import platform
8
+ import secrets
9
+ import signal
10
+ import socket
11
+ import subprocess
12
+ import time
13
+ import urllib.parse
14
+ from contextlib import closing
15
+ from types import TracebackType
16
+ from typing import Any
17
+ from typing import Dict
18
+ from typing import List
19
+ from typing import Optional
20
+ from typing import Type
21
+
22
+ try:
23
+ import docker
24
+ has_docker = True
25
+ except ImportError:
26
+ has_docker = False
27
+ raise RuntimeError('docker python package is not installed')
28
+
29
+ from .. import connect
30
+ from ..connection import Connection
31
+
32
+ try:
33
+ import pymongo
34
+ has_pymongo = True
35
+ except ImportError:
36
+ has_pymongo = False
37
+
38
+ DEFAULT_IMAGE = 'ghcr.io/singlestore-labs/singlestoredb-dev:latest'
39
+
40
+
41
+ class SingleStoreDB:
42
+ """
43
+ Manager for SingleStoreDB server running in Docker.
44
+
45
+ Parameters
46
+ -----------
47
+ name : str, optional
48
+ Name of the container.
49
+ password : str, optional
50
+ Password for the SingleStoreDB server.
51
+ license : str, optional
52
+ License key for SingleStoreDB.
53
+ enable_kai : bool, optional
54
+ Enable Kai (MongoDB) server.
55
+ server_port : int, optional
56
+ Port for the SingleStoreDB server.
57
+ studio_port : int, optional
58
+ Port for the SingleStoreDB Studio.
59
+ data_api_port : int, optional
60
+ Port for the SingleStoreDB Data API.
61
+ kai_port : int, optional
62
+ Port for the Kai server.
63
+ hostname : str, optional
64
+ Hostname for the container.
65
+ data_dir : str, optional
66
+ Path to the data directory.
67
+ logs_dir : str, optional
68
+ Path to the logs directory.
69
+ server_dir : str, optional
70
+ Path to the server directory.
71
+ global_vars : dict, optional
72
+ Global variables to set in the SingleStoreDB server.
73
+ init_sql : str, optional
74
+ Path to an SQL file to run on startup.
75
+ image : str, optional
76
+ Docker image to use.
77
+ database : str, optional
78
+ Default database to connect to.
79
+
80
+ """
81
+
82
+ user: str
83
+ password: str
84
+ kai_enabled: bool
85
+ server_port: int
86
+ studio_port: int
87
+ data_api_port: int
88
+ kai_port: Optional[int]
89
+ data_dir: Optional[str]
90
+ logs_dir: Optional[str]
91
+ server_dir: Optional[str]
92
+
93
+ def __init__(
94
+ self,
95
+ name: Optional[str] = None,
96
+ *,
97
+ password: Optional[str] = None,
98
+ license: Optional[str] = None,
99
+ enable_kai: bool = False,
100
+ server_port: Optional[int] = None,
101
+ studio_port: Optional[int] = None,
102
+ data_api_port: Optional[int] = None,
103
+ kai_port: Optional[int] = None,
104
+ hostname: Optional[str] = None,
105
+ data_dir: Optional[str] = None,
106
+ logs_dir: Optional[str] = None,
107
+ server_dir: Optional[str] = None,
108
+ global_vars: Optional[Dict[str, Any]] = None,
109
+ init_sql: Optional[str] = None,
110
+ image: str = DEFAULT_IMAGE,
111
+ database: Optional[str] = None,
112
+ ):
113
+ self.kai_enabled = enable_kai
114
+ self.kai_port = None
115
+ self.server_port = server_port or self._get_available_port()
116
+ self.studio_port = studio_port or self._get_available_port()
117
+ self.data_api_port = data_api_port or self._get_available_port()
118
+ self.data_dir = data_dir
119
+ self.logs_dir = logs_dir
120
+ self.server_dir = server_dir
121
+ self.user = 'root'
122
+
123
+ # Setup container ports
124
+ ports = {
125
+ '3306/tcp': self.server_port,
126
+ '8080/tcp': self.studio_port,
127
+ '9000/tcp': self.data_api_port,
128
+ }
129
+
130
+ if enable_kai:
131
+ self.kai_port = kai_port or self._get_available_port()
132
+ ports['27017/tcp'] = self.kai_port
133
+
134
+ # Setup password
135
+ self.password = password or secrets.token_urlsafe(10)
136
+
137
+ # Setup license value
138
+ if license is None:
139
+ try:
140
+ license = os.environ['SINGLESTORE_LICENSE']
141
+ except KeyError:
142
+ raise ValueError('a SingleStore license must be supplied')
143
+
144
+ # Setup environment variables for the container
145
+ env = {'ROOT_PASSWORD': self.password}
146
+
147
+ if license:
148
+ env['SINGLESTORE_LICENSE'] = license
149
+
150
+ if enable_kai:
151
+ env['ENABLE_KAI'] = '1'
152
+
153
+ # Construct Docker arguments
154
+ kwargs = {
155
+ 'environment': env,
156
+ 'ports': ports,
157
+ 'detach': True,
158
+ 'auto_remove': True,
159
+ 'remove': True,
160
+ }
161
+
162
+ if 'macOS' in platform.platform():
163
+ kwargs['platform'] = 'linux/amd64'
164
+
165
+ for pname, pvalue in [
166
+ ('name', name),
167
+ ('hostname', hostname),
168
+ ]:
169
+ if pvalue is not None:
170
+ kwargs[pname] = pvalue
171
+
172
+ # Setup volumes
173
+ volumes: Dict[str, Dict[str, str]] = {}
174
+ if data_dir:
175
+ volumes[data_dir] = {'bind': '/data', 'mode': 'rw'}
176
+ if logs_dir:
177
+ volumes[logs_dir] = {'bind': '/logs', 'mode': 'ro'}
178
+ if server_dir:
179
+ volumes[server_dir] = {'bind': '/server', 'mode': 'ro'}
180
+ if init_sql:
181
+ volumes[os.path.abspath(init_sql)] = {'bind': '/init.sql', 'mode': 'ro'}
182
+ if volumes:
183
+ kwargs['volumes'] = volumes
184
+
185
+ # Setup global vars
186
+ for k, v in (global_vars or {}).items():
187
+ env['SINGLESTORE_SET_GLOBAL_' + k.upper()] = str(v)
188
+
189
+ self._saved_server_urls: Dict[str, Optional[str]] = {}
190
+
191
+ docker_client = docker.from_env()
192
+ self.container = docker_client.containers.run(image, **kwargs)
193
+
194
+ # Make sure container gets cleaned up at exit
195
+ atexit.register(self.stop)
196
+ signal.signal(signal.SIGINT, self.stop)
197
+ signal.signal(signal.SIGTERM, self.stop)
198
+
199
+ if not self._wait_on_ready():
200
+ raise RuntimeError('server did not come up properly')
201
+
202
+ self._database = database
203
+ self._set_server_urls()
204
+
205
+ def __str__(self) -> str:
206
+ return f"SingleStoreDB('{self.connection_url}')"
207
+
208
+ def __repr__(self) -> str:
209
+ return str(self)
210
+
211
+ def _get_available_port(self) -> int:
212
+ with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
213
+ s.bind(('', 0))
214
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
215
+ return s.getsockname()[1]
216
+
217
+ def _set_server_urls(self) -> None:
218
+ self._saved_server_urls['DATABASE_URL'] = os.environ.get('DATABASE_URL')
219
+ os.environ['DATABASE_URL'] = self.connection_url
220
+ self._saved_server_urls['SINGLESTOREDB_URL'] = os.environ.get('SINGLESTOREDB_URL')
221
+ os.environ['SINGLESTOREDB_URL'] = self.connection_url
222
+
223
+ def _restore_server_urls(self) -> None:
224
+ try:
225
+ for k, v in self._saved_server_urls.items():
226
+ if v is None:
227
+ del os.environ[k]
228
+ else:
229
+ os.environ[k] = v
230
+ except KeyError:
231
+ pass
232
+
233
+ def _wait_on_ready(self) -> bool:
234
+ for i in range(80):
235
+ for line in self.logs():
236
+ if 'INFO: ' in line:
237
+ return True
238
+ time.sleep(3)
239
+ return False
240
+
241
+ def logs(self) -> List[str]:
242
+ return self.container.logs().decode('utf8').split('\n')
243
+
244
+ @property
245
+ def connection_url(self) -> str:
246
+ """Connection URL for the SingleStoreDB server."""
247
+ dbname = f'/{self._database}' if self._database else ''
248
+ password = urllib.parse.quote_plus(self.password)
249
+ return f'singlestoredb://{self.user}:{password}@' + \
250
+ f'localhost:{self.server_port}{dbname}'
251
+
252
+ @property
253
+ def http_connection_url(self) -> str:
254
+ """HTTP Connection URL for the SingleStoreDB server."""
255
+ dbname = f'/{self._database}' if self._database else ''
256
+ password = urllib.parse.quote_plus(self.password)
257
+ return f'singlestoredb+http://{self.user}:{password}@' + \
258
+ f'localhost:{self.data_api_port}{dbname}'
259
+
260
+ def connect(
261
+ self,
262
+ use_data_api: bool = False,
263
+ **kwargs: Any,
264
+ ) -> Connection:
265
+ """
266
+ Connect to the SingleStoreDB server.
267
+
268
+ Parameters
269
+ -----------
270
+ use_data_api : bool, optional
271
+ Use the Data API for the connection.
272
+ **kwargs : Any, optional
273
+ Additional keyword arguments to pass to the connection.
274
+
275
+ Returns
276
+ --------
277
+ Connection : Connection to the SingleStoreDB server.
278
+
279
+ """
280
+ if use_data_api:
281
+ return connect(self.http_connection_url, **kwargs)
282
+ return connect(self.connection_url, **kwargs)
283
+
284
+ @property
285
+ def kai_url(self) -> Optional[str]:
286
+ """Connection URL for the Kai (MongoDB) server."""
287
+ if not self.kai_enabled:
288
+ return None
289
+ password = urllib.parse.quote_plus(self.password)
290
+ return f'mongodb://{self.user}:{password}@' + \
291
+ f'localhost:{self.kai_port}/?authMechanism=PLAIN&loadBalanced=true'
292
+
293
+ def connect_kai(self) -> 'pymongo.MongoClient':
294
+ """Connect to the Kai (MongoDB) server."""
295
+ if not self.kai_enabled:
296
+ raise RuntimeError('kai is not enabled')
297
+ if not has_pymongo:
298
+ raise RuntimeError('pymongo is not installed')
299
+ return pymongo.MongoClient(self.kai_url)
300
+
301
+ @property
302
+ def studio_url(self) -> str:
303
+ """URL for the SingleStoreDB Studio."""
304
+ return f'http://localhost:{self.studio_port}'
305
+
306
+ def open_studio(self) -> None:
307
+ """Open the SingleStoreDB Studio in a web browser."""
308
+ import webbrowser
309
+ if platform.platform().lower().startswith('macos'):
310
+ chrome_path = r'open -a /Applications/Google\ Chrome.app %s'
311
+ webbrowser.get(chrome_path).open(self.studio_url, new=2)
312
+ else:
313
+ webbrowser.open(self.studio_url, new=2)
314
+
315
+ def open_shell(self) -> None:
316
+ """Open a shell in the SingleStoreDB server."""
317
+ if platform.platform().lower().startswith('macos'):
318
+ subprocess.call([
319
+ 'osascript', '-e',
320
+ 'tell app "Terminal" to do script '
321
+ f'"docker exec -it {self.container.id} singlestore-auth"',
322
+ ])
323
+ elif platform.platform().lower().startswith('linux'):
324
+ subprocess.call([
325
+ 'gnome-terminal', '--',
326
+ 'docker', 'exec', '-it', self.container.id, 'singlestore-auth',
327
+ ])
328
+ elif platform.platform().lower().startswith('windows'):
329
+ subprocess.call([
330
+ 'start', 'cmd', '/k'
331
+ 'docker', 'exec', '-it', self.container.id, 'singlestore-auth',
332
+ ])
333
+ else:
334
+ raise RuntimeError('unsupported platform')
335
+
336
+ def open_mongosh(self) -> None:
337
+ """Open a mongosh in the SingleStoreDB server."""
338
+ if not self.kai_enabled:
339
+ raise RuntimeError('kai interface is not enabled')
340
+ if platform.platform().lower().startswith('macos'):
341
+ subprocess.call([
342
+ 'osascript', '-e',
343
+ 'tell app "Terminal" to do script '
344
+ f'"docker exec -it {self.container.id} mongosh-auth"',
345
+ ])
346
+ elif platform.platform().lower().startswith('linux'):
347
+ subprocess.call([
348
+ 'gnome-terminal', '--',
349
+ 'docker', 'exec', '-it', self.container.id, 'mongosh-auth',
350
+ ])
351
+ elif platform.platform().lower().startswith('windows'):
352
+ subprocess.call([
353
+ 'start', 'cmd', '/k'
354
+ 'docker', 'exec', '-it', self.container.id, 'mongosh-auth',
355
+ ])
356
+ else:
357
+ raise RuntimeError('unsupported platform')
358
+
359
+ def __enter__(self) -> SingleStoreDB:
360
+ return self
361
+
362
+ def __exit__(
363
+ self,
364
+ exc_type: Optional[Type[BaseException]],
365
+ exc_val: Optional[BaseException],
366
+ exc_tb: Optional[TracebackType],
367
+ ) -> Optional[bool]:
368
+ self.stop()
369
+ return None
370
+
371
+ def stop(self, *args: Any) -> None:
372
+ """Stop the SingleStoreDB server."""
373
+ if self.container is not None:
374
+ self._restore_server_urls()
375
+ try:
376
+ self.container.stop()
377
+ finally:
378
+ self.container = None
379
+
380
+
381
+ def start(
382
+ name: Optional[str] = None,
383
+ password: Optional[str] = None,
384
+ license: Optional[str] = None,
385
+ enable_kai: bool = False,
386
+ server_port: Optional[int] = None,
387
+ studio_port: Optional[int] = None,
388
+ data_api_port: Optional[int] = None,
389
+ kai_port: Optional[int] = None,
390
+ hostname: Optional[str] = None,
391
+ data_dir: Optional[str] = None,
392
+ logs_dir: Optional[str] = None,
393
+ server_dir: Optional[str] = None,
394
+ global_vars: Optional[Dict[str, Any]] = None,
395
+ init_sql: Optional[str] = None,
396
+ image: str = DEFAULT_IMAGE,
397
+ database: Optional[str] = None,
398
+ ) -> SingleStoreDB:
399
+ """
400
+ Manager for SingleStoreDB server running in Docker.
401
+
402
+ Parameters
403
+ -----------
404
+ name : str, optional
405
+ Name of the container.
406
+ password : str, optional
407
+ Password for the SingleStoreDB server.
408
+ license : str, optional
409
+ License key for SingleStoreDB.
410
+ enable_kai : bool, optional
411
+ Enable Kai (MongoDB) server.
412
+ server_port : int, optional
413
+ Port for the SingleStoreDB server.
414
+ studio_port : int, optional
415
+ Port for the SingleStoreDB Studio.
416
+ data_api_port : int, optional
417
+ Port for the SingleStoreDB Data API.
418
+ kai_port : int, optional
419
+ Port for the Kai server.
420
+ hostname : str, optional
421
+ Hostname for the container.
422
+ data_dir : str, optional
423
+ Path to the data directory.
424
+ logs_dir : str, optional
425
+ Path to the logs directory.
426
+ server_dir : str, optional
427
+ Path to the server directory.
428
+ global_vars : dict, optional
429
+ Global variables to set in the SingleStoreDB server.
430
+ init_sql : str, optional
431
+ Path to an SQL file to run on startup.
432
+ image : str, optional
433
+ Docker image to use.
434
+ database : str, optional
435
+ Default database to connect to.
436
+
437
+ """
438
+ return SingleStoreDB(
439
+ name=name,
440
+ password=password,
441
+ license=license,
442
+ enable_kai=enable_kai,
443
+ server_port=server_port,
444
+ studio_port=studio_port,
445
+ data_api_port=data_api_port,
446
+ kai_port=kai_port,
447
+ hostname=hostname,
448
+ data_dir=data_dir,
449
+ logs_dir=logs_dir,
450
+ server_dir=server_dir,
451
+ global_vars=global_vars,
452
+ init_sql=init_sql,
453
+ image=image,
454
+ database=database,
455
+ )