mlrun 1.7.0rc25__py3-none-any.whl → 1.7.0rc27__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 (49) hide show
  1. mlrun/__main__.py +7 -7
  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/base.py +9 -9
  6. mlrun/common/schemas/alert.py +4 -8
  7. mlrun/common/schemas/api_gateway.py +7 -0
  8. mlrun/common/schemas/constants.py +3 -0
  9. mlrun/common/schemas/model_monitoring/constants.py +20 -9
  10. mlrun/config.py +6 -11
  11. mlrun/datastore/datastore.py +3 -3
  12. mlrun/datastore/google_cloud_storage.py +6 -2
  13. mlrun/datastore/snowflake_utils.py +3 -1
  14. mlrun/datastore/sources.py +23 -9
  15. mlrun/datastore/targets.py +27 -13
  16. mlrun/db/base.py +10 -0
  17. mlrun/db/httpdb.py +45 -33
  18. mlrun/db/nopdb.py +10 -1
  19. mlrun/execution.py +18 -10
  20. mlrun/feature_store/retrieval/spark_merger.py +2 -1
  21. mlrun/model.py +21 -0
  22. mlrun/model_monitoring/db/stores/__init__.py +5 -3
  23. mlrun/model_monitoring/db/stores/base/store.py +36 -1
  24. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +4 -38
  25. mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +19 -27
  26. mlrun/model_monitoring/db/tsdb/__init__.py +4 -7
  27. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +4 -1
  28. mlrun/model_monitoring/helpers.py +9 -5
  29. mlrun/projects/project.py +68 -69
  30. mlrun/render.py +10 -5
  31. mlrun/run.py +2 -2
  32. mlrun/runtimes/nuclio/function.py +20 -0
  33. mlrun/runtimes/pod.py +5 -29
  34. mlrun/serving/routers.py +75 -59
  35. mlrun/serving/server.py +1 -0
  36. mlrun/serving/v2_serving.py +8 -1
  37. mlrun/utils/helpers.py +33 -1
  38. mlrun/utils/notifications/notification/base.py +4 -0
  39. mlrun/utils/notifications/notification/git.py +21 -0
  40. mlrun/utils/notifications/notification/slack.py +8 -0
  41. mlrun/utils/notifications/notification/webhook.py +29 -0
  42. mlrun/utils/notifications/notification_pusher.py +1 -1
  43. mlrun/utils/version/version.json +2 -2
  44. {mlrun-1.7.0rc25.dist-info → mlrun-1.7.0rc27.dist-info}/METADATA +6 -6
  45. {mlrun-1.7.0rc25.dist-info → mlrun-1.7.0rc27.dist-info}/RECORD +49 -49
  46. {mlrun-1.7.0rc25.dist-info → mlrun-1.7.0rc27.dist-info}/WHEEL +1 -1
  47. {mlrun-1.7.0rc25.dist-info → mlrun-1.7.0rc27.dist-info}/LICENSE +0 -0
  48. {mlrun-1.7.0rc25.dist-info → mlrun-1.7.0rc27.dist-info}/entry_points.txt +0 -0
  49. {mlrun-1.7.0rc25.dist-info → mlrun-1.7.0rc27.dist-info}/top_level.txt +0 -0
mlrun/projects/project.py CHANGED
@@ -1007,8 +1007,13 @@ class ProjectSpec(ModelObj):
1007
1007
  key = artifact.key
1008
1008
  artifact = artifact.to_dict()
1009
1009
  else: # artifact is a dict
1010
- # imported artifacts don't have metadata,spec,status fields
1011
- key_field = "key" if _is_imported_artifact(artifact) else "metadata.key"
1010
+ # imported/legacy artifacts don't have metadata,spec,status fields
1011
+ key_field = (
1012
+ "key"
1013
+ if _is_imported_artifact(artifact)
1014
+ or mlrun.utils.is_legacy_artifact(artifact)
1015
+ else "metadata.key"
1016
+ )
1012
1017
  key = mlrun.utils.get_in(artifact, key_field, "")
