We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Cant get GUI if `storage_dir` is relative path in config
If specify in config.json storage_dir as relative path, then `GET /index.html` return 404. With absolute paths all work fine.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `mindsdb/utilities/config.py`
Content:
```
1 import os
2 import json
3 import hashlib
4 import datetime
5 
6 
7 default_config = {
8     "log": {
9         "level": {
10             "console": "ERROR",
11             "file": "WARNING"
12         }
13     },
14     "debug": False,
15     "integrations": {},
16     "api": {
17         "http": {
18             "host": "127.0.0.1",
19             "port": "47334"
20         },
21         "mysql": {
22             "host": "127.0.0.1",
23             "password": "",
24             "port": "47335",
25             "user": "mindsdb",
26             "database": "mindsdb",
27             "ssl": True
28         },
29         "mongodb": {
30             "host": "127.0.0.1",
31             "port": "47336",
32             "database": "mindsdb"
33         }
34     }
35 }
36 
37 
38 class Config(object):
39     current_version = '1.3'
40     _config = {}
41     paths = {
42         'root': '',
43         'datasources': '',
44         'predictors': '',
45         'static': '',
46         'tmp': '',
47         'log': '',
48         'obsolete': {
49             'predictors': '',
50             'datasources': ''
51         }
52     }
53     versions = {}
54 
55     def __init__(self, config_path):
56         self._config_path = None
57         self._config_hash = None
58         self._config = None
59         if isinstance(config_path, str):
60             self.config_path = config_path
61             self._read()
62             self._config_hash = self._gen_hash()
63 
64             storage_dir = self._config['storage_dir']
65             if os.path.isabs(storage_dir) is False:
66                 storage_dir = os.path.normpath(
67                     os.path.join(
68                         os.path.dirname(config_path),
69                         storage_dir
70                     )
71                 )
72             self.paths['root'] = storage_dir
73             self.paths['datasources'] = os.path.join(storage_dir, 'datasources')
74             self.paths['predictors'] = os.path.join(storage_dir, 'predictors')
75             self.paths['static'] = os.path.join(storage_dir, 'static')
76             self.paths['tmp'] = os.path.join(storage_dir, 'tmp')
77             self.paths['log'] = os.path.join(storage_dir, 'log')
78             self.paths['obsolete']['predictors'] = os.path.join(storage_dir, 'obsolete', 'predictors')
79             self.paths['obsolete']['datasources'] = os.path.join(storage_dir, 'obsolete', 'datasources')
80 
81             self._read_versions_file(os.path.join(self.paths['root'], 'versions.json'))
82         else:
83             raise TypeError('Argument must be string representing a file path <Later on to be switched to file path and/or database connection info>')
84 
85     def _read_versions_file(self, path):
86         if os.path.isfile(path):
87             with open(path, 'rt') as f:
88                 self.versions = json.loads(f.read())
89 
90     def _migrate(self):
91         def m1_0(config):
92             if 'default_clickhouse' in config['integrations'] and 'type' not in config['integrations']['default_clickhouse']:
93                 config['integrations']['default_clickhouse']['type'] = 'clickhouse'
94             if 'default_mariadb' in config['integrations'] and 'type' not in config['integrations']['default_mariadb']:
95                 config['integrations']['default_mariadb']['type'] = 'mariadb'
96             if 'datasources' in config['api']['mysql']:
97                 del config['api']['mysql']['datasources']
98             config['config_version'] = '1.1'
99             return config
100 
101         def m1_1(config):
102             import tempfile
103             import shutil
104             from pathlib import Path
105 
106             ds_storage_path = Path(config['interface']['datastore']['storage_dir'])
107             mdb_storage_path = Path(config['interface']['mindsdb_native']['storage_dir'])
108 
109             temp_dir_path = tempfile.mkdtemp()
110 
111             if ds_storage_path.is_dir():
112                 shutil.move(
113                     str(ds_storage_path),
114                     temp_dir_path
115                 )
116 
117             ds_storage_path.mkdir(mode=0o777, exist_ok=True, parents=True)
118 
119             if Path(temp_dir_path).joinpath('datastore').is_dir():
120                 shutil.move(
121                     str(Path(temp_dir_path).joinpath('datastore')),
122                     str(ds_storage_path.joinpath('datasources'))
123                 )
124             else:
125                 ds_storage_path.joinpath('datasources').mkdir(mode=0o777, exist_ok=True)
126 
127             if ds_storage_path == mdb_storage_path:
128                 shutil.move(
129                     str(Path(temp_dir_path)),
130                     str(ds_storage_path.joinpath('predictors'))
131                 )
132             elif mdb_storage_path.is_dir():
133                 shutil.move(
134                     str(mdb_storage_path),
135                     str(ds_storage_path.joinpath('predictors'))
136                 )
137             else:
138                 mdb_storage_path.joinpath('predictors').mkdir(mode=0o777, exist_ok=True)
139 
140             ds_storage_path.joinpath('tmp').mkdir(mode=0o777, exist_ok=True)
141             ds_storage_path.joinpath('static').mkdir(mode=0o777, exist_ok=True)
142 
143             if Path(temp_dir_path).is_dir():
144                 shutil.rmtree(temp_dir_path)
145 
146             config['storage_dir'] = str(ds_storage_path)
147             del config['interface']['datastore']['storage_dir']
148             del config['interface']['mindsdb_native']['storage_dir']
149             config['config_version'] = '1.2'
150             return config
151 
152         def m1_2(config):
153             ''' remove no longer needed fields
154             '''
155             try:
156                 del config['api']['mysql']['log']
157             except Exception:
158                 pass
159 
160             try:
161                 del config['interface']
162             except Exception:
163                 pass
164 
165             if 'pip_path' in config and config['pip_path'] is None:
166                 del config['pip_path']
167 
168             if 'python_interpreter' in config and config['python_interpreter'] is None:
169                 del config['python_interpreter']
170 
171             config['config_version'] = '1.3'
172             return config
173 
174         migrations = {
175             '1.0': m1_0,
176             '1.1': m1_1,
177             '1.2': m1_2
178         }
179 
180         current_version = self._parse_version(self._config['config_version'])
181         target_version = self._parse_version(self.current_version)
182         while current_version < target_version:
183             str_version = '.'.join([str(x) for x in current_version])
184             self._config = migrations[str_version](self._config)
185             current_version = self._parse_version(self._config['config_version'])
186 
187     def _validate(self):
188         integrations = self._config.get('integrations', {})
189         for key, value in integrations.items():
190             if not isinstance(value, dict):
191                 raise TypeError(f"Config error: integration '{key}' must be a json")
192             if 'type' not in integrations[key]:
193                 raise KeyError(f"Config error: for integration '{key}' key 'type' must be specified")
194 
195         storage_dir = self._config.get('storage_dir')
196         if storage_dir is None:
197             raise KeyError("'storage_dir' mandatory key in config")
198 
199     def _parse_version(self, version):
200         if isinstance(version, str):
201             version = [int(x) for x in version.split('.')]
202         elif isinstance(version, int):
203             version = [version]
204         if len(version) == 1:
205             version.append(0)
206         return version
207 
208     def _format(self):
209         ''' changing user input to formalised view
210         '''
211         for integration in self._config.get('integrations', {}).values():
212             password = integration.get('password')
213             password = '' if password is None else str(password)
214             integration['password'] = str(password)
215 
216         password = self._config['api']['mysql'].get('password')
217         password = '' if password is None else str(password)
218         self._config['api']['mysql']['password'] = str(password)
219 
220     def _merge_default_config(self):
221         def merge_key_recursive(target_dict, source_dict, key):
222             if key not in target_dict:
223                 target_dict[key] = source_dict[key]
224             elif isinstance(target_dict[key], dict) and isinstance(source_dict[key], dict):
225                 for k in source_dict[key]:
226                     merge_key_recursive(target_dict[key], source_dict[key], k)
227 
228         for key in default_config:
229             merge_key_recursive(self._config, default_config, key)
230 
231     def _read(self):
232         if isinstance(self.config_path, str) and os.path.isfile(self.config_path):
233             with open(self.config_path, 'r') as fp:
234                 self._config = json.load(fp)
235                 if self._parse_version(self._config['config_version']) < self._parse_version(self.current_version):
236                     self._migrate()
237                     self._save()
238                 self._validate()
239                 self._format()
240                 self._merge_default_config()
241         else:
242             raise TypeError('`self.config_path` must be a string representing a local file path to a json config')
243 
244     def _save(self):
245         with open(self.config_path, 'w') as fp:
246             json.dump(self._config, fp, indent=4, sort_keys=True)
247 
248     def _gen_hash(self):
249         with open(self.config_path, 'rb') as fp:
250             return hashlib.md5(fp.read()).hexdigest()
251 
252     def _set_updated(self, key):
253         # Only check this for dynamically generated keys, won't be needed once we switch to using a database here
254         if key in ['integrations']:
255             file_hash = self._gen_hash()
256             if file_hash != self._config_hash:
257                 self._read()
258                 self._config_hash = self._gen_hash()
259 
260     def __getitem__(self, key):
261         self._set_updated(key)
262         return self._config[key]
263 
264     def get(self, key, default=None):
265         self._set_updated(key)
266         return self._config.get(key, default)
267 
268     def get_all(self):
269         return self._config
270 
271     def set(self, key_chain, value, delete=False):
272         with open(self.config_path, 'r') as fp:
273             self._config = json.load(fp)
274 
275         c = self._config
276         for i, k in enumerate(key_chain):
277             if k in c and i + 1 < len(key_chain):
278                 c = c[k]
279             elif k not in c and i + 1 < len(key_chain):
280                 c[k] = {}
281                 c = c[k]
282             else:
283                 if delete:
284                     del c[k]
285                 else:
286                     c[k] = value
287         self._save()
288 
289     # Higher level interface
290     def add_db_integration(self, name, dict):
291         dict['date_last_update'] = str(datetime.datetime.now()).split('.')[0]
292         if 'database_name' not in dict:
293             dict['database_name'] = name
294         if 'enabled' not in dict:
295             dict['enabled'] = True
296 
297         self.set(['integrations', name], dict)
298 
299     def modify_db_integration(self, name, dict):
300         old_dict = self._config['integrations'][name]
301         for k in old_dict:
302             if k not in dict:
303                 dict[k] = old_dict[k]
304 
305         self.add_db_integration(name, dict)
306 
307     def remove_db_integration(self, name):
308         self.set(['integrations', name], None, True)
309
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/mindsdb/utilities/config.py b/mindsdb/utilities/config.py
--- a/mindsdb/utilities/config.py
+++ b/mindsdb/utilities/config.py
@@ -57,7 +57,7 @@
         self._config_hash = None
         self._config = None
         if isinstance(config_path, str):
-            self.config_path = config_path
+            self.config_path = os.path.abspath(config_path)
             self._read()
             self._config_hash = self._gen_hash()
 
@@ -65,7 +65,7 @@
             if os.path.isabs(storage_dir) is False:
                 storage_dir = os.path.normpath(
                     os.path.join(
-                        os.path.dirname(config_path),
+                        os.path.dirname(self.config_path),
                         storage_dir
                     )
                 )

{"golden_diff": "diff --git a/mindsdb/utilities/config.py b/mindsdb/utilities/config.py\n--- a/mindsdb/utilities/config.py\n+++ b/mindsdb/utilities/config.py\n@@ -57,7 +57,7 @@\n         self._config_hash = None\n         self._config = None\n         if isinstance(config_path, str):\n-            self.config_path = config_path\n+            self.config_path = os.path.abspath(config_path)\n             self._read()\n             self._config_hash = self._gen_hash()\n \n@@ -65,7 +65,7 @@\n             if os.path.isabs(storage_dir) is False:\n                 storage_dir = os.path.normpath(\n                     os.path.join(\n-                        os.path.dirname(config_path),\n+                        os.path.dirname(self.config_path),\n                         storage_dir\n                     )\n                 )\n", "issue": "Cant get GUI if `storage_dir` is relative path in config\nIf specify in config.json storage_dir as relative path, then `GET /index.html` return 404. With absolute paths all work fine.\n", "before_files": [{"content": "import os\nimport json\nimport hashlib\nimport datetime\n\n\ndefault_config = {\n    \"log\": {\n        \"level\": {\n            \"console\": \"ERROR\",\n            \"file\": \"WARNING\"\n        }\n    },\n    \"debug\": False,\n    \"integrations\": {},\n    \"api\": {\n        \"http\": {\n            \"host\": \"127.0.0.1\",\n            \"port\": \"47334\"\n        },\n        \"mysql\": {\n            \"host\": \"127.0.0.1\",\n            \"password\": \"\",\n            \"port\": \"47335\",\n            \"user\": \"mindsdb\",\n            \"database\": \"mindsdb\",\n            \"ssl\": True\n        },\n        \"mongodb\": {\n            \"host\": \"127.0.0.1\",\n            \"port\": \"47336\",\n            \"database\": \"mindsdb\"\n        }\n    }\n}\n\n\nclass Config(object):\n    current_version = '1.3'\n    _config = {}\n    paths = {\n        'root': '',\n        'datasources': '',\n        'predictors': '',\n        'static': '',\n        'tmp': '',\n        'log': '',\n        'obsolete': {\n            'predictors': '',\n            'datasources': ''\n        }\n    }\n    versions = {}\n\n    def __init__(self, config_path):\n        self._config_path = None\n        self._config_hash = None\n        self._config = None\n        if isinstance(config_path, str):\n            self.config_path = config_path\n            self._read()\n            self._config_hash = self._gen_hash()\n\n            storage_dir = self._config['storage_dir']\n            if os.path.isabs(storage_dir) is False:\n                storage_dir = os.path.normpath(\n                    os.path.join(\n                        os.path.dirname(config_path),\n                        storage_dir\n                    )\n                )\n            self.paths['root'] = storage_dir\n            self.paths['datasources'] = os.path.join(storage_dir, 'datasources')\n            self.paths['predictors'] = os.path.join(storage_dir, 'predictors')\n            self.paths['static'] = os.path.join(storage_dir, 'static')\n            self.paths['tmp'] = os.path.join(storage_dir, 'tmp')\n            self.paths['log'] = os.path.join(storage_dir, 'log')\n            self.paths['obsolete']['predictors'] = os.path.join(storage_dir, 'obsolete', 'predictors')\n            self.paths['obsolete']['datasources'] = os.path.join(storage_dir, 'obsolete', 'datasources')\n\n            self._read_versions_file(os.path.join(self.paths['root'], 'versions.json'))\n        else:\n            raise TypeError('Argument must be string representing a file path <Later on to be switched to file path and/or database connection info>')\n\n    def _read_versions_file(self, path):\n        if os.path.isfile(path):\n            with open(path, 'rt') as f:\n                self.versions = json.loads(f.read())\n\n    def _migrate(self):\n        def m1_0(config):\n            if 'default_clickhouse' in config['integrations'] and 'type' not in config['integrations']['default_clickhouse']:\n                config['integrations']['default_clickhouse']['type'] = 'clickhouse'\n            if 'default_mariadb' in config['integrations'] and 'type' not in config['integrations']['default_mariadb']:\n                config['integrations']['default_mariadb']['type'] = 'mariadb'\n            if 'datasources' in config['api']['mysql']:\n                del config['api']['mysql']['datasources']\n            config['config_version'] = '1.1'\n            return config\n\n        def m1_1(config):\n            import tempfile\n            import shutil\n            from pathlib import Path\n\n            ds_storage_path = Path(config['interface']['datastore']['storage_dir'])\n            mdb_storage_path = Path(config['interface']['mindsdb_native']['storage_dir'])\n\n            temp_dir_path = tempfile.mkdtemp()\n\n            if ds_storage_path.is_dir():\n                shutil.move(\n                    str(ds_storage_path),\n                    temp_dir_path\n                )\n\n            ds_storage_path.mkdir(mode=0o777, exist_ok=True, parents=True)\n\n            if Path(temp_dir_path).joinpath('datastore').is_dir():\n                shutil.move(\n                    str(Path(temp_dir_path).joinpath('datastore')),\n                    str(ds_storage_path.joinpath('datasources'))\n                )\n            else:\n                ds_storage_path.joinpath('datasources').mkdir(mode=0o777, exist_ok=True)\n\n            if ds_storage_path == mdb_storage_path:\n                shutil.move(\n                    str(Path(temp_dir_path)),\n                    str(ds_storage_path.joinpath('predictors'))\n                )\n            elif mdb_storage_path.is_dir():\n                shutil.move(\n                    str(mdb_storage_path),\n                    str(ds_storage_path.joinpath('predictors'))\n                )\n            else:\n                mdb_storage_path.joinpath('predictors').mkdir(mode=0o777, exist_ok=True)\n\n            ds_storage_path.joinpath('tmp').mkdir(mode=0o777, exist_ok=True)\n            ds_storage_path.joinpath('static').mkdir(mode=0o777, exist_ok=True)\n\n            if Path(temp_dir_path).is_dir():\n                shutil.rmtree(temp_dir_path)\n\n            config['storage_dir'] = str(ds_storage_path)\n            del config['interface']['datastore']['storage_dir']\n            del config['interface']['mindsdb_native']['storage_dir']\n            config['config_version'] = '1.2'\n            return config\n\n        def m1_2(config):\n            ''' remove no longer needed fields\n            '''\n            try:\n                del config['api']['mysql']['log']\n            except Exception:\n                pass\n\n            try:\n                del config['interface']\n            except Exception:\n                pass\n\n            if 'pip_path' in config and config['pip_path'] is None:\n                del config['pip_path']\n\n            if 'python_interpreter' in config and config['python_interpreter'] is None:\n                del config['python_interpreter']\n\n            config['config_version'] = '1.3'\n            return config\n\n        migrations = {\n            '1.0': m1_0,\n            '1.1': m1_1,\n            '1.2': m1_2\n        }\n\n        current_version = self._parse_version(self._config['config_version'])\n        target_version = self._parse_version(self.current_version)\n        while current_version < target_version:\n            str_version = '.'.join([str(x) for x in current_version])\n            self._config = migrations[str_version](self._config)\n            current_version = self._parse_version(self._config['config_version'])\n\n    def _validate(self):\n        integrations = self._config.get('integrations', {})\n        for key, value in integrations.items():\n            if not isinstance(value, dict):\n                raise TypeError(f\"Config error: integration '{key}' must be a json\")\n            if 'type' not in integrations[key]:\n                raise KeyError(f\"Config error: for integration '{key}' key 'type' must be specified\")\n\n        storage_dir = self._config.get('storage_dir')\n        if storage_dir is None:\n            raise KeyError(\"'storage_dir' mandatory key in config\")\n\n    def _parse_version(self, version):\n        if isinstance(version, str):\n            version = [int(x) for x in version.split('.')]\n        elif isinstance(version, int):\n            version = [version]\n        if len(version) == 1:\n            version.append(0)\n        return version\n\n    def _format(self):\n        ''' changing user input to formalised view\n        '''\n        for integration in self._config.get('integrations', {}).values():\n            password = integration.get('password')\n            password = '' if password is None else str(password)\n            integration['password'] = str(password)\n\n        password = self._config['api']['mysql'].get('password')\n        password = '' if password is None else str(password)\n        self._config['api']['mysql']['password'] = str(password)\n\n    def _merge_default_config(self):\n        def merge_key_recursive(target_dict, source_dict, key):\n            if key not in target_dict:\n                target_dict[key] = source_dict[key]\n            elif isinstance(target_dict[key], dict) and isinstance(source_dict[key], dict):\n                for k in source_dict[key]:\n                    merge_key_recursive(target_dict[key], source_dict[key], k)\n\n        for key in default_config:\n            merge_key_recursive(self._config, default_config, key)\n\n    def _read(self):\n        if isinstance(self.config_path, str) and os.path.isfile(self.config_path):\n            with open(self.config_path, 'r') as fp:\n                self._config = json.load(fp)\n                if self._parse_version(self._config['config_version']) < self._parse_version(self.current_version):\n                    self._migrate()\n                    self._save()\n                self._validate()\n                self._format()\n                self._merge_default_config()\n        else:\n            raise TypeError('`self.config_path` must be a string representing a local file path to a json config')\n\n    def _save(self):\n        with open(self.config_path, 'w') as fp:\n            json.dump(self._config, fp, indent=4, sort_keys=True)\n\n    def _gen_hash(self):\n        with open(self.config_path, 'rb') as fp:\n            return hashlib.md5(fp.read()).hexdigest()\n\n    def _set_updated(self, key):\n        # Only check this for dynamically generated keys, won't be needed once we switch to using a database here\n        if key in ['integrations']:\n            file_hash = self._gen_hash()\n            if file_hash != self._config_hash:\n                self._read()\n                self._config_hash = self._gen_hash()\n\n    def __getitem__(self, key):\n        self._set_updated(key)\n        return self._config[key]\n\n    def get(self, key, default=None):\n        self._set_updated(key)\n        return self._config.get(key, default)\n\n    def get_all(self):\n        return self._config\n\n    def set(self, key_chain, value, delete=False):\n        with open(self.config_path, 'r') as fp:\n            self._config = json.load(fp)\n\n        c = self._config\n        for i, k in enumerate(key_chain):\n            if k in c and i + 1 < len(key_chain):\n                c = c[k]\n            elif k not in c and i + 1 < len(key_chain):\n                c[k] = {}\n                c = c[k]\n            else:\n                if delete:\n                    del c[k]\n                else:\n                    c[k] = value\n        self._save()\n\n    # Higher level interface\n    def add_db_integration(self, name, dict):\n        dict['date_last_update'] = str(datetime.datetime.now()).split('.')[0]\n        if 'database_name' not in dict:\n            dict['database_name'] = name\n        if 'enabled' not in dict:\n            dict['enabled'] = True\n\n        self.set(['integrations', name], dict)\n\n    def modify_db_integration(self, name, dict):\n        old_dict = self._config['integrations'][name]\n        for k in old_dict:\n            if k not in dict:\n                dict[k] = old_dict[k]\n\n        self.add_db_integration(name, dict)\n\n    def remove_db_integration(self, name):\n        self.set(['integrations', name], None, True)\n", "path": "mindsdb/utilities/config.py"}], "after_files": [{"content": "import os\nimport json\nimport hashlib\nimport datetime\n\n\ndefault_config = {\n    \"log\": {\n        \"level\": {\n            \"console\": \"ERROR\",\n            \"file\": \"WARNING\"\n        }\n    },\n    \"debug\": False,\n    \"integrations\": {},\n    \"api\": {\n        \"http\": {\n            \"host\": \"127.0.0.1\",\n            \"port\": \"47334\"\n        },\n        \"mysql\": {\n            \"host\": \"127.0.0.1\",\n            \"password\": \"\",\n            \"port\": \"47335\",\n            \"user\": \"mindsdb\",\n            \"database\": \"mindsdb\",\n            \"ssl\": True\n        },\n        \"mongodb\": {\n            \"host\": \"127.0.0.1\",\n            \"port\": \"47336\",\n            \"database\": \"mindsdb\"\n        }\n    }\n}\n\n\nclass Config(object):\n    current_version = '1.3'\n    _config = {}\n    paths = {\n        'root': '',\n        'datasources': '',\n        'predictors': '',\n        'static': '',\n        'tmp': '',\n        'log': '',\n        'obsolete': {\n            'predictors': '',\n            'datasources': ''\n        }\n    }\n    versions = {}\n\n    def __init__(self, config_path):\n        self._config_path = None\n        self._config_hash = None\n        self._config = None\n        if isinstance(config_path, str):\n            self.config_path = os.path.abspath(config_path)\n            self._read()\n            self._config_hash = self._gen_hash()\n\n            storage_dir = self._config['storage_dir']\n            if os.path.isabs(storage_dir) is False:\n                storage_dir = os.path.normpath(\n                    os.path.join(\n                        os.path.dirname(self.config_path),\n                        storage_dir\n                    )\n                )\n            self.paths['root'] = storage_dir\n            self.paths['datasources'] = os.path.join(storage_dir, 'datasources')\n            self.paths['predictors'] = os.path.join(storage_dir, 'predictors')\n            self.paths['static'] = os.path.join(storage_dir, 'static')\n            self.paths['tmp'] = os.path.join(storage_dir, 'tmp')\n            self.paths['log'] = os.path.join(storage_dir, 'log')\n            self.paths['obsolete']['predictors'] = os.path.join(storage_dir, 'obsolete', 'predictors')\n            self.paths['obsolete']['datasources'] = os.path.join(storage_dir, 'obsolete', 'datasources')\n\n            self._read_versions_file(os.path.join(self.paths['root'], 'versions.json'))\n        else:\n            raise TypeError('Argument must be string representing a file path <Later on to be switched to file path and/or database connection info>')\n\n    def _read_versions_file(self, path):\n        if os.path.isfile(path):\n            with open(path, 'rt') as f:\n                self.versions = json.loads(f.read())\n\n    def _migrate(self):\n        def m1_0(config):\n            if 'default_clickhouse' in config['integrations'] and 'type' not in config['integrations']['default_clickhouse']:\n                config['integrations']['default_clickhouse']['type'] = 'clickhouse'\n            if 'default_mariadb' in config['integrations'] and 'type' not in config['integrations']['default_mariadb']:\n                config['integrations']['default_mariadb']['type'] = 'mariadb'\n            if 'datasources' in config['api']['mysql']:\n                del config['api']['mysql']['datasources']\n            config['config_version'] = '1.1'\n            return config\n\n        def m1_1(config):\n            import tempfile\n            import shutil\n            from pathlib import Path\n\n            ds_storage_path = Path(config['interface']['datastore']['storage_dir'])\n            mdb_storage_path = Path(config['interface']['mindsdb_native']['storage_dir'])\n\n            temp_dir_path = tempfile.mkdtemp()\n\n            if ds_storage_path.is_dir():\n                shutil.move(\n                    str(ds_storage_path),\n                    temp_dir_path\n                )\n\n            ds_storage_path.mkdir(mode=0o777, exist_ok=True, parents=True)\n\n            if Path(temp_dir_path).joinpath('datastore').is_dir():\n                shutil.move(\n                    str(Path(temp_dir_path).joinpath('datastore')),\n                    str(ds_storage_path.joinpath('datasources'))\n                )\n            else:\n                ds_storage_path.joinpath('datasources').mkdir(mode=0o777, exist_ok=True)\n\n            if ds_storage_path == mdb_storage_path:\n                shutil.move(\n                    str(Path(temp_dir_path)),\n                    str(ds_storage_path.joinpath('predictors'))\n                )\n            elif mdb_storage_path.is_dir():\n                shutil.move(\n                    str(mdb_storage_path),\n                    str(ds_storage_path.joinpath('predictors'))\n                )\n            else:\n                mdb_storage_path.joinpath('predictors').mkdir(mode=0o777, exist_ok=True)\n\n            ds_storage_path.joinpath('tmp').mkdir(mode=0o777, exist_ok=True)\n            ds_storage_path.joinpath('static').mkdir(mode=0o777, exist_ok=True)\n\n            if Path(temp_dir_path).is_dir():\n                shutil.rmtree(temp_dir_path)\n\n            config['storage_dir'] = str(ds_storage_path)\n            del config['interface']['datastore']['storage_dir']\n            del config['interface']['mindsdb_native']['storage_dir']\n            config['config_version'] = '1.2'\n            return config\n\n        def m1_2(config):\n            ''' remove no longer needed fields\n            '''\n            try:\n                del config['api']['mysql']['log']\n            except Exception:\n                pass\n\n            try:\n                del config['interface']\n            except Exception:\n                pass\n\n            if 'pip_path' in config and config['pip_path'] is None:\n                del config['pip_path']\n\n            if 'python_interpreter' in config and config['python_interpreter'] is None:\n                del config['python_interpreter']\n\n            config['config_version'] = '1.3'\n            return config\n\n        migrations = {\n            '1.0': m1_0,\n            '1.1': m1_1,\n            '1.2': m1_2\n        }\n\n        current_version = self._parse_version(self._config['config_version'])\n        target_version = self._parse_version(self.current_version)\n        while current_version < target_version:\n            str_version = '.'.join([str(x) for x in current_version])\n            self._config = migrations[str_version](self._config)\n            current_version = self._parse_version(self._config['config_version'])\n\n    def _validate(self):\n        integrations = self._config.get('integrations', {})\n        for key, value in integrations.items():\n            if not isinstance(value, dict):\n                raise TypeError(f\"Config error: integration '{key}' must be a json\")\n            if 'type' not in integrations[key]:\n                raise KeyError(f\"Config error: for integration '{key}' key 'type' must be specified\")\n\n        storage_dir = self._config.get('storage_dir')\n        if storage_dir is None:\n            raise KeyError(\"'storage_dir' mandatory key in config\")\n\n    def _parse_version(self, version):\n        if isinstance(version, str):\n            version = [int(x) for x in version.split('.')]\n        elif isinstance(version, int):\n            version = [version]\n        if len(version) == 1:\n            version.append(0)\n        return version\n\n    def _format(self):\n        ''' changing user input to formalised view\n        '''\n        for integration in self._config.get('integrations', {}).values():\n            password = integration.get('password')\n            password = '' if password is None else str(password)\n            integration['password'] = str(password)\n\n        password = self._config['api']['mysql'].get('password')\n        password = '' if password is None else str(password)\n        self._config['api']['mysql']['password'] = str(password)\n\n    def _merge_default_config(self):\n        def merge_key_recursive(target_dict, source_dict, key):\n            if key not in target_dict:\n                target_dict[key] = source_dict[key]\n            elif isinstance(target_dict[key], dict) and isinstance(source_dict[key], dict):\n                for k in source_dict[key]:\n                    merge_key_recursive(target_dict[key], source_dict[key], k)\n\n        for key in default_config:\n            merge_key_recursive(self._config, default_config, key)\n\n    def _read(self):\n        if isinstance(self.config_path, str) and os.path.isfile(self.config_path):\n            with open(self.config_path, 'r') as fp:\n                self._config = json.load(fp)\n                if self._parse_version(self._config['config_version']) < self._parse_version(self.current_version):\n                    self._migrate()\n                    self._save()\n                self._validate()\n                self._format()\n                self._merge_default_config()\n        else:\n            raise TypeError('`self.config_path` must be a string representing a local file path to a json config')\n\n    def _save(self):\n        with open(self.config_path, 'w') as fp:\n            json.dump(self._config, fp, indent=4, sort_keys=True)\n\n    def _gen_hash(self):\n        with open(self.config_path, 'rb') as fp:\n            return hashlib.md5(fp.read()).hexdigest()\n\n    def _set_updated(self, key):\n        # Only check this for dynamically generated keys, won't be needed once we switch to using a database here\n        if key in ['integrations']:\n            file_hash = self._gen_hash()\n            if file_hash != self._config_hash:\n                self._read()\n                self._config_hash = self._gen_hash()\n\n    def __getitem__(self, key):\n        self._set_updated(key)\n        return self._config[key]\n\n    def get(self, key, default=None):\n        self._set_updated(key)\n        return self._config.get(key, default)\n\n    def get_all(self):\n        return self._config\n\n    def set(self, key_chain, value, delete=False):\n        with open(self.config_path, 'r') as fp:\n            self._config = json.load(fp)\n\n        c = self._config\n        for i, k in enumerate(key_chain):\n            if k in c and i + 1 < len(key_chain):\n                c = c[k]\n            elif k not in c and i + 1 < len(key_chain):\n                c[k] = {}\n                c = c[k]\n            else:\n                if delete:\n                    del c[k]\n                else:\n                    c[k] = value\n        self._save()\n\n    # Higher level interface\n    def add_db_integration(self, name, dict):\n        dict['date_last_update'] = str(datetime.datetime.now()).split('.')[0]\n        if 'database_name' not in dict:\n            dict['database_name'] = name\n        if 'enabled' not in dict:\n            dict['enabled'] = True\n\n        self.set(['integrations', name], dict)\n\n    def modify_db_integration(self, name, dict):\n        old_dict = self._config['integrations'][name]\n        for k in old_dict:\n            if k not in dict:\n                dict[k] = old_dict[k]\n\n        self.add_db_integration(name, dict)\n\n    def remove_db_integration(self, name):\n        self.set(['integrations', name], None, True)\n", "path": "mindsdb/utilities/config.py"}]}