Path: blob/main/singlestoredb/management/workspace.py
801 views
#!/usr/bin/env python1"""SingleStoreDB Workspace Management."""2from __future__ import annotations34import datetime5import glob6import io7import os8import re9import time10from collections.abc import Mapping11from typing import Any12from typing import cast13from typing import Dict14from typing import List15from typing import Literal16from typing import Optional17from typing import overload18from typing import Union1920from .. import config21from .. import connection22from ..exceptions import ManagementError23from .billing_usage import BillingUsageItem24from .files import FileLocation25from .files import FilesObject26from .files import FilesObjectBytesReader27from .files import FilesObjectBytesWriter28from .files import FilesObjectTextReader29from .files import FilesObjectTextWriter30from .manager import Manager31from .organization import Organization32from .region import Region33from .utils import camel_to_snake_dict34from .utils import from_datetime35from .utils import NamedList36from .utils import PathLike37from .utils import snake_to_camel38from .utils import snake_to_camel_dict39from .utils import to_datetime40from .utils import ttl_property41from .utils import vars_to_str424344def get_organization() -> Organization:45"""Get the organization."""46return manage_workspaces().organization474849def get_secret(name: str) -> Optional[str]:50"""Get a secret from the organization."""51return get_organization().get_secret(name).value525354def get_workspace_group(55workspace_group: Optional[Union[WorkspaceGroup, str]] = None,56) -> WorkspaceGroup:57"""Get the stage for the workspace group."""58if isinstance(workspace_group, WorkspaceGroup):59return workspace_group60elif workspace_group:61return manage_workspaces().workspace_groups[workspace_group]62elif 'SINGLESTOREDB_WORKSPACE_GROUP' in os.environ:63return manage_workspaces().workspace_groups[64os.environ['SINGLESTOREDB_WORKSPACE_GROUP']65]66raise RuntimeError('no workspace group specified')676869def get_stage(70workspace_group: Optional[Union[WorkspaceGroup, str]] = None,71) -> Stage:72"""Get the stage for the workspace group."""73return get_workspace_group(workspace_group).stage747576def get_workspace(77workspace_group: Optional[Union[WorkspaceGroup, str]] = None,78workspace: Optional[Union[Workspace, str]] = None,79) -> Workspace:80"""Get the workspaces for a workspace_group."""81if isinstance(workspace, Workspace):82return workspace83wg = get_workspace_group(workspace_group)84if workspace:85return wg.workspaces[workspace]86elif 'SINGLESTOREDB_WORKSPACE' in os.environ:87return wg.workspaces[88os.environ['SINGLESTOREDB_WORKSPACE']89]90raise RuntimeError('no workspace group specified')919293class Stage(FileLocation):94"""95Stage manager.9697This object is not instantiated directly.98It is returned by ``WorkspaceGroup.stage`` or ``StarterWorkspace.stage``.99100"""101102def __init__(self, deployment_id: str, manager: WorkspaceManager):103self._deployment_id = deployment_id104self._manager = manager105106def open(107self,108stage_path: PathLike,109mode: str = 'r',110encoding: Optional[str] = None,111) -> Union[io.StringIO, io.BytesIO]:112"""113Open a Stage path for reading or writing.114115Parameters116----------117stage_path : Path or str118The stage path to read / write119mode : str, optional120The read / write mode. The following modes are supported:121* 'r' open for reading (default)122* 'w' open for writing, truncating the file first123* 'x' create a new file and open it for writing124The data type can be specified by adding one of the following:125* 'b' binary mode126* 't' text mode (default)127encoding : str, optional128The string encoding to use for text129130Returns131-------132FilesObjectBytesReader - 'rb' or 'b' mode133FilesObjectBytesWriter - 'wb' or 'xb' mode134FilesObjectTextReader - 'r' or 'rt' mode135FilesObjectTextWriter - 'w', 'x', 'wt' or 'xt' mode136137"""138if '+' in mode or 'a' in mode:139raise ValueError('modifying an existing stage file is not supported')140141if 'w' in mode or 'x' in mode:142exists = self.exists(stage_path)143if exists:144if 'x' in mode:145raise FileExistsError(f'stage path already exists: {stage_path}')146self.remove(stage_path)147if 'b' in mode:148return FilesObjectBytesWriter(b'', self, stage_path)149return FilesObjectTextWriter('', self, stage_path)150151if 'r' in mode:152content = self.download_file(stage_path)153if isinstance(content, bytes):154if 'b' in mode:155return FilesObjectBytesReader(content)156encoding = 'utf-8' if encoding is None else encoding157return FilesObjectTextReader(content.decode(encoding))158159if isinstance(content, str):160return FilesObjectTextReader(content)161162raise ValueError(f'unrecognized file content type: {type(content)}')163164raise ValueError(f'must have one of create/read/write mode specified: {mode}')165166def upload_file(167self,168local_path: Union[PathLike, io.IOBase],169stage_path: PathLike,170*,171overwrite: bool = False,172) -> FilesObject:173"""174Upload a local file.175176Parameters177----------178local_path : Path or str or file-like179Path to the local file or an open file object180stage_path : Path or str181Path to the stage file182overwrite : bool, optional183Should the ``stage_path`` be overwritten if it exists already?184185"""186if isinstance(local_path, io.IOBase):187pass188elif not os.path.isfile(local_path):189raise IsADirectoryError(f'local path is not a file: {local_path}')190191if self.exists(stage_path):192if not overwrite:193raise OSError(f'stage path already exists: {stage_path}')194195self.remove(stage_path)196197if isinstance(local_path, io.IOBase):198return self._upload(local_path, stage_path, overwrite=overwrite)199200return self._upload(open(local_path, 'rb'), stage_path, overwrite=overwrite)201202def upload_folder(203self,204local_path: PathLike,205stage_path: PathLike,206*,207overwrite: bool = False,208recursive: bool = True,209include_root: bool = False,210ignore: Optional[Union[PathLike, List[PathLike]]] = None,211) -> FilesObject:212"""213Upload a folder recursively.214215Only the contents of the folder are uploaded. To include the216folder name itself in the target path use ``include_root=True``.217218Parameters219----------220local_path : Path or str221Local directory to upload222stage_path : Path or str223Path of stage folder to upload to224overwrite : bool, optional225If a file already exists, should it be overwritten?226recursive : bool, optional227Should nested folders be uploaded?228include_root : bool, optional229Should the local root folder itself be uploaded as the top folder?230ignore : Path or str or List[Path] or List[str], optional231Glob patterns of files to ignore, for example, ``**/*.pyc`` will232ignore all ``*.pyc`` files in the directory tree233234"""235if not os.path.isdir(local_path):236raise NotADirectoryError(f'local path is not a directory: {local_path}')237if self.exists(stage_path) and not self.is_dir(stage_path):238raise NotADirectoryError(f'stage path is not a directory: {stage_path}')239240ignore_files = set()241if ignore:242if isinstance(ignore, list):243for item in ignore:244ignore_files.update(glob.glob(str(item), recursive=recursive))245else:246ignore_files.update(glob.glob(str(ignore), recursive=recursive))247248parent_dir = os.path.basename(os.getcwd())249250files = glob.glob(os.path.join(local_path, '**'), recursive=recursive)251252for src in files:253if ignore_files and src in ignore_files:254continue255target = os.path.join(parent_dir, src) if include_root else src256self.upload_file(src, target, overwrite=overwrite)257258return self.info(stage_path)259260def _upload(261self,262content: Union[str, bytes, io.IOBase],263stage_path: PathLike,264*,265overwrite: bool = False,266) -> FilesObject:267"""268Upload content to a stage file.269270Parameters271----------272content : str or bytes or file-like273Content to upload to stage274stage_path : Path or str275Path to the stage file276overwrite : bool, optional277Should the ``stage_path`` be overwritten if it exists already?278279"""280if self.exists(stage_path):281if not overwrite:282raise OSError(f'stage path already exists: {stage_path}')283self.remove(stage_path)284285self._manager._put(286f'stage/{self._deployment_id}/fs/{stage_path}',287files={'file': content},288headers={'Content-Type': None},289)290291return self.info(stage_path)292293def mkdir(self, stage_path: PathLike, overwrite: bool = False) -> FilesObject:294"""295Make a directory in the stage.296297Parameters298----------299stage_path : Path or str300Path of the folder to create301overwrite : bool, optional302Should the stage path be overwritten if it exists already?303304Returns305-------306FilesObject307308"""309stage_path = re.sub(r'/*$', r'', str(stage_path)) + '/'310311if self.exists(stage_path):312if not overwrite:313return self.info(stage_path)314315self.remove(stage_path)316317self._manager._put(318f'stage/{self._deployment_id}/fs/{stage_path}?isFile=false',319)320321return self.info(stage_path)322323mkdirs = mkdir324325def rename(326self,327old_path: PathLike,328new_path: PathLike,329*,330overwrite: bool = False,331) -> FilesObject:332"""333Move the stage file to a new location.334335Paraemeters336-----------337old_path : Path or str338Original location of the path339new_path : Path or str340New location of the path341overwrite : bool, optional342Should the ``new_path`` be overwritten if it exists already?343344"""345if not self.exists(old_path):346raise OSError(f'stage path does not exist: {old_path}')347348if self.exists(new_path):349if not overwrite:350raise OSError(f'stage path already exists: {new_path}')351352if str(old_path).endswith('/') and not str(new_path).endswith('/'):353raise OSError('original and new paths are not the same type')354355if str(new_path).endswith('/'):356self.removedirs(new_path)357else:358self.remove(new_path)359360self._manager._patch(361f'stage/{self._deployment_id}/fs/{old_path}',362json=dict(newPath=new_path),363)364365return self.info(new_path)366367def info(self, stage_path: PathLike) -> FilesObject:368"""369Return information about a stage location.370371Parameters372----------373stage_path : Path or str374Path to the stage location375376Returns377-------378FilesObject379380"""381res = self._manager._get(382re.sub(r'/+$', r'/', f'stage/{self._deployment_id}/fs/{stage_path}'),383params=dict(metadata=1),384).json()385386return FilesObject.from_dict(res, self)387388def exists(self, stage_path: PathLike) -> bool:389"""390Does the given stage path exist?391392Parameters393----------394stage_path : Path or str395Path to stage object396397Returns398-------399bool400401"""402try:403self.info(stage_path)404return True405except ManagementError as exc:406if exc.errno == 404:407return False408raise409410def is_dir(self, stage_path: PathLike) -> bool:411"""412Is the given stage path a directory?413414Parameters415----------416stage_path : Path or str417Path to stage object418419Returns420-------421bool422423"""424try:425return self.info(stage_path).type == 'directory'426except ManagementError as exc:427if exc.errno == 404:428return False429raise430431def is_file(self, stage_path: PathLike) -> bool:432"""433Is the given stage path a file?434435Parameters436----------437stage_path : Path or str438Path to stage object439440Returns441-------442bool443444"""445try:446return self.info(stage_path).type != 'directory'447except ManagementError as exc:448if exc.errno == 404:449return False450raise451452def _listdir(453self, stage_path: PathLike, *,454recursive: bool = False,455return_objects: bool = False,456) -> List[Union[str, 'FilesObject']]:457"""458Return the names (or FilesObject instances) of files in a directory.459460Parameters461----------462stage_path : Path or str463Path to the folder in Stage464recursive : bool, optional465Should folders be listed recursively?466return_objects : bool, optional467If True, return list of FilesObject instances. Otherwise just paths.468469"""470from .files import FilesObject471res = self._manager._get(472re.sub(r'/+$', r'/', f'stage/{self._deployment_id}/fs/{stage_path}'),473).json()474if recursive:475out: List[Union[str, FilesObject]] = []476for item in res['content'] or []:477if return_objects:478out.append(FilesObject.from_dict(item, self))479else:480out.append(item['path'])481if item['type'] == 'directory':482out.extend(483self._listdir(484item['path'],485recursive=recursive,486return_objects=return_objects,487),488)489return out490if return_objects:491return [492FilesObject.from_dict(x, self)493for x in res['content'] or []494]495return [x['path'] for x in res['content'] or []]496497@overload498def listdir(499self,500stage_path: PathLike = '/',501*,502recursive: bool = False,503return_objects: Literal[True],504) -> List['FilesObject']:505...506507@overload508def listdir(509self,510stage_path: PathLike = '/',511*,512recursive: bool = False,513return_objects: Literal[False] = False,514) -> List[str]:515...516517def listdir(518self,519stage_path: PathLike = '/',520*,521recursive: bool = False,522return_objects: bool = False,523) -> Union[List[str], List['FilesObject']]:524"""525List the files / folders at the given path.526527Parameters528----------529stage_path : Path or str, optional530Path to the stage location531recursive : bool, optional532If True, recursively list all files and folders533return_objects : bool, optional534If True, return list of FilesObject instances. Otherwise just paths.535536Returns537-------538List[str] or List[FilesObject]539540"""541from .files import FilesObject542stage_path = re.sub(r'^(\./|/)+', r'', str(stage_path))543stage_path = re.sub(r'/+$', r'', stage_path) + '/'544545if self.is_dir(stage_path):546out = self._listdir(547stage_path,548recursive=recursive,549return_objects=return_objects,550)551if stage_path != '/':552stage_path_n = len(stage_path.split('/')) - 1553if return_objects:554result: List[FilesObject] = []555for item in out:556if isinstance(item, FilesObject):557rel = '/'.join(item.path.split('/')[stage_path_n:])558item.path = rel559result.append(item)560return result561out = ['/'.join(str(x).split('/')[stage_path_n:]) for x in out]562if return_objects:563return cast(List[FilesObject], out)564return cast(List[str], out)565566raise NotADirectoryError(f'stage path is not a directory: {stage_path}')567568def download_file(569self,570stage_path: PathLike,571local_path: Optional[PathLike] = None,572*,573overwrite: bool = False,574encoding: Optional[str] = None,575) -> Optional[Union[bytes, str]]:576"""577Download the content of a stage path.578579Parameters580----------581stage_path : Path or str582Path to the stage file583local_path : Path or str584Path to local file target location585overwrite : bool, optional586Should an existing file be overwritten if it exists?587encoding : str, optional588Encoding used to convert the resulting data589590Returns591-------592bytes or str - ``local_path`` is None593None - ``local_path`` is a Path or str594595"""596if local_path is not None and not overwrite and os.path.exists(local_path):597raise OSError('target file already exists; use overwrite=True to replace')598if self.is_dir(stage_path):599raise IsADirectoryError(f'stage path is a directory: {stage_path}')600601out = self._manager._get(602f'stage/{self._deployment_id}/fs/{stage_path}',603).content604605if local_path is not None:606with open(local_path, 'wb') as outfile:607outfile.write(out)608return None609610if encoding:611return out.decode(encoding)612613return out614615def download_folder(616self,617stage_path: PathLike,618local_path: PathLike = '.',619*,620overwrite: bool = False,621) -> None:622"""623Download a Stage folder to a local directory.624625Parameters626----------627stage_path : Path or str628Path to the stage file629local_path : Path or str630Path to local directory target location631overwrite : bool, optional632Should an existing directory / files be overwritten if they exist?633634"""635if local_path is not None and not overwrite and os.path.exists(local_path):636raise OSError(637'target directory already exists; '638'use overwrite=True to replace',639)640if not self.is_dir(stage_path):641raise NotADirectoryError(f'stage path is not a directory: {stage_path}')642643for f in self.listdir(stage_path, recursive=True, return_objects=False):644if self.is_dir(f):645continue646target = os.path.normpath(os.path.join(local_path, f))647os.makedirs(os.path.dirname(target), exist_ok=True)648self.download_file(f, target, overwrite=overwrite)649650def remove(self, stage_path: PathLike) -> None:651"""652Delete a stage location.653654Parameters655----------656stage_path : Path or str657Path to the stage location658659"""660if self.is_dir(stage_path):661raise IsADirectoryError(662'stage path is a directory, '663f'use rmdir or removedirs: {stage_path}',664)665666self._manager._delete(f'stage/{self._deployment_id}/fs/{stage_path}')667668def removedirs(self, stage_path: PathLike) -> None:669"""670Delete a stage folder recursively.671672Parameters673----------674stage_path : Path or str675Path to the stage location676677"""678stage_path = re.sub(r'/*$', r'', str(stage_path)) + '/'679self._manager._delete(f'stage/{self._deployment_id}/fs/{stage_path}')680681def rmdir(self, stage_path: PathLike) -> None:682"""683Delete a stage folder.684685Parameters686----------687stage_path : Path or str688Path to the stage location689690"""691stage_path = re.sub(r'/*$', r'', str(stage_path)) + '/'692693if self.listdir(stage_path):694raise OSError(f'stage folder is not empty, use removedirs: {stage_path}')695696self._manager._delete(f'stage/{self._deployment_id}/fs/{stage_path}')697698def __str__(self) -> str:699"""Return string representation."""700return vars_to_str(self)701702def __repr__(self) -> str:703"""Return string representation."""704return str(self)705706707StageObject = FilesObject # alias for backward compatibility708709710class Workspace(object):711"""712SingleStoreDB workspace definition.713714This object is not instantiated directly. It is used in the results715of API calls on the :class:`WorkspaceManager`. Workspaces are created using716:meth:`WorkspaceManager.create_workspace`, or existing workspaces are717accessed by either :attr:`WorkspaceManager.workspaces` or by calling718:meth:`WorkspaceManager.get_workspace`.719720See Also721--------722:meth:`WorkspaceManager.create_workspace`723:meth:`WorkspaceManager.get_workspace`724:attr:`WorkspaceManager.workspaces`725726"""727728name: str729id: str730group_id: str731size: str732state: str733created_at: Optional[datetime.datetime]734terminated_at: Optional[datetime.datetime]735endpoint: Optional[str]736auto_suspend: Optional[Dict[str, Any]]737cache_config: Optional[int]738deployment_type: Optional[str]739resume_attachments: Optional[List[Dict[str, Any]]]740scaling_progress: Optional[int]741last_resumed_at: Optional[datetime.datetime]742743def __init__(744self,745name: str,746workspace_id: str,747workspace_group: Union[str, 'WorkspaceGroup'],748size: str,749state: str,750created_at: Union[str, datetime.datetime],751terminated_at: Optional[Union[str, datetime.datetime]] = None,752endpoint: Optional[str] = None,753auto_suspend: Optional[Dict[str, Any]] = None,754cache_config: Optional[int] = None,755deployment_type: Optional[str] = None,756resume_attachments: Optional[List[Dict[str, Any]]] = None,757scaling_progress: Optional[int] = None,758last_resumed_at: Optional[Union[str, datetime.datetime]] = None,759):760#: Name of the workspace761self.name = name762763#: Unique ID of the workspace764self.id = workspace_id765766#: Unique ID of the workspace group767if isinstance(workspace_group, WorkspaceGroup):768self.group_id = workspace_group.id769else:770self.group_id = workspace_group771772#: Size of the workspace in workspace size notation (S-00, S-1, etc.)773self.size = size774775#: State of the workspace: PendingCreation, Transitioning, Active,776#: Terminated, Suspended, Resuming, Failed777self.state = state.strip()778779#: Timestamp of when the workspace was created780self.created_at = to_datetime(created_at)781782#: Timestamp of when the workspace was terminated783self.terminated_at = to_datetime(terminated_at)784785#: Hostname (or IP address) of the workspace database server786self.endpoint = endpoint787788#: Current auto-suspend settings789self.auto_suspend = camel_to_snake_dict(auto_suspend)790791#: Multiplier for the persistent cache792self.cache_config = cache_config793794#: Deployment type of the workspace795self.deployment_type = deployment_type796797#: Database attachments798self.resume_attachments = [799camel_to_snake_dict(x) # type: ignore800for x in resume_attachments or []801if x is not None802]803804#: Current progress percentage for scaling the workspace805self.scaling_progress = scaling_progress806807#: Timestamp when workspace was last resumed808self.last_resumed_at = to_datetime(last_resumed_at)809810self._manager: Optional[WorkspaceManager] = None811812def __str__(self) -> str:813"""Return string representation."""814return vars_to_str(self)815816def __repr__(self) -> str:817"""Return string representation."""818return str(self)819820@classmethod821def from_dict(cls, obj: Dict[str, Any], manager: 'WorkspaceManager') -> 'Workspace':822"""823Construct a Workspace from a dictionary of values.824825Parameters826----------827obj : dict828Dictionary of values829manager : WorkspaceManager, optional830The WorkspaceManager the Workspace belongs to831832Returns833-------834:class:`Workspace`835836"""837out = cls(838name=obj['name'],839workspace_id=obj['workspaceID'],840workspace_group=obj['workspaceGroupID'],841size=obj.get('size', 'Unknown'),842state=obj['state'],843created_at=obj['createdAt'],844terminated_at=obj.get('terminatedAt'),845endpoint=obj.get('endpoint'),846auto_suspend=obj.get('autoSuspend'),847cache_config=obj.get('cacheConfig'),848deployment_type=obj.get('deploymentType'),849last_resumed_at=obj.get('lastResumedAt'),850resume_attachments=obj.get('resumeAttachments'),851scaling_progress=obj.get('scalingProgress'),852)853out._manager = manager854return out855856def update(857self,858auto_suspend: Optional[Dict[str, Any]] = None,859cache_config: Optional[int] = None,860deployment_type: Optional[str] = None,861size: Optional[str] = None,862) -> None:863"""864Update the workspace definition.865866Parameters867----------868auto_suspend : Dict[str, Any], optional869Auto-suspend mode for the workspace: IDLE, SCHEDULED, DISABLED870cache_config : int, optional871Specifies the multiplier for the persistent cache associated872with the workspace. If specified, it enables the cache configuration873multiplier. It can have one of the following values: 1, 2, or 4.874deployment_type : str, optional875The deployment type that will be applied to all the workspaces876within the group877size : str, optional878Size of the workspace (in workspace size notation), such as "S-1".879880"""881if self._manager is None:882raise ManagementError(883msg='No workspace manager is associated with this object.',884)885data = {886k: v for k, v in dict(887autoSuspend=snake_to_camel_dict(auto_suspend),888cacheConfig=cache_config,889deploymentType=deployment_type,890size=size,891).items() if v is not None892}893self._manager._patch(f'workspaces/{self.id}', json=data)894self.refresh()895896def refresh(self) -> Workspace:897"""Update the object to the current state."""898if self._manager is None:899raise ManagementError(900msg='No workspace manager is associated with this object.',901)902new_obj = self._manager.get_workspace(self.id)903for name, value in vars(new_obj).items():904if isinstance(value, Mapping):905setattr(self, name, snake_to_camel_dict(value))906else:907setattr(self, name, value)908return self909910def terminate(911self,912wait_on_terminated: bool = False,913wait_interval: int = 10,914wait_timeout: int = 600,915force: bool = False,916) -> None:917"""918Terminate the workspace.919920Parameters921----------922wait_on_terminated : bool, optional923Wait for the workspace to go into 'Terminated' mode before returning924wait_interval : int, optional925Number of seconds between each server check926wait_timeout : int, optional927Total number of seconds to check server before giving up928force : bool, optional929Should the workspace group be terminated even if it has workspaces?930931Raises932------933ManagementError934If timeout is reached935936"""937if self._manager is None:938raise ManagementError(939msg='No workspace manager is associated with this object.',940)941force_str = 'true' if force else 'false'942self._manager._delete(f'workspaces/{self.id}?force={force_str}')943if wait_on_terminated:944self._manager._wait_on_state(945self._manager.get_workspace(self.id),946'Terminated', interval=wait_interval, timeout=wait_timeout,947)948self.refresh()949950def connect(self, **kwargs: Any) -> connection.Connection:951"""952Create a connection to the database server for this workspace.953954Parameters955----------956**kwargs : keyword-arguments, optional957Parameters to the SingleStoreDB `connect` function except host958and port which are supplied by the workspace object959960Returns961-------962:class:`Connection`963964"""965if not self.endpoint:966raise ManagementError(967msg='An endpoint has not been set in this workspace configuration',968)969kwargs['host'] = self.endpoint970return connection.connect(**kwargs)971972def suspend(973self,974wait_on_suspended: bool = False,975wait_interval: int = 20,976wait_timeout: int = 600,977) -> None:978"""979Suspend the workspace.980981Parameters982----------983wait_on_suspended : bool, optional984Wait for the workspace to go into 'Suspended' mode before returning985wait_interval : int, optional986Number of seconds between each server check987wait_timeout : int, optional988Total number of seconds to check server before giving up989990Raises991------992ManagementError993If timeout is reached994995"""996if self._manager is None:997raise ManagementError(998msg='No workspace manager is associated with this object.',999)1000self._manager._post(f'workspaces/{self.id}/suspend')1001if wait_on_suspended:1002self._manager._wait_on_state(1003self._manager.get_workspace(self.id),1004'Suspended', interval=wait_interval, timeout=wait_timeout,1005)1006self.refresh()10071008def resume(1009self,1010disable_auto_suspend: bool = False,1011wait_on_resumed: bool = False,1012wait_interval: int = 20,1013wait_timeout: int = 600,1014) -> None:1015"""1016Resume the workspace.10171018Parameters1019----------1020disable_auto_suspend : bool, optional1021Should auto-suspend be disabled?1022wait_on_resumed : bool, optional1023Wait for the workspace to go into 'Resumed' or 'Active' mode before returning1024wait_interval : int, optional1025Number of seconds between each server check1026wait_timeout : int, optional1027Total number of seconds to check server before giving up10281029Raises1030------1031ManagementError1032If timeout is reached10331034"""1035if self._manager is None:1036raise ManagementError(1037msg='No workspace manager is associated with this object.',1038)1039self._manager._post(1040f'workspaces/{self.id}/resume',1041json=dict(disableAutoSuspend=disable_auto_suspend),1042)1043if wait_on_resumed:1044self._manager._wait_on_state(1045self._manager.get_workspace(self.id),1046['Resumed', 'Active'], interval=wait_interval, timeout=wait_timeout,1047)1048self.refresh()104910501051class WorkspaceGroup(object):1052"""1053SingleStoreDB workspace group definition.10541055This object is not instantiated directly. It is used in the results1056of API calls on the :class:`WorkspaceManager`. Workspace groups are created using1057:meth:`WorkspaceManager.create_workspace_group`, or existing workspace groups are1058accessed by either :attr:`WorkspaceManager.workspace_groups` or by calling1059:meth:`WorkspaceManager.get_workspace_group`.10601061See Also1062--------1063:meth:`WorkspaceManager.create_workspace_group`1064:meth:`WorkspaceManager.get_workspace_group`1065:attr:`WorkspaceManager.workspace_groups`10661067"""10681069name: str1070id: str1071created_at: Optional[datetime.datetime]1072region: Optional[Region]1073firewall_ranges: List[str]1074terminated_at: Optional[datetime.datetime]1075allow_all_traffic: bool10761077def __init__(1078self,1079name: str,1080id: str,1081created_at: Union[str, datetime.datetime],1082region: Optional[Region],1083firewall_ranges: List[str],1084terminated_at: Optional[Union[str, datetime.datetime]],1085allow_all_traffic: Optional[bool],1086):1087#: Name of the workspace group1088self.name = name10891090#: Unique ID of the workspace group1091self.id = id10921093#: Timestamp of when the workspace group was created1094self.created_at = to_datetime(created_at)10951096#: Region of the workspace group (see :class:`Region`)1097self.region = region10981099#: List of allowed incoming IP addresses / ranges1100self.firewall_ranges = firewall_ranges11011102#: Timestamp of when the workspace group was terminated1103self.terminated_at = to_datetime(terminated_at)11041105#: Should all traffic be allowed?1106self.allow_all_traffic = allow_all_traffic or False11071108self._manager: Optional[WorkspaceManager] = None11091110def __str__(self) -> str:1111"""Return string representation."""1112return vars_to_str(self)11131114def __repr__(self) -> str:1115"""Return string representation."""1116return str(self)11171118@classmethod1119def from_dict(1120cls, obj: Dict[str, Any], manager: 'WorkspaceManager',1121) -> 'WorkspaceGroup':1122"""1123Construct a WorkspaceGroup from a dictionary of values.11241125Parameters1126----------1127obj : dict1128Dictionary of values1129manager : WorkspaceManager, optional1130The WorkspaceManager the WorkspaceGroup belongs to11311132Returns1133-------1134:class:`WorkspaceGroup`11351136"""1137try:1138region = [x for x in manager.regions if x.id == obj['regionID']][0]1139except IndexError:1140region = Region('<unknown>', '<unknown>', obj.get('regionID', '<unknown>'))1141out = cls(1142name=obj['name'],1143id=obj['workspaceGroupID'],1144created_at=obj['createdAt'],1145region=region,1146firewall_ranges=obj.get('firewallRanges', []),1147terminated_at=obj.get('terminatedAt'),1148allow_all_traffic=obj.get('allowAllTraffic'),1149)1150out._manager = manager1151return out11521153@property1154def organization(self) -> Organization:1155if self._manager is None:1156raise ManagementError(1157msg='No workspace manager is associated with this object.',1158)1159return self._manager.organization11601161@property1162def stage(self) -> Stage:1163"""Stage manager."""1164if self._manager is None:1165raise ManagementError(1166msg='No workspace manager is associated with this object.',1167)1168return Stage(self.id, self._manager)11691170stages = stage11711172def refresh(self) -> 'WorkspaceGroup':1173"""Update the object to the current state."""1174if self._manager is None:1175raise ManagementError(1176msg='No workspace manager is associated with this object.',1177)1178new_obj = self._manager.get_workspace_group(self.id)1179for name, value in vars(new_obj).items():1180if isinstance(value, Mapping):1181setattr(self, name, camel_to_snake_dict(value))1182else:1183setattr(self, name, value)1184return self11851186def update(1187self,1188name: Optional[str] = None,1189firewall_ranges: Optional[List[str]] = None,1190admin_password: Optional[str] = None,1191expires_at: Optional[str] = None,1192allow_all_traffic: Optional[bool] = None,1193update_window: Optional[Dict[str, int]] = None,1194) -> None:1195"""1196Update the workspace group definition.11971198Parameters1199----------1200name : str, optional1201Name of the workspace group1202firewall_ranges : list[str], optional1203List of allowed CIDR ranges. An empty list indicates that all1204inbound requests are allowed.1205admin_password : str, optional1206Admin password for the workspace group. If no password is supplied,1207a password will be generated and retured in the response.1208expires_at : str, optional1209The timestamp of when the workspace group will expire.1210If the expiration time is not specified,1211the workspace group will have no expiration time.1212At expiration, the workspace group is terminated and all the data is lost.1213Expiration time can be specified as a timestamp or duration.1214Example: "2021-01-02T15:04:05Z07:00", "2021-01-02", "3h30m"1215allow_all_traffic : bool, optional1216Allow all traffic to the workspace group1217update_window : Dict[str, int], optional1218Specify the day and hour of an update window: dict(day=0-6, hour=0-23)12191220"""1221if self._manager is None:1222raise ManagementError(1223msg='No workspace manager is associated with this object.',1224)1225data = {1226k: v for k, v in dict(1227name=name,1228firewallRanges=firewall_ranges,1229adminPassword=admin_password,1230expiresAt=expires_at,1231allowAllTraffic=allow_all_traffic,1232updateWindow=snake_to_camel_dict(update_window),1233).items() if v is not None1234}1235self._manager._patch(f'workspaceGroups/{self.id}', json=data)1236self.refresh()12371238def terminate(1239self, force: bool = False,1240wait_on_terminated: bool = False,1241wait_interval: int = 10,1242wait_timeout: int = 600,1243) -> None:1244"""1245Terminate the workspace group.12461247Parameters1248----------1249force : bool, optional1250Terminate a workspace group even if it has active workspaces1251wait_on_terminated : bool, optional1252Wait for the workspace group to go into 'Terminated' mode before returning1253wait_interval : int, optional1254Number of seconds between each server check1255wait_timeout : int, optional1256Total number of seconds to check server before giving up12571258Raises1259------1260ManagementError1261If timeout is reached12621263"""1264if self._manager is None:1265raise ManagementError(1266msg='No workspace manager is associated with this object.',1267)1268self._manager._delete(f'workspaceGroups/{self.id}', params=dict(force=force))1269if wait_on_terminated:1270while True:1271self.refresh()1272if self.terminated_at is not None:1273break1274if wait_timeout <= 0:1275raise ManagementError(1276msg='Exceeded waiting time for WorkspaceGroup to terminate',1277)1278time.sleep(wait_interval)1279wait_timeout -= wait_interval12801281def create_workspace(1282self,1283name: str,1284size: Optional[str] = None,1285auto_suspend: Optional[Dict[str, Any]] = None,1286cache_config: Optional[int] = None,1287enable_kai: Optional[bool] = None,1288wait_on_active: bool = False,1289wait_interval: int = 10,1290wait_timeout: int = 600,1291) -> Workspace:1292"""1293Create a new workspace.12941295Parameters1296----------1297name : str1298Name of the workspace1299size : str, optional1300Workspace size in workspace size notation (S-00, S-1, etc.)1301auto_suspend : Dict[str, Any], optional1302Auto suspend settings for the workspace. If this field is not1303provided, no settings will be enabled.1304cache_config : int, optional1305Specifies the multiplier for the persistent cache associated1306with the workspace. If specified, it enables the cache configuration1307multiplier. It can have one of the following values: 1, 2, or 4.1308enable_kai : bool, optional1309Whether to create a SingleStore Kai-enabled workspace1310wait_on_active : bool, optional1311Wait for the workspace to be active before returning1312wait_timeout : int, optional1313Maximum number of seconds to wait before raising an exception1314if wait=True1315wait_interval : int, optional1316Number of seconds between each polling interval13171318Returns1319-------1320:class:`Workspace`13211322"""1323if self._manager is None:1324raise ManagementError(1325msg='No workspace manager is associated with this object.',1326)13271328out = self._manager.create_workspace(1329name=name,1330workspace_group=self,1331size=size,1332auto_suspend=snake_to_camel_dict(auto_suspend),1333cache_config=cache_config,1334enable_kai=enable_kai,1335wait_on_active=wait_on_active,1336wait_interval=wait_interval,1337wait_timeout=wait_timeout,1338)13391340return out13411342@property1343def workspaces(self) -> NamedList[Workspace]:1344"""Return a list of available workspaces."""1345if self._manager is None:1346raise ManagementError(1347msg='No workspace manager is associated with this object.',1348)1349res = self._manager._get('workspaces', params=dict(workspaceGroupID=self.id))1350return NamedList(1351[Workspace.from_dict(item, self._manager) for item in res.json()],1352)135313541355class StarterWorkspace(object):1356"""1357SingleStoreDB starter workspace definition.13581359This object is not instantiated directly. It is used in the results1360of API calls on the :class:`WorkspaceManager`. Existing starter workspaces are1361accessed by either :attr:`WorkspaceManager.starter_workspaces` or by calling1362:meth:`WorkspaceManager.get_starter_workspace`.13631364See Also1365--------1366:meth:`WorkspaceManager.get_starter_workspace`1367:meth:`WorkspaceManager.create_starter_workspace`1368:meth:`WorkspaceManager.terminate_starter_workspace`1369:meth:`WorkspaceManager.create_starter_workspace_user`1370:attr:`WorkspaceManager.starter_workspaces`13711372"""13731374name: str1375id: str1376database_name: str1377endpoint: Optional[str]13781379def __init__(1380self,1381name: str,1382id: str,1383database_name: str,1384endpoint: Optional[str] = None,1385):1386#: Name of the starter workspace1387self.name = name13881389#: Unique ID of the starter workspace1390self.id = id13911392#: Name of the database associated with the starter workspace1393self.database_name = database_name13941395#: Endpoint to connect to the starter workspace. The endpoint is in the form1396#: of ``hostname:port``1397self.endpoint = endpoint13981399self._manager: Optional[WorkspaceManager] = None14001401def __str__(self) -> str:1402"""Return string representation."""1403return vars_to_str(self)14041405def __repr__(self) -> str:1406"""Return string representation."""1407return str(self)14081409@classmethod1410def from_dict(1411cls, obj: Dict[str, Any], manager: 'WorkspaceManager',1412) -> 'StarterWorkspace':1413"""1414Construct a StarterWorkspace from a dictionary of values.14151416Parameters1417----------1418obj : dict1419Dictionary of values1420manager : WorkspaceManager, optional1421The WorkspaceManager the StarterWorkspace belongs to14221423Returns1424-------1425:class:`StarterWorkspace`14261427"""1428out = cls(1429name=obj['name'],1430id=obj['virtualWorkspaceID'],1431database_name=obj['databaseName'],1432endpoint=obj.get('endpoint'),1433)1434out._manager = manager1435return out14361437def connect(self, **kwargs: Any) -> connection.Connection:1438"""1439Create a connection to the database server for this starter workspace.14401441Parameters1442----------1443**kwargs : keyword-arguments, optional1444Parameters to the SingleStoreDB `connect` function except host1445and port which are supplied by the starter workspace object14461447Returns1448-------1449:class:`Connection`14501451"""1452if not self.endpoint:1453raise ManagementError(1454msg='An endpoint has not been set in this '1455'starter workspace configuration',1456)14571458kwargs['host'] = self.endpoint1459kwargs['database'] = self.database_name14601461return connection.connect(**kwargs)14621463def terminate(self) -> None:1464"""Terminate the starter workspace."""1465if self._manager is None:1466raise ManagementError(1467msg='No workspace manager is associated with this object.',1468)1469self._manager._delete(f'sharedtier/virtualWorkspaces/{self.id}')14701471def refresh(self) -> StarterWorkspace:1472"""Update the object to the current state."""1473if self._manager is None:1474raise ManagementError(1475msg='No workspace manager is associated with this object.',1476)1477new_obj = self._manager.get_starter_workspace(self.id)1478for name, value in vars(new_obj).items():1479if isinstance(value, Mapping):1480setattr(self, name, snake_to_camel_dict(value))1481else:1482setattr(self, name, value)1483return self14841485@property1486def organization(self) -> Organization:1487if self._manager is None:1488raise ManagementError(1489msg='No workspace manager is associated with this object.',1490)1491return self._manager.organization14921493@property1494def stage(self) -> Stage:1495"""Stage manager."""1496if self._manager is None:1497raise ManagementError(1498msg='No workspace manager is associated with this object.',1499)1500return Stage(self.id, self._manager)15011502stages = stage15031504@property1505def starter_workspaces(self) -> NamedList['StarterWorkspace']:1506"""Return a list of available starter workspaces."""1507if self._manager is None:1508raise ManagementError(1509msg='No workspace manager is associated with this object.',1510)1511res = self._manager._get('sharedtier/virtualWorkspaces')1512return NamedList(1513[StarterWorkspace.from_dict(item, self._manager) for item in res.json()],1514)15151516def create_user(1517self,1518username: str,1519password: Optional[str] = None,1520) -> Dict[str, str]:1521"""1522Create a new user for this starter workspace.15231524Parameters1525----------1526username : str1527The starter workspace user name to connect the new user to the database1528password : str, optional1529Password for the new user. If not provided, a password will be1530auto-generated by the system.15311532Returns1533-------1534Dict[str, str]1535Dictionary containing 'userID' and 'password' of the created user15361537Raises1538------1539ManagementError1540If no workspace manager is associated with this object.1541"""1542if self._manager is None:1543raise ManagementError(1544msg='No workspace manager is associated with this object.',1545)15461547payload = {1548'userName': username,1549}1550if password is not None:1551payload['password'] = password15521553res = self._manager._post(1554f'sharedtier/virtualWorkspaces/{self.id}/users',1555json=payload,1556)15571558response_data = res.json()1559user_id = response_data.get('userID')1560if not user_id:1561raise ManagementError(msg='No userID returned from API')15621563# Return the password provided by user or generated by API1564returned_password = password if password is not None \1565else response_data.get('password')1566if not returned_password:1567raise ManagementError(msg='No password available from API response')15681569return {1570'user_id': user_id,1571'password': returned_password,1572}157315741575class Billing(object):1576"""Billing information."""15771578COMPUTE_CREDIT = 'compute_credit'1579STORAGE_AVG_BYTE = 'storage_avg_byte'15801581HOUR = 'hour'1582DAY = 'day'1583MONTH = 'month'15841585def __init__(self, manager: Manager):1586self._manager = manager15871588def usage(1589self,1590start_time: datetime.datetime,1591end_time: datetime.datetime,1592metric: Optional[str] = None,1593aggregate_by: Optional[str] = None,1594) -> List[BillingUsageItem]:1595"""1596Get usage information.15971598Parameters1599----------1600start_time : datetime.datetime1601Start time for usage interval1602end_time : datetime.datetime1603End time for usage interval1604metric : str, optional1605Possible metrics are ``mgr.billing.COMPUTE_CREDIT`` and1606``mgr.billing.STORAGE_AVG_BYTE`` (default is all)1607aggregate_by : str, optional1608Aggregate type used to group usage: ``mgr.billing.HOUR``,1609``mgr.billing.DAY``, or ``mgr.billing.MONTH``16101611Returns1612-------1613List[BillingUsage]16141615"""1616res = self._manager._get(1617'billing/usage',1618params={1619k: v for k, v in dict(1620metric=snake_to_camel(metric),1621startTime=from_datetime(start_time),1622endTime=from_datetime(end_time),1623aggregate_by=aggregate_by.lower() if aggregate_by else None,1624).items() if v is not None1625},1626)1627return [1628BillingUsageItem.from_dict(x, self._manager)1629for x in res.json()['billingUsage']1630]163116321633class Organizations(object):1634"""Organizations."""16351636def __init__(self, manager: Manager):1637self._manager = manager16381639@property1640def current(self) -> Organization:1641"""Get current organization."""1642res = self._manager._get('organizations/current').json()1643return Organization.from_dict(res, self._manager)164416451646class WorkspaceManager(Manager):1647"""1648SingleStoreDB workspace manager.16491650This class should be instantiated using :func:`singlestoredb.manage_workspaces`.16511652Parameters1653----------1654access_token : str, optional1655The API key or other access token for the workspace management API1656version : str, optional1657Version of the API to use1658base_url : str, optional1659Base URL of the workspace management API16601661See Also1662--------1663:func:`singlestoredb.manage_workspaces`16641665"""16661667#: Workspace management API version if none is specified.1668default_version = config.get_option('management.version') or 'v1'16691670#: Base URL if none is specified.1671default_base_url = config.get_option('management.base_url') \1672or 'https://api.singlestore.com'16731674#: Object type1675obj_type = 'workspace'16761677@property1678def workspace_groups(self) -> NamedList[WorkspaceGroup]:1679"""Return a list of available workspace groups."""1680res = self._get('workspaceGroups')1681return NamedList([WorkspaceGroup.from_dict(item, self) for item in res.json()])16821683@property1684def starter_workspaces(self) -> NamedList[StarterWorkspace]:1685"""Return a list of available starter workspaces."""1686res = self._get('sharedtier/virtualWorkspaces')1687return NamedList([StarterWorkspace.from_dict(item, self) for item in res.json()])16881689@property1690def organizations(self) -> Organizations:1691"""Return the organizations."""1692return Organizations(self)16931694@property1695def organization(self) -> Organization:1696""" Return the current organization."""1697return self.organizations.current16981699@property1700def billing(self) -> Billing:1701"""Return the current billing information."""1702return Billing(self)17031704@ttl_property(datetime.timedelta(hours=1))1705def regions(self) -> NamedList[Region]:1706"""Return a list of available regions."""1707res = self._get('regions')1708return NamedList([Region.from_dict(item, self) for item in res.json()])17091710@ttl_property(datetime.timedelta(hours=1))1711def shared_tier_regions(self) -> NamedList[Region]:1712"""Return a list of regions that support shared tier workspaces."""1713res = self._get('regions/sharedtier')1714return NamedList(1715[Region.from_dict(item, self) for item in res.json()],1716)17171718def create_workspace_group(1719self,1720name: str,1721region: Union[str, Region],1722firewall_ranges: List[str],1723admin_password: Optional[str] = None,1724backup_bucket_kms_key_id: Optional[str] = None,1725data_bucket_kms_key_id: Optional[str] = None,1726expires_at: Optional[str] = None,1727smart_dr: Optional[bool] = None,1728allow_all_traffic: Optional[bool] = None,1729update_window: Optional[Dict[str, int]] = None,1730) -> WorkspaceGroup:1731"""1732Create a new workspace group.17331734Parameters1735----------1736name : str1737Name of the workspace group1738region : str or Region1739ID of the region where the workspace group should be created1740firewall_ranges : list[str]1741List of allowed CIDR ranges. An empty list indicates that all1742inbound requests are allowed.1743admin_password : str, optional1744Admin password for the workspace group. If no password is supplied,1745a password will be generated and retured in the response.1746backup_bucket_kms_key_id : str, optional1747Specifies the KMS key ID associated with the backup bucket.1748If specified, enables Customer-Managed Encryption Keys (CMEK)1749encryption for the backup bucket of the workspace group.1750This feature is only supported in workspace groups deployed in AWS.1751data_bucket_kms_key_id : str, optional1752Specifies the KMS key ID associated with the data bucket.1753If specified, enables Customer-Managed Encryption Keys (CMEK)1754encryption for the data bucket and Amazon Elastic Block Store1755(EBS) volumes of the workspace group. This feature is only supported1756in workspace groups deployed in AWS.1757expires_at : str, optional1758The timestamp of when the workspace group will expire.1759If the expiration time is not specified,1760the workspace group will have no expiration time.1761At expiration, the workspace group is terminated and all the data is lost.1762Expiration time can be specified as a timestamp or duration.1763Example: "2021-01-02T15:04:05Z07:00", "2021-01-02", "3h30m"1764smart_dr : bool, optional1765Enables Smart Disaster Recovery (SmartDR) for the workspace group.1766SmartDR is a disaster recovery solution that ensures seamless and1767continuous replication of data from the primary region to a secondary region1768allow_all_traffic : bool, optional1769Allow all traffic to the workspace group1770update_window : Dict[str, int], optional1771Specify the day and hour of an update window: dict(day=0-6, hour=0-23)17721773Returns1774-------1775:class:`WorkspaceGroup`17761777"""1778if isinstance(region, Region) and region.id:1779region = region.id1780res = self._post(1781'workspaceGroups', json=dict(1782name=name, regionID=region,1783adminPassword=admin_password,1784backupBucketKMSKeyID=backup_bucket_kms_key_id,1785dataBucketKMSKeyID=data_bucket_kms_key_id,1786firewallRanges=firewall_ranges or [],1787expiresAt=expires_at,1788smartDR=smart_dr,1789allowAllTraffic=allow_all_traffic,1790updateWindow=snake_to_camel_dict(update_window),1791),1792)1793return self.get_workspace_group(res.json()['workspaceGroupID'])17941795def create_workspace(1796self,1797name: str,1798workspace_group: Union[str, WorkspaceGroup],1799size: Optional[str] = None,1800auto_suspend: Optional[Dict[str, Any]] = None,1801cache_config: Optional[int] = None,1802enable_kai: Optional[bool] = None,1803wait_on_active: bool = False,1804wait_interval: int = 10,1805wait_timeout: int = 600,1806) -> Workspace:1807"""1808Create a new workspace.18091810Parameters1811----------1812name : str1813Name of the workspace1814workspace_group : str or WorkspaceGroup1815The workspace ID of the workspace1816size : str, optional1817Workspace size in workspace size notation (S-00, S-1, etc.)1818auto_suspend : Dict[str, Any], optional1819Auto suspend settings for the workspace. If this field is not1820provided, no settings will be enabled.1821cache_config : int, optional1822Specifies the multiplier for the persistent cache associated1823with the workspace. If specified, it enables the cache configuration1824multiplier. It can have one of the following values: 1, 2, or 4.1825enable_kai : bool, optional1826Whether to create a SingleStore Kai-enabled workspace1827wait_on_active : bool, optional1828Wait for the workspace to be active before returning1829wait_timeout : int, optional1830Maximum number of seconds to wait before raising an exception1831if wait=True1832wait_interval : int, optional1833Number of seconds between each polling interval18341835Returns1836-------1837:class:`Workspace`18381839"""1840if isinstance(workspace_group, WorkspaceGroup):1841workspace_group = workspace_group.id1842res = self._post(1843'workspaces', json=dict(1844name=name,1845workspaceGroupID=workspace_group,1846size=size,1847autoSuspend=snake_to_camel_dict(auto_suspend),1848cacheConfig=cache_config,1849enableKai=enable_kai,1850),1851)1852out = self.get_workspace(res.json()['workspaceID'])1853if wait_on_active:1854out = self._wait_on_state(1855out,1856'Active',1857interval=wait_interval,1858timeout=wait_timeout,1859)1860# After workspace is active, wait for endpoint to be ready1861out = self._wait_on_endpoint(1862out,1863interval=wait_interval,1864timeout=wait_timeout,1865)1866return out18671868def get_workspace_group(self, id: str) -> WorkspaceGroup:1869"""1870Retrieve a workspace group definition.18711872Parameters1873----------1874id : str1875ID of the workspace group18761877Returns1878-------1879:class:`WorkspaceGroup`18801881"""1882res = self._get(f'workspaceGroups/{id}')1883return WorkspaceGroup.from_dict(res.json(), manager=self)18841885def get_workspace(self, id: str) -> Workspace:1886"""1887Retrieve a workspace definition.18881889Parameters1890----------1891id : str1892ID of the workspace18931894Returns1895-------1896:class:`Workspace`18971898"""1899res = self._get(f'workspaces/{id}')1900return Workspace.from_dict(res.json(), manager=self)19011902def get_starter_workspace(self, id: str) -> StarterWorkspace:1903"""1904Retrieve a starter workspace definition.19051906Parameters1907----------1908id : str1909ID of the starter workspace19101911Returns1912-------1913:class:`StarterWorkspace`19141915"""1916res = self._get(f'sharedtier/virtualWorkspaces/{id}')1917return StarterWorkspace.from_dict(res.json(), manager=self)19181919def create_starter_workspace(1920self,1921name: str,1922database_name: str,1923provider: str,1924region_name: str,1925) -> 'StarterWorkspace':1926"""1927Create a new starter (shared tier) workspace.19281929Parameters1930----------1931name : str1932Name of the starter workspace1933database_name : str1934Name of the database for the starter workspace1935provider : str1936Cloud provider for the starter workspace (e.g., 'aws', 'gcp', 'azure')1937region_name : str1938Cloud provider region for the starter workspace (e.g., 'us-east-1')19391940Returns1941-------1942:class:`StarterWorkspace`1943"""19441945payload = {1946'name': name,1947'databaseName': database_name,1948'provider': provider,1949'regionName': region_name,1950}19511952res = self._post('sharedtier/virtualWorkspaces', json=payload)1953virtual_workspace_id = res.json().get('virtualWorkspaceID')1954if not virtual_workspace_id:1955raise ManagementError(msg='No virtualWorkspaceID returned from API')19561957res = self._get(f'sharedtier/virtualWorkspaces/{virtual_workspace_id}')1958return StarterWorkspace.from_dict(res.json(), self)195919601961def manage_workspaces(1962access_token: Optional[str] = None,1963version: Optional[str] = None,1964base_url: Optional[str] = None,1965*,1966organization_id: Optional[str] = None,1967) -> WorkspaceManager:1968"""1969Retrieve a SingleStoreDB workspace manager.19701971Parameters1972----------1973access_token : str, optional1974The API key or other access token for the workspace management API1975version : str, optional1976Version of the API to use1977base_url : str, optional1978Base URL of the workspace management API1979organization_id : str, optional1980ID of organization, if using a JWT for authentication19811982Returns1983-------1984:class:`WorkspaceManager`19851986"""1987return WorkspaceManager(1988access_token=access_token, base_url=base_url,1989version=version, organization_id=organization_id,1990)199119921993