MindsDB 25.5.4.2__py3-none-any.whl → 25.6.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of MindsDB might be problematic. Click here for more details.

Files changed (76) hide show
  1. mindsdb/__about__.py +1 -1
  2. mindsdb/api/a2a/agent.py +50 -26
  3. mindsdb/api/a2a/common/server/server.py +32 -26
  4. mindsdb/api/a2a/task_manager.py +68 -6
  5. mindsdb/api/executor/command_executor.py +69 -14
  6. mindsdb/api/executor/datahub/datanodes/integration_datanode.py +49 -65
  7. mindsdb/api/executor/datahub/datanodes/mindsdb_tables.py +91 -84
  8. mindsdb/api/executor/datahub/datanodes/project_datanode.py +29 -48
  9. mindsdb/api/executor/datahub/datanodes/system_tables.py +35 -61
  10. mindsdb/api/executor/planner/plan_join.py +67 -77
  11. mindsdb/api/executor/planner/query_planner.py +176 -155
  12. mindsdb/api/executor/planner/steps.py +37 -12
  13. mindsdb/api/executor/sql_query/result_set.py +45 -64
  14. mindsdb/api/executor/sql_query/steps/fetch_dataframe.py +14 -18
  15. mindsdb/api/executor/sql_query/steps/fetch_dataframe_partition.py +17 -18
  16. mindsdb/api/executor/sql_query/steps/insert_step.py +13 -33
  17. mindsdb/api/executor/sql_query/steps/subselect_step.py +43 -35
  18. mindsdb/api/executor/utilities/sql.py +42 -48
  19. mindsdb/api/http/namespaces/config.py +1 -1
  20. mindsdb/api/http/namespaces/file.py +14 -23
  21. mindsdb/api/http/namespaces/knowledge_bases.py +132 -154
  22. mindsdb/api/mysql/mysql_proxy/data_types/mysql_datum.py +12 -28
  23. mindsdb/api/mysql/mysql_proxy/data_types/mysql_packets/binary_resultset_row_package.py +59 -50
  24. mindsdb/api/mysql/mysql_proxy/data_types/mysql_packets/resultset_row_package.py +9 -8
  25. mindsdb/api/mysql/mysql_proxy/libs/constants/mysql.py +449 -461
  26. mindsdb/api/mysql/mysql_proxy/utilities/dump.py +87 -36
  27. mindsdb/integrations/handlers/bigquery_handler/bigquery_handler.py +219 -28
  28. mindsdb/integrations/handlers/file_handler/file_handler.py +15 -9
  29. mindsdb/integrations/handlers/file_handler/tests/test_file_handler.py +43 -24
  30. mindsdb/integrations/handlers/litellm_handler/litellm_handler.py +10 -3
  31. mindsdb/integrations/handlers/llama_index_handler/requirements.txt +1 -1
  32. mindsdb/integrations/handlers/mysql_handler/mysql_handler.py +29 -33
  33. mindsdb/integrations/handlers/openai_handler/openai_handler.py +277 -356
  34. mindsdb/integrations/handlers/oracle_handler/oracle_handler.py +74 -51
  35. mindsdb/integrations/handlers/postgres_handler/postgres_handler.py +305 -98
  36. mindsdb/integrations/handlers/salesforce_handler/salesforce_handler.py +145 -40
  37. mindsdb/integrations/handlers/salesforce_handler/salesforce_tables.py +136 -6
  38. mindsdb/integrations/handlers/snowflake_handler/snowflake_handler.py +352 -83
  39. mindsdb/integrations/libs/api_handler.py +279 -57
  40. mindsdb/integrations/libs/base.py +185 -30
  41. mindsdb/integrations/utilities/files/file_reader.py +99 -73
  42. mindsdb/integrations/utilities/handler_utils.py +23 -8
  43. mindsdb/integrations/utilities/sql_utils.py +35 -40
  44. mindsdb/interfaces/agents/agents_controller.py +226 -196
  45. mindsdb/interfaces/agents/constants.py +8 -1
  46. mindsdb/interfaces/agents/langchain_agent.py +42 -11
  47. mindsdb/interfaces/agents/mcp_client_agent.py +29 -21
  48. mindsdb/interfaces/agents/mindsdb_database_agent.py +23 -18
  49. mindsdb/interfaces/data_catalog/__init__.py +0 -0
  50. mindsdb/interfaces/data_catalog/base_data_catalog.py +54 -0
  51. mindsdb/interfaces/data_catalog/data_catalog_loader.py +375 -0
  52. mindsdb/interfaces/data_catalog/data_catalog_reader.py +38 -0
  53. mindsdb/interfaces/database/database.py +81 -57
  54. mindsdb/interfaces/database/integrations.py +222 -234
  55. mindsdb/interfaces/database/log.py +72 -104
  56. mindsdb/interfaces/database/projects.py +156 -193
  57. mindsdb/interfaces/file/file_controller.py +21 -65
  58. mindsdb/interfaces/knowledge_base/controller.py +66 -25
  59. mindsdb/interfaces/knowledge_base/evaluate.py +516 -0
  60. mindsdb/interfaces/knowledge_base/llm_client.py +75 -0
  61. mindsdb/interfaces/skills/custom/text2sql/mindsdb_kb_tools.py +83 -43
  62. mindsdb/interfaces/skills/skills_controller.py +31 -36
  63. mindsdb/interfaces/skills/sql_agent.py +113 -86
  64. mindsdb/interfaces/storage/db.py +242 -82
  65. mindsdb/migrations/versions/2025-05-28_a44643042fe8_added_data_catalog_tables.py +118 -0
  66. mindsdb/migrations/versions/2025-06-09_608e376c19a7_updated_data_catalog_data_types.py +58 -0
  67. mindsdb/utilities/config.py +13 -2
  68. mindsdb/utilities/log.py +35 -26
  69. mindsdb/utilities/ml_task_queue/task.py +19 -22
  70. mindsdb/utilities/render/sqlalchemy_render.py +129 -181
  71. mindsdb/utilities/starters.py +40 -0
  72. {mindsdb-25.5.4.2.dist-info → mindsdb-25.6.3.0.dist-info}/METADATA +257 -257
  73. {mindsdb-25.5.4.2.dist-info → mindsdb-25.6.3.0.dist-info}/RECORD +76 -68
  74. {mindsdb-25.5.4.2.dist-info → mindsdb-25.6.3.0.dist-info}/WHEEL +0 -0
  75. {mindsdb-25.5.4.2.dist-info → mindsdb-25.6.3.0.dist-info}/licenses/LICENSE +0 -0
  76. {mindsdb-25.5.4.2.dist-info → mindsdb-25.6.3.0.dist-info}/top_level.txt +0 -0
