MindsDB 25.7.3.0__py3-none-any.whl → 25.8.2.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 (102) hide show
  1. mindsdb/__about__.py +1 -1
  2. mindsdb/__main__.py +11 -1
  3. mindsdb/api/a2a/common/server/server.py +16 -6
  4. mindsdb/api/executor/command_executor.py +215 -150
  5. mindsdb/api/executor/datahub/datanodes/project_datanode.py +14 -3
  6. mindsdb/api/executor/planner/plan_join.py +3 -0
  7. mindsdb/api/executor/planner/plan_join_ts.py +117 -100
  8. mindsdb/api/executor/planner/query_planner.py +1 -0
  9. mindsdb/api/executor/sql_query/steps/apply_predictor_step.py +54 -85
  10. mindsdb/api/executor/sql_query/steps/fetch_dataframe.py +21 -24
  11. mindsdb/api/executor/sql_query/steps/fetch_dataframe_partition.py +9 -3
  12. mindsdb/api/executor/sql_query/steps/subselect_step.py +11 -8
  13. mindsdb/api/executor/utilities/mysql_to_duckdb_functions.py +264 -0
  14. mindsdb/api/executor/utilities/sql.py +30 -0
  15. mindsdb/api/http/initialize.py +18 -44
  16. mindsdb/api/http/namespaces/agents.py +23 -20
  17. mindsdb/api/http/namespaces/chatbots.py +83 -120
  18. mindsdb/api/http/namespaces/file.py +1 -1
  19. mindsdb/api/http/namespaces/jobs.py +38 -60
  20. mindsdb/api/http/namespaces/tree.py +69 -61
  21. mindsdb/api/http/namespaces/views.py +56 -72
  22. mindsdb/api/mcp/start.py +2 -0
  23. mindsdb/api/mysql/mysql_proxy/utilities/dump.py +3 -2
  24. mindsdb/integrations/handlers/autogluon_handler/requirements.txt +1 -1
  25. mindsdb/integrations/handlers/autosklearn_handler/requirements.txt +1 -1
  26. mindsdb/integrations/handlers/bigquery_handler/bigquery_handler.py +25 -5
  27. mindsdb/integrations/handlers/chromadb_handler/chromadb_handler.py +3 -3
  28. mindsdb/integrations/handlers/db2_handler/db2_handler.py +19 -23
  29. mindsdb/integrations/handlers/flaml_handler/requirements.txt +1 -1
  30. mindsdb/integrations/handlers/gong_handler/__about__.py +2 -0
  31. mindsdb/integrations/handlers/gong_handler/__init__.py +30 -0
  32. mindsdb/integrations/handlers/gong_handler/connection_args.py +37 -0
  33. mindsdb/integrations/handlers/gong_handler/gong_handler.py +164 -0
  34. mindsdb/integrations/handlers/gong_handler/gong_tables.py +508 -0
  35. mindsdb/integrations/handlers/gong_handler/icon.svg +25 -0
  36. mindsdb/integrations/handlers/gong_handler/test_gong_handler.py +125 -0
  37. mindsdb/integrations/handlers/google_calendar_handler/google_calendar_tables.py +82 -73
  38. mindsdb/integrations/handlers/hubspot_handler/requirements.txt +1 -1
  39. mindsdb/integrations/handlers/huggingface_handler/__init__.py +8 -12
  40. mindsdb/integrations/handlers/huggingface_handler/finetune.py +203 -223
  41. mindsdb/integrations/handlers/huggingface_handler/huggingface_handler.py +360 -383
  42. mindsdb/integrations/handlers/huggingface_handler/requirements.txt +7 -7
  43. mindsdb/integrations/handlers/huggingface_handler/requirements_cpu.txt +7 -7
  44. mindsdb/integrations/handlers/huggingface_handler/settings.py +25 -25
  45. mindsdb/integrations/handlers/langchain_handler/langchain_handler.py +83 -77
  46. mindsdb/integrations/handlers/lightwood_handler/requirements.txt +4 -4
  47. mindsdb/integrations/handlers/litellm_handler/litellm_handler.py +5 -2
  48. mindsdb/integrations/handlers/litellm_handler/settings.py +2 -1
  49. mindsdb/integrations/handlers/openai_handler/constants.py +11 -30
  50. mindsdb/integrations/handlers/openai_handler/helpers.py +27 -34
  51. mindsdb/integrations/handlers/openai_handler/openai_handler.py +14 -12
  52. mindsdb/integrations/handlers/pgvector_handler/pgvector_handler.py +106 -90
  53. mindsdb/integrations/handlers/postgres_handler/postgres_handler.py +41 -39
  54. mindsdb/integrations/handlers/salesforce_handler/constants.py +215 -0
  55. mindsdb/integrations/handlers/salesforce_handler/salesforce_handler.py +141 -80
  56. mindsdb/integrations/handlers/salesforce_handler/salesforce_tables.py +0 -1
  57. mindsdb/integrations/handlers/tpot_handler/requirements.txt +1 -1
  58. mindsdb/integrations/handlers/web_handler/urlcrawl_helpers.py +32 -17
  59. mindsdb/integrations/handlers/web_handler/web_handler.py +19 -22
  60. mindsdb/integrations/libs/llm/config.py +0 -14
  61. mindsdb/integrations/libs/llm/utils.py +0 -15
  62. mindsdb/integrations/libs/vectordatabase_handler.py +10 -1
  63. mindsdb/integrations/utilities/files/file_reader.py +5 -19
  64. mindsdb/integrations/utilities/handler_utils.py +32 -12
  65. mindsdb/integrations/utilities/rag/rerankers/base_reranker.py +1 -1
  66. mindsdb/interfaces/agents/agents_controller.py +246 -149
  67. mindsdb/interfaces/agents/constants.py +0 -1
  68. mindsdb/interfaces/agents/langchain_agent.py +11 -6
  69. mindsdb/interfaces/data_catalog/data_catalog_loader.py +4 -4
  70. mindsdb/interfaces/database/database.py +38 -13
  71. mindsdb/interfaces/database/integrations.py +20 -5
  72. mindsdb/interfaces/database/projects.py +174 -23
  73. mindsdb/interfaces/database/views.py +86 -60
  74. mindsdb/interfaces/jobs/jobs_controller.py +103 -110
  75. mindsdb/interfaces/knowledge_base/controller.py +33 -6
  76. mindsdb/interfaces/knowledge_base/evaluate.py +2 -1
  77. mindsdb/interfaces/knowledge_base/executor.py +24 -0
  78. mindsdb/interfaces/knowledge_base/preprocessing/document_preprocessor.py +6 -10
  79. mindsdb/interfaces/knowledge_base/preprocessing/text_splitter.py +73 -0
  80. mindsdb/interfaces/query_context/context_controller.py +111 -145
  81. mindsdb/interfaces/skills/skills_controller.py +18 -6
  82. mindsdb/interfaces/storage/db.py +40 -6
  83. mindsdb/interfaces/variables/variables_controller.py +8 -15
  84. mindsdb/utilities/config.py +5 -3
  85. mindsdb/utilities/fs.py +54 -17
  86. mindsdb/utilities/functions.py +72 -60
  87. mindsdb/utilities/log.py +38 -6
  88. mindsdb/utilities/ps.py +7 -7
  89. {mindsdb-25.7.3.0.dist-info → mindsdb-25.8.2.0.dist-info}/METADATA +282 -268
  90. {mindsdb-25.7.3.0.dist-info → mindsdb-25.8.2.0.dist-info}/RECORD +94 -92
  91. mindsdb/integrations/handlers/anyscale_endpoints_handler/__about__.py +0 -9
  92. mindsdb/integrations/handlers/anyscale_endpoints_handler/__init__.py +0 -20
  93. mindsdb/integrations/handlers/anyscale_endpoints_handler/anyscale_endpoints_handler.py +0 -290
  94. mindsdb/integrations/handlers/anyscale_endpoints_handler/creation_args.py +0 -14
  95. mindsdb/integrations/handlers/anyscale_endpoints_handler/icon.svg +0 -4
  96. mindsdb/integrations/handlers/anyscale_endpoints_handler/requirements.txt +0 -2
  97. mindsdb/integrations/handlers/anyscale_endpoints_handler/settings.py +0 -51
  98. mindsdb/integrations/handlers/anyscale_endpoints_handler/tests/test_anyscale_endpoints_handler.py +0 -212
  99. /mindsdb/integrations/handlers/{anyscale_endpoints_handler/tests/__init__.py → gong_handler/requirements.txt} +0 -0
  100. {mindsdb-25.7.3.0.dist-info → mindsdb-25.8.2.0.dist-info}/WHEEL +0 -0
  101. {mindsdb-25.7.3.0.dist-info → mindsdb-25.8.2.0.dist-info}/licenses/LICENSE +0 -0
  102. {mindsdb-25.7.3.0.dist-info → mindsdb-25.8.2.0.dist-info}/top_level.txt +0 -0
