From 60ad5ec70bc6c74b5628b0987cecda9c0580a6be Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Wed, 25 Jun 2025 00:31:38 +0100 Subject: [PATCH 1/4] free tier endpoints --- singlestoredb/management/workspace.py | 248 ++++++++++++++++++++++++- singlestoredb/tests/test_management.py | 115 ++++++++++++ 2 files changed, 362 insertions(+), 1 deletion(-) diff --git a/singlestoredb/management/workspace.py b/singlestoredb/management/workspace.py index 8d61030d1..12c7ef894 100644 --- a/singlestoredb/management/workspace.py +++ b/singlestoredb/management/workspace.py @@ -1301,17 +1301,24 @@ class StarterWorkspace(object): See Also -------- :meth:`WorkspaceManager.get_starter_workspace` + :meth:`WorkspaceManager.create_starter_workspace` + :meth:`WorkspaceManager.terminate_starter_workspace` + :meth:`WorkspaceManager.create_starter_workspace_user` :attr:`WorkspaceManager.starter_workspaces` """ name: str id: str + database_name: str + endpoint: Optional[str] def __init__( self, name: str, id: str, + database_name: str, + endpoint: Optional[str] = None, ): #: Name of the starter workspace self.name = name @@ -1319,6 +1326,13 @@ def __init__( #: Unique ID of the starter workspace self.id = id + #: Name of the database associated with the starter workspace + self.database_name = database_name + + #: Endpoint to connect to the starter workspace. The endpoint is in the form + #: of ``hostname:port`` + self.endpoint = endpoint + self._manager: Optional[WorkspaceManager] = None def __str__(self) -> str: @@ -1351,10 +1365,56 @@ def from_dict( out = cls( name=obj['name'], id=obj['virtualWorkspaceID'], + database_name=obj['databaseName'], + endpoint=obj.get('endpoint'), ) out._manager = manager return out + def connect(self, **kwargs: Any) -> connection.Connection: + """ + Create a connection to the database server for this starter workspace. + + Parameters + ---------- + **kwargs : keyword-arguments, optional + Parameters to the SingleStoreDB `connect` function except host + and port which are supplied by the starter workspace object + + Returns + ------- + :class:`Connection` + + """ + if not self.endpoint: + raise ManagementError( + msg='An endpoint has not been set in this ' + 'starter workspace configuration', + ) + # Parse endpoint as host:port + if ':' in self.endpoint: + host, port = self.endpoint.split(':', 1) + kwargs['host'] = host + kwargs['port'] = int(port) + else: + kwargs['host'] = self.endpoint + return connection.connect(**kwargs) + + def terminate(self) -> None: + """ + Terminate the starter workspace. + + Raises + ------ + ManagementError + If no workspace manager is associated with this object. + """ + if self._manager is None: + raise ManagementError( + msg='No workspace manager is associated with this object.', + ) + self._manager.terminate_starter_workspace(self.id) + @property def organization(self) -> Organization: if self._manager is None: @@ -1375,7 +1435,7 @@ def stage(self) -> Stage: stages = stage @property - def starter_workspaces(self) -> NamedList[StarterWorkspace]: + def starter_workspaces(self) -> NamedList['StarterWorkspace']: """Return a list of available starter workspaces.""" if self._manager is None: raise ManagementError( @@ -1386,6 +1446,67 @@ def starter_workspaces(self) -> NamedList[StarterWorkspace]: [StarterWorkspace.from_dict(item, self._manager) for item in res.json()], ) + def create_user( + self, + user_name: str, + password: Optional[str] = None, + ) -> Dict[str, str]: + """ + Create a new user for this starter workspace. + + Parameters + ---------- + user_name : str + The starter workspace user name to connect the new user to the database + password : str, optional + Password for the new user. If not provided, a password will be + auto-generated by the system. + + Returns + ------- + Dict[str, str] + Dictionary containing 'userID' and 'password' of the created user + + Raises + ------ + ManagementError + If no workspace manager is associated with this object. + """ + if self._manager is None: + raise ManagementError( + msg='No workspace manager is associated with this object.', + ) + + return self._manager.create_starter_workspace_user(self.id, user_name, password) + + @classmethod + def create_starter_workspace( + cls, + manager: 'WorkspaceManager', + name: str, + database_name: str, + workspace_group: dict[str, str], + ) -> 'StarterWorkspace': + """ + Create a new starter (shared tier) workspace. + + Parameters + ---------- + manager : WorkspaceManager + The WorkspaceManager instance to use for the API call + name : str + Name of the starter workspace + database_name : str + Name of the database for the starter workspace + workspace_group : dict[str, str] + Workspace group input (dict with keys: 'name', 'cell_id') + + Returns + ------- + :class:`StarterWorkspace` + """ + return manager.create_starter_workspace(name, database_name, workspace_group) + class Billing(object): """Billing information.""" @@ -1717,6 +1838,131 @@ def get_starter_workspace(self, id: str) -> StarterWorkspace: res = self._get(f'sharedtier/virtualWorkspaces/{id}') return StarterWorkspace.from_dict(res.json(), manager=self) + def create_starter_workspace( + self, + name: str, + database_name: str, + workspace_group: dict[str, str], + ) -> 'StarterWorkspace': + """ + Create a new starter (shared tier) workspace. + + Parameters + ---------- + name : str + Name of the starter workspace + database_name : str + Name of the database for the starter workspace + workspace_group : dict[str, str] + Workspace group input (dict with keys: 'name', 'cell_id') + + Returns + ------- + :class:`StarterWorkspace` + """ + if not workspace_group or not isinstance(workspace_group, dict): + raise ValueError( + 'workspace_group must be a dict with keys: ' + "'name', 'cell_id'", + ) + if set(workspace_group.keys()) != {'name', 'cell_id'}: + raise ValueError("workspace_group must contain only 'name' and 'cell_id'") + + payload = { + 'name': name, + 'databaseName': database_name, + 'workspaceGroup': { + 'name': workspace_group['name'], + 'cellID': workspace_group['cell_id'], + }, + } + + res = self._post('sharedtier/virtualWorkspaces', json=payload) + virtual_workspace_id = res.json().get('virtualWorkspaceID') + if not virtual_workspace_id: + raise ManagementError(msg='No virtualWorkspaceID returned from API') + + res = self._get(f'sharedtier/virtualWorkspaces/{virtual_workspace_id}') + return StarterWorkspace.from_dict(res.json(), self) + + def terminate_starter_workspace( + self, + id: str, + ) -> None: + """ + Terminate a starter (shared tier) workspace. + + Parameters + ---------- + id : str + ID of the starter workspace + wait_on_terminated : bool, optional + Wait for the starter workspace to go into 'Terminated' mode before returning + wait_interval : int, optional + Number of seconds between each server check + wait_timeout : int, optional + Total number of seconds to check server before giving up + + Raises + ------ + ManagementError + If timeout is reached + + """ + self._delete(f'sharedtier/virtualWorkspaces/{id}') + + def create_starter_workspace_user( + self, + starter_workspace_id: str, + username: str, + password: Optional[str] = None, + ) -> Dict[str, str]: + """ + Create a new user for a starter workspace. + + Parameters + ---------- + starter_workspace_id : str + ID of the starter workspace + user_name : str + The starter workspace user name to connect the new user to the database + password : str, optional + Password for the new user. If not provided, a password will be + auto-generated by the system. + + Returns + ------- + Dict[str, str] + Dictionary containing 'userID' and 'password' of the created user + + """ + payload = { + 'userName': username, + } + if password is not None: + payload['password'] = password + + res = self._post( + f'sharedtier/virtualWorkspaces/{starter_workspace_id}/users', + json=payload, + ) + + response_data = res.json() + user_id = response_data.get('userID') + if not user_id: + raise ManagementError(msg='No userID returned from API') + + # Return the password provided by user or generated by API + returned_password = password if password is not None \ + else response_data.get('password') + if not returned_password: + raise ManagementError(msg='No password available from API response') + + return { + 'userID': user_id, + 'password': returned_password, + } + def manage_workspaces( access_token: Optional[str] = None, diff --git a/singlestoredb/tests/test_management.py b/singlestoredb/tests/test_management.py index 222c80001..f530c93ec 100755 --- a/singlestoredb/tests/test_management.py +++ b/singlestoredb/tests/test_management.py @@ -363,6 +363,121 @@ def test_connect(self): assert 'endpoint' in cm.exception.msg, cm.exception.msg +class TestStarterWorkspace(unittest.TestCase): + + manager = None + starter_workspace = None + starter_workspace_user = { + 'username': 'starter_user', + 'password': None, + } + + @property + def starter_username(self): + """Return the username for the starter workspace user.""" + return self.starter_workspace_user['username'] + + @property + def password(self): + """Return the password for the starter workspace user.""" + return self.starter_workspace_user['password'] + + @classmethod + def setUpClass(cls): + cls.manager = s2.manage_workspaces() + + us_regions = [x for x in cls.manager.regions if 'US' in x.name] + cls.password = secrets.token_urlsafe(20) + '-x&$' + + name = clean_name(secrets.token_urlsafe(20)[:20]) + + cls.starter_workspace = cls.manager.create_starter_workspace( + f'starter-ws-test-{name}', + database_name=f'starter_db_{name}', + workspace_group={ + 'name': f'starter-wg-test-{name}', + 'cell_id': random.choice(us_regions).id, + }, + ) + + cls.manager.create_starter_workspace_user( + starter_workspace_id=cls.starter_workspace.id, + username=cls.starter_username, + password=cls.password, + ) + + @classmethod + def tearDownClass(cls): + if cls.starter_workspace is not None: + cls.starter_workspace.terminate(force=True) + cls.manager = None + cls.password = None + + def test_str(self): + assert self.starter_workspace.name in str(self.starter_workspace.name) + + def test_repr(self): + assert repr(self.starter_workspace) == str(self.starter_workspace) + + def test_get_starter_workspace(self): + workspace = self.manager.get_starter_workspace(self.starter_workspace.id) + assert workspace.id == self.starter_workspace.id, workspace.id + + with self.assertRaises(s2.ManagementError) as cm: + workspace = self.manager.get_starter_workspace('bad id') + + assert 'UUID' in cm.exception.msg, cm.exception.msg + + def test_starter_workspaces(self): + workspaces = self.manager.starter_workspaces + ids = [x.id for x in workspaces] + names = [x.name for x in workspaces] + assert self.starter_workspace.id in ids + assert self.starter_workspace.name in names + + objs = {} + for item in workspaces: + objs[item.id] = item + objs[item.name] = item + + name = random.choice(names) + assert workspaces[name] == objs[name] + id = random.choice(ids) + assert workspaces[id] == objs[id] + + def test_no_manager(self): + workspace = self.manager.get_starter_workspace(self.starter_workspace.id) + workspace._manager = None + + with self.assertRaises(s2.ManagementError) as cm: + workspace.refresh() + + assert 'No workspace manager' in cm.exception.msg, cm.exception.msg + + with self.assertRaises(s2.ManagementError) as cm: + workspace.terminate() + + assert 'No workspace manager' in cm.exception.msg, cm.exception.msg + + def test_connect(self): + with self.starter_workspace.connect( + user=self.starter_username, + password=self.password, + ) as conn: + with conn.cursor() as cur: + cur.execute('show databases') + assert 'starter_db' in [x[0] for x in list(cur)] + + # Test missing endpoint + workspace = self.manager.get_starter_workspace(self.starter_workspace.id) + workspace.endpoint = None + + with self.assertRaises(s2.ManagementError) as cm: + workspace.connect(user='admin', password=self.password) + + assert 'endpoint' in cm.exception.msg, cm.exception.msg + + @pytest.mark.management class TestStage(unittest.TestCase): From c1f458c5bd382bd6b5936559b114fbe905200a63 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Wed, 25 Jun 2025 15:22:12 +0100 Subject: [PATCH 2/4] add management api key to starter workspace tests --- coverage-mysql.cov | Bin 0 -> 118784 bytes singlestoredb/tests/test_management.py | 1 + 2 files changed, 1 insertion(+) create mode 100644 coverage-mysql.cov diff --git a/coverage-mysql.cov b/coverage-mysql.cov new file mode 100644 index 0000000000000000000000000000000000000000..4d5f12c899f98039e358b0c8d8f198be1bfae655 GIT binary patch literal 118784 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 D>OzJO literal 0 HcmV?d00001 diff --git a/singlestoredb/tests/test_management.py b/singlestoredb/tests/test_management.py index f530c93ec..e5bf35b53 100755 --- a/singlestoredb/tests/test_management.py +++ b/singlestoredb/tests/test_management.py @@ -363,6 +363,7 @@ def test_connect(self): assert 'endpoint' in cm.exception.msg, cm.exception.msg +@pytest.mark.management class TestStarterWorkspace(unittest.TestCase): manager = None From e157e27583eea6d809e9af2e54ad6b29ad68884e Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Thu, 26 Jun 2025 01:36:25 +0100 Subject: [PATCH 3/4] remove name of workspacegroup from starter workspace creation --- singlestoredb/management/workspace.py | 11 +++++------ singlestoredb/tests/test_management.py | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/singlestoredb/management/workspace.py b/singlestoredb/management/workspace.py index 12c7ef894..dedbe170b 100644 --- a/singlestoredb/management/workspace.py +++ b/singlestoredb/management/workspace.py @@ -1499,7 +1499,7 @@ def create_starter_workspace( database_name : str Name of the database for the starter workspace workspace_group : dict[str, str] - Workspace group input (dict with keys: 'name', 'cell_id') + Workspace group input (dict with keys: 'cell_id') Returns ------- @@ -1854,7 +1854,7 @@ def create_starter_workspace( database_name : str Name of the database for the starter workspace workspace_group : dict[str, str] - Workspace group input (dict with keys: 'name', 'cell_id') + Workspace group input (dict with keys: 'cell_id') Returns ------- @@ -1863,16 +1863,15 @@ def create_starter_workspace( if not workspace_group or not isinstance(workspace_group, dict): raise ValueError( 'workspace_group must be a dict with keys: ' - "'name', 'cell_id'", + "'cell_id'", ) - if set(workspace_group.keys()) != {'name', 'cell_id'}: - raise ValueError("workspace_group must contain only 'name' and 'cell_id'") + if set(workspace_group.keys()) != {'cell_id'}: + raise ValueError("workspace_group must contain only 'cell_id'") payload = { 'name': name, 'databaseName': database_name, 'workspaceGroup': { - 'name': workspace_group['name'], 'cellID': workspace_group['cell_id'], }, } diff --git a/singlestoredb/tests/test_management.py b/singlestoredb/tests/test_management.py index e5bf35b53..7c51b34db 100755 --- a/singlestoredb/tests/test_management.py +++ b/singlestoredb/tests/test_management.py @@ -396,7 +396,6 @@ def setUpClass(cls): f'starter-ws-test-{name}', database_name=f'starter_db_{name}', workspace_group={ - 'name': f'starter-wg-test-{name}', 'cell_id': random.choice(us_regions).id, }, ) From c394d6b6280b5de47c782f8d2bb7eda71ba06efa Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Mon, 14 Jul 2025 15:12:19 +0100 Subject: [PATCH 4/4] replace userID with user_id on create_starter_workspace_user return dictionary --- singlestoredb/management/workspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/singlestoredb/management/workspace.py b/singlestoredb/management/workspace.py index dedbe170b..7b3c08cc2 100644 --- a/singlestoredb/management/workspace.py +++ b/singlestoredb/management/workspace.py @@ -1958,7 +1958,7 @@ def create_starter_workspace_user( raise ManagementError(msg='No password available from API response') return { - 'userID': user_id, + 'user_id': user_id, 'password': returned_password, }