@@ -32,19 +32,19 @@ from mindsdb.utilities import log
32
32
  from mindsdb.integrations.libs.ml_exec_base import BaseMLEngineExec
33
33
  from mindsdb.integrations.libs.base import BaseHandler
34
34
  import mindsdb.utilities.profiler as profiler
35
+ from mindsdb.interfaces.data_catalog.data_catalog_loader import DataCatalogLoader
35
36
 
36
37
  logger = log.getLogger(__name__)
37
38
 
38
39
 
39
40
  class HandlersCache:
40
- """ Cache for data handlers that keep connections opened during ttl time from handler last use
41
- """
41
+ """Cache for data handlers that keep connections opened during ttl time from handler last use"""
42
42
 
43
43
  def __init__(self, ttl: int = 60):
44
- """ init cache
44
+ """init cache
45
45
 
46
- Args:
47
- ttl (int): time to live (in seconds) for record in cache
46
+ Args:
47
+ ttl (int): time to live (in seconds) for record in cache
48
48
  """
49
49
  self.ttl = ttl
50
50
  self.handlers = {}
@@ -56,50 +56,46 @@ class HandlersCache:
56
56
  self._stop_clean()
57
57
 
58
58
  def _start_clean(self) -> None:
59
- """ start worker that close connections after ttl expired
60
- """
61
- if (
62
- isinstance(self.cleaner_thread, threading.Thread)
63
- and self.cleaner_thread.is_alive()
64
- ):
59
+ """start worker that close connections after ttl expired"""
60
+ if isinstance(self.cleaner_thread, threading.Thread) and self.cleaner_thread.is_alive():
65
61
  return
66
62
  self._stop_event.clear()
67
- self.cleaner_thread = threading.Thread(target=self._clean, name='HandlersCache.clean')
63
+ self.cleaner_thread = threading.Thread(target=self._clean, name="HandlersCache.clean")
68
64
  self.cleaner_thread.daemon = True
69
65
  self.cleaner_thread.start()
70
66
 
71
67
  def _stop_clean(self) -> None:
72
- """ stop clean worker
73
- """
68
+ """stop clean worker"""
74
69
  self._stop_event.set()
75
70
 
76
71
  def set(self, handler: DatabaseHandler):
77
- """ add (or replace) handler in cache
72
+ """add (or replace) handler in cache
78
73
 
79
- Args:
80
- handler (DatabaseHandler)
74
+ Args:
75
+ handler (DatabaseHandler)
81
76
  """
82
77
  with self._lock:
83
78
  try:
84
79
  # If the handler is defined to be thread safe, set 0 as the last element of the key, otherwise set the thrad ID.
85
- key = (handler.name, ctx.company_id, 0 if getattr(handler, 'thread_safe', False) else threading.get_native_id())
80
+ key = (
81
+ handler.name,
82
+ ctx.company_id,
83
+ 0 if getattr(handler, "thread_safe", False) else threading.get_native_id(),
84
+ )
86
85
  handler.connect()
87
- self.handlers[key] = {
88
- 'handler': handler,
89
- 'expired_at': time.time() + self.ttl
90
- }
86
+ self.handlers[key] = {"handler": handler, "expired_at": time.time() + self.ttl}
91
87
  except Exception:
92
88
  pass
93
89
  self._start_clean()
94
90
 
95
91
  def get(self, name: str) -> Optional[DatabaseHandler]:
96
- """ get handler from cache by name
92
+ """get handler from cache by name
97
93
 
98
- Args:
99
- name (str): handler name
94
+ Args:
95
+ name (str): handler name
100
96
 
101
- Returns:
102
- DatabaseHandler
97
+ Returns:
98
+ DatabaseHandler
103
99
  """
104
100
  with self._lock:
105
101
  # If the handler is not thread safe, the thread ID will be assigned to the last element of the key.
@@ -107,19 +103,16 @@ class HandlersCache:
107
103
  if key not in self.handlers:
108
104
  # If the handler is thread safe, a 0 will be assigned to the last element of the key.
109
105
  key = (name, ctx.company_id, 0)
110
- if (
111
- key not in self.handlers
112
- or self.handlers[key]['expired_at'] < time.time()
113
- ):
106
+ if key not in self.handlers or self.handlers[key]["expired_at"] < time.time():
114
107
  return None
115
- self.handlers[key]['expired_at'] = time.time() + self.ttl
116
- return self.handlers[key]['handler']
108
+ self.handlers[key]["expired_at"] = time.time() + self.ttl
109
+ return self.handlers[key]["handler"]
117
110
 
118
111
  def delete(self, name: str) -> None:
119
- """ delete handler from cache
112
+ """delete handler from cache
120
113
 
121
- Args:
122
- name (str): handler name
114
+ Args:
115
+ name (str): handler name
123
116
  """
124
117
  with self._lock:
125
118
  key = (name, ctx.company_id, threading.get_native_id())
@@ -133,14 +126,13 @@ class HandlersCache:
133
126
  self._stop_clean()
134
127
 
135
128
  def _clean(self) -> None:
136
- """ worker that delete from cache handlers that was not in use for ttl
137
- """
129
+ """worker that delete from cache handlers that was not in use for ttl"""
138
130
  while self._stop_event.wait(timeout=3) is False:
139
131
  with self._lock:
140
132
  for key in list(self.handlers.keys()):
141
133
  if (
142
- self.handlers[key]['expired_at'] < time.time()
143
- and sys.getrefcount(self.handlers[key]) == 2 # returned ref count is always 1 higher
134
+ self.handlers[key]["expired_at"] < time.time()
135
+ and sys.getrefcount(self.handlers[key]) == 2 # returned ref count is always 1 higher
144
136
  ):
145
137
  try:
146
138
  self.handlers[key].disconnect()
@@ -163,50 +155,43 @@ class IntegrationController:
163
155
 
164
156
  def _add_integration_record(self, name, engine, connection_args):
165
157
  integration_record = db.Integration(
166
- name=name,
167
- engine=engine,
168
- data=connection_args or {},
169
- company_id=ctx.company_id
158
+ name=name, engine=engine, data=connection_args or {}, company_id=ctx.company_id
170
159
  )
171
160
  db.session.add(integration_record)
172
161
  db.session.commit()
173
162
  return integration_record.id
174
163
 
175
164
  def add(self, name, engine, connection_args):
176
-
177
165
  logger.debug(
178
166
  "%s: add method calling name=%s, engine=%s, connection_args=%s, company_id=%s",
179
- self.__class__.__name__, name, engine, connection_args, ctx.company_id
167
+ self.__class__.__name__,
168
+ name,
169
+ engine,
170
+ connection_args,
171
+ ctx.company_id,
180
172
  )
181
173
  handler_meta = self.get_handler_meta(engine)
182
174
 
183
- accept_connection_args = handler_meta.get('connection_args')
175
+ accept_connection_args = handler_meta.get("connection_args")
184
176
  logger.debug("%s: accept_connection_args - %s", self.__class__.__name__, accept_connection_args)
185
177
 
186
178
  files_dir = None
187
179
  if accept_connection_args is not None and connection_args is not None:
188
180
  for arg_name, arg_value in connection_args.items():
189
- if (
190
- arg_name in accept_connection_args
191
- and accept_connection_args[arg_name]['type'] == ARG_TYPE.PATH
192
- ):
181
+ if arg_name in accept_connection_args and accept_connection_args[arg_name]["type"] == ARG_TYPE.PATH:
193
182
  if files_dir is None:
194
- files_dir = tempfile.mkdtemp(prefix='mindsdb_files_')
183
+ files_dir = tempfile.mkdtemp(prefix="mindsdb_files_")
195
184
  shutil.copy(arg_value, files_dir)
196
185
  connection_args[arg_name] = Path(arg_value).name
197
186
 
198
187
  integration_id = self._add_integration_record(name, engine, connection_args)
199
188
 
200
189
  if files_dir is not None:
201
- store = FileStorage(
202
- resource_group=RESOURCE_GROUP.INTEGRATION,
203
- resource_id=integration_id,
204
- sync=False
205
- )
206
- store.add(files_dir, '')
190
+ store = FileStorage(resource_group=RESOURCE_GROUP.INTEGRATION, resource_id=integration_id, sync=False)
191
+ store.add(files_dir, "")
207
192
  store.push()
208
193
 
209
- if handler_meta.get('type') == HANDLER_TYPE.ML:
194
+ if handler_meta.get("type") == HANDLER_TYPE.ML:
210
195
  ml_handler = self.get_ml_handler(name)
211
196
  ml_handler.create_engine(connection_args, integration_id)
212
197
 
@@ -215,7 +200,7 @@ class IntegrationController:
215
200
  def modify(self, name, data):
216
201
  self.handlers_cache.delete(name)
217
202
  integration_record = self._get_integration_record(name)
218
- if isinstance(integration_record.data, dict) and integration_record.data.get('is_demo') is True:
203
+ if isinstance(integration_record.data, dict) and integration_record.data.get("is_demo") is True:
219
204
  raise ValueError("It is forbidden to change properties of the demo object")
220
205
  old_data = deepcopy(integration_record.data)
221
206
  for k in old_data:
@@ -226,8 +211,8 @@ class IntegrationController:
226
211
  db.session.commit()
227
212
 
228
213
  def delete(self, name):
229
- if name in ('files', 'lightwood'):
230
- raise Exception('Unable to drop: is system database')
214
+ if name in ("files", "lightwood"):
215
+ raise Exception("Unable to drop: is system database")
231
216
 
232
217
  self.handlers_cache.delete(name)
233
218
 
@@ -235,32 +220,32 @@ class IntegrationController:
235
220
  if name in self.handler_modules:
236
221
  handler = self.handler_modules[name]
237
222
 
238
- if getattr(handler, 'permanent', False) is True:
239
- raise Exception('Unable to drop permanent integration')
223
+ if getattr(handler, "permanent", False) is True:
224
+ raise Exception("Unable to drop permanent integration")
240
225
 
241
226
  integration_record = self._get_integration_record(name)
242
- if isinstance(integration_record.data, dict) and integration_record.data.get('is_demo') is True:
243
- raise Exception('Unable to drop demo object')
227
+ if isinstance(integration_record.data, dict) and integration_record.data.get("is_demo") is True:
228
+ raise Exception("Unable to drop demo object")
244
229
 
245
230
  # if this is ml engine
246
231
  engine_models = get_model_records(ml_handler_name=name, deleted_at=None)
247
232
  active_models = [m.name for m in engine_models if m.deleted_at is None]
248
233
  if len(active_models) > 0:
249
- raise Exception(f'Unable to drop ml engine with active models: {active_models}')
234
+ raise Exception(f"Unable to drop ml engine with active models: {active_models}")
250
235
 
251
236
  # check linked KBs
252
237
  kb = db.KnowledgeBase.query.filter_by(vector_database_id=integration_record.id).first()