@@ -23,13 +23,16 @@ class SkillsController:
23
23
  project_controller = ProjectController()
24
24
  self.project_controller = project_controller
25
25
 
26
- def get_skill(self, skill_name: str, project_name: str = default_project) -> Optional[db.Skills]:
26
+ def get_skill(
27
+ self, skill_name: str, project_name: str = default_project, strict_case: bool = False
28
+ ) -> Optional[db.Skills]:
27
29
  """
28
30
  Gets a skill by name. Skills are expected to have unique names.
29
31
 
30
32
  Parameters:
31
33
  skill_name (str): The name of the skill
32
34
  project_name (str): The name of the containing project
35
+ strict_case (bool): If True, the skill name is case-sensitive. Defaults to False.
33
36
 
34
37
  Returns:
35
38
  skill (Optional[db.Skills]): The database skill object
@@ -39,11 +42,16 @@ class SkillsController:
39
42
  """
40
43
 
41
44
  project = self.project_controller.get(name=project_name)
42
- return db.Skills.query.filter(
43
- func.lower(db.Skills.name) == func.lower(skill_name),
45
+ query = db.Skills.query.filter(
44
46
  db.Skills.project_id == project.id,
45
47
  db.Skills.deleted_at == null(),
46
- ).first()
48
+ )
49
+ if strict_case:
50
+ query = query.filter(db.Skills.name == skill_name)
51
+ else:
52
+ query = query.filter(func.lower(db.Skills.name) == func.lower(skill_name))
53
+
54
+ return query.first()
47
55
 
