singlestoredb 1.7.2__cp38-abi3-win32.whl → 1.8.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.

_singlestoredb_accel.pyd CHANGED
Binary file
singlestoredb/__init__.py CHANGED
@@ -13,7 +13,7 @@ Examples
13
13
 
14
14
  """
15
15
 
16
- __version__ = '1.7.2'
16
+ __version__ = '1.8.0'
17
17
 
18
18
  from typing import Any
19
19
 
@@ -11,6 +11,7 @@ from typing import Dict
11
11
  from typing import Iterable
12
12
  from typing import List
13
13
  from typing import Optional
14
+ from typing import Set
14
15
  from typing import Tuple
15
16
 
16
17
  from parsimonious import Grammar
@@ -23,9 +24,9 @@ from ..connection import Connection
23
24
 
24
25
  CORE_GRAMMAR = r'''
25
26
  ws = ~r"(\s+|(\s*/\*.*\*/\s*)+)"
26
- qs = ~r"\"([^\"]*)\"|'([^\']*)'|`([^\`]*)`|([A-Za-z0-9_\-\.]+)"
27
- number = ~r"[-+]?(\d*\.)?\d+(e[-+]?\d+)?"i
28
- integer = ~r"-?\d+"
27
+ qs = ~r"\"([^\"]*)\"|'([^\']*)'|([A-Za-z0-9_\-\.]+)|`([^\`]+)`" ws*
28
+ number = ~r"[-+]?(\d*\.)?\d+(e[-+]?\d+)?"i ws*
29
+ integer = ~r"-?\d+" ws*
29
30
  comma = ws* "," ws*
30
31
  eq = ws* "=" ws*
31
32
  open_paren = ws* "(" ws*
@@ -33,6 +34,10 @@ CORE_GRAMMAR = r'''
33
34
  open_repeats = ws* ~r"[\(\[\{]" ws*
34
35
  close_repeats = ws* ~r"[\)\]\}]" ws*
35
36
  select = ~r"SELECT"i ws+ ~r".+" ws*
37
+ table = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*
38
+ column = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*
39
+ link_name = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*
40
+ catalog_name = ~r"(?:([A-Za-z0-9_\-]+)|`([^\`]+)`)(?:\.(?:([A-Za-z0-9_\-]+)|`([^\`]+)`))?" ws*
36
41
 
37
42
  json = ws* json_object ws*
38
43
  json_object = ~r"{\s*" json_members? ~r"\s*}"
@@ -65,6 +70,10 @@ BUILTINS = {
65
70
  '<integer>': '',
66
71
  '<number>': '',
67
72
  '<json>': '',
73
+ '<table>': '',
74
+ '<column>': '',
75
+ '<catalog-name>': '',
76
+ '<link-name>': '',
68
77
  }
69
78
 
70
79
  BUILTIN_DEFAULTS = { # type: ignore
@@ -226,9 +235,13 @@ def build_syntax(grammar: str) -> str:
226
235
  # Split on ';' on a line by itself
227
236
  cmd, end = grammar.split(';', 1)
228
237
 
229
- rules = {}
238
+ name = ''
239
+ rules: Dict[str, Any] = {}
230
240
  for line in end.split('\n'):
231
241
  line = line.strip()
242
+ if line.startswith('&'):
243
+ rules[name] += '\n' + line
244
+ continue
232
245
  if not line:
233
246
  continue
234
247
  name, value = line.split('=', 1)
@@ -239,10 +252,16 @@ def build_syntax(grammar: str) -> str:
239
252
  while re.search(r' [a-z0-9_]+\b', cmd):
240
253
  cmd = re.sub(r' ([a-z0-9_]+)\b', functools.partial(expand_rules, rules), cmd)
241
254
 
255
+ def add_indent(m: Any) -> str:
256
+ return ' ' + (len(m.group(1)) * ' ')
257
+
258
+ # Indent line-continuations
259
+ cmd = re.sub(r'^(\&+)\s*', add_indent, cmd, flags=re.M)
260
+
242
261
  cmd = textwrap.dedent(cmd).rstrip() + ';'
243
- cmd = re.sub(r' +', ' ', cmd)
244
- cmd = re.sub(r'^ ', ' ', cmd, flags=re.M)
245
- cmd = re.sub(r'\s+,\.\.\.', ',...', cmd)
262
+ cmd = re.sub(r'(\S) +', r'\1 ', cmd)
263
+ cmd = re.sub(r'<comma>', ',', cmd)
264
+ cmd = re.sub(r'\s+,\s*\.\.\.', ',...', cmd)
246
265
 
247
266
  return cmd
248
267
 
@@ -399,9 +418,15 @@ def process_grammar(
399
418
  help_txt = build_help(syntax_txt, full_grammar)
400
419
  grammar = build_cmd(grammar)
401
420
 
421
+ # Remove line-continuations
422
+ grammar = re.sub(r'\n\s*&+', r'', grammar)
423
+
402
424
  # Make sure grouping characters all have whitespace around them
403
425
  grammar = re.sub(r' *(\[|\{|\||\}|\]) *', r' \1 ', grammar)
404
426
 
427
+ grammar = re.sub(r'\(', r' open_paren ', grammar)
428
+ grammar = re.sub(r'\)', r' close_paren ', grammar)
429
+
405
430
  for line in grammar.split('\n'):
406
431
  if not line.strip():
407
432
  continue
@@ -418,7 +443,7 @@ def process_grammar(
418
443
  sql = re.sub(r'\]\s+\[', r' | ', sql)
419
444
 
420
445
  # Lower-case keywords and make them case-insensitive
421
- sql = re.sub(r'(\b|@+)([A-Z0-9]+)\b', lower_and_regex, sql)
446
+ sql = re.sub(r'(\b|@+)([A-Z0-9_]+)\b', lower_and_regex, sql)
422
447
 
423
448
  # Convert literal strings to 'qs'
424
449
  sql = re.sub(r"'[^']+'", r'qs', sql)
@@ -461,12 +486,18 @@ def process_grammar(
461
486
  sql = re.sub(r'\s+ws$', r' ws*', sql)
462
487
  sql = re.sub(r'\s+ws\s+\(', r' ws* (', sql)
463
488
  sql = re.sub(r'\)\s+ws\s+', r') ws* ', sql)
464
- sql = re.sub(r'\s+ws\s+', r' ws+ ', sql)
489
+ sql = re.sub(r'\s+ws\s+', r' ws* ', sql)
465
490
  sql = re.sub(r'\?\s+ws\+', r'? ws*', sql)
466
491
 
467
492
  # Remove extra ws around eq
468
493
  sql = re.sub(r'ws\+\s*eq\b', r'eq', sql)
469
494
 
495
+ # Remove optional groupings when mandatory groupings are specified
496
+ sql = re.sub(r'open_paren\s+ws\*\s+open_repeats\?', r'open_paren', sql)
497
+ sql = re.sub(r'close_repeats\?\s+ws\*\s+close_paren', r'close_paren', sql)
498
+ sql = re.sub(r'open_paren\s+open_repeats\?', r'open_paren', sql)
499
+ sql = re.sub(r'close_repeats\?\s+close_paren', r'close_paren', sql)
500
+
470
501
  out.append(f'{op} = {sql}')
471
502
 
472
503
  for k, v in list(rules.items()):
@@ -548,6 +579,7 @@ class SQLHandler(NodeVisitor):
548
579
 
549
580
  def __init__(self, connection: Connection):
550
581
  self.connection = connection
582
+ self._handled: Set[str] = set()
551
583
 
552
584
  @classmethod
553
585
  def compile(cls, grammar: str = '') -> None:
@@ -614,12 +646,16 @@ class SQLHandler(NodeVisitor):
614
646
  )
615
647
 
616
648
  type(self).compile()
649
+ self._handled = set()
617
650
  try:
618
651
  params = self.visit(type(self).grammar.parse(sql))
619
652
  for k, v in params.items():
620
653
  params[k] = self.validate_rule(k, v)
621
654
 
622
655
  res = self.run(params)
656
+
657
+ self._handled = set()
658
+
623
659
  if res is not None:
624
660
  res.format_results(self.connection)
625
661
  return res
@@ -666,16 +702,20 @@ class SQLHandler(NodeVisitor):
666
702
  """Quoted strings."""
667
703
  if node is None:
668
704
  return None
669
- return node.match.group(1) or node.match.group(2) or \
670
- node.match.group(3) or node.match.group(4)
705
+ return flatten(visited_children)[0]
706
+
707
+ def visit_compound(self, node: Node, visited_children: Iterable[Any]) -> Any:
708
+ """Compound name."""
709
+ print(visited_children)
710
+ return flatten(visited_children)[0]
671
711
 
672
712
  def visit_number(self, node: Node, visited_children: Iterable[Any]) -> Any:
673
713
  """Numeric value."""
674
- return float(node.match.group(0))
714
+ return float(flatten(visited_children)[0])
675
715
 
676
716
  def visit_integer(self, node: Node, visited_children: Iterable[Any]) -> Any:
677
717
  """Integer value."""
678
- return int(node.match.group(0))
718
+ return int(flatten(visited_children)[0])
679
719
 
680
720
  def visit_ws(self, node: Node, visited_children: Iterable[Any]) -> Any:
681
721
  """Whitespace and comments."""
@@ -804,19 +844,29 @@ class SQLHandler(NodeVisitor):
804
844
  if node.expr_name.endswith('_cmd'):
805
845
  final = merge_dicts(flatten(visited_children)[n_keywords:])
806
846
  for k, v in type(self).rule_info.items():
807
- if k.endswith('_cmd') or k.endswith('_'):
847
+ if k.endswith('_cmd') or k.endswith('_') or k.startswith('_'):
808
848
  continue
809
- if k not in final:
849
+ if k not in final and k not in self._handled:
810
850
  final[k] = BUILTIN_DEFAULTS.get(k, v['default'])
811
851
  return final
812
852
 
813
853
  # Filter out stray empty strings
814
854
  out = [x for x in flatten(visited_children)[n_keywords:] if x]
815
855
 
816
- if repeats or len(out) > 1:
817
- return {node.expr_name: out}
856
+ # Remove underscore prefixes from rule name
857
+ key_name = re.sub(r'^_+', r'', node.expr_name)
818
858
 
819
- return {node.expr_name: out[0] if out else True}
859
+ if repeats or len(out) > 1:
860
+ self._handled.add(node.expr_name)
861
+ # If all outputs are dicts, merge them
862
+ if len(out) > 1 and not repeats:
863
+ is_dicts = [x for x in out if isinstance(x, dict)]
864
+ if len(is_dicts) == len(out):
865
+ return {key_name: merge_dicts(out)}
866
+ return {key_name: out}
867
+
868
+ self._handled.add(node.expr_name)
869
+ return {key_name: out[0] if out else True}
820
870
 
821
871
  if hasattr(node, 'match'):
822
872
  if not visited_children and not node.match.groups():
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ from typing import Any
4
+ from typing import Dict
5
+ from typing import Optional
6
+
7
+ from .. import result
8
+ from ...management.export import Catalog
9
+ from ...management.export import ExportService
10
+ from ...management.export import ExportStatus
11
+ from ...management.export import Link
12
+ from ..handler import SQLHandler
13
+ from ..result import FusionSQLResult
14
+ from .utils import get_workspace_group
15
+
16
+
17
+ class CreateClusterIdentity(SQLHandler):
18
+ """
19
+ CREATE CLUSTER IDENTITY
20
+ catalog
21
+ storage
22
+ ;
23
+
24
+ # Catolog
25
+ catalog = CATALOG { _catalog_config | _catalog_creds }
26
+ _catalog_config = CONFIG '<catalog-config>'
27
+ _catalog_creds = CREDENTIALS '<catalog-creds>'
28
+
29
+ # Storage
30
+ storage = LINK { _link_config | _link_creds }
31
+ _link_config = S3 CONFIG '<link-config>'
32
+ _link_creds = CREDENTIALS '<link-creds>'
33
+
34
+ Description
35
+ -----------
36
+ Create a cluster identity for allowing the export service to access
37
+ external cloud resources.
38
+
39
+ Arguments
40
+ ---------
41
+ * ``<catalog-config>`` and ``<catalog-creds>``: Catalog configuration
42
+ and credentials in JSON format.
43
+ * ``<link-config>`` and ``<link-creds>``: Storage link configuration
44
+ and credentials in JSON format.
45
+
46
+ Remarks
47
+ -------
48
+ * ``FROM <table>`` specifies the SingleStore table to export. The same name will
49
+ be used for the exported table.
50
+ * ``CATALOG`` specifies the details of the catalog to connect to.
51
+ * ``LINK`` specifies the details of the data storage to connect to.
52
+
53
+ Example
54
+ -------
55
+ The following statement creates a cluster identity for the catalog
56
+ and link::
57
+
58
+ CREATE CLUSTER IDENTITY
59
+ CATALOG CONFIG '{
60
+ "type": "GLUE",
61
+ "table_format": "ICEBERG",
62
+ "id": "13983498723498",
63
+ "region": "us-east-1"
64
+ }'
65
+ LINK S3 CONFIG '{
66
+ "region": "us-east-1",
67
+ "endpoint_url": "s3://bucket-name"
68
+
69
+ }'
70
+ ;
71
+
72
+ """
73
+
74
+ _enabled = False
75
+
76
+ def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
77
+ # Catalog
78
+ catalog_config = json.loads(params['catalog'].get('catalog_config', '{}') or '{}')
79
+ catalog_creds = json.loads(params['catalog'].get('catalog_creds', '{}') or '{}')
80
+
81
+ # Storage
82
+ storage_config = json.loads(params['storage'].get('link_config', '{}') or '{}')
83
+ storage_creds = json.loads(params['storage'].get('link_creds', '{}') or '{}')
84
+
85
+ wsg = get_workspace_group({})
86
+
87
+ if wsg._manager is None:
88
+ raise TypeError('no workspace manager is associated with workspace group')
89
+
90
+ out = ExportService(
91
+ wsg,
92
+ 'none',
93
+ 'none',
94
+ Catalog.from_config_and_creds(catalog_config, catalog_creds, wsg._manager),
95
+ Link.from_config_and_creds('S3', storage_config, storage_creds, wsg._manager),
96
+ columns=None,
97
+ ).create_cluster_identity()
98
+
99
+ res = FusionSQLResult()
100
+ res.add_field('RoleARN', result.STRING)
101
+ res.set_rows([(out['roleARN'],)])
102
+
103
+ return res
104
+
105
+
106
+ CreateClusterIdentity.register(overwrite=True)
107
+
108
+
109
+ class CreateExport(SQLHandler):
110
+ """
111
+ START EXPORT
112
+ from_table
113
+ catalog
114
+ storage
115
+ ;
116
+
117
+ # From table
118
+ from_table = FROM <table>
119
+
120
+ # Catolog
121
+ catalog = CATALOG [ _catalog_config ] [ _catalog_creds ]
122
+ _catalog_config = CONFIG '<catalog-config>'
123
+ _catalog_creds = CREDENTIALS '<catalog-creds>'
124
+
125
+ # Storage
126
+ storage = LINK [ _link_config ] [ _link_creds ]
127
+ _link_config = S3 CONFIG '<link-config>'
128
+ _link_creds = CREDENTIALS '<link-creds>'
129
+
130
+ Description
131
+ -----------
132
+ Create an export configuration.
133
+
134
+ Arguments
135
+ ---------
136
+ * ``<catalog-config>`` and ``<catalog-creds>``: The catalog configuration.
137
+ * ``<link-config>`` and ``<link-creds>``: The storage link configuration.
138
+
139
+ Remarks
140
+ -------
141
+ * ``FROM <table>`` specifies the SingleStore table to export. The same name will
142
+ be used for the exported table.
143
+ * ``CATALOG`` specifies the details of the catalog to connect to.
144
+ * ``LINK`` specifies the details of the data storage to connect to.
145
+
146
+ Examples
147
+ --------
148
+ The following statement starts an export operation with the given
149
+ catalog and link configurations. The source table to export is
150
+ named "customer_data"::
151
+
152
+ START EXPORT FROM customer_data
153
+ CATALOG CONFIG '{
154
+ "type": "GLUE",
155
+ "table_format": "ICEBERG",
156
+ "id": "13983498723498",
157
+ "region": "us-east-1"
158
+ }'
159
+ LINK S3 CONFIG '{
160
+ "region": "us-east-1",
161
+ "endpoint_url": "s3://bucket-name"
162
+
163
+ }'
164
+ ;
165
+
166
+ """ # noqa
167
+
168
+ _enabled = False
169
+
170
+ def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
171
+ # From table
172
+ if isinstance(params['from_table'], str):
173
+ from_database = None
174
+ from_table = params['from_table']
175
+ else:
176
+ from_database, from_table = params['from_table']
177
+
178
+ # Catalog
179
+ catalog_config = json.loads(params['catalog'].get('catalog_config', '{}') or '{}')
180
+ catalog_creds = json.loads(params['catalog'].get('catalog_creds', '{}') or '{}')
181
+
182
+ # Storage
183
+ storage_config = json.loads(params['storage'].get('link_config', '{}') or '{}')
184
+ storage_creds = json.loads(params['storage'].get('link_creds', '{}') or '{}')
185
+
186
+ wsg = get_workspace_group({})
187
+
188
+ if from_database is None:
189
+ raise ValueError('database name must be specified for source table')
190
+
191
+ if wsg._manager is None:
192
+ raise TypeError('no workspace manager is associated with workspace group')
193
+
194
+ out = ExportService(
195
+ wsg,
196
+ from_database,
197
+ from_table,
198
+ Catalog.from_config_and_creds(catalog_config, catalog_creds, wsg._manager),
199
+ Link.from_config_and_creds('S3', storage_config, storage_creds, wsg._manager),
200
+ columns=None,
201
+ ).start()
202
+
203
+ res = FusionSQLResult()
204
+ res.add_field('ExportID', result.STRING)
205
+ res.set_rows([(out.export_id,)])
206
+
207
+ return res
208
+
209
+
210
+ CreateExport.register(overwrite=True)
211
+
212
+
213
+ class ShowExport(SQLHandler):
214
+ """
215
+ SHOW EXPORT export_id;
216
+
217
+ # ID of export
218
+ export_id = '<export-id>'
219
+
220
+ """
221
+
222
+ _enabled = False
223
+
224
+ def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
225
+ wsg = get_workspace_group({})
226
+ out = ExportStatus(params['export_id'], wsg)
227
+
228
+ status = out._info()
229
+
230
+ res = FusionSQLResult()
231
+ res.add_field('ExportID', result.STRING)
232
+ res.add_field('Status', result.STRING)
233
+ res.add_field('Message', result.STRING)
234
+ res.set_rows([
235
+ (
236
+ params['export_id'],
237
+ status.get('status', 'Unknown'),
238
+ status.get('statusMsg', ''),
239
+ ),
240
+ ])
241
+
242
+ return res
243
+
244
+
245
+ ShowExport.register(overwrite=True)
@@ -122,8 +122,6 @@ class ScheduleJobHandler(SQLHandler):
122
122
  ;
123
123
  """
