Path: blob/main/singlestoredb/management/workspace.py
469 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 Dict13from typing import List14from typing import Optional15from typing import Union1617from .. import config18from .. import connection19from ..exceptions import ManagementError20from .billing_usage import BillingUsageItem21from .files import FileLocation22from .files import FilesObject23from .files import FilesObjectBytesReader24from .files import FilesObjectBytesWriter25from .files import FilesObjectTextReader26from .files import FilesObjectTextWriter27from .manager import Manager28from .organization import Organization29from .region import Region30from .utils import camel_to_snake_dict31from .utils import from_datetime32from .utils import NamedList33from .utils import PathLike34from .utils import snake_to_camel35from .utils import snake_to_camel_dict36from .utils import to_datetime37from .utils import ttl_property38from .utils import vars_to_str394041def get_organization() -> Organization:42"""Get the organization."""43return manage_workspaces().organization444546def get_secret(name: str) -> Optional[str]:47"""Get a secret from the organization."""48return get_organization().get_secret(name).value495051def get_workspace_group(52workspace_group: Optional[Union[WorkspaceGroup, str]] = None,53) -> WorkspaceGroup:54"""Get the stage for the workspace group."""55if isinstance(workspace_group, WorkspaceGroup):56return workspace_group57elif workspace_group:58return manage_workspaces().workspace_groups[workspace_group]59elif 'SINGLESTOREDB_WORKSPACE_GROUP' in os.environ:60return manage_workspaces().workspace_groups[61os.environ['SINGLESTOREDB_WORKSPACE_GROUP']62]63raise RuntimeError('no workspace group specified')646566def get_stage(67workspace_group: Optional[Union[WorkspaceGroup, str]] = None,68) -> Stage:69"""Get the stage for the workspace group."""70return get_workspace_group(workspace_group).stage717273def get_workspace(74workspace_group: Optional[Union[WorkspaceGroup, str]] = None,75workspace: Optional[Union[Workspace, str]] = None,76) -> Workspace:77"""Get the workspaces for a workspace_group."""78if isinstance(workspace, Workspace):79return workspace80wg = get_workspace_group(workspace_group)81if workspace:82return wg.workspaces[workspace]83elif 'SINGLESTOREDB_WORKSPACE' in os.environ:84return wg.workspaces[85os.environ['SINGLESTOREDB_WORKSPACE']86]87raise RuntimeError('no workspace group specified')888990class Stage(FileLocation):91"""92Stage manager.9394This object is not instantiated directly.95It is returned by ``WorkspaceGroup.stage`` or ``StarterWorkspace.stage``.9697"""9899def __init__(self, deployment_id: str, manager: WorkspaceManager):100self._deployment_id = deployment_id101self._manager = manager102103def open(104self,105stage_path: PathLike,106mode: str = 'r',107encoding: Optional[str] = None,108) -> Union[io.StringIO, io.BytesIO]:109"""110Open a Stage path for reading or writing.111112Parameters113----------114stage_path : Path or str115The stage path to read / write116mode : str, optional117The read / write mode. The following modes are supported:118* 'r' open for reading (default)119* 'w' open for writing, truncating the file first120* 'x' create a new file and open it for writing121The data type can be specified by adding one of the following:122* 'b' binary mode123* 't' text mode (default)124encoding : str, optional125The string encoding to use for text126127Returns128-------129FilesObjectBytesReader - 'rb' or 'b' mode130FilesObjectBytesWriter - 'wb' or 'xb' mode131FilesObjectTextReader - 'r' or 'rt' mode132FilesObjectTextWriter - 'w', 'x', 'wt' or 'xt' mode133134"""135if '+' in mode or 'a' in mode:136raise ValueError('modifying an existing stage file is not supported')137138if 'w' in mode or 'x' in mode:139exists = self.exists(stage_path)140if exists:141if 'x' in mode:142raise FileExistsError(f'stage path already exists: {stage_path}')143self.remove(stage_path)144if 'b' in mode:145return FilesObjectBytesWriter(b'', self, stage_path)146return FilesObjectTextWriter('', self, stage_path)147148if 'r' in mode:149content = self.download_file(stage_path)150if isinstance(content, bytes):151if 'b' in mode:152return FilesObjectBytesReader(content)153encoding = 'utf-8' if encoding is None else encoding154return FilesObjectTextReader(content.decode(encoding))155156if isinstance(content, str):157return FilesObjectTextReader(content)158159raise ValueError(f'unrecognized file content type: {type(content)}')160161raise ValueError(f'must have one of create/read/write mode specified: {mode}')162163def upload_file(164self,165local_path: Union[PathLike, io.IOBase],166stage_path: PathLike,167*,168overwrite: bool = False,169) -> FilesObject:170"""171Upload a local file.172173Parameters174----------175local_path : Path or str or file-like176Path to the local file or an open file object177stage_path : Path or str178Path to the stage file179overwrite : bool, optional180Should the ``stage_path`` be overwritten if it exists already?181182"""183if isinstance(local_path, io.IOBase):184pass185elif not os.path.isfile(local_path):186raise IsADirectoryError(f'local path is not a file: {local_path}')187188if self.exists(stage_path):189if not overwrite:190raise OSError(f'stage path already exists: {stage_path}')191192self.remove(stage_path)193194if isinstance(local_path, io.IOBase):195return self._upload(local_path, stage_path, overwrite=overwrite)196197return self._upload(open(local_path, 'rb'), stage_path, overwrite=overwrite)198199def upload_folder(200self,201local_path: PathLike,202stage_path: PathLike,203*,204overwrite: bool = False,205recursive: bool = True,206include_root: bool = False,207ignore: Optional[Union[PathLike, List[PathLike]]] = None,208) -> FilesObject:209"""210Upload a folder recursively.211212Only the contents of the folder are uploaded. To include the213folder name itself in the target path use ``include_root=True``.214215Parameters216----------217local_path : Path or str218Local directory to upload219stage_path : Path or str220Path of stage folder to upload to221overwrite : bool, optional222If a file already exists, should it be overwritten?223recursive : bool, optional224Should nested folders be uploaded?225include_root : bool, optional226Should the local root folder itself be uploaded as the top folder?227ignore : Path or str or List[Path] or List[str], optional228Glob patterns of files to ignore, for example, '**/*.pyc` will229ignore all '*.pyc' files in the directory tree230231"""232if not os.path.isdir(local_path):233raise NotADirectoryError(f'local path is not a directory: {local_path}')234if self.exists(stage_path) and not self.is_dir(stage_path):235raise NotADirectoryError(f'stage path is not a directory: {stage_path}')236237ignore_files = set()238if ignore:239if isinstance(ignore, list):240for item in ignore:241ignore_files.update(glob.glob(str(item), recursive=recursive))242else:243ignore_files.update(glob.glob(str(ignore), recursive=recursive))244245parent_dir = os.path.basename(os.getcwd())246247files = glob.glob(os.path.join(local_path, '**'), recursive=recursive)248249for src in files:250if ignore_files and src in ignore_files:251continue252target = os.path.join(parent_dir, src) if include_root else src253self.upload_file(src, target, overwrite=overwrite)254255return self.info(stage_path)256257def _upload(258self,259content: Union[str, bytes, io.IOBase],260stage_path: PathLike,261*,262overwrite: bool = False,263) -> FilesObject:264"""265Upload content to a stage file.266267Parameters268----------269content : str or bytes or file-like270Content to upload to stage271stage_path : Path or str272Path to the stage file273overwrite : bool, optional274Should the ``stage_path`` be overwritten if it exists already?275276"""277if self.exists(stage_path):278if not overwrite:279raise OSError(f'stage path already exists: {stage_path}')280self.remove(stage_path)281282self._manager._put(283f'stage/{self._deployment_id}/fs/{stage_path}',284files={'file': content},285headers={'Content-Type': None},286)287288return self.info(stage_path)289290def mkdir(self, stage_path: PathLike, overwrite: bool = False) -> FilesObject:291"""292Make a directory in the stage.293294Parameters295----------296stage_path : Path or str297Path of the folder to create298overwrite : bool, optional299Should the stage path be overwritten if it exists already?300301Returns302-------303FilesObject304305"""306stage_path = re.sub(r'/*$', r'', str(stage_path)) + '/'307308if self.exists(stage_path):309if not overwrite:310return self.info(stage_path)311312self.remove(stage_path)313314self._manager._put(315f'stage/{self._deployment_id}/fs/{stage_path}?isFile=false',316)317318return self.info(stage_path)319320mkdirs = mkdir321322def rename(323self,324old_path: PathLike,325new_path: PathLike,326*,327overwrite: bool = False,328) -> FilesObject:329"""330Move the stage file to a new location.331332Paraemeters333-----------334old_path : Path or str335Original location of the path336new_path : Path or str337New location of the path338overwrite : bool, optional339Should the ``new_path`` be overwritten if it exists already?340341"""342if not self.exists(old_path):343raise OSError(f'stage path does not exist: {old_path}')344345if self.exists(new_path):346if not overwrite:347raise OSError(f'stage path already exists: {new_path}')348349if str(old_path).endswith('/') and not str(new_path).endswith('/'):350raise OSError('original and new paths are not the same type')351352if str(new_path).endswith('/'):353self.removedirs(new_path)354else:355self.remove(new_path)356357self._manager._patch(358f'stage/{self._deployment_id}/fs/{old_path}',359json=dict(newPath=new_path),360)361362return self.info(new_path)363364def info(self, stage_path: PathLike) -> FilesObject:365"""366Return information about a stage location.367368Parameters369----------370stage_path : Path or str371Path to the stage location372373Returns374-------375FilesObject376377"""378res = self._manager._get(379re.sub(r'/+$', r'/', f'stage/{self._deployment_id}/fs/{stage_path}'),380params=dict(metadata=1),381).json()382383return FilesObject.from_dict(res, self)384385def exists(self, stage_path: PathLike) -> bool:386"""387Does the given stage path exist?388389Parameters390----------391stage_path : Path or str392Path to stage object393394Returns395-------396bool397398"""399try:400self.info(stage_path)401return True402except ManagementError as exc:403if exc.errno == 404:404return False405raise406407def is_dir(self, stage_path: PathLike) -> bool:408"""409Is the given stage path a directory?410411Parameters412----------413stage_path : Path or str414Path to stage object415416Returns417-------418bool419420"""421try:422return self.info(stage_path).type == 'directory'423except ManagementError as exc:424if exc.errno == 404:425return False426raise427428def is_file(self, stage_path: PathLike) -> bool:429"""430Is the given stage path a file?431432Parameters433----------434stage_path : Path or str435Path to stage object436437Returns438-------439bool440441"""442try:443return self.info(stage_path).type != 'directory'444except ManagementError as exc:445if exc.errno == 404:446return False447raise448449def _listdir(self, stage_path: PathLike, *, recursive: bool = False) -> List[str]:450"""451Return the names of files in a directory.452453Parameters454----------455stage_path : Path or str456Path to the folder in Stage457recursive : bool, optional458Should folders be listed recursively?459460"""461res = self._manager._get(462f'stage/{self._deployment_id}/fs/{stage_path}',463).json()464if recursive:465out = []466for item in res['content'] or []:467out.append(item['path'])468if item['type'] == 'directory':469out.extend(self._listdir(item['path'], recursive=recursive))470return out471return [x['path'] for x in res['content'] or []]472473def listdir(474self,475stage_path: PathLike = '/',476*,477recursive: bool = False,478) -> List[str]:479"""480List the files / folders at the given path.481482Parameters483----------484stage_path : Path or str, optional485Path to the stage location486487Returns488-------489List[str]490491"""492stage_path = re.sub(r'^(\./|/)+', r'', str(stage_path))493stage_path = re.sub(r'/+$', r'', stage_path) + '/'494495if self.is_dir(stage_path):496out = self._listdir(stage_path, recursive=recursive)497if stage_path != '/':498stage_path_n = len(stage_path.split('/')) - 1499out = ['/'.join(x.split('/')[stage_path_n:]) for x in out]500return out501502raise NotADirectoryError(f'stage path is not a directory: {stage_path}')503504def download_file(505self,506stage_path: PathLike,507local_path: Optional[PathLike] = None,508*,509overwrite: bool = False,510encoding: Optional[str] = None,511) -> Optional[Union[bytes, str]]:512"""513Download the content of a stage path.514515Parameters516----------517stage_path : Path or str518Path to the stage file519local_path : Path or str520Path to local file target location521overwrite : bool, optional522Should an existing file be overwritten if it exists?523encoding : str, optional524Encoding used to convert the resulting data525526Returns527-------528bytes or str - ``local_path`` is None529None - ``local_path`` is a Path or str530531"""532if local_path is not None and not overwrite and os.path.exists(local_path):533raise OSError('target file already exists; use overwrite=True to replace')534if self.is_dir(stage_path):535raise IsADirectoryError(f'stage path is a directory: {stage_path}')536537out = self._manager._get(538f'stage/{self._deployment_id}/fs/{stage_path}',539).content540541if local_path is not None:542with open(local_path, 'wb') as outfile:543outfile.write(out)544return None545546if encoding:547return out.decode(encoding)548549return out550551def download_folder(552self,553stage_path: PathLike,554local_path: PathLike = '.',555*,556overwrite: bool = False,557) -> None:558"""559Download a Stage folder to a local directory.560561Parameters562----------563stage_path : Path or str564Path to the stage file565local_path : Path or str566Path to local directory target location567overwrite : bool, optional568Should an existing directory / files be overwritten if they exist?569570"""571if local_path is not None and not overwrite and os.path.exists(local_path):572raise OSError(573'target directory already exists; '574'use overwrite=True to replace',575)576if not self.is_dir(stage_path):577raise NotADirectoryError(f'stage path is not a directory: {stage_path}')578579for f in self.listdir(stage_path, recursive=True):580if self.is_dir(f):581continue582target = os.path.normpath(os.path.join(local_path, f))583os.makedirs(os.path.dirname(target), exist_ok=True)584self.download_file(f, target, overwrite=overwrite)585586def remove(self, stage_path: PathLike) -> None:587"""588Delete a stage location.589590Parameters591----------592stage_path : Path or str593Path to the stage location594595"""596if self.is_dir(stage_path):597raise IsADirectoryError(598'stage path is a directory, '599f'use rmdir or removedirs: {stage_path}',600)601602self._manager._delete(f'stage/{self._deployment_id}/fs/{stage_path}')603604def removedirs(self, stage_path: PathLike) -> None:605"""606Delete a stage folder recursively.607608Parameters609----------610stage_path : Path or str611Path to the stage location612613"""614stage_path = re.sub(r'/*$', r'', str(stage_path)) + '/'615self._manager._delete(f'stage/{self._deployment_id}/fs/{stage_path}')616617def rmdir(self, stage_path: PathLike) -> None:618"""619Delete a stage folder.620621Parameters622----------623stage_path : Path or str624Path to the stage location625626"""627stage_path = re.sub(r'/*$', r'', str(stage_path)) + '/'628629if self.listdir(stage_path):630raise OSError(f'stage folder is not empty, use removedirs: {stage_path}')631632self._manager._delete(f'stage/{self._deployment_id}/fs/{stage_path}')633634def __str__(self) -> str:635"""Return string representation."""636return vars_to_str(self)637638def __repr__(self) -> str:639"""Return string representation."""640return str(self)641642643StageObject = FilesObject # alias for backward compatibility644645646class Workspace(object):647"""648SingleStoreDB workspace definition.649650This object is not instantiated directly. It is used in the results651of API calls on the :class:`WorkspaceManager`. Workspaces are created using652:meth:`WorkspaceManager.create_workspace`, or existing workspaces are653accessed by either :attr:`WorkspaceManager.workspaces` or by calling654:meth:`WorkspaceManager.get_workspace`.655656See Also657--------658:meth:`WorkspaceManager.create_workspace`659:meth:`WorkspaceManager.get_workspace`660:attr:`WorkspaceManager.workspaces`661662"""663664name: str665id: str666group_id: str667size: str668state: str669created_at: Optional[datetime.datetime]670terminated_at: Optional[datetime.datetime]671endpoint: Optional[str]672auto_suspend: Optional[Dict[str, Any]]673cache_config: Optional[int]674deployment_type: Optional[str]675resume_attachments: Optional[List[Dict[str, Any]]]676scaling_progress: Optional[int]677last_resumed_at: Optional[datetime.datetime]678679def __init__(680self,681name: str,682workspace_id: str,683workspace_group: Union[str, 'WorkspaceGroup'],684size: str,685state: str,686created_at: Union[str, datetime.datetime],687terminated_at: Optional[Union[str, datetime.datetime]] = None,688endpoint: Optional[str] = None,689auto_suspend: Optional[Dict[str, Any]] = None,690cache_config: Optional[int] = None,691deployment_type: Optional[str] = None,692resume_attachments: Optional[List[Dict[str, Any]]] = None,693scaling_progress: Optional[int] = None,694last_resumed_at: Optional[Union[str, datetime.datetime]] = None,695):696#: Name of the workspace697self.name = name698699#: Unique ID of the workspace700self.id = workspace_id701702#: Unique ID of the workspace group703if isinstance(workspace_group, WorkspaceGroup):704self.group_id = workspace_group.id705else:706self.group_id = workspace_group707708#: Size of the workspace in workspace size notation (S-00, S-1, etc.)709self.size = size710711#: State of the workspace: PendingCreation, Transitioning, Active,712#: Terminated, Suspended, Resuming, Failed713self.state = state.strip()714715#: Timestamp of when the workspace was created716self.created_at = to_datetime(created_at)717718#: Timestamp of when the workspace was terminated719self.terminated_at = to_datetime(terminated_at)720721#: Hostname (or IP address) of the workspace database server722self.endpoint = endpoint723724#: Current auto-suspend settings725self.auto_suspend = camel_to_snake_dict(auto_suspend)726727#: Multiplier for the persistent cache728self.cache_config = cache_config729730#: Deployment type of the workspace731self.deployment_type = deployment_type732733#: Database attachments734self.resume_attachments = [735camel_to_snake_dict(x) # type: ignore736for x in resume_attachments or []737if x is not None738]739740#: Current progress percentage for scaling the workspace741self.scaling_progress = scaling_progress742743#: Timestamp when workspace was last resumed744self.last_resumed_at = to_datetime(last_resumed_at)745746self._manager: Optional[WorkspaceManager] = None747748def __str__(self) -> str:749"""Return string representation."""750return vars_to_str(self)751752def __repr__(self) -> str:753"""Return string representation."""754return str(self)755756@classmethod757def from_dict(cls, obj: Dict[str, Any], manager: 'WorkspaceManager') -> 'Workspace':758"""759Construct a Workspace from a dictionary of values.760761Parameters762----------763obj : dict764Dictionary of values765manager : WorkspaceManager, optional766The WorkspaceManager the Workspace belongs to767768Returns769-------770:class:`Workspace`771772"""773out = cls(774name=obj['name'],775workspace_id=obj['workspaceID'],776workspace_group=obj['workspaceGroupID'],777size=obj.get('size', 'Unknown'),778state=obj['state'],779created_at=obj['createdAt'],780terminated_at=obj.get('terminatedAt'),781endpoint=obj.get('endpoint'),782auto_suspend=obj.get('autoSuspend'),783cache_config=obj.get('cacheConfig'),784deployment_type=obj.get('deploymentType'),785last_resumed_at=obj.get('lastResumedAt'),786resume_attachments=obj.get('resumeAttachments'),787scaling_progress=obj.get('scalingProgress'),788)789out._manager = manager790return out791792def update(793self,794auto_suspend: Optional[Dict[str, Any]] = None,795cache_config: Optional[int] = None,796deployment_type: Optional[str] = None,797size: Optional[str] = None,798) -> None:799"""800Update the workspace definition.801802Parameters803----------804auto_suspend : Dict[str, Any], optional805Auto-suspend mode for the workspace: IDLE, SCHEDULED, DISABLED806cache_config : int, optional807Specifies the multiplier for the persistent cache associated808with the workspace. If specified, it enables the cache configuration809multiplier. It can have one of the following values: 1, 2, or 4.810deployment_type : str, optional811The deployment type that will be applied to all the workspaces812within the group813size : str, optional814Size of the workspace (in workspace size notation), such as "S-1".815816"""817if self._manager is None:818raise ManagementError(819msg='No workspace manager is associated with this object.',820)821data = {822k: v for k, v in dict(823autoSuspend=snake_to_camel_dict(auto_suspend),824cacheConfig=cache_config,825deploymentType=deployment_type,826size=size,827).items() if v is not None828}829self._manager._patch(f'workspaces/{self.id}', json=data)830self.refresh()831832def refresh(self) -> Workspace:833"""Update the object to the current state."""834if self._manager is None:835raise ManagementError(836msg='No workspace manager is associated with this object.',837)838new_obj = self._manager.get_workspace(self.id)839for name, value in vars(new_obj).items():840if isinstance(value, Mapping):841setattr(self, name, snake_to_camel_dict(value))842else:843setattr(self, name, value)844return self845846def terminate(847self,848wait_on_terminated: bool = False,849wait_interval: int = 10,850wait_timeout: int = 600,851force: bool = False,852) -> None:853"""854Terminate the workspace.855856Parameters857----------858wait_on_terminated : bool, optional859Wait for the workspace to go into 'Terminated' mode before returning860wait_interval : int, optional861Number of seconds between each server check862wait_timeout : int, optional863Total number of seconds to check server before giving up864force : bool, optional865Should the workspace group be terminated even if it has workspaces?866867Raises868------869ManagementError870If timeout is reached871872"""873if self._manager is None:874raise ManagementError(875msg='No workspace manager is associated with this object.',876)877force_str = 'true' if force else 'false'878self._manager._delete(f'workspaces/{self.id}?force={force_str}')879if wait_on_terminated:880self._manager._wait_on_state(881self._manager.get_workspace(self.id),882'Terminated', interval=wait_interval, timeout=wait_timeout,883)884self.refresh()885886def connect(self, **kwargs: Any) -> connection.Connection:887"""888Create a connection to the database server for this workspace.889890Parameters891----------892**kwargs : keyword-arguments, optional893Parameters to the SingleStoreDB `connect` function except host894and port which are supplied by the workspace object895896Returns897-------898:class:`Connection`899900"""901if not self.endpoint:902raise ManagementError(903msg='An endpoint has not been set in this workspace configuration',904)905kwargs['host'] = self.endpoint906return connection.connect(**kwargs)907908def suspend(909self,910wait_on_suspended: bool = False,911wait_interval: int = 20,912wait_timeout: int = 600,913) -> None:914"""915Suspend the workspace.916917Parameters918----------919wait_on_suspended : bool, optional920Wait for the workspace to go into 'Suspended' mode before returning921wait_interval : int, optional922Number of seconds between each server check923wait_timeout : int, optional924Total number of seconds to check server before giving up925926Raises927------928ManagementError929If timeout is reached930931"""932if self._manager is None:933raise ManagementError(934msg='No workspace manager is associated with this object.',935)936self._manager._post(f'workspaces/{self.id}/suspend')937if wait_on_suspended:938self._manager._wait_on_state(939self._manager.get_workspace(self.id),940'Suspended', interval=wait_interval, timeout=wait_timeout,941)942self.refresh()943944def resume(945self,946disable_auto_suspend: bool = False,947wait_on_resumed: bool = False,948wait_interval: int = 20,949wait_timeout: int = 600,950) -> None:951"""952Resume the workspace.953954Parameters955----------956disable_auto_suspend : bool, optional957Should auto-suspend be disabled?958wait_on_resumed : bool, optional959Wait for the workspace to go into 'Resumed' or 'Active' mode before returning960wait_interval : int, optional961Number of seconds between each server check962wait_timeout : int, optional963Total number of seconds to check server before giving up964965Raises966------967ManagementError968If timeout is reached969970"""971if self._manager is None:972raise ManagementError(973msg='No workspace manager is associated with this object.',974)975self._manager._post(976f'workspaces/{self.id}/resume',977json=dict(disableAutoSuspend=disable_auto_suspend),978)979if wait_on_resumed:980self._manager._wait_on_state(981self._manager.get_workspace(self.id),982['Resumed', 'Active'], interval=wait_interval, timeout=wait_timeout,983)984self.refresh()985986987class WorkspaceGroup(object):988"""989SingleStoreDB workspace group definition.990991This object is not instantiated directly. It is used in the results992of API calls on the :class:`WorkspaceManager`. Workspace groups are created using993:meth:`WorkspaceManager.create_workspace_group`, or existing workspace groups are994accessed by either :attr:`WorkspaceManager.workspace_groups` or by calling995:meth:`WorkspaceManager.get_workspace_group`.996997See Also998--------999:meth:`WorkspaceManager.create_workspace_group`1000:meth:`WorkspaceManager.get_workspace_group`1001:attr:`WorkspaceManager.workspace_groups`10021003"""10041005name: str1006id: str1007created_at: Optional[datetime.datetime]1008region: Optional[Region]1009firewall_ranges: List[str]1010terminated_at: Optional[datetime.datetime]1011allow_all_traffic: bool10121013def __init__(1014self,1015name: str,1016id: str,1017created_at: Union[str, datetime.datetime],1018region: Optional[Region],1019firewall_ranges: List[str],1020terminated_at: Optional[Union[str, datetime.datetime]],1021allow_all_traffic: Optional[bool],1022):1023#: Name of the workspace group1024self.name = name10251026#: Unique ID of the workspace group1027self.id = id10281029#: Timestamp of when the workspace group was created1030self.created_at = to_datetime(created_at)10311032#: Region of the workspace group (see :class:`Region`)1033self.region = region10341035#: List of allowed incoming IP addresses / ranges1036self.firewall_ranges = firewall_ranges10371038#: Timestamp of when the workspace group was terminated1039self.terminated_at = to_datetime(terminated_at)10401041#: Should all traffic be allowed?1042self.allow_all_traffic = allow_all_traffic or False10431044self._manager: Optional[WorkspaceManager] = None10451046def __str__(self) -> str:1047"""Return string representation."""1048return vars_to_str(self)10491050def __repr__(self) -> str:1051"""Return string representation."""1052return str(self)10531054@classmethod1055def from_dict(1056cls, obj: Dict[str, Any], manager: 'WorkspaceManager',1057) -> 'WorkspaceGroup':1058"""1059Construct a WorkspaceGroup from a dictionary of values.10601061Parameters1062----------1063obj : dict1064Dictionary of values1065manager : WorkspaceManager, optional1066The WorkspaceManager the WorkspaceGroup belongs to10671068Returns1069-------1070:class:`WorkspaceGroup`10711072"""1073try:1074region = [x for x in manager.regions if x.id == obj['regionID']][0]1075except IndexError:1076region = Region('<unknown>', '<unknown>', obj.get('regionID', '<unknown>'))1077out = cls(1078name=obj['name'],1079id=obj['workspaceGroupID'],1080created_at=obj['createdAt'],1081region=region,1082firewall_ranges=obj.get('firewallRanges', []),1083terminated_at=obj.get('terminatedAt'),1084allow_all_traffic=obj.get('allowAllTraffic'),1085)1086out._manager = manager1087return out10881089@property1090def organization(self) -> Organization:1091if self._manager is None:1092raise ManagementError(1093msg='No workspace manager is associated with this object.',1094)1095return self._manager.organization10961097@property1098def stage(self) -> Stage:1099"""Stage manager."""1100if self._manager is None:1101raise ManagementError(1102msg='No workspace manager is associated with this object.',1103)1104return Stage(self.id, self._manager)11051106stages = stage11071108def refresh(self) -> 'WorkspaceGroup':1109"""Update the object to the current state."""1110if self._manager is None:1111raise ManagementError(1112msg='No workspace manager is associated with this object.',1113)1114new_obj = self._manager.get_workspace_group(self.id)1115for name, value in vars(new_obj).items():1116if isinstance(value, Mapping):1117setattr(self, name, camel_to_snake_dict(value))1118else:1119setattr(self, name, value)1120return self11211122def update(1123self,1124name: Optional[str] = None,1125firewall_ranges: Optional[List[str]] = None,1126admin_password: Optional[str] = None,1127expires_at: Optional[str] = None,1128allow_all_traffic: Optional[bool] = None,1129update_window: Optional[Dict[str, int]] = None,1130) -> None:1131"""1132Update the workspace group definition.11331134Parameters1135----------1136name : str, optional1137Name of the workspace group1138firewall_ranges : list[str], optional1139List of allowed CIDR ranges. An empty list indicates that all1140inbound requests are allowed.1141admin_password : str, optional1142Admin password for the workspace group. If no password is supplied,1143a password will be generated and retured in the response.1144expires_at : str, optional1145The timestamp of when the workspace group will expire.1146If the expiration time is not specified,1147the workspace group will have no expiration time.1148At expiration, the workspace group is terminated and all the data is lost.1149Expiration time can be specified as a timestamp or duration.1150Example: "2021-01-02T15:04:05Z07:00", "2021-01-02", "3h30m"1151allow_all_traffic : bool, optional1152Allow all traffic to the workspace group1153update_window : Dict[str, int], optional1154Specify the day and hour of an update window: dict(day=0-6, hour=0-23)11551156"""1157if self._manager is None:1158raise ManagementError(1159msg='No workspace manager is associated with this object.',1160)1161data = {1162k: v for k, v in dict(1163name=name,1164firewallRanges=firewall_ranges,1165adminPassword=admin_password,1166expiresAt=expires_at,1167allowAllTraffic=allow_all_traffic,1168updateWindow=snake_to_camel_dict(update_window),1169).items() if v is not None1170}1171self._manager._patch(f'workspaceGroups/{self.id}', json=data)1172self.refresh()11731174def terminate(1175self, force: bool = False,1176wait_on_terminated: bool = False,1177wait_interval: int = 10,1178wait_timeout: int = 600,1179) -> None:1180"""1181Terminate the workspace group.11821183Parameters1184----------1185force : bool, optional1186Terminate a workspace group even if it has active workspaces1187wait_on_terminated : bool, optional1188Wait for the workspace group to go into 'Terminated' mode before returning1189wait_interval : int, optional1190Number of seconds between each server check1191wait_timeout : int, optional1192Total number of seconds to check server before giving up11931194Raises1195------1196ManagementError1197If timeout is reached11981199"""1200if self._manager is None:1201raise ManagementError(1202msg='No workspace manager is associated with this object.',1203)1204self._manager._delete(f'workspaceGroups/{self.id}', params=dict(force=force))1205if wait_on_terminated:1206while True:1207self.refresh()1208if self.terminated_at is not None:1209break1210if wait_timeout <= 0:1211raise ManagementError(1212msg='Exceeded waiting time for WorkspaceGroup to terminate',1213)1214time.sleep(wait_interval)1215wait_timeout -= wait_interval12161217def create_workspace(1218self,1219name: str,1220size: Optional[str] = None,1221auto_suspend: Optional[Dict[str, Any]] = None,1222cache_config: Optional[int] = None,1223enable_kai: Optional[bool] = None,1224wait_on_active: bool = False,1225wait_interval: int = 10,1226wait_timeout: int = 600,1227) -> Workspace:1228"""1229Create a new workspace.12301231Parameters1232----------1233name : str1234Name of the workspace1235size : str, optional1236Workspace size in workspace size notation (S-00, S-1, etc.)1237auto_suspend : Dict[str, Any], optional1238Auto suspend settings for the workspace. If this field is not1239provided, no settings will be enabled.1240cache_config : int, optional1241Specifies the multiplier for the persistent cache associated1242with the workspace. If specified, it enables the cache configuration1243multiplier. It can have one of the following values: 1, 2, or 4.1244enable_kai : bool, optional1245Whether to create a SingleStore Kai-enabled workspace1246wait_on_active : bool, optional1247Wait for the workspace to be active before returning1248wait_timeout : int, optional1249Maximum number of seconds to wait before raising an exception1250if wait=True1251wait_interval : int, optional1252Number of seconds between each polling interval12531254Returns1255-------1256:class:`Workspace`12571258"""1259if self._manager is None:1260raise ManagementError(1261msg='No workspace manager is associated with this object.',1262)12631264out = self._manager.create_workspace(1265name=name,1266workspace_group=self,1267size=size,1268auto_suspend=snake_to_camel_dict(auto_suspend),1269cache_config=cache_config,1270enable_kai=enable_kai,1271wait_on_active=wait_on_active,1272wait_interval=wait_interval,1273wait_timeout=wait_timeout,1274)12751276return out12771278@property1279def workspaces(self) -> NamedList[Workspace]:1280"""Return a list of available workspaces."""1281if self._manager is None:1282raise ManagementError(1283msg='No workspace manager is associated with this object.',1284)1285res = self._manager._get('workspaces', params=dict(workspaceGroupID=self.id))1286return NamedList(1287[Workspace.from_dict(item, self._manager) for item in res.json()],1288)128912901291class StarterWorkspace(object):1292"""1293SingleStoreDB starter workspace definition.12941295This object is not instantiated directly. It is used in the results1296of API calls on the :class:`WorkspaceManager`. Existing starter workspaces are1297accessed by either :attr:`WorkspaceManager.starter_workspaces` or by calling1298:meth:`WorkspaceManager.get_starter_workspace`.12991300See Also1301--------1302:meth:`WorkspaceManager.get_starter_workspace`1303:meth:`WorkspaceManager.create_starter_workspace`1304:meth:`WorkspaceManager.terminate_starter_workspace`1305:meth:`WorkspaceManager.create_starter_workspace_user`1306:attr:`WorkspaceManager.starter_workspaces`13071308"""13091310name: str1311id: str1312database_name: str1313endpoint: Optional[str]13141315def __init__(1316self,1317name: str,1318id: str,1319database_name: str,1320endpoint: Optional[str] = None,1321):1322#: Name of the starter workspace1323self.name = name13241325#: Unique ID of the starter workspace1326self.id = id13271328#: Name of the database associated with the starter workspace1329self.database_name = database_name13301331#: Endpoint to connect to the starter workspace. The endpoint is in the form1332#: of ``hostname:port``1333self.endpoint = endpoint13341335self._manager: Optional[WorkspaceManager] = None13361337def __str__(self) -> str:1338"""Return string representation."""1339return vars_to_str(self)13401341def __repr__(self) -> str:1342"""Return string representation."""1343return str(self)13441345@classmethod1346def from_dict(1347cls, obj: Dict[str, Any], manager: 'WorkspaceManager',1348) -> 'StarterWorkspace':1349"""1350Construct a StarterWorkspace from a dictionary of values.13511352Parameters1353----------1354obj : dict1355Dictionary of values1356manager : WorkspaceManager, optional1357The WorkspaceManager the StarterWorkspace belongs to13581359Returns1360-------1361:class:`StarterWorkspace`13621363"""1364out = cls(1365name=obj['name'],1366id=obj['virtualWorkspaceID'],1367database_name=obj['databaseName'],1368endpoint=obj.get('endpoint'),1369)1370out._manager = manager1371return out13721373def connect(self, **kwargs: Any) -> connection.Connection:1374"""1375Create a connection to the database server for this starter workspace.13761377Parameters1378----------1379**kwargs : keyword-arguments, optional1380Parameters to the SingleStoreDB `connect` function except host1381and port which are supplied by the starter workspace object13821383Returns1384-------1385:class:`Connection`13861387"""1388if not self.endpoint:1389raise ManagementError(1390msg='An endpoint has not been set in this '1391'starter workspace configuration',1392)13931394kwargs['host'] = self.endpoint1395kwargs['database'] = self.database_name13961397return connection.connect(**kwargs)13981399def terminate(self) -> None:1400"""Terminate the starter workspace."""1401if self._manager is None:1402raise ManagementError(1403msg='No workspace manager is associated with this object.',1404)1405self._manager._delete(f'sharedtier/virtualWorkspaces/{self.id}')14061407def refresh(self) -> StarterWorkspace:1408"""Update the object to the current state."""1409if self._manager is None:1410raise ManagementError(1411msg='No workspace manager is associated with this object.',1412)1413new_obj = self._manager.get_starter_workspace(self.id)1414for name, value in vars(new_obj).items():1415if isinstance(value, Mapping):1416setattr(self, name, snake_to_camel_dict(value))1417else:1418setattr(self, name, value)1419return self14201421@property1422def organization(self) -> Organization:1423if self._manager is None:1424raise ManagementError(1425msg='No workspace manager is associated with this object.',1426)1427return self._manager.organization14281429@property1430def stage(self) -> Stage:1431"""Stage manager."""1432if self._manager is None:1433raise ManagementError(1434msg='No workspace manager is associated with this object.',1435)1436return Stage(self.id, self._manager)14371438stages = stage14391440@property1441def starter_workspaces(self) -> NamedList['StarterWorkspace']:1442"""Return a list of available starter workspaces."""1443if self._manager is None:1444raise ManagementError(1445msg='No workspace manager is associated with this object.',1446)1447res = self._manager._get('sharedtier/virtualWorkspaces')1448return NamedList(1449[StarterWorkspace.from_dict(item, self._manager) for item in res.json()],1450)14511452def create_user(1453self,1454username: str,1455password: Optional[str] = None,1456) -> Dict[str, str]:1457"""1458Create a new user for this starter workspace.14591460Parameters1461----------1462username : str1463The starter workspace user name to connect the new user to the database1464password : str, optional1465Password for the new user. If not provided, a password will be1466auto-generated by the system.14671468Returns1469-------1470Dict[str, str]1471Dictionary containing 'userID' and 'password' of the created user14721473Raises1474------1475ManagementError1476If no workspace manager is associated with this object.1477"""1478if self._manager is None:1479raise ManagementError(1480msg='No workspace manager is associated with this object.',1481)14821483payload = {1484'userName': username,1485}1486if password is not None:1487payload['password'] = password14881489res = self._manager._post(1490f'sharedtier/virtualWorkspaces/{self.id}/users',1491json=payload,1492)14931494response_data = res.json()1495user_id = response_data.get('userID')1496if not user_id:1497raise ManagementError(msg='No userID returned from API')14981499# Return the password provided by user or generated by API1500returned_password = password if password is not None \1501else response_data.get('password')1502if not returned_password:1503raise ManagementError(msg='No password available from API response')15041505return {1506'user_id': user_id,1507'password': returned_password,1508}150915101511class Billing(object):1512"""Billing information."""15131514COMPUTE_CREDIT = 'compute_credit'1515STORAGE_AVG_BYTE = 'storage_avg_byte'15161517HOUR = 'hour'1518DAY = 'day'1519MONTH = 'month'15201521def __init__(self, manager: Manager):1522self._manager = manager15231524def usage(1525self,1526start_time: datetime.datetime,1527end_time: datetime.datetime,1528metric: Optional[str] = None,1529aggregate_by: Optional[str] = None,1530) -> List[BillingUsageItem]:1531"""1532Get usage information.15331534Parameters1535----------1536start_time : datetime.datetime1537Start time for usage interval1538end_time : datetime.datetime1539End time for usage interval1540metric : str, optional1541Possible metrics are ``mgr.billing.COMPUTE_CREDIT`` and1542``mgr.billing.STORAGE_AVG_BYTE`` (default is all)1543aggregate_by : str, optional1544Aggregate type used to group usage: ``mgr.billing.HOUR``,1545``mgr.billing.DAY``, or ``mgr.billing.MONTH``15461547Returns1548-------1549List[BillingUsage]15501551"""1552res = self._manager._get(1553'billing/usage',1554params={1555k: v for k, v in dict(1556metric=snake_to_camel(metric),1557startTime=from_datetime(start_time),1558endTime=from_datetime(end_time),1559aggregate_by=aggregate_by.lower() if aggregate_by else None,1560).items() if v is not None1561},1562)1563return [1564BillingUsageItem.from_dict(x, self._manager)1565for x in res.json()['billingUsage']1566]156715681569class Organizations(object):1570"""Organizations."""15711572def __init__(self, manager: Manager):1573self._manager = manager15741575@property1576def current(self) -> Organization:1577"""Get current organization."""1578res = self._manager._get('organizations/current').json()1579return Organization.from_dict(res, self._manager)158015811582class WorkspaceManager(Manager):1583"""1584SingleStoreDB workspace manager.15851586This class should be instantiated using :func:`singlestoredb.manage_workspaces`.15871588Parameters1589----------1590access_token : str, optional1591The API key or other access token for the workspace management API1592version : str, optional1593Version of the API to use1594base_url : str, optional1595Base URL of the workspace management API15961597See Also1598--------1599:func:`singlestoredb.manage_workspaces`16001601"""16021603#: Workspace management API version if none is specified.1604default_version = config.get_option('management.version') or 'v1'16051606#: Base URL if none is specified.1607default_base_url = config.get_option('management.base_url') \1608or 'https://api.singlestore.com'16091610#: Object type1611obj_type = 'workspace'16121613@property1614def workspace_groups(self) -> NamedList[WorkspaceGroup]:1615"""Return a list of available workspace groups."""1616res = self._get('workspaceGroups')1617return NamedList([WorkspaceGroup.from_dict(item, self) for item in res.json()])16181619@property1620def starter_workspaces(self) -> NamedList[StarterWorkspace]:1621"""Return a list of available starter workspaces."""1622res = self._get('sharedtier/virtualWorkspaces')1623return NamedList([StarterWorkspace.from_dict(item, self) for item in res.json()])16241625@property1626def organizations(self) -> Organizations:1627"""Return the organizations."""1628return Organizations(self)16291630@property1631def organization(self) -> Organization:1632""" Return the current organization."""1633return self.organizations.current16341635@property1636def billing(self) -> Billing:1637"""Return the current billing information."""1638return Billing(self)16391640@ttl_property(datetime.timedelta(hours=1))1641def regions(self) -> NamedList[Region]:1642"""Return a list of available regions."""1643res = self._get('regions')1644return NamedList([Region.from_dict(item, self) for item in res.json()])16451646@ttl_property(datetime.timedelta(hours=1))1647def shared_tier_regions(self) -> NamedList[Region]:1648"""Return a list of regions that support shared tier workspaces."""1649res = self._get('regions/sharedtier')1650return NamedList(1651[Region.from_dict(item, self) for item in res.json()],1652)16531654def create_workspace_group(1655self,1656name: str,1657region: Union[str, Region],1658firewall_ranges: List[str],1659admin_password: Optional[str] = None,1660backup_bucket_kms_key_id: Optional[str] = None,1661data_bucket_kms_key_id: Optional[str] = None,1662expires_at: Optional[str] = None,1663smart_dr: Optional[bool] = None,1664allow_all_traffic: Optional[bool] = None,1665update_window: Optional[Dict[str, int]] = None,1666) -> WorkspaceGroup:1667"""1668Create a new workspace group.16691670Parameters1671----------1672name : str1673Name of the workspace group1674region : str or Region1675ID of the region where the workspace group should be created1676firewall_ranges : list[str]1677List of allowed CIDR ranges. An empty list indicates that all1678inbound requests are allowed.1679admin_password : str, optional1680Admin password for the workspace group. If no password is supplied,1681a password will be generated and retured in the response.1682backup_bucket_kms_key_id : str, optional1683Specifies the KMS key ID associated with the backup bucket.1684If specified, enables Customer-Managed Encryption Keys (CMEK)1685encryption for the backup bucket of the workspace group.1686This feature is only supported in workspace groups deployed in AWS.1687data_bucket_kms_key_id : str, optional1688Specifies the KMS key ID associated with the data bucket.1689If specified, enables Customer-Managed Encryption Keys (CMEK)1690encryption for the data bucket and Amazon Elastic Block Store1691(EBS) volumes of the workspace group. This feature is only supported1692in workspace groups deployed in AWS.1693expires_at : str, optional1694The timestamp of when the workspace group will expire.1695If the expiration time is not specified,1696the workspace group will have no expiration time.1697At expiration, the workspace group is terminated and all the data is lost.1698Expiration time can be specified as a timestamp or duration.1699Example: "2021-01-02T15:04:05Z07:00", "2021-01-02", "3h30m"1700smart_dr : bool, optional1701Enables Smart Disaster Recovery (SmartDR) for the workspace group.1702SmartDR is a disaster recovery solution that ensures seamless and1703continuous replication of data from the primary region to a secondary region1704allow_all_traffic : bool, optional1705Allow all traffic to the workspace group1706update_window : Dict[str, int], optional1707Specify the day and hour of an update window: dict(day=0-6, hour=0-23)17081709Returns1710-------1711:class:`WorkspaceGroup`17121713"""1714if isinstance(region, Region) and region.id:1715region = region.id1716res = self._post(1717'workspaceGroups', json=dict(1718name=name, regionID=region,1719adminPassword=admin_password,1720backupBucketKMSKeyID=backup_bucket_kms_key_id,1721dataBucketKMSKeyID=data_bucket_kms_key_id,1722firewallRanges=firewall_ranges or [],1723expiresAt=expires_at,1724smartDR=smart_dr,1725allowAllTraffic=allow_all_traffic,1726updateWindow=snake_to_camel_dict(update_window),1727),1728)1729return self.get_workspace_group(res.json()['workspaceGroupID'])17301731def create_workspace(1732self,1733name: str,1734workspace_group: Union[str, WorkspaceGroup],1735size: Optional[str] = None,1736auto_suspend: Optional[Dict[str, Any]] = None,1737cache_config: Optional[int] = None,1738enable_kai: Optional[bool] = None,1739wait_on_active: bool = False,1740wait_interval: int = 10,1741wait_timeout: int = 600,1742) -> Workspace:1743"""1744Create a new workspace.17451746Parameters1747----------1748name : str1749Name of the workspace1750workspace_group : str or WorkspaceGroup1751The workspace ID of the workspace1752size : str, optional1753Workspace size in workspace size notation (S-00, S-1, etc.)1754auto_suspend : Dict[str, Any], optional1755Auto suspend settings for the workspace. If this field is not1756provided, no settings will be enabled.1757cache_config : int, optional1758Specifies the multiplier for the persistent cache associated1759with the workspace. If specified, it enables the cache configuration1760multiplier. It can have one of the following values: 1, 2, or 4.1761enable_kai : bool, optional1762Whether to create a SingleStore Kai-enabled workspace1763wait_on_active : bool, optional1764Wait for the workspace to be active before returning1765wait_timeout : int, optional1766Maximum number of seconds to wait before raising an exception1767if wait=True1768wait_interval : int, optional1769Number of seconds between each polling interval17701771Returns1772-------1773:class:`Workspace`17741775"""1776if isinstance(workspace_group, WorkspaceGroup):1777workspace_group = workspace_group.id1778res = self._post(1779'workspaces', json=dict(1780name=name,1781workspaceGroupID=workspace_group,1782size=size,1783autoSuspend=snake_to_camel_dict(auto_suspend),1784cacheConfig=cache_config,1785enableKai=enable_kai,1786),1787)1788out = self.get_workspace(res.json()['workspaceID'])1789if wait_on_active:1790out = self._wait_on_state(1791out,1792'Active',1793interval=wait_interval,1794timeout=wait_timeout,1795)1796return out17971798def get_workspace_group(self, id: str) -> WorkspaceGroup:1799"""1800Retrieve a workspace group definition.18011802Parameters1803----------1804id : str1805ID of the workspace group18061807Returns1808-------1809:class:`WorkspaceGroup`18101811"""1812res = self._get(f'workspaceGroups/{id}')1813return WorkspaceGroup.from_dict(res.json(), manager=self)18141815def get_workspace(self, id: str) -> Workspace:1816"""1817Retrieve a workspace definition.18181819Parameters1820----------1821id : str1822ID of the workspace18231824Returns1825-------1826:class:`Workspace`18271828"""1829res = self._get(f'workspaces/{id}')1830return Workspace.from_dict(res.json(), manager=self)18311832def get_starter_workspace(self, id: str) -> StarterWorkspace:1833"""1834Retrieve a starter workspace definition.18351836Parameters1837----------1838id : str1839ID of the starter workspace18401841Returns1842-------1843:class:`StarterWorkspace`18441845"""1846res = self._get(f'sharedtier/virtualWorkspaces/{id}')1847return StarterWorkspace.from_dict(res.json(), manager=self)18481849def create_starter_workspace(1850self,1851name: str,1852database_name: str,1853provider: str,1854region_name: str,1855) -> 'StarterWorkspace':1856"""1857Create a new starter (shared tier) workspace.18581859Parameters1860----------1861name : str1862Name of the starter workspace1863database_name : str1864Name of the database for the starter workspace1865provider : str1866Cloud provider for the starter workspace (e.g., 'aws', 'gcp', 'azure')1867region_name : str1868Cloud provider region for the starter workspace (e.g., 'us-east-1')18691870Returns1871-------1872:class:`StarterWorkspace`1873"""18741875payload = {1876'name': name,1877'databaseName': database_name,1878'provider': provider,1879'regionName': region_name,1880}18811882res = self._post('sharedtier/virtualWorkspaces', json=payload)1883virtual_workspace_id = res.json().get('virtualWorkspaceID')1884if not virtual_workspace_id:1885raise ManagementError(msg='No virtualWorkspaceID returned from API')18861887res = self._get(f'sharedtier/virtualWorkspaces/{virtual_workspace_id}')1888return StarterWorkspace.from_dict(res.json(), self)188918901891def manage_workspaces(1892access_token: Optional[str] = None,1893version: Optional[str] = None,1894base_url: Optional[str] = None,1895*,1896organization_id: Optional[str] = None,1897) -> WorkspaceManager:1898"""1899Retrieve a SingleStoreDB workspace manager.19001901Parameters1902----------1903access_token : str, optional1904The API key or other access token for the workspace management API1905version : str, optional1906Version of the API to use1907base_url : str, optional1908Base URL of the workspace management API1909organization_id : str, optional1910ID of organization, if using a JWT for authentication19111912Returns1913-------1914:class:`WorkspaceManager`19151916"""1917return WorkspaceManager(1918access_token=access_token, base_url=base_url,1919version=version, organization_id=organization_id,1920)192119221923