mlrun 1.7.0rc22__py3-none-any.whl → 1.7.0rc28__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 (81) hide show
  1. mlrun/__main__.py +10 -8
  2. mlrun/alerts/alert.py +13 -1
  3. mlrun/artifacts/manager.py +5 -0
  4. mlrun/common/constants.py +2 -2
  5. mlrun/common/formatters/__init__.py +1 -0
  6. mlrun/common/formatters/artifact.py +26 -3
  7. mlrun/common/formatters/base.py +9 -9
  8. mlrun/common/formatters/run.py +26 -0
  9. mlrun/common/helpers.py +11 -0
  10. mlrun/common/schemas/__init__.py +4 -0
  11. mlrun/common/schemas/alert.py +5 -9
  12. mlrun/common/schemas/api_gateway.py +64 -16
  13. mlrun/common/schemas/artifact.py +11 -0
  14. mlrun/common/schemas/constants.py +3 -0
  15. mlrun/common/schemas/feature_store.py +58 -28
  16. mlrun/common/schemas/model_monitoring/constants.py +21 -12
  17. mlrun/common/schemas/model_monitoring/model_endpoints.py +0 -12
  18. mlrun/common/schemas/pipeline.py +16 -0
  19. mlrun/common/schemas/project.py +17 -0
  20. mlrun/common/schemas/runs.py +17 -0
  21. mlrun/common/schemas/schedule.py +1 -1
  22. mlrun/common/types.py +5 -0
  23. mlrun/config.py +10 -25
  24. mlrun/datastore/azure_blob.py +2 -1
  25. mlrun/datastore/datastore.py +3 -3
  26. mlrun/datastore/google_cloud_storage.py +6 -2
  27. mlrun/datastore/snowflake_utils.py +3 -1
  28. mlrun/datastore/sources.py +26 -11
  29. mlrun/datastore/store_resources.py +2 -0
  30. mlrun/datastore/targets.py +68 -16
  31. mlrun/db/base.py +64 -2
  32. mlrun/db/httpdb.py +129 -41
  33. mlrun/db/nopdb.py +44 -3
  34. mlrun/errors.py +5 -3
  35. mlrun/execution.py +18 -10
  36. mlrun/feature_store/retrieval/spark_merger.py +2 -1
  37. mlrun/frameworks/__init__.py +0 -6
  38. mlrun/model.py +23 -0
  39. mlrun/model_monitoring/api.py +6 -52
  40. mlrun/model_monitoring/applications/histogram_data_drift.py +1 -1
  41. mlrun/model_monitoring/db/stores/__init__.py +37 -24
  42. mlrun/model_monitoring/db/stores/base/store.py +40 -1
  43. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +42 -87
  44. mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +27 -35
  45. mlrun/model_monitoring/db/tsdb/__init__.py +15 -15
  46. mlrun/model_monitoring/db/tsdb/base.py +1 -1
  47. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +6 -4
  48. mlrun/model_monitoring/helpers.py +17 -9
  49. mlrun/model_monitoring/stream_processing.py +9 -11
  50. mlrun/model_monitoring/writer.py +11 -11
  51. mlrun/package/__init__.py +1 -13
  52. mlrun/package/packagers/__init__.py +1 -6
  53. mlrun/projects/pipelines.py +10 -9
  54. mlrun/projects/project.py +95 -81
  55. mlrun/render.py +10 -5
  56. mlrun/run.py +13 -8
  57. mlrun/runtimes/base.py +11 -4
  58. mlrun/runtimes/daskjob.py +7 -1
  59. mlrun/runtimes/local.py +16 -3
  60. mlrun/runtimes/nuclio/application/application.py +0 -2
  61. mlrun/runtimes/nuclio/function.py +20 -0
  62. mlrun/runtimes/nuclio/serving.py +9 -6
  63. mlrun/runtimes/pod.py +5 -29
  64. mlrun/serving/routers.py +75 -59
  65. mlrun/serving/server.py +11 -0
  66. mlrun/serving/states.py +29 -0
  67. mlrun/serving/v2_serving.py +62 -39
  68. mlrun/utils/helpers.py +39 -1
  69. mlrun/utils/logger.py +36 -2
  70. mlrun/utils/notifications/notification/base.py +43 -7
  71. mlrun/utils/notifications/notification/git.py +21 -0
  72. mlrun/utils/notifications/notification/slack.py +9 -14
  73. mlrun/utils/notifications/notification/webhook.py +41 -1
  74. mlrun/utils/notifications/notification_pusher.py +3 -9
  75. mlrun/utils/version/version.json +2 -2
  76. {mlrun-1.7.0rc22.dist-info → mlrun-1.7.0rc28.dist-info}/METADATA +12 -7
  77. {mlrun-1.7.0rc22.dist-info → mlrun-1.7.0rc28.dist-info}/RECORD +81 -80
  78. {mlrun-1.7.0rc22.dist-info → mlrun-1.7.0rc28.dist-info}/WHEEL +1 -1
  79. {mlrun-1.7.0rc22.dist-info → mlrun-1.7.0rc28.dist-info}/LICENSE +0 -0
  80. {mlrun-1.7.0rc22.dist-info → mlrun-1.7.0rc28.dist-info}/entry_points.txt +0 -0
  81. {mlrun-1.7.0rc22.dist-info → mlrun-1.7.0rc28.dist-info}/top_level.txt +0 -0