48
56
  def get_skills(self, project_name: Optional[str]) -> List[dict]:
49
57
  """
@@ -92,6 +100,9 @@ class SkillsController:
92
100
  project_name = default_project
93
101
  project = self.project_controller.get(name=project_name)
94
102
 
103
+ if not name.islower():
104
+ raise ValueError(f"The name must be in lower case: {name}")
105
+
95
106
  skill = self.get_skill(name, project_name)
96
107
 
97
108
  if skill is not None:
@@ -158,19 +169,20 @@ class SkillsController:
158
169
 
159
170
  return existing_skill
160
171
 
161
- def delete_skill(self, skill_name: str, project_name: str = default_project):
172
+ def delete_skill(self, skill_name: str, project_name: str = default_project, strict_case: bool = False):
162
173
  """
163
174
  Deletes a skill by name.
164
175
 
165
176
  Parameters:
166
177
  skill_name (str): The name of the skill to delete
167
178
  project_name (str): The name of the containing project
179
+ strict_case (bool): If true, then skill_name is case sensitive
168
180
 
169
181
  Raises:
170
182
  ValueError: If `project_name` does not exist or skill doesn't exist
171
183
  """
172
184
 
173
- skill = self.get_skill(skill_name, project_name)
185
+ skill = self.get_skill(skill_name, project_name, strict_case)
174
186
  if skill is None:
175
187
  raise ValueError(f"Skill with name doesn't exist: {skill_name}")
176
188
  if isinstance(skill.params, dict) and skill.params.get("is_demo") is True:
@@ -448,19 +448,53 @@ class Agents(Base):
448
448
  deleted_at = Column(DateTime)
449
449
 
450
450
  def as_dict(self) -> Dict:
451
- return {
451
+ skills = []
452
+ skills_extra_parameters = {}
453
+ for rel in self.skills_relationships:
454
+ skill = rel.skill
455
+ # Skip auto-generated SQL skills
456
+ if skill.params.get("description", "").startswith("Auto-generated SQL skill for agent"):
457
+ continue
458
+ skills.append(skill.as_dict())
459
+ skills_extra_parameters[skill.name] = rel.parameters or {}
460
+
461
+ params = self.params.copy()
462
+
463
+ agent_dict = {
452
464
  "id": self.id,
453
465
  "name": self.name,
454
466
  "project_id": self.project_id,
455
- "model_name": self.model_name,
456
- "skills": [rel.skill.as_dict() for rel in self.skills_relationships],
457
- "skills_extra_parameters": {rel.skill.name: (rel.parameters or {}) for rel in self.skills_relationships},
458
- "provider": self.provider,
459
- "params": self.params,
460
467
  "updated_at": self.updated_at,
461
468
  "created_at": self.created_at,
462
469
  }
463
470
 
471
+ if self.model_name:
472
+ agent_dict["model_name"] = self.model_name
473
+
474
+ if self.provider:
475
+ agent_dict["provider"] = self.provider
476
+
477
+ # Since skills were depreciated, they are only used with Minds
478
+ # Minds expects the parameters to be provided as is without breaking them down
479
+ if skills:
480
+ agent_dict["skills"] = skills
481
+ agent_dict["skills_extra_parameters"] = skills_extra_parameters
482
+ agent_dict["params"] = params
483
+ else:
484
+ data = params.pop("data", {})
485
+ model = params.pop("model", {})
486
+ prompt_template = params.pop("prompt_template", None)
487
+ if data:
488
+ agent_dict["data"] = data
489
+ if model:
490
+ agent_dict["model"] = model
491
+ if prompt_template:
492
+ agent_dict["prompt_template"] = prompt_template
493
+ if params:
494
+ agent_dict["params"] = params
495
+
496
+ return agent_dict
497
+
464
498
 
465
499
  class KnowledgeBase(Base):
466
500
  __tablename__ = "knowledge_base"
@@ -15,13 +15,9 @@ ENV_VAR_PREFIX = "MDB_"
15
15
 
16
16
 
17
17
  class VariablesController:
18
-
19
18
  def __init__(self) -> None:
20
- self._storage = get_json_storage(
21
- resource_id=0,
22
- resource_group=RESOURCE_GROUP.SYSTEM
23
- )
24
- self._store_key = 'variables'
19
+ self._storage = get_json_storage(resource_id=0, resource_group=RESOURCE_GROUP.SYSTEM)
20
+ self._store_key = "variables"
25
21
  self._data = None
26
22
 
27
23
  def _get_data(self) -> dict:
@@ -54,7 +50,7 @@ class VariablesController:
54
50
  return os.environ[var_name]
55
51
 
56
52
  def _get_function(self, name: str) -> Callable:
57
- if name == 'from_env':
53
+ if name == "from_env":
58
54
  return self._from_env
59
55
  raise ValueError(f"Function {name} is not found")
60
56
 
@@ -81,16 +77,13 @@ class VariablesController:
81
77
 
82
78
  if isinstance(var, Variable):
83
79
  return self.get_value(var.value.lower())
80
+ if isinstance(var, Function):
81
+ fnc = self._get_function(var.op)
82
+ return fnc(*var.args)
84
83
  elif isinstance(var, dict):
85
- return {
86
- key: self.fill_parameters(value)
87
- for key, value in var.items()
88
- }
84
+ return {key: self.fill_parameters(value) for key, value in var.items()}
89
85
  elif isinstance(var, list):
90
- return [
91
- self.fill_parameters(value)
92
- for value in var
93
- ]
86
+ return [self.fill_parameters(value) for value in var]
94
87
  return var
95
88
 
96
89
 
@@ -318,7 +318,7 @@ class Config:
318
318
  self._env_config["logging"]["handlers"]["console"]["level"] = os.environ["MINDSDB_LOG_LEVEL"]
319
319
  self._env_config["logging"]["handlers"]["console"]["enabled"] = True
320
320
  if os.environ.get("MINDSDB_CONSOLE_LOG_LEVEL", "") != "":
321
- self._env_config["logging"]["handlers"]["console"]["level"] = os.environ["MINDSDB_LOG_LEVEL"]
321
+ self._env_config["logging"]["handlers"]["console"]["level"] = os.environ["MINDSDB_CONSOLE_LOG_LEVEL"]
322
322
  self._env_config["logging"]["handlers"]["console"]["enabled"] = True
323
323
  if os.environ.get("MINDSDB_FILE_LOG_LEVEL", "") != "":
324
324
  self._env_config["logging"]["handlers"]["file"]["level"] = os.environ["MINDSDB_FILE_LOG_LEVEL"]
@@ -459,8 +459,8 @@ class Config:
459
459
  """Merge multiple configs to one."""
460
460
  new_config = deepcopy(self._default_config)
461
461
  _merge_configs(new_config, self._user_config)
462
- _merge_configs(new_config, self._auto_config)
463
- _merge_configs(new_config, self._env_config)
462
+ _merge_configs(new_config, self._auto_config or {})
463
+ _merge_configs(new_config, self._env_config or {})
464
464
 
465
465
  # Apply command-line arguments for A2A
466
466
  a2a_config = {}
@@ -599,6 +599,7 @@ class Config:
599
599
  ml_task_queue_consumer=None,
600
600
  agent=None,
601
601
  project=None,
602
+ update_gui=False,
602
603
  )
603
604
  return
604
605
 
@@ -635,6 +636,7 @@ class Config:
635
636
  help="MindsDB agent name to connect to",
636
637
  )
637
638
  parser.add_argument("--project-name", type=str, default=None, help="MindsDB project name")
639
+ parser.add_argument("--update-gui", action="store_true", default=False, help="Update GUI and exit")
638
640
 
639
641
  self._cmd_args = parser.parse_args()
640
642
 
mindsdb/utilities/fs.py CHANGED
@@ -12,6 +12,10 @@ from mindsdb.utilities import log
12
12
  logger = log.getLogger(__name__)
13
13
 
14
14
 
15
+ def get_tmp_dir() -> Path:
16
+ return Path(tempfile.gettempdir()).joinpath("mindsdb")
17
+
18
+
15
19
  def _get_process_mark_id(unified: bool = False) -> str:
16
20
  """Creates a text that can be used to identify process+thread
