singlestoredb 1.14.2__cp38-abi3-win32.whl → 1.15.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 +0 -0
- singlestoredb/__init__.py +2 -2
- singlestoredb/apps/_python_udfs.py +3 -3
- singlestoredb/config.py +5 -0
- singlestoredb/functions/decorator.py +32 -13
- singlestoredb/functions/ext/asgi.py +287 -27
- singlestoredb/functions/ext/timer.py +98 -0
- singlestoredb/functions/typing/numpy.py +20 -0
- singlestoredb/functions/typing/pandas.py +2 -0
- singlestoredb/functions/typing/polars.py +2 -0
- singlestoredb/functions/typing/pyarrow.py +2 -0
- singlestoredb/magics/run_personal.py +82 -1
- singlestoredb/magics/run_shared.py +82 -1
- singlestoredb/management/__init__.py +1 -0
- singlestoredb/management/region.py +92 -0
- singlestoredb/management/workspace.py +180 -1
- singlestoredb/tests/ext_funcs/__init__.py +94 -55
- singlestoredb/tests/test.sql +22 -0
- singlestoredb/tests/test_ext_func.py +90 -0
- singlestoredb/tests/test_management.py +223 -0
- {singlestoredb-1.14.2.dist-info → singlestoredb-1.15.0.dist-info}/METADATA +1 -1
- {singlestoredb-1.14.2.dist-info → singlestoredb-1.15.0.dist-info}/RECORD +27 -22
- /singlestoredb/functions/{typing.py → typing/__init__.py} +0 -0
- {singlestoredb-1.14.2.dist-info → singlestoredb-1.15.0.dist-info}/LICENSE +0 -0
- {singlestoredb-1.14.2.dist-info → singlestoredb-1.15.0.dist-info}/WHEEL +0 -0
- {singlestoredb-1.14.2.dist-info → singlestoredb-1.15.0.dist-info}/entry_points.txt +0 -0
- {singlestoredb-1.14.2.dist-info → singlestoredb-1.15.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing import Dict
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from . import utils
|
|
8
|
+
|
|
9
|
+
logger = utils.get_logger('singlestoredb.functions.ext.metrics')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RoundedFloatEncoder(json.JSONEncoder):
|
|
13
|
+
|
|
14
|
+
def encode(self, obj: Any) -> str:
|
|
15
|
+
if isinstance(obj, dict):
|
|
16
|
+
return '{' + ', '.join(
|
|
17
|
+
f'"{k}": {self._format_value(v)}'
|
|
18
|
+
for k, v in obj.items()
|
|
19
|
+
) + '}'
|
|
20
|
+
return super().encode(obj)
|
|
21
|
+
|
|
22
|
+
def _format_value(self, value: Any) -> str:
|
|
23
|
+
if isinstance(value, float):
|
|
24
|
+
return f'{value:.2f}'
|
|
25
|
+
return json.dumps(value)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Timer:
|
|
29
|
+
"""
|
|
30
|
+
Timer context manager that supports nested timing using a stack.
|
|
31
|
+
|
|
32
|
+
Example
|
|
33
|
+
-------
|
|
34
|
+
timer = Timer()
|
|
35
|
+
|
|
36
|
+
with timer('total'):
|
|
37
|
+
with timer('receive_data'):
|
|
38
|
+
time.sleep(0.1)
|
|
39
|
+
with timer('parse_input'):
|
|
40
|
+
time.sleep(0.2)
|
|
41
|
+
with timer('call_function'):
|
|
42
|
+
with timer('inner_operation'):
|
|
43
|
+
time.sleep(0.05)
|
|
44
|
+
time.sleep(0.3)
|
|
45
|
+
|
|
46
|
+
print(timer.metrics)
|
|
47
|
+
# {'receive_data': 0.1, 'parse_input': 0.2, 'inner_operation': 0.05,
|
|
48
|
+
# 'call_function': 0.35, 'total': 0.65}
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
53
|
+
self.metadata: Dict[str, Any] = kwargs
|
|
54
|
+
self.metrics: Dict[str, float] = dict()
|
|
55
|
+
self.entries: Dict[str, float] = dict()
|
|
56
|
+
self._current_key: Optional[str] = None
|
|
57
|
+
self.start_time = time.perf_counter()
|
|
58
|
+
|
|
59
|
+
def __call__(self, key: str) -> 'Timer':
|
|
60
|
+
self._current_key = key
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def __enter__(self) -> 'Timer':
|
|
64
|
+
if self._current_key is None:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
"No key specified. Use timer('key_name') as context manager.",
|
|
67
|
+
)
|
|
68
|
+
self.entries[self._current_key] = time.perf_counter()
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
72
|
+
key = self._current_key
|
|
73
|
+
if key and key in self.entries:
|
|
74
|
+
start = self.entries.pop(key)
|
|
75
|
+
elapsed = time.perf_counter() - start
|
|
76
|
+
self.metrics[key] = elapsed
|
|
77
|
+
self._current_key = None
|
|
78
|
+
|
|
79
|
+
async def __aenter__(self) -> 'Timer':
|
|
80
|
+
return self.__enter__()
|
|
81
|
+
|
|
82
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
83
|
+
self.__exit__(exc_type, exc_val, exc_tb)
|
|
84
|
+
|
|
85
|
+
def reset(self) -> None:
|
|
86
|
+
self.metrics.clear()
|
|
87
|
+
self.entries.clear()
|
|
88
|
+
self._current_key = None
|
|
89
|
+
|
|
90
|
+
def finish(self) -> None:
|
|
91
|
+
"""Finish the current timing context and store the elapsed time."""
|
|
92
|
+
self.metrics['total'] = time.perf_counter() - self.start_time
|
|
93
|
+
self.log_metrics()
|
|
94
|
+
|
|
95
|
+
def log_metrics(self) -> None:
|
|
96
|
+
if self.metadata.get('function'):
|
|
97
|
+
result = dict(type='function_metrics', **self.metadata, **self.metrics)
|
|
98
|
+
logger.info(json.dumps(result, cls=RoundedFloatEncoder))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import numpy.typing as npt
|
|
3
|
+
|
|
4
|
+
NDArray = npt.NDArray
|
|
5
|
+
|
|
6
|
+
StringArray = StrArray = npt.NDArray[np.str_]
|
|
7
|
+
BytesArray = npt.NDArray[np.bytes_]
|
|
8
|
+
Float32Array = FloatArray = npt.NDArray[np.float32]
|
|
9
|
+
Float64Array = DoubleArray = npt.NDArray[np.float64]
|
|
10
|
+
IntArray = npt.NDArray[np.int_]
|
|
11
|
+
Int8Array = npt.NDArray[np.int8]
|
|
12
|
+
Int16Array = npt.NDArray[np.int16]
|
|
13
|
+
Int32Array = npt.NDArray[np.int32]
|
|
14
|
+
Int64Array = npt.NDArray[np.int64]
|
|
15
|
+
UInt8Array = npt.NDArray[np.uint8]
|
|
16
|
+
UInt16Array = npt.NDArray[np.uint16]
|
|
17
|
+
UInt32Array = npt.NDArray[np.uint32]
|
|
18
|
+
UInt64Array = npt.NDArray[np.uint64]
|
|
19
|
+
DateTimeArray = npt.NDArray[np.datetime64]
|
|
20
|
+
TimeDeltaArray = npt.NDArray[np.timedelta64]
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
from typing import Any
|
|
5
|
+
from warnings import warn
|
|
4
6
|
|
|
5
7
|
from IPython.core.interactiveshell import InteractiveShell
|
|
6
8
|
from IPython.core.magic import line_magic
|
|
@@ -8,6 +10,8 @@ from IPython.core.magic import Magics
|
|
|
8
10
|
from IPython.core.magic import magics_class
|
|
9
11
|
from IPython.core.magic import needs_local_scope
|
|
10
12
|
from IPython.core.magic import no_var_expand
|
|
13
|
+
from IPython.utils.contexts import preserve_keys
|
|
14
|
+
from IPython.utils.syspathcontext import prepended_to_syspath
|
|
11
15
|
from jinja2 import Template
|
|
12
16
|
|
|
13
17
|
|
|
@@ -53,4 +57,81 @@ class RunPersonalMagic(Magics):
|
|
|
53
57
|
# Execute the SQL command
|
|
54
58
|
self.shell.run_line_magic('sql', sql_command)
|
|
55
59
|
# Run the downloaded file
|
|
56
|
-
self.shell.
|
|
60
|
+
with preserve_keys(self.shell.user_ns, '__file__'):
|
|
61
|
+
self.shell.user_ns['__file__'] = temp_file_path
|
|
62
|
+
self.safe_execfile_ipy(temp_file_path, raise_exceptions=True)
|
|
63
|
+
|
|
64
|
+
def safe_execfile_ipy(
|
|
65
|
+
self,
|
|
66
|
+
fname: str,
|
|
67
|
+
shell_futures: bool = False,
|
|
68
|
+
raise_exceptions: bool = False,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Like safe_execfile, but for .ipy or .ipynb files with IPython syntax.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
fname : str
|
|
75
|
+
The name of the file to execute. The filename must have a
|
|
76
|
+
.ipy or .ipynb extension.
|
|
77
|
+
shell_futures : bool (False)
|
|
78
|
+
If True, the code will share future statements with the interactive
|
|
79
|
+
shell. It will both be affected by previous __future__ imports, and
|
|
80
|
+
any __future__ imports in the code will affect the shell. If False,
|
|
81
|
+
__future__ imports are not shared in either direction.
|
|
82
|
+
raise_exceptions : bool (False)
|
|
83
|
+
If True raise exceptions everywhere. Meant for testing.
|
|
84
|
+
"""
|
|
85
|
+
fpath = Path(fname).expanduser().resolve()
|
|
86
|
+
|
|
87
|
+
# Make sure we can open the file
|
|
88
|
+
try:
|
|
89
|
+
with fpath.open('rb'):
|
|
90
|
+
pass
|
|
91
|
+
except Exception:
|
|
92
|
+
warn('Could not open file <%s> for safe execution.' % fpath)
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
# Find things also in current directory. This is needed to mimic the
|
|
96
|
+
# behavior of running a script from the system command line, where
|
|
97
|
+
# Python inserts the script's directory into sys.path
|
|
98
|
+
dname = str(fpath.parent)
|
|
99
|
+
|
|
100
|
+
def get_cells() -> Any:
|
|
101
|
+
"""generator for sequence of code blocks to run"""
|
|
102
|
+
if fpath.suffix == '.ipynb':
|
|
103
|
+
from nbformat import read
|
|
104
|
+
nb = read(fpath, as_version=4)
|
|
105
|
+
if not nb.cells:
|
|
106
|
+
return
|
|
107
|
+
for cell in nb.cells:
|
|
108
|
+
if cell.cell_type == 'code':
|
|
109
|
+
if not cell.source.strip():
|
|
110
|
+
continue
|
|
111
|
+
if getattr(cell, 'metadata', {}).get('language', '') == 'sql':
|
|
112
|
+
output_redirect = getattr(
|
|
113
|
+
cell, 'metadata', {},
|
|
114
|
+
).get('output_variable', '') or ''
|
|
115
|
+
if output_redirect:
|
|
116
|
+
output_redirect = f' {output_redirect} <<'
|
|
117
|
+
yield f'%%sql{output_redirect}\n{cell.source}'
|
|
118
|
+
else:
|
|
119
|
+
yield cell.source
|
|
120
|
+
else:
|
|
121
|
+
yield fpath.read_text(encoding='utf-8')
|
|
122
|
+
|
|
123
|
+
with prepended_to_syspath(dname):
|
|
124
|
+
try:
|
|
125
|
+
for cell in get_cells():
|
|
126
|
+
result = self.shell.run_cell(
|
|
127
|
+
cell, silent=True, shell_futures=shell_futures,
|
|
128
|
+
)
|
|
129
|
+
if raise_exceptions:
|
|
130
|
+
result.raise_error()
|
|
131
|
+
elif not result.success:
|
|
132
|
+
break
|
|
133
|
+
except Exception:
|
|
134
|
+
if raise_exceptions:
|
|
135
|
+
raise
|
|
136
|
+
self.shell.showtraceback()
|
|
137
|
+
warn('Unknown failure executing file: <%s>' % fpath)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
from typing import Any
|
|
5
|
+
from warnings import warn
|
|
4
6
|
|
|
5
7
|
from IPython.core.interactiveshell import InteractiveShell
|
|
6
8
|
from IPython.core.magic import line_magic
|
|
@@ -8,6 +10,8 @@ from IPython.core.magic import Magics
|
|
|
8
10
|
from IPython.core.magic import magics_class
|
|
9
11
|
from IPython.core.magic import needs_local_scope
|
|
10
12
|
from IPython.core.magic import no_var_expand
|
|
13
|
+
from IPython.utils.contexts import preserve_keys
|
|
14
|
+
from IPython.utils.syspathcontext import prepended_to_syspath
|
|
11
15
|
from jinja2 import Template
|
|
12
16
|
|
|
13
17
|
|
|
@@ -50,4 +54,81 @@ class RunSharedMagic(Magics):
|
|
|
50
54
|
# Execute the SQL command
|
|
51
55
|
self.shell.run_line_magic('sql', sql_command)
|
|
52
56
|
# Run the downloaded file
|
|
53
|
-
self.shell.
|
|
57
|
+
with preserve_keys(self.shell.user_ns, '__file__'):
|
|
58
|
+
self.shell.user_ns['__file__'] = temp_file_path
|
|
59
|
+
self.safe_execfile_ipy(temp_file_path, raise_exceptions=True)
|
|
60
|
+
|
|
61
|
+
def safe_execfile_ipy(
|
|
62
|
+
self,
|
|
63
|
+
fname: str,
|
|
64
|
+
shell_futures: bool = False,
|
|
65
|
+
raise_exceptions: bool = False,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Like safe_execfile, but for .ipy or .ipynb files with IPython syntax.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
fname : str
|
|
72
|
+
The name of the file to execute. The filename must have a
|
|
73
|
+
.ipy or .ipynb extension.
|
|
74
|
+
shell_futures : bool (False)
|
|
75
|
+
If True, the code will share future statements with the interactive
|
|
76
|
+
shell. It will both be affected by previous __future__ imports, and
|
|
77
|
+
any __future__ imports in the code will affect the shell. If False,
|
|
78
|
+
__future__ imports are not shared in either direction.
|
|
79
|
+
raise_exceptions : bool (False)
|
|
80
|
+
If True raise exceptions everywhere. Meant for testing.
|
|
81
|
+
"""
|
|
82
|
+
fpath = Path(fname).expanduser().resolve()
|
|
83
|
+
|
|
84
|
+
# Make sure we can open the file
|
|
85
|
+
try:
|
|
86
|
+
with fpath.open('rb'):
|
|
87
|
+
pass
|
|
88
|
+
except Exception:
|
|
89
|
+
warn('Could not open file <%s> for safe execution.' % fpath)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Find things also in current directory. This is needed to mimic the
|
|
93
|
+
# behavior of running a script from the system command line, where
|
|
94
|
+
# Python inserts the script's directory into sys.path
|
|
95
|
+
dname = str(fpath.parent)
|
|
96
|
+
|
|
97
|
+
def get_cells() -> Any:
|
|
98
|
+
"""generator for sequence of code blocks to run"""
|
|
99
|
+
if fpath.suffix == '.ipynb':
|
|
100
|
+
from nbformat import read
|
|
101
|
+
nb = read(fpath, as_version=4)
|
|
102
|
+
if not nb.cells:
|
|
103
|
+
return
|
|
104
|
+
for cell in nb.cells:
|
|
105
|
+
if cell.cell_type == 'code':
|
|
106
|
+
if not cell.source.strip():
|
|
107
|
+
continue
|
|
108
|
+
if getattr(cell, 'metadata', {}).get('language', '') == 'sql':
|
|
109
|
+
output_redirect = getattr(
|
|
110
|
+
cell, 'metadata', {},
|
|
111
|
+
).get('output_variable', '') or ''
|
|
112
|
+
if output_redirect:
|
|
113
|
+
output_redirect = f' {output_redirect} <<'
|
|
114
|
+
yield f'%%sql{output_redirect}\n{cell.source}'
|
|
115
|
+
else:
|
|
116
|
+
yield cell.source
|
|
117
|
+
else:
|
|
118
|
+
yield fpath.read_text(encoding='utf-8')
|
|
119
|
+
|
|
120
|
+
with prepended_to_syspath(dname):
|
|
121
|
+
try:
|
|
122
|
+
for cell in get_cells():
|
|
123
|
+
result = self.shell.run_cell(
|
|
124
|
+
cell, silent=True, shell_futures=shell_futures,
|
|
125
|
+
)
|
|
126
|
+
if raise_exceptions:
|
|
127
|
+
result.raise_error()
|
|
128
|
+
elif not result.success:
|
|
129
|
+
break
|
|
130
|
+
except Exception:
|
|
131
|
+
if raise_exceptions:
|
|
132
|
+
raise
|
|
133
|
+
self.shell.showtraceback()
|
|
134
|
+
warn('Unknown failure executing file: <%s>' % fpath)
|
|
@@ -4,6 +4,7 @@ from typing import Dict
|
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
6
|
from .manager import Manager
|
|
7
|
+
from .utils import NamedList
|
|
7
8
|
from .utils import vars_to_str
|
|
8
9
|
|
|
9
10
|
|
|
@@ -65,3 +66,94 @@ class Region(object):
|
|
|
65
66
|
)
|
|
66
67
|
out._manager = manager
|
|
67
68
|
return out
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class RegionManager(Manager):
|
|
72
|
+
"""
|
|
73
|
+
SingleStoreDB region manager.
|
|
74
|
+
|
|
75
|
+
This class should be instantiated using :func:`singlestoredb.manage_regions`.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
access_token : str, optional
|
|
80
|
+
The API key or other access token for the workspace management API
|
|
81
|
+
version : str, optional
|
|
82
|
+
Version of the API to use
|
|
83
|
+
base_url : str, optional
|
|
84
|
+
Base URL of the workspace management API
|
|
85
|
+
|
|
86
|
+
See Also
|
|
87
|
+
--------
|
|
88
|
+
:func:`singlestoredb.manage_regions`
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
#: Object type
|
|
92
|
+
obj_type = 'region'
|
|
93
|
+
|
|
94
|
+
def list_regions(self) -> NamedList[Region]:
|
|
95
|
+
"""
|
|
96
|
+
List all available regions.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
NamedList[Region]
|
|
101
|
+
List of available regions
|
|
102
|
+
|
|
103
|
+
Raises
|
|
104
|
+
------
|
|
105
|
+
ManagementError
|
|
106
|
+
If there is an error getting the regions
|
|
107
|
+
"""
|
|
108
|
+
res = self._get('regions')
|
|
109
|
+
return NamedList(
|
|
110
|
+
[Region.from_dict(item, self) for item in res.json()],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def list_shared_tier_regions(self) -> NamedList[Region]:
|
|
114
|
+
"""
|
|
115
|
+
List regions that support shared tier workspaces.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
NamedList[Region]
|
|
120
|
+
List of regions that support shared tier workspaces
|
|
121
|
+
|
|
122
|
+
Raises
|
|
123
|
+
------
|
|
124
|
+
ManagementError
|
|
125
|
+
If there is an error getting the regions
|
|
126
|
+
"""
|
|
127
|
+
res = self._get('regions/sharedtier')
|
|
128
|
+
return NamedList(
|
|
129
|
+
[Region.from_dict(item, self) for item in res.json()],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def manage_regions(
|
|
134
|
+
access_token: Optional[str] = None,
|
|
135
|
+
version: Optional[str] = None,
|
|
136
|
+
base_url: Optional[str] = None,
|
|
137
|
+
) -> RegionManager:
|
|
138
|
+
"""
|
|
139
|
+
Retrieve a SingleStoreDB region manager.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
access_token : str, optional
|
|
144
|
+
The API key or other access token for the workspace management API
|
|
145
|
+
version : str, optional
|
|
146
|
+
Version of the API to use
|
|
147
|
+
base_url : str, optional
|
|
148
|
+
Base URL of the workspace management API
|
|
149
|
+
|
|
150
|
+
Returns
|
|
151
|
+
-------
|
|
152
|
+
:class:`RegionManager`
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
return RegionManager(
|
|
156
|
+
access_token=access_token,
|
|
157
|
+
version=version,
|
|
158
|
+
base_url=base_url,
|
|
159
|
+
)
|
|
@@ -1301,17 +1301,24 @@ class StarterWorkspace(object):
|
|
|
1301
1301
|
See Also
|
|
1302
1302
|
--------
|
|
1303
1303
|
:meth:`WorkspaceManager.get_starter_workspace`
|
|
1304
|
+
:meth:`WorkspaceManager.create_starter_workspace`
|
|
1305
|
+
:meth:`WorkspaceManager.terminate_starter_workspace`
|
|
1306
|
+
:meth:`WorkspaceManager.create_starter_workspace_user`
|
|
1304
1307
|
:attr:`WorkspaceManager.starter_workspaces`
|
|
1305
1308
|
|
|
1306
1309
|
"""
|
|
1307
1310
|
|
|
1308
1311
|
name: str
|
|
1309
1312
|
id: str
|
|
1313
|
+
database_name: str
|
|
1314
|
+
endpoint: Optional[str]
|
|
1310
1315
|
|
|
1311
1316
|
def __init__(
|
|
1312
1317
|
self,
|
|
1313
1318
|
name: str,
|
|
1314
1319
|
id: str,
|
|
1320
|
+
database_name: str,
|
|
1321
|
+
endpoint: Optional[str] = None,
|
|
1315
1322
|
):
|
|
1316
1323
|
#: Name of the starter workspace
|
|
1317
1324
|
self.name = name
|
|
@@ -1319,6 +1326,13 @@ class StarterWorkspace(object):
|
|
|
1319
1326
|
#: Unique ID of the starter workspace
|
|
1320
1327
|
self.id = id
|
|
1321
1328
|
|
|
1329
|
+
#: Name of the database associated with the starter workspace
|
|
1330
|
+
self.database_name = database_name
|
|
1331
|
+
|
|
1332
|
+
#: Endpoint to connect to the starter workspace. The endpoint is in the form
|
|
1333
|
+
#: of ``hostname:port``
|
|
1334
|
+
self.endpoint = endpoint
|
|
1335
|
+
|
|
1322
1336
|
self._manager: Optional[WorkspaceManager] = None
|
|
1323
1337
|
|
|
1324
1338
|
def __str__(self) -> str:
|
|
@@ -1351,10 +1365,63 @@ class StarterWorkspace(object):
|
|
|
1351
1365
|
out = cls(
|
|
1352
1366
|
name=obj['name'],
|
|
1353
1367
|
id=obj['virtualWorkspaceID'],
|
|
1368
|
+
database_name=obj['databaseName'],
|
|
1369
|
+
endpoint=obj.get('endpoint'),
|
|
1354
1370
|
)
|
|
1355
1371
|
out._manager = manager
|
|
1356
1372
|
return out
|
|
1357
1373
|
|
|
1374
|
+
def connect(self, **kwargs: Any) -> connection.Connection:
|
|
1375
|
+
"""
|
|
1376
|
+
Create a connection to the database server for this starter workspace.
|
|
1377
|
+
|
|
1378
|
+
Parameters
|
|
1379
|
+
----------
|
|
1380
|
+
**kwargs : keyword-arguments, optional
|
|
1381
|
+
Parameters to the SingleStoreDB `connect` function except host
|
|
1382
|
+
and port which are supplied by the starter workspace object
|
|
1383
|
+
|
|
1384
|
+
Returns
|
|
1385
|
+
-------
|
|
1386
|
+
:class:`Connection`
|
|
1387
|
+
|
|
1388
|
+
"""
|
|
1389
|
+
if not self.endpoint:
|
|
1390
|
+
raise ManagementError(
|
|
1391
|
+
msg='An endpoint has not been set in this '
|
|
1392
|
+
'starter workspace configuration',
|
|
1393
|
+
)
|
|
1394
|
+
# Parse endpoint as host:port
|
|
1395
|
+
if ':' in self.endpoint:
|
|
1396
|
+
host, port = self.endpoint.split(':', 1)
|
|
1397
|
+
kwargs['host'] = host
|
|
1398
|
+
kwargs['port'] = int(port)
|
|
1399
|
+
else:
|
|
1400
|
+
kwargs['host'] = self.endpoint
|
|
1401
|
+
return connection.connect(**kwargs)
|
|
1402
|
+
|
|
1403
|
+
def terminate(self) -> None:
|
|
1404
|
+
"""Terminate the starter workspace."""
|
|
1405
|
+
if self._manager is None:
|
|
1406
|
+
raise ManagementError(
|
|
1407
|
+
msg='No workspace manager is associated with this object.',
|
|
1408
|
+
)
|
|
1409
|
+
self._manager._delete(f'sharedtier/virtualWorkspaces/{self.id}')
|
|
1410
|
+
|
|
1411
|
+
def refresh(self) -> StarterWorkspace:
|
|
1412
|
+
"""Update the object to the current state."""
|
|
1413
|
+
if self._manager is None:
|
|
1414
|
+
raise ManagementError(
|
|
1415
|
+
msg='No workspace manager is associated with this object.',
|
|
1416
|
+
)
|
|
1417
|
+
new_obj = self._manager.get_starter_workspace(self.id)
|
|
1418
|
+
for name, value in vars(new_obj).items():
|
|
1419
|
+
if isinstance(value, Mapping):
|
|
1420
|
+
setattr(self, name, snake_to_camel_dict(value))
|
|
1421
|
+
else:
|
|
1422
|
+
setattr(self, name, value)
|
|
1423
|
+
return self
|
|
1424
|
+
|
|
1358
1425
|
@property
|
|
1359
1426
|
def organization(self) -> Organization:
|
|
1360
1427
|
if self._manager is None:
|
|
@@ -1375,7 +1442,7 @@ class StarterWorkspace(object):
|
|
|
1375
1442
|
stages = stage
|
|
1376
1443
|
|
|
1377
1444
|
@property
|
|
1378
|
-
def starter_workspaces(self) -> NamedList[StarterWorkspace]:
|
|
1445
|
+
def starter_workspaces(self) -> NamedList['StarterWorkspace']:
|
|
1379
1446
|
"""Return a list of available starter workspaces."""
|
|
1380
1447
|
if self._manager is None:
|
|
1381
1448
|
raise ManagementError(
|
|
@@ -1386,6 +1453,64 @@ class StarterWorkspace(object):
|
|
|
1386
1453
|
[StarterWorkspace.from_dict(item, self._manager) for item in res.json()],
|
|
1387
1454
|
)
|
|
1388
1455
|
|
|
1456
|
+
def create_user(
|
|
1457
|
+
self,
|
|
1458
|
+
user_name: str,
|
|
1459
|
+
password: Optional[str] = None,
|
|
1460
|
+
) -> Dict[str, str]:
|
|
1461
|
+
"""
|
|
1462
|
+
Create a new user for this starter workspace.
|
|
1463
|
+
|
|
1464
|
+
Parameters
|
|
1465
|
+
----------
|
|
1466
|
+
user_name : str
|
|
1467
|
+
The starter workspace user name to connect the new user to the database
|
|
1468
|
+
password : str, optional
|
|
1469
|
+
Password for the new user. If not provided, a password will be
|
|
1470
|
+
auto-generated by the system.
|
|
1471
|
+
|
|
1472
|
+
Returns
|
|
1473
|
+
-------
|
|
1474
|
+
Dict[str, str]
|
|
1475
|
+
Dictionary containing 'userID' and 'password' of the created user
|
|
1476
|
+
|
|
1477
|
+
Raises
|
|
1478
|
+
------
|
|
1479
|
+
ManagementError
|
|
1480
|
+
If no workspace manager is associated with this object.
|
|
1481
|
+
"""
|
|
1482
|
+
if self._manager is None:
|
|
1483
|
+
raise ManagementError(
|
|
1484
|
+
msg='No workspace manager is associated with this object.',
|
|
1485
|
+
)
|
|
1486
|
+
|
|
1487
|
+
payload = {
|
|
1488
|
+
'userName': user_name,
|
|
1489
|
+
}
|
|
1490
|
+
if password is not None:
|
|
1491
|
+
payload['password'] = password
|
|
1492
|
+
|
|
1493
|
+
res = self._manager._post(
|
|
1494
|
+
f'sharedtier/virtualWorkspaces/{self.id}/users',
|
|
1495
|
+
json=payload,
|
|
1496
|
+
)
|
|
1497
|
+
|
|
1498
|
+
response_data = res.json()
|
|
1499
|
+
user_id = response_data.get('userID')
|
|
1500
|
+
if not user_id:
|
|
1501
|
+
raise ManagementError(msg='No userID returned from API')
|
|
1502
|
+
|
|
1503
|
+
# Return the password provided by user or generated by API
|
|
1504
|
+
returned_password = password if password is not None \
|
|
1505
|
+
else response_data.get('password')
|
|
1506
|
+
if not returned_password:
|
|
1507
|
+
raise ManagementError(msg='No password available from API response')
|
|
1508
|
+
|
|
1509
|
+
return {
|
|
1510
|
+
'user_id': user_id,
|
|
1511
|
+
'password': returned_password,
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1389
1514
|
|
|
1390
1515
|
class Billing(object):
|
|
1391
1516
|
"""Billing information."""
|
|
@@ -1522,6 +1647,14 @@ class WorkspaceManager(Manager):
|
|
|
1522
1647
|
res = self._get('regions')
|
|
1523
1648
|
return NamedList([Region.from_dict(item, self) for item in res.json()])
|
|
1524
1649
|
|
|
1650
|
+
@ttl_property(datetime.timedelta(hours=1))
|
|
1651
|
+
def shared_tier_regions(self) -> NamedList[Region]:
|
|
1652
|
+
"""Return a list of regions that support shared tier workspaces."""
|
|
1653
|
+
res = self._get('regions/sharedtier')
|
|
1654
|
+
return NamedList(
|
|
1655
|
+
[Region.from_dict(item, self) for item in res.json()],
|
|
1656
|
+
)
|
|
1657
|
+
|
|
1525
1658
|
def create_workspace_group(
|
|
1526
1659
|
self,
|
|
1527
1660
|
name: str,
|
|
@@ -1717,6 +1850,52 @@ class WorkspaceManager(Manager):
|
|
|
1717
1850
|
res = self._get(f'sharedtier/virtualWorkspaces/{id}')
|
|
1718
1851
|
return StarterWorkspace.from_dict(res.json(), manager=self)
|
|
1719
1852
|
|
|
1853
|
+
def create_starter_workspace(
|
|
1854
|
+
self,
|
|
1855
|
+
name: str,
|
|
1856
|
+
database_name: str,
|
|
1857
|
+
workspace_group: dict[str, str],
|
|
1858
|
+
) -> 'StarterWorkspace':
|
|
1859
|
+
"""
|
|
1860
|
+
Create a new starter (shared tier) workspace.
|
|
1861
|
+
|
|
1862
|
+
Parameters
|
|
1863
|
+
----------
|
|
1864
|
+
name : str
|
|
1865
|
+
Name of the starter workspace
|
|
1866
|
+
database_name : str
|
|
1867
|
+
Name of the database for the starter workspace
|
|
1868
|
+
workspace_group : dict[str, str]
|
|
1869
|
+
Workspace group input (dict with keys: 'cell_id')
|
|
1870
|
+
|
|
1871
|
+
Returns
|
|
1872
|
+
-------
|
|
1873
|
+
:class:`StarterWorkspace`
|
|
1874
|
+
"""
|
|
1875
|
+
if not workspace_group or not isinstance(workspace_group, dict):
|
|
1876
|
+
raise ValueError(
|
|
1877
|
+
'workspace_group must be a dict with keys: '
|
|
1878
|
+
"'cell_id'",
|
|
1879
|
+
)
|
|
1880
|
+
if set(workspace_group.keys()) != {'cell_id'}:
|
|
1881
|
+
raise ValueError("workspace_group must contain only 'cell_id'")
|
|
1882
|
+
|
|
1883
|
+
payload = {
|
|
1884
|
+
'name': name,
|
|
1885
|
+
'databaseName': database_name,
|
|
1886
|
+
'workspaceGroup': {
|
|
1887
|
+
'cellID': workspace_group['cell_id'],
|
|
1888
|
+
},
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
res = self._post('sharedtier/virtualWorkspaces', json=payload)
|
|
1892
|
+
virtual_workspace_id = res.json().get('virtualWorkspaceID')
|
|
1893
|
+
if not virtual_workspace_id:
|
|
1894
|
+
raise ManagementError(msg='No virtualWorkspaceID returned from API')
|
|
1895
|
+
|
|
1896
|
+
res = self._get(f'sharedtier/virtualWorkspaces/{virtual_workspace_id}')
|
|
1897
|
+
return StarterWorkspace.from_dict(res.json(), self)
|
|
1898
|
+
|
|
1720
1899
|
|
|
1721
1900
|
def manage_workspaces(
|
|
1722
1901
|
access_token: Optional[str] = None,
|