mlrun 1.8.0rc19__py3-none-any.whl → 1.8.0rc20__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.

mlrun/__main__.py CHANGED
@@ -32,6 +32,7 @@ from tabulate import tabulate
32
32
  import mlrun
33
33
  import mlrun.common.constants as mlrun_constants
34
34
  import mlrun.common.schemas
35
+ import mlrun.utils.helpers
35
36
  from mlrun.common.helpers import parse_versioned_object_uri
36
37
  from mlrun.runtimes.mounts import auto_mount as auto_mount_modifier
37
38
 
@@ -304,6 +305,7 @@ def run(
304
305
  update_in(runtime, "spec.build.code_origin", url_file)
305
306
  elif runtime:
306
307
  runtime = py_eval(runtime)
308
+ runtime = mlrun.utils.helpers.as_dict(runtime)
307
309
  if not isinstance(runtime, dict):
308
310
  print(f"Runtime parameter must be a dict, not {type(runtime)}")
309
311
  exit(1)
@@ -515,6 +517,7 @@ def build(
515
517
 
516
518
  if runtime:
517
519
  runtime = py_eval(runtime)
520
+ runtime = mlrun.utils.helpers.as_dict(runtime)
518
521
  if not isinstance(runtime, dict):
519
522
  print(f"Runtime parameter must be a dict, not {type(runtime)}")
520
523
  exit(1)
@@ -662,6 +665,8 @@ def deploy(
662
665
  runtime = py_eval(spec)
663
666
  else:
664
667
  runtime = {}
668
+
669
+ runtime = mlrun.utils.helpers.as_dict(runtime)
665
670
  if not isinstance(runtime, dict):
666
671
  print(f"Runtime parameter must be a dict, not {type(runtime)}")
667
672
  exit(1)
@@ -214,6 +214,23 @@ class RunStates:
214
214
  RunStates.skipped: mlrun_pipelines.common.models.RunStatuses.skipped,
215
215
  }[run_state]
216
216
 
217
+ @staticmethod
218
+ def pipeline_run_status_to_run_state(pipeline_run_status):
219
+ if pipeline_run_status not in mlrun_pipelines.common.models.RunStatuses.all():
220
+ raise ValueError(f"Invalid pipeline run status: {pipeline_run_status}")
221
+ return {
222
+ mlrun_pipelines.common.models.RunStatuses.succeeded: RunStates.completed,
223
+ mlrun_pipelines.common.models.RunStatuses.failed: RunStates.error,
224
+ mlrun_pipelines.common.models.RunStatuses.running: RunStates.running,
225
+ mlrun_pipelines.common.models.RunStatuses.pending: RunStates.pending,
226
+ mlrun_pipelines.common.models.RunStatuses.canceled: RunStates.aborted,
227
+ mlrun_pipelines.common.models.RunStatuses.canceling: RunStates.aborting,
228
+ mlrun_pipelines.common.models.RunStatuses.skipped: RunStates.skipped,
229
+ mlrun_pipelines.common.models.RunStatuses.runtime_state_unspecified: RunStates.unknown,
230
+ mlrun_pipelines.common.models.RunStatuses.error: RunStates.error,
231
+ mlrun_pipelines.common.models.RunStatuses.paused: RunStates.unknown,
232
+ }[pipeline_run_status]
233
+
217
234
 
218
235
  # TODO: remove this class in 1.9.0 - use only MlrunInternalLabels
219
236
  class RunLabels(enum.Enum):
@@ -61,6 +61,7 @@ class ModelEndpointSchema(MonitoringStrEnum):
61
61
  STATE = "state"
62
62
  MONITORING_MODE = "monitoring_mode"
63
63
  FIRST_REQUEST = "first_request"
64
+ SAMPLING_PERCENTAGE = "sampling_percentage"
64
65
 
65
66
  # status - operative
66
67
  LAST_REQUEST = "last_request"
@@ -137,6 +138,10 @@ class EventFieldType:
137
138
  SAMPLE_PARQUET_PATH = "sample_parquet_path"
138
139
  TIME = "time"
139
140
  TABLE_COLUMN = "table_column"
141
+ SAMPLING_PERCENTAGE = "sampling_percentage"
142
+ SAMPLING_RATE = "sampling_rate"
143
+ ESTIMATED_PREDICTION_COUNT = "estimated_prediction_count"
144
+ EFFECTIVE_SAMPLE_COUNT = "effective_sample_count"
140
145
 
141
146
 
142
147
  class FeatureSetFeatures(MonitoringStrEnum):
@@ -160,6 +160,7 @@ class ModelEndpointStatus(ObjectStatus, ModelEndpointParser):
160
160
  state: Optional[str] = "unknown" # will be updated according to the function state
161
161
  first_request: Optional[datetime] = None
162
162
  monitoring_mode: Optional[ModelMonitoringMode] = ModelMonitoringMode.disabled
163
+ sampling_percentage: Optional[float] = 100
163
164
 
164
165
  # operative
165
166
  last_request: Optional[datetime] = None
@@ -177,6 +178,7 @@ class ModelEndpointStatus(ObjectStatus, ModelEndpointParser):
177
178
  "monitoring_mode",
178
179
  "first_request",
179
180
  "last_request",
181
+ "sampling_percentage",
180
182
  ]
181
183
 
182
184
 
mlrun/db/base.py CHANGED
@@ -68,6 +68,15 @@ class RunDBInterface(ABC):
68
68
  ):
69
69
  pass
70
70
 
71
+ def push_pipeline_notifications(
72
+ self,
73
+ pipeline_id,
74
+ project="",
75
+ notifications=None,
76
+ timeout=45,
77
+ ):
78
+ pass
79
+
71
80
  @abstractmethod
72
81
  def read_run(
73
82
  self,
mlrun/db/httpdb.py CHANGED
@@ -780,9 +780,84 @@ class HTTPRunDB(RunDBInterface):
780
780
  )
781
781
  if response.status_code == http.HTTPStatus.ACCEPTED:
782
782
  background_task = mlrun.common.schemas.BackgroundTask(**response.json())