17
21
  Args:
@@ -26,7 +30,7 @@ def _get_process_mark_id(unified: bool = False) -> str:
26
30
 
27
31
 
28
32
  def create_process_mark(folder="learn"):
29
- p = Path(tempfile.gettempdir()).joinpath(f"mindsdb/processes/{folder}/")
33
+ p = get_tmp_dir().joinpath(f"processes/{folder}/")
30
34
  p.mkdir(parents=True, exist_ok=True)
31
35
  mark = _get_process_mark_id()
32
36
  p.joinpath(mark).touch()
@@ -43,7 +47,7 @@ def set_process_mark(folder: str, mark: str) -> None:
43
47
  Returns:
44
48
  str: process mark
45
49
  """
46
- p = Path(tempfile.gettempdir()).joinpath(f"mindsdb/processes/{folder}/")
50
+ p = get_tmp_dir().joinpath(f"processes/{folder}/")
47
51
  p.mkdir(parents=True, exist_ok=True)
48
52
  mark = f"{os.getpid()}-{threading.get_native_id()}-{mark}"
49
53
  p.joinpath(mark).touch()
@@ -53,11 +57,7 @@ def set_process_mark(folder: str, mark: str) -> None:
53
57
  def delete_process_mark(folder: str = "learn", mark: Optional[str] = None):
54
58
  if mark is None:
55
59
  mark = _get_process_mark_id()
56
- p = (
57
- Path(tempfile.gettempdir())
58
- .joinpath(f"mindsdb/processes/{folder}/")
59
- .joinpath(mark)
60
- )
60
+ p = get_tmp_dir().joinpath(f"processes/{folder}/").joinpath(mark)
61
61
  if p.exists():
62
62
  p.unlink()
63
63
 
@@ -65,7 +65,7 @@ def delete_process_mark(folder: str = "learn", mark: Optional[str] = None):
65
65
  def clean_process_marks():
66
66
  """delete all existing processes marks"""
67
67
  logger.debug("Deleting PIDs..")
68
- p = Path(tempfile.gettempdir()).joinpath("mindsdb/processes/")
68
+ p = get_tmp_dir().joinpath("processes/")
69
69
  if p.exists() is False:
70
70
  return
71
71
  for path in p.iterdir():
@@ -81,7 +81,7 @@ def get_processes_dir_files_generator() -> Tuple[Path, int, int]:
81
81
  Yields:
82
82
  Tuple[Path, int, int]: file object, process is and thread id
83
83
  """
