mlrun 1.10.0rc13__py3-none-any.whl → 1.10.0rc42__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 mlrun might be problematic. Click here for more details.

Files changed (107) hide show
  1. mlrun/__init__.py +22 -2
  2. mlrun/artifacts/base.py +0 -31
  3. mlrun/artifacts/document.py +6 -1
  4. mlrun/artifacts/llm_prompt.py +123 -25
  5. mlrun/artifacts/manager.py +0 -5
  6. mlrun/artifacts/model.py +3 -3
  7. mlrun/common/constants.py +10 -1
  8. mlrun/common/formatters/artifact.py +1 -0
  9. mlrun/common/model_monitoring/helpers.py +86 -0
  10. mlrun/common/schemas/__init__.py +3 -0
  11. mlrun/common/schemas/auth.py +2 -0
  12. mlrun/common/schemas/function.py +10 -0
  13. mlrun/common/schemas/hub.py +30 -18
  14. mlrun/common/schemas/model_monitoring/__init__.py +3 -0
  15. mlrun/common/schemas/model_monitoring/constants.py +30 -6
  16. mlrun/common/schemas/model_monitoring/functions.py +14 -5
  17. mlrun/common/schemas/model_monitoring/model_endpoints.py +21 -0
  18. mlrun/common/schemas/pipeline.py +1 -1
  19. mlrun/common/schemas/serving.py +3 -0
  20. mlrun/common/schemas/workflow.py +3 -1
  21. mlrun/common/secrets.py +22 -1
  22. mlrun/config.py +33 -11
  23. mlrun/datastore/__init__.py +11 -3
  24. mlrun/datastore/azure_blob.py +162 -47
  25. mlrun/datastore/datastore.py +9 -4
  26. mlrun/datastore/datastore_profile.py +61 -5
  27. mlrun/datastore/model_provider/huggingface_provider.py +363 -0
  28. mlrun/datastore/model_provider/mock_model_provider.py +87 -0
  29. mlrun/datastore/model_provider/model_provider.py +230 -65
  30. mlrun/datastore/model_provider/openai_provider.py +295 -42
  31. mlrun/datastore/s3.py +24 -2
  32. mlrun/datastore/storeytargets.py +2 -3
  33. mlrun/datastore/utils.py +15 -3
  34. mlrun/db/base.py +47 -19
  35. mlrun/db/httpdb.py +120 -56
  36. mlrun/db/nopdb.py +38 -10
  37. mlrun/execution.py +70 -19
  38. mlrun/hub/__init__.py +15 -0
  39. mlrun/hub/module.py +181 -0
  40. mlrun/k8s_utils.py +105 -16
  41. mlrun/launcher/base.py +13 -6
  42. mlrun/launcher/local.py +15 -0
  43. mlrun/model.py +24 -3
  44. mlrun/model_monitoring/__init__.py +1 -0
  45. mlrun/model_monitoring/api.py +66 -27
  46. mlrun/model_monitoring/applications/__init__.py +1 -1
  47. mlrun/model_monitoring/applications/base.py +509 -117
  48. mlrun/model_monitoring/applications/context.py +2 -4
  49. mlrun/model_monitoring/applications/results.py +4 -7
  50. mlrun/model_monitoring/controller.py +239 -101
  51. mlrun/model_monitoring/db/_schedules.py +116 -33
  52. mlrun/model_monitoring/db/_stats.py +4 -3
  53. mlrun/model_monitoring/db/tsdb/base.py +100 -9
  54. mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +11 -6
  55. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +191 -50
  56. mlrun/model_monitoring/db/tsdb/tdengine/writer_graph_steps.py +51 -0
  57. mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +17 -4
  58. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +259 -40
  59. mlrun/model_monitoring/helpers.py +54 -9
  60. mlrun/model_monitoring/stream_processing.py +45 -14
  61. mlrun/model_monitoring/writer.py +220 -1
  62. mlrun/platforms/__init__.py +3 -2
  63. mlrun/platforms/iguazio.py +7 -3
  64. mlrun/projects/operations.py +6 -1
  65. mlrun/projects/pipelines.py +46 -26
  66. mlrun/projects/project.py +166 -58
  67. mlrun/run.py +94 -17
  68. mlrun/runtimes/__init__.py +18 -0
  69. mlrun/runtimes/base.py +14 -6
  70. mlrun/runtimes/daskjob.py +7 -0
  71. mlrun/runtimes/local.py +5 -2
  72. mlrun/runtimes/mounts.py +20 -2
  73. mlrun/runtimes/mpijob/abstract.py +6 -0
  74. mlrun/runtimes/mpijob/v1.py +6 -0
  75. mlrun/runtimes/nuclio/__init__.py +1 -0
  76. mlrun/runtimes/nuclio/application/application.py +149 -17
  77. mlrun/runtimes/nuclio/function.py +76 -27
  78. mlrun/runtimes/nuclio/serving.py +97 -15
  79. mlrun/runtimes/pod.py +234 -21
  80. mlrun/runtimes/remotesparkjob.py +6 -0
  81. mlrun/runtimes/sparkjob/spark3job.py +6 -0
  82. mlrun/runtimes/utils.py +49 -11
  83. mlrun/secrets.py +54 -13
  84. mlrun/serving/__init__.py +2 -0
  85. mlrun/serving/remote.py +79 -6
  86. mlrun/serving/routers.py +23 -41
  87. mlrun/serving/server.py +320 -80
  88. mlrun/serving/states.py +725 -157
  89. mlrun/serving/steps.py +62 -0
  90. mlrun/serving/system_steps.py +200 -119
  91. mlrun/serving/v2_serving.py +9 -10
  92. mlrun/utils/helpers.py +288 -88
  93. mlrun/utils/logger.py +3 -1
  94. mlrun/utils/notifications/notification/base.py +18 -0
  95. mlrun/utils/notifications/notification/git.py +2 -4
  96. mlrun/utils/notifications/notification/slack.py +2 -4
  97. mlrun/utils/notifications/notification/webhook.py +2 -5
  98. mlrun/utils/notifications/notification_pusher.py +1 -1
  99. mlrun/utils/retryer.py +15 -2
  100. mlrun/utils/version/version.json +2 -2
  101. {mlrun-1.10.0rc13.dist-info → mlrun-1.10.0rc42.dist-info}/METADATA +45 -51
  102. {mlrun-1.10.0rc13.dist-info → mlrun-1.10.0rc42.dist-info}/RECORD +106 -101
  103. mlrun/api/schemas/__init__.py +0 -259
  104. {mlrun-1.10.0rc13.dist-info → mlrun-1.10.0rc42.dist-info}/WHEEL +0 -0
  105. {mlrun-1.10.0rc13.dist-info → mlrun-1.10.0rc42.dist-info}/entry_points.txt +0 -0
  106. {mlrun-1.10.0rc13.dist-info → mlrun-1.10.0rc42.dist-info}/licenses/LICENSE +0 -0
  107. {mlrun-1.10.0rc13.dist-info → mlrun-1.10.0rc42.dist-info}/top_level.txt +0 -0
