singlestoredb 0.4.0__py3-none-any.whl → 1.0.3__py3-none-any.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/__init__.py +33 -1
- singlestoredb/alchemy/__init__.py +90 -0
- singlestoredb/auth.py +5 -1
- singlestoredb/config.py +116 -14
- singlestoredb/connection.py +483 -516
- singlestoredb/converters.py +238 -135
- singlestoredb/exceptions.py +30 -2
- singlestoredb/functions/__init__.py +1 -0
- singlestoredb/functions/decorator.py +142 -0
- singlestoredb/functions/dtypes.py +1639 -0
- singlestoredb/functions/ext/__init__.py +2 -0
- singlestoredb/functions/ext/arrow.py +375 -0
- singlestoredb/functions/ext/asgi.py +661 -0
- singlestoredb/functions/ext/json.py +427 -0
- singlestoredb/functions/ext/mmap.py +306 -0
- singlestoredb/functions/ext/rowdat_1.py +744 -0
- singlestoredb/functions/signature.py +673 -0
- singlestoredb/fusion/__init__.py +11 -0
- singlestoredb/fusion/graphql.py +213 -0
- singlestoredb/fusion/handler.py +621 -0
- singlestoredb/fusion/handlers/stage.py +257 -0
- singlestoredb/fusion/handlers/utils.py +162 -0
- singlestoredb/fusion/handlers/workspace.py +412 -0
- singlestoredb/fusion/registry.py +164 -0
- singlestoredb/fusion/result.py +399 -0
- singlestoredb/http/__init__.py +27 -0
- singlestoredb/{http.py → http/connection.py} +555 -154
- singlestoredb/management/__init__.py +3 -0
- singlestoredb/management/billing_usage.py +148 -0
- singlestoredb/management/cluster.py +14 -6
- singlestoredb/management/manager.py +100 -38
- singlestoredb/management/organization.py +188 -0
- singlestoredb/management/region.py +5 -5
- singlestoredb/management/utils.py +251 -2
- singlestoredb/management/workspace.py +1149 -28
- singlestoredb/{clients/pymysqlsv → mysql}/__init__.py +16 -21
- singlestoredb/{clients/pymysqlsv → mysql}/_auth.py +39 -8
- singlestoredb/{clients/pymysqlsv → mysql}/charset.py +26 -23
- singlestoredb/{clients/pymysqlsv/connections.py → mysql/connection.py} +532 -165
- singlestoredb/{clients/pymysqlsv → mysql}/constants/CLIENT.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/COMMAND.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/CR.py +0 -2
- singlestoredb/{clients/pymysqlsv → mysql}/constants/ER.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/FIELD_TYPE.py +1 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/FLAG.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/SERVER_STATUS.py +0 -1
- singlestoredb/mysql/converters.py +271 -0
- singlestoredb/{clients/pymysqlsv → mysql}/cursors.py +228 -112
- singlestoredb/mysql/err.py +92 -0
- singlestoredb/{clients/pymysqlsv → mysql}/optionfile.py +5 -4
- singlestoredb/{clients/pymysqlsv → mysql}/protocol.py +49 -20
- singlestoredb/mysql/tests/__init__.py +19 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/base.py +32 -12
- singlestoredb/mysql/tests/conftest.py +37 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_DictCursor.py +11 -7
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_SSCursor.py +17 -12
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_basic.py +32 -24
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_connection.py +130 -119
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_converters.py +9 -7
- singlestoredb/mysql/tests/test_cursor.py +141 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_err.py +3 -2
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_issues.py +35 -27
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_load_local.py +13 -11
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_nextset.py +7 -3
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_optionfile.py +2 -1
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/__init__.py +1 -1
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/capabilities.py +19 -17
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/dbapi20.py +31 -22
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +3 -4
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +24 -20
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +4 -4
- singlestoredb/{clients/pymysqlsv → mysql}/times.py +3 -4
- singlestoredb/pytest.py +283 -0
- singlestoredb/tests/empty.sql +0 -0
- singlestoredb/tests/ext_funcs/__init__.py +385 -0
- singlestoredb/tests/test.sql +210 -0
- singlestoredb/tests/test2.sql +1 -0
- singlestoredb/tests/test_basics.py +482 -115
- singlestoredb/tests/test_config.py +13 -13
- singlestoredb/tests/test_connection.py +241 -305
- singlestoredb/tests/test_dbapi.py +27 -0
- singlestoredb/tests/test_ext_func.py +1193 -0
- singlestoredb/tests/test_ext_func_data.py +1101 -0
- singlestoredb/tests/test_fusion.py +465 -0
- singlestoredb/tests/test_http.py +32 -26
- singlestoredb/tests/test_management.py +588 -8
- singlestoredb/tests/test_plugin.py +33 -0
- singlestoredb/tests/test_results.py +11 -12
- singlestoredb/tests/test_udf.py +687 -0
- singlestoredb/tests/utils.py +3 -2
- singlestoredb/utils/config.py +58 -0
- singlestoredb/utils/debug.py +13 -0
- singlestoredb/utils/mogrify.py +151 -0
- singlestoredb/utils/results.py +4 -1
- singlestoredb-1.0.3.dist-info/METADATA +139 -0
- singlestoredb-1.0.3.dist-info/RECORD +112 -0
- {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.3.dist-info}/WHEEL +1 -1
- singlestoredb-1.0.3.dist-info/entry_points.txt +2 -0
- singlestoredb/clients/pymysqlsv/converters.py +0 -365
- singlestoredb/clients/pymysqlsv/err.py +0 -144
- singlestoredb/clients/pymysqlsv/tests/__init__.py +0 -19
- singlestoredb/clients/pymysqlsv/tests/test_cursor.py +0 -133
- singlestoredb/clients/pymysqlsv/tests/thirdparty/test_MySQLdb/__init__.py +0 -9
- singlestoredb/drivers/__init__.py +0 -45
- singlestoredb/drivers/base.py +0 -198
- singlestoredb/drivers/cymysql.py +0 -38
- singlestoredb/drivers/http.py +0 -47
- singlestoredb/drivers/mariadb.py +0 -40
- singlestoredb/drivers/mysqlconnector.py +0 -49
- singlestoredb/drivers/mysqldb.py +0 -60
- singlestoredb/drivers/pymysql.py +0 -37
- singlestoredb/drivers/pymysqlsv.py +0 -35
- singlestoredb/drivers/pyodbc.py +0 -65
- singlestoredb-0.4.0.dist-info/METADATA +0 -111
- singlestoredb-0.4.0.dist-info/RECORD +0 -86
- /singlestoredb/{clients → fusion/handlers}/__init__.py +0 -0
- /singlestoredb/{clients/pymysqlsv → mysql}/constants/__init__.py +0 -0
- {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.3.dist-info}/LICENSE +0 -0
- {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.3.dist-info}/top_level.txt +0 -0
|
@@ -1,20 +1,911 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
"""SingleStoreDB Workspace Management."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import datetime
|
|
6
|
+
import glob
|
|
7
|
+
import io
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import socket
|
|
11
|
+
import time
|
|
4
12
|
from typing import Any
|
|
13
|
+
from typing import BinaryIO
|
|
5
14
|
from typing import Dict
|
|
6
15
|
from typing import List
|
|
7
16
|
from typing import Optional
|
|
17
|
+
from typing import TextIO
|
|
8
18
|
from typing import Union
|
|
9
19
|
|
|
10
20
|
from .. import connection
|
|
11
21
|
from ..exceptions import ManagementError
|
|
22
|
+
from .billing_usage import BillingUsageItem
|
|
12
23
|
from .manager import Manager
|
|
24
|
+
from .organization import Organization
|
|
13
25
|
from .region import Region
|
|
26
|
+
from .utils import from_datetime
|
|
27
|
+
from .utils import NamedList
|
|
28
|
+
from .utils import PathLike
|
|
29
|
+
from .utils import snake_to_camel
|
|
14
30
|
from .utils import to_datetime
|
|
31
|
+
from .utils import ttl_property
|
|
15
32
|
from .utils import vars_to_str
|
|
16
33
|
|
|
17
34
|
|
|
35
|
+
def get_secret(name: str) -> str:
|
|
36
|
+
"""Get a secret from the organization."""
|
|
37
|
+
return manage_workspaces().organization.get_secret(name).value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class StageObject(object):
|
|
41
|
+
"""
|
|
42
|
+
Stage file / folder object.
|
|
43
|
+
|
|
44
|
+
This object is not instantiated directly. It is used in the results
|
|
45
|
+
of various operations in ``WorkspaceGroup.stage`` methods.
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
name: str,
|
|
52
|
+
path: str,
|
|
53
|
+
size: int,
|
|
54
|
+
type: str,
|
|
55
|
+
format: str,
|
|
56
|
+
mimetype: str,
|
|
57
|
+
created: Optional[datetime.datetime],
|
|
58
|
+
last_modified: Optional[datetime.datetime],
|
|
59
|
+
writable: bool,
|
|
60
|
+
content: Optional[List[str]] = None,
|
|
61
|
+
):
|
|
62
|
+
#: Name of file / folder
|
|
63
|
+
self.name = name
|
|
64
|
+
|
|
65
|
+
if type == 'directory':
|
|
66
|
+
path = re.sub(r'/*$', r'', str(path)) + '/'
|
|
67
|
+
|
|
68
|
+
#: Path of file / folder
|
|
69
|
+
self.path = path
|
|
70
|
+
|
|
71
|
+
#: Size of the object (in bytes)
|
|
72
|
+
self.size = size
|
|
73
|
+
|
|
74
|
+
#: Data type: file or directory
|
|
75
|
+
self.type = type
|
|
76
|
+
|
|
77
|
+
#: Data format
|
|
78
|
+
self.format = format
|
|
79
|
+
|
|
80
|
+
#: Mime type
|
|
81
|
+
self.mimetype = mimetype
|
|
82
|
+
|
|
83
|
+
#: Datetime the object was created
|
|
84
|
+
self.created_at = created
|
|
85
|
+
|
|
86
|
+
#: Datetime the object was modified last
|
|
87
|
+
self.last_modified_at = last_modified
|
|
88
|
+
|
|
89
|
+
#: Is the object writable?
|
|
90
|
+
self.writable = writable
|
|
91
|
+
|
|
92
|
+
#: Contents of a directory
|
|
93
|
+
self.content: List[str] = content or []
|
|
94
|
+
|
|
95
|
+
self._stage: Optional[Stage] = None
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def from_dict(
|
|
99
|
+
cls,
|
|
100
|
+
obj: Dict[str, Any],
|
|
101
|
+
stage: Stage,
|
|
102
|
+
) -> StageObject:
|
|
103
|
+
"""
|
|
104
|
+
Construct a StageObject from a dictionary of values.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
obj : dict
|
|
109
|
+
Dictionary of values
|
|
110
|
+
stage : Stage
|
|
111
|
+
Stage object to use as the parent
|
|
112
|
+
|
|
113
|
+
Returns
|
|
114
|
+
-------
|
|
115
|
+
:class:`StageObject`
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
out = cls(
|
|
119
|
+
name=obj['name'],
|
|
120
|
+
path=obj['path'],
|
|
121
|
+
size=obj['size'],
|
|
122
|
+
type=obj['type'],
|
|
123
|
+
format=obj['format'],
|
|
124
|
+
mimetype=obj['mimetype'],
|
|
125
|
+
created=to_datetime(obj.get('created')),
|
|
126
|
+
last_modified=to_datetime(obj.get('last_modified')),
|
|
127
|
+
writable=bool(obj['writable']),
|
|
128
|
+
)
|
|
129
|
+
out._stage = stage
|
|
130
|
+
return out
|
|
131
|
+
|
|
132
|
+
def __str__(self) -> str:
|
|
133
|
+
"""Return string representation."""
|
|
134
|
+
return vars_to_str(self)
|
|
135
|
+
|
|
136
|
+
def __repr__(self) -> str:
|
|
137
|
+
"""Return string representation."""
|
|
138
|
+
return str(self)
|
|
139
|
+
|
|
140
|
+
def open(
|
|
141
|
+
self,
|
|
142
|
+
mode: str = 'r',
|
|
143
|
+
encoding: Optional[str] = None,
|
|
144
|
+
) -> Union[io.StringIO, io.BytesIO]:
|
|
145
|
+
"""
|
|
146
|
+
Open a Stage path for reading or writing.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
mode : str, optional
|
|
151
|
+
The read / write mode. The following modes are supported:
|
|
152
|
+
* 'r' open for reading (default)
|
|
153
|
+
* 'w' open for writing, truncating the file first
|
|
154
|
+
* 'x' create a new file and open it for writing
|
|
155
|
+
The data type can be specified by adding one of the following:
|
|
156
|
+
* 'b' binary mode
|
|
157
|
+
* 't' text mode (default)
|
|
158
|
+
encoding : str, optional
|
|
159
|
+
The string encoding to use for text
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
StageObjectBytesReader - 'rb' or 'b' mode
|
|
164
|
+
StageObjectBytesWriter - 'wb' or 'xb' mode
|
|
165
|
+
StageObjectTextReader - 'r' or 'rt' mode
|
|
166
|
+
StageObjectTextWriter - 'w', 'x', 'wt' or 'xt' mode
|
|
167
|
+
|
|
168
|
+
"""
|
|
169
|
+
if self._stage is None:
|
|
170
|
+
raise ManagementError(
|
|
171
|
+
msg='No Stage object is associated with this object.',
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if self.is_dir():
|
|
175
|
+
raise IsADirectoryError(
|
|
176
|
+
f'directories can not be read or written: {self.path}',
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return self._stage.open(self.path, mode=mode, encoding=encoding)
|
|
180
|
+
|
|
181
|
+
def download(
|
|
182
|
+
self,
|
|
183
|
+
local_path: Optional[PathLike] = None,
|
|
184
|
+
*,
|
|
185
|
+
overwrite: bool = False,
|
|
186
|
+
encoding: Optional[str] = None,
|
|
187
|
+
) -> Optional[Union[bytes, str]]:
|
|
188
|
+
"""
|
|
189
|
+
Download the content of a stage path.
|
|
190
|
+
|
|
191
|
+
Parameters
|
|
192
|
+
----------
|
|
193
|
+
local_path : Path or str
|
|
194
|
+
Path to local file target location
|
|
195
|
+
overwrite : bool, optional
|
|
196
|
+
Should an existing file be overwritten if it exists?
|
|
197
|
+
encoding : str, optional
|
|
198
|
+
Encoding used to convert the resulting data
|
|
199
|
+
|
|
200
|
+
Returns
|
|
201
|
+
-------
|
|
202
|
+
bytes or str or None
|
|
203
|
+
|
|
204
|
+
"""
|
|
205
|
+
if self._stage is None:
|
|
206
|
+
raise ManagementError(
|
|
207
|
+
msg='No Stage object is associated with this object.',
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return self._stage.download_file(
|
|
211
|
+
self.path, local_path=local_path,
|
|
212
|
+
overwrite=overwrite, encoding=encoding,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
download_file = download
|
|
216
|
+
|
|
217
|
+
def remove(self) -> None:
|
|
218
|
+
"""Delete the stage file."""
|
|
219
|
+
if self._stage is None:
|
|
220
|
+
raise ManagementError(
|
|
221
|
+
msg='No Stage object is associated with this object.',
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if self.type == 'directory':
|
|
225
|
+
raise IsADirectoryError(
|
|
226
|
+
f'path is a directory; use rmdir or removedirs {self.path}',
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
self._stage.remove(self.path)
|
|
230
|
+
|
|
231
|
+
def rmdir(self) -> None:
|
|
232
|
+
"""Delete the empty stage directory."""
|
|
233
|
+
if self._stage is None:
|
|
234
|
+
raise ManagementError(
|
|
235
|
+
msg='No Stage object is associated with this object.',
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if self.type != 'directory':
|
|
239
|
+
raise NotADirectoryError(
|
|
240
|
+
f'path is not a directory: {self.path}',
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
self._stage.rmdir(self.path)
|
|
244
|
+
|
|
245
|
+
def removedirs(self) -> None:
|
|
246
|
+
"""Delete the stage directory recursively."""
|
|
247
|
+
if self._stage is None:
|
|
248
|
+
raise ManagementError(
|
|
249
|
+
msg='No Stage object is associated with this object.',
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if self.type != 'directory':
|
|
253
|
+
raise NotADirectoryError(
|
|
254
|
+
f'path is not a directory: {self.path}',
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
self._stage.removedirs(self.path)
|
|
258
|
+
|
|
259
|
+
def rename(self, new_path: PathLike, *, overwrite: bool = False) -> None:
|
|
260
|
+
"""
|
|
261
|
+
Move the stage file to a new location.
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
new_path : Path or str
|
|
266
|
+
The new location of the file
|
|
267
|
+
overwrite : bool, optional
|
|
268
|
+
Should path be overwritten if it already exists?
|
|
269
|
+
|
|
270
|
+
"""
|
|
271
|
+
if self._stage is None:
|
|
272
|
+
raise ManagementError(
|
|
273
|
+
msg='No Stage object is associated with this object.',
|
|
274
|
+
)
|
|
275
|
+
out = self._stage.rename(self.path, new_path, overwrite=overwrite)
|
|
276
|
+
self.name = out.name
|
|
277
|
+
self.path = out.path
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
def exists(self) -> bool:
|
|
281
|
+
"""Does the file / folder exist?"""
|
|
282
|
+
if self._stage is None:
|
|
283
|
+
raise ManagementError(
|
|
284
|
+
msg='No Stage object is associated with this object.',
|
|
285
|
+
)
|
|
286
|
+
return self._stage.exists(self.path)
|
|
287
|
+
|
|
288
|
+
def is_dir(self) -> bool:
|
|
289
|
+
"""Is the stage object a directory?"""
|
|
290
|
+
return self.type == 'directory'
|
|
291
|
+
|
|
292
|
+
def is_file(self) -> bool:
|
|
293
|
+
"""Is the stage object a file?"""
|
|
294
|
+
return self.type != 'directory'
|
|
295
|
+
|
|
296
|
+
def abspath(self) -> str:
|
|
297
|
+
"""Return the full path of the object."""
|
|
298
|
+
return str(self.path)
|
|
299
|
+
|
|
300
|
+
def basename(self) -> str:
|
|
301
|
+
"""Return the basename of the object."""
|
|
302
|
+
return self.name
|
|
303
|
+
|
|
304
|
+
def dirname(self) -> str:
|
|
305
|
+
"""Return the directory name of the object."""
|
|
306
|
+
return re.sub(r'/*$', r'', os.path.dirname(re.sub(r'/*$', r'', self.path))) + '/'
|
|
307
|
+
|
|
308
|
+
def getmtime(self) -> float:
|
|
309
|
+
"""Return the last modified datetime as a UNIX timestamp."""
|
|
310
|
+
if self.last_modified_at is None:
|
|
311
|
+
return 0.0
|
|
312
|
+
return self.last_modified_at.timestamp()
|
|
313
|
+
|
|
314
|
+
def getctime(self) -> float:
|
|
315
|
+
"""Return the creation datetime as a UNIX timestamp."""
|
|
316
|
+
if self.created_at is None:
|
|
317
|
+
return 0.0
|
|
318
|
+
return self.created_at.timestamp()
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class StageObjectTextWriter(io.StringIO):
|
|
322
|
+
"""StringIO wrapper for writing to Stage."""
|
|
323
|
+
|
|
324
|
+
def __init__(self, buffer: Optional[str], stage: Stage, stage_path: PathLike):
|
|
325
|
+
self._stage = stage
|
|
326
|
+
self._stage_path = stage_path
|
|
327
|
+
super().__init__(buffer)
|
|
328
|
+
|
|
329
|
+
def close(self) -> None:
|
|
330
|
+
"""Write the content to the stage path."""
|
|
331
|
+
self._stage._upload(self.getvalue(), self._stage_path)
|
|
332
|
+
super().close()
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class StageObjectTextReader(io.StringIO):
|
|
336
|
+
"""StringIO wrapper for reading from Stage."""
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class StageObjectBytesWriter(io.BytesIO):
|
|
340
|
+
"""BytesIO wrapper for writing to Stage."""
|
|
341
|
+
|
|
342
|
+
def __init__(self, buffer: bytes, stage: Stage, stage_path: PathLike):
|
|
343
|
+
self._stage = stage
|
|
344
|
+
self._stage_path = stage_path
|
|
345
|
+
super().__init__(buffer)
|
|
346
|
+
|
|
347
|
+
def close(self) -> None:
|
|
348
|
+
"""Write the content to the stage path."""
|
|
349
|
+
self._stage._upload(self.getvalue(), self._stage_path)
|
|
350
|
+
super().close()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class StageObjectBytesReader(io.BytesIO):
|
|
354
|
+
"""BytesIO wrapper for reading from Stage."""
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class Stage(object):
|
|
358
|
+
"""
|
|
359
|
+
Stage manager.
|
|
360
|
+
|
|
361
|
+
This object is not instantiated directly.
|
|
362
|
+
It is returned by ``WorkspaceGroup.stage``.
|
|
363
|
+
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
def __init__(self, workspace_group: WorkspaceGroup, manager: WorkspaceManager):
|
|
367
|
+
self._workspace_group = workspace_group
|
|
368
|
+
self._manager = manager
|
|
369
|
+
|
|
370
|
+
def open(
|
|
371
|
+
self,
|
|
372
|
+
stage_path: PathLike,
|
|
373
|
+
mode: str = 'r',
|
|
374
|
+
encoding: Optional[str] = None,
|
|
375
|
+
) -> Union[io.StringIO, io.BytesIO]:
|
|
376
|
+
"""
|
|
377
|
+
Open a Stage path for reading or writing.
|
|
378
|
+
|
|
379
|
+
Parameters
|
|
380
|
+
----------
|
|
381
|
+
stage_path : Path or str
|
|
382
|
+
The stage path to read / write
|
|
383
|
+
mode : str, optional
|
|
384
|
+
The read / write mode. The following modes are supported:
|
|
385
|
+
* 'r' open for reading (default)
|
|
386
|
+
* 'w' open for writing, truncating the file first
|
|
387
|
+
* 'x' create a new file and open it for writing
|
|
388
|
+
The data type can be specified by adding one of the following:
|
|
389
|
+
* 'b' binary mode
|
|
390
|
+
* 't' text mode (default)
|
|
391
|
+
encoding : str, optional
|
|
392
|
+
The string encoding to use for text
|
|
393
|
+
|
|
394
|
+
Returns
|
|
395
|
+
-------
|
|
396
|
+
StageObjectBytesReader - 'rb' or 'b' mode
|
|
397
|
+
StageObjectBytesWriter - 'wb' or 'xb' mode
|
|
398
|
+
StageObjectTextReader - 'r' or 'rt' mode
|
|
399
|
+
StageObjectTextWriter - 'w', 'x', 'wt' or 'xt' mode
|
|
400
|
+
|
|
401
|
+
"""
|
|
402
|
+
if '+' in mode or 'a' in mode:
|
|
403
|
+
raise ValueError('modifying an existing stage file is not supported')
|
|
404
|
+
|
|
405
|
+
if 'w' in mode or 'x' in mode:
|
|
406
|
+
exists = self.exists(stage_path)
|
|
407
|
+
if exists:
|
|
408
|
+
if 'x' in mode:
|
|
409
|
+
raise FileExistsError(f'stage path already exists: {stage_path}')
|
|
410
|
+
self.remove(stage_path)
|
|
411
|
+
if 'b' in mode:
|
|
412
|
+
return StageObjectBytesWriter(b'', self, stage_path)
|
|
413
|
+
return StageObjectTextWriter('', self, stage_path)
|
|
414
|
+
|
|
415
|
+
if 'r' in mode:
|
|
416
|
+
content = self.download_file(stage_path)
|
|
417
|
+
if isinstance(content, bytes):
|
|
418
|
+
if 'b' in mode:
|
|
419
|
+
return StageObjectBytesReader(content)
|
|
420
|
+
encoding = 'utf-8' if encoding is None else encoding
|
|
421
|
+
return StageObjectTextReader(content.decode(encoding))
|
|
422
|
+
|
|
423
|
+
if isinstance(content, str):
|
|
424
|
+
return StageObjectTextReader(content)
|
|
425
|
+
|
|
426
|
+
raise ValueError(f'unrecognized file content type: {type(content)}')
|
|
427
|
+
|
|
428
|
+
raise ValueError(f'must have one of create/read/write mode specified: {mode}')
|
|
429
|
+
|
|
430
|
+
def upload_file(
|
|
431
|
+
self,
|
|
432
|
+
local_path: Union[PathLike, TextIO, BinaryIO],
|
|
433
|
+
stage_path: PathLike,
|
|
434
|
+
*,
|
|
435
|
+
overwrite: bool = False,
|
|
436
|
+
) -> StageObject:
|
|
437
|
+
"""
|
|
438
|
+
Upload a local file.
|
|
439
|
+
|
|
440
|
+
Parameters
|
|
441
|
+
----------
|
|
442
|
+
local_path : Path or str or file-like
|
|
443
|
+
Path to the local file or an open file object
|
|
444
|
+
stage_path : Path or str
|
|
445
|
+
Path to the stage file
|
|
446
|
+
overwrite : bool, optional
|
|
447
|
+
Should the ``stage_path`` be overwritten if it exists already?
|
|
448
|
+
|
|
449
|
+
"""
|
|
450
|
+
if isinstance(local_path, (TextIO, BinaryIO)):
|
|
451
|
+
pass
|
|
452
|
+
elif not os.path.isfile(local_path):
|
|
453
|
+
raise IsADirectoryError(f'local path is not a file: {local_path}')
|
|
454
|
+
|
|
455
|
+
if self.exists(stage_path):
|
|
456
|
+
if not overwrite:
|
|
457
|
+
raise OSError(f'stage path already exists: {stage_path}')
|
|
458
|
+
|
|
459
|
+
self.remove(stage_path)
|
|
460
|
+
|
|
461
|
+
if isinstance(local_path, (TextIO, BinaryIO)):
|
|
462
|
+
return self._upload(local_path, stage_path, overwrite=overwrite)
|
|
463
|
+
return self._upload(open(local_path, 'rb'), stage_path, overwrite=overwrite)
|
|
464
|
+
|
|
465
|
+
def upload_folder(
|
|
466
|
+
self,
|
|
467
|
+
local_path: PathLike,
|
|
468
|
+
stage_path: PathLike,
|
|
469
|
+
*,
|
|
470
|
+
overwrite: bool = False,
|
|
471
|
+
recursive: bool = True,
|
|
472
|
+
include_root: bool = False,
|
|
473
|
+
ignore: Optional[Union[PathLike, List[PathLike]]] = None,
|
|
474
|
+
) -> StageObject:
|
|
475
|
+
"""
|
|
476
|
+
Upload a folder recursively.
|
|
477
|
+
|
|
478
|
+
Only the contents of the folder are uploaded. To include the
|
|
479
|
+
folder name itself in the target path use ``include_root=True``.
|
|
480
|
+
|
|
481
|
+
Parameters
|
|
482
|
+
----------
|
|
483
|
+
local_path : Path or str
|
|
484
|
+
Local directory to upload
|
|
485
|
+
stage_path : Path or str
|
|
486
|
+
Path of stage folder to upload to
|
|
487
|
+
overwrite : bool, optional
|
|
488
|
+
If a file already exists, should it be overwritten?
|
|
489
|
+
recursive : bool, optional
|
|
490
|
+
Should nested folders be uploaded?
|
|
491
|
+
include_root : bool, optional
|
|
492
|
+
Should the local root folder itself be uploaded as the top folder?
|
|
493
|
+
ignore : Path or str or List[Path] or List[str], optional
|
|
494
|
+
Glob patterns of files to ignore, for example, '**/*.pyc` will
|
|
495
|
+
ignore all '*.pyc' files in the directory tree
|
|
496
|
+
|
|
497
|
+
"""
|
|
498
|
+
if not os.path.isdir(local_path):
|
|
499
|
+
raise NotADirectoryError(f'local path is not a directory: {local_path}')
|
|
500
|
+
if self.exists(stage_path) and not self.is_dir(stage_path):
|
|
501
|
+
raise NotADirectoryError(f'stage path is not a directory: {stage_path}')
|
|
502
|
+
|
|
503
|
+
ignore_files = set()
|
|
504
|
+
if ignore:
|
|
505
|
+
if isinstance(ignore, list):
|
|
506
|
+
for item in ignore:
|
|
507
|
+
ignore_files.update(glob.glob(str(item), recursive=recursive))
|
|
508
|
+
else:
|
|
509
|
+
ignore_files.update(glob.glob(str(ignore), recursive=recursive))
|
|
510
|
+
|
|
511
|
+
parent_dir = os.path.basename(os.getcwd())
|
|
512
|
+
|
|
513
|
+
files = glob.glob(os.path.join(local_path, '**'), recursive=recursive)
|
|
514
|
+
|
|
515
|
+
for src in files:
|
|
516
|
+
if ignore_files and src in ignore_files:
|
|
517
|
+
continue
|
|
518
|
+
target = os.path.join(parent_dir, src) if include_root else src
|
|
519
|
+
self.upload_file(src, target, overwrite=overwrite)
|
|
520
|
+
|
|
521
|
+
return self.info(stage_path)
|
|
522
|
+
|
|
523
|
+
def _upload(
|
|
524
|
+
self,
|
|
525
|
+
content: Union[str, bytes, TextIO, BinaryIO],
|
|
526
|
+
stage_path: PathLike,
|
|
527
|
+
*,
|
|
528
|
+
overwrite: bool = False,
|
|
529
|
+
) -> StageObject:
|
|
530
|
+
"""
|
|
531
|
+
Upload content to a stage file.
|
|
532
|
+
|
|
533
|
+
Parameters
|
|
534
|
+
----------
|
|
535
|
+
content : str or bytes or file-like
|
|
536
|
+
Content to upload to stage
|
|
537
|
+
stage_path : Path or str
|
|
538
|
+
Path to the stage file
|
|
539
|
+
overwrite : bool, optional
|
|
540
|
+
Should the ``stage_path`` be overwritten if it exists already?
|
|
541
|
+
|
|
542
|
+
"""
|
|
543
|
+
if self.exists(stage_path):
|
|
544
|
+
if not overwrite:
|
|
545
|
+
raise OSError(f'stage path already exists: {stage_path}')
|
|
546
|
+
self.remove(stage_path)
|
|
547
|
+
|
|
548
|
+
self._manager._put(
|
|
549
|
+
f'stage/{self._workspace_group.id}/fs/{stage_path}',
|
|
550
|
+
files={'file': content},
|
|
551
|
+
headers={'Content-Type': None},
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
return self.info(stage_path)
|
|
555
|
+
|
|
556
|
+
def mkdir(self, stage_path: PathLike, overwrite: bool = False) -> StageObject:
|
|
557
|
+
"""
|
|
558
|
+
Make a directory in the stage.
|
|
559
|
+
|
|
560
|
+
Parameters
|
|
561
|
+
----------
|
|
562
|
+
stage_path : Path or str
|
|
563
|
+
Path of the folder to create
|
|
564
|
+
overwrite : bool, optional
|
|
565
|
+
Should the stage path be overwritten if it exists already?
|
|
566
|
+
|
|
567
|
+
Returns
|
|
568
|
+
-------
|
|
569
|
+
StageObject
|
|
570
|
+
|
|
571
|
+
"""
|
|
572
|
+
stage_path = re.sub(r'/*$', r'', str(stage_path)) + '/'
|
|
573
|
+
|
|
574
|
+
if self.exists(stage_path):
|
|
575
|
+
if not overwrite:
|
|
576
|
+
return self.info(stage_path)
|
|
577
|
+
|
|
578
|
+
self.remove(stage_path)
|
|
579
|
+
|
|
580
|
+
self._manager._put(
|
|
581
|
+
f'stage/{self._workspace_group.id}/fs/{stage_path}?isFile=false',
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
return self.info(stage_path)
|
|
585
|
+
|
|
586
|
+
mkdirs = mkdir
|
|
587
|
+
|
|
588
|
+
def rename(
|
|
589
|
+
self,
|
|
590
|
+
old_path: PathLike,
|
|
591
|
+
new_path: PathLike,
|
|
592
|
+
*,
|
|
593
|
+
overwrite: bool = False,
|
|
594
|
+
) -> StageObject:
|
|
595
|
+
"""
|
|
596
|
+
Move the stage file to a new location.
|
|
597
|
+
|
|
598
|
+
Paraemeters
|
|
599
|
+
-----------
|
|
600
|
+
old_path : Path or str
|
|
601
|
+
Original location of the path
|
|
602
|
+
new_path : Path or str
|
|
603
|
+
New location of the path
|
|
604
|
+
overwrite : bool, optional
|
|
605
|
+
Should the ``new_path`` be overwritten if it exists already?
|
|
606
|
+
|
|
607
|
+
"""
|
|
608
|
+
if not self.exists(old_path):
|
|
609
|
+
raise OSError(f'stage path does not exist: {old_path}')
|
|
610
|
+
|
|
611
|
+
if self.exists(new_path):
|
|
612
|
+
if not overwrite:
|
|
613
|
+
raise OSError(f'stage path already exists: {new_path}')
|
|
614
|
+
|
|
615
|
+
if str(old_path).endswith('/') and not str(new_path).endswith('/'):
|
|
616
|
+
raise OSError('original and new paths are not the same type')
|
|
617
|
+
|
|
618
|
+
if str(new_path).endswith('/'):
|
|
619
|
+
self.removedirs(new_path)
|
|
620
|
+
else:
|
|
621
|
+
self.remove(new_path)
|
|
622
|
+
|
|
623
|
+
self._manager._patch(
|
|
624
|
+
f'stage/{self._workspace_group.id}/fs/{old_path}',
|
|
625
|
+
json=dict(newPath=new_path),
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
return self.info(new_path)
|
|
629
|
+
|
|
630
|
+
def info(self, stage_path: PathLike) -> StageObject:
|
|
631
|
+
"""
|
|
632
|
+
Return information about a stage location.
|
|
633
|
+
|
|
634
|
+
Parameters
|
|
635
|
+
----------
|
|
636
|
+
stage_path : Path or str
|
|
637
|
+
Path to the stage location
|
|
638
|
+
|
|
639
|
+
Returns
|
|
640
|
+
-------
|
|
641
|
+
StageObject
|
|
642
|
+
|
|
643
|
+
"""
|
|
644
|
+
res = self._manager._get(
|
|
645
|
+
re.sub(r'/+$', r'/', f'stage/{self._workspace_group.id}/fs/{stage_path}'),
|
|
646
|
+
params=dict(metadata=1),
|
|
647
|
+
).json()
|
|
648
|
+
|
|
649
|
+
return StageObject.from_dict(res, self)
|
|
650
|
+
|
|
651
|
+
def exists(self, stage_path: PathLike) -> bool:
|
|
652
|
+
"""
|
|
653
|
+
Does the given stage path exist?
|
|
654
|
+
|
|
655
|
+
Parameters
|
|
656
|
+
----------
|
|
657
|
+
stage_path : Path or str
|
|
658
|
+
Path to stage object
|
|
659
|
+
|
|
660
|
+
Returns
|
|
661
|
+
-------
|
|
662
|
+
bool
|
|
663
|
+
|
|
664
|
+
"""
|
|
665
|
+
try:
|
|
666
|
+
self.info(stage_path)
|
|
667
|
+
return True
|
|
668
|
+
except ManagementError as exc:
|
|
669
|
+
if exc.errno == 404:
|
|
670
|
+
return False
|
|
671
|
+
raise
|
|
672
|
+
|
|
673
|
+
def is_dir(self, stage_path: PathLike) -> bool:
|
|
674
|
+
"""
|
|
675
|
+
Is the given stage path a directory?
|
|
676
|
+
|
|
677
|
+
Parameters
|
|
678
|
+
----------
|
|
679
|
+
stage_path : Path or str
|
|
680
|
+
Path to stage object
|
|
681
|
+
|
|
682
|
+
Returns
|
|
683
|
+
-------
|
|
684
|
+
bool
|
|
685
|
+
|
|
686
|
+
"""
|
|
687
|
+
try:
|
|
688
|
+
return self.info(stage_path).type == 'directory'
|
|
689
|
+
except ManagementError as exc:
|
|
690
|
+
if exc.errno == 404:
|
|
691
|
+
return False
|
|
692
|
+
raise
|
|
693
|
+
|
|
694
|
+
def is_file(self, stage_path: PathLike) -> bool:
|
|
695
|
+
"""
|
|
696
|
+
Is the given stage path a file?
|
|
697
|
+
|
|
698
|
+
Parameters
|
|
699
|
+
----------
|
|
700
|
+
stage_path : Path or str
|
|
701
|
+
Path to stage object
|
|
702
|
+
|
|
703
|
+
Returns
|
|
704
|
+
-------
|
|
705
|
+
bool
|
|
706
|
+
|
|
707
|
+
"""
|
|
708
|
+
try:
|
|
709
|
+
return self.info(stage_path).type != 'directory'
|
|
710
|
+
except ManagementError as exc:
|
|
711
|
+
if exc.errno == 404:
|
|
712
|
+
return False
|
|
713
|
+
raise
|
|
714
|
+
|
|
715
|
+
def _listdir(self, stage_path: PathLike, *, recursive: bool = False) -> List[str]:
|
|
716
|
+
"""
|
|
717
|
+
Return the names of files in a directory.
|
|
718
|
+
|
|
719
|
+
Parameters
|
|
720
|
+
----------
|
|
721
|
+
stage_path : Path or str
|
|
722
|
+
Path to the folder in Stage
|
|
723
|
+
recursive : bool, optional
|
|
724
|
+
Should folders be listed recursively?
|
|
725
|
+
|
|
726
|
+
"""
|
|
727
|
+
res = self._manager._get(
|
|
728
|
+
f'stage/{self._workspace_group.id}/fs/{stage_path}',
|
|
729
|
+
).json()
|
|
730
|
+
if recursive:
|
|
731
|
+
out = []
|
|
732
|
+
for item in res['content'] or []:
|
|
733
|
+
out.append(item['path'])
|
|
734
|
+
if item['type'] == 'directory':
|
|
735
|
+
out.extend(self._listdir(item['path'], recursive=recursive))
|
|
736
|
+
return out
|
|
737
|
+
return [x['path'] for x in res['content'] or []]
|
|
738
|
+
|
|
739
|
+
def listdir(
|
|
740
|
+
self,
|
|
741
|
+
stage_path: PathLike = '/',
|
|
742
|
+
*,
|
|
743
|
+
recursive: bool = False,
|
|
744
|
+
) -> List[str]:
|
|
745
|
+
"""
|
|
746
|
+
List the files / folders at the given path.
|
|
747
|
+
|
|
748
|
+
Parameters
|
|
749
|
+
----------
|
|
750
|
+
stage_path : Path or str, optional
|
|
751
|
+
Path to the stage location
|
|
752
|
+
|
|
753
|
+
Returns
|
|
754
|
+
-------
|
|
755
|
+
List[str]
|
|
756
|
+
|
|
757
|
+
"""
|
|
758
|
+
stage_path = re.sub(r'^(\./|/)+', r'', str(stage_path))
|
|
759
|
+
stage_path = re.sub(r'/+$', r'', stage_path) + '/'
|
|
760
|
+
|
|
761
|
+
if self.is_dir(stage_path):
|
|
762
|
+
out = self._listdir(stage_path, recursive=recursive)
|
|
763
|
+
if stage_path != '/':
|
|
764
|
+
stage_path_n = len(stage_path.split('/')) - 1
|
|
765
|
+
out = ['/'.join(x.split('/')[stage_path_n:]) for x in out]
|
|
766
|
+
return out
|
|
767
|
+
|
|
768
|
+
raise NotADirectoryError(f'stage path is not a directory: {stage_path}')
|
|
769
|
+
|
|
770
|
+
def download_file(
|
|
771
|
+
self,
|
|
772
|
+
stage_path: PathLike,
|
|
773
|
+
local_path: Optional[PathLike] = None,
|
|
774
|
+
*,
|
|
775
|
+
overwrite: bool = False,
|
|
776
|
+
encoding: Optional[str] = None,
|
|
777
|
+
) -> Optional[Union[bytes, str]]:
|
|
778
|
+
"""
|
|
779
|
+
Download the content of a stage path.
|
|
780
|
+
|
|
781
|
+
Parameters
|
|
782
|
+
----------
|
|
783
|
+
stage_path : Path or str
|
|
784
|
+
Path to the stage file
|
|
785
|
+
local_path : Path or str
|
|
786
|
+
Path to local file target location
|
|
787
|
+
overwrite : bool, optional
|
|
788
|
+
Should an existing file be overwritten if it exists?
|
|
789
|
+
encoding : str, optional
|
|
790
|
+
Encoding used to convert the resulting data
|
|
791
|
+
|
|
792
|
+
Returns
|
|
793
|
+
-------
|
|
794
|
+
bytes or str - ``local_path`` is None
|
|
795
|
+
None - ``local_path`` is a Path or str
|
|
796
|
+
|
|
797
|
+
"""
|
|
798
|
+
if local_path is not None and not overwrite and os.path.exists(local_path):
|
|
799
|
+
raise OSError('target file already exists; use overwrite=True to replace')
|
|
800
|
+
if self.is_dir(stage_path):
|
|
801
|
+
raise IsADirectoryError(f'stage path is a directory: {stage_path}')
|
|
802
|
+
|
|
803
|
+
out = self._manager._get(
|
|
804
|
+
f'stage/{self._workspace_group.id}/fs/{stage_path}',
|
|
805
|
+
).content
|
|
806
|
+
|
|
807
|
+
if local_path is not None:
|
|
808
|
+
with open(local_path, 'wb') as outfile:
|
|
809
|
+
outfile.write(out)
|
|
810
|
+
return None
|
|
811
|
+
|
|
812
|
+
if encoding:
|
|
813
|
+
return out.decode(encoding)
|
|
814
|
+
|
|
815
|
+
return out
|
|
816
|
+
|
|
817
|
+
def download_folder(
|
|
818
|
+
self,
|
|
819
|
+
stage_path: PathLike,
|
|
820
|
+
local_path: PathLike = '.',
|
|
821
|
+
*,
|
|
822
|
+
overwrite: bool = False,
|
|
823
|
+
) -> None:
|
|
824
|
+
"""
|
|
825
|
+
Download a Stage folder to a local directory.
|
|
826
|
+
|
|
827
|
+
Parameters
|
|
828
|
+
----------
|
|
829
|
+
stage_path : Path or str
|
|
830
|
+
Path to the stage file
|
|
831
|
+
local_path : Path or str
|
|
832
|
+
Path to local directory target location
|
|
833
|
+
overwrite : bool, optional
|
|
834
|
+
Should an existing directory / files be overwritten if they exist?
|
|
835
|
+
|
|
836
|
+
"""
|
|
837
|
+
if local_path is not None and not overwrite and os.path.exists(local_path):
|
|
838
|
+
raise OSError(
|
|
839
|
+
'target directory already exists; '
|
|
840
|
+
'use overwrite=True to replace',
|
|
841
|
+
)
|
|
842
|
+
if not self.is_dir(stage_path):
|
|
843
|
+
raise NotADirectoryError(f'stage path is not a directory: {stage_path}')
|
|
844
|
+
|
|
845
|
+
for f in self.listdir(stage_path, recursive=True):
|
|
846
|
+
if self.is_dir(f):
|
|
847
|
+
continue
|
|
848
|
+
target = os.path.normpath(os.path.join(local_path, f))
|
|
849
|
+
os.makedirs(os.path.dirname(target), exist_ok=True)
|
|
850
|
+
self.download_file(f, target, overwrite=overwrite)
|
|
851
|
+
|
|
852
|
+
def remove(self, stage_path: PathLike) -> None:
|
|
853
|
+
"""
|
|
854
|
+
Delete a stage location.
|
|
855
|
+
|
|
856
|
+
Parameters
|
|
857
|
+
----------
|
|
858
|
+
stage_path : Path or str
|
|
859
|
+
Path to the stage location
|
|
860
|
+
|
|
861
|
+
"""
|
|
862
|
+
if self.is_dir(stage_path):
|
|
863
|
+
raise IsADirectoryError(
|
|
864
|
+
'stage path is a directory, '
|
|
865
|
+
f'use rmdir or removedirs: {stage_path}',
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
self._manager._delete(f'stage/{self._workspace_group.id}/fs/{stage_path}')
|
|
869
|
+
|
|
870
|
+
def removedirs(self, stage_path: PathLike) -> None:
|
|
871
|
+
"""
|
|
872
|
+
Delete a stage folder recursively.
|
|
873
|
+
|
|
874
|
+
Parameters
|
|
875
|
+
----------
|
|
876
|
+
stage_path : Path or str
|
|
877
|
+
Path to the stage location
|
|
878
|
+
|
|
879
|
+
"""
|
|
880
|
+
stage_path = re.sub(r'/*$', r'', str(stage_path)) + '/'
|
|
881
|
+
self._manager._delete(f'stage/{self._workspace_group.id}/fs/{stage_path}')
|
|
882
|
+
|
|
883
|
+
def rmdir(self, stage_path: PathLike) -> None:
|
|
884
|
+
"""
|
|
885
|
+
Delete a stage folder.
|
|
886
|
+
|
|
887
|
+
Parameters
|
|
888
|
+
----------
|
|
889
|
+
stage_path : Path or str
|
|
890
|
+
Path to the stage location
|
|
891
|
+
|
|
892
|
+
"""
|
|
893
|
+
stage_path = re.sub(r'/*$', r'', str(stage_path)) + '/'
|
|
894
|
+
|
|
895
|
+
if self.listdir(stage_path):
|
|
896
|
+
raise OSError(f'stage folder is not empty, use removedirs: {stage_path}')
|
|
897
|
+
|
|
898
|
+
self._manager._delete(f'stage/{self._workspace_group.id}/fs/{stage_path}')
|
|
899
|
+
|
|
900
|
+
def __str__(self) -> str:
|
|
901
|
+
"""Return string representation."""
|
|
902
|
+
return vars_to_str(self)
|
|
903
|
+
|
|
904
|
+
def __repr__(self) -> str:
|
|
905
|
+
"""Return string representation."""
|
|
906
|
+
return str(self)
|
|
907
|
+
|
|
908
|
+
|
|
18
909
|
class Workspace(object):
|
|
19
910
|
"""
|
|
20
911
|
SingleStoreDB workspace definition.
|
|
@@ -34,9 +925,12 @@ class Workspace(object):
|
|
|
34
925
|
"""
|
|
35
926
|
|
|
36
927
|
def __init__(
|
|
37
|
-
self,
|
|
928
|
+
self,
|
|
929
|
+
name: str,
|
|
930
|
+
workspace_id: str,
|
|
38
931
|
workspace_group: Union[str, 'WorkspaceGroup'],
|
|
39
|
-
size: str,
|
|
932
|
+
size: str,
|
|
933
|
+
state: str,
|
|
40
934
|
created_at: Union[str, datetime.datetime],
|
|
41
935
|
terminated_at: Optional[Union[str, datetime.datetime]] = None,
|
|
42
936
|
endpoint: Optional[str] = None,
|
|
@@ -106,7 +1000,7 @@ class Workspace(object):
|
|
|
106
1000
|
out._manager = manager
|
|
107
1001
|
return out
|
|
108
1002
|
|
|
109
|
-
def refresh(self) ->
|
|
1003
|
+
def refresh(self) -> Workspace:
|
|
110
1004
|
"""Update the object to the current state."""
|
|
111
1005
|
if self._manager is None:
|
|
112
1006
|
raise ManagementError(
|
|
@@ -122,6 +1016,7 @@ class Workspace(object):
|
|
|
122
1016
|
wait_on_terminated: bool = False,
|
|
123
1017
|
wait_interval: int = 10,
|
|
124
1018
|
wait_timeout: int = 600,
|
|
1019
|
+
force: bool = False,
|
|
125
1020
|
) -> None:
|
|
126
1021
|
"""
|
|
127
1022
|
Terminate the workspace.
|
|
@@ -134,6 +1029,8 @@ class Workspace(object):
|
|
|
134
1029
|
Number of seconds between each server check
|
|
135
1030
|
wait_timeout : int, optional
|
|
136
1031
|
Total number of seconds to check server before giving up
|
|
1032
|
+
force : bool, optional
|
|
1033
|
+
Should the workspace group be terminated even if it has workspaces?
|
|
137
1034
|
|
|
138
1035
|
Raises
|
|
139
1036
|
------
|
|
@@ -145,7 +1042,8 @@ class Workspace(object):
|
|
|
145
1042
|
raise ManagementError(
|
|
146
1043
|
msg='No workspace manager is associated with this object.',
|
|
147
1044
|
)
|
|
148
|
-
|
|
1045
|
+
force_str = 'true' if force else 'false'
|
|
1046
|
+
self._manager._delete(f'workspaces/{self.id}?force={force_str}')
|
|
149
1047
|
if wait_on_terminated:
|
|
150
1048
|
self._manager._wait_on_state(
|
|
151
1049
|
self._manager.get_workspace(self.id),
|
|
@@ -175,6 +1073,78 @@ class Workspace(object):
|
|
|
175
1073
|
kwargs['host'] = self.endpoint
|
|
176
1074
|
return connection.connect(**kwargs)
|
|
177
1075
|
|
|
1076
|
+
def suspend(
|
|
1077
|
+
self,
|
|
1078
|
+
wait_on_suspended: bool = False,
|
|
1079
|
+
wait_interval: int = 20,
|
|
1080
|
+
wait_timeout: int = 600,
|
|
1081
|
+
) -> None:
|
|
1082
|
+
"""
|
|
1083
|
+
Suspend the workspace.
|
|
1084
|
+
|
|
1085
|
+
Parameters
|
|
1086
|
+
----------
|
|
1087
|
+
wait_on_suspended : bool, optional
|
|
1088
|
+
Wait for the workspace to go into 'Suspended' mode before returning
|
|
1089
|
+
wait_interval : int, optional
|
|
1090
|
+
Number of seconds between each server check
|
|
1091
|
+
wait_timeout : int, optional
|
|
1092
|
+
Total number of seconds to check server before giving up
|
|
1093
|
+
|
|
1094
|
+
Raises
|
|
1095
|
+
------
|
|
1096
|
+
ManagementError
|
|
1097
|
+
If timeout is reached
|
|
1098
|
+
|
|
1099
|
+
"""
|
|
1100
|
+
if self._manager is None:
|
|
1101
|
+
raise ManagementError(
|
|
1102
|
+
msg='No workspace manager is associated with this object.',
|
|
1103
|
+
)
|
|
1104
|
+
self._manager._post(f'workspaces/{self.id}/suspend')
|
|
1105
|
+
if wait_on_suspended:
|
|
1106
|
+
self._manager._wait_on_state(
|
|
1107
|
+
self._manager.get_workspace(self.id),
|
|
1108
|
+
'Suspended', interval=wait_interval, timeout=wait_timeout,
|
|
1109
|
+
)
|
|
1110
|
+
self.refresh()
|
|
1111
|
+
|
|
1112
|
+
def resume(
|
|
1113
|
+
self,
|
|
1114
|
+
wait_on_resumed: bool = False,
|
|
1115
|
+
wait_interval: int = 20,
|
|
1116
|
+
wait_timeout: int = 600,
|
|
1117
|
+
) -> None:
|
|
1118
|
+
"""
|
|
1119
|
+
Resume the workspace.
|
|
1120
|
+
|
|
1121
|
+
Parameters
|
|
1122
|
+
----------
|
|
1123
|
+
wait_on_resumed : bool, optional
|
|
1124
|
+
Wait for the workspace to go into 'Resumed' or 'Active' mode before returning
|
|
1125
|
+
wait_interval : int, optional
|
|
1126
|
+
Number of seconds between each server check
|
|
1127
|
+
wait_timeout : int, optional
|
|
1128
|
+
Total number of seconds to check server before giving up
|
|
1129
|
+
|
|
1130
|
+
Raises
|
|
1131
|
+
------
|
|
1132
|
+
ManagementError
|
|
1133
|
+
If timeout is reached
|
|
1134
|
+
|
|
1135
|
+
"""
|
|
1136
|
+
if self._manager is None:
|
|
1137
|
+
raise ManagementError(
|
|
1138
|
+
msg='No workspace manager is associated with this object.',
|
|
1139
|
+
)
|
|
1140
|
+
self._manager._post(f'workspaces/{self.id}/resume')
|
|
1141
|
+
if wait_on_resumed:
|
|
1142
|
+
self._manager._wait_on_state(
|
|
1143
|
+
self._manager.get_workspace(self.id),
|
|
1144
|
+
['Resumed', 'Active'], interval=wait_interval, timeout=wait_timeout,
|
|
1145
|
+
)
|
|
1146
|
+
self.refresh()
|
|
1147
|
+
|
|
178
1148
|
|
|
179
1149
|
class WorkspaceGroup(object):
|
|
180
1150
|
"""
|
|
@@ -197,7 +1167,7 @@ class WorkspaceGroup(object):
|
|
|
197
1167
|
def __init__(
|
|
198
1168
|
self, name: str, id: str,
|
|
199
1169
|
created_at: Union[str, datetime.datetime],
|
|
200
|
-
region: Region,
|
|
1170
|
+
region: Optional[Region],
|
|
201
1171
|
firewall_ranges: List[str],
|
|
202
1172
|
terminated_at: Optional[Union[str, datetime.datetime]],
|
|
203
1173
|
):
|
|
@@ -210,7 +1180,7 @@ class WorkspaceGroup(object):
|
|
|
210
1180
|
#: Timestamp of when the workspace group was created
|
|
211
1181
|
self.created_at = to_datetime(created_at)
|
|
212
1182
|
|
|
213
|
-
#: Region of the
|
|
1183
|
+
#: Region of the workspace group (see :class:`Region`)
|
|
214
1184
|
self.region = region
|
|
215
1185
|
|
|
216
1186
|
#: List of allowed incoming IP addresses / ranges
|
|
@@ -248,18 +1218,42 @@ class WorkspaceGroup(object):
|
|
|
248
1218
|
:class:`WorkspaceGroup`
|
|
249
1219
|
|
|
250
1220
|
"""
|
|
1221
|
+
try:
|
|
1222
|
+
region = [x for x in manager.regions if x.id == obj['regionID']][0]
|
|
1223
|
+
except IndexError:
|
|
1224
|
+
region = Region(obj.get('regionID', '<unknown>'), '<unknown>', '<unknown>')
|
|
251
1225
|
out = cls(
|
|
252
|
-
name=obj['name'],
|
|
1226
|
+
name=obj['name'],
|
|
1227
|
+
id=obj['workspaceGroupID'],
|
|
253
1228
|
created_at=obj['createdAt'],
|
|
254
|
-
region=
|
|
1229
|
+
region=region,
|
|
255
1230
|
firewall_ranges=obj.get('firewallRanges', []),
|
|
256
1231
|
terminated_at=obj.get('terminatedAt'),
|
|
257
1232
|
)
|
|
258
1233
|
out._manager = manager
|
|
259
1234
|
return out
|
|
260
1235
|
|
|
1236
|
+
@property
|
|
1237
|
+
def organization(self) -> Organization:
|
|
1238
|
+
if self._manager is None:
|
|
1239
|
+
raise ManagementError(
|
|
1240
|
+
msg='No workspace manager is associated with this object.',
|
|
1241
|
+
)
|
|
1242
|
+
return self._manager.organization
|
|
1243
|
+
|
|
1244
|
+
@property
|
|
1245
|
+
def stage(self) -> Stage:
|
|
1246
|
+
"""Stage manager."""
|
|
1247
|
+
if self._manager is None:
|
|
1248
|
+
raise ManagementError(
|
|
1249
|
+
msg='No workspace manager is associated with this object.',
|
|
1250
|
+
)
|
|
1251
|
+
return Stage(self, self._manager)
|
|
1252
|
+
|
|
1253
|
+
stages = stage
|
|
1254
|
+
|
|
261
1255
|
def refresh(self) -> 'WorkspaceGroup':
|
|
262
|
-
"""Update
|
|
1256
|
+
"""Update the object to the current state."""
|
|
263
1257
|
if self._manager is None:
|
|
264
1258
|
raise ManagementError(
|
|
265
1259
|
msg='No workspace manager is associated with this object.',
|
|
@@ -275,14 +1269,14 @@ class WorkspaceGroup(object):
|
|
|
275
1269
|
firewall_ranges: Optional[List[str]] = None,
|
|
276
1270
|
) -> None:
|
|
277
1271
|
"""
|
|
278
|
-
Update the
|
|
1272
|
+
Update the workspace group definition.
|
|
279
1273
|
|
|
280
1274
|
Parameters
|
|
281
1275
|
----------
|
|
282
1276
|
name : str, optional
|
|
283
|
-
|
|
1277
|
+
Workspace group name
|
|
284
1278
|
admim_password : str, optional
|
|
285
|
-
Admin password for the
|
|
1279
|
+
Admin password for the workspace group
|
|
286
1280
|
firewall_ranges : Sequence[str], optional
|
|
287
1281
|
List of allowed incoming IP addresses
|
|
288
1282
|
|
|
@@ -314,7 +1308,7 @@ class WorkspaceGroup(object):
|
|
|
314
1308
|
force : bool, optional
|
|
315
1309
|
Terminate a workspace group even if it has active workspaces
|
|
316
1310
|
wait_on_terminated : bool, optional
|
|
317
|
-
Wait for the
|
|
1311
|
+
Wait for the workspace group to go into 'Terminated' mode before returning
|
|
318
1312
|
wait_interval : int, optional
|
|
319
1313
|
Number of seconds between each server check
|
|
320
1314
|
wait_timeout : int, optional
|
|
@@ -332,16 +1326,21 @@ class WorkspaceGroup(object):
|
|
|
332
1326
|
)
|
|
333
1327
|
self._manager._delete(f'workspaceGroups/{self.id}', params=dict(force=force))
|
|
334
1328
|
if wait_on_terminated:
|
|
335
|
-
|
|
336
|
-
self.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
1329
|
+
while True:
|
|
1330
|
+
self.refresh()
|
|
1331
|
+
if self.terminated_at is not None:
|
|
1332
|
+
break
|
|
1333
|
+
if wait_timeout <= 0:
|
|
1334
|
+
raise ManagementError(
|
|
1335
|
+
msg='Exceeded waiting time for WorkspaceGroup to terminate',
|
|
1336
|
+
)
|
|
1337
|
+
time.sleep(wait_interval)
|
|
1338
|
+
wait_timeout -= wait_interval
|
|
340
1339
|
|
|
341
1340
|
def create_workspace(
|
|
342
1341
|
self, name: str, size: Optional[str] = None,
|
|
343
1342
|
wait_on_active: bool = False, wait_interval: int = 10,
|
|
344
|
-
wait_timeout: int = 600,
|
|
1343
|
+
wait_timeout: int = 600, add_endpoint_to_firewall_ranges: bool = True,
|
|
345
1344
|
) -> Workspace:
|
|
346
1345
|
"""
|
|
347
1346
|
Create a new workspace.
|
|
@@ -359,6 +1358,9 @@ class WorkspaceGroup(object):
|
|
|
359
1358
|
if wait=True
|
|
360
1359
|
wait_interval : int, optional
|
|
361
1360
|
Number of seconds between each polling interval
|
|
1361
|
+
add_endpoint_to_firewall_ranges : bool, optional
|
|
1362
|
+
Should the workspace endpoint be added to the workspace group
|
|
1363
|
+
firewall ranges?
|
|
362
1364
|
|
|
363
1365
|
Returns
|
|
364
1366
|
-------
|
|
@@ -369,20 +1371,100 @@ class WorkspaceGroup(object):
|
|
|
369
1371
|
raise ManagementError(
|
|
370
1372
|
msg='No workspace manager is associated with this object.',
|
|
371
1373
|
)
|
|
372
|
-
|
|
1374
|
+
|
|
1375
|
+
out = self._manager.create_workspace(
|
|
373
1376
|
name=name, workspace_group=self, size=size, wait_on_active=wait_on_active,
|
|
374
1377
|
wait_interval=wait_interval, wait_timeout=wait_timeout,
|
|
375
1378
|
)
|
|
376
1379
|
|
|
1380
|
+
if add_endpoint_to_firewall_ranges and out.endpoint is not None:
|
|
1381
|
+
ip_address = '{}/32'.format(socket.gethostbyname(out.endpoint))
|
|
1382
|
+
self.update(firewall_ranges=self.firewall_ranges+[ip_address])
|
|
1383
|
+
|
|
1384
|
+
return out
|
|
1385
|
+
|
|
377
1386
|
@property
|
|
378
|
-
def workspaces(self) ->
|
|
1387
|
+
def workspaces(self) -> NamedList[Workspace]:
|
|
379
1388
|
"""Return a list of available workspaces."""
|
|
380
1389
|
if self._manager is None:
|
|
381
1390
|
raise ManagementError(
|
|
382
1391
|
msg='No workspace manager is associated with this object.',
|
|
383
1392
|
)
|
|
384
1393
|
res = self._manager._get('workspaces', params=dict(workspaceGroupID=self.id))
|
|
385
|
-
return
|
|
1394
|
+
return NamedList(
|
|
1395
|
+
[Workspace.from_dict(item, self._manager) for item in res.json()],
|
|
1396
|
+
)
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
class Billing(object):
|
|
1400
|
+
"""Billing information."""
|
|
1401
|
+
|
|
1402
|
+
COMPUTE_CREDIT = 'compute_credit'
|
|
1403
|
+
STORAGE_AVG_BYTE = 'storage_avg_byte'
|
|
1404
|
+
|
|
1405
|
+
HOUR = 'hour'
|
|
1406
|
+
DAY = 'day'
|
|
1407
|
+
MONTH = 'month'
|
|
1408
|
+
|
|
1409
|
+
def __init__(self, manager: Manager):
|
|
1410
|
+
self._manager = manager
|
|
1411
|
+
|
|
1412
|
+
def usage(
|
|
1413
|
+
self,
|
|
1414
|
+
start_time: datetime.datetime,
|
|
1415
|
+
end_time: datetime.datetime,
|
|
1416
|
+
metric: Optional[str] = None,
|
|
1417
|
+
aggregate_by: Optional[str] = None,
|
|
1418
|
+
) -> List[BillingUsageItem]:
|
|
1419
|
+
"""
|
|
1420
|
+
Get usage information.
|
|
1421
|
+
|
|
1422
|
+
Parameters
|
|
1423
|
+
----------
|
|
1424
|
+
start_time : datetime.datetime
|
|
1425
|
+
Start time for usage interval
|
|
1426
|
+
end_time : datetime.datetime
|
|
1427
|
+
End time for usage interval
|
|
1428
|
+
metric : str, optional
|
|
1429
|
+
Possible metrics are ``mgr.billing.COMPUTE_CREDIT`` and
|
|
1430
|
+
``mgr.billing.STORAGE_AVG_BYTE`` (default is all)
|
|
1431
|
+
aggregate_by : str, optional
|
|
1432
|
+
Aggregate type used to group usage: ``mgr.billing.HOUR``,
|
|
1433
|
+
``mgr.billing.DAY``, or ``mgr.billing.MONTH``
|
|
1434
|
+
|
|
1435
|
+
Returns
|
|
1436
|
+
-------
|
|
1437
|
+
List[BillingUsage]
|
|
1438
|
+
|
|
1439
|
+
"""
|
|
1440
|
+
res = self._manager._get(
|
|
1441
|
+
'billing/usage',
|
|
1442
|
+
params={
|
|
1443
|
+
k: v for k, v in dict(
|
|
1444
|
+
metric=snake_to_camel(metric),
|
|
1445
|
+
startTime=from_datetime(start_time),
|
|
1446
|
+
endTime=from_datetime(end_time),
|
|
1447
|
+
aggregate_by=aggregate_by.lower() if aggregate_by else None,
|
|
1448
|
+
).items() if v is not None
|
|
1449
|
+
},
|
|
1450
|
+
)
|
|
1451
|
+
return [
|
|
1452
|
+
BillingUsageItem.from_dict(x, self._manager)
|
|
1453
|
+
for x in res.json()['billingUsage']
|
|
1454
|
+
]
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
class Organizations(object):
|
|
1458
|
+
"""Organizations."""
|
|
1459
|
+
|
|
1460
|
+
def __init__(self, manager: Manager):
|
|
1461
|
+
self._manager = manager
|
|
1462
|
+
|
|
1463
|
+
@property
|
|
1464
|
+
def current(self) -> Organization:
|
|
1465
|
+
"""Get current organization."""
|
|
1466
|
+
res = self._manager._get('organizations/current').json()
|
|
1467
|
+
return Organization.from_dict(res, self._manager)
|
|
386
1468
|
|
|
387
1469
|
|
|
388
1470
|
class WorkspaceManager(Manager):
|
|
@@ -416,20 +1498,38 @@ class WorkspaceManager(Manager):
|
|
|
416
1498
|
obj_type = 'workspace'
|
|
417
1499
|
|
|
418
1500
|
@property
|
|
419
|
-
def workspace_groups(self) ->
|
|
1501
|
+
def workspace_groups(self) -> NamedList[WorkspaceGroup]:
|
|
420
1502
|
"""Return a list of available workspace groups."""
|
|
421
1503
|
res = self._get('workspaceGroups')
|
|
422
|
-
return [WorkspaceGroup.from_dict(item, self) for item in res.json()]
|
|
1504
|
+
return NamedList([WorkspaceGroup.from_dict(item, self) for item in res.json()])
|
|
423
1505
|
|
|
424
1506
|
@property
|
|
425
|
-
def
|
|
1507
|
+
def organizations(self) -> Organizations:
|
|
1508
|
+
"""Return the organizations."""
|
|
1509
|
+
return Organizations(self)
|
|
1510
|
+
|
|
1511
|
+
@property
|
|
1512
|
+
def organization(self) -> Organization:
|
|
1513
|
+
""" Return the current organization."""
|
|
1514
|
+
return self.organizations.current
|
|
1515
|
+
|
|
1516
|
+
@property
|
|
1517
|
+
def billing(self) -> Billing:
|
|
1518
|
+
"""Return the current billing information."""
|
|
1519
|
+
return Billing(self)
|
|
1520
|
+
|
|
1521
|
+
@ttl_property(datetime.timedelta(hours=1))
|
|
1522
|
+
def regions(self) -> NamedList[Region]:
|
|
426
1523
|
"""Return a list of available regions."""
|
|
427
1524
|
res = self._get('regions')
|
|
428
|
-
return [Region.from_dict(item, self) for item in res.json()]
|
|
1525
|
+
return NamedList([Region.from_dict(item, self) for item in res.json()])
|
|
429
1526
|
|
|
430
1527
|
def create_workspace_group(
|
|
431
1528
|
self, name: str, region: Union[str, Region],
|
|
432
1529
|
firewall_ranges: List[str], admin_password: Optional[str] = None,
|
|
1530
|
+
expires_at: Optional[str] = None,
|
|
1531
|
+
allow_all_traffic: Optional[bool] = None,
|
|
1532
|
+
update_window: Optional[Dict[str, int]] = None,
|
|
433
1533
|
) -> WorkspaceGroup:
|
|
434
1534
|
"""
|
|
435
1535
|
Create a new workspace group.
|
|
@@ -446,6 +1546,17 @@ class WorkspaceManager(Manager):
|
|
|
446
1546
|
admin_password : str, optional
|
|
447
1547
|
Admin password for the workspace group. If no password is supplied,
|
|
448
1548
|
a password will be generated and retured in the response.
|
|
1549
|
+
expires_at : str, optional
|
|
1550
|
+
The timestamp of when the workspace group will expire.
|
|
1551
|
+
If the expiration time is not specified,
|
|
1552
|
+
the workspace group will have no expiration time.
|
|
1553
|
+
At expiration, the workspace group is terminated and all the data is lost.
|
|
1554
|
+
Expiration time can be specified as a timestamp or duration.
|
|
1555
|
+
Example: "2021-01-02T15:04:05Z07:00", "2021-01-02", "3h30m"
|
|
1556
|
+
allow_all_traffic : bool, optional
|
|
1557
|
+
Allow all traffic to the workspace group
|
|
1558
|
+
update_window : Dict[str, int], optional
|
|
1559
|
+
Specify the day and hour of an update window: dict(day=0-6, hour=0-23)
|
|
449
1560
|
|
|
450
1561
|
Returns
|
|
451
1562
|
-------
|
|
@@ -458,7 +1569,10 @@ class WorkspaceManager(Manager):
|
|
|
458
1569
|
'workspaceGroups', json=dict(
|
|
459
1570
|
name=name, regionID=region,
|
|
460
1571
|
adminPassword=admin_password,
|
|
461
|
-
firewallRanges=firewall_ranges,
|
|
1572
|
+
firewallRanges=firewall_ranges or [],
|
|
1573
|
+
expiresAt=expires_at,
|
|
1574
|
+
allowAllTraffic=allow_all_traffic,
|
|
1575
|
+
updateWindow=update_window,
|
|
462
1576
|
),
|
|
463
1577
|
)
|
|
464
1578
|
return self.get_workspace_group(res.json()['workspaceGroupID'])
|
|
@@ -547,6 +1661,8 @@ def manage_workspaces(
|
|
|
547
1661
|
access_token: Optional[str] = None,
|
|
548
1662
|
version: str = WorkspaceManager.default_version,
|
|
549
1663
|
base_url: str = WorkspaceManager.default_base_url,
|
|
1664
|
+
*,
|
|
1665
|
+
organization_id: Optional[str] = None,
|
|
550
1666
|
) -> WorkspaceManager:
|
|
551
1667
|
"""
|
|
552
1668
|
Retrieve a SingleStoreDB workspace manager.
|
|
@@ -559,10 +1675,15 @@ def manage_workspaces(
|
|
|
559
1675
|
Version of the API to use
|
|
560
1676
|
base_url : str, optional
|
|
561
1677
|
Base URL of the workspace management API
|
|
1678
|
+
organization_id : str, optional
|
|
1679
|
+
ID of organization, if using a JWT for authentication
|
|
562
1680
|
|
|
563
1681
|
Returns
|
|
564
1682
|
-------
|
|
565
1683
|
:class:`WorkspaceManager`
|
|
566
1684
|
|
|
567
1685
|
"""
|
|
568
|
-
return WorkspaceManager(
|
|
1686
|
+
return WorkspaceManager(
|
|
1687
|
+
access_token=access_token, base_url=base_url,
|
|
1688
|
+
version=version, organization_id=organization_id,
|
|
1689
|
+
)
|