1013
1018
  if not key:
1014
1019
  raise ValueError(f'artifacts "{key_field}" must be specified')
@@ -2127,6 +2132,7 @@ class MlrunProject(ModelObj):
2127
2132
  deploy_histogram_data_drift_app: bool = True,
2128
2133
  wait_for_deployment: bool = False,
2129
2134
  rebuild_images: bool = False,
2135
+ fetch_credentials_from_sys_config: bool = False,
2130
2136
  ) -> None:
2131
2137
  """
2132
2138
  Deploy model monitoring application controller, writer and stream functions.
@@ -2136,17 +2142,18 @@ class MlrunProject(ModelObj):
2136
2142
  The stream function goal is to monitor the log of the data stream. It is triggered when a new log entry
2137
2143
  is detected. It processes the new events into statistics that are then written to statistics databases.
2138
2144
 
2139
- :param default_controller_image: Deprecated.
2140
- :param base_period: The time period in minutes in which the model monitoring controller
2141
- function is triggered. By default, the base period is 10 minutes.
2142
- :param image: The image of the model monitoring controller, writer, monitoring
2143
- stream & histogram data drift functions, which are real time nuclio
2144
- functions. By default, the image is mlrun/mlrun.
2145
- :param deploy_histogram_data_drift_app: If true, deploy the default histogram-based data drift application.
2146
- :param wait_for_deployment: If true, return only after the deployment is done on the backend.
2147
- Otherwise, deploy the model monitoring infrastructure on the
2148
- background, including the histogram data drift app if selected.
2149
- :param rebuild_images: If true, force rebuild of model monitoring infrastructure images.
2145
+ :param default_controller_image: Deprecated.
2146
+ :param base_period: The time period in minutes in which the model monitoring controller
2147
+ function is triggered. By default, the base period is 10 minutes.
2148
+ :param image: The image of the model monitoring controller, writer, monitoring
2149
+ stream & histogram data drift functions, which are real time nuclio
2150
+ functions. By default, the image is mlrun/mlrun.
2151
+ :param deploy_histogram_data_drift_app: If true, deploy the default histogram-based data drift application.
2152
+ :param wait_for_deployment: If true, return only after the deployment is done on the backend.
2153
+ Otherwise, deploy the model monitoring infrastructure on the
2154
+ background, including the histogram data drift app if selected.
2155
+ :param rebuild_images: If true, force rebuild of model monitoring infrastructure images.
2156
+ :param fetch_credentials_from_sys_config: If true, fetch the credentials from the system configuration.
2150
2157
  """
2151
2158
  if default_controller_image != "mlrun/mlrun":
2152
2159
  # TODO: Remove this in 1.9.0
@@ -2163,6 +2170,7 @@ class MlrunProject(ModelObj):
2163
2170
  base_period=base_period,
2164
2171
  deploy_histogram_data_drift_app=deploy_histogram_data_drift_app,
2165
2172
  rebuild_images=rebuild_images,
2173
+ fetch_credentials_from_sys_config=fetch_credentials_from_sys_config,
2166
2174
  )
2167
2175
 
2168
2176
  if wait_for_deployment:
@@ -2337,7 +2345,8 @@ class MlrunProject(ModelObj):
2337
2345
  Default: job
2338
2346
  :param image: Docker image to be used, can also be specified in the function object/yaml
2339
2347
  :param handler: Default function handler to invoke (can only be set with .py/.ipynb files)
2340
- :param with_repo: Add (clone) the current repo to the build source
2348
+ :param with_repo: Add (clone) the current repo to the build source - use when the function code is in
2349
+ the project repo (project.spec.source).
2341
2350
  :param tag: Function version tag to set (none for current or 'latest')
2342
2351
  Specifying a tag as a parameter will update the project's tagged function