@@ -14,7 +14,7 @@
14
14
 
15
15
  import threading
16
16
  from datetime import datetime, timedelta
17
- from typing import Callable, Final, Literal, Optional, Union
17
+ from typing import Final, Literal, Optional, Union
18
18
 
19
19
  import pandas as pd
20
20
  import taosws
@@ -22,7 +22,7 @@ import taosws
22
22
  import mlrun.common.schemas.model_monitoring as mm_schemas
23
23
  import mlrun.common.types
24
24
  import mlrun.model_monitoring.db.tsdb.tdengine.schemas as tdengine_schemas
25
- import mlrun.model_monitoring.db.tsdb.tdengine.stream_graph_steps
25
+ from mlrun.config import config
26
26
  from mlrun.datastore.datastore_profile import DatastoreProfile
27
27
  from mlrun.model_monitoring.db import TSDBConnector
28
28
  from mlrun.model_monitoring.db.tsdb.tdengine.tdengine_connection import (
@@ -55,14 +55,12 @@ class TDEngineConnector(TSDBConnector):
55
55
  """
56
56
 
57
57
  type: str = mm_schemas.TSDBTarget.TDEngine
58
- database = f"{tdengine_schemas._MODEL_MONITORING_DATABASE}_{mlrun.mlconf.system_id}"
59
58
 
60
59
  def __init__(
61
60
  self,
62
61
  project: str,
63
62
  profile: DatastoreProfile,
64
63
  timestamp_precision: TDEngineTimestampPrecision = TDEngineTimestampPrecision.MICROSECOND,
65
- **kwargs,
66
64
  ):
67
65
  super().__init__(project=project)
68
66
 
@@ -72,6 +70,15 @@ class TDEngineConnector(TSDBConnector):
72
70
  timestamp_precision
73
71
  )
74
72
 
73
+ if not mlrun.mlconf.system_id:
74
+ raise mlrun.errors.MLRunInvalidArgumentError(
75
+ "system_id is not set in mlrun.mlconf. "
76
+ "TDEngineConnector requires system_id to be configured for database name construction. "
77
+ "Please ensure MLRun configuration is properly loaded before creating TDEngineConnector."
78
+ )
79
+ self.database = (
80
+ f"{tdengine_schemas._MODEL_MONITORING_DATABASE}_{mlrun.mlconf.system_id}"
81
+ )
75
82
  self._init_super_tables()
76
83
 
77
84
  @property
@@ -205,7 +212,7 @@ class TDEngineConnector(TSDBConnector):
205
212
  @staticmethod
206
213
  def _generate_filter_query(
207
214
  filter_column: str, filter_values: Union[str, list[Union[str, int]]]
208
- ) -> Optional[str]:
215
+ ) -> str:
209
216
  """
210
217
  Generate a filter query for TDEngine based on the provided column and values.
211
218
 
@@ -213,15 +220,14 @@ class TDEngineConnector(TSDBConnector):
213
220
  :param filter_values: A single value or a list of values to filter by.
214
221
 
215
222
  :return: A string representing the filter query.
216
- :raise: MLRunInvalidArgumentError if the filter values are not of type string or list.
223
+ :raise: ``MLRunValueError`` if the filter values are not of type string or list.
217
224
  """
218
-
219
225
  if isinstance(filter_values, str):
220
226
  return f"{filter_column}='{filter_values}'"
221
227
  elif isinstance(filter_values, list):
222
228
  return f"{filter_column} IN ({', '.join(repr(v) for v in filter_values)}) "
223
229
  else:
224
- raise mlrun.errors.MLRunInvalidArgumentError(
230
+ raise mlrun.errors.MLRunValueError(
225
231
  f"Invalid filter values {filter_values}: must be a string or a list, "
226
232
  f"got {type(filter_values).__name__}; filter values: {filter_values}"
227
233
  )
@@ -279,6 +285,65 @@ class TDEngineConnector(TSDBConnector):
279
285
  after="ProcessBeforeTDEngine",
280
286
  )
281
287
 
288
+ def add_pre_writer_steps(self, graph, after):
289
+ return graph.add_step(
290
+ "mlrun.model_monitoring.db.tsdb.tdengine.writer_graph_steps.ProcessBeforeTDEngine",
291
+ name="ProcessBeforeTDEngine",
292
+ after=after,
293
+ )
294
+
295
+ def apply_writer_steps(self, graph, after, **kwargs) -> None:
296
+ graph.add_step(
297
+ "mlrun.datastore.storeytargets.TDEngineStoreyTarget",
298
+ name="tsdb_metrics",
299
+ after=after,
300
+ url=f"ds://{self._tdengine_connection_profile.name}",
301
+ supertable=self.tables[mm_schemas.TDEngineSuperTables.METRICS].super_table,
302
+ table_col=mm_schemas.EventFieldType.TABLE_COLUMN,
303
+ time_col=mm_schemas.WriterEvent.END_INFER_TIME,
304
+ database=self.database,
305
+ graph_shape="cylinder",
306
+ columns=[
307
+ mm_schemas.WriterEvent.START_INFER_TIME,
308
+ mm_schemas.MetricData.METRIC_VALUE,
309
+ ],
310
+ tag_cols=[
311
+ mm_schemas.WriterEvent.ENDPOINT_ID,
312
+ mm_schemas.WriterEvent.APPLICATION_NAME,
313
+ mm_schemas.MetricData.METRIC_NAME,
314
+ ],
315
+ max_events=config.model_endpoint_monitoring.writer_graph.max_events,
316
+ flush_after_seconds=config.model_endpoint_monitoring.writer_graph.flush_after_seconds,
317
+ )
318
+
319
+ graph.add_step(
320
+ "mlrun.datastore.storeytargets.TDEngineStoreyTarget",
321
+ name="tsdb_app_results",
322
+ after=after,
323
+ url=f"ds://{self._tdengine_connection_profile.name}",
324
+ supertable=self.tables[
325
+ mm_schemas.TDEngineSuperTables.APP_RESULTS
326
+ ].super_table,
327
+ table_col=mm_schemas.EventFieldType.TABLE_COLUMN,
328
+ time_col=mm_schemas.WriterEvent.END_INFER_TIME,
329
+ database=self.database,
330
+ graph_shape="cylinder",
331
+ columns=[
332
+ mm_schemas.WriterEvent.START_INFER_TIME,
333
+ mm_schemas.ResultData.RESULT_VALUE,
334
+ mm_schemas.ResultData.RESULT_STATUS,
335
+ mm_schemas.ResultData.RESULT_EXTRA_DATA,
336
+ ],
337
+ tag_cols=[
338
+ mm_schemas.WriterEvent.ENDPOINT_ID,
339
+ mm_schemas.WriterEvent.APPLICATION_NAME,
340
+ mm_schemas.ResultData.RESULT_NAME,
341
+ mm_schemas.ResultData.RESULT_KIND,
342
+ ],
343
+ max_events=config.model_endpoint_monitoring.writer_graph.max_events,
344
+ flush_after_seconds=config.model_endpoint_monitoring.writer_graph.flush_after_seconds,
345
+ )
346
+
282
347
  def handle_model_error(
283
348
  self,
284
349
  graph,
@@ -311,10 +376,7 @@ class TDEngineConnector(TSDBConnector):
311
376
  flush_after_seconds=tsdb_batching_timeout_secs,
312
377
  )
313
378
 
314
- def delete_tsdb_records(
315
- self,
316
- endpoint_ids: list[str],
317
- ):
379
+ def delete_tsdb_records(self, endpoint_ids: list[str]) -> None:
318
380
  """
319
381
  To delete subtables within TDEngine, we first query the subtables names with the provided endpoint_ids.
320
382
  Then, we drop each subtable.
@@ -332,9 +394,7 @@ class TDEngineConnector(TSDBConnector):
332
394
  get_subtable_query = self.tables[table]._get_subtables_query_by_tag(
333
395
  filter_tag="endpoint_id", filter_values=endpoint_ids
334
396
  )
335
- subtables_result = self.connection.run(
336
- query=get_subtable_query,
337
- )
397
+ subtables_result = self.connection.run(query=get_subtable_query)
338
398
  subtables.extend([subtable[0] for subtable in subtables_result.data])
339
399
  except Exception as e:
340
400
  logger.warning(
@@ -346,15 +406,13 @@ class TDEngineConnector(TSDBConnector):
346
406
  )
347
407
 
348
408
  # Prepare the drop statements
349
- drop_statements = []
350
- for subtable in subtables:
351
- drop_statements.append(
352
- self.tables[table].drop_subtable_query(subtable=subtable)
353
- )
409
+ drop_statements = [
410
+ self.tables[table].drop_subtable_query(subtable=subtable)
411
+ for subtable in subtables
412
+ ]
354
413
  try:
355
- self.connection.run(
356
- statements=drop_statements,
357
- )
414
+ logger.debug("Dropping subtables", drop_statements=drop_statements)
415
+ self.connection.run(statements=drop_statements)
358
416
  except Exception as e:
359
417
  logger.warning(
360
418
  "Failed to delete model endpoint resources. You may need to delete them manually. "
@@ -369,6 +427,48 @@ class TDEngineConnector(TSDBConnector):
369
427
  number_of_endpoints_to_delete=len(endpoint_ids),
370
428
  )
371
429
 
430
+ def delete_application_records(
431
+ self, application_name: str, endpoint_ids: Optional[list[str]] = None
432
+ ) -> None:
433
+ """
434
+ Delete application records from the TSDB for the given model endpoints or all if ``endpoint_ids`` is ``None``.
435
+ """
436
+ logger.debug(
437
+ "Deleting application records",
438
+ project=self.project,
439
+ application_name=application_name,
440
+ endpoint_ids=endpoint_ids,
441
+ )
442
+ tables = [
443
+ self.tables[mm_schemas.TDEngineSuperTables.APP_RESULTS],
444
+ self.tables[mm_schemas.TDEngineSuperTables.METRICS],
445
+ ]
446
+
447
+ filter_query = self._generate_filter_query(
448
+ filter_column=mm_schemas.ApplicationEvent.APPLICATION_NAME,
449
+ filter_values=application_name,
450
+ )
451
+ if endpoint_ids:
452
+ endpoint_ids_filter = self._generate_filter_query(
453
+ filter_column=mm_schemas.EventFieldType.ENDPOINT_ID,
454
+ filter_values=endpoint_ids,
455
+ )
456
+ filter_query += f" AND {endpoint_ids_filter}"
457
+
458
+ drop_statements: list[str] = []
459
+ for table in tables:
460
+ get_subtable_query = table._get_tables_query_by_condition(filter_query)
461
+ subtables_result = self.connection.run(query=get_subtable_query)
462
+ drop_statements.extend(
463
+ [
464
+ table.drop_subtable_query(subtable=subtable[0])
465
+ for subtable in subtables_result.data
466
+ ]
467
+ )
468
+
469
+ logger.debug("Dropping application records", drop_statements=drop_statements)
470
+ self.connection.run(statements=drop_statements)
471
+
372
472
  def delete_tsdb_resources(self):
373
473
  """
374
474
  Delete all project resources in the TSDB connector, such as model endpoints data and drift results.
@@ -469,6 +569,7 @@ class TDEngineConnector(TSDBConnector):
469
569
  preform_agg_columns: Optional[list] = None,
470
570
  order_by: Optional[str] = None,
471
571
  desc: Optional[bool] = None,
572
+ partition_by: Optional[str] = None,
472
573
  ) -> pd.DataFrame:
473
574
  """
474
575
  Getting records from TSDB data collection.
@@ -496,6 +597,8 @@ class TDEngineConnector(TSDBConnector):
496
597
  if an empty list was provided The aggregation won't be performed.
497
598
  :param order_by: The column or alias to preform ordering on the query.
498
599
  :param desc: Whether or not to sort the results in descending order.
600
+ :param partition_by: The column to partition the results by. Note that if interval is provided,
601
+ `agg_funcs` must bg provided as well.
499
602
 
500
603
  :return: DataFrame with the provided attributes from the data collection.
501
604
  :raise: MLRunInvalidArgumentError if query the provided table failed.
@@ -517,6 +620,7 @@ class TDEngineConnector(TSDBConnector):
517
620
  preform_agg_funcs_columns=preform_agg_columns,
518
621
  order_by=order_by,
519
622
  desc=desc,
623
+ partition_by=partition_by,
520
624
  )
