singlestoredb 0.9.0__cp36-abi3-win32.whl → 0.9.2__cp36-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.

@@ -12,6 +12,7 @@ from .. import connection
12
12
  from ..exceptions import ManagementError
13
13
  from .manager import Manager
14
14
  from .region import Region
15
+ from .utils import NamedList
15
16
  from .utils import to_datetime
16
17
  from .utils import vars_to_str
17
18
 
@@ -337,16 +338,16 @@ class ClusterManager(Manager):
337
338
  obj_type = 'cluster'
338
339
 
339
340
  @property
340
- def clusters(self) -> List[Cluster]:
341
+ def clusters(self) -> NamedList[Cluster]:
341
342
  """Return a list of available clusters."""
342
343
  res = self._get('clusters')
343
- return [Cluster.from_dict(item, self) for item in res.json()]
344
+ return NamedList([Cluster.from_dict(item, self) for item in res.json()])
344
345
 
345
346
  @property
346
- def regions(self) -> List[Region]:
347
+ def regions(self) -> NamedList[Region]:
347
348
  """Return a list of available regions."""
348
349
  res = self._get('regions')
349
- return [Region.from_dict(item, self) for item in res.json()]
350
+ return NamedList([Region.from_dict(item, self) for item in res.json()])
350
351
 