124
124
 
125
- _enabled = False
126
-
127
125
  def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
128
126
  res = FusionSQLResult()
129
127
  res.add_field('JobID', result.STRING)
@@ -138,8 +136,8 @@ class ScheduleJobHandler(SQLHandler):
138
136
 
139
137
  execution_interval_in_mins = None
140
138
  if params.get('execute_every'):
141
- execution_interval_in_mins = params['execute_every'][0]['interval']
142
- time_unit = params['execute_every'][-1]['time_unit'].upper()
139
+ execution_interval_in_mins = params['execute_every']['interval']
140
+ time_unit = params['execute_every']['time_unit'].upper()
143
141
  if time_unit == 'MINUTES':
144
142
  pass
145
143
  elif time_unit == 'HOURS':
@@ -224,8 +222,6 @@ class RunJobHandler(SQLHandler):
224
222
 
225
223
  """
226
224
 
227
- _enabled = False
228
-
229
225
  def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
230
226
  res = FusionSQLResult()
231
227
  res.add_field('JobID', result.STRING)
@@ -288,8 +284,6 @@ class WaitOnJobsHandler(SQLHandler):
288
284
 
289
285
  """
290
286
 
291
- _enabled = False
292
-
293
287
  def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
294
288
  res = FusionSQLResult()