521
625
  logger.debug("Querying TDEngine", query=full_query)
522
626
  try:
@@ -684,7 +788,9 @@ class TDEngineConnector(TSDBConnector):
684
788
  endpoint_ids: Union[str, list[str]],
685
789
  start: Optional[datetime] = None,
686
790
  end: Optional[datetime] = None,
687
- ) -> pd.DataFrame:
791
+ ) -> Union[pd.DataFrame, dict[str, float]]:
792
+ if not endpoint_ids:
793
+ return {}
688
794
  filter_query = self._generate_filter_query(
689
795
  filter_column=mm_schemas.EventFieldType.ENDPOINT_ID,
690
796
  filter_values=endpoint_ids,
@@ -819,7 +925,7 @@ class TDEngineConnector(TSDBConnector):
819
925
  # Convert DataFrame to a dictionary
820
926
  return {
821
927
  (
822
- row[mm_schemas.WriterEvent.APPLICATION_NAME],
928
+ row[mm_schemas.WriterEvent.APPLICATION_NAME].lower(),
823
929
  row[mm_schemas.ResultData.RESULT_STATUS],
824
930
  ): row["count(result_value)"]
825
931
  for _, row in df.iterrows()
@@ -904,26 +1010,34 @@ class TDEngineConnector(TSDBConnector):
904
1010
  mm_schemas.WriterEvent.END_INFER_TIME,
905
1011
  mm_schemas.WriterEvent.APPLICATION_NAME,
906
1012
  ]
1013
+ agg_columns = [mm_schemas.WriterEvent.END_INFER_TIME]
1014
+ group_by_columns = [mm_schemas.WriterEvent.APPLICATION_NAME]
907
1015
  if record_type == "results":
908
1016
  table = self.tables[
909
1017
  mm_schemas.TDEngineSuperTables.APP_RESULTS
910
1018
  ].super_table
911
1019
  columns += [
912
1020
  mm_schemas.ResultData.RESULT_NAME,
1021
+ mm_schemas.ResultData.RESULT_KIND,
1022
+ mm_schemas.ResultData.RESULT_STATUS,
1023
+ mm_schemas.ResultData.RESULT_VALUE,
1024
+ ]
1025
+ agg_columns += [
913
1026
  mm_schemas.ResultData.RESULT_VALUE,
914
1027
  mm_schemas.ResultData.RESULT_STATUS,
915
1028
  mm_schemas.ResultData.RESULT_KIND,
916
1029
  ]
917
- agg_column = mm_schemas.ResultData.RESULT_VALUE
1030
+ group_by_columns += [mm_schemas.ResultData.RESULT_NAME]
918
1031
  else:
919
1032
  table = self.tables[mm_schemas.TDEngineSuperTables.METRICS].super_table
920
1033
  columns += [
921
1034
  mm_schemas.MetricData.METRIC_NAME,
922
1035
  mm_schemas.MetricData.METRIC_VALUE,
923
1036
  ]
924
- agg_column = mm_schemas.MetricData.METRIC_VALUE
1037
+ agg_columns += [mm_schemas.MetricData.METRIC_VALUE]
1038
+ group_by_columns += [mm_schemas.MetricData.METRIC_NAME]
925
1039
 
926
- return self._get_records(
1040
+ df = self._get_records(
927
1041
  table=table,
928
1042
  start=start,
929
1043
  end=end,
@@ -931,10 +1045,17 @@ class TDEngineConnector(TSDBConnector):
931
1045
  filter_query=filter_query,
932
1046
  timestamp_column=mm_schemas.WriterEvent.END_INFER_TIME,
933
1047
  # Aggregate per application/metric pair regardless of timestamp
934
- group_by=columns[1:],
935
- preform_agg_columns=[agg_column],
1048
+ group_by=group_by_columns,
1049
+ preform_agg_columns=agg_columns,
936
1050
  agg_funcs=["last"],
937
1051
  )
1052
+ if not df.empty:
1053
+ for column in agg_columns:
1054
+ df.rename(
1055
+ columns={f"last({column})": column},
1056
+ inplace=True,
1057
+ )
1058
+ return df
938
1059
 
939
1060
  df_results = get_latest_metrics_records(record_type="results")
940
1061
  df_metrics = get_latest_metrics_records(record_type="metrics")
@@ -951,19 +1072,14 @@ class TDEngineConnector(TSDBConnector):
951
1072
  ]
952
1073
  ):