84
- p = Path(tempfile.gettempdir()).joinpath("mindsdb/processes/")
84
+ p = get_tmp_dir().joinpath("processes/")
85
85
  if p.exists() is False:
86
86
  return
87
87
  for path in p.iterdir():
@@ -112,9 +112,7 @@ def clean_unlinked_process_marks() -> List[int]:
112
112
  try:
113
113
  next(t for t in threads if t.id == thread_id)
114
114
  except StopIteration:
115
- logger.warning(
116
- f"We have mark for process/thread {process_id}/{thread_id} but it does not exists"
117
- )
115
+ logger.warning(f"We have mark for process/thread {process_id}/{thread_id} but it does not exists")
118
116
  deleted_pids.append(process_id)
119
117
  file.unlink()
120
118
 
@@ -124,14 +122,53 @@ def clean_unlinked_process_marks() -> List[int]:
124
122
  continue
125
123
 
126
124
  except psutil.NoSuchProcess:
127
- logger.warning(
128
- f"We have mark for process/thread {process_id}/{thread_id} but it does not exists"
129
- )
125
+ logger.warning(f"We have mark for process/thread {process_id}/{thread_id} but it does not exists")
130
126
  deleted_pids.append(process_id)
131
127
  file.unlink()
132
128
  return deleted_pids
133
129
 
134
130
 