783
- return self._wait_for_background_task_to_reach_terminal_state(
783
+ background_task = self._wait_for_background_task_to_reach_terminal_state(
784
784
  background_task.metadata.name, project=project
785
785
  )
786
+ if (
787
+ background_task.status.state
788
+ == mlrun.common.schemas.BackgroundTaskState.succeeded
789
+ ):
790
+ logger.info(
791
+ "Notifications for the run have been pushed",
792
+ project=project,
793
+ run_id=uid,
794
+ )
795
+ elif (
796
+ background_task.status.state
797
+ == mlrun.common.schemas.BackgroundTaskState.failed
798
+ ):
799
+ logger.error(
800
+ "Failed to push run notifications",
801
+ project=project,
802
+ run_id=uid,
803
+ error=background_task.status.error,
804
+ )
805
+ return None
806
+
807
+ def push_pipeline_notifications(
808
+ self,
809
+ pipeline_id,
810
+ project="",
811
+ notifications=None,
812
+ timeout=45,
813
+ ):
814
+ """
815
+ Push notifications for a pipeline.
816
+
817
+ :param pipeline_id: Unique ID of the pipeline(KFP).
818
+ :param project: Project that the run belongs to.
819
+ :param notifications: List of notifications to push.
820
+ :returns: :py:class:`~mlrun.common.schemas.BackgroundTask`.
821
+ """
822
+ if notifications is None or type(notifications) is not list:
823
+ raise MLRunInvalidArgumentError(
824
+ "The 'notifications' parameter must be a list."
825
+ )
826
+
827
+ project = project or config.default_project
828
+
829
+ response = self.api_call(
830
+ "POST",
831
+ path=f"projects/{project}/pipelines/{pipeline_id}/push-notifications",
832
+ error="Failed push notifications",
833
+ body=_as_json([notification.to_dict() for notification in notifications]),
834
+ timeout=timeout,
835
+ )
836
+ if response.status_code == http.HTTPStatus.ACCEPTED:
837
+ background_task = mlrun.common.schemas.BackgroundTask(**response.json())
838
+ background_task = self._wait_for_background_task_to_reach_terminal_state(
839
+ background_task.metadata.name, project=project
840
+ )
841
+ if (
842
+ background_task.status.state
843
+ == mlrun.common.schemas.BackgroundTaskState.succeeded
844
+ ):
845
+ logger.info(
846
+ "Pipeline notifications have been pushed",
847
+ project=project,
848
+ pipeline_id=pipeline_id,
849
+ )
850
+ elif (
851
+ background_task.status.state
852
+ == mlrun.common.schemas.BackgroundTaskState.failed
853
+ ):
854
+ logger.error(
855
+ "Failed to push pipeline notifications",
856
+ project=project,
857
+ pipeline_id=pipeline_id,
858
+ error=background_task.status.error,
859
+ )
860
+
786
861
  return None
787
862
 
788
863
  def read_run(
mlrun/db/nopdb.py CHANGED
@@ -84,6 +84,15 @@ class NopDB(RunDBInterface):
84
84
  ):
85
85
  pass
86
86
 
87
+ def push_pipeline_notifications(
88
+ self,
89
+ pipeline_id,
90
+ project="",
91
+ notifications=None,
92
+ timeout=45,
93
+ ):
94
+ pass
95
+
87
96
  def list_runtime_resources(
88
97
  self,
89
98
  project: Optional[str] = None,
@@ -976,7 +976,6 @@ class ModelHandler(ABC, Generic[CommonTypes.ModelType, CommonTypes.IOSampleType]
976
976
  custom_objects_map_json,
977
977
  local_path=custom_objects_map_json,
978
978
  artifact_path=self._context.artifact_path,
979
- db_key=False,
980
979
  )
981
980
 
982
981
  # Zip the custom objects directory:
@@ -997,7 +996,6 @@ class ModelHandler(ABC, Generic[CommonTypes.ModelType, CommonTypes.IOSampleType]
997
996
  custom_objects_zip,
998
997
  local_path=custom_objects_zip,
999
998
  artifact_path=self._context.artifact_path,
1000
- db_key=False,
1001
999
  )
1002
1000
 
1003
1001
  return artifacts
@@ -298,6 +298,8 @@ class Predictions(TDEngineSchema):
298
298
  mm_schemas.EventFieldType.TIME: _TDEngineColumn.TIMESTAMP,
299
299
  mm_schemas.EventFieldType.LATENCY: _TDEngineColumn.FLOAT,
300
300
  mm_schemas.EventKeyMetrics.CUSTOM_METRICS: _TDEngineColumn.BINARY_1000,
301
+ mm_schemas.EventFieldType.ESTIMATED_PREDICTION_COUNT: _TDEngineColumn.FLOAT,
302
+ mm_schemas.EventFieldType.EFFECTIVE_SAMPLE_COUNT: _TDEngineColumn.INT,
301
303
  }
302
304
  tags = {
303
305
  mm_schemas.WriterEvent.ENDPOINT_ID: _TDEngineColumn.BINARY_64,
@@ -165,7 +165,7 @@ class TDEngineConnector(TSDBConnector):
165
165
  return datetime.fromisoformat(val) if isinstance(val, str) else val
166
166
 
167
167
  @staticmethod
168
- def _get_endpoint_filter(endpoint_id: typing.Union[str, list[str]]):
168
+ def _get_endpoint_filter(endpoint_id: typing.Union[str, list[str]]) -> str:
169
169
  if isinstance(endpoint_id, str):
170
170
  return f"endpoint_id='{endpoint_id}'"
171
171
  elif isinstance(endpoint_id, list):
@@ -206,6 +206,8 @@ class TDEngineConnector(TSDBConnector):
206
206
  columns=[
207
207
  mm_schemas.EventFieldType.LATENCY,
208
208
  mm_schemas.EventKeyMetrics.CUSTOM_METRICS,
209
+ mm_schemas.EventFieldType.ESTIMATED_PREDICTION_COUNT,
210
+ mm_schemas.EventFieldType.EFFECTIVE_SAMPLE_COUNT,
209
211
  ],
210
212
  tag_cols=[
211
213
  mm_schemas.EventFieldType.ENDPOINT_ID,
@@ -483,7 +485,7 @@ class TDEngineConnector(TSDBConnector):
483
485
  table=self.tables[mm_schemas.TDEngineSuperTables.PREDICTIONS].super_table,
484
486
  start=start,
485
487
  end=end,
486
- columns=[mm_schemas.EventFieldType.LATENCY],
488
+ columns=[mm_schemas.EventFieldType.ESTIMATED_PREDICTION_COUNT],
487
489
  filter_query=f"endpoint_id='{endpoint_id}'",
488
490
  agg_funcs=agg_funcs,
489
491
  interval=aggregation_window,
@@ -503,10 +505,10 @@ class TDEngineConnector(TSDBConnector):
503
505
  df["_wend"] = pd.to_datetime(df["_wend"])
504
506
  df.set_index("_wend", inplace=True)
505
507
 
506
- latency_column = (
507
- f"{agg_funcs[0]}({mm_schemas.EventFieldType.LATENCY})"
508
+ estimated_prediction_count = (
509
+ f"{agg_funcs[0]}({mm_schemas.EventFieldType.ESTIMATED_PREDICTION_COUNT})"
508
510
  if agg_funcs
509
- else mm_schemas.EventFieldType.LATENCY
511
+ else mm_schemas.EventFieldType.ESTIMATED_PREDICTION_COUNT
510
512
  )
511
513
 
512
514
  return mm_schemas.ModelEndpointMonitoringMetricValues(
@@ -514,7 +516,7 @@ class TDEngineConnector(TSDBConnector):
514
516
  values=list(
515
517
  zip(
516
518
  df.index,
517
- df[latency_column],
519
+ df[estimated_prediction_count],
518
520
  )
519
521
  ), # pyright: ignore[reportArgumentType]
520
522
  )
@@ -525,9 +527,7 @@ class TDEngineConnector(TSDBConnector):
525
527
  start: typing.Optional[datetime] = None,
526
528
  end: typing.Optional[datetime] = None,
527
529
  ) -> pd.DataFrame:
528
- endpoint_ids = (
529
- endpoint_ids if isinstance(endpoint_ids, list) else [endpoint_ids]
530
- )
530
+ filter_query = self._get_endpoint_filter(endpoint_id=endpoint_ids)
531
531
  start, end = self._get_start_end(start, end)
532
532
  df = self._get_records(
533
533
  table=self.tables[mm_schemas.TDEngineSuperTables.PREDICTIONS].super_table,
@@ -538,7 +538,7 @@ class TDEngineConnector(TSDBConnector):
538
538
  mm_schemas.EventFieldType.TIME,
539
539
  mm_schemas.EventFieldType.LATENCY,
540
540
  ],
541
- filter_query=f"endpoint_id IN({str(endpoint_ids)[1:-1]})",
541
+ filter_query=filter_query,
542
542
  timestamp_column=mm_schemas.EventFieldType.TIME,
543
543
  agg_funcs=["last"],
544
544
  group_by=mm_schemas.EventFieldType.ENDPOINT_ID,
@@ -567,9 +567,7 @@ class TDEngineConnector(TSDBConnector):
567
567
  start: typing.Optional[datetime] = None,
568
568
  end: typing.Optional[datetime] = None,
569
569
  ) -> pd.DataFrame:
570
- endpoint_ids = (
571
- endpoint_ids if isinstance(endpoint_ids, list) else [endpoint_ids]
572
- )
570
+ filter_query = self._get_endpoint_filter(endpoint_id=endpoint_ids)
573
571
  start = start or (mlrun.utils.datetime_now() - timedelta(hours=24))
574
572
  start, end = self._get_start_end(start, end)
575
573
  df = self._get_records(
@@ -580,7 +578,7 @@ class TDEngineConnector(TSDBConnector):
580
578
  mm_schemas.ResultData.RESULT_STATUS,
581
579
  mm_schemas.EventFieldType.ENDPOINT_ID,
582
580
  ],
583
- filter_query=f"endpoint_id IN({str(endpoint_ids)[1:-1]})",
581
+ filter_query=filter_query,
584
582
  timestamp_column=mm_schemas.WriterEvent.END_INFER_TIME,
585
583
  agg_funcs=["max"],
586
584
  group_by=mm_schemas.EventFieldType.ENDPOINT_ID,
@@ -678,9 +676,8 @@ class TDEngineConnector(TSDBConnector):
678
676
  start: typing.Optional[datetime] = None,
679
677
  end: typing.Optional[datetime] = None,
680
678
  ) -> pd.DataFrame:
681
- endpoint_ids = (
682
- endpoint_ids if isinstance(endpoint_ids, list) else [endpoint_ids]
683
- )
679
+ filter_query = self._get_endpoint_filter(endpoint_id=endpoint_ids)
680
+ filter_query += f"AND {mm_schemas.EventFieldType.ERROR_TYPE} = '{mm_schemas.EventFieldType.INFER_ERROR}'"
684
681
  start, end = self._get_start_end(start, end)
685
682
  df = self._get_records(
686
683
  table=self.tables[mm_schemas.TDEngineSuperTables.ERRORS].super_table,
@@ -691,8 +688,7 @@ class TDEngineConnector(TSDBConnector):
691
688
  mm_schemas.EventFieldType.ENDPOINT_ID,
692
689
  ],
693
690
  agg_funcs=["count"],
694
- filter_query=f"endpoint_id IN({str(endpoint_ids)[1:-1]}) "
695
- f"AND {mm_schemas.EventFieldType.ERROR_TYPE} = '{mm_schemas.EventFieldType.INFER_ERROR}'",
691
+ filter_query=filter_query,
696
692
  group_by=mm_schemas.EventFieldType.ENDPOINT_ID,
697
693
  preform_agg_columns=[mm_schemas.EventFieldType.MODEL_ERROR],
698
694
  )
@@ -33,7 +33,7 @@ _TSDB_BE = "tsdb"
33
33
  _TSDB_RATE = "1/s"
34
34
  _CONTAINER = "users"
35
35
 
36
- V3IO_MEPS_LIMIT = 50 # TODO remove limitation after fixing ML-8886
36
+ V3IO_MEPS_LIMIT = 200
37
37
 
38
38
 
39
39
  def _is_no_schema_error(exc: v3io_frames.Error) -> bool:
@@ -234,6 +234,8 @@ class V3IOTSDBConnector(TSDBConnector):
234
234
  columns=[
235
235
  mm_schemas.EventFieldType.LATENCY,
236
236
  mm_schemas.EventFieldType.LAST_REQUEST_TIMESTAMP,
237
+ mm_schemas.EventFieldType.ESTIMATED_PREDICTION_COUNT,
238
+ mm_schemas.EventFieldType.EFFECTIVE_SAMPLE_COUNT,
237
239
  ],