mlrun/runtimes/daskjob.py CHANGED
@@ -548,7 +548,13 @@ class DaskCluster(KubejobRuntime):
548
548
  "specified handler (string) without command "
549
549
  "(py file path), specify command or use handler pointer"
550
550
  )
551
- handler = load_module(self.spec.command, handler, context=context)
551
+ # Do not embed the module in system as it is not persistent with the dask cluster
552
+ handler = load_module(
553
+ self.spec.command,
554
+ handler,
555
+ context=context,
556
+ embed_in_sys=False,
557
+ )
552
558
  client = self.client
553
559
  setattr(context, "dask_client", client)
554
560
  sout, serr = exec_from_params(handler, runobj, context)
mlrun/runtimes/local.py CHANGED
@@ -372,8 +372,20 @@ class LocalRuntime(BaseRuntime, ParallelRunner):
372
372
  return run_obj_dict
373
373
 
374
374
 
375
- def load_module(file_name, handler, context):
376
- """Load module from file name"""
375
+ def load_module(
376
+ file_name: str,
377
+ handler: str,
378
+ context: MLClientCtx,
379
+ embed_in_sys: bool = True,
380
+ ):
381
+ """
382
+ Load module from filename
383
+ :param file_name: The module path to load
384
+ :param handler: The callable to load
385
+ :param context: Execution context
386
+ :param embed_in_sys: Embed the file-named module in sys.modules. This is not persistent with remote
387
+ environments and therefore can effect pickling.
388
+ """
377
389
  module = None
378
390
  if file_name:
379
391
  path = Path(file_name)
@@ -384,7 +396,8 @@ def load_module(file_name, handler, context):
384
396
  if spec is None:
385
397
  raise RunError(f"Cannot import from {file_name!r}")
386
398
  module = imputil.module_from_spec(spec)
387
- sys.modules[mod_name] = module
399
+ if embed_in_sys:
400
+ sys.modules[mod_name] = module
388
401
  spec.loader.exec_module(module)
389
402
 
390
403
  class_args = {}
@@ -263,7 +263,6 @@ class ApplicationRuntime(RemoteRuntime):
263
263
  is_kfp=False,
264
264
  mlrun_version_specifier=None,
265
265
  show_on_failure: bool = False,
266
- skip_access_key_auth: bool = False,
267
266
  direct_port_access: bool = False,
268
267
  authentication_mode: schemas.APIGatewayAuthenticationMode = None,
269
268
  authentication_creds: tuple[str] = None,
@@ -283,7 +282,6 @@ class ApplicationRuntime(RemoteRuntime):
283
282
  :param is_kfp: Deploy as part of a kfp pipeline
284
283
  :param mlrun_version_specifier: Which mlrun package version to include (if not current)
285
284
  :param show_on_failure: Show logs only in case of build failure
286
- :param skip_access_key_auth: Skip adding access key auth to the API Gateway
287
285
  :param direct_port_access: Set True to allow direct port access to the application sidecar
288
286
  :param authentication_mode: API Gateway authentication mode
289
287
  :param authentication_creds: API Gateway authentication credentials as a tuple (username, password)