953
1074
  metric_objects = []
954
-
955
1075
  if not df_results.empty:
956
- df_results.rename(
957
- columns={
958
- f"last({mm_schemas.ResultData.RESULT_VALUE})": mm_schemas.ResultData.RESULT_VALUE,
959
- },
960
- inplace=True,
961
- )
962
1076
  for _, row in df_results.iterrows():
963
1077
  metric_objects.append(
964
1078
  mm_schemas.ApplicationResultRecord(
965
1079
  time=datetime.fromisoformat(
966
- row[mm_schemas.WriterEvent.END_INFER_TIME]
1080
+ row[mm_schemas.WriterEvent.END_INFER_TIME].replace(
1081
+ " +", "+"
1082
+ )
967
1083
  ),
968
1084
  result_name=row[mm_schemas.ResultData.RESULT_NAME],
969
1085
  kind=row[mm_schemas.ResultData.RESULT_KIND],
@@ -973,17 +1089,13 @@ class TDEngineConnector(TSDBConnector):
973
1089
  )
974
1090
 
975
1091
  if not df_metrics.empty:
976
- df_metrics.rename(
977
- columns={
978
- f"last({mm_schemas.MetricData.METRIC_VALUE})": mm_schemas.MetricData.METRIC_VALUE,
979
- },
980
- inplace=True,
981
- )
982
1092
  for _, row in df_metrics.iterrows():
983
1093
  metric_objects.append(
984
1094
  mm_schemas.ApplicationMetricRecord(
985
1095
  time=datetime.fromisoformat(
986
- row[mm_schemas.WriterEvent.END_INFER_TIME]
1096
+ row[mm_schemas.WriterEvent.END_INFER_TIME].replace(
1097
+ " +", "+"
1098
+ )
987
1099
  ),
988
1100
  metric_name=row[mm_schemas.MetricData.METRIC_NAME],
989
1101
  value=row[mm_schemas.MetricData.METRIC_VALUE],
@@ -1142,11 +1254,9 @@ class TDEngineConnector(TSDBConnector):
1142
1254
  df.dropna(inplace=True)
1143
1255
  return df
1144
1256
 
1145
- async def add_basic_metrics(
1257
+ def add_basic_metrics(
1146
1258
  self,
1147
1259
  model_endpoint_objects: list[mlrun.common.schemas.ModelEndpoint],
1148
- project: str,
1149
- run_in_threadpool: Callable,
1150
1260
  metric_list: Optional[list[str]] = None,
1151
1261
  ) -> list[mlrun.common.schemas.ModelEndpoint]:
1152
1262
  """
@@ -1154,8 +1264,6 @@ class TDEngineConnector(TSDBConnector):
1154
1264
 
1155
1265
  :param model_endpoint_objects: A list of `ModelEndpoint` objects that will
1156
1266
  be filled with the relevant basic metrics.
1157
- :param project: The name of the project.
1158
- :param run_in_threadpool: A function that runs another function in a thread pool.
1159
1267
  :param metric_list: List of metrics to include from the time series DB. Defaults to all metrics.
1160
1268
 
1161
1269
  :return: A list of `ModelEndpointMonitoringMetric` objects.
@@ -1205,6 +1313,39 @@ class TDEngineConnector(TSDBConnector):
1205
1313
  )
1206
1314
  )
1207
1315
 
1316
+ def get_drift_data(
1317
+ self,
1318
+ start: datetime,
1319
+ end: datetime,
1320
+ ) -> mm_schemas.ModelEndpointDriftValues:
1321
+ filter_query = self._generate_filter_query(
1322
+ filter_column=mm_schemas.ResultData.RESULT_STATUS,
1323
+ filter_values=[
1324
+ mm_schemas.ResultStatusApp.potential_detection.value,
1325
+ mm_schemas.ResultStatusApp.detected.value,
1326
+ ],
1327
+ )
1328
+ table = self.tables[mm_schemas.TDEngineSuperTables.APP_RESULTS].super_table
1329
+ start, end, interval = self._prepare_aligned_start_end(start, end)
1330
+
1331
+ # get per time-interval x endpoint_id combination the max result status
1332
+ df = self._get_records(
1333
+ table=table,
1334
+ start=start,
1335
+ end=end,
1336
+ interval=interval,
1337
+ columns=[mm_schemas.ResultData.RESULT_STATUS],
1338
+ filter_query=filter_query,
1339
+ timestamp_column=mm_schemas.WriterEvent.END_INFER_TIME,
1340
+ agg_funcs=["max"],
1341
+ partition_by=mm_schemas.WriterEvent.ENDPOINT_ID,
1342
+ )
1343
+ if df.empty:
1344
+ return mm_schemas.ModelEndpointDriftValues(values=[])
1345
+
1346
+ df["_wstart"] = pd.to_datetime(df["_wstart"])
1347
+ return self._df_to_drift_data(df)
1348
+
1208
1349
  # Note: this function serves as a reference for checking the TSDB for the existence of a metric.
1209
1350
  #
1210
1351
  # def read_prediction_metric_for_endpoint_if_exists(
@@ -0,0 +1,51 @@
1
+ # Copyright 2025 Iguazio
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from datetime import datetime
16
+
17
+ import mlrun.common.schemas.model_monitoring as mm_schemas
18
+ import mlrun.feature_store.steps
19
+ from mlrun.utils import logger
20
+
21
+
22
+ class ProcessBeforeTDEngine(mlrun.feature_store.steps.MapClass):
23
+ def __init__(self, **kwargs):
24
+ """
25
+ Process the data before writing to TDEngine. This step create the table name.
26
+
27
+ :returns: Event as a dictionary which will be written into the TDEngine Metrics/Results tables.
28
+ """
29
+ super().__init__(**kwargs)
30
+
31
+ def do(self, event):
32
+ logger.info("Process event before writing to TDEngine", event=event)
33
+ kind = event.get("kind")
34
+ table_name = (
35
+ f"{event[mm_schemas.WriterEvent.ENDPOINT_ID]}_"
36
+ f"{event[mm_schemas.WriterEvent.APPLICATION_NAME]}"
37
+ )
38
+ if kind == mm_schemas.WriterEventKind.RESULT:
39
+ # Write a new result
40
+ event[mm_schemas.EventFieldType.TABLE_COLUMN] = (
41
+ f"{table_name}_{event[mm_schemas.ResultData.RESULT_NAME]}"
42
+ ).replace("-", "_")
43
+ elif kind == mm_schemas.WriterEventKind.METRIC:
44
+ # Write a new metric
45
+ event[mm_schemas.EventFieldType.TABLE_COLUMN] = (
46
+ f"{table_name}_{event[mm_schemas.MetricData.METRIC_NAME]}"
47
+ ).replace("-", "_")
48
+ event[mm_schemas.WriterEvent.START_INFER_TIME] = datetime.fromisoformat(
49
+ event[mm_schemas.WriterEvent.START_INFER_TIME]
50
+ )
51
+ return event
@@ -25,10 +25,12 @@ from mlrun.utils import logger
25
25
 
26
26
  def _normalize_dict_for_v3io_frames(event: dict[str, Any]) -> dict[str, Any]:
27
27
  """
28
- Normalize user defined keys - input data to a model and its predictions,
29
- to a form V3IO frames tolerates.
28
+ Normalize user-defined keys (e.g., model input data and predictions) to a format V3IO Frames tolerates.
30
29
 
31
- The dictionary keys should conform to '^[a-zA-Z_:]([a-zA-Z0-9_:])*$'.
30
+ - Keys must match regex: '^[a-zA-Z_:]([a-zA-Z0-9_:])*$'
31
+ - Replace invalid characters (e.g., '-') with '_'.
32
+ - Prefix keys starting with digits with '_'.
33
+ - Flatten nested dictionaries using dot notation, while normalizing keys recursively.
32
34
  """
33
35
  prefix = "_"
34
36
 
@@ -38,7 +40,18 @@ def _normalize_dict_for_v3io_frames(event: dict[str, Any]) -> dict[str, Any]:
38
40
  return prefix + key
39
41
  return key
40
42
 
41
- return {norm_key(k): v for k, v in event.items()}
43
+ def flatten_dict(d: dict[str, Any], parent_key: str = "") -> dict[str, Any]:
44
+ items = {}
45
+ for k, v in d.items():
46
+ new_key = norm_key(k)
47
+ full_key = f"{parent_key}.{new_key}" if parent_key else new_key
48
+ if isinstance(v, dict):
49
+ items.update(flatten_dict(v, full_key))
50
+ else:
51
+ items[full_key] = v
52
+ return items
53
+
54
+ return flatten_dict(event)
42
55
 
43
56
 
44
57
  class ProcessBeforeTSDB(mlrun.feature_store.steps.MapClass):