238
240
  index_cols=[
239
241
  mm_schemas.EventFieldType.ENDPOINT_ID,
@@ -580,14 +582,18 @@ class V3IOTSDBConnector(TSDBConnector):
580
582
  )
581
583
 
582
584
  @staticmethod
583
- def _get_endpoint_filter(endpoint_id: Union[str, list[str]]):
585
+ def _get_endpoint_filter(endpoint_id: Union[str, list[str]]) -> Optional[str]:
584
586
  if isinstance(endpoint_id, str):
585
587
  return f"endpoint_id=='{endpoint_id}'"
586
588
  elif isinstance(endpoint_id, list):
587
589
  if len(endpoint_id) > V3IO_MEPS_LIMIT:
588
- raise mlrun.errors.MLRunInvalidArgumentError(
589
- f"Filtering more than {V3IO_MEPS_LIMIT} model endpoints in the V3IO connector is not supported."
590
+ logger.info(
591
+ "The number of endpoint ids exceeds the v3io-engine filter-expression limit, "
592
+ "retrieving all the model endpoints from the db.",
593
+ limit=V3IO_MEPS_LIMIT,
594
+ amount=len(endpoint_id),
590
595
  )
596
+ return None
591
597
  return f"endpoint_id IN({str(endpoint_id)[1:-1]}) "
592
598
  else:
593
599
  raise mlrun.errors.MLRunInvalidArgumentError(
@@ -737,7 +743,7 @@ class V3IOTSDBConnector(TSDBConnector):
737
743
  table=mm_schemas.FileTargetKind.PREDICTIONS,
738
744
  start=start,
739
745
  end=end,
740
- columns=[mm_schemas.EventFieldType.LATENCY],
746
+ columns=[mm_schemas.EventFieldType.ESTIMATED_PREDICTION_COUNT],
741
747
  filter_query=f"endpoint_id=='{endpoint_id}'",
742
748
  agg_funcs=agg_funcs,
743
749
  sliding_window_step=aggregation_window,
@@ -751,10 +757,10 @@ class V3IOTSDBConnector(TSDBConnector):
751
757
  type=mm_schemas.ModelEndpointMonitoringMetricType.METRIC,
752
758
  )
753
759
 
754
- latency_column = (
755
- f"{agg_funcs[0]}({mm_schemas.EventFieldType.LATENCY})"
760
+ estimated_prediction_count = (
761
+ f"{agg_funcs[0]}({mm_schemas.EventFieldType.ESTIMATED_PREDICTION_COUNT})"
756
762
  if agg_funcs
757
- else mm_schemas.EventFieldType.LATENCY
763
+ else mm_schemas.EventFieldType.ESTIMATED_PREDICTION_COUNT
758
764
  )
759
765
 
760
766
  return mm_schemas.ModelEndpointMonitoringMetricValues(
@@ -762,7 +768,7 @@ class V3IOTSDBConnector(TSDBConnector):
762
768
  values=list(
763
769
  zip(
764
770
  df.index,
765
- df[latency_column],
771
+ df[estimated_prediction_count],
766
772
  )
767
773
  ), # pyright: ignore[reportArgumentType]
768
774
  )
@@ -773,15 +779,13 @@ class V3IOTSDBConnector(TSDBConnector):
773
779
  start: Optional[datetime] = None,
774
780
  end: Optional[datetime] = None,
775
781
  ) -> pd.DataFrame:
776
- endpoint_ids = (
777
- endpoint_ids if isinstance(endpoint_ids, list) else [endpoint_ids]
778
- )
782
+ filter_query = self._get_endpoint_filter(endpoint_id=endpoint_ids)
779
783
  start, end = self._get_start_end(start, end)
780
784
  df = self._get_records(
781
785
  table=mm_schemas.FileTargetKind.PREDICTIONS,
782
786
  start=start,
783
787
  end=end,
784
- filter_query=f"endpoint_id IN({str(endpoint_ids)[1:-1]})",
788
+ filter_query=filter_query,
785
789
  agg_funcs=["last"],
786
790
  )
787
791
  if not df.empty:
@@ -808,9 +812,7 @@ class V3IOTSDBConnector(TSDBConnector):
808
812
  start: Optional[datetime] = None,
809
813
  end: Optional[datetime] = None,
810
814
  ) -> pd.DataFrame:
811
- endpoint_ids = (
812
- endpoint_ids if isinstance(endpoint_ids, list) else [endpoint_ids]
813
- )
815
+ filter_query = self._get_endpoint_filter(endpoint_id=endpoint_ids)
814
816
  start = start or (mlrun.utils.datetime_now() - timedelta(hours=24))
815
817
  start, end = self._get_start_end(start, end)
816
818
  df = self._get_records(
@@ -818,7 +820,7 @@ class V3IOTSDBConnector(TSDBConnector):
818
820
  start=start,
819
821
  end=end,
820
822
  columns=[mm_schemas.ResultData.RESULT_STATUS],
821
- filter_query=f"endpoint_id IN({str(endpoint_ids)[1:-1]})",
823
+ filter_query=filter_query,
822
824
  agg_funcs=["max"],
823
825
  group_by="endpoint_id",
824
826
  )
@@ -883,17 +885,18 @@ class V3IOTSDBConnector(TSDBConnector):
883
885
  start: Optional[datetime] = None,
884
886
  end: Optional[datetime] = None,
885
887
  ) -> pd.DataFrame:
886
- endpoint_ids = (
887
- endpoint_ids if isinstance(endpoint_ids, list) else [endpoint_ids]
888
- )
888
+ filter_query = self._get_endpoint_filter(endpoint_id=endpoint_ids)
889
+ if filter_query:
890
+ filter_query += f"AND {mm_schemas.EventFieldType.ERROR_TYPE} == '{mm_schemas.EventFieldType.INFER_ERROR}'"
891
+ else:
892
+ filter_query = f"{mm_schemas.EventFieldType.ERROR_TYPE} == '{mm_schemas.EventFieldType.INFER_ERROR}' z"
889
893
  start, end = self._get_start_end(start, end)
890
894
  df = self._get_records(
891
895
  table=mm_schemas.FileTargetKind.ERRORS,
892
896
  start=start,
893
897
  end=end,
894
898
  columns=[mm_schemas.EventFieldType.ERROR_COUNT],
895
- filter_query=f"endpoint_id IN({str(endpoint_ids)[1:-1]}) "
896
- f"AND {mm_schemas.EventFieldType.ERROR_TYPE} == '{mm_schemas.EventFieldType.INFER_ERROR}'",
899
+ filter_query=filter_query,
897
900
  agg_funcs=["count"],
898
901
  )
899
902
  if not df.empty:
@@ -912,9 +915,7 @@ class V3IOTSDBConnector(TSDBConnector):
912
915
  start: Optional[datetime] = None,
913
916
  end: Optional[datetime] = None,
914
917
  ) -> pd.DataFrame:
915
- endpoint_ids = (
916
- endpoint_ids if isinstance(endpoint_ids, list) else [endpoint_ids]
917
- )
918
+ filter_query = self._get_endpoint_filter(endpoint_id=endpoint_ids)
918
919
  start = start or (mlrun.utils.datetime_now() - timedelta(hours=24))
919
920
  start, end = self._get_start_end(start, end)
920
921
  df = self._get_records(
@@ -922,7 +923,7 @@ class V3IOTSDBConnector(TSDBConnector):
922
923
  start=start,
923
924
  end=end,
924
925
  columns=[mm_schemas.EventFieldType.LATENCY],
925
- filter_query=f"endpoint_id IN({str(endpoint_ids)[1:-1]})",
926
+ filter_query=filter_query,
926
927
  agg_funcs=["avg"],
927
928
  )
928
929
  if not df.empty:
@@ -430,6 +430,10 @@ class ProcessEndpointEvent(mlrun.feature_store.steps.MapClass):
430
430
  if not isinstance(feature, list):
431
431
  feature = [feature]
432
432
 
433
+ effective_sample_count, estimated_prediction_count = (
434
+ self._get_effective_and_estimated_counts(event=event)
435
+ )
436
+
433
437
  events.append(
434
438
  {
435
439
  EventFieldType.FUNCTION_URI: function_uri,
@@ -453,6 +457,8 @@ class ProcessEndpointEvent(mlrun.feature_store.steps.MapClass):
453
457
  EventFieldType.ENTITIES: event.get("request", {}).get(
454
458
  EventFieldType.ENTITIES, {}
455
459
  ),
460
+ EventFieldType.EFFECTIVE_SAMPLE_COUNT: effective_sample_count,
461
+ EventFieldType.ESTIMATED_PREDICTION_COUNT: estimated_prediction_count,
456
462
  }
457
463
  )
458
464
 
@@ -507,6 +513,20 @@ class ProcessEndpointEvent(mlrun.feature_store.steps.MapClass):
507
513
  self.error_count[endpoint_id] += 1
508
514
  return False
509
515
 
516
+ @staticmethod
517
+ def _get_effective_and_estimated_counts(event):
518
+ """
519
+ Calculate the `effective_sample_count` and the `estimated_prediction_count` based on the event's
520
+ sampling percentage. These values will be stored in the TSDB target.
521
+ Note that In non-batch serving, the `effective_sample_count` is always set to 1. In addition, when the sampling
522
+ percentage is 100%, the `estimated_prediction_count` is equal to the `effective_sample_count`.
523
+ """
524
+ effective_sample_count = event.get(EventFieldType.EFFECTIVE_SAMPLE_COUNT, 1)
525
+ estimated_prediction_count = effective_sample_count * (
526
+ 100 / event.get(EventFieldType.SAMPLING_PERCENTAGE, 100)
527
+ )
528
+ return effective_sample_count, estimated_prediction_count
529
+
510
530
 
