singlestoredb 0.3.3__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 -2
- singlestoredb/alchemy/__init__.py +90 -0
- singlestoredb/auth.py +6 -4
- singlestoredb/config.py +116 -16
- singlestoredb/connection.py +489 -523
- singlestoredb/converters.py +275 -26
- singlestoredb/exceptions.py +30 -4
- 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/__init__.py +0 -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/connection.py +1192 -0
- singlestoredb/management/__init__.py +3 -2
- singlestoredb/management/billing_usage.py +148 -0
- singlestoredb/management/cluster.py +19 -14
- singlestoredb/management/manager.py +100 -40
- singlestoredb/management/organization.py +188 -0
- singlestoredb/management/region.py +6 -8
- singlestoredb/management/utils.py +253 -4
- singlestoredb/management/workspace.py +1153 -35
- singlestoredb/mysql/__init__.py +177 -0
- singlestoredb/mysql/_auth.py +298 -0
- singlestoredb/mysql/charset.py +214 -0
- singlestoredb/mysql/connection.py +1814 -0
- singlestoredb/mysql/constants/CLIENT.py +38 -0
- singlestoredb/mysql/constants/COMMAND.py +32 -0
- singlestoredb/mysql/constants/CR.py +78 -0
- singlestoredb/mysql/constants/ER.py +474 -0
- singlestoredb/mysql/constants/FIELD_TYPE.py +32 -0
- singlestoredb/mysql/constants/FLAG.py +15 -0
- singlestoredb/mysql/constants/SERVER_STATUS.py +10 -0
- singlestoredb/mysql/constants/__init__.py +0 -0
- singlestoredb/mysql/converters.py +271 -0
- singlestoredb/mysql/cursors.py +713 -0
- singlestoredb/mysql/err.py +92 -0
- singlestoredb/mysql/optionfile.py +20 -0
- singlestoredb/mysql/protocol.py +388 -0
- singlestoredb/mysql/tests/__init__.py +19 -0
- singlestoredb/mysql/tests/base.py +126 -0
- singlestoredb/mysql/tests/conftest.py +37 -0
- singlestoredb/mysql/tests/test_DictCursor.py +132 -0
- singlestoredb/mysql/tests/test_SSCursor.py +141 -0
- singlestoredb/mysql/tests/test_basic.py +452 -0
- singlestoredb/mysql/tests/test_connection.py +851 -0
- singlestoredb/mysql/tests/test_converters.py +58 -0
- singlestoredb/mysql/tests/test_cursor.py +141 -0
- singlestoredb/mysql/tests/test_err.py +16 -0
- singlestoredb/mysql/tests/test_issues.py +514 -0
- singlestoredb/mysql/tests/test_load_local.py +75 -0
- singlestoredb/mysql/tests/test_nextset.py +88 -0
- singlestoredb/mysql/tests/test_optionfile.py +27 -0
- singlestoredb/mysql/tests/thirdparty/__init__.py +6 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/capabilities.py +323 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/dbapi20.py +865 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +110 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +224 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +101 -0
- singlestoredb/mysql/times.py +23 -0
- 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 -117
- singlestoredb/tests/test_config.py +13 -15
- singlestoredb/tests/test_connection.py +241 -289
- singlestoredb/tests/test_dbapi.py +27 -0
- singlestoredb/tests/test_exceptions.py +0 -2
- 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 -28
- singlestoredb/tests/test_management.py +588 -10
- singlestoredb/tests/test_plugin.py +33 -0
- singlestoredb/tests/test_results.py +11 -14
- singlestoredb/tests/test_types.py +0 -2
- singlestoredb/tests/test_udf.py +687 -0
- singlestoredb/tests/test_xdict.py +0 -2
- singlestoredb/tests/utils.py +3 -4
- singlestoredb/types.py +4 -5
- singlestoredb/utils/config.py +71 -12
- singlestoredb/utils/convert_rows.py +0 -2
- singlestoredb/utils/debug.py +13 -0
- singlestoredb/utils/mogrify.py +151 -0
- singlestoredb/utils/results.py +4 -3
- singlestoredb/utils/xdict.py +12 -12
- singlestoredb-1.0.3.dist-info/METADATA +139 -0
- singlestoredb-1.0.3.dist-info/RECORD +112 -0
- {singlestoredb-0.3.3.dist-info → singlestoredb-1.0.3.dist-info}/WHEEL +1 -1
- singlestoredb-1.0.3.dist-info/entry_points.txt +2 -0
- singlestoredb/drivers/__init__.py +0 -46
- singlestoredb/drivers/base.py +0 -200
- singlestoredb/drivers/cymysql.py +0 -40
- singlestoredb/drivers/http.py +0 -49
- singlestoredb/drivers/mariadb.py +0 -42
- singlestoredb/drivers/mysqlconnector.py +0 -51
- singlestoredb/drivers/mysqldb.py +0 -62
- singlestoredb/drivers/pymysql.py +0 -39
- singlestoredb/drivers/pyodbc.py +0 -67
- singlestoredb/http.py +0 -794
- singlestoredb-0.3.3.dist-info/METADATA +0 -105
- singlestoredb-0.3.3.dist-info/RECORD +0 -46
- {singlestoredb-0.3.3.dist-info → singlestoredb-1.0.3.dist-info}/LICENSE +0 -0
- {singlestoredb-0.3.3.dist-info → singlestoredb-1.0.3.dist-info}/top_level.txt +0 -0
|
@@ -3,21 +3,909 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import datetime
|
|
6
|
-
|
|
6
|
+
import glob
|
|
7
|
+
import io
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import socket
|
|
11
|
+
import time
|
|
7
12
|
from typing import Any
|
|
13
|
+
from typing import BinaryIO
|
|
8
14
|
from typing import Dict
|
|
9
15
|
from typing import List
|
|
10
16
|
from typing import Optional
|
|
17
|
+
from typing import TextIO
|
|
11
18
|
from typing import Union
|
|
12
19
|
|
|
13
20
|
from .. import connection
|
|
14
21
|
from ..exceptions import ManagementError
|
|
22
|
+
from .billing_usage import BillingUsageItem
|
|
15
23
|
from .manager import Manager
|
|
24
|
+
from .organization import Organization
|
|
16
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
|
|
17
30
|
from .utils import to_datetime
|
|
31
|
+
from .utils import ttl_property
|
|
18
32
|
from .utils import vars_to_str
|
|
19
33
|
|
|
20
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
|
+
|
|
21
909
|
class Workspace(object):
|
|
22
910
|
"""
|
|
23
911
|
SingleStoreDB workspace definition.
|
|
@@ -37,9 +925,12 @@ class Workspace(object):
|
|
|
37
925
|
"""
|
|
38
926
|
|
|
39
927
|
def __init__(
|
|
40
|
-
self,
|
|
41
|
-
|
|
42
|
-
|
|
928
|
+
self,
|
|
929
|
+
name: str,
|
|
930
|
+
workspace_id: str,
|
|
931
|
+
workspace_group: Union[str, 'WorkspaceGroup'],
|
|
932
|
+
size: str,
|
|
933
|
+
state: str,
|
|
43
934
|
created_at: Union[str, datetime.datetime],
|
|
44
935
|
terminated_at: Optional[Union[str, datetime.datetime]] = None,
|
|
45
936
|
endpoint: Optional[str] = None,
|
|
@@ -83,7 +974,7 @@ class Workspace(object):
|
|
|
83
974
|
return str(self)
|
|
84
975
|
|
|
85
976
|
@classmethod
|
|
86
|
-
def from_dict(cls, obj: Dict[str, Any], manager: 'WorkspaceManager') -> Workspace:
|
|
977
|
+
def from_dict(cls, obj: Dict[str, Any], manager: 'WorkspaceManager') -> 'Workspace':
|
|
87
978
|
"""
|
|
88
979
|
Construct a Workspace from a dictionary of values.
|
|
89
980
|
|
|
@@ -125,6 +1016,7 @@ class Workspace(object):
|
|
|
125
1016
|
wait_on_terminated: bool = False,
|
|
126
1017
|
wait_interval: int = 10,
|
|
127
1018
|
wait_timeout: int = 600,
|
|
1019
|
+
force: bool = False,
|
|
128
1020
|
) -> None:
|
|
129
1021
|
"""
|
|
130
1022
|
Terminate the workspace.
|
|
@@ -137,6 +1029,8 @@ class Workspace(object):
|
|
|
137
1029
|
Number of seconds between each server check
|
|
138
1030
|
wait_timeout : int, optional
|
|
139
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?
|
|
140
1034
|
|
|
141
1035
|
Raises
|
|
142
1036
|
------
|
|
@@ -148,7 +1042,8 @@ class Workspace(object):
|
|
|
148
1042
|
raise ManagementError(
|
|
149
1043
|
msg='No workspace manager is associated with this object.',
|
|
150
1044
|
)
|
|
151
|
-
|
|
1045
|
+
force_str = 'true' if force else 'false'
|
|
1046
|
+
self._manager._delete(f'workspaces/{self.id}?force={force_str}')
|
|
152
1047
|
if wait_on_terminated:
|
|
153
1048
|
self._manager._wait_on_state(
|
|
154
1049
|
self._manager.get_workspace(self.id),
|
|
@@ -178,6 +1073,78 @@ class Workspace(object):
|
|
|
178
1073
|
kwargs['host'] = self.endpoint
|
|
179
1074
|
return connection.connect(**kwargs)
|
|
180
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
|
+
|
|
181
1148
|
|
|
182
1149
|
class WorkspaceGroup(object):
|
|
183
1150
|
"""
|
|
@@ -200,8 +1167,8 @@ class WorkspaceGroup(object):
|
|
|
200
1167
|
def __init__(
|
|
201
1168
|
self, name: str, id: str,
|
|
202
1169
|
created_at: Union[str, datetime.datetime],
|
|
203
|
-
region: Region,
|
|
204
|
-
firewall_ranges:
|
|
1170
|
+
region: Optional[Region],
|
|
1171
|
+
firewall_ranges: List[str],
|
|
205
1172
|
terminated_at: Optional[Union[str, datetime.datetime]],
|
|
206
1173
|
):
|
|
207
1174
|
#: Name of the workspace group
|
|
@@ -213,7 +1180,7 @@ class WorkspaceGroup(object):
|
|
|
213
1180
|
#: Timestamp of when the workspace group was created
|
|
214
1181
|
self.created_at = to_datetime(created_at)
|
|
215
1182
|
|
|
216
|
-
#: Region of the
|
|
1183
|
+
#: Region of the workspace group (see :class:`Region`)
|
|
217
1184
|
self.region = region
|
|
218
1185
|
|
|
219
1186
|
#: List of allowed incoming IP addresses / ranges
|
|
@@ -235,7 +1202,7 @@ class WorkspaceGroup(object):
|
|
|
235
1202
|
@classmethod
|
|
236
1203
|
def from_dict(
|
|
237
1204
|
cls, obj: Dict[str, Any], manager: 'WorkspaceManager',
|
|
238
|
-
) -> WorkspaceGroup:
|
|
1205
|
+
) -> 'WorkspaceGroup':
|
|
239
1206
|
"""
|
|
240
1207
|
Construct a WorkspaceGroup from a dictionary of values.
|
|
241
1208
|
|
|
@@ -251,18 +1218,42 @@ class WorkspaceGroup(object):
|
|
|
251
1218
|
:class:`WorkspaceGroup`
|
|
252
1219
|
|
|
253
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>')
|
|
254
1225
|
out = cls(
|
|
255
|
-
name=obj['name'],
|
|
1226
|
+
name=obj['name'],
|
|
1227
|
+
id=obj['workspaceGroupID'],
|
|
256
1228
|
created_at=obj['createdAt'],
|
|
257
|
-
region=
|
|
1229
|
+
region=region,
|
|
258
1230
|
firewall_ranges=obj.get('firewallRanges', []),
|
|
259
1231
|
terminated_at=obj.get('terminatedAt'),
|
|
260
1232
|
)
|
|
261
1233
|
out._manager = manager
|
|
262
1234
|
return out
|
|
263
1235
|
|
|
264
|
-
|
|
265
|
-
|
|
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
|
+
|
|
1255
|
+
def refresh(self) -> 'WorkspaceGroup':
|
|
1256
|
+
"""Update the object to the current state."""
|
|
266
1257
|
if self._manager is None:
|
|
267
1258
|
raise ManagementError(
|
|
268
1259
|
msg='No workspace manager is associated with this object.',
|
|
@@ -275,17 +1266,17 @@ class WorkspaceGroup(object):
|
|
|
275
1266
|
def update(
|
|
276
1267
|
self, name: Optional[str] = None,
|
|
277
1268
|
admin_password: Optional[str] = None,
|
|
278
|
-
firewall_ranges: Optional[
|
|
1269
|
+
firewall_ranges: Optional[List[str]] = None,
|
|
279
1270
|
) -> None:
|
|
280
1271
|
"""
|
|
281
|
-
Update the
|
|
1272
|
+
Update the workspace group definition.
|
|
282
1273
|
|
|
283
1274
|
Parameters
|
|
284
1275
|
----------
|
|
285
1276
|
name : str, optional
|
|
286
|
-
|
|
1277
|
+
Workspace group name
|
|
287
1278
|
admim_password : str, optional
|
|
288
|
-
Admin password for the
|
|
1279
|
+
Admin password for the workspace group
|
|
289
1280
|
firewall_ranges : Sequence[str], optional
|
|
290
1281
|
List of allowed incoming IP addresses
|
|
291
1282
|
|
|
@@ -317,7 +1308,7 @@ class WorkspaceGroup(object):
|
|
|
317
1308
|
force : bool, optional
|
|
318
1309
|
Terminate a workspace group even if it has active workspaces
|
|
319
1310
|
wait_on_terminated : bool, optional
|
|
320
|
-
Wait for the
|
|
1311
|
+
Wait for the workspace group to go into 'Terminated' mode before returning
|
|
321
1312
|
wait_interval : int, optional
|
|
322
1313
|
Number of seconds between each server check
|
|
323
1314
|
wait_timeout : int, optional
|
|
@@ -335,16 +1326,21 @@ class WorkspaceGroup(object):
|
|
|
335
1326
|
)
|
|
336
1327
|
self._manager._delete(f'workspaceGroups/{self.id}', params=dict(force=force))
|
|
337
1328
|
if wait_on_terminated:
|
|
338
|
-
|
|
339
|
-
self.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
343
1339
|
|
|
344
1340
|
def create_workspace(
|
|
345
1341
|
self, name: str, size: Optional[str] = None,
|
|
346
1342
|
wait_on_active: bool = False, wait_interval: int = 10,
|
|
347
|
-
wait_timeout: int = 600,
|
|
1343
|
+
wait_timeout: int = 600, add_endpoint_to_firewall_ranges: bool = True,
|
|
348
1344
|
) -> Workspace:
|
|
349
1345
|
"""
|
|
350
1346
|
Create a new workspace.
|
|
@@ -362,6 +1358,9 @@ class WorkspaceGroup(object):
|
|
|
362
1358
|
if wait=True
|
|
363
1359
|
wait_interval : int, optional
|
|
364
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?
|
|
365
1364
|
|
|
366
1365
|
Returns
|
|
367
1366
|
-------
|
|
@@ -372,20 +1371,100 @@ class WorkspaceGroup(object):
|
|
|
372
1371
|
raise ManagementError(
|
|
373
1372
|
msg='No workspace manager is associated with this object.',
|
|
374
1373
|
)
|
|
375
|
-
|
|
1374
|
+
|
|
1375
|
+
out = self._manager.create_workspace(
|
|
376
1376
|
name=name, workspace_group=self, size=size, wait_on_active=wait_on_active,
|
|
377
1377
|
wait_interval=wait_interval, wait_timeout=wait_timeout,
|
|
378
1378
|
)
|
|
379
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
|
+
|
|
380
1386
|
@property
|
|
381
|
-
def workspaces(self) ->
|
|
1387
|
+
def workspaces(self) -> NamedList[Workspace]:
|
|
382
1388
|
"""Return a list of available workspaces."""
|
|
383
1389
|
if self._manager is None:
|
|
384
1390
|
raise ManagementError(
|
|
385
1391
|
msg='No workspace manager is associated with this object.',
|
|
386
1392
|
)
|
|
387
1393
|
res = self._manager._get('workspaces', params=dict(workspaceGroupID=self.id))
|
|
388
|
-
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)
|
|
389
1468
|
|
|
390
1469
|
|
|
391
1470
|
class WorkspaceManager(Manager):
|
|
@@ -419,20 +1498,38 @@ class WorkspaceManager(Manager):
|
|
|
419
1498
|
obj_type = 'workspace'
|
|
420
1499
|
|
|
421
1500
|
@property
|
|
422
|
-
def workspace_groups(self) ->
|
|
1501
|
+
def workspace_groups(self) -> NamedList[WorkspaceGroup]:
|
|
423
1502
|
"""Return a list of available workspace groups."""
|
|
424
1503
|
res = self._get('workspaceGroups')
|
|
425
|
-
return [WorkspaceGroup.from_dict(item, self) for item in res.json()]
|
|
1504
|
+
return NamedList([WorkspaceGroup.from_dict(item, self) for item in res.json()])
|
|
426
1505
|
|
|
427
1506
|
@property
|
|
428
|
-
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]:
|
|
429
1523
|
"""Return a list of available regions."""
|
|
430
1524
|
res = self._get('regions')
|
|
431
|
-
return [Region.from_dict(item, self) for item in res.json()]
|
|
1525
|
+
return NamedList([Region.from_dict(item, self) for item in res.json()])
|
|
432
1526
|
|
|
433
1527
|
def create_workspace_group(
|
|
434
1528
|
self, name: str, region: Union[str, Region],
|
|
435
|
-
firewall_ranges:
|
|
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,
|
|
436
1533
|
) -> WorkspaceGroup:
|
|
437
1534
|
"""
|
|
438
1535
|
Create a new workspace group.
|
|
@@ -449,6 +1546,17 @@ class WorkspaceManager(Manager):
|
|
|
449
1546
|
admin_password : str, optional
|
|
450
1547
|
Admin password for the workspace group. If no password is supplied,
|
|
451
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)
|
|
452
1560
|
|
|
453
1561
|
Returns
|
|
454
1562
|
-------
|
|
@@ -461,7 +1569,10 @@ class WorkspaceManager(Manager):
|
|
|
461
1569
|
'workspaceGroups', json=dict(
|
|
462
1570
|
name=name, regionID=region,
|
|
463
1571
|
adminPassword=admin_password,
|
|
464
|
-
firewallRanges=firewall_ranges,
|
|
1572
|
+
firewallRanges=firewall_ranges or [],
|
|
1573
|
+
expiresAt=expires_at,
|
|
1574
|
+
allowAllTraffic=allow_all_traffic,
|
|
1575
|
+
updateWindow=update_window,
|
|
465
1576
|
),
|
|
466
1577
|
)
|
|
467
1578
|
return self.get_workspace_group(res.json()['workspaceGroupID'])
|
|
@@ -550,6 +1661,8 @@ def manage_workspaces(
|
|
|
550
1661
|
access_token: Optional[str] = None,
|
|
551
1662
|
version: str = WorkspaceManager.default_version,
|
|
552
1663
|
base_url: str = WorkspaceManager.default_base_url,
|
|
1664
|
+
*,
|
|
1665
|
+
organization_id: Optional[str] = None,
|
|
553
1666
|
) -> WorkspaceManager:
|
|
554
1667
|
"""
|
|
555
1668
|
Retrieve a SingleStoreDB workspace manager.
|
|
@@ -562,10 +1675,15 @@ def manage_workspaces(
|
|
|
562
1675
|
Version of the API to use
|
|
563
1676
|
base_url : str, optional
|
|
564
1677
|
Base URL of the workspace management API
|
|
1678
|
+
organization_id : str, optional
|
|
1679
|
+
ID of organization, if using a JWT for authentication
|
|
565
1680
|
|
|
566
1681
|
Returns
|
|
567
1682
|
-------
|
|
568
1683
|
:class:`WorkspaceManager`
|
|
569
1684
|
|
|
570
1685
|
"""
|
|
571
|
-
return WorkspaceManager(
|
|
1686
|
+
return WorkspaceManager(
|
|
1687
|
+
access_token=access_token, base_url=base_url,
|
|
1688
|
+
version=version, organization_id=organization_id,
|
|
1689
|
+
)
|