295
289
  res.add_field('Success', result.BOOL)
@@ -298,8 +292,8 @@ class WaitOnJobsHandler(SQLHandler):
298
292
 
299
293
  timeout_in_secs = None
300
294
  if params.get('with_timeout'):
301
- timeout_in_secs = params['with_timeout'][0]['time']
302
- time_unit = params['with_timeout'][-1]['time_unit'].upper()
295
+ timeout_in_secs = params['with_timeout']['time']
296
+ time_unit = params['with_timeout']['time_unit'].upper()
303
297
  if time_unit == 'SECONDS':
304
298
  pass
305
299
  elif time_unit == 'MINUTES':
@@ -359,8 +353,6 @@ class ShowJobsHandler(SQLHandler):
359
353
 
360
354
  """
361
355
 
362
- _enabled = False
363
-
364
356
  def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
365
357
  res = FusionSQLResult()
366
358
  res.add_field('JobID', result.STRING)
@@ -492,8 +484,6 @@ class ShowJobExecutionsHandler(SQLHandler):
492
484
  EXTENDED;
493
485
  """
494
486
 
495
- _enabled = False
496
-
497
487
  def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
498
488
  res = FusionSQLResult()
499
489
  res.add_field('ExecutionID', result.STRING)
@@ -564,8 +554,6 @@ class ShowJobParametersHandler(SQLHandler):
564
554
  SHOW JOB PARAMETERS FOR 'job1';