253
238
  if kb is not None:
254
- raise Exception(f'Unable to drop, integration is used by knowledge base: {kb.name}')
239
+ raise Exception(f"Unable to drop, integration is used by knowledge base: {kb.name}")
255
240
 
256
241
  # check linked predictors
257
242
  models = get_model_records()
258
243
  for model in models:
259
244
  if (
260
245
  model.data_integration_ref is not None
261
- and model.data_integration_ref.get('type') == 'integration'
262
- and isinstance(model.data_integration_ref.get('id'), int)
263
- and model.data_integration_ref['id'] == integration_record.id
246
+ and model.data_integration_ref.get("type") == "integration"
247
+ and isinstance(model.data_integration_ref.get("id"), int)
248
+ and model.data_integration_ref["id"] == integration_record.id
264
249
  ):
265
250
  model.data_integration_ref = None
266
251
 
@@ -269,6 +254,14 @@ class IntegrationController:
269
254
  if model.deleted_at is not None:
270
255
  model.integration_id = None
271
256
 
257
+ # Remove the integration metadata from the data catalog (if enabled).
258
+ # TODO: Can this be handled via cascading delete in the database?
259
+ if self.get_handler_meta(integration_record.engine).get("type") == HANDLER_TYPE.DATA and Config().get(
260
+ "data_catalog", {}
261
+ ).get("enabled", False):
262
+ data_catalog_reader = DataCatalogLoader(database_name=name)
263
+ data_catalog_reader.unload_metadata()
264
+
272
265
  db.session.delete(integration_record)
273
266
  db.session.commit()
274
267
 
@@ -281,82 +274,77 @@ class IntegrationController:
281
274
  return None
282
275
  data = deepcopy(integration_record.data)
283
276
 
284
- bundle_path = data.get('secure_connect_bundle')
285
- mysql_ssl_ca = data.get('ssl_ca')
286
- mysql_ssl_cert = data.get('ssl_cert')
287
- mysql_ssl_key = data.get('ssl_key')
277
+ bundle_path = data.get("secure_connect_bundle")
278
+ mysql_ssl_ca = data.get("ssl_ca")
279
+ mysql_ssl_cert = data.get("ssl_cert")
280
+ mysql_ssl_key = data.get("ssl_key")
288
281
  if (
289
- data.get('type') in ('mysql', 'mariadb')
282
+ data.get("type") in ("mysql", "mariadb")
290
283
  and (
291
284
  self._is_not_empty_str(mysql_ssl_ca)
292
285
  or self._is_not_empty_str(mysql_ssl_cert)
293
286
  or self._is_not_empty_str(mysql_ssl_key)
294
287
  )
295
- or data.get('type') in ('cassandra', 'scylla')
288
+ or data.get("type") in ("cassandra", "scylla")
296
289
  and bundle_path is not None
297
290
  ):
298
291
  fs_store = FsStore()
299
- integrations_dir = Config()['paths']['integrations']
300
- folder_name = f'integration_files_{integration_record.company_id}_{integration_record.id}'
301
- fs_store.get(
302
- folder_name,
303
- base_dir=integrations_dir
304
- )
292
+ integrations_dir = Config()["paths"]["integrations"]
293
+ folder_name = f"integration_files_{integration_record.company_id}_{integration_record.id}"
294
+ fs_store.get(folder_name, base_dir=integrations_dir)
305
295
 
306
296
  handler_meta = self.get_handler_metadata(integration_record.engine)
307
297
  integration_type = None
308
298
  if isinstance(handler_meta, dict):
309
299
  # in other cases, the handler directory is likely not exist.
310
- integration_type = handler_meta.get('type')
300
+ integration_type = handler_meta.get("type")
311
301
 
312
302
  if show_secrets is False and handler_meta is not None:
313
- connection_args = handler_meta.get('connection_args', None)
303
+ connection_args = handler_meta.get("connection_args", None)
314
304
  if isinstance(connection_args, dict):
315
305
  if integration_type == HANDLER_TYPE.DATA:
316
306
  for key, value in connection_args.items():
317
- if key in data and value.get('secret', False) is True:
318
- data[key] = '******'
307
+ if key in data and value.get("secret", False) is True:
308
+ data[key] = "******"
319
309
  elif integration_type == HANDLER_TYPE.ML:
320
- creation_args = connection_args.get('creation_args')
310
+ creation_args = connection_args.get("creation_args")
321
311
  if isinstance(creation_args, dict):
322
312
  for key, value in creation_args.items():
323
- if key in data and value.get('secret', False) is True:
324
- data[key] = '******'
313
+ if key in data and value.get("secret", False) is True:
314
+ data[key] = "******"
325
315
  else:
326
- raise ValueError(f'Unexpected handler type: {integration_type}')
316
+ raise ValueError(f"Unexpected handler type: {integration_type}")
327
317
  else:
328
318
  # region obsolete, del in future
329
- if 'password' in data:
330
- data['password'] = None
319
+ if "password" in data:
320
+ data["password"] = None
331
321
  if (
332
- data.get('type') == 'redis'
333
- and isinstance(data.get('connection'), dict)
334
- and 'password' in data['connection']
322
+ data.get("type") == "redis"
323
+ and isinstance(data.get("connection"), dict)
324
+ and "password" in data["connection"]
335
325
  ):
336
- data['connection'] = None
326
+ data["connection"] = None
337
327
  # endregion
338
328
 
339
329
  class_type, permanent = None, False
340
330
  if handler_meta is not None:
341
- class_type = handler_meta.get('class_type')
342
- permanent = handler_meta.get('permanent', False)
331
+ class_type = handler_meta.get("class_type")
332
+ permanent = handler_meta.get("permanent", False)
343
333
 