351
352
  def create_cluster(
352
353
  self, name: str, region: Union[str, Region], admin_password: str,
@@ -8,6 +8,8 @@ from typing import Any
8
8
  from typing import Dict
9
9
  from typing import List
10
10
  from typing import Optional
11
+ from typing import SupportsIndex
12
+ from typing import TypeVar
11
13
  from typing import Union
12
14
 
13
15
  from .. import converters
@@ -15,6 +17,7 @@ from .. import converters
15
17
  JSON = Union[str, List[str], Dict[str, 'JSON']]
16
18
  JSONObj = Dict[str, JSON]
17
19
  JSONList = List[JSON]
20
+ T = TypeVar('T')
18
21
 
19
22
  if sys.version_info < (3, 10):
20
23
  PathLike = Union[str, os.PathLike]
@@ -24,6 +27,49 @@ else:
24
27
  PathLikeABC = os.PathLike[str]
25
28
 
26
29
 
30
+ class NamedList(List[T]):
31
+ """List class which also allows selection by ``name`` and ``id`` attribute."""
32
+
33
+ def _find_item(self, key: str) -> T:
34
+ for item in self:
35
+ if getattr(item, 'name', '') == key:
36
+ return item
37
+ if getattr(item, 'id', '') == key:
38
+ return item
39
+ raise KeyError(key)
40
+
41
+ def __getitem__(self, key: Union[SupportsIndex, slice, str]) -> Any:
42
+ if isinstance(key, str):
43
+ return self._find_item(key)
44
+ return super().__getitem__(key)
45
+
46
+ def __contains__(self, key: Any) -> bool:
47
+ if isinstance(key, str):
48
+ try:
49
+ self._find_item(key)
50
+ return True
51
+ except KeyError:
52
+ return False
53
+ return super().__contains__(key)
54
+
55
+ def names(self) -> List[str]:
56
+ """Return ``name`` attribute of each item."""
57
+ return [y for y in [getattr(x, 'name', None) for x in self] if y is not None]
58
+
59
+ def ids(self) -> List[str]:
60
+ """Return ``id`` attribute of each item."""
61
+ return [y for y in [getattr(x, 'id', None) for x in self] if y is not None]
62
+
63
+ def get(self, name_or_id: str, *default: Any) -> Any:
64
+ """Return object with name / ID if it exists, or return default value."""
65
+ try:
66
+ return self._find_item(name_or_id)
67
+ except KeyError:
68
+ if default:
69
+ return default[0]
70
+ raise
71
+
72
+
27
73
  def enable_http_tracing() -> None:
28
74
  """Enable tracing of HTTP requests."""
29
75
  import logging
@@ -23,6 +23,7 @@ from .manager import Manager
23
23
  from .organization import Organization
24
24
  from .region import Region
25
25
  from .utils import from_datetime
26
+ from .utils import NamedList
26
27
  from .utils import PathLike
27
28
  from .utils import snake_to_camel
28
29
  from .utils import to_datetime
@@ -126,6 +127,47 @@ class StagesObject(object):
126
127
  """Return string representation."""
127
128
  return str(self)
128
129
 
130
+ def open(
131
+ self,
132
+ mode: str = 'r',
133
+ encoding: Optional[str] = None,
134
+ ) -> Union[io.StringIO, io.BytesIO]:
135
+ """
136
+ Open a Stage path for reading or writing.
137
+
138
+ Parameters
139
+ ----------
140
+ mode : str, optional
141
+ The read / write mode. The following modes are supported:
142
+ * 'r' open for reading (default)
143
+ * 'w' open for writing, truncating the file first
144
+ * 'x' create a new file and open it for writing
145
+ The data type can be specified by adding one of the following:
146
+ * 'b' binary mode
147
+ * 't' text mode (default)
148
+ encoding : str, optional
149
+ The string encoding to use for text
150
+
151
+ Returns
152
+ -------
153
+ StagesObjectBytesReader - 'rb' or 'b' mode
154
+ StagesObjectBytesWriter - 'wb' or 'xb' mode
155
+ StagesObjectTextReader - 'r' or 'rt' mode
156
+ StagesObjectTextWriter - 'w', 'x', 'wt' or 'xt' mode
157
+
158
+ """
159
+ if self._stages is None:
160
+ raise ManagementError(
161
+ msg='No Stages object is associated with this object.',
162
+ )
163
+
164
+ if self.is_dir():
165
+ raise IsADirectoryError(
166
+ f'directories can not be read or written: {self.path}',
167
+ )
168
+
169
+ return self._stages.open(self.path, mode=mode, encoding=encoding)
170
+
129
171
  def download(
130
172
  self,
131
173
  local_path: Optional[PathLike] = None,
@@ -256,7 +298,6 @@ class StagesObjectTextWriter(io.StringIO):
256
298
 
257
299
  def close(self) -> None:
258
300
  """Write the content to the stage path."""
259
- print('CLOSING')
260
301
  self._stages._upload(self.getvalue(), self._stage_path)
261
302
  super().close()
262
303
 
@@ -653,13 +694,18 @@ class Stages(object):
653
694
  return out
654
695
  return [x['path'] for x in res['content'] or []]
655
696
 
656
- def listdir(self, stage_path: PathLike, *, recursive: bool = False) -> List[str]:
697
+ def listdir(
698
+ self,
699
+ stage_path: PathLike = '/',
700
+ *,
701
+ recursive: bool = False,
702
+ ) -> List[str]:
657
703
  """
658
704
  List the files / folders at the given path.
659
705
 
660
706
  Parameters
661
707
  ----------
662
- stage_path : Path or str
708
+ stage_path : Path or str, optional
663
709
  Path to the stage location
664
710
 
665
711
  Returns
@@ -1163,14 +1209,16 @@ class WorkspaceGroup(object):
1163
1209
  )
1164
1210
 
1165
1211
  @property
1166
- def workspaces(self) -> List[Workspace]:
1212
+ def workspaces(self) -> NamedList[Workspace]:
1167
1213
  """Return a list of available workspaces."""
1168
1214
  if self._manager is None:
1169
1215
  raise ManagementError(
1170
1216
  msg='No workspace manager is associated with this object.',
1171
1217
  )
1172
1218
  res = self._manager._get('workspaces', params=dict(workspaceGroupID=self.id))
1173
- return [Workspace.from_dict(item, self._manager) for item in res.json()]
1219
+ return NamedList(
1220
+ [Workspace.from_dict(item, self._manager) for item in res.json()],
1221
+ )
1174
1222
 
1175
1223
 
1176
1224
  class Billing(object):
@@ -1275,10 +1323,10 @@ class WorkspaceManager(Manager):
1275
1323
  obj_type = 'workspace'
1276
1324
 
1277
1325
  @ property
1278
- def workspace_groups(self) -> List[WorkspaceGroup]:
1326
+ def workspace_groups(self) -> NamedList[WorkspaceGroup]:
1279
1327
  """Return a list of available workspace groups."""
1280
1328
  res = self._get('workspaceGroups')
1281
- return [WorkspaceGroup.from_dict(item, self) for item in res.json()]
1329
+ return NamedList([WorkspaceGroup.from_dict(item, self) for item in res.json()])
1282
1330
 
1283
1331
  @ property
1284
1332
  def organizations(self) -> Organizations:
@@ -1291,10 +1339,10 @@ class WorkspaceManager(Manager):
1291
1339
  return Billing(self)
1292
1340
 
1293
1341
  @ property
1294
- def regions(self) -> List[Region]:
1342
+ def regions(self) -> NamedList[Region]:
1295
1343
  """Return a list of available regions."""
1296
1344
  res = self._get('regions')
1297
- return [Region.from_dict(item, self) for item in res.json()]
1345
+ return NamedList([Region.from_dict(item, self) for item in res.json()])
1298
1346
 
1299
1347
  def create_workspace_group(
1300
1348
  self, name: str, region: Union[str, Region],
@@ -1341,7 +1389,7 @@ class WorkspaceManager(Manager):
1341
1389
  'workspaceGroups', json=dict(
1342
1390
  name=name, regionID=region,
1343
1391
  adminPassword=admin_password,
1344
- firewallRanges=firewall_ranges,
1392
+ firewallRanges=firewall_ranges or [],
1345
1393
  expiresAt=expires_at,
1346
1394
  allowAllTraffic=allow_all_traffic,
1347
1395
  updateWindow=update_window,
@@ -47,6 +47,7 @@ from .protocol import (
47
47
  )
48
48
  from . import err
49
49
  from ..config import get_option
50
+ from .. import fusion
50
51
  from ..connection import Connection as BaseConnection
51
52
 
52
53
  try:
@@ -758,10 +759,15 @@ class Connection(BaseConnection):
758
759
  """
759
760
  # if DEBUG:
760
761
  # print("DEBUG: sending query:", sql)
761
- if isinstance(sql, str):
762
- sql = sql.encode(self.encoding, 'surrogateescape')
763
- self._execute_command(COMMAND.COM_QUERY, sql)
764
- self._affected_rows = self._read_query_result(unbuffered=unbuffered)
762
+ handler = fusion.get_handler(sql)
763
+ if handler is not None:
764
+ self._result = fusion.execute(self, sql, handler=handler)
765
+ self._affected_rows = self._result.affected_rows
766
+ else:
767
+ if isinstance(sql, str):
768
+ sql = sql.encode(self.encoding, 'surrogateescape')
769
+ self._execute_command(COMMAND.COM_QUERY, sql)
770
+ self._affected_rows = self._read_query_result(unbuffered=unbuffered)
765
771
  return self._affected_rows
766
772
 
767
773
  def next_result(self, unbuffered=False):
@@ -29,3 +29,4 @@ GEOMETRY = 255
29
29
 
30
30
  CHAR = TINY
31
31
  INTERVAL = ENUM
32
+ BOOL = TINY
@@ -69,10 +69,22 @@ class TestCluster(unittest.TestCase):
69
69
  def test_regions(self):
70
70
  out = self.manager.regions
71
71
  providers = {x.provider for x in out}
72
+ names = [x.name for x in out]
73
+ ids = [x.id for x in out]
72
74
  assert 'Azure' in providers, providers
73
75
  assert 'GCP' in providers, providers
74
76
  assert 'AWS' in providers, providers
75
77
 
78
+ objs = {}
79
+ for item in out:
80
+ objs[item.id] = item
81
+ objs[item.name] = item
82
+
83
+ name = random.choice(names)
84
+ assert out[name] == objs[name]
85
+ id = random.choice(ids)
86
+ assert out[id] == objs[id]
87
+
76
88
  def test_clusters(self):
77
89
  clusters = self.manager.clusters
78
90
  ids = [x.id for x in clusters]
@@ -232,19 +244,61 @@ class TestWorkspace(unittest.TestCase):
232
244
  def test_regions(self):
233
245
  out = self.manager.regions
234
246
  providers = {x.provider for x in out}
247
+ names = [x.name for x in out]
248
+ ids = [x.id for x in out]
235
249
  assert 'Azure' in providers, providers
236
250
  assert 'GCP' in providers, providers
237
251
  assert 'AWS' in providers, providers
238
252
 
253
+ objs = {}
254
+ for item in out:
255
+ objs[item.id] = item
256
+ objs[item.name] = item
257
+
258
+ name = random.choice(names)
259
+ assert out[name] == objs[name]
260
+ id = random.choice(ids)
261
+ assert out[id] == objs[id]
262
+
239
263
  def test_workspace_groups(self):
240
264
  workspace_groups = self.manager.workspace_groups
241
265
  ids = [x.id for x in workspace_groups]
242
- assert self.workspace_group.id in ids, ids
266
+ names = [x.name for x in workspace_groups]
267
+ assert self.workspace_group.id in ids
268
+ assert self.workspace_group.name in names
269
+
270
+ assert workspace_groups.ids() == ids
271
+ assert workspace_groups.names() == names
272
+
273
+ objs = {}
274
+ for item in workspace_groups:
275
+ objs[item.id] = item
276
+ objs[item.name] = item
277
+
278
+ name = random.choice(names)
279
+ assert workspace_groups[name] == objs[name]
280
+ id = random.choice(ids)
281
+ assert workspace_groups[id] == objs[id]
243
282
 
244
283
  def test_workspaces(self):
245
284
  spaces = self.workspace_group.workspaces
246
285
  ids = [x.id for x in spaces]
247
- assert self.workspace.id in ids, ids
286
+ names = [x.name for x in spaces]
287
+ assert self.workspace.id in ids
288
+ assert self.workspace.name in names
289
+
290
+ assert spaces.ids() == ids
291
+ assert spaces.names() == names
292
+
293
+ objs = {}
294
+ for item in spaces:
295
+ objs[item.id] = item
296
+ objs[item.name] = item
297
+
298
+ name = random.choice(names)
299
+ assert spaces[name] == objs[name]
300
+ id = random.choice(ids)
301
+ assert spaces[id] == objs[id]
248
302
 
249
303
  def test_get_workspace_group(self):
250
304
  group = self.manager.get_workspace_group(self.workspace_group.id)
@@ -439,6 +493,46 @@ class TestStages(unittest.TestCase):
439
493
 
440
494
  assert txt == open(TEST_DIR / 'test.sql').read()
441
495
 
496
+ def test_obj_open(self):
497
+ st = self.wg.stages
498
+
499
+ # Load test file
500
+ f = st.upload_file(TEST_DIR / 'test.sql', 'obj_open_test.sql')
501
+
502
+ # Read file using `open`
503
+ with f.open() as rfile:
504
+ assert rfile.read() == open(TEST_DIR / 'test.sql').read()
505
+
506
+ # Make sure directories error out
507
+ d = st.mkdir('obj_open_dir')
508
+ with self.assertRaises(IsADirectoryError):
509
+ d.open()
510
+
511
+ # Write file using `open`
512
+ with f.open('w', encoding='utf-8') as wfile:
513
+ wfile.write(open(TEST_DIR / 'test2.sql').read())
514
+
515
+ assert f.download(encoding='utf-8') == open(TEST_DIR / 'test2.sql').read()
516
+
517
+ # Test writer without context manager
518
+ wfile = f.open('w')
519
+ for line in open(TEST_DIR / 'test.sql'):
520
+ wfile.write(line)
521
+ wfile.close()
522
+
523
+ txt = st.download(f.path, encoding='utf-8')
524
+
525
+ assert txt == open(TEST_DIR / 'test.sql').read()
526
+
527
+ # Test reader without context manager
528
+ rfile = f.open('r')
529
+ txt = ''
530
+ for line in rfile:
531
+ txt += line
532
+ rfile.close()
533
+
534
+ assert txt == open(TEST_DIR / 'test.sql').read()
535
+
442
536
  def test_os_directories(self):
443
537
  st = self.wg.stages
444
538
 
@@ -51,7 +51,7 @@ class TestResults(unittest.TestCase):
51
51
  with conn.cursor() as cur:
52
52
  cur.execute('select * from data')
53
53
  out = cur.fetchone()
54
- assert type(out) == tuple, type(out)
54
+ assert type(out) is tuple, type(out)
55
55
  assert len(out) == 3, len(out)
56
56
  cur.fetchall()
57
57
 
@@ -59,7 +59,7 @@ class TestResults(unittest.TestCase):
59
59
  out = cur.fetchall()
60
60
  assert len(out) == 5, len(out)
61
61
  assert len(out[0]) == 3, len(out[0])
62
- assert type(out[0]) == tuple, type(out[0])
62
+ assert type(out[0]) is tuple, type(out[0])
63
63
  assert sorted(out) == sorted([
64
64
  ('a', 'antelopes', 2),
65
65
  ('b', 'bears', 2),
@@ -111,13 +111,13 @@ class TestResults(unittest.TestCase):
111
111
  with conn.cursor() as cur:
112
112
  cur.execute('select * from data')
113
113
  out = cur.fetchone()
114
- assert type(out) == dict, type(out)
114
+ assert type(out) is dict, type(out)
115
115
  assert len(out) == 3, len(out)
116
116
  cur.fetchall()
117
117
 
118
118
  cur.execute('select * from data')
119
119
  out = cur.fetchall()
120
- assert type(out[0]) == dict, type(out[0])
120
+ assert type(out[0]) is dict, type(out[0])
121
121
  assert len(out) == 5, len(out)
122
122
  assert len(out[0]) == 3, len(out[0])
123
123
  assert sorted(out, key=lambda x: x['id']) == sorted(
@@ -139,13 +139,13 @@ class TestResults(unittest.TestCase):
139
139
  with conn.cursor() as cur:
140
140
  cur.execute('select * from data')
141
141
  out = cur.fetchone()
142
- assert type(out) == pd.DataFrame, type(out)
142
+ assert type(out) is pd.DataFrame, type(out)
143
143
  assert len(out) == 1, len(out)
144
144
  cur.fetchall()
145
145
 
146
146
  cur.execute('select * from data')
147
147
  out = cur.fetchall()
148
- assert type(out) == pd.DataFrame, type(out)
148
+ assert type(out) is pd.DataFrame, type(out)
149
149
  assert len(out) == 5, len(out)
150
150
  out = out.sort_values('id').reset_index(drop=True)
151
151
  exp = pd.DataFrame(
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env python3
2
+ from typing import Any
3
+ from typing import Dict
4
+ from typing import Optional
5
+ from typing import Sequence
6
+ from typing import Union
7
+
8
+ from ..mysql import converters
9
+ from ..mysql.constants import SERVER_STATUS
10
+
11
+
12
+ Encoders = converters.Encoders
13
+
14
+
15
+ def escape(
16
+ obj: Any,
17
+ charset: str = 'utf8',
18
+ mapping: Optional[Encoders] = None,
19
+ server_status: int = 0,
20
+ binary_prefix: bool = False,
21
+ ) -> str:
22
+ """
23
+ Escape whatever value is passed.
24
+
25
+ Non-standard, for internal use; do not use this in your applications.
26
+
27
+ """
28
+ dtype = type(obj)
29
+ if dtype is str or isinstance(obj, str):
30
+ return "'{}'".format(escape_string(obj, server_status=server_status))
31
+ if dtype is bytes or dtype is bytearray or isinstance(obj, (bytes, bytearray)):
32
+ return _quote_bytes(
33
+ obj,
34
+ server_status=server_status,
35
+ binary_prefix=binary_prefix,
36
+ )
37
+ if mapping is None:
38
+ mapping = converters.encoders
39
+ return converters.escape_item(obj, charset, mapping=mapping)
40
+
41
+
42
+ def literal(
43
+ obj: Any,
44
+ charset: str = 'utf8',
45
+ encoders: Optional[Encoders] = None,
46
+ server_status: int = 0,
47
+ binary_prefix: bool = False,
48
+ ) -> str:
49
+ """
50
+ Alias for escape().
51
+
52
+ Non-standard, for internal use; do not use this in your applications.
53
+
54
+ """
55
+ return escape(
56
+ obj, charset=charset, mapping=encoders,
57
+ server_status=server_status, binary_prefix=binary_prefix,
58
+ )
59
+
60
+
61
+ def escape_string(
62
+ s: str,
63
+ server_status: int = 0,
64
+ ) -> str:
65
+ """Escape a string value."""
66
+ if server_status & SERVER_STATUS.SERVER_STATUS_NO_BACKSLASH_ESCAPES:
67
+ return s.replace("'", "''")
68
+ return converters.escape_string(s)
69
+
70
+
71
+ def _quote_bytes(
72
+ s: bytes,
73
+ server_status: int = 0,
74
+ binary_prefix: bool = False,
75
+ ) -> str:
76
+ if server_status & SERVER_STATUS.SERVER_STATUS_NO_BACKSLASH_ESCAPES:
77
+ if binary_prefix:
78
+ return "_binary X'{}'".format(s.hex())
79
+ return "X'{}'".format(s.hex())
80
+ return converters.escape_bytes(s)
81
+
82
+
83
+ def _escape_args(
84
+ args: Union[Sequence[Any], Dict[str, Any], None],
85
+ charset: str = 'utf8',
86
+ encoders: Optional[Encoders] = None,
87
+ server_status: int = 0,
88
+ binary_prefix: bool = False,
89
+ ) -> Any:
90
+ if encoders is None:
91
+ encoders = converters.encoders
92
+
93
+ if isinstance(args, (tuple, list)):
94
+ return tuple(
95
+ literal(
96
+ arg, charset=charset, encoders=encoders,
97
+ server_status=server_status,
98
+ binary_prefix=binary_prefix,
99
+ ) for arg in args
100
+ )
101
+
102
+ elif isinstance(args, dict):
103
+ return {
104
+ key: literal(
105
+ val, charset=charset, encoders=encoders,
106
+ server_status=server_status,
107
+ binary_prefix=binary_prefix,
108
+ ) for (key, val) in args.items()
109
+ }
110
+
111
+ # If it's not a dictionary let's try escaping it anyways.
112
+ # Worst case it will throw a Value error
113
+ return escape(
114
+ args, charset=charset, mapping=encoders,
115
+ server_status=server_status, binary_prefix=binary_prefix,
116
+ )
117
+
118
+
119
+ def mogrify(
120
+ query: Union[str, bytes],
121
+ args: Union[Sequence[Any], Dict[str, Any], None] = None,
122
+ charset: str = 'utf8',
123
+ encoders: Optional[Encoders] = None,
124
+ server_status: int = 0,
125
+ binary_prefix: bool = False,
126
+ ) -> Union[str, bytes]:
127
+ """
128
+ Returns the exact string sent to the database by calling the execute() method.
129
+
130
+ This method follows the extension to the DB API 2.0 followed by Psycopg.
131
+
132
+ Parameters
133
+ ----------
134
+ query : str
135
+ Query to mogrify.
136
+ args : Sequence[Any] or Dict[str, Any] or Any, optional
137
+ Parameters used with query. (optional)
138
+
139
+ Returns
140
+ -------
141
+ str : The query with argument binding applied.
142
+
143
+ """
144
+ if args:
145
+ query = query % _escape_args(
146
+ args, charset=charset,
147
+ encoders=encoders,
148
+ server_status=server_status,
149
+ binary_prefix=binary_prefix,
150
+ )
151
+ return query
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: singlestoredb
3
- Version: 0.9.0
3
+ Version: 0.9.2
4
4
  Summary: Interface to the SingleStoreDB database and workspace management APIs
5
5
  Home-page: https://github.com/singlestore-labs/singlestoredb-python
6
6
  Author: SingleStore
@@ -16,6 +16,7 @@ Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: PyJWT
18
18
  Requires-Dist: build
19
+ Requires-Dist: parsimonious
19
20
  Requires-Dist: requests
20
21
  Requires-Dist: sqlparams
21
22
  Requires-Dist: wheel