565
555
  """
566
556
 
567
- _enabled = False
568
-
569
557
  def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
570
558
  res = FusionSQLResult()
571
559
  res.add_field('Name', result.STRING)
@@ -606,8 +594,6 @@ class ShowJobRuntimesHandler(SQLHandler):
606
594
  SHOW JOB RUNTIMES;
607
595
  """
608
596
 
609
- _enabled = False
610
-
611
597
  def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
612
598
  res = FusionSQLResult()
613
599
  res.add_field('Name', result.STRING)
@@ -653,8 +639,6 @@ class DropJobHandler(SQLHandler):
653
639
  DROP JOBS 'job1', 'job2';
654
640
  """
655
641
 
656
- _enabled = False
657
-
658
642
  def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
659
643
  res = FusionSQLResult()
660
644
  res.add_field('JobID', result.STRING)
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env python
2
+ """SingleStoreDB export service."""
3
+ from __future__ import annotations
4
+
5
+ import abc
6
+ import re
7
+ from typing import Any
8
+ from typing import Dict
9
+ from typing import List
10
+ from typing import Optional
11
+
12
+ from .. import ManagementError
13
+ from .utils import vars_to_str
14
+ from .workspace import WorkspaceGroup
15
+ from .workspace import WorkspaceManager
16
+
17
+
18
+ class Link(object):
19
+ """Generic storage base class."""
20
+ scheme: str = 'unknown'
21
+
22
+ def __str__(self) -> str:
23
+ """Return string representation."""
24
+ return vars_to_str(self)
25
+
26
+ def __repr__(self) -> str:
27
+ """Return string representation."""
28
+ return str(self)
29
+
30
+ @abc.abstractmethod
31
+ def to_storage_location(self) -> Dict[str, Any]:
32
+ raise NotImplementedError
33
+
34
+ @classmethod
35
+ def from_config_and_creds(
36
+ cls,
37
+ scheme: str,
38
+ config: Dict[str, Any],
39
+ credentials: Dict[str, Any],
40
+ manager: 'WorkspaceManager',
41
+ ) -> 'Link':
42
+ out_cls = None
43
+ for c in cls.__subclasses__():
44
+ if c.scheme == scheme.upper():
45
+ out_cls = c
46
+ break
47
+
48
+ if out_cls is None:
49
+ raise TypeError(f'No link class found for given information: {scheme}')
50
+
51
+ return out_cls.from_config_and_creds(scheme, config, credentials, manager)
52
+
53
+
54
+ class S3Link(Link):
55
+ """S3 link."""
56
+
57
+ scheme: str = 'S3'
58
+ region: str
59
+ storage_base_url: str
60
+
61
+ def __init__(self, region: str, storage_base_url: str):
62
+ self.region = region
63
+ self.storage_base_url = storage_base_url
64
+ self._manager: Optional[WorkspaceManager] = None
65
+
66
+ def to_storage_location(self) -> Dict[str, Any]:
67
+ return dict(
68
+ storageBaseURL=self.storage_base_url,
69
+ storageRegion=self.region,
70
+ )
71
+
72
+ @classmethod
73
+ def from_config_and_creds(
74
+ cls,
75
+ scheme: str,
76
+ config: Dict[str, Any],
77
+ credentials: Dict[str, Any],
78
+ manager: 'WorkspaceManager',
79
+ ) -> 'S3Link':
80
+ assert scheme.upper() == cls.scheme
81
+
82
+ params: Dict[str, Any] = {}
83
+ params.update(config)
84
+ params.update(credentials)
85
+
86
+ assert params.get('region'), 'region is required'
87
+ assert params.get('endpoint_url'), 'endpoint_url is required'
88
+
89
+ out = cls(params['region'], params['endpoint_url'])
90
+ out._manager = manager
91
+ return out
92
+
93
+
94
+ class Catalog(object):
95
+ """Generic catalog base class."""
96
+
97
+ catalog_type: str = 'UNKNOWN'
98
+ table_format: str = 'UNKNOWN'
99
+
100
+ def __str__(self) -> str:
101
+ """Return string representation."""
102
+ return vars_to_str(self)
103
+
104
+ def __repr__(self) -> str:
105
+ """Return string representation."""
106
+ return str(self)
107
+
108
+ @classmethod
109
+ def from_config_and_creds(
110
+ cls,
111
+ config: Dict[str, Any],
112
+ credentials: Dict[str, Any],
113
+ manager: 'WorkspaceManager',
114
+ ) -> 'Catalog':
115
+ catalog_type = config['type'].upper()
116
+ table_format = config['table_format'].upper()
117
+
118
+ out_cls = None
119
+ for c in cls.__subclasses__():
120
+ if c.catalog_type == catalog_type and c.table_format == table_format:
121
+ out_cls = c
122
+ break
123
+
124
+ if out_cls is None:
125
+ raise TypeError(f'No catalog class found for given information: {config}')
126
+
127
+ return out_cls.from_config_and_creds(config, credentials, manager)
128
+
129
+ @abc.abstractmethod
130
+ def to_catalog_info(self) -> Dict[str, Any]:
131
+ """Return a catalog info dictionary."""
132
+ raise NotImplementedError
133
+
134
+
135
+ class IcebergGlueCatalog(Catalog):
136
+ """Iceberg glue catalog."""
137
+
138
+ table_format = 'ICEBERG'
139
+ catalog_type = 'GLUE'
140
+
141
+ region: str
142
+ catalog_id: str
143
+
144
+ def __init__(self, region: str, catalog_id: str):
145
+ self.region = region
146
+ self.catalog_id = catalog_id
147
+ self._manager: Optional[WorkspaceManager] = None
148
+
149
+ @classmethod
150
+ def from_config_and_creds(
151
+ cls,
152
+ config: Dict[str, Any],
153
+ credentials: Dict[str, Any],
154
+ manager: 'WorkspaceManager',
155
+ ) -> 'IcebergGlueCatalog':
156
+ params = {}
157
+ params.update(config)
158
+ params.update(credentials)
159
+
160
+ out = cls(
161
+ region=params['region'],
162
+ catalog_id=params['id'],
163
+ )
164
+ out._manager = manager
165
+ return out
166
+
167
+ def to_catalog_info(self) -> Dict[str, Any]:
168
+ """Return a catalog info dictionary."""
169
+ return dict(
170
+ catalogSource=self.catalog_type,
171
+ tableFormat=self.table_format,
172
+ glueRegion=self.region,
173
+ glueCatalogID=self.catalog_id,
174
+ )
175
+
176
+
177
+ class ExportService(object):
178
+ """Export service."""
179
+
180
+ database: str
181
+ table: str
182
+ catalog: Catalog
183
+ storage_link: Link
184
+ columns: Optional[List[str]]
185
+
186
+ def __init__(
187
+ self,
188
+ workspace_group: WorkspaceGroup,
189
+ database: str,
190
+ table: str,
191
+ catalog: Catalog,
192
+ storage_link: Link,
193
+ columns: Optional[List[str]],
194
+ ):
195
+ #: Workspace group
196
+ self.workspace_group = workspace_group
197
+
198
+ #: Name of SingleStoreDB database
199
+ self.database = database
200
+
201
+ #: Name of SingleStoreDB table
202
+ self.table = table
203
+
204
+ #: List of columns to export
205
+ self.columns = columns
206
+
207
+ #: Catalog
208
+ self.catalog = catalog
209
+
210
+ #: Storage
211
+ self.storage_link = storage_link
212
+
213
+ self._manager: Optional[WorkspaceManager] = workspace_group._manager
214
+
215
+ def __str__(self) -> str:
216
+ """Return string representation."""
217
+ return vars_to_str(self)
218
+
219
+ def __repr__(self) -> str:
220
+ """Return string representation."""
221
+ return str(self)
222
+
223
+ def create_cluster_identity(self) -> Dict[str, Any]:
224
+ """Create a cluster identity."""
225
+ if self._manager is None:
226
+ raise ManagementError(
227
+ msg='No workspace manager is associated with this object.',
228
+ )
229
+
230
+ if not isinstance(self.catalog, IcebergGlueCatalog):
231
+ raise TypeError('Only Iceberg Glue catalog is supported at this time.')
232
+
233
+ if not isinstance(self.storage_link, S3Link):
234
+ raise TypeError('Only S3 links are supported at this time.')
235
+
236
+ out = self._manager._post(
237
+ f'workspaceGroups/{self.workspace_group.id}/'
238
+ 'egress/createEgressClusterIdentity',
239
+ json=dict(
240
+ storageBucketName=re.split(
241
+ r'/+', self.storage_link.storage_base_url,
242
+ )[1],
243
+ glueRegion=self.catalog.region,
244
+ glueCatalogID=self.catalog.catalog_id,
245
+ ),
246
+ )
247
+
248
+ return out.json()
249
+
250
+ def start(self, tags: Optional[List[str]] = None) -> 'ExportStatus':
251
+ """Start the export process."""
252
+ if self._manager is None:
253
+ raise ManagementError(
254
+ msg='No workspace manager is associated with this object.',
255
+ )
256
+
257
+ if not isinstance(self.storage_link, S3Link):
258
+ raise TypeError('Only S3 links are supported at this time.')
259
+
260
+ out = self._manager._post(
261
+ f'workspaceGroups/{self.workspace_group.id}/egress/startTableEgress',
262
+ json=dict(
263
+ databaseName=self.database,
264
+ tableName=self.table,
265
+ storageLocation=self.storage_link.to_storage_location(),
266
+ catalogInfo=self.catalog.to_catalog_info(),
267
+ ),
268
+ )
269
+
270
+ return ExportStatus(out.json()['egressID'], self.workspace_group)
271
+
272
+
273
+ class ExportStatus(object):
274
+
275
+ export_id: str
276
+
277
+ def __init__(self, export_id: str, workspace_group: WorkspaceGroup):
278
+ self.export_id = export_id
279
+ self.workspace_group = workspace_group
280
+ self._manager: Optional[WorkspaceManager] = workspace_group._manager
281
+
282
+ def _info(self) -> Dict[str, Any]:
283
+ """Return export status."""
284
+ if self._manager is None:
285
+ raise ManagementError(
286
+ msg='No workspace manager is associated with this object.',
287
+ )
288
+
289
+ out = self._manager._post(
290
+ f'workspaceGroups/{self.workspace_group.id}/egress/tableEgressStatus',
291
+ json=dict(egressID=self.export_id),
292
+ )
293
+
294
+ return out.json()
295
+
296
+ @property
297
+ def status(self) -> str:
298
+ """Return export status."""
299
+ return self._info().get('status', 'Unknown')
300
+
301
+ @property
302
+ def message(self) -> str:
303
+ """Return export status message."""
304
+ return self._info().get('statusMsg', '')
305
+
306
+ def __str__(self) -> str:
307
+ return self.status
308
+
309
+ def __repr__(self) -> str:
310
+ return self.status
singlestoredb/py.typed ADDED
File without changes
@@ -465,10 +465,6 @@ class TestWorkspaceFusion(unittest.TestCase):
465
465
  pass
466
466
 
467
467
 
468
- @unittest.skipIf(
469
- os.environ.get('SINGLESTOREDB_FUSION_ENABLE_HIDDEN', '0') == '0',
470
- 'Hidden Fusion commands are not enabled.',
471
- )
472
468
  @pytest.mark.management
473
469
  class TestJobsFusion(unittest.TestCase):
474
470
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: singlestoredb
3
- Version: 1.7.2
3
+ Version: 1.8.0
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
@@ -1,10 +1,11 @@
1
- _singlestoredb_accel.pyd,sha256=LoGB-5Zns3uPLNp8tjMn0JiMygZwpwrulgXeFGJGUOU,59904
2
- singlestoredb/__init__.py,sha256=jSf2uwMGpvrTEf60dTZMxvO63yzzmKGE12bZUuCMOQA,1697
1
+ _singlestoredb_accel.pyd,sha256=Nb9bnw8jGKxtNd-VkNIfMrizqIHmYVwHsnxXbJhBAmY,59904
2
+ singlestoredb/__init__.py,sha256=YzFLtsA1I_g42ghOHzVvbTkNTFzBTqkclAYTShE_N3M,1697
3
3
  singlestoredb/auth.py,sha256=RmYiH0Wlc2RXc4pTlRMysxtBI445ggCIwojWKC_eDLE,7844
4
4
  singlestoredb/config.py,sha256=LlrwKor_23uA9u7jWBYb-IaOKs30CjWIM7o9xCXEPcc,12721
5
5
  singlestoredb/connection.py,sha256=QC5YQemwJOhdW_-ZBFpNLE15xFxwI1fB2LuvE1hNi9k,46812
6
6
  singlestoredb/converters.py,sha256=7_Of1f3Ow-JUoY1pHFlMPYxvt8llzdk-7X8qk5z2xJM,21631
7
7
  singlestoredb/exceptions.py,sha256=WCCJrNSsU-hD-621Jpd6bwmvGftQ7byXkk-XKXlaxpg,3354
8
+ singlestoredb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
9
  singlestoredb/pytest.py,sha256=TH364xRCN7_QaN0oRQDHixrEcDx_ZBgu3bmY0tvKrYU,9357
9
10
  singlestoredb/types.py,sha256=Lv0BEQl6aSZBiAe0OSI07FEJhcHZ9HX45iT9NU_mxHQ,10334
10
11
  singlestoredb/ai/__init__.py,sha256=nT048t90xqjaNhz7KJ10KfSVW4RcZRoujyC6po6Nmb8,61
@@ -31,11 +32,12 @@ singlestoredb/functions/ext/rowdat_1.py,sha256=KYj_y5JWm3_B2-QC47HK-CNOrzujBqGUw
31
32
  singlestoredb/functions/ext/utils.py,sha256=OPMFD-tTCx2Kk9jguQkrTr7e4AgNkt15YsvaT1YSmN8,5480
32
33
  singlestoredb/fusion/__init__.py,sha256=FHWtrg6OJFTf6Ye197V5sU6ssryr2h6FBcDIgXP7-H4,367
33
34
  singlestoredb/fusion/graphql.py,sha256=SHqsPe4xgawdsTPHEtJGQlybYGWqPrGMmyK-v20RLac,5420
34
- singlestoredb/fusion/handler.py,sha256=iILH6aeVWOPbqbkJ8m8CVYV61uJnw8ohYIv8U7bXklM,26231
35
+ singlestoredb/fusion/handler.py,sha256=KqBqssL7KVXxQWPw9bG8H8zto4ac76qEeExteBr-t1c,28437
35
36
  singlestoredb/fusion/registry.py,sha256=_eT1gd38VPlFKs5f9Pu6lqQyoDQ_ixW5O56QwYLQ89Y,6361
36
37
  singlestoredb/fusion/result.py,sha256=EcFY5Qv43ySlQsfl_JB-I3ko7PzVdjuhhoKN96uHSAM,12171
37
38
  singlestoredb/fusion/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
- singlestoredb/fusion/handlers/job.py,sha256=TE57c3IDsKb6C5QH4AKAZBMMeZCIaQ4NrZPzXIq0KaI,21959
39
+ singlestoredb/fusion/handlers/export.py,sha256=UCsAXOaZaQ1cD4NkslVBQ_sgzMGeZvemi-cTHcU9pBw,7342
40
+ singlestoredb/fusion/handlers/job.py,sha256=3enfxHwERH7T4u0FEwOPN0IL0GtepaCYgEsisiy3Df4,21753
39
41
  singlestoredb/fusion/handlers/stage.py,sha256=uPqawMvchnpyrPYLhB0joTremCURNYKOvYntFc3zTRU,14133
40
42
  singlestoredb/fusion/handlers/utils.py,sha256=7xWb_1mJzxW0po9iHVY2ZVnRvHIQgOlKZQZ1zllJBjk,5271
41
43
  singlestoredb/fusion/handlers/workspace.py,sha256=NxoEY5xd5lCQmXiim4nhAYCL0agHo1H_rGPpqa31hiw,28397
@@ -44,6 +46,7 @@ singlestoredb/http/connection.py,sha256=LFUeWx7maS7xhQLqEX3pgvIGoosqyJTovtWwJ1Dy
44
46
  singlestoredb/management/__init__.py,sha256=pQbsOffl-i6zIzR63MCxSjxPY6TNmy7Kg0BCTExt3mk,244
45
47
  singlestoredb/management/billing_usage.py,sha256=0UHFSPCrN0nyeGFFM-HXS3NP8pYmYo2BCCahDEPXvzg,3883
46
48
  singlestoredb/management/cluster.py,sha256=XfdBuTlrAG-mnW1BFKeoAr4YSE5IVgxLjbuBSpqIySo,14823
49
+ singlestoredb/management/export.py,sha256=PPasIoAvsdsylq1iFy1Hq6JfRSpqIuD4cZZXxUYqGEU,8968
47
50
  singlestoredb/management/job.py,sha256=Npfe1JLYJlggGBrXLniPKwKUKF1i3alvSY1SFtvauSs,25498
48
51
  singlestoredb/management/manager.py,sha256=uGNrUe3zhuP-HUqdfwvy4MdEXTCmq-FZKjIwZSc3hOM,9096
49
52
  singlestoredb/management/organization.py,sha256=JBsNC4R3boUKdYvyCZyfGoVMC1mD6SPuMI1UssBVoOM,5611
@@ -106,7 +109,7 @@ singlestoredb/tests/test_dbapi.py,sha256=cNJoTEZvYG7ckcwT7xqlkJX-2TDEYGTDDU1Iguc
106
109
  singlestoredb/tests/test_exceptions.py,sha256=vscMYmdOJr0JmkTAJrNI2w0Q96Nfugjkrt5_lYnw8i0,1176
107
110
  singlestoredb/tests/test_ext_func.py,sha256=gQErR-wAN8BqLNG5U4pNbg4qkQEo6Re8Hd9_Ztqo1RM,38550
108
111
  singlestoredb/tests/test_ext_func_data.py,sha256=9kn8BWmCjkbnP6hSbFhmhcdW4OmVT-GSvBTIzFBLEys,48796
109
- singlestoredb/tests/test_fusion.py,sha256=Qab2TTVwUPo7NPso6nIQgDkR6hHtDLp5j1gO7X4bDsk,24685
112
+ singlestoredb/tests/test_fusion.py,sha256=ckATjXKcDBvej68PZgTRJirdoywtUJ7WXkZmfR0la_4,24544
110
113
  singlestoredb/tests/test_http.py,sha256=7hwXe61hlUes3nji0MTTZweo94tJAlJ-vA5ct9geXFQ,8868
111
114
  singlestoredb/tests/test_management.py,sha256=B4NkK7J0luuS7T-7OR5qzu-v8gkViIiXie-58bHQIDQ,35334
112
115
  singlestoredb/tests/test_plugin.py,sha256=P1nXLnTafaHkHN-6bVbGryxTu7OWJPU9SYFZ_WQUwq8,845
@@ -125,9 +128,9 @@ singlestoredb/utils/events.py,sha256=rC9cHAetua_E1f-EiFkFM-gJzQSQIH5Uk-4sspC3KjI
125
128
  singlestoredb/utils/mogrify.py,sha256=gCcn99-vgsGVjTUV7RHJ6hH4vCNrsGB_Xo4z8kiSPDQ,4201
126
129
  singlestoredb/utils/results.py,sha256=wR70LhCqlobniZf52r67zYLBOKjWHQm68NAskdRQND8,15862
127
130
  singlestoredb/utils/xdict.py,sha256=-wi1lSPTnY99fhVMBhPKJ8cCsQhNG4GMUfkEBDKYgCw,13321
128
- singlestoredb-1.7.2.dist-info/LICENSE,sha256=Bojenzui8aPNjlF3w4ojguDP7sTf8vFV_9Gc2UAG1sg,11542
129
- singlestoredb-1.7.2.dist-info/METADATA,sha256=EcxKSeGY473TFRuGP6W42122Mz_hry8hPLXAVseir0A,5710
130
- singlestoredb-1.7.2.dist-info/WHEEL,sha256=c4k7z5HB0t-y0nBCv6KyJ6KCjn8SEGPddD0lhaPtU3E,96
131
- singlestoredb-1.7.2.dist-info/entry_points.txt,sha256=bSLaTWB5zGjpVYPAaI46MkkDup0su-eb3uAhCNYuRV0,48
132
- singlestoredb-1.7.2.dist-info/top_level.txt,sha256=SDtemIXf-Kp-_F2f_S6x0db33cHGOILdAEsIQZe2LZc,35
133
- singlestoredb-1.7.2.dist-info/RECORD,,
131
+ singlestoredb-1.8.0.dist-info/LICENSE,sha256=Bojenzui8aPNjlF3w4ojguDP7sTf8vFV_9Gc2UAG1sg,11542
132
+ singlestoredb-1.8.0.dist-info/METADATA,sha256=Y5VQXcKumZcpjgYh4pPjA4d28O5HP5BQpjNwzYyjYI8,5710
133
+ singlestoredb-1.8.0.dist-info/WHEEL,sha256=c4k7z5HB0t-y0nBCv6KyJ6KCjn8SEGPddD0lhaPtU3E,96
134
+ singlestoredb-1.8.0.dist-info/entry_points.txt,sha256=bSLaTWB5zGjpVYPAaI46MkkDup0su-eb3uAhCNYuRV0,48
135
+ singlestoredb-1.8.0.dist-info/top_level.txt,sha256=SDtemIXf-Kp-_F2f_S6x0db33cHGOILdAEsIQZe2LZc,35
136
+ singlestoredb-1.8.0.dist-info/RECORD,,