344
334
  return {
345
- 'id': integration_record.id,
346
- 'name': integration_record.name,
347
- 'type': integration_type,
348
- 'class_type': class_type,
349
- 'engine': integration_record.engine,
350
- 'permanent': permanent,
351
- 'date_last_update': deepcopy(integration_record.updated_at), # to del ?
352
- 'connection_data': data
335
+ "id": integration_record.id,
336
+ "name": integration_record.name,
337
+ "type": integration_type,
338
+ "class_type": class_type,
339
+ "engine": integration_record.engine,
340
+ "permanent": permanent,
341
+ "date_last_update": deepcopy(integration_record.updated_at), # to del ?
342
+ "connection_data": data,
353
343
  }
354
344
 
355
345
  def get_by_id(self, integration_id, show_secrets=True):
356
346
  integration_record = (
357
- db.session.query(db.Integration)
358
- .filter_by(company_id=ctx.company_id, id=integration_id)
359
- .first()
347
+ db.session.query(db.Integration).filter_by(company_id=ctx.company_id, id=integration_id).first()
360
348
  )
361
349
  return self._get_integration_record_data(integration_record, show_secrets)
362
350
 
@@ -379,20 +367,21 @@ class IntegrationController:
379
367
  db.Integration
380
368
  """
381
369
  if case_sensitive:
382
- integration_records = db.session.query(db.Integration).filter_by(
383
- company_id=ctx.company_id,
384
- name=name
385
- ).all()
370
+ integration_records = db.session.query(db.Integration).filter_by(company_id=ctx.company_id, name=name).all()
386
371
  if len(integration_records) > 1:
387
372
  raise Exception(f"There is {len(integration_records)} integrations with name '{name}'")
388
373
  if len(integration_records) == 0:
389
374
  raise EntityNotExistsError(f"There is no integration with name '{name}'")
390
375
  integration_record = integration_records[0]
391
376
  else:
392
- integration_record = db.session.query(db.Integration).filter(
393
- (db.Integration.company_id == ctx.company_id)
394
- & (func.lower(db.Integration.name) == func.lower(name))
395
- ).first()
377
+ integration_record = (
378
+ db.session.query(db.Integration)
379
+ .filter(
380
+ (db.Integration.company_id == ctx.company_id)
381
+ & (func.lower(db.Integration.name) == func.lower(name))
382
+ )
383
+ .first()
384
+ )
396
385
  if integration_record is None:
397
386
  raise EntityNotExistsError(f"There is no integration with name '{name}'")
398
387
 
@@ -407,21 +396,28 @@ class IntegrationController:
407
396
  integration_dict[record.name] = self._get_integration_record_data(record, show_secrets)
408
397
  return integration_dict
409
398
 
410
- def _make_handler_args(self, name: str, handler_type: str, connection_data: dict, integration_id: int = None,
411
- file_storage: FileStorage = None, handler_storage: HandlerStorage = None):
399
+ def _make_handler_args(
400
+ self,
401
+ name: str,
402
+ handler_type: str,
403
+ connection_data: dict,
404
+ integration_id: int = None,
405
+ file_storage: FileStorage = None,
406
+ handler_storage: HandlerStorage = None,
407
+ ):
412
408
  handler_args = dict(
413
409
  name=name,
414
410
  integration_id=integration_id,
415
411
  connection_data=connection_data,
416
412
  file_storage=file_storage,
417
- handler_storage=handler_storage
413
+ handler_storage=handler_storage,
418
414
  )
419
415
 
420
- if handler_type == 'files':
421
- handler_args['file_controller'] = FileController()
416
+ if handler_type == "files":
417
+ handler_args["file_controller"] = FileController()
422
418
  elif self.handler_modules.get(handler_type, False).type == HANDLER_TYPE.ML:
423
- handler_args['handler_controller'] = self
424
- handler_args['company_id'] = ctx.company_id
419
+ handler_args["handler_controller"] = self
420
+ handler_args["company_id"] = ctx.company_id
425
421
 
426
422
  return handler_args
427
423
 
@@ -439,12 +435,9 @@ class IntegrationController:
439
435
  integration_id = int(time.time() * 10000)
440
436
 
441
437
  file_storage = FileStorage(
442
- resource_group=RESOURCE_GROUP.INTEGRATION,
443
- resource_id=integration_id,
444
- root_dir='tmp',
445
- sync=False
438
+ resource_group=RESOURCE_GROUP.INTEGRATION, resource_id=integration_id, root_dir="tmp", sync=False
446
439
  )
447
- handler_storage = HandlerStorage(integration_id, root_dir='tmp', is_temporal=True)
440
+ handler_storage = HandlerStorage(integration_id, root_dir="tmp", is_temporal=True)
448
441
 
449
442
  handler_meta = self.get_handler_meta(engine)
450
443
  if handler_meta is None:
@@ -466,7 +459,7 @@ class IntegrationController:
466
459
 
467
460
  def copy_integration_storage(self, integration_id_from, integration_id_to):
468
461
  storage_from = HandlerStorage(integration_id_from)
469
- root_path = ''
462
+ root_path = ""
470
463
 
471
464
  if storage_from.is_empty():
472
465
  return None
@@ -494,7 +487,7 @@ class IntegrationController:
494
487
  if integration_meta is None:
495
488
  raise Exception(f"Handler '{name}' does not exists")
496
489
 
497
- if integration_meta.get('type') != HANDLER_TYPE.ML:
490
+ if integration_meta.get("type") != HANDLER_TYPE.ML:
498
491
  raise Exception(f"Handler '{name}' must be ML type")
499
492
 
500
493
  logger.info(
@@ -504,7 +497,7 @@ class IntegrationController:
504
497
  handler = BaseMLEngineExec(
505
498
  name=integration_record.name,
506
499
  integration_id=integration_record.id,
507
- handler_module=self.handler_modules[integration_engine]
500
+ handler_module=self.handler_modules[integration_engine],
508
501
  )
509
502
 
510
503
  return handler
@@ -532,35 +525,36 @@ class IntegrationController:
532
525
  if integration_meta is None:
533
526
  raise Exception(f"Handler '{name}' does not exist")
534
527
 
535
- if integration_meta.get('type') != HANDLER_TYPE.DATA:
528
+ if integration_meta.get("type") != HANDLER_TYPE.DATA:
536
529
  raise Exception(f"Handler '{name}' must be DATA type")
537
530
 
538
531
  integration_data = self._get_integration_record_data(integration_record, True)
539
532
  if integration_data is None:
540
533
  raise Exception(f"Can't find integration_record for handler '{name}'")
541
- connection_data = integration_data.get('connection_data', {})
534
+ connection_data = integration_data.get("connection_data", {})
542
535
  logger.debug(
543
536
  "%s.get_handler: connection_data=%s, engine=%s",
544
537
  self.__class__.__name__,
545
- connection_data, integration_engine
538
+ connection_data,
539
+ integration_engine,
546
540
  )
547
541
 
548
542
  if integration_meta["import"]["success"] is False:
549
- msg = dedent(f'''\
543
+ msg = dedent(f"""\
550
544
  Handler '{integration_engine}' cannot be used. Reason is:
551
- {integration_meta['import']['error_message']}
552
- ''')
553
- is_cloud = Config().get('cloud', False)
545
+ {integration_meta["import"]["error_message"]}
546
+ """)
547
+ is_cloud = Config().get("cloud", False)
554
548
  if is_cloud is False:
555
- msg += dedent(f'''
549
+ msg += dedent(f"""
556
550
 
557
551
  If error is related to missing dependencies, then try to run command in shell and restart mindsdb:
558
552
  pip install mindsdb[{integration_engine}]
559
- ''')
553
+ """)
560
554
  logger.debug(msg)
561
555
  raise Exception(msg)
562
556
 
563
- connection_args = integration_meta.get('connection_args')
557
+ connection_args = integration_meta.get("connection_args")
564
558
  logger.debug("%s.get_handler: connection args - %s", self.__class__.__name__, connection_args)
565
559
 
566
560
  file_storage = FileStorage(
@@ -572,11 +566,11 @@ class IntegrationController:
572
566
 
573
567
  if isinstance(connection_args, (dict, OrderedDict)):
574
568
  files_to_get = {
575
- arg_name: arg_value for arg_name, arg_value in connection_data.items()
576
- if arg_name in connection_args and connection_args.get(arg_name)['type'] == ARG_TYPE.PATH
569
+ arg_name: arg_value
570
+ for arg_name, arg_value in connection_data.items()
571
+ if arg_name in connection_args and connection_args.get(arg_name)["type"] == ARG_TYPE.PATH
577
572
  }
578
573
  if len(files_to_get) > 0:
579
-
580
574
  for file_name, file_path in files_to_get.items():
581
575
  connection_data[file_name] = file_storage.get_path(file_path)
582
576
 
@@ -584,9 +578,9 @@ class IntegrationController:
584
578
  name=name,
585
579
  handler_type=integration_engine,
586
580
  connection_data=connection_data,
587
- integration_id=integration_data['id'],
581
+ integration_id=integration_data["id"],
588
582
  file_storage=file_storage,
589
- handler_storage=handler_storage
583
+ handler_storage=handler_storage,
590
584
  )
591
585
 
592
586
  HandlerClass = self.handler_modules[integration_engine].Handler
@@ -602,82 +596,76 @@ class IntegrationController:
602
596
  handler_meta = self._get_handler_meta(handler_name)
603
597
  except Exception as e:
604
598
  handler_meta = self.handlers_import_status[handler_name]
605
- handler_meta['import']['success'] = False
606
- handler_meta['import']['error_message'] = str(e)
599
+ handler_meta["import"]["success"] = False
600
+ handler_meta["import"]["error_message"] = str(e)
607
601
 
608
602
  self.handlers_import_status[handler_name] = handler_meta
609
603
 
610
604
  def _read_dependencies(self, path):
611
605
  dependencies = []
612
- requirements_txt = Path(path).joinpath('requirements.txt')
606
+ requirements_txt = Path(path).joinpath("requirements.txt")
613
607
  if requirements_txt.is_file():
614
- with open(str(requirements_txt), 'rt') as f:
615
- dependencies = [x.strip(' \t\n') for x in f.readlines()]
608
+ with open(str(requirements_txt), "rt") as f:
609
+ dependencies = [x.strip(" \t\n") for x in f.readlines()]
616
610
  dependencies = [x for x in dependencies if len(x) > 0]
617
611
  return dependencies
618
612
 
619
613
  def _get_handler_meta(self, handler_name):
620
-
621
614
  module = self.handler_modules[handler_name]
622
615
 
623
616
  handler_dir = Path(module.__path__[0])
624
617
  handler_folder_name = handler_dir.name
625
618
 
626
- import_error = getattr(module, 'import_error', None)
619
+ import_error = getattr(module, "import_error", None)
627
620
  handler_meta = self.handlers_import_status[handler_name]
628
- handler_meta['import']['success'] = import_error is None
629
- handler_meta['version'] = module.version
630
- handler_meta['thread_safe'] = getattr(module, 'thread_safe', False)
621
+ handler_meta["import"]["success"] = import_error is None
622
+ handler_meta["version"] = module.version
623
+ handler_meta["thread_safe"] = getattr(module, "thread_safe", False)
631
624
 
632
625
  if import_error is not None:
633
- handler_meta['import']['error_message'] = str(import_error)
626
+ handler_meta["import"]["error_message"] = str(import_error)
634
627
 
635
- handler_type = getattr(module, 'type', None)
628
+ handler_type = getattr(module, "type", None)
636
629
  handler_class = None
637
- if hasattr(module, 'Handler') and inspect.isclass(module.Handler):
630
+ if hasattr(module, "Handler") and inspect.isclass(module.Handler):
638
631
  handler_class = module.Handler
639
632
  if issubclass(handler_class, BaseMLEngine):