511
531
  def is_not_none(field: typing.Any, dict_path: list[str]):
512
532
  if field is not None:
@@ -672,6 +692,7 @@ class MapFeatureNames(mlrun.feature_store.steps.MapClass):
672
692
  )
673
693
  )
674
694
  self.first_request[endpoint_id] = True
695
+
675
696
  if attributes_to_update:
676
697
  logger.info(
677
698
  "Updating endpoint record",
@@ -523,11 +523,12 @@ class _PipelineRunner(abc.ABC):
523
523
  text = _PipelineRunner._generate_workflow_finished_message(
524
524
  run.run_id, errors_counter, run._state
525
525
  )
526
-
527
526
  notifiers = notifiers or project.notifiers
528
527
  if notifiers:
529
528
  notifiers.push(text, "info", runs)
530
529
 
530
+ project.push_pipeline_notification_kfp_runner(run.run_id, run._state, text)
531
+
531
532
  if raise_error:
532
533
  raise raise_error
533
534
  return state or run._state, errors_counter, text
@@ -620,6 +621,8 @@ class _KFPRunner(_PipelineRunner):
620
621
  params.update(notification.secret_params)
621
622
  project.notifiers.add_notification(notification.kind, params)
622
623
 
624
+ project.spec.notifications = notifications
625
+
623
626
  run_id = _run_pipeline(
624
627
  workflow_handler,
625
628
  project=project.metadata.name,
@@ -647,13 +650,23 @@ class _KFPRunner(_PipelineRunner):
647
650
  exc_info=err_to_str(exc),
648
651
  )
649
652
 
650
- # TODO: we should check how can we get the run uid when we don't have the context (for example on
651
- # mlrun.load_project() and later call directly to project.run)
653
+ # Pushing only relevant notification for the client (ipython and console)
654
+ project.notifiers.push_pipeline_start_message_from_client(
655
+ project.metadata.name, pipeline_id=run_id
656
+ )
657
+
652
658
  if context:
653
659
  project.notifiers.push_pipeline_start_message(
654
660
  project.metadata.name,
655
661
  context.uid,
656
662
  )
663
+ else:
664
+ project.push_pipeline_notification_kfp_runner(
665
+ run_id,
666
+ mlrun_pipelines.common.models.RunStatuses.running,
667
+ f"Workflow {run_id} started in project {project.metadata.name}",
668
+ notifications,
669
+ )
657
670
  pipeline_context.clear()
658
671
  return _PipelineRunStatus(run_id, cls, project=project, workflow=workflow_spec)
659
672
 
mlrun/projects/project.py CHANGED
@@ -83,6 +83,7 @@ from ..artifacts import (
83
83
  ModelArtifact,
84
84
  )
85
85
  from ..artifacts.manager import ArtifactManager, dict_to_artifact, extend_artifact_path
86
+ from ..common.runtimes.constants import RunStates
86
87
  from ..datastore import store_manager
87
88
  from ..features import Feature
88
89
  from ..model import EntrypointParam, ImageBuilder, ModelObj
@@ -851,6 +852,7 @@ class ProjectSpec(ModelObj):
851
852
  build=None,
852
853
  custom_packagers: Optional[list[tuple[str, bool]]] = None,
853
854
  default_function_node_selector=None,
855
+ notifications=None,
854
856
  ):
855
857
  self.repo = None
856
858
 
@@ -891,6 +893,7 @@ class ProjectSpec(ModelObj):
891
893
  # whether it is mandatory for a run (raise exception on collection error) or not.
892
894
  self.custom_packagers = custom_packagers or []
893
895
  self._default_function_node_selector = default_function_node_selector or None
896
+ self.notifications = notifications or []
894
897
 
895
898
  @property
896
899
  def source(self) -> str:
@@ -1172,7 +1175,6 @@ class MlrunProject(ModelObj):
1172
1175
  self._artifact_manager = None
1173
1176
  self._notifiers = CustomNotificationPusher(
1174
1177
  [
1175
- NotificationTypes.slack,
1176
1178
  NotificationTypes.console,
1177
1179
  NotificationTypes.ipython,
1178
1180
  ]
@@ -2670,6 +2672,36 @@ class MlrunProject(ModelObj):
2670
2672
  timeout=timeout,
2671
2673
  )
2672
2674
 