@@ -1327,3 +1327,23 @@ def get_nuclio_deploy_status(
1327
1327
  else:
1328
1328
  text = "\n".join(outputs) if outputs else ""
1329
1329
  return state, address, name, last_log_timestamp, text, function_status
1330
+
1331
+
1332
+ def enrich_nuclio_function_from_headers(
1333
+ func: RemoteRuntime,
1334
+ headers: dict,
1335
+ ):
1336
+ func.status.state = headers.get("x-mlrun-function-status", "")
1337
+ func.status.address = headers.get("x-mlrun-address", "")
1338
+ func.status.nuclio_name = headers.get("x-mlrun-name", "")
1339
+ func.status.internal_invocation_urls = (
1340
+ headers.get("x-mlrun-internal-invocation-urls", "").split(",")
1341
+ if headers.get("x-mlrun-internal-invocation-urls")
1342
+ else []
1343
+ )
1344
+ func.status.external_invocation_urls = (
1345
+ headers.get("x-mlrun-external-invocation-urls", "").split(",")
1346
+ if headers.get("x-mlrun-external-invocation-urls")
1347
+ else []
1348
+ )
1349
+ func.status.container_image = headers.get("x-mlrun-container-image", "")
@@ -312,15 +312,18 @@ class ServingRuntime(RemoteRuntime):
312
312
  sample: Optional[int] = None,
313
313
  stream_args: Optional[dict] = None,
314
314
  tracking_policy: Optional[Union["TrackingPolicy", dict]] = None,
315
+ enable_tracking: bool = True,
315
316
  ) -> None:
316
317
  """apply on your serving function to monitor a deployed model, including real-time dashboards to detect drift
317
318
  and analyze performance.
318
319
 
319
- :param stream_path: Path/url of the tracking stream e.g. v3io:///users/mike/mystream
320
- you can use the "dummy://" path for test/simulation.
321
- :param batch: Micro batch size (send micro batches of N records at a time).
322
- :param sample: Sample size (send only one of N records).
323
- :param stream_args: Stream initialization parameters, e.g. shards, retention_in_hours, ..
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).
324
327
 
325
328
  example::
326
329
 
@@ -331,7 +334,7 @@ class ServingRuntime(RemoteRuntime):
331
334
 
332
335
  """
333
336
  # Applying model monitoring configurations
334
- self.spec.track_models = True
337
+ self.spec.track_models = enable_tracking
335
338
 
336
339
  if stream_path:
337
340
  self.spec.parameters["log_stream"] = stream_path
mlrun/runtimes/pod.py CHANGED
@@ -532,7 +532,9 @@ class KubeResourceSpec(FunctionSpec):
532
532
  return
533
533
 
534
534
  # merge node selectors - precedence to existing node selector
535
- self.node_selector = {**node_selector, **self.node_selector}
535
+ self.node_selector = mlrun.utils.helpers.merge_with_precedence(
536
+ node_selector, self.node_selector
537
+ )
536
538
 