131
+ def create_pid_file():
132
+ """
133
+ Create mindsdb process pid file. Check if previous process exists and is running
134
+ """
135
+
136
+ p = get_tmp_dir()
137
+ p.mkdir(parents=True, exist_ok=True)
138
+ pid_file = p.joinpath("pid")
139
+ if pid_file.exists():
140
+ # if process exists raise exception
141
+ pid = pid_file.read_text().strip()
142
+ try:
143
+ psutil.Process(int(pid))
144
+ raise Exception(f"Found PID file with existing process: {pid}")
145
+ except (psutil.Error, ValueError):
146
+ ...
147
+
148
+ logger.warning(f"Found existing PID file ({pid}), removing")
149
+ pid_file.unlink()
150
+
151
+ pid_file.write_text(str(os.getpid()))
152
+
153
+
154
+ def delete_pid_file():
155
+ """
156
+ Remove existing process pid file if it matches current process
157
+ """
158
+ pid_file = get_tmp_dir().joinpath("pid")
159
+
160
+ if not pid_file.exists():
161
+ logger.warning("Mindsdb PID file does not exist")
162
+ return
163
+
164
+ pid = pid_file.read_text().strip()
165
+ if pid != str(os.getpid()):
166
+ logger.warning("Process id in PID file doesn't match mindsdb pid")
167
+ return
168
+
169
+ pid_file.unlink()
170
+
171
+
135
172
  def __is_within_directory(directory, target):
136
173
  abs_directory = os.path.abspath(directory)
137
174
  abs_target = os.path.abspath(target)
@@ -141,8 +178,8 @@ def __is_within_directory(directory, target):
141
178
 
142
179
  def safe_extract(tarfile, path=".", members=None, *, numeric_owner=False):
143
180
  # for py >= 3.12
144
- if hasattr(tarfile, 'data_filter'):
145
- tarfile.extractall(path, members=members, numeric_owner=numeric_owner, filter='data')
181
+ if hasattr(tarfile, "data_filter"):
182
+ tarfile.extractall(path, members=members, numeric_owner=numeric_owner, filter="data")
146
183
  return
147
184
 
148
185
  # for py < 3.12
@@ -35,20 +35,19 @@ def get_handler_install_message(handler_name):
35
35
 
36
36
 
37
37
  def cast_row_types(row, field_types):
38
- '''
39
- '''
38
+ """ """
40
39
  keys = [x for x in row.keys() if x in field_types]
41
40
  for key in keys:
42
41
  t = field_types[key]
43
- if t == 'Timestamp' and isinstance(row[key], (int, float)):
44
- timestamp = datetime.datetime.utcfromtimestamp(row[key])
45
- row[key] = timestamp.strftime('%Y-%m-%d %H:%M:%S')
46
- elif t == 'Date' and isinstance(row[key], (int, float)):
47
- timestamp = datetime.datetime.utcfromtimestamp(row[key])
48
- row[key] = timestamp.strftime('%Y-%m-%d')
49
- elif t == 'Int' and isinstance(row[key], (int, float, str)):
42
+ if t == "Timestamp" and isinstance(row[key], (int, float)):
43
+ timestamp = datetime.datetime.fromtimestamp(row[key], datetime.timezone.utc)
44
+ row[key] = timestamp.strftime("%Y-%m-%d %H:%M:%S")
45
+ elif t == "Date" and isinstance(row[key], (int, float)):
46
+ timestamp = datetime.datetime.fromtimestamp(row[key], datetime.timezone.utc)
47
+ row[key] = timestamp.strftime("%Y-%m-%d")
48
+ elif t == "Int" and isinstance(row[key], (int, float, str)):
50
49
  try:
51
- logger.debug(f'cast {row[key]} to {int(row[key])}')
50
+ logger.debug(f"cast {row[key]} to {int(row[key])}")
52
51
  row[key] = int(row[key])
53
52
  except Exception:
54
53
  pass
@@ -67,13 +66,16 @@ def mark_process(name: str, custom_mark: str = None) -> Callable:
67
66
  return func(*args, **kwargs)
68
67
  finally:
69
68
  delete_process_mark(name, mark)
69
+
70
70
  return wrapper
71
+
71
72
  return mark_process_wrapper
72
73
 
73
74
 
74
75
  def init_lexer_parsers():
75
76
  from mindsdb_sql_parser.lexer import MindsDBLexer
76
77
  from mindsdb_sql_parser.parser import MindsDBParser
78
+
77
79
  return MindsDBLexer(), MindsDBParser()
78
80
 
79
81
 