640
- handler_meta['class_type'] = 'ml'
633
+ handler_meta["class_type"] = "ml"
641
634
  elif issubclass(handler_class, DatabaseHandler):
642
- handler_meta['class_type'] = 'sql'
635
+ handler_meta["class_type"] = "sql"
643
636
  if issubclass(handler_class, APIHandler):
644
- handler_meta['class_type'] = 'api'
637
+ handler_meta["class_type"] = "api"
645
638
 
646
639
  if handler_type == HANDLER_TYPE.ML:
647
640
  # for ml engines, patch the connection_args from the argument probing
648
641
  if handler_class:
649
642
  try:
650
643
  prediction_args = handler_class.prediction_args()
651
- creation_args = getattr(module, 'creation_args', handler_class.creation_args())
652
- connection_args = {
653
- "prediction": prediction_args,
654
- "creation_args": creation_args
655
- }
656
- setattr(module, 'connection_args', connection_args)
644
+ creation_args = getattr(module, "creation_args", handler_class.creation_args())
645
+ connection_args = {"prediction": prediction_args, "creation_args": creation_args}
646
+ setattr(module, "connection_args", connection_args)
657
647
  logger.debug("Patched connection_args for %s", handler_folder_name)
658
648
  except Exception as e:
659
649
  # do nothing
660
650
  logger.debug("Failed to patch connection_args for %s, reason: %s", handler_folder_name, str(e))
661
651
 
662
- module_attrs = [attr for attr in [
663
- 'connection_args_example',
664
- 'connection_args',
665
- 'description',
666
- 'type',
667
- 'title'
668
- ] if hasattr(module, attr)]
652
+ module_attrs = [
653
+ attr
654
+ for attr in ["connection_args_example", "connection_args", "description", "type", "title"]
655
+ if hasattr(module, attr)
656
+ ]
669
657
 
670
658
  for attr in module_attrs:
671
659
  handler_meta[attr] = getattr(module, attr)
672
660
 
673
661
  # endregion
674
- if hasattr(module, 'permanent'):
675
- handler_meta['permanent'] = module.permanent
662
+ if hasattr(module, "permanent"):
663
+ handler_meta["permanent"] = module.permanent
676
664
  else:
677
- if handler_meta.get('name') in ('files', 'views', 'lightwood'):
678
- handler_meta['permanent'] = True
665
+ if handler_meta.get("name") in ("files", "views", "lightwood"):
666
+ handler_meta["permanent"] = True
679
667
  else:
680
- handler_meta['permanent'] = False
668
+ handler_meta["permanent"] = False
681
669
 
682
670
  return handler_meta
683
671
 
@@ -685,60 +673,60 @@ class IntegrationController:
685
673
  icon = {}
686
674
  try:
687
675
  icon_path = handler_dir.joinpath(icon_path)
688
- icon_type = icon_path.name[icon_path.name.rfind('.') + 1:].lower()
676
+ icon_type = icon_path.name[icon_path.name.rfind(".") + 1 :].lower()
689
677
 
690
- if icon_type == 'svg':
691
- with open(str(icon_path), 'rt') as f:
692
- icon['data'] = f.read()
678
+ if icon_type == "svg":
679
+ with open(str(icon_path), "rt") as f:
680
+ icon["data"] = f.read()
693
681
  else:
694
- with open(str(icon_path), 'rb') as f:
695
- icon['data'] = base64.b64encode(f.read()).decode('utf-8')
682
+ with open(str(icon_path), "rb") as f:
683
+ icon["data"] = base64.b64encode(f.read()).decode("utf-8")
696
684
 
697
- icon['name'] = icon_path.name
698
- icon['type'] = icon_type
685
+ icon["name"] = icon_path.name
686
+ icon["type"] = icon_type
699
687
 
700
688
  except Exception as e:
701
- logger.error(f'Error reading icon for {handler_dir}, {e}!')
689
+ logger.error(f"Error reading icon for {handler_dir}, {e}!")
702
690
  return icon
703
691
 
704
692
  def _load_handler_modules(self):
705
- mindsdb_path = Path(importlib.util.find_spec('mindsdb').origin).parent
706
- handlers_path = mindsdb_path.joinpath('integrations/handlers')
693
+ mindsdb_path = Path(importlib.util.find_spec("mindsdb").origin).parent
694
+ handlers_path = mindsdb_path.joinpath("integrations/handlers")
707
695
 
708
696
  # edge case: running from tests directory, find_spec finds the base folder instead of actual package
709
697
  if not os.path.isdir(handlers_path):
710
- mindsdb_path = Path(importlib.util.find_spec('mindsdb').origin).parent.joinpath('mindsdb')
711
- handlers_path = mindsdb_path.joinpath('integrations/handlers')
698
+ mindsdb_path = Path(importlib.util.find_spec("mindsdb").origin).parent.joinpath("mindsdb")
699
+ handlers_path = mindsdb_path.joinpath("integrations/handlers")
712
700
 
713
701
  self.handler_modules = {}
714
702
  self.handlers_import_status = {}
715
703
  for handler_dir in handlers_path.iterdir():
716
- if handler_dir.is_dir() is False or handler_dir.name.startswith('__'):
704
+ if handler_dir.is_dir() is False or handler_dir.name.startswith("__"):
717
705
  continue
718
706
 
719
707
  handler_info = self._get_handler_info(handler_dir)
720
- if 'name' not in handler_info:
708
+ if "name" not in handler_info:
721
709
  continue
722
- handler_name = handler_info['name']
710
+ handler_name = handler_info["name"]
723
711
  dependencies = self._read_dependencies(handler_dir)
724
712
  handler_meta = {
725
- 'path': handler_dir,
726
- 'import': {
727
- 'success': None,
728
- 'error_message': None,
729
- 'folder': handler_dir.name,
730
- 'dependencies': dependencies,
713
+ "path": handler_dir,
714
+ "import": {
715
+ "success": None,
716
+ "error_message": None,
717
+ "folder": handler_dir.name,
718
+ "dependencies": dependencies,
731
719
  },
732
- 'name': handler_name,
733
- 'permanent': handler_info.get('permanent', False),
734
- 'connection_args': handler_info.get('connection_args', None),
735
- 'class_type': handler_info.get('class_type', None),
736
- 'type': handler_info.get('type')
720
+ "name": handler_name,
721
+ "permanent": handler_info.get("permanent", False),
722
+ "connection_args": handler_info.get("connection_args", None),
723
+ "class_type": handler_info.get("class_type", None),
724
+ "type": handler_info.get("type"),
737
725
  }
738
- if 'icon_path' in handler_info:
739
- icon = self._get_handler_icon(handler_dir, handler_info['icon_path'])
726
+ if "icon_path" in handler_info:
727
+ icon = self._get_handler_icon(handler_dir, handler_info["icon_path"])
740
728
  if icon:
741
- handler_meta['icon'] = icon
729
+ handler_meta["icon"] = icon
742
730
  self.handlers_import_status[handler_name] = handler_meta
743
731
 
744
732
  def _get_connection_args(self, args_file: Path, param_name: str) -> dict:
@@ -758,7 +746,7 @@ class IntegrationController:
758
746
  continue
759
747
  if not item.targets[0].id == param_name:
760
748
  continue
761
- if hasattr(item.value, 'keywords'):
749
+ if hasattr(item.value, "keywords"):
762
750
  for keyword in item.value.keywords:
763
751
  name = keyword.arg
764
752
  params = keyword.value
@@ -802,7 +790,7 @@ class IntegrationController:
802
790
  if module_file is None:
803
791
  return
804
792
 
805
- path = handler_dir / f'{module_file}.py'
793
+ path = handler_dir / f"{module_file}.py"
806
794
 
807
795
  if not path.exists():
808
796
  return
@@ -812,9 +800,9 @@ class IntegrationController:
812
800
  for item in code.body:
813
801
  if isinstance(item, ast.ClassDef):
814
802
  bases = [base.id for base in item.bases]
815
- if 'APIHandler' in bases:
816
- return 'api'
817
- return 'sql'
803
+ if "APIHandler" in bases or "MetaAPIHandler" in bases:
804
+ return "api"
805
+ return "sql"
818
806
 
819
807
  def _get_handler_info(self, handler_dir: Path) -> dict:
820
808
  """
@@ -825,7 +813,7 @@ class IntegrationController:
825
813
  - connection arguments
826
814
  """
827
815
 
828
- init_file = handler_dir / '__init__.py'
816
+ init_file = handler_dir / "__init__.py"
829
817
  if not init_file.exists():
830
818
  return {}
831
819
  code = ast.parse(init_file.read_text())
@@ -838,47 +826,47 @@ class IntegrationController:
838
826
  name = item.targets[0].id
839
827
  if isinstance(item.value, ast.Constant):
840
828
  info[name] = item.value.value
841
- if isinstance(item.value, ast.Attribute) and name == 'type':
842
- if item.value.attr == 'ML':
829
+ if isinstance(item.value, ast.Attribute) and name == "type":
830
+ if item.value.attr == "ML":
843
831
  info[name] = HANDLER_TYPE.ML
844
- info['class_type'] = 'ml'
832
+ info["class_type"] = "ml"
845
833
  else:
846
834
  info[name] = HANDLER_TYPE.DATA
847
- info['class_type'] = self._get_base_class_type(code, handler_dir) or 'sql'
835
+ info["class_type"] = self._get_base_class_type(code, handler_dir) or "sql"
848
836
 
849
837
  # connection args
850
- if info['type'] == HANDLER_TYPE.ML:
851
- args_file = handler_dir / 'creation_args.py'
838
+ if info["type"] == HANDLER_TYPE.ML:
839
+ args_file = handler_dir / "creation_args.py"
852
840
  if args_file.exists():
853
- info['connection_args'] = {
841
+ info["connection_args"] = {
854
842
  "prediction": {},
855
- "creation_args": self._get_connection_args(args_file, 'creation_args')
843
+ "creation_args": self._get_connection_args(args_file, "creation_args"),
856
844
  }
857
845
  else:
858
- args_file = handler_dir / 'connection_args.py'
846
+ args_file = handler_dir / "connection_args.py"
859
847
  if args_file.exists():
860
- info['connection_args'] = self._get_connection_args(args_file, 'connection_args')
848
+ info["connection_args"] = self._get_connection_args(args_file, "connection_args")
861
849
 
862
850
  return info
863
851
 
864
852
  def import_handler(self, handler_name: str, base_import: str = None):
865
853
  with self._import_lock:
866
854
  handler_meta = self.handlers_import_status[handler_name]
867
- handler_dir = handler_meta['path']
855
+ handler_dir = handler_meta["path"]
868
856
 
869
857
  handler_folder_name = str(handler_dir.name)
870
858
  if base_import is None:
871
- base_import = 'mindsdb.integrations.handlers.'
859
+ base_import = "mindsdb.integrations.handlers."
872
860
 
873
861
  try:
874
- handler_module = importlib.import_module(f'{base_import}{handler_folder_name}')
862
+ handler_module = importlib.import_module(f"{base_import}{handler_folder_name}")
875
863
  self.handler_modules[handler_name] = handler_module
876
864
  handler_meta = self._get_handler_meta(handler_name)
877
865
  except Exception as e:
878
- handler_meta['import']['success'] = False
879
- handler_meta['import']['error_message'] = str(e)
866
+ handler_meta["import"]["success"] = False
867
+ handler_meta["import"]["error_message"] = str(e)
880
868
 
881
- self.handlers_import_status[handler_meta['name']] = handler_meta
869
+ self.handlers_import_status[handler_meta["name"]] = handler_meta
882
870
  return handler_meta
883
871
 
884
872
  def get_handlers_import_status(self):