2675
+ def push_pipeline_notification_kfp_runner(
2676
+ self,
2677
+ pipeline_id: str,
2678
+ current_run_state: mlrun_pipelines.common.models.RunStatuses,
2679
+ message: str,
2680
+ notifications: Optional[list] = None,
2681
+ ):
2682
+ """
2683
+ Push notifications for a pipeline run(KFP).
2684
+
2685
+ :param pipeline_id: Unique ID of the pipeline run.
2686
+ :param current_run_state: Current run state of the pipeline.
2687
+ :param message: Message to send in the notification.
2688
+ :param notifications: List of notifications to send.
2689
+ """
2690
+ current_run_state = RunStates.pipeline_run_status_to_run_state(
2691
+ current_run_state
2692
+ )
2693
+ db = mlrun.get_run_db()
2694
+ notifications = notifications or self.spec.notifications
2695
+ notifications_to_send = []
2696
+ for notification in notifications:
2697
+ if current_run_state in notification.when:
2698
+ notification_copy = notification.copy()
2699
+ notification_copy.message = message
2700
+ notifications_to_send.append(notification_copy)
2701
+ db.push_pipeline_notifications(
2702
+ pipeline_id, self.metadata.name, notifications_to_send
2703
+ )
2704
+
2673
2705
  def _instantiate_function(
2674
2706
  self,
2675
2707
  func: typing.Union[str, mlrun.runtimes.BaseRuntime] = None,
@@ -309,7 +309,7 @@ class ServingRuntime(RemoteRuntime):
309
309
  self,
310
310
  stream_path: Optional[str] = None,
311
311
  batch: Optional[int] = None,
312
- sample: Optional[int] = None,
312
+ sampling_percentage: float = 100,
313
313
  stream_args: Optional[dict] = None,
314
314
  tracking_policy: Optional[Union["TrackingPolicy", dict]] = None,
315
315
  enable_tracking: bool = True,
@@ -317,13 +317,13 @@ class ServingRuntime(RemoteRuntime):
317
317
  """Apply on your serving function to monitor a deployed model, including real-time dashboards to detect drift
318
318
  and analyze performance.
319
319
 
320
- :param stream_path: Path/url of the tracking stream e.g. v3io:///users/mike/mystream
321
- you can use the "dummy://" path for test/simulation.
322
- :param batch: Micro batch size (send micro batches of N records at a time).
323
- :param sample: Sample size (send only one of N records).
324
- :param stream_args: Stream initialization parameters, e.g. shards, retention_in_hours, ..
325
- :param enable_tracking: Enabled/Disable model-monitoring tracking.
326
- Default True (tracking enabled).
320
+ :param stream_path: Path/url of the tracking stream e.g. v3io:///users/mike/mystream
321
+ you can use the "dummy://" path for test/simulation.
322
+ :param batch: Deprecated. Micro batch size (send micro batches of N records at a time).
323
+ :param sampling_percentage: Down sampling events that will be pushed to the monitoring stream based on
324
+ a specified percentage. e.g. 50 for 50%. By default, all events are pushed.
325
+ :param stream_args: Stream initialization parameters, e.g. shards, retention_in_hours, ..
326
+ :param enable_tracking: Enabled/Disable model-monitoring tracking. Default True (tracking enabled).
327
327
 
328
328
  Example::
329
329
 
@@ -336,12 +336,21 @@ class ServingRuntime(RemoteRuntime):
336
336
  # Applying model monitoring configurations
337
337
  self.spec.track_models = enable_tracking
338
338
 
339
+ if not 0 < sampling_percentage <= 100:
340
+ raise mlrun.errors.MLRunInvalidArgumentError(
341
+ "`sampling_percentage` must be greater than 0 and less or equal to 100."
342
+ )
343
+ self.spec.parameters["sampling_percentage"] = sampling_percentage
344
+
339
345
  if stream_path:
340
346
  self.spec.parameters["log_stream"] = stream_path
341
347
  if batch:
342
- self.spec.parameters["log_stream_batch"] = batch
343
- if sample:
344
- self.spec.parameters["log_stream_sample"] = sample
348
+ warnings.warn(
349
+ "The `batch` size parameter was deprecated in version 1.8.0 and is no longer used. "
350
+ "It will be removed in 1.10.",
351
+ # TODO: Remove this in 1.10
352
+ FutureWarning,
353
+ )
345
354
  if stream_args:
346
355
  self.spec.parameters["stream_args"] = stream_args
347
356
  if tracking_policy is not None:
@@ -12,6 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ import random
15
16
  import threading
16
17
  import time
17
18
  import traceback
@@ -283,7 +284,6 @@ class V2ModelServer(StepToDict):
283
284
  }
284
285
  if self.version:
285
286
  response["model_version"] = self.version
286
-
287
287
  elif op == "ready" and event.method == "GET":
288
288
  # get model health operation
289
289
  setattr(event, "terminated", True)
@@ -468,13 +468,9 @@ class _ModelLogPusher:
468
468
  self.hostname = context.stream.hostname
469
469
  self.function_uri = context.stream.function_uri
470
470
  self.stream_path = context.stream.stream_uri
471
- self.stream_batch = int(context.get_param("log_stream_batch", 1))
472
- self.stream_sample = int(context.get_param("log_stream_sample", 1))
471
+ self.sampling_percentage = float(context.get_param("sampling_percentage", 100))
473
472
  self.output_stream = output_stream or context.stream.output_stream
474
473
  self._worker = context.worker_id
475
- self._sample_iter = 0
476
- self._batch_iter = 0
477
- self._batch = []
478
474
 
479
475
  def base_data(self):
480
476
  base_data = {
@@ -485,6 +481,7 @@ class _ModelLogPusher:
485
481
  "host": self.hostname,
486
482
  "function_uri": self.function_uri,
487
483
  "endpoint_id": self.model.model_endpoint_uid,
484
+ "sampling_percentage": self.sampling_percentage,
488
485
  }
489
486
  if getattr(self.model, "labels", None):
490
487
  base_data["labels"] = self.model.labels
@@ -504,37 +501,55 @@ class _ModelLogPusher:
504
501
  self.output_stream.push([data], partition_key=partition_key)
505
502
  return
506
503
 
507
- self._sample_iter = (self._sample_iter + 1) % self.stream_sample
508
- if self.output_stream and self._sample_iter == 0:
504
+ if self.output_stream:
505
+ # Ensure that the inputs are a list of lists
506
+ request["inputs"] = (
507
+ request["inputs"]
508
+ if not any(not isinstance(req, list) for req in request["inputs"])
509
+ else [request["inputs"]]
510
+ )
509
511
  microsec = (now_date() - start).microseconds
510
512
 
511
- if self.stream_batch > 1:
512
- if self._batch_iter == 0:
513
- self._batch = []
514
- self._batch.append(
515
- [request, op, resp, str(start), microsec, self.model.metrics]
513
+ if self.sampling_percentage != 100:
514
+ # Randomly select a subset of the requests based on the percentage
515
+ num_of_inputs = len(request["inputs"])
516
+ sampled_requests_indices = self._pick_random_requests(
517
+ num_of_inputs, self.sampling_percentage
516
518
  )
517
- self._batch_iter = (self._batch_iter + 1) % self.stream_batch
518
-
519
- if self._batch_iter == 0:
520
- data = self.base_data()
521
- data["headers"] = [
522
- "request",
523
- "op",
524
- "resp",
525
- "when",
526
- "microsec",
527
- "metrics",
519
+ if not sampled_requests_indices:
520
+ # No events were selected for sampling
521
+ return
522
+
523
+ request["inputs"] = [
524
+ request["inputs"][i] for i in sampled_requests_indices
525
+ ]
526
+
527
+ if resp and "outputs" in resp and isinstance(resp["outputs"], list):
528
+ resp["outputs"] = [
529
+ resp["outputs"][i] for i in sampled_requests_indices
528
530
  ]
529
- data["values"] = self._batch
530
- self.output_stream.push([data], partition_key=partition_key)
531
- else:
532
- data = self.base_data()
533
- data["request"] = request
534
- data["op"] = op
535
- data["resp"] = resp
536
- data["when"] = start_str
537
- data["microsec"] = microsec
538
- if getattr(self.model, "metrics", None):
539
- data["metrics"] = self.model.metrics
540
- self.output_stream.push([data], partition_key=partition_key)
531
+
532
+ data = self.base_data()
533
+ data["request"] = request
534
+ data["op"] = op
535
+ data["resp"] = resp
536
+ data["when"] = start_str
537
+ data["microsec"] = microsec
538
+ if getattr(self.model, "metrics", None):
539
+ data["metrics"] = self.model.metrics
540
+ data["effective_sample_count"] = len(request["inputs"])
541
+ self.output_stream.push([data], partition_key=partition_key)
542
+
543
+ @staticmethod
544
+ def _pick_random_requests(num_of_reqs: int, percentage: float) -> list[int]:
545
+ """
546
+ Randomly selects indices of requests to sample based on the given percentage
547
+
548
+ :param num_of_reqs: Number of requests to select from
549
+ :param percentage: Sample percentage for each request
550
+ :return: A list containing the indices of the selected requests
551
+ """
552
+
553
+ return [
554
+ req for req in range(num_of_reqs) if random.random() < (percentage / 100)
555
+ ]
mlrun/utils/helpers.py CHANGED
@@ -13,8 +13,10 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import asyncio
16
+ import base64
16
17
  import enum
17
18
  import functools
19
+ import gzip
18
20
  import hashlib
19
21
  import inspect
20
22
  import itertools
@@ -1709,7 +1711,14 @@ def get_serving_spec():
1709
1711
  raise mlrun.errors.MLRunInvalidArgumentError(
1710
1712
  "Failed to find serving spec in env var or config file"
1711
1713
  )
1712
- spec = json.loads(data)
1714
+ # Attempt to decode and decompress, or use as-is for backward compatibility
1715
+ try:
1716
+ decoded_data = base64.b64decode(data)
1717
+ decompressed_data = gzip.decompress(decoded_data)
1718
+ spec = json.loads(decompressed_data.decode("utf-8"))
1719
+ except (OSError, gzip.BadGzipFile, base64.binascii.Error, json.JSONDecodeError):
1720
+ spec = json.loads(data)
1721
+
1713
1722
  return spec
1714
1723
 
1715
1724
 
@@ -1981,7 +1990,20 @@ class Workflow:
1981
1990
  if not workflow_id:
1982
1991
  return steps
1983
1992
 
1984
- workflow_manifest = Workflow._get_workflow_manifest(workflow_id)
1993
+ try:
1994
+ workflow_manifest = Workflow._get_workflow_manifest(workflow_id)
1995
+ except Exception:
1996
+ logger.warning(
1997
+ "Failed to extract workflow steps from workflow manifest, "
1998
+ "returning all runs with the workflow id label",
1999
+ workflow_id=workflow_id,
2000
+ traceback=traceback.format_exc(),
2001
+ )
2002
+ return db.list_runs(
2003
+ project=project,
2004
+ labels=f"workflow={workflow_id}",
2005
+ )
2006
+
1985
2007
  if not workflow_manifest:
1986
2008
  return steps
1987
2009
 
@@ -2038,3 +2060,9 @@ class Workflow:
2038
2060
 
2039
2061
  kfp_run = mlrun_pipelines.models.PipelineRun(kfp_run)
2040
2062
  return kfp_run.workflow_manifest()
2063
+
2064
+
2065
+ def as_dict(data: typing.Union[dict, str]) -> dict:
2066
+ if isinstance(data, str):
2067
+ return json.loads(data)
2068
+ return data
@@ -139,15 +139,25 @@ class NotificationPusher(_NotificationPusherBase):
139
139
  error=mlrun.errors.err_to_str(exc),
140
140
  )
141
141
 
142
- def _process_notification(self, notification, run):
143
- notification.status = run.status.notifications.get(notification.name, {}).get(
142
+ def _process_notification(self, notification_object, run):
143
+ notification_object.status = run.status.notifications.get(
144
+ notification_object.name, {}
145
+ ).get(
144
146
  "status",
145
147
  mlrun.common.schemas.NotificationStatus.PENDING,
146
148
  )
147
- if self._should_notify(run, notification):
148
- self._load_notification(run, notification)
149
+ if self._should_notify(run, notification_object):
150
+ notification = self._load_notification(notification_object)
151
+ if notification.is_async:
152
+ self._async_notifications.append(
153
+ (notification, run, notification_object)
154
+ )
155
+ else:
156
+ self._sync_notifications.append(
157
+ (notification, run, notification_object)
158
+ )
149
159
 
150
- def push(self):
160
+ def push(self, sync_push_callback=None, async_push_callback=None):
151
161
  """
152
162
  Asynchronously push notifications for all runs in the initialized runs list (if they should be pushed).
153
163
  When running from a sync environment, the notifications will be pushed asynchronously however the function will
@@ -201,8 +211,9 @@ class NotificationPusher(_NotificationPusherBase):
201
211
  notifications_amount=len(self._sync_notifications)
202
212
  + len(self._async_notifications),
203
213
  )
204
-
205
- self._push(sync_push, async_push)
214
+ sync_push_callback = sync_push_callback or sync_push
215
+ async_push_callback = async_push_callback or async_push
216
+ self._push(sync_push_callback, async_push_callback)
206
217
 
207
218
  @staticmethod
208
219
  def _should_notify(
@@ -241,24 +252,19 @@ class NotificationPusher(_NotificationPusherBase):
241
252
  return False
242
253
 
243
254
  def _load_notification(
244
- self, run: mlrun.model.RunObject, notification_object: mlrun.model.Notification
255
+ self, notification_object: mlrun.model.Notification
245
256
  ) -> base.NotificationBase:
246
257
  name = notification_object.name
247
258
  notification_type = notification_module.NotificationTypes(
248
259
  notification_object.kind or notification_module.NotificationTypes.console
249
260
  )
250
261
  params = {}
251
- params.update(notification_object.secret_params)
252
- params.update(notification_object.params)
262
+ params.update(notification_object.secret_params or {})
263
+ params.update(notification_object.params or {})
253
264
  default_params = self._default_params.get(notification_type.value, {})
254
265
  notification = notification_type.get_notification()(
255
266
  name, params, default_params
256
267
  )
257
- if notification.is_async:
258
- self._async_notifications.append((notification, run, notification_object))
259
- else:
260
- self._sync_notifications.append((notification, run, notification_object))
261
-
262
268
  logger.debug(
263
269
  "Loaded notification", notification=name, type=notification_type.value
264
270
  )
@@ -496,6 +502,14 @@ class CustomNotificationPusher(_NotificationPusherBase):
496
502
  notification_type: str,
497
503
  params: typing.Optional[dict[str, str]] = None,
498
504
  ):
505
+ if notification_type not in [
506
+ notification_module.NotificationTypes.console,
507
+ notification_module.NotificationTypes.ipython,
508
+ ]:
509
+ # We want that only the console and ipython notifications will be notified by the client.
510
+ # The rest of the notifications will be notified by the BE.
511
+ return
512
+
499
513
  if notification_type in self._async_notifications:
500
514
  self._async_notifications[notification_type].load_notification(params)
501
515
  elif notification_type in self._sync_notifications:
@@ -565,25 +579,9 @@ class CustomNotificationPusher(_NotificationPusherBase):
565
579
  pipeline_id: typing.Optional[str] = None,
566
580
  has_workflow_url: bool = False,
567
581
  ):
568
- message = f"Workflow started in project {project}"
569
- if pipeline_id:
570
- message += f" id={pipeline_id}"
571
- commit_id = (
572
- commit_id or os.environ.get("GITHUB_SHA") or os.environ.get("CI_COMMIT_SHA")
582
+ html, message = self.generate_start_message(
583
+ commit_id, has_workflow_url, pipeline_id, project
573
584
  )
574
- if commit_id:
575
- message += f", commit={commit_id}"
576
- if has_workflow_url:
577
- url = mlrun.utils.helpers.get_workflow_url(project, pipeline_id)
578
- else:
579
- url = mlrun.utils.helpers.get_ui_url(project)
580
- html = ""
581
- if url:
582
- html = (
583
- message
584
- + f'<div><a href="{url}" target="_blank">click here to view progress</a></div>'
585
- )
586
- message = message + f", check progress in {url}"
587
585
  self.push(message, "info", custom_html=html)
588
586
 
589
587
  def push_pipeline_run_results(
@@ -616,6 +614,30 @@ class CustomNotificationPusher(_NotificationPusherBase):
616
614
  text += f", state={state}"
617
615
  self.push(text, "info", runs=runs_list)
618
616
 
617
+ def generate_start_message(
618
+ self, commit_id=None, has_workflow_url=None, pipeline_id=None, project=None
619
+ ):
620
+ message = f"Workflow started in project {project}"
621
+ if pipeline_id:
622
+ message += f" id={pipeline_id}"
623
+ commit_id = (
624
+ commit_id or os.environ.get("GITHUB_SHA") or os.environ.get("CI_COMMIT_SHA")
625
+ )
626
+ if commit_id:
627
+ message += f", commit={commit_id}"
628
+ if has_workflow_url:
629
+ url = mlrun.utils.helpers.get_workflow_url(project, pipeline_id)
630
+ else:
631
+ url = mlrun.utils.helpers.get_ui_url(project)
632
+ html = ""
633
+ if url:
634
+ html = (
635
+ message
636
+ + f'<div><a href="{url}" target="_blank">click here to view progress</a></div>'
637
+ )
638
+ message = message + f", check progress in {url}"
639
+ return html, message
640
+
619
641
 
620
642
  def sanitize_notification(notification_dict: dict):
621
643
  notification_dict.pop("secret_params", None)
@@ -1,4 +1,4 @@
1
1
  {
2
- "git_commit": "6671b915fe08ba2b9afa054585f86b955b48b671",
3
- "version": "1.8.0-rc19"
2
+ "git_commit": "91ab2fb641a35e93a5056d93c55b98b96d74ebc1",
3
+ "version": "1.8.0-rc20"
4
4
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mlrun
3
- Version: 1.8.0rc19
3
+ Version: 1.8.0rc20
4
4
  Summary: Tracking and config of machine learning runs
5
5
  Home-page: https://github.com/mlrun/mlrun
6
6
  Author: Yaron Haviv
@@ -1,5 +1,5 @@
1
1
  mlrun/__init__.py,sha256=7vuMpUiigXXDrghLRq680LKWy1faC0kQyGCZb_7cwyE,7473
2
- mlrun/__main__.py,sha256=o65gXHhmFA9GV_n2mqmAO80nW3MAwo_s7j80IKgCzRE,45949
2
+ mlrun/__main__.py,sha256=3CJdwbSQGpbEhnAnVN_-CkQmLOPUUXTKhMf7xIWNQrc,46138
3
3
  mlrun/config.py,sha256=zHp7wtbaewFbHYsA_SLxwuwSDYwrcL6A3knWSPcRtAA,70547
4
4
  mlrun/errors.py,sha256=5raKb1PXQpTcIvWQ4sr1qn2IS7P_GT_FydBJ0dXkVuc,8097
5
5
  mlrun/execution.py,sha256=Up9U6xonTElRIaesF9Vej2JK1Isk2AZNK9ke0XcF5Dg,49030
@@ -38,7 +38,7 @@ mlrun/common/formatters/project.py,sha256=0G4lhcTAsxQCxd40dKC4894cMH8nKt03BcGyp9
38
38
  mlrun/common/formatters/run.py,sha256=Gcf9lVDqxPMNfWcPX0RJasjTC_N_U0yTBkQ02jOPJ7A,1062
39
39
  mlrun/common/model_monitoring/__init__.py,sha256=kXGBqhLN0rlAx0kTXhozGzFsIdSqW0uTSKMmsLgq_is,569
40
40
  mlrun/common/model_monitoring/helpers.py,sha256=lV86teJYoE3MNDx4yhpbzO1KylWmvDbuNODw5yGZwgs,2943
41
- mlrun/common/runtimes/constants.py,sha256=Mok3m9Rv182TTMp7uYNfWalm9Xcz86yva-4fTxfMOPI,10988
41
+ mlrun/common/runtimes/constants.py,sha256=07wD1g8QjXZe1fm2hSMOxZG19aAUsEZM8WeXnyoBd6Q,12127
42
42
  mlrun/common/schemas/__init__.py,sha256=PBuIAhXSkVEVxxKcv5hR_xvTwNAUqxOXHVPugOoWTyM,5386
43
43
  mlrun/common/schemas/alert.py,sha256=G9lFTXFYDor-RVLpJxMorIPlLWr_-GYCFKRN9DkKwXs,10124
44
44
  mlrun/common/schemas/api_gateway.py,sha256=3a0QxECLmoDkD5IiOKtXJL-uiWB26Hg55WMA3nULYuI,7127
@@ -72,9 +72,9 @@ mlrun/common/schemas/secret.py,sha256=CCxFYiPwJtDxwg2VVJH9nUG9cAZ2a34IjeuaWv-BYl
72
72
  mlrun/common/schemas/tag.py,sha256=HRZi5QZ4vVGaCr2AMk9eJgcNiAIXmH4YDc8a4fvF770,893
73
73
  mlrun/common/schemas/workflow.py,sha256=rwYzDJYxpE9k4kC88j_eUCmqK4ZsWV_h-_nli7Fs7Ow,2078
74
74
  mlrun/common/schemas/model_monitoring/__init__.py,sha256=jz0fvdn8BEecgUCKhiSNH6QtFhSW4O19Ql9KXo0AxOg,1900
75
- mlrun/common/schemas/model_monitoring/constants.py,sha256=KHpZiTruqr1iQ4XfEz8Ptj9ytCBuvwOXp30YTt5YQws,11834
75
+ mlrun/common/schemas/model_monitoring/constants.py,sha256=kok1NFXJDicmcJ30MhOqR-vxd185sotXYU0CGLdrQxA,12082
76
76
  mlrun/common/schemas/model_monitoring/grafana.py,sha256=Rq10KKOyyUYr7qOQFZfwGZtUim0LY9O0LQ5uc9jmIVQ,1562
77
- mlrun/common/schemas/model_monitoring/model_endpoints.py,sha256=j60-_puybc4yLlmoWkZ04m6PuY4p5yfzVJpPo0_n1sY,11725
77
+ mlrun/common/schemas/model_monitoring/model_endpoints.py,sha256=0gBH-KnDDbGLOkiqHtk0_iNIdW-NPVy0TKJnZ1fDcEQ,11807
78
78
  mlrun/data_types/__init__.py,sha256=unRo9GGwCmj0hBKBRsXJ2P4BzpQaddlQTvIrVQaKluI,984
79
79
  mlrun/data_types/data_types.py,sha256=0_oKLC6-sXL2_nnaDMP_HSXB3fD1nJAG4J2Jq6sGNNw,4998
80
80
  mlrun/data_types/infer.py,sha256=KdaRgWcqvLkuLjXrMuDr3ik6WY7JP5wJO0Yii_Vl5kw,6173
@@ -107,10 +107,10 @@ mlrun/datastore/wasbfs/__init__.py,sha256=s5Ul-0kAhYqFjKDR2X0O2vDGDbLQQduElb32Ev
107
107
  mlrun/datastore/wasbfs/fs.py,sha256=ge8NK__5vTcFT-krI155_8RDUywQw4SIRX6BWATXy9Q,6299
108
108
  mlrun/db/__init__.py,sha256=WqJ4x8lqJ7ZoKbhEyFqkYADd9P6E3citckx9e9ZLcIU,1163
109
109
  mlrun/db/auth_utils.py,sha256=hpg8D2r82oN0BWabuWN04BTNZ7jYMAF242YSUpK7LFM,5211
110
- mlrun/db/base.py,sha256=aFtDl4J_yeEovso7uvnnn9KLYMnIRiS52qM1uemdG8k,30218
110
+ mlrun/db/base.py,sha256=Ke3atYsWfwhTlo3OoLKAz9gOkmu_a18De1lNKxV29Cc,30379
111
111
  mlrun/db/factory.py,sha256=yP2vVmveUE7LYTCHbS6lQIxP9rW--zdISWuPd_I3d_4,2111
112
- mlrun/db/httpdb.py,sha256=VXp3ETu5fl_-6lEF_bsUerKESuRXbAFxLYvduH6DlKs,228070
113
- mlrun/db/nopdb.py,sha256=v285LHP_Onfuo8KRF078IAPHIXTeEhQsU58QoY7x-b0,26673
112
+ mlrun/db/httpdb.py,sha256=n1bGlooT2OQnPmzzFyKAPJO-2pCWf5B0EHctHa1h9rQ,230865
113
+ mlrun/db/nopdb.py,sha256=rte_AcP2itEwi7wpA26SH-AOAzm5ntPPoqXFHHtuewE,26834
114
114
  mlrun/feature_store/__init__.py,sha256=AVnY2AFUNc2dKxLLUMx2K3Wo1eGviv0brDcYlDnmtf4,1506
115
115
  mlrun/feature_store/api.py,sha256=qkojZpzqGAn3r9ww0ynBRKOs8ji8URaK4DSYD4SE-CE,50395
116
116
  mlrun/feature_store/common.py,sha256=Z7USI-d1fo0iwBMsqMBtJflJfyuiV3BLoDXQPSAoBAs,12826
@@ -130,7 +130,7 @@ mlrun/frameworks/parallel_coordinates.py,sha256=UuZ0b0ACsaaH0rDya_0YMOWwaH6zhEyD
130
130
  mlrun/frameworks/_common/__init__.py,sha256=1ovfHxNW8V9ERVVZx8lPFVGBtsXHaHli7pZPR-Ixn8g,860
131
131
  mlrun/frameworks/_common/artifacts_library.py,sha256=O0z74o3Z6k5NruTqXMLDugZ6midOmSFqNlt7WhYIRP8,8517
132
132
  mlrun/frameworks/_common/mlrun_interface.py,sha256=gjNV_siKyJ7xZdwoH8uvRmPuuQrc8WjnhoXr3GUCkAc,21093
133
- mlrun/frameworks/_common/model_handler.py,sha256=0hsLFxMNaLhGIeXCMeHqUp7p2L9lJgcQKI4b7ef8CLQ,55596
133
+ mlrun/frameworks/_common/model_handler.py,sha256=xoelZloxZ1BcLX7krwWrs-TmqIn52CRoPvGsB7Hoar4,55544
134
134
  mlrun/frameworks/_common/plan.py,sha256=Yr98b5lkCV0K0u_krnU8gZJiXj14xfrFjJ6xD6QJdn0,3444
135
135
  mlrun/frameworks/_common/producer.py,sha256=R67XRbiz1bk0XNvuW7ybbA4v6o6Q5qzD0h3AkC-tM1A,5766
136
136
  mlrun/frameworks/_common/utils.py,sha256=NqoKbgj6UGPMBNhpK6mkKK4GOt5ko1lDqExFhQm9oEc,9131
@@ -220,7 +220,7 @@ mlrun/model_monitoring/api.py,sha256=nH5aEUkmUEJF0CurrWJxmxVv1tQed2yzCLhQByG1L00
220
220
  mlrun/model_monitoring/controller.py,sha256=dBfZQswF67vqeUFnmgsm9jU_5sOs9dLwMPEiYHG-Kk8,19786
221
221
  mlrun/model_monitoring/features_drift_table.py,sha256=c6GpKtpOJbuT1u5uMWDL_S-6N4YPOmlktWMqPme3KFY,25308
222
222
  mlrun/model_monitoring/helpers.py,sha256=6L-IO4EUAYoAf74snGNiGDln_p77OIdxVdrf392VBzY,17936
223
- mlrun/model_monitoring/stream_processing.py,sha256=ltCVgo_b3yay16CUbqeGkRfzCHZSn14lVeBng5m9keY,31738
223
+ mlrun/model_monitoring/stream_processing.py,sha256=wAPKdySwh8kGc7k6Zqu952X5E7uAom2J_YJmPXQUAlA,32859
224
224
  mlrun/model_monitoring/tracking_policy.py,sha256=PBIGrUYWrwcE5gwXupBIVzOb0QRRwPJsgQm_yLGQxB4,5595
225
225
  mlrun/model_monitoring/writer.py,sha256=vbL7bqTyNu8q4bNcebX72sUMybVDAoTWg-CXq4fov3Y,8429
226
226
  mlrun/model_monitoring/applications/__init__.py,sha256=QYvzgCutFdAkzqKPD3mvkX_3c1X4tzd-kW8ojUOE9ic,889
@@ -237,12 +237,12 @@ mlrun/model_monitoring/db/tsdb/__init__.py,sha256=_ejhGA-bi5-6kEMEVYUN5TLxND8hTj
237
237
  mlrun/model_monitoring/db/tsdb/base.py,sha256=JjLBzZXE4ZxtBmihVXjUYZ2HKmgqX03ZhUynXp4948o,25372
238
238
  mlrun/model_monitoring/db/tsdb/helpers.py,sha256=0oUXc4aUkYtP2SGP6jTb3uPPKImIUsVsrb9otX9a7O4,1189
239
239
  mlrun/model_monitoring/db/tsdb/tdengine/__init__.py,sha256=vgBdsKaXUURKqIf3M0y4sRatmSVA4CQiJs7J5dcVBkQ,620
240
- mlrun/model_monitoring/db/tsdb/tdengine/schemas.py,sha256=6de8P0CJMqe7PGttoZNt9UtrbBcJnpIp82hk_MbtepA,12477
240
+ mlrun/model_monitoring/db/tsdb/tdengine/schemas.py,sha256=qfKDUZhgteL0mp2A1aP1iMmcthgUMKmZqMUidZjQktQ,12649
241
241
  mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py,sha256=Uadj0UvAmln2MxDWod-kAzau1uNlqZh981rPhbUH_5M,2857
242
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py,sha256=IEpJknjqx_LYcZjIccPuujOfEruXsRm8c8YzYaWWNEQ,30175
242
+ mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py,sha256=Be-DdprjCdRQz-Goxy5sRxBdHp94V7d868N_IBEQxrY,30205
243
243
  mlrun/model_monitoring/db/tsdb/v3io/__init__.py,sha256=aL3bfmQsUQ-sbvKGdNihFj8gLCK3mSys0qDcXtYOwgc,616
244
244
  mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py,sha256=_-zo9relCDtjGgievxAcAP9gVN9nDWs8BzGtFwTjb9M,6284
245
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py,sha256=9j_TWS_3OXTwjiYw9jb92z8URHUXDW7hCgwZnpb2P8c,36834
245
+ mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py,sha256=O9j0Hnmz5LbaJO2RxKRSDJU54MOCjICap2G9tuut8JE,37044
246
246
  mlrun/model_monitoring/metrics/__init__.py,sha256=6CsTXAxeLbbf8yfCADTaxmiavqwrLEdYFJ-qc5kgDAY,569
247
247
  mlrun/model_monitoring/metrics/histogram_distance.py,sha256=E9_WIl2vd6qNvoHVHoFcnuQk3ekbFWOdi8aU7sHrfk4,4724
248
248
  mlrun/package/__init__.py,sha256=v7VDyK9kDOOuDvFo4oiGV2fx-vM1KL7fdN9pGLakhUQ,7008
@@ -266,8 +266,8 @@ mlrun/platforms/__init__.py,sha256=ZuyeHCHHUxYEoZRmaJqzFSfwhaTyUdBZXMeVp75ql1w,3
266
266
  mlrun/platforms/iguazio.py,sha256=6VBTq8eQ3mzT96tzjYhAtcMQ2VjF4x8LpIPW5DAcX2Q,13749
267
267
  mlrun/projects/__init__.py,sha256=0Krf0WIKfnZa71WthYOg0SoaTodGg3sV_hK3f_OlTPI,1220
268
268
  mlrun/projects/operations.py,sha256=VXUlMrouFTls-I-bMhdN5pPfQ34TR7bFQ-NUSWNvl84,20029
269
- mlrun/projects/pipelines.py,sha256=vZpyiERUzwPMS7NCC5ghI0KB_DItIddr7MMWGTwLawY,47437
270
- mlrun/projects/project.py,sha256=WKIgBy1nqHxQ4d2AHId6wptnGeYzfZTY61K8pOQUrvI,228386
269
+ mlrun/projects/pipelines.py,sha256=UwRwK2e8T0463quQ3d-ud_swmTLkBuSVUroY4YkgzZU,47903
270
+ mlrun/projects/project.py,sha256=C1OcldXuyCUIucvPFQX-tJbwjBkht8Ocn4m7lTHNZeM,229691
271
271
  mlrun/runtimes/__init__.py,sha256=J9Sy2HiyMlztNv6VUurMzF5H2XzttNil8nRsWDsqLyg,8923
272
272
  mlrun/runtimes/base.py,sha256=Yt2l7srrXjK783cunBEKH0yQxQZRH8lkedXNOXuLbbo,37841
273
273
  mlrun/runtimes/daskjob.py,sha256=JwuGvOiPsxEDHHMMUS4Oie4hLlYYIZwihAl6DjroTY0,19521
@@ -291,7 +291,7 @@ mlrun/runtimes/nuclio/__init__.py,sha256=gx1kizzKv8pGT5TNloN1js1hdbxqDw3rM90sLVY
291
291
  mlrun/runtimes/nuclio/api_gateway.py,sha256=vH9ClKVP4Mb24rvA67xPuAvAhX-gAv6vVtjVxyplhdc,26969
292
292
  mlrun/runtimes/nuclio/function.py,sha256=Bff8Veg-eaqNrQ7yn20HpRhwAO4OA7FTnzXnAyoaBPU,52365
293
293
  mlrun/runtimes/nuclio/nuclio.py,sha256=sLK8KdGO1LbftlL3HqPZlFOFTAAuxJACZCVl1c0Ha6E,2942
294
- mlrun/runtimes/nuclio/serving.py,sha256=yuBSJe8AM4ar87Tc9x0vLRc47w8f5r-joMNza9hVCOo,31399
294
+ mlrun/runtimes/nuclio/serving.py,sha256=NG9OOTxbOGXhyVPI8BjIcZX5CDXvyk931o5DYOSfCg0,31977
295
295
  mlrun/runtimes/nuclio/application/__init__.py,sha256=rRs5vasy_G9IyoTpYIjYDafGoL6ifFBKgBtsXn31Atw,614
296
296
  mlrun/runtimes/nuclio/application/application.py,sha256=HlEq4A6hbFqr3Ba3TL4m7nbmfMYI06Zb_NAKGjzkEFU,29242
297
297
  mlrun/runtimes/nuclio/application/reverse_proxy.go,sha256=JIIYae6bXzCLf3jXuu49KWPQYoXr_FDQ2Rbo1OWKAd0,3150
@@ -306,7 +306,7 @@ mlrun/serving/serving_wrapper.py,sha256=R670-S6PX_d5ER6jiHtRvacuPyFzQH0mEf2K0sBI
306
306
  mlrun/serving/states.py,sha256=g6UIeaS6B9v8k4eDMmOxyoB8Gdqm9PiNIkeuzDyTJA8,67565
307
307
  mlrun/serving/utils.py,sha256=k2EIYDWHUGkE-IBI6T0UNT32fw-KySsccIJM_LObI00,4171
308
308
  mlrun/serving/v1_serving.py,sha256=c6J_MtpE-Tqu00-6r4eJOCO6rUasHDal9W2eBIcrl50,11853
309
- mlrun/serving/v2_serving.py,sha256=B1Vmca2_YidXyit4wuxR6JGooMGdaeZI3Ja90JHCz10,21882
309
+ mlrun/serving/v2_serving.py,sha256=Nm-mPa40JlhpAFH5JuaS4Kc38g_o70cBPGgrqo7zDRM,22525
310
310
  mlrun/track/__init__.py,sha256=yVXbT52fXvGKRlc_ByHqIVt7-9L3DRE634RSeQwgXtU,665
311
311
  mlrun/track/tracker.py,sha256=CyTU6Qd3_5GGEJ_hpocOj71wvV65EuFYUjaYEUKAL6Q,3575
312
312
  mlrun/track/tracker_manager.py,sha256=IYBl99I62IC6VCCmG1yt6JoHNOQXa53C4DURJ2sWgio,5726
@@ -318,7 +318,7 @@ mlrun/utils/azure_vault.py,sha256=IEFizrDGDbAaoWwDr1WoA88S_EZ0T--vjYtY-i0cvYQ,34
318
318
  mlrun/utils/clones.py,sha256=y3zC9QS7z5mLuvyQ6vFd6sJnikbgtDwrBvieQq0sovY,7359
319
319
  mlrun/utils/condition_evaluator.py,sha256=-nGfRmZzivn01rHTroiGY4rqEv8T1irMyhzxEei-sKc,1897
320
320
  mlrun/utils/db.py,sha256=blQgkWMfFH9lcN4sgJQcPQgEETz2Dl_zwbVA0SslpFg,2186
321
- mlrun/utils/helpers.py,sha256=aPMmd5dEm7PIbx7maXtNGhNKzEHp8JK8uSmOgTtoDbc,69549
321
+ mlrun/utils/helpers.py,sha256=TYDhMDWGtkrMJLslanSAb1sg7kR92q3etOWpTcqCoJk,70499
322
322
  mlrun/utils/http.py,sha256=t6FrXQstZm9xVVjxqIGiLzrwZNCR4CSienSOuVgNIcI,8706
323
323
  mlrun/utils/logger.py,sha256=_v4UTv1-STzC2c6aAWAa0NNl9STQoBYbR3OHgAiL41s,14606
324
324
  mlrun/utils/regex.py,sha256=IQqwPna6Z8J31xkTUduYbGk48GkQBUJFZSuxAWm1pzU,5162
@@ -327,7 +327,7 @@ mlrun/utils/singleton.py,sha256=p1Y-X0mPSs_At092GS-pZCA8CTR62HOqPU07_ZH6-To,869
327
327
  mlrun/utils/v3io_clients.py,sha256=0aCFiQFBmgdSeLzJr_nEP6SG-zyieSgH8RdtcUq4dc0,1294
328
328
  mlrun/utils/vault.py,sha256=xUiKL17dCXjwQJ33YRzQj0oadUXATlFWPzKKYAESoQk,10447
329
329
  mlrun/utils/notifications/__init__.py,sha256=eUzQDBxSQmMZASRY-YAnYS6tL5801P0wEjycp3Dvoe0,990
330
- mlrun/utils/notifications/notification_pusher.py,sha256=WN7RMfaZtCoG3bNfMjub1phbvJ2Xw8lKFiq2GStnKGw,24132
330
+ mlrun/utils/notifications/notification_pusher.py,sha256=9KXwuMGgA340TfYlMbudlJ-MEwOyBJbbJGu_iBrIhrM,25106
331
331
  mlrun/utils/notifications/notification/__init__.py,sha256=9Rfy6Jm8n0LaEDO1VAQb6kIbr7_uVuQhK1pS_abELIY,2581
332
332
  mlrun/utils/notifications/notification/base.py,sha256=VOgrzRakRfjYYBqvkc0cgEC5pl7KMidP7u-TL4HpGCY,5280
333
333
  mlrun/utils/notifications/notification/console.py,sha256=ICbIhOf9fEBJky_3j9TFiKAewDGyDHJr9l4VeT7G2sc,2745
@@ -337,11 +337,11 @@ mlrun/utils/notifications/notification/mail.py,sha256=ZyJ3eqd8simxffQmXzqd3bgbAq
337
337
  mlrun/utils/notifications/notification/slack.py,sha256=NKV4RFiY3gLsS8uPppgniPLyag8zJ9O1VhixoXkM7kw,7108
338
338
  mlrun/utils/notifications/notification/webhook.py,sha256=NeyIMSBojjjTJaUHmPbxMByp34GxYkl1-16NqzU27fU,4943
339
339
  mlrun/utils/version/__init__.py,sha256=7kkrB7hEZ3cLXoWj1kPoDwo4MaswsI2JVOBpbKgPAgc,614
340
- mlrun/utils/version/version.json,sha256=9e_fs0UZTGdzgD6yjggI41O7Hv5n_tbuYDMtJsSNspY,89
340
+ mlrun/utils/version/version.json,sha256=sRM_WBR-ofAH26VoS0Q65ShgfsiCSPD48jGAO0SgsQs,89
341
341
  mlrun/utils/version/version.py,sha256=eEW0tqIAkU9Xifxv8Z9_qsYnNhn3YH7NRAfM-pPLt1g,1878
342
- mlrun-1.8.0rc19.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
343
- mlrun-1.8.0rc19.dist-info/METADATA,sha256=A2YOQIMx1P8pckWVn0bKsr8oEpcWxVbhkXhPxTplJMM,24885
344
- mlrun-1.8.0rc19.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
345
- mlrun-1.8.0rc19.dist-info/entry_points.txt,sha256=1Owd16eAclD5pfRCoJpYC2ZJSyGNTtUr0nCELMioMmU,46
346
- mlrun-1.8.0rc19.dist-info/top_level.txt,sha256=NObLzw3maSF9wVrgSeYBv-fgnHkAJ1kEkh12DLdd5KM,6
347
- mlrun-1.8.0rc19.dist-info/RECORD,,
342
+ mlrun-1.8.0rc20.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
343
+ mlrun-1.8.0rc20.dist-info/METADATA,sha256=_8piCk7gLmlLxJNtutoEqFwdr9jK71ACI8Q6yxDd8Do,24885
344
+ mlrun-1.8.0rc20.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
345
+ mlrun-1.8.0rc20.dist-info/entry_points.txt,sha256=1Owd16eAclD5pfRCoJpYC2ZJSyGNTtUr0nCELMioMmU,46
346
+ mlrun-1.8.0rc20.dist-info/top_level.txt,sha256=NObLzw3maSF9wVrgSeYBv-fgnHkAJ1kEkh12DLdd5KM,6
347
+ mlrun-1.8.0rc20.dist-info/RECORD,,