@@ -86,62 +88,72 @@ def resolve_table_identifier(identifier: Identifier, default_database: str = Non
86
88
  elif parts_count == 2:
87
89
  return (parts[0], parts[1])
88
90
  else:
89
- raise Exception(f'Table identifier must contain max 2 parts: {parts}')
91
+ raise Exception(f"Table identifier must contain max 2 parts: {parts}")
90
92
 
91
93
 
92
94
  def resolve_model_identifier(identifier: Identifier) -> tuple:
93
- """ split model name to parts
94
-
95
- Identifier may be:
96
-
97
- Examples:
98
- >>> resolve_model_identifier(['a', 'b'])
99
- ('a', 'b', None)
100
-
101
- >>> resolve_model_identifier(['a', '1'])
102
- (None, 'a', 1)
103
-
104
- >>> resolve_model_identifier(['a'])
105
- (None, 'a', None)
106
-
107
- >>> resolve_model_identifier(['a', 'b', 'c'])
108
- (None, None, None) # not found
109
-
110
- Args:
111
- name (Identifier): Identifier parts
112
-
113
- Returns:
114
- tuple: (database_name, model_name, model_version)
115
95
  """
116
- parts = identifier.parts
117
- database_name = None
96
+ Splits a model identifier into its database, model name, and version components.
97
+
98
+ The identifier may contain one, two, or three parts.
99
+ The function supports both quoted and unquoted identifiers, and normalizes names to lowercase if unquoted.
100
+
101
+ Examples:
102
+ >>> resolve_model_identifier(Identifier(parts=['a', 'b']))
103
+ ('a', 'b', None)
104
+ >>> resolve_model_identifier(Identifier(parts=['a', '1']))
105
+ (None, 'a', 1)
106
+ >>> resolve_model_identifier(Identifier(parts=['a']))
107
+ (None, 'a', None)
108
+ >>> resolve_model_identifier(Identifier(parts=['a', 'b', 'c']))
109
+ (None, None, None) # not found
110
+
111
+ Args:
112
+ identifier (Identifier): The identifier object containing parts and is_quoted attributes.
113
+
114
+ Returns:
115
+ tuple: (database_name, model_name, model_version)
116
+ - database_name (str or None): The name of the database/project, or None if not specified.
117
+ - model_name (str or None): The name of the model, or None if not found.
118
+ - model_version (int or None): The model version as an integer, or None if not specified.
119
+ """
118
120
  model_name = None
119
- model_version = None
121
+ db_name = None
122
+ version = None
123
+ model_name_quoted = None
124
+ db_name_quoted = None
125
+
126
+ match identifier.parts, identifier.is_quoted:
127
+ case [model_name], [model_name_quoted]:
128
+ ...
129
+ case [model_name, str(version)], [model_name_quoted, _] if version.isdigit():
130
+ ...
131
+ case [model_name, int(version)], [model_name_quoted, _]:
132
+ ...
133
+ case [db_name, model_name], [db_name_quoted, model_name_quoted]:
134
+ ...
135
+ case [db_name, model_name, str(version)], [db_name_quoted, model_name_quoted, _] if version.isdigit():
136
+ ...
137
+ case [db_name, model_name, int(version)], [db_name_quoted, model_name_quoted, _]:
138
+ ...
139
+ case [db_name, model_name, str(version)], [db_name_quoted, model_name_quoted, _]:
140
+ # for back compatibility. May be delete?
141
+ return (None, None, None)
142
+ case _:
143
+ ... # may be raise ValueError?
144
+
145
+ if model_name_quoted is False:
146
+ model_name = model_name.lower()
147
+
148
+ if db_name_quoted is False:
149
+ db_name = db_name.lower()
150
+
151
+ if isinstance(version, int) or isinstance(version, str) and version.isdigit():
152
+ version = int(version)
153
+ else:
154
+ version = None
120
155
 
121
- parts_count = len(parts)
122
- if parts_count == 1:
123
- database_name = None
124
- model_name = parts[0]
125
- model_version = None
126
- elif parts_count == 2:
127
- if parts[-1].isdigit():
128
- database_name = None
129
- model_name = parts[0]
130
- model_version = int(parts[-1])
131
- else:
132
- database_name = parts[0]
133
- model_name = parts[1]
134
- model_version = None
135
- elif parts_count == 3:
136
- database_name = parts[0]
137
- model_name = parts[1]
138
- if parts[2].isdigit():
139
- model_version = int(parts[2])
140
- else:
141
- # not found
142
- return None, None, None
143
-
144
- return database_name, model_name, model_version
156
+ return db_name, model_name, version
145
157
 
146
158
 
147
159
  def encrypt(string: bytes, key: str) -> bytes:
mindsdb/utilities/log.py CHANGED
@@ -43,6 +43,13 @@ class ColorFormatter(logging.Formatter):
43
43
  return log_fmt.format(record)
44
44
 
45
45
 
46
+ FORMATTERS = {
47
+ "default": {"()": ColorFormatter},
48
+ "json": {"()": JsonFormatter},
49
+ "file": {"format": "%(asctime)s %(processName)15s %(levelname)-8s %(name)s: %(message)s"},
50
+ }
51
+
52
+
46
53
  def get_console_handler_config_level() -> int:
47
54
  console_handler_config = app_config["logging"]["handlers"]["console"]
48
55
  return getattr(logging, console_handler_config["level"])
@@ -60,7 +67,7 @@ def get_mindsdb_log_level() -> int:
60
67
  return min(console_handler_config_level, file_handler_config_level)
61
68
 
62
69
 
63
- def configure_logging(process_name: str = None):
70
+ def get_handlers_config(process_name: str) -> dict:
64
71
  handlers_config = {}
65
72
  console_handler_config = app_config["logging"]["handlers"]["console"]
66
73
  console_handler_config_level = getattr(logging, console_handler_config["level"])
@@ -89,16 +96,41 @@ def configure_logging(process_name: str = None):
89
96
  "maxBytes": file_handler_config["maxBytes"], # 0.5 Mb
90
97
  "backupCount": file_handler_config["backupCount"],
91
98
  }
99
+ return handlers_config
100
+
101
+
102
+ def get_uvicorn_logging_config(process_name: str) -> dict:
103
+ """Generate a logging configuration dictionary for Uvicorn using MindsDB's logging settings.
104
+
105
+ Args:
106
+ process_name (str): The name of the process to include in log file names and handlers.
107
+
108
+ Returns:
109
+ dict: A dictionary suitable for use with logging.config.dictConfig, configured for Uvicorn logging.
110
+ """
111
+ handlers_config = get_handlers_config(process_name)
112
+ mindsdb_log_level = get_mindsdb_log_level()
113
+ return {
114
+ "version": 1,
115
+ "formatters": FORMATTERS,
116
+ "handlers": handlers_config,
117
+ "loggers": {
118
+ "uvicorn": {
119
+ "handlers": list(handlers_config.keys()),
120
+ "level": mindsdb_log_level,
121
+ "propagate": False,
122
+ }
123
+ },
124
+ }
125
+
92
126
 
127
+ def configure_logging(process_name: str = None):
128
+ handlers_config = get_handlers_config(process_name)
93
129
  mindsdb_log_level = get_mindsdb_log_level()
94
130
 
95
131
  logging_config = dict(
96
132
  version=1,
97
- formatters={
98
- "default": {"()": ColorFormatter},
99
- "json": {"()": JsonFormatter},
100
- "file": {"format": "%(asctime)s %(processName)15s %(levelname)-8s %(name)s: %(message)s"},
101
- },
133
+ formatters=FORMATTERS,
102
134
  handlers=handlers_config,
103
135
  loggers={
104
136
  "": { # root logger
mindsdb/utilities/ps.py CHANGED
@@ -11,23 +11,23 @@ def get_child_pids(pid):
11
11
 
12
12
  def net_connections():
13
13
  """Cross-platform psutil.net_connections like interface"""
14
- if sys.platform.lower().startswith('linux'):
14
+ if sys.platform.lower().startswith("linux"):
15
15
  return psutil.net_connections()
16
16
 
17
17
  all_connections = []
18
18
  Pconn = None
19
- for p in psutil.process_iter(['pid']):
19
+ for p in psutil.process_iter(["pid"]):
20
20
  try:
21
21
  process = psutil.Process(p.pid)
22
- connections = process.connections()
22
+ connections = process.net_connections()
23
23
  if connections:
24
24
  for conn in connections:
25
25
  # Adding pid to the returned instance
26
26
  # for consistency with psutil.net_connections()
27
27
  if Pconn is None:
28
28
  fields = list(conn._fields)
29
- fields.append('pid')
30
- _conn = namedtuple('Pconn', fields)
29
+ fields.append("pid")
30
+ _conn = namedtuple("Pconn", fields)
31
31
  for attr in conn._fields:
32
32
  setattr(_conn, attr, getattr(conn, attr))
33
33
  _conn.pid = p.pid
@@ -43,7 +43,7 @@ def is_port_in_use(port_num):
43
43
  parent_process = psutil.Process()
44
44
  child_pids = [x.pid for x in parent_process.children(recursive=True)]
45
45
  conns = net_connections()
46
- portsinuse = [x.laddr[1] for x in conns if x.pid in child_pids and x.status == 'LISTEN']
46
+ portsinuse = [x.laddr[1] for x in conns if x.pid in child_pids and x.status == "LISTEN"]
47
47
  portsinuse.sort()
48
48
  return int(port_num) in portsinuse
49
49
 
@@ -66,7 +66,7 @@ def wait_port(port_num, timeout):
66
66
  def get_listen_ports(pid):
67
67
  try:
68
68
  p = psutil.Process(pid)
69
- cons = p.connections()
69
+ cons = p.net_connections()
70
70
  cons = [x.laddr.port for x in cons]
71
71
  except Exception:
72
72
  return []