Repository: mindsdb/minds_python_sdk

Issue:
Improve Minds name requirements
## Short description of current behavior
Improve the name requirements for minds

## Steps 🕵️‍♂️ 🕵️‍♀️
- Add name requirements.
- Mind name should start with a letter and contain only letters, numbers or underscore, with a maximum of 32 characters. Spaces are not allowed.

## 📢 Additional rewards 🏆
This issue brings 4️⃣ 0️⃣  points for SWAG and entry into the draw for a 👇 :

3️⃣ ❌ 💻 [Razer Blade 16 Gaming Laptop](https://www.razer.com/gaming-laptops/Razer-Blade-16/RZ09-0510TEN3-R3U1)
👕 🐻 [MindsDB Swag](https://store.covver.io/mindsdb/collections/swag-collection)
ℹ️ For more info check out https://mindsdb.com/hacktoberfest 👈

Hints from issue comments:
@Better-Boy: I want to work on this

Solution patch:
diff --git a/minds/datasources/datasources.py b/minds/datasources/datasources.py
index 688faa4..2e548e1 100644
--- a/minds/datasources/datasources.py
+++ b/minds/datasources/datasources.py
@@ -16,11 +16,12 @@ class DatabaseConfig(BaseModel):
 class Datasource(DatabaseConfig):
     ...
 
+
 class Datasources:
     def __init__(self, client):
         self.api = client.api
 
-    def create(self, ds_config: DatabaseConfig, replace=False):
+    def create(self, ds_config: DatabaseConfig, update=False):
         """
         Create new datasource and return it
 
@@ -30,19 +31,16 @@ def create(self, ds_config: DatabaseConfig, replace=False):
            - description: str, description of the database. Used by mind to know what data can be got from it.
            - connection_data: dict, optional, credentials to connect to database
            - tables: list of str, optional, list of allowed tables
+        :param update: if true - to update datasourse if exists, default is false
         :return: datasource object
         """
 
         name = ds_config.name
 
-        if replace:
-            try:
-                self.get(name)
-                self.drop(name, force=True)
-            except exc.ObjectNotFound:
-                ...
-
-        self.api.post('/datasources', data=ds_config.model_dump())
+        if update:
+            self.api.put('/datasources', data=ds_config.model_dump())
+        else:
+            self.api.post('/datasources', data=ds_config.model_dump())
         return self.get(name)
 
     def list(self) -> List[Datasource]:
diff --git a/minds/exceptions.py b/minds/exceptions.py
index 4dbd8b4..dd1d717 100644
--- a/minds/exceptions.py
+++ b/minds/exceptions.py
@@ -18,3 +18,6 @@ class Unauthorized(Exception):
 class UnknownError(Exception):
     ...
 
+
+class MindNameInvalid(Exception):
+    ...
\ No newline at end of file
diff --git a/minds/minds.py b/minds/minds.py
index 4aaf68e..02a6ee8 100644
--- a/minds/minds.py
+++ b/minds/minds.py
@@ -1,11 +1,9 @@
 from typing import List, Union, Iterable
 from urllib.parse import urlparse, urlunparse
-from datetime import datetime
 
 from openai import OpenAI
-
+import minds.utils as utils
 import minds.exceptions as exc
-
 from minds.datasources import Datasource, DatabaseConfig
 
 DEFAULT_PROMPT_TEMPLATE = 'Use your database tools to answer the user\'s question: {{question}}'
@@ -25,7 +23,7 @@ def __init__(
         self.api = client.api
         self.client = client
         self.project = 'mindsdb'
-
+        
         self.name = name
         self.model_name = model_name
         self.provider = provider
@@ -74,6 +72,9 @@ def update(
         :param parameters, dict: alter other parameters of the mind, optional
         """
         data = {}
+        
+        if name is not None:
+            utils.validate_mind_name(name)
 
         if datasources is not None:
             ds_names = []
@@ -216,7 +217,7 @@ def get(self, name: str) -> Mind:
         :param name: name of the mind
         :return: a mind object
         """
-
+        
         item = self.api.get(f'/projects/{self.project}/minds/{name}').json()
         return Mind(self.client, **item)
 
@@ -243,6 +244,7 @@ def create(
         datasources=None,
         parameters=None,
         replace=False,
+        update=False,
     ) -> Mind:
         """
         Create a new mind and return it
@@ -259,8 +261,12 @@ def create(
         :param datasources: list of datasources used by mind, optional
         :param parameters, dict: other parameters of the mind, optional
         :param replace: if true - to remove existing mind, default is false
+        :param update: if true - to update mind if exists, default is false
         :return: created mind
         """
+        
+        if name is not None:
+            utils.validate_mind_name(name)
 
         if replace:
             try:
@@ -284,7 +290,12 @@ def create(
         if 'prompt_template' not in parameters:
             parameters['prompt_template'] = DEFAULT_PROMPT_TEMPLATE
 
-        self.api.post(
+        if update:
+            method = self.api.put
+        else:
+            method = self.api.post
+
+        method(
             f'/projects/{self.project}/minds',
             data={
                 'name': name,
diff --git a/minds/rest_api.py b/minds/rest_api.py
index 5bc00d5..29b7012 100644
--- a/minds/rest_api.py
+++ b/minds/rest_api.py
@@ -57,6 +57,16 @@ def post(self, url, data):
         _raise_for_status(resp)
         return resp
 
+    def put(self, url, data):
+        resp = requests.put(
+            self.base_url + url,
+            headers=self._headers(),
+            json=data,
+        )
+
+        _raise_for_status(resp)
+        return resp
+
     def patch(self, url, data):
         resp = requests.patch(
             self.base_url + url,
diff --git a/minds/utils.py b/minds/utils.py
new file mode 100644
index 0000000..261ef68
--- /dev/null
+++ b/minds/utils.py
@@ -0,0 +1,26 @@
+import re
+import minds.exceptions as exc
+
+def validate_mind_name(mind_name):
+    """
+    Validate the Mind name.
+
+    A valid Mind name should:
+    - Start with a letter
+    - Contain only letters, numbers, or underscores
+    - Have a maximum length of 32 characters
+    - Not contain spaces
+
+    Parameters:
+    mind_name (str): The Mind name to validate.
+
+    Returns:
+    bool: True if valid, False otherwise.
+    """
+    # Regular expression pattern
+    pattern = r'^[A-Za-z][A-Za-z0-9_]{0,31}$'
+
+    # Check if the Mind name matches the pattern
+    if not re.match(pattern, mind_name):
+        raise exc.MindNameInvalid("Mind name should start with a letter and contain only letters, numbers or underscore, with a maximum of 32 characters. Spaces are not allowed.")
+    


Test patch:
diff --git a/tests/integration/test_base_flow.py b/tests/integration/test_base_flow.py
index 8356d77..7f37b6d 100644
--- a/tests/integration/test_base_flow.py
+++ b/tests/integration/test_base_flow.py
@@ -9,7 +9,7 @@
 
 from minds.datasources.examples import example_ds
 
-from minds.exceptions import ObjectNotFound
+from minds.exceptions import ObjectNotFound, MindNameInvalid
 
 
 def get_client():
@@ -37,7 +37,8 @@ def test_datasources():
 
     # create
     ds = client.datasources.create(example_ds)
-    ds = client.datasources.create(example_ds, replace=True)
+    assert ds.name == example_ds.name
+    ds = client.datasources.create(example_ds, update=True)
     assert ds.name == example_ds.name
 
     # get
@@ -57,6 +58,7 @@ def test_minds():
     ds_name = 'test_datasource_'
     ds_name2 = 'test_datasource2_'
     mind_name = 'int_test_mind_'
+    invalid_mind_name = 'mind-123'
     mind_name2 = 'int_test_mind2_'
     prompt1 = 'answer in german'
     prompt2 = 'answer in spanish'
@@ -79,6 +81,13 @@ def test_minds():
     ds2_cfg.tables = ['home_rentals']
 
     # create
+    with pytest.raises(MindNameInvalid):
+        mind = client.minds.create(
+            invalid_mind_name,
+            datasources=[ds],
+            provider='openai'
+        )
+    
     mind = client.minds.create(
         mind_name,
         datasources=[ds],
@@ -90,11 +99,20 @@ def test_minds():
         datasources=[ds.name, ds2_cfg],
         prompt_template=prompt1
     )
+    mind = client.minds.create(
+        mind_name,
+        update=True,
+        datasources=[ds.name, ds2_cfg],
+        prompt_template=prompt1
+    )
 
     # get
     mind = client.minds.get(mind_name)
     assert len(mind.datasources) == 2
     assert mind.prompt_template == prompt1
+    
+    with pytest.raises(MindNameInvalid):
+        client.minds.get(invalid_mind_name)
 
     # list
     mind_list = client.minds.list()
@@ -106,6 +124,14 @@ def test_minds():
         datasources=[ds.name],
         prompt_template=prompt2
     )
+    
+    with pytest.raises(MindNameInvalid):
+        mind.update(
+            name=invalid_mind_name,
+            datasources=[ds.name],
+            prompt_template=prompt2
+        )
+    
     with pytest.raises(ObjectNotFound):
         # this name not exists
         client.minds.get(mind_name)
@@ -153,3 +179,6 @@ def test_minds():
     client.minds.drop(mind_name2)
     client.datasources.drop(ds.name)
     client.datasources.drop(ds2_cfg.name)
+    
+    with pytest.raises(MindNameInvalid):
+        client.minds.drop(invalid_mind_name)
diff --git a/tests/unit/test_unit.py b/tests/unit/test_unit.py
index 6757a01..ee6819a 100644
--- a/tests/unit/test_unit.py
+++ b/tests/unit/test_unit.py
@@ -36,9 +36,10 @@ def _compare_ds(self, ds1, ds2):
         assert ds1.tables == ds2.tables
 
     @patch('requests.get')
+    @patch('requests.put')
     @patch('requests.post')
     @patch('requests.delete')
-    def test_create_datasources(self, mock_del, mock_post, mock_get):
+    def test_create_datasources(self, mock_del, mock_post, mock_put, mock_get):
         client = get_client()
         response_mock(mock_get, example_ds.model_dump())
 
@@ -53,12 +54,9 @@ def check_ds_created(ds, mock_post):
 
         check_ds_created(ds, mock_post)
 
-        # with replace
-        ds = client.datasources.create(example_ds, replace=True)
-        args, _ = mock_del.call_args
-        assert args[0].endswith(f'/api/datasources/{example_ds.name}')
-
-        check_ds_created(ds, mock_post)
+        # with update
+        ds = client.datasources.create(example_ds, update=True)
+        check_ds_created(ds, mock_put)
 
     @patch('requests.get')
     def test_get_datasource(self, mock_get):
@@ -115,9 +113,10 @@ def compare_mind(self, mind, mind_json):
         assert mind.parameters == mind_json['parameters']
 
     @patch('requests.get')
+    @patch('requests.put')
     @patch('requests.post')
     @patch('requests.delete')
-    def test_create(self, mock_del, mock_post, mock_get):
+    def test_create(self, mock_del, mock_post, mock_put, mock_get):
         client = get_client()
 
         mind_name = 'test_mind'
@@ -145,7 +144,7 @@ def check_mind_created(mind, mock_post, create_params):
 
         check_mind_created(mind, mock_post, create_params)
 
-        # with replace
+        # -- with replace --
         create_params = {
             'name': mind_name,
             'prompt_template': prompt_template,
@@ -159,6 +158,14 @@ def check_mind_created(mind, mock_post, create_params):
 
         check_mind_created(mind, mock_post, create_params)
 
+        # -- with update --
+        mock_del.reset_mock()
+        mind = client.minds.create(update=True, **create_params)
+        # is not deleted
+        assert not mock_del.called
+
+        check_mind_created(mind, mock_put, create_params)
+
     @patch('requests.get')
     @patch('requests.patch')
     def test_update(self, mock_patch, mock_get):