2343
2352
  (myfunc:v1) and the untagged function (myfunc)
@@ -2484,25 +2493,17 @@ class MlrunProject(ModelObj):
2484
2493
  self.spec.remove_function(name)
2485
2494
 
2486
2495
  def remove_model_monitoring_function(self, name: Union[str, list[str]]):
2487
- """remove the specified model-monitoring-app function/s from the project spec
2496
+ """delete the specified model-monitoring-app function/s
2488
2497
 
2489
2498
  :param name: name of the model-monitoring-function/s (under the project)
2490
2499
  """
2491
- names = name if isinstance(name, list) else [name]
2492
- for func_name in names:
2493
- function = self.get_function(key=func_name)
2494
- if (
2495
- function.metadata.labels.get(mm_constants.ModelMonitoringAppLabel.KEY)
2496
- == mm_constants.ModelMonitoringAppLabel.VAL
2497
- ):
2498
- self.remove_function(name=func_name)
2499
- logger.info(
2500
- f"{func_name} function has been removed from {self.name} project"
2501
- )
2502
- else:
2503
- raise logger.warn(
2504
- f"There is no model monitoring function with {func_name} name"
2505
- )
2500
+ # TODO: Remove this in 1.9.0
2501
+ warnings.warn(
2502
+ "'remove_model_monitoring_function' is deprecated and will be removed in 1.9.0. "
2503
+ "Please use `delete_model_monitoring_function` instead.",
2504
+ FutureWarning,
2505
+ )
2506
+ self.delete_model_monitoring_function(name)
2506
2507
 
2507
2508
  def delete_model_monitoring_function(self, name: Union[str, list[str]]):
2508
2509
  """delete the specified model-monitoring-app function/s
@@ -3204,49 +3205,44 @@ class MlrunProject(ModelObj):
3204
3205
  stream_path: Optional[str] = None,
3205
3206
  tsdb_connection: Optional[str] = None,
3206
3207
  ):
3207
- """Set the credentials that will be used by the project's model monitoring
3208
+ """
3209
+ Set the credentials that will be used by the project's model monitoring
3208
3210
  infrastructure functions. Important to note that you have to set the credentials before deploying any
3209
3211
  model monitoring or serving function.
3210
3212
 