537
539
  def _merge_tolerations(
538
540
  self,
@@ -1038,32 +1040,6 @@ class KubeResource(BaseRuntime, KfpAdapterMixin):
1038
1040
  return True
1039
1041
  return False
1040
1042
 
1041
- def enrich_runtime_spec(
1042
- self,
1043
- project_node_selector: dict[str, str],
1044
- ):
1045
- """
1046
- Enriches the runtime spec with the project-level node selector.
1047
-
1048
- This method merges the project-level node selector with the existing function node_selector.
1049
- The merge logic used here combines the two dictionaries, giving precedence to
1050
- the keys in the runtime node_selector. If there are conflicting keys between the
1051
- two dictionaries, the values from self.spec.node_selector will overwrite the
1052
- values from project_node_selector.
1053
-
1054
- Example:
1055
- Suppose self.spec.node_selector = {"type": "gpu", "zone": "us-east-1"}
1056
- and project_node_selector = {"type": "cpu", "environment": "production"}.
1057
- After the merge, the resulting node_selector will be:
1058
- {"type": "gpu", "zone": "us-east-1", "environment": "production"}
1059
-
1060
- Note:
1061
- - The merge uses the ** operator, also known as the "unpacking" operator in Python,
1062
- combining key-value pairs from each dictionary. Later dictionaries take precedence
1063
- when there are conflicting keys.
1064
- """
1065
- self.spec.node_selector = {**project_node_selector, **self.spec.node_selector}
1066
-
1067
1043
  def _set_env(self, name, value=None, value_from=None):
1068
1044
  new_var = k8s_client.V1EnvVar(name=name, value=value, value_from=value_from)
1069
1045
 
@@ -1542,7 +1518,7 @@ def get_sanitized_attribute(spec, attribute_name: str):
1542
1518
 
1543
1519
  # check if attribute of type dict, and then check if type is sanitized
1544
1520
  if isinstance(attribute, dict):
1545
- if attribute_config["not_sanitized_class"] != dict:
1521
+ if not isinstance(attribute_config["not_sanitized_class"], dict):
1546
1522
  raise mlrun.errors.MLRunInvalidArgumentTypeError(
1547
1523
  f"expected to be of type {attribute_config.get('not_sanitized_class')} but got dict"
1548
1524
  )
@@ -1552,7 +1528,7 @@ def get_sanitized_attribute(spec, attribute_name: str):
1552
1528
  elif isinstance(attribute, list) and not isinstance(
1553
1529
  attribute[0], attribute_config["sub_attribute_type"]
1554
1530
  ):
1555
- if attribute_config["not_sanitized_class"] != list:
1531
+ if not isinstance(attribute_config["not_sanitized_class"], list):
1556
1532
  raise mlrun.errors.MLRunInvalidArgumentTypeError(
1557
1533
  f"expected to be of type {attribute_config.get('not_sanitized_class')} but got list"
1558
1534
  )
mlrun/serving/routers.py CHANGED
@@ -1030,74 +1030,90 @@ def _init_endpoint_record(
1030
1030
  function_uri=graph_server.function_uri, versioned_model=versioned_model_name
1031
1031
  ).uid
1032
1032
 
1033
- # If model endpoint object was found in DB, skip the creation process.
1034
1033
  try:
1035
- mlrun.get_run_db().get_model_endpoint(project=project, endpoint_id=endpoint_uid)
1036
-
1034
+ model_ep = mlrun.get_run_db().get_model_endpoint(
1035
+ project=project, endpoint_id=endpoint_uid
1036
+ )
1037
1037
  except mlrun.errors.MLRunNotFoundError:
1038
- logger.info("Creating a new model endpoint record", endpoint_id=endpoint_uid)
1038
+ model_ep = None
1039
+ except mlrun.errors.MLRunBadRequestError as err:
1040
+ logger.debug(
1041
+ f"Cant reach to model endpoints store, due to : {err}",
1042
+ )
1043
+ return
1039
1044
 
1040
- try:
1041
- # Get the children model endpoints ids
1042
- children_uids = []
1043
- for _, c in voting_ensemble.routes.items():
1044
- if hasattr(c, "endpoint_uid"):
1045
- children_uids.append(c.endpoint_uid)
1046
-
1047
- model_endpoint = mlrun.common.schemas.ModelEndpoint(
1048
- metadata=mlrun.common.schemas.ModelEndpointMetadata(
1049
- project=project, uid=endpoint_uid
1050
- ),
1051
- spec=mlrun.common.schemas.ModelEndpointSpec(
1052
- function_uri=graph_server.function_uri,
1053
- model=versioned_model_name,
1054
- model_class=voting_ensemble.__class__.__name__,
1055
- stream_path=config.model_endpoint_monitoring.store_prefixes.default.format(
1056
- project=project, kind="stream"
1057
- ),
1058
- active=True,
1059
- monitoring_mode=mlrun.common.schemas.model_monitoring.ModelMonitoringMode.enabled
1060
- if voting_ensemble.context.server.track_models
1061
- else mlrun.common.schemas.model_monitoring.ModelMonitoringMode.disabled,
1062
- ),
1063
- status=mlrun.common.schemas.ModelEndpointStatus(
1064
- children=list(voting_ensemble.routes.keys()),
1065
- endpoint_type=mlrun.common.schemas.model_monitoring.EndpointType.ROUTER,
1066
- children_uids=children_uids,
1045
+ if voting_ensemble.context.server.track_models and not model_ep:
1046
+ logger.info("Creating a new model endpoint record", endpoint_id=endpoint_uid)
1047
+ # Get the children model endpoints ids
1048
+ children_uids = []
1049
+ for _, c in voting_ensemble.routes.items():
1050
+ if hasattr(c, "endpoint_uid"):
1051
+ children_uids.append(c.endpoint_uid)
1052
+ model_endpoint = mlrun.common.schemas.ModelEndpoint(
1053
+ metadata=mlrun.common.schemas.ModelEndpointMetadata(
1054
+ project=project, uid=endpoint_uid
1055
+ ),
1056
+ spec=mlrun.common.schemas.ModelEndpointSpec(
1057
+ function_uri=graph_server.function_uri,
1058
+ model=versioned_model_name,
1059
+ model_class=voting_ensemble.__class__.__name__,
1060
+ stream_path=config.model_endpoint_monitoring.store_prefixes.default.format(
1061
+ project=project, kind="stream"
1067
1062
  ),
1068
- )
1063
+ active=True,
1064
+ monitoring_mode=mlrun.common.schemas.model_monitoring.ModelMonitoringMode.enabled,
1065
+ ),
1066
+ status=mlrun.common.schemas.ModelEndpointStatus(
1067
+ children=list(voting_ensemble.routes.keys()),
1068
+ endpoint_type=mlrun.common.schemas.model_monitoring.EndpointType.ROUTER,
1069
+ children_uids=children_uids,
1070
+ ),
1071
+ )
1069
1072
 
1070
- db = mlrun.get_run_db()
1073
+ db = mlrun.get_run_db()
1074
+
1075
+ db.create_model_endpoint(
1076
+ project=project,
1077
+ endpoint_id=model_endpoint.metadata.uid,
1078
+ model_endpoint=model_endpoint.dict(),
1079
+ )
1071
1080
 
1081
+ # Update model endpoint children type
1082
+ for model_endpoint in children_uids:
1083
+ current_endpoint = db.get_model_endpoint(
1084
+ project=project, endpoint_id=model_endpoint
1085
+ )
1086
+ current_endpoint.status.endpoint_type = (
1087
+ mlrun.common.schemas.model_monitoring.EndpointType.LEAF_EP
1088
+ )
1072
1089
  db.create_model_endpoint(
1073
1090
  project=project,
1074
- endpoint_id=model_endpoint.metadata.uid,
1075
- model_endpoint=model_endpoint.dict(),
1076
- )
1077
-
1078
- # Update model endpoint children type
1079
- for model_endpoint in children_uids:
1080
- current_endpoint = db.get_model_endpoint(
1081
- project=project, endpoint_id=model_endpoint
1082
- )
1083
- current_endpoint.status.endpoint_type = (
1084
- mlrun.common.schemas.model_monitoring.EndpointType.LEAF_EP
1085
- )
1086
- db.create_model_endpoint(
1087
- project=project,
1088
- endpoint_id=model_endpoint,
1089
- model_endpoint=current_endpoint,
1090
- )
1091
-
1092
- except Exception as exc:
1093
- logger.warning(
1094
- "Failed creating model endpoint record",
1095
- exc=err_to_str(exc),
1096
- traceback=traceback.format_exc(),
1091
+ endpoint_id=model_endpoint,
1092
+ model_endpoint=current_endpoint,
1097
1093
  )
1098
-
1099
- except Exception as e:
1100
- logger.error("Failed to retrieve model endpoint object", exc=err_to_str(e))
1094
+ elif (
1095
+ model_ep
1096
+ and (
1097
+ model_ep.spec.monitoring_mode
1098
+ == mlrun.common.schemas.model_monitoring.ModelMonitoringMode.enabled
1099
+ )
1100
+ != voting_ensemble.context.server.track_models
1101
+ ):
1102
+ monitoring_mode = (
1103
+ mlrun.common.schemas.model_monitoring.ModelMonitoringMode.enabled
1104
+ if voting_ensemble.context.server.track_models
1105
+ else mlrun.common.schemas.model_monitoring.ModelMonitoringMode.disabled
1106
+ )
1107
+ db = mlrun.get_run_db()
1108
+ db.patch_model_endpoint(
1109
+ project=project,
1110
+ endpoint_id=endpoint_uid,
1111
+ attributes={"monitoring_mode": monitoring_mode},
1112
+ )
1113
+ logger.debug(
1114
+ f"Updating model endpoint monitoring_mode to {monitoring_mode}",
1115
+ endpoint_id=endpoint_uid,
1116
+ )
1101
1117
 
1102
1118
  return endpoint_uid
1103
1119
 
mlrun/serving/server.py CHANGED
@@ -383,6 +383,17 @@ def v2_serving_handler(context, event, get_body=False):
383
383
  if event.body == b"":
384
384
  event.body = None
385
385
 
386
+ # original path is saved in stream_path so it can be used by explicit ack, but path is reset to / as a
387
+ # workaround for NUC-178
388
+ event.stream_path = event.path
389
+ if hasattr(event, "trigger") and event.trigger.kind in (
390
+ "kafka",
391
+ "kafka-cluster",
392
+ "v3ioStream",
393
+ "v3io-stream",
394
+ ):
395
+ event.path = "/"
396
+
386
397
  return context._server.run(event, context, get_body)
387
398
 
388
399
 
mlrun/serving/states.py CHANGED
@@ -832,6 +832,35 @@ class QueueStep(BaseStep):
832
832
  def async_object(self):
833
833
  return self._async_object
834
834
 
835
+ def to(
836
+ self,
837
+ class_name: Union[str, StepToDict] = None,
838
+ name: str = None,
839
+ handler: str = None,
840
+ graph_shape: str = None,
841
+ function: str = None,
842
+ full_event: bool = None,
843
+ input_path: str = None,
844
+ result_path: str = None,
845
+ **class_args,
846
+ ):
847
+ if not function:
848
+ name = get_name(name, class_name)
849
+ raise mlrun.errors.MLRunInvalidArgumentError(
850
+ f"step '{name}' must specify a function, because it follows a queue step"
851
+ )
852
+ return super().to(
853
+ class_name,
854
+ name,
855
+ handler,
856
+ graph_shape,
857
+ function,
858
+ full_event,
859
+ input_path,
860
+ result_path,
861
+ **class_args,
862
+ )
863
+
835
864
  def run(self, event, *args, **kwargs):
836
865
  data = event.body
837
866
  if not data:
@@ -531,7 +531,9 @@ def _init_endpoint_record(
531
531
  if model.model_path and model.model_path.startswith("store://"):
532
532
  # Enrich the model server with the model artifact metadata
533
533
  model.get_model()
534
- model.version = model.model_spec.tag
534
+ if not model.version:
535
+ # Enrich the model version with the model artifact tag
536
+ model.version = model.model_spec.tag
535
537
  model.labels = model.model_spec.labels
536
538
  versioned_model_name = f"{model.name}:{model.version}"
537
539
  else:
@@ -542,48 +544,69 @@ def _init_endpoint_record(
542
544
  function_uri=graph_server.function_uri, versioned_model=versioned_model_name
543
545
  ).uid
544
546
 
545
- # If model endpoint object was found in DB, skip the creation process.
546
547
  try:
547
- mlrun.get_run_db().get_model_endpoint(project=project, endpoint_id=uid)
548
-
548
+ model_ep = mlrun.get_run_db().get_model_endpoint(
549
+ project=project, endpoint_id=uid
550
+ )
549
551
  except mlrun.errors.MLRunNotFoundError:
550
- logger.info("Creating a new model endpoint record", endpoint_id=uid)
551
-
552
- try:
553
- model_endpoint = mlrun.common.schemas.ModelEndpoint(
554
- metadata=mlrun.common.schemas.ModelEndpointMetadata(
555
- project=project, labels=model.labels, uid=uid
556
- ),
557
- spec=mlrun.common.schemas.ModelEndpointSpec(
558
- function_uri=graph_server.function_uri,
559
- model=versioned_model_name,
560
- model_class=model.__class__.__name__,
561
- model_uri=model.model_path,
562
- stream_path=config.model_endpoint_monitoring.store_prefixes.default.format(
563
- project=project, kind="stream"
564
- ),
565
- active=True,
566
- monitoring_mode=mlrun.common.schemas.model_monitoring.ModelMonitoringMode.enabled
567
- if model.context.server.track_models
568
- else mlrun.common.schemas.model_monitoring.ModelMonitoringMode.disabled,
569
- ),
570
- status=mlrun.common.schemas.ModelEndpointStatus(
571
- endpoint_type=mlrun.common.schemas.model_monitoring.EndpointType.NODE_EP
552
+ model_ep = None
553
+ except mlrun.errors.MLRunBadRequestError as err:
554
+ logger.debug(
555
+ f"Cant reach to model endpoints store, due to : {err}",
556
+ )
557
+ return
558
+
559
+ if model.context.server.track_models and not model_ep:
560
+ logger.debug("Creating a new model endpoint record", endpoint_id=uid)
561
+ model_endpoint = mlrun.common.schemas.ModelEndpoint(
562
+ metadata=mlrun.common.schemas.ModelEndpointMetadata(
563
+ project=project, labels=model.labels, uid=uid
564
+ ),
565
+ spec=mlrun.common.schemas.ModelEndpointSpec(
566
+ function_uri=graph_server.function_uri,
567
+ model=versioned_model_name,
568
+ model_class=model.__class__.__name__,
569
+ model_uri=model.model_path,
570
+ stream_path=config.model_endpoint_monitoring.store_prefixes.default.format(
571
+ project=project, kind="stream"
572
572
  ),
573
- )
574
-
575
- db = mlrun.get_run_db()
576
-
577
- db.create_model_endpoint(
578
- project=project,
579
- endpoint_id=uid,
580
- model_endpoint=model_endpoint.dict(),
581
- )
573
+ active=True,
574
+ monitoring_mode=mlrun.common.schemas.model_monitoring.ModelMonitoringMode.enabled,
575
+ ),
576
+ status=mlrun.common.schemas.ModelEndpointStatus(
577
+ endpoint_type=mlrun.common.schemas.model_monitoring.EndpointType.NODE_EP
578
+ ),
579
+ )
582
580
 
583
- except Exception as e:
584
- logger.error("Failed to create endpoint record", exc=err_to_str(e))
581
+ db = mlrun.get_run_db()
582
+ db.create_model_endpoint(
583
+ project=project,
584
+ endpoint_id=uid,
585
+ model_endpoint=model_endpoint.dict(),
586
+ )
585
587
 
586
- except Exception as e:
587
- logger.error("Failed to retrieve model endpoint object", exc=err_to_str(e))
588
+ elif (
589
+ model_ep
590
+ and (
591
+ model_ep.spec.monitoring_mode
592
+ == mlrun.common.schemas.model_monitoring.ModelMonitoringMode.enabled
593
+ )
594
+ != model.context.server.track_models
595
+ ):
596
+ monitoring_mode = (
597
+ mlrun.common.schemas.model_monitoring.ModelMonitoringMode.enabled
598
+ if model.context.server.track_models
599
+ else mlrun.common.schemas.model_monitoring.ModelMonitoringMode.disabled
600
+ )
601
+ db = mlrun.get_run_db()
602
+ db.patch_model_endpoint(
603
+ project=project,
604
+ endpoint_id=uid,
605
+ attributes={"monitoring_mode": monitoring_mode},
606
+ )
607
+ logger.debug(
608
+ f"Updating model endpoint monitoring_mode to {monitoring_mode}",
609
+ endpoint_id=uid,
610
+ )
588
611
 
589
612
  return uid
mlrun/utils/helpers.py CHANGED
@@ -109,10 +109,13 @@ def get_artifact_target(item: dict, project=None):
109
109
  db_key = item["spec"].get("db_key")
110
110
  project_str = project or item["metadata"].get("project")
111
111
  tree = item["metadata"].get("tree")
112
+ tag = item["metadata"].get("tag")
112
113
 
113
114
  kind = item.get("kind")
114
115
  if kind in ["dataset", "model", "artifact"] and db_key:
115
116
  target = f"{DB_SCHEMA}://{StorePrefix.Artifact}/{project_str}/{db_key}"
117
+ if tag:
118
+ target = f"{target}:{tag}"
116
119
  if tree:
117
120
  target = f"{target}@{tree}"
118
121
  return target
@@ -149,7 +152,7 @@ if is_ipython and config.nest_asyncio_enabled in ["1", "True"]:
149
152
  nest_asyncio.apply()
150
153
 
151
154
 
152
- class run_keys:
155
+ class RunKeys:
153
156
  input_path = "input_path"
154
157
  output_path = "output_path"
155
158
  inputs = "inputs"
@@ -160,6 +163,10 @@ class run_keys:
160
163
  secrets = "secret_sources"
161
164
 
162
165
 
166
+ # for Backward compatibility
167
+ run_keys = RunKeys
168
+
169
+
163
170
  def verify_field_regex(
164
171
  field_name,
165
172
  field_value,
@@ -674,6 +681,8 @@ def parse_artifact_uri(uri, default_project=""):
674
681
  raise ValueError(
675
682
  f"illegal store path '{uri}', iteration must be integer value"
676
683
  )
684
+ else:
685
+ iteration = 0
677
686
  return (
678
687
  group_dict["project"] or default_project,
679
688
  group_dict["key"],
@@ -1257,6 +1266,10 @@ def _fill_project_path_template(artifact_path, project):
1257
1266
  return artifact_path
1258
1267
 
1259
1268
 
1269
+ def to_non_empty_values_dict(input_dict: dict) -> dict:
1270
+ return {key: value for key, value in input_dict.items() if value}
1271
+
1272
+
1260
1273
  def str_to_timestamp(time_str: str, now_time: Timestamp = None):
1261
1274
  """convert fixed/relative time string to Pandas Timestamp
1262
1275
 
@@ -1314,6 +1327,7 @@ def format_run(run: PipelineRun, with_project=False) -> dict:
1314
1327
  "scheduled_at",
1315
1328
  "finished_at",
1316
1329
  "description",
1330
+ "experiment_id",
1317
1331
  ]
1318
1332
 
1319
1333
  if with_project:
@@ -1603,6 +1617,30 @@ def additional_filters_warning(additional_filters, class_name):
1603
1617
  )
1604
1618
 
1605
1619
 
1620
+ def merge_with_precedence(first_dict: dict, second_dict: dict) -> dict:
1621
+ """
1622
+ Merge two dictionaries with precedence given to keys from the second dictionary.
1623
+
1624
+ This function merges two dictionaries, `first_dict` and `second_dict`, where keys from `second_dict`
1625
+ take precedence in case of conflicts. If both dictionaries contain the same key,
1626
+ the value from `second_dict` will overwrite the value from `first_dict`.
1627
+
1628
+ Example:
1629
+ >>> first_dict = {"key1": "value1", "key2": "value2"}
1630
+ >>> second_dict = {"key2": "new_value2", "key3": "value3"}
1631
+ >>> merge_with_precedence(first_dict, second_dict)
1632
+ {'key1': 'value1', 'key2': 'new_value2', 'key3': 'value3'}
1633
+
1634
+ Note:
1635
+ - The merge operation uses the ** operator in Python, which combines key-value pairs
1636
+ from each dictionary. Later dictionaries take precedence when there are conflicting keys.
1637
+ """
1638
+ return {
1639
+ **(first_dict or {}),
1640
+ **(second_dict or {}),
1641
+ }
1642
+
1643
+
1606
1644
  def validate_component_version_compatibility(
1607
1645
  component_name: typing.Literal["iguazio", "nuclio"], *min_versions: str
1608
1646
  ):
mlrun/utils/logger.py CHANGED
@@ -13,8 +13,10 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import logging
16
+ import os
16
17
  import typing
17
18
  from enum import Enum
19
+ from functools import cached_property
18
20
  from sys import stdout
19
21
  from traceback import format_exception
20
22
  from typing import IO, Optional, Union
@@ -92,6 +94,16 @@ class HumanReadableFormatter(_BaseFormatter):
92
94
 
93
95
 
94
96
  class HumanReadableExtendedFormatter(HumanReadableFormatter):
97
+ _colors = {
98
+ logging.NOTSET: "",
99
+ logging.DEBUG: "\x1b[34m",
100
+ logging.INFO: "\x1b[36m",
101
+ logging.WARNING: "\x1b[33m",
102
+ logging.ERROR: "\x1b[0;31m",
103
+ logging.CRITICAL: "\x1b[1;31m",
104
+ }
105
+ _color_reset = "\x1b[0m"
106
+
95
107
  def format(self, record) -> str:
96
108
  more = ""
97
109
  record_with = self._record_with(record)
@@ -113,12 +125,34 @@ class HumanReadableExtendedFormatter(HumanReadableFormatter):
113
125
  [f"{key}: {_format_value(val)}" for key, val in record_with.items()]
114
126
  )
115
127
  return (
116
- "> "
128
+ f"{self._get_message_color(record.levelno)}> "
117
129
  f"{self.formatTime(record, self.datefmt)} "
118
130
  f"[{record.name}:{record.levelname.lower()}] "
119
- f"{record.getMessage()}{more}"
131
+ f"{record.getMessage()}{more}{self._get_color_reset()}"
120
132
  )
121
133
 
134
+ def _get_color_reset(self):
135
+ if not self._have_color_support:
136
+ return ""
137
+
138
+ return self._color_reset
139
+
140
+ def _get_message_color(self, levelno):
141
+ if not self._have_color_support:
142
+ return ""
143
+
144
+ return self._colors[levelno]
145
+
146
+ @cached_property
147
+ def _have_color_support(self):
148
+ if os.environ.get("PYCHARM_HOSTED"):
149
+ return True
150
+ if os.environ.get("NO_COLOR"):
151
+ return False
152
+ if os.environ.get("CLICOLOR_FORCE"):
153
+ return True
154
+ return stdout.isatty()
155
+
122
156
 
123
157
  class Logger:
124
158
  def __init__(