3211
- :param access_key: Model Monitoring access key for managing user permissions
3212
- :param endpoint_store_connection: Endpoint store connection string
3213
- :param stream_path: Path to the model monitoring stream
3214
- :param tsdb_connection: Connection string to the time series database
3213
+ :param access_key: Model Monitoring access key for managing user permissions.
3214
+ :param endpoint_store_connection: Endpoint store connection string. By default, None.
3215
+ Options:
3216
+ 1. None, will be set from the system configuration.
3217
+ 2. v3io - for v3io endpoint store,
3218
+ pass `v3io` and the system will generate the exact path.
3219
+ 3. MySQL/SQLite - for SQL endpoint store, please provide full
3220
+ connection string, for example
3221
+ mysql+pymysql://<username>:<password>@<host>:<port>/<db_name>
3222
+ :param stream_path: Path to the model monitoring stream. By default, None.
3223
+ Options:
3224
+ 1. None, will be set from the system configuration.
3225
+ 2. v3io - for v3io stream,
3226
+ pass `v3io` and the system will generate the exact path.
3227
+ 3. Kafka - for Kafka stream, please provide full connection string without
3228
+ custom topic, for example kafka://<some_kafka_broker>:<port>.
3229
+ :param tsdb_connection: Connection string to the time series database. By default, None.
3230
+ Options:
3231
+ 1. None, will be set from the system configuration.
3232
+ 2. v3io - for v3io stream,
3233
+ pass `v3io` and the system will generate the exact path.
3234
+ 3. TDEngine - for TDEngine tsdb, please provide full websocket connection URL,
3235
+ for example taosws://<username>:<password>@<host>:<port>.
3215
3236
  """
3216
-
3217
- secrets_dict = {}
3218
- if access_key:
3219
- secrets_dict[
3220
- mlrun.common.schemas.model_monitoring.ProjectSecretKeys.ACCESS_KEY
3221
- ] = access_key
3222
-
3223
- if endpoint_store_connection:
3224
- secrets_dict[
3225
- mlrun.common.schemas.model_monitoring.ProjectSecretKeys.ENDPOINT_STORE_CONNECTION
3226
- ] = endpoint_store_connection
3227
-
3228
- if stream_path:
3229
- if stream_path.startswith("kafka://") and "?topic" in stream_path:
3230
- raise mlrun.errors.MLRunInvalidArgumentError(
3231
- "Custom kafka topic is not allowed"
3232
- )
3233
- secrets_dict[
3234
- mlrun.common.schemas.model_monitoring.ProjectSecretKeys.STREAM_PATH
3235
- ] = stream_path
3236
-
3237
- if tsdb_connection:
3238
- if not tsdb_connection.startswith("taosws://"):
3239
- raise mlrun.errors.MLRunInvalidArgumentError(
3240
- "Currently only TDEngine websocket connection is supported for non-v3io TSDB,"
3241
- "please provide a full URL (e.g. taosws://user:password@host:port)"
3242
- )
3243
- secrets_dict[
3244
- mlrun.common.schemas.model_monitoring.ProjectSecretKeys.TSDB_CONNECTION
3245
- ] = tsdb_connection
3246
-
3247
- self.set_secrets(
3248
- secrets=secrets_dict,
3249
- provider=mlrun.common.schemas.SecretProviderName.kubernetes,
3237
+ db = mlrun.db.get_run_db(secrets=self._secrets)
3238
+ db.set_model_monitoring_credentials(
3239
+ project=self.name,
3240
+ credentials={
3241
+ "access_key": access_key,
3242
+ "endpoint_store_connection": endpoint_store_connection,
3243
+ "stream_path": stream_path,
3244
+ "tsdb_connection": tsdb_connection,
3245
+ },
3250
3246
  )
3251
3247
 
3252
3248
  def run_function(
@@ -3663,6 +3659,7 @@ class MlrunProject(ModelObj):
3663
3659
  kind: str = None,
3664
3660
  category: typing.Union[str, mlrun.common.schemas.ArtifactCategories] = None,
3665
3661
  tree: str = None,
3662
+ limit: int = None,
3666
3663
  ) -> mlrun.lists.ArtifactList:
3667
3664
  """List artifacts filtered by various parameters.
3668
3665
 
@@ -3692,6 +3689,7 @@ class MlrunProject(ModelObj):
3692
3689
  :param kind: Return artifacts of the requested kind.
3693
3690
  :param category: Return artifacts of the requested category.
3694
3691
  :param tree: Return artifacts of the requested tree.
3692
+ :param limit: Maximum number of artifacts to return.
3695
3693
  """
3696
3694
  db = mlrun.db.get_run_db(secrets=self._secrets)
3697
3695
  return db.list_artifacts(
@@ -3706,6 +3704,7 @@ class MlrunProject(ModelObj):
3706
3704
  kind=kind,
3707
3705
  category=category,
3708
3706
  tree=tree,
3707
+ limit=limit,
3709
3708
  )
3710
3709
 
3711
3710
  def list_models(
mlrun/render.py CHANGED
@@ -283,9 +283,14 @@ function copyToClipboard(fld) {
283
283
  }
284
284
  function expandPanel(el) {
285
285
  const panelName = "#" + el.getAttribute('paneName');
286
- console.log(el.title);
287
286
 
288
- document.querySelector(panelName + "-title").innerHTML = el.title
287
+ // Get the base URL of the current notebook
288
+ var baseUrl = window.location.origin;
289
+
290
+ // Construct the full URL
291
+ var fullUrl = new URL(el.title, baseUrl).href;
292
+
293
+ document.querySelector(panelName + "-title").innerHTML = fullUrl
289
294
  iframe = document.querySelector(panelName + "-body");
290
295
 
291
296
  const tblcss = `<style> body { font-family: Arial, Helvetica, sans-serif;}
@@ -299,7 +304,7 @@ function expandPanel(el) {
299
304
  }
300
305
 
301
306
  function reqListener () {
302
- if (el.title.endsWith(".csv")) {
307
+ if (fullUrl.endsWith(".csv")) {
303
308
  iframe.setAttribute("srcdoc", tblcss + csvToHtmlTable(this.responseText));
304
309
  } else {
305
310
  iframe.setAttribute("srcdoc", this.responseText);
@@ -309,11 +314,11 @@ function expandPanel(el) {
309
314
 
310
315
  const oReq = new XMLHttpRequest();
311
316
  oReq.addEventListener("load", reqListener);
312
- oReq.open("GET", el.title);
317
+ oReq.open("GET", fullUrl);
313
318
  oReq.send();
314
319
 
315
320
 
316
- //iframe.src = el.title;
321
+ //iframe.src = fullUrl;
317
322
  const resultPane = document.querySelector(panelName + "-pane");
318
323
  if (resultPane.classList.contains("hidden")) {
319
324
  resultPane.classList.remove("hidden");
mlrun/run.py CHANGED
@@ -63,11 +63,11 @@ from .runtimes.funcdoc import update_function_entry_points
63
63
  from .runtimes.nuclio.application import ApplicationRuntime
64
64
  from .runtimes.utils import add_code_metadata, global_context
65
65
  from .utils import (
66
+ RunKeys,
66
67
  extend_hub_uri_if_needed,
67
68
  get_in,
68
69
  logger,
69
70
  retry_until_successful,
70
- run_keys,
71
71
  update_in,
72
72
  )
73
73
 
@@ -280,7 +280,7 @@ def get_or_create_ctx(
280
280
  artifact_path = mlrun.utils.helpers.template_artifact_path(
281
281
  mlconf.artifact_path, project or mlconf.default_project
282
282
  )
283
- update_in(newspec, ["spec", run_keys.output_path], artifact_path)
283
+ update_in(newspec, ["spec", RunKeys.output_path], artifact_path)
284
284
 
285
285
  newspec.setdefault("metadata", {})
286
286
  update_in(newspec, "metadata.name", name, replace=False)
@@ -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", "")
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
@@ -390,6 +390,7 @@ def v2_serving_handler(context, event, get_body=False):
390
390
  "kafka",
391
391
  "kafka-cluster",
392
392
  "v3ioStream",
393
+ "v3io-stream",
393
394
  ):
394
395
  event.path = "/"
395
396
 
@@ -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:
@@ -548,6 +550,11 @@ def _init_endpoint_record(
548
550
  )
549
551
  except mlrun.errors.MLRunNotFoundError:
550
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
551
558
 
552
559
  if model.context.server.track_models and not model_ep:
553
560
  logger.debug("Creating a new model endpoint record", endpoint_id=uid)
mlrun/utils/helpers.py CHANGED
@@ -149,7 +149,7 @@ if is_ipython and config.nest_asyncio_enabled in ["1", "True"]:
149
149
  nest_asyncio.apply()
150
150
 
151
151
 
152
- class run_keys:
152
+ class RunKeys:
153
153
  input_path = "input_path"
154
154
  output_path = "output_path"
155
155
  inputs = "inputs"
@@ -160,6 +160,10 @@ class run_keys:
160
160
  secrets = "secret_sources"
161
161
 
162
162
 
163
+ # for Backward compatibility
164
+ run_keys = RunKeys
165
+
166
+
163
167
  def verify_field_regex(
164
168
  field_name,
165
169
  field_value,
@@ -1259,6 +1263,10 @@ def _fill_project_path_template(artifact_path, project):
1259
1263
  return artifact_path
1260
1264
 
1261
1265
 
1266
+ def to_non_empty_values_dict(input_dict: dict) -> dict:
1267
+ return {key: value for key, value in input_dict.items() if value}
1268
+
1269
+
1262
1270
  def str_to_timestamp(time_str: str, now_time: Timestamp = None):
1263
1271
  """convert fixed/relative time string to Pandas Timestamp
1264
1272
 
@@ -1606,6 +1614,30 @@ def additional_filters_warning(additional_filters, class_name):
1606
1614
  )
1607
1615
 
1608
1616
 
1617
+ def merge_with_precedence(first_dict: dict, second_dict: dict) -> dict:
1618
+ """
1619
+ Merge two dictionaries with precedence given to keys from the second dictionary.
1620
+
1621
+ This function merges two dictionaries, `first_dict` and `second_dict`, where keys from `second_dict`
1622
+ take precedence in case of conflicts. If both dictionaries contain the same key,
1623
+ the value from `second_dict` will overwrite the value from `first_dict`.
1624
+
1625
+ Example:
1626
+ >>> first_dict = {"key1": "value1", "key2": "value2"}
1627
+ >>> second_dict = {"key2": "new_value2", "key3": "value3"}
1628
+ >>> merge_with_precedence(first_dict, second_dict)
1629
+ {'key1': 'value1', 'key2': 'new_value2', 'key3': 'value3'}
1630
+
1631
+ Note:
1632
+ - The merge operation uses the ** operator in Python, which combines key-value pairs
1633
+ from each dictionary. Later dictionaries take precedence when there are conflicting keys.
1634
+ """
1635
+ return {
1636
+ **(first_dict or {}),
1637
+ **(second_dict or {}),
1638
+ }
1639
+
1640
+
1609
1641
  def validate_component_version_compatibility(
1610
1642
  component_name: typing.Literal["iguazio", "nuclio"], *min_versions: str
1611
1643
  ):
@@ -28,6 +28,10 @@ class NotificationBase:
28
28
  self.name = name
29
29
  self.params = params or {}
30
30
 
31
+ @classmethod
32
+ def validate_params(cls, params):
33
+ pass
34
+
31
35
  @property
32
36
  def active(self) -> bool:
33
37
  return True
@@ -30,6 +30,27 @@ class GitNotification(NotificationBase):
30
30
  API/Client notification for setting a rich run statuses git issue comment (github/gitlab)
31
31
  """
32
32
 
33
+ @classmethod
34
+ def validate_params(cls, params):
35
+ git_repo = params.get("repo", None)
36
+ git_issue = params.get("issue", None)
37
+ git_merge_request = params.get("merge_request", None)
38
+ token = (
39
+ params.get("token", None)
40
+ or params.get("GIT_TOKEN", None)
41
+ or params.get("GITHUB_TOKEN", None)
42
+ )
43
+ if not git_repo:
44
+ raise ValueError("Parameter 'repo' is required for GitNotification")
45
+
46
+ if not token:
47
+ raise ValueError("Parameter 'token' is required for GitNotification")
48
+
49
+ if not git_issue and not git_merge_request:
50
+ raise ValueError(
51
+ "At least one of 'issue' or 'merge_request' is required for GitNotification"
52
+ )
53
+
33
54
  async def push(
34
55
  self,
35
56
  message: str,
@@ -35,6 +35,14 @@ class SlackNotification(NotificationBase):
35
35
  "skipped": ":zzz:",
36
36
  }
37
37
 
38
+ @classmethod
39
+ def validate_params(cls, params):
40
+ webhook = params.get("webhook", None) or mlrun.get_secret_or_env(
41
+ "SLACK_WEBHOOK"
42
+ )
43
+ if not webhook:
44
+ raise ValueError("Parameter 'webhook' is required for SlackNotification")
45
+
38
46
  async def push(
39
47
  self,
40
48
  message: str,