mlrun 1.7.0rc28__py3-none-any.whl → 1.7.0rc55__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 (135) hide show
  1. mlrun/__main__.py +4 -2
  2. mlrun/alerts/alert.py +75 -8
  3. mlrun/artifacts/base.py +1 -0
  4. mlrun/artifacts/manager.py +9 -2
  5. mlrun/common/constants.py +4 -1
  6. mlrun/common/db/sql_session.py +3 -2
  7. mlrun/common/formatters/__init__.py +1 -0
  8. mlrun/common/formatters/artifact.py +1 -0
  9. mlrun/{model_monitoring/application.py → common/formatters/feature_set.py} +20 -6
  10. mlrun/common/formatters/run.py +3 -0
  11. mlrun/common/helpers.py +0 -1
  12. mlrun/common/schemas/__init__.py +3 -1
  13. mlrun/common/schemas/alert.py +15 -12
  14. mlrun/common/schemas/api_gateway.py +6 -6
  15. mlrun/common/schemas/auth.py +5 -0
  16. mlrun/common/schemas/client_spec.py +0 -1
  17. mlrun/common/schemas/common.py +7 -4
  18. mlrun/common/schemas/frontend_spec.py +7 -0
  19. mlrun/common/schemas/function.py +7 -0
  20. mlrun/common/schemas/model_monitoring/__init__.py +4 -3
  21. mlrun/common/schemas/model_monitoring/constants.py +41 -26
  22. mlrun/common/schemas/model_monitoring/model_endpoints.py +23 -47
  23. mlrun/common/schemas/notification.py +69 -12
  24. mlrun/common/schemas/project.py +45 -12
  25. mlrun/common/schemas/workflow.py +10 -2
  26. mlrun/common/types.py +1 -0
  27. mlrun/config.py +91 -35
  28. mlrun/data_types/data_types.py +6 -1
  29. mlrun/data_types/spark.py +2 -2
  30. mlrun/data_types/to_pandas.py +57 -25
  31. mlrun/datastore/__init__.py +1 -0
  32. mlrun/datastore/alibaba_oss.py +3 -2
  33. mlrun/datastore/azure_blob.py +125 -37
  34. mlrun/datastore/base.py +42 -21
  35. mlrun/datastore/datastore.py +4 -2
  36. mlrun/datastore/datastore_profile.py +1 -1
  37. mlrun/datastore/dbfs_store.py +3 -7
  38. mlrun/datastore/filestore.py +1 -3
  39. mlrun/datastore/google_cloud_storage.py +85 -29
  40. mlrun/datastore/inmem.py +4 -1
  41. mlrun/datastore/redis.py +1 -0
  42. mlrun/datastore/s3.py +25 -12
  43. mlrun/datastore/sources.py +76 -4
  44. mlrun/datastore/spark_utils.py +30 -0
  45. mlrun/datastore/storeytargets.py +151 -0
  46. mlrun/datastore/targets.py +102 -131
  47. mlrun/datastore/v3io.py +1 -0
  48. mlrun/db/base.py +15 -6
  49. mlrun/db/httpdb.py +57 -28
  50. mlrun/db/nopdb.py +29 -5
  51. mlrun/errors.py +20 -3
  52. mlrun/execution.py +46 -5
  53. mlrun/feature_store/api.py +25 -1
  54. mlrun/feature_store/common.py +6 -11
  55. mlrun/feature_store/feature_vector.py +3 -1
  56. mlrun/feature_store/retrieval/job.py +4 -1
  57. mlrun/feature_store/retrieval/spark_merger.py +10 -39
  58. mlrun/feature_store/steps.py +8 -0
  59. mlrun/frameworks/_common/plan.py +3 -3
  60. mlrun/frameworks/_ml_common/plan.py +1 -1
  61. mlrun/frameworks/parallel_coordinates.py +2 -3
  62. mlrun/frameworks/sklearn/mlrun_interface.py +13 -3
  63. mlrun/k8s_utils.py +48 -2
  64. mlrun/launcher/client.py +6 -6
  65. mlrun/launcher/local.py +2 -2
  66. mlrun/model.py +215 -34
  67. mlrun/model_monitoring/api.py +38 -24
  68. mlrun/model_monitoring/applications/__init__.py +1 -2
  69. mlrun/model_monitoring/applications/_application_steps.py +60 -29
  70. mlrun/model_monitoring/applications/base.py +2 -174
  71. mlrun/model_monitoring/applications/context.py +197 -70
  72. mlrun/model_monitoring/applications/evidently_base.py +11 -85
  73. mlrun/model_monitoring/applications/histogram_data_drift.py +21 -16
  74. mlrun/model_monitoring/applications/results.py +4 -4
  75. mlrun/model_monitoring/controller.py +110 -282
  76. mlrun/model_monitoring/db/stores/__init__.py +8 -3
  77. mlrun/model_monitoring/db/stores/base/store.py +3 -0
  78. mlrun/model_monitoring/db/stores/sqldb/models/base.py +9 -7
  79. mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +18 -3
  80. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +43 -23
  81. mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +48 -35
  82. mlrun/model_monitoring/db/tsdb/__init__.py +7 -2
  83. mlrun/model_monitoring/db/tsdb/base.py +147 -15
  84. mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +94 -55
  85. mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +0 -3
  86. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +144 -38
  87. mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +44 -3
  88. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +246 -57
  89. mlrun/model_monitoring/helpers.py +70 -50
  90. mlrun/model_monitoring/stream_processing.py +96 -195
  91. mlrun/model_monitoring/writer.py +13 -5
  92. mlrun/package/packagers/default_packager.py +2 -2
  93. mlrun/projects/operations.py +16 -8
  94. mlrun/projects/pipelines.py +126 -115
  95. mlrun/projects/project.py +286 -129
  96. mlrun/render.py +3 -3
  97. mlrun/run.py +38 -19
  98. mlrun/runtimes/__init__.py +19 -8
  99. mlrun/runtimes/base.py +4 -1
  100. mlrun/runtimes/daskjob.py +1 -1
  101. mlrun/runtimes/funcdoc.py +1 -1
  102. mlrun/runtimes/kubejob.py +6 -6
  103. mlrun/runtimes/local.py +12 -5
  104. mlrun/runtimes/nuclio/api_gateway.py +68 -8
  105. mlrun/runtimes/nuclio/application/application.py +307 -70
  106. mlrun/runtimes/nuclio/function.py +63 -14
  107. mlrun/runtimes/nuclio/serving.py +10 -10
  108. mlrun/runtimes/pod.py +25 -19
  109. mlrun/runtimes/remotesparkjob.py +2 -5
  110. mlrun/runtimes/sparkjob/spark3job.py +16 -17
  111. mlrun/runtimes/utils.py +34 -0
  112. mlrun/serving/routers.py +2 -5
  113. mlrun/serving/server.py +37 -19
  114. mlrun/serving/states.py +30 -3
  115. mlrun/serving/v2_serving.py +44 -35
  116. mlrun/track/trackers/mlflow_tracker.py +5 -0
  117. mlrun/utils/async_http.py +1 -1
  118. mlrun/utils/db.py +18 -0
  119. mlrun/utils/helpers.py +150 -36
  120. mlrun/utils/http.py +1 -1
  121. mlrun/utils/notifications/notification/__init__.py +0 -1
  122. mlrun/utils/notifications/notification/webhook.py +8 -1
  123. mlrun/utils/notifications/notification_pusher.py +1 -1
  124. mlrun/utils/v3io_clients.py +2 -2
  125. mlrun/utils/version/version.json +2 -2
  126. {mlrun-1.7.0rc28.dist-info → mlrun-1.7.0rc55.dist-info}/METADATA +153 -66
  127. {mlrun-1.7.0rc28.dist-info → mlrun-1.7.0rc55.dist-info}/RECORD +131 -134
  128. {mlrun-1.7.0rc28.dist-info → mlrun-1.7.0rc55.dist-info}/WHEEL +1 -1
  129. mlrun/feature_store/retrieval/conversion.py +0 -271
  130. mlrun/model_monitoring/controller_handler.py +0 -37
  131. mlrun/model_monitoring/evidently_application.py +0 -20
  132. mlrun/model_monitoring/prometheus.py +0 -216
  133. {mlrun-1.7.0rc28.dist-info → mlrun-1.7.0rc55.dist-info}/LICENSE +0 -0
  134. {mlrun-1.7.0rc28.dist-info → mlrun-1.7.0rc55.dist-info}/entry_points.txt +0 -0
  135. {mlrun-1.7.0rc28.dist-info → mlrun-1.7.0rc55.dist-info}/top_level.txt +0 -0
@@ -13,45 +13,16 @@
13
13
  # limitations under the License.
14
14
  #
15
15
 
16
- import pandas as pd
17
- import semver
18
16
 
19
17
  import mlrun
18
+ from mlrun.data_types.to_pandas import spark_df_to_pandas
20
19
  from mlrun.datastore.sources import ParquetSource
21
20
  from mlrun.datastore.targets import get_offline_target
21
+ from mlrun.runtimes import RemoteSparkRuntime
22
+ from mlrun.runtimes.sparkjob import Spark3Runtime
22
23
  from mlrun.utils.helpers import additional_filters_warning
23
24
 
24
- from ...runtimes import RemoteSparkRuntime
25
- from ...runtimes.sparkjob import Spark3Runtime
26
25
  from .base import BaseMerger
27
- from .conversion import PandasConversionMixin
28
-
29
-
30
- def spark_df_to_pandas(spark_df):
31
- # as of pyspark 3.2.3, toPandas fails to convert timestamps unless we work around the issue
32
- # when we upgrade pyspark, we should check whether this workaround is still necessary
33
- # see https://stackoverflow.com/questions/76389694/transforming-pyspark-to-pandas-dataframe
34
- if semver.parse(pd.__version__)["major"] >= 2:
35
- import pyspark.sql.functions as pyspark_functions
36
-
37
- type_conversion_dict = {}
38
- for field in spark_df.schema.fields:
39
- if str(field.dataType) == "TimestampType":
40
- spark_df = spark_df.withColumn(
41
- field.name,
42
- pyspark_functions.date_format(
43
- pyspark_functions.to_timestamp(field.name),
44
- "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS",
45
- ),
46
- )
47
- type_conversion_dict[field.name] = "datetime64[ns]"
48
-
49
- df = PandasConversionMixin.toPandas(spark_df)
50
- if type_conversion_dict:
51
- df = df.astype(type_conversion_dict)
52
- return df
53
- else:
54
- return PandasConversionMixin.toPandas(spark_df)
55
26
 
56
27
 
57
28
  class SparkFeatureMerger(BaseMerger):
@@ -217,9 +188,13 @@ class SparkFeatureMerger(BaseMerger):
217
188
 
218
189
  if self.spark is None:
219
190
  # create spark context
220
- self.spark = SparkSession.builder.appName(
221
- f"vector-merger-{self.vector.metadata.name}"
222
- ).getOrCreate()
191
+ self.spark = (
192
+ SparkSession.builder.appName(
193
+ f"vector-merger-{self.vector.metadata.name}"
194
+ )
195
+ .config("spark.driver.memory", "2g")
196
+ .getOrCreate()
197
+ )
223
198
 
224
199
  def _get_engine_df(
225
200
  self,
@@ -231,10 +206,6 @@ class SparkFeatureMerger(BaseMerger):
231
206
  time_column=None,
232
207
  additional_filters=None,
233
208
  ):
234
- mlrun.utils.helpers.additional_filters_warning(
235
- additional_filters, self.__class__
236
- )
237
-
238
209
  source_kwargs = {}
239
210
  if feature_set.spec.passthrough:
240
211
  if not feature_set.spec.source:
@@ -743,3 +743,11 @@ class DropFeatures(StepToDict, MLRunStep):
743
743
  raise mlrun.errors.MLRunInvalidArgumentError(
744
744
  f"DropFeatures can only drop features, not entities: {dropped_entities}"
745
745
  )
746
+ if feature_set.spec.label_column in features:
747
+ raise mlrun.errors.MLRunInvalidArgumentError(
748
+ f"DropFeatures can not drop label_column: {feature_set.spec.label_column}"
749
+ )
750
+ if feature_set.spec.timestamp_key in features:
751
+ raise mlrun.errors.MLRunInvalidArgumentError(
752
+ f"DropFeatures can not drop timestamp_key: {feature_set.spec.timestamp_key}"
753
+ )
@@ -11,12 +11,12 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
- #
14
+
15
15
  from abc import ABC, abstractmethod
16
16
 
17
17
  import mlrun
18
18
  from mlrun.artifacts import Artifact
19
- from mlrun.utils.helpers import is_ipython
19
+ from mlrun.utils.helpers import is_jupyter
20
20
 
21
21
 
22
22
  class Plan(ABC):
@@ -84,7 +84,7 @@ class Plan(ABC):
84
84
  return
85
85
 
86
86
  # Call the correct display method according to the kernel:
87
- if is_ipython:
87
+ if is_jupyter:
88
88
  self._gui_display()
89
89
  else:
90
90
  self._cli_display()
@@ -16,7 +16,7 @@ import json
16
16
  from abc import ABC, abstractmethod
17
17
  from enum import Enum
18
18
 
19
- from IPython.core.display import HTML, display
19
+ from IPython.display import HTML, display
20
20
 
21
21
  import mlrun
22
22
 
@@ -18,8 +18,7 @@ from typing import Union
18
18
 
19
19
  import numpy as np
20
20
  import pandas as pd
21
- from IPython.core.display import HTML
22
- from IPython.display import display
21
+ from IPython.display import HTML, display
23
22
  from pandas.api.types import is_numeric_dtype, is_string_dtype
24
23
 
25
24
  import mlrun
@@ -216,7 +215,7 @@ def _show_and_export_html(html: str, show=None, filename=None, runs_list=None):
216
215
  fp.write("</body></html>")
217
216
  else:
218
217
  fp.write(html)
219
- if show or (show is None and mlrun.utils.is_ipython):
218
+ if show or (show is None and mlrun.utils.is_jupyter):
220
219
  display(HTML(html))
221
220
  if runs_list and len(runs_list) <= max_table_rows:
222
221
  display(HTML(html_table))
@@ -97,7 +97,7 @@ class SKLearnMLRunInterface(MLRunInterface, ABC):
97
97
 
98
98
  def wrapper(
99
99
  self: SKLearnTypes.ModelType,
100
- X: SKLearnTypes.DatasetType,
100
+ X: SKLearnTypes.DatasetType, # noqa: N803 - should be lowercase "x", kept for BC
101
101
  y: SKLearnTypes.DatasetType = None,
102
102
  *args,
103
103
  **kwargs,
@@ -124,7 +124,12 @@ class SKLearnMLRunInterface(MLRunInterface, ABC):
124
124
 
125
125
  return wrapper
126
126
 
127
- def mlrun_predict(self, X: SKLearnTypes.DatasetType, *args, **kwargs):
127
+ def mlrun_predict(
128
+ self,
129
+ X: SKLearnTypes.DatasetType, # noqa: N803 - should be lowercase "x", kept for BC
130
+ *args,
131
+ **kwargs,
132
+ ):
128
133
  """
129
134
  MLRun's wrapper for the common ML API predict method.
130
135
  """
@@ -136,7 +141,12 @@ class SKLearnMLRunInterface(MLRunInterface, ABC):
136
141
 
137
142
  return y_pred
138
143
 
139
- def mlrun_predict_proba(self, X: SKLearnTypes.DatasetType, *args, **kwargs):
144
+ def mlrun_predict_proba(
145
+ self,
146
+ X: SKLearnTypes.DatasetType, # noqa: N803 - should be lowercase "x", kept for BC
147
+ *args,
148
+ **kwargs,
149
+ ):
140
150
  """
141
151
  MLRun's wrapper for the common ML API predict_proba method.
142
152
  """
mlrun/k8s_utils.py CHANGED
@@ -12,6 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  import re
15
+ import warnings
15
16
 
16
17
  import kubernetes.client
17
18
 
@@ -133,7 +134,7 @@ def sanitize_label_value(value: str) -> str:
133
134
  return re.sub(r"([^a-zA-Z0-9_.-]|^[^a-zA-Z0-9]|[^a-zA-Z0-9]$)", "-", value[:63])
134
135
 
135
136
 
136
- def verify_label_key(key: str):
137
+ def verify_label_key(key: str, allow_k8s_prefix: bool = False):
137
138
  """
138
139
  Verify that the label key is valid for Kubernetes.
139
140
  Refer to https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
@@ -146,6 +147,10 @@ def verify_label_key(key: str):
146
147
  name = parts[0]
147
148
  elif len(parts) == 2:
148
149
  prefix, name = parts
150
+ if len(name) == 0:
151
+ raise mlrun.errors.MLRunInvalidArgumentError(
152
+ "Label key name cannot be empty when a prefix is set"
153
+ )
149
154
  if len(prefix) == 0:
150
155
  raise mlrun.errors.MLRunInvalidArgumentError(
151
156
  "Label key prefix cannot be empty"
@@ -173,7 +178,13 @@ def verify_label_key(key: str):
173
178
  mlrun.utils.regex.qualified_name,
174
179
  )
175
180
 
176
- if key.startswith("k8s.io/") or key.startswith("kubernetes.io/"):
181
+ # Allow the use of Kubernetes reserved prefixes ('k8s.io/' or 'kubernetes.io/')
182
+ # only when setting node selectors, not when adding new labels.
183
+ if (
184
+ key.startswith("k8s.io/")
185
+ or key.startswith("kubernetes.io/")
186
+ and not allow_k8s_prefix
187
+ ):
177
188
  raise mlrun.errors.MLRunInvalidArgumentError(
178
189
  "Labels cannot start with 'k8s.io/' or 'kubernetes.io/'"
179
190
  )
@@ -185,3 +196,38 @@ def verify_label_value(value, label_key):
185
196
  value,
186
197
  mlrun.utils.regex.label_value,
187
198
  )
199
+
200
+
201
+ def validate_node_selectors(
202
+ node_selectors: dict[str, str], raise_on_error: bool = True
203
+ ) -> bool:
204
+ """
205
+ Ensures that user-defined node selectors adhere to Kubernetes label standards:
206
+ - Validates that each key conforms to Kubernetes naming conventions, with specific rules for name and prefix.
207
+ - Ensures values comply with Kubernetes label value rules.
208
+ - If raise_on_error is True, raises errors for invalid selectors.
209
+ - If raise_on_error is False, logs warnings for invalid selectors.
210
+ """
211
+
212
+ # Helper function for handling errors or warnings
213
+ def handle_invalid(message):
214
+ if raise_on_error:
215
+ raise
216
+ else:
217
+ warnings.warn(
218
+ f"{message}\n"
219
+ f"The node selector you’ve set does not meet the validation rules for the current Kubernetes version. "
220
+ f"Please note that invalid node selectors may cause issues with function scheduling."
221
+ )
222
+
223
+ node_selectors = node_selectors or {}
224
+ for key, value in node_selectors.items():
225
+ try:
226
+ verify_label_key(key, allow_k8s_prefix=True)
227
+ verify_label_value(value, label_key=key)
228
+ except mlrun.errors.MLRunInvalidArgumentError as err:
229
+ # An error or warning is raised by handle_invalid due to validation failure.
230
+ # Returning False indicates validation failed, allowing us to exit the function.
231
+ handle_invalid(str(err))
232
+ return False
233
+ return True
mlrun/launcher/client.py CHANGED
@@ -14,7 +14,7 @@
14
14
  import abc
15
15
  from typing import Optional
16
16
 
17
- import IPython
17
+ import IPython.display
18
18
 
19
19
  import mlrun.common.constants as mlrun_constants
20
20
  import mlrun.errors
@@ -22,7 +22,7 @@ import mlrun.launcher.base as launcher
22
22
  import mlrun.lists
23
23
  import mlrun.model
24
24
  import mlrun.runtimes
25
- from mlrun.utils import logger
25
+ import mlrun.utils
26
26
 
27
27
 
28
28
  class ClientBaseLauncher(launcher.BaseLauncher, abc.ABC):
@@ -128,10 +128,10 @@ class ClientBaseLauncher(launcher.BaseLauncher, abc.ABC):
128
128
  if result:
129
129
  results_tbl.append(result)
130
130
  else:
131
- logger.info("no returned result (job may still be in progress)")
131
+ mlrun.utils.logger.info("no returned result (job may still be in progress)")
132
132
  results_tbl.append(run.to_dict())
133
133
 
134
- if mlrun.utils.is_ipython and mlrun.mlconf.ipython_widget:
134
+ if mlrun.utils.is_jupyter and mlrun.mlconf.ipython_widget:
135
135
  results_tbl.show()
136
136
  print()
137
137
  ui_url = mlrun.utils.get_ui_url(project, uid)
@@ -147,9 +147,9 @@ class ClientBaseLauncher(launcher.BaseLauncher, abc.ABC):
147
147
  project_flag = f"-p {project}" if project else ""
148
148
  info_cmd = f"mlrun get run {uid} {project_flag}"
149
149
  logs_cmd = f"mlrun logs {uid} {project_flag}"
150
- logger.info(
150
+ mlrun.utils.logger.info(
151
151
  "To track results use the CLI", info_cmd=info_cmd, logs_cmd=logs_cmd
152
152
  )
153
153
  ui_url = mlrun.utils.get_ui_url(project, uid)
154
154
  if ui_url:
155
- logger.info("Or click for UI", ui_url=ui_url)
155
+ mlrun.utils.logger.info("Or click for UI", ui_url=ui_url)
mlrun/launcher/local.py CHANGED
@@ -72,9 +72,9 @@ class ClientLocalLauncher(launcher.ClientBaseLauncher):
72
72
  reset_on_run: Optional[bool] = None,
73
73
  ) -> "mlrun.run.RunObject":
74
74
  # do not allow local function to be scheduled
75
- if self._is_run_local and schedule is not None:
75
+ if schedule is not None:
76
76
  raise mlrun.errors.MLRunInvalidArgumentError(
77
- "local and schedule cannot be used together"
77
+ f"Unexpected {schedule=} parameter for local function execution"
78
78
  )
79
79
 
80
80
  self.enrich_runtime(runtime, project)
mlrun/model.py CHANGED
@@ -487,7 +487,7 @@ class ImageBuilder(ModelObj):
487
487
 
488
488
  def __init__(
489
489
  self,
490
- functionSourceCode=None,
490
+ functionSourceCode=None, # noqa: N803 - should be "snake_case", kept for BC
491
491
  source=None,
492
492
  image=None,
493
493
  base_image=None,
@@ -679,7 +679,25 @@ class ImageBuilder(ModelObj):
679
679
 
680
680
 
681
681
  class Notification(ModelObj):
682
- """Notification specification"""
682
+ """Notification object
683
+
684
+ :param kind: notification implementation kind - slack, webhook, etc. See
685
+ :py:class:`mlrun.common.schemas.notification.NotificationKind`
686
+ :param name: for logging and identification
687
+ :param message: message content in the notification
688
+ :param severity: severity to display in the notification
689
+ :param when: list of statuses to trigger the notification: 'running', 'completed', 'error'
690
+ :param condition: optional condition to trigger the notification, a jinja2 expression that can use run data
691
+ to evaluate if the notification should be sent in addition to the 'when' statuses.
692
+ e.g.: '{{ run["status"]["results"]["accuracy"] < 0.9}}'
693
+ :param params: Implementation specific parameters for the notification implementation (e.g. slack webhook url,
694
+ git repository details, etc.)
695
+ :param secret_params: secret parameters for the notification implementation, same as params but will be stored
696
+ in a k8s secret and passed as a secret reference to the implementation.
697
+ :param status: notification status - pending, sent, error
698
+ :param sent_time: time the notification was sent
699
+ :param reason: failure reason if the notification failed to send
700
+ """
683
701
 
684
702
  def __init__(
685
703
  self,
@@ -737,19 +755,41 @@ class Notification(ModelObj):
737
755
  self.kind
738
756
  ).get_notification()
739
757
 
740
- secret_params = self.secret_params
741
- params = self.params
758
+ secret_params = self.secret_params or {}
759
+ params = self.params or {}
760
+
761
+ # if the secret_params are already masked - no need to validate
762
+ params_secret = secret_params.get("secret", "")
763
+ if params_secret:
764
+ if len(secret_params) > 1:
765
+ raise mlrun.errors.MLRunInvalidArgumentError(
766
+ "When the 'secret' key is present, 'secret_params' should not contain any other keys."
767
+ )
768
+ return
742
769
 
743
770
  if not secret_params and not params:
744
771
  raise mlrun.errors.MLRunInvalidArgumentError(
745
772
  "Both 'secret_params' and 'params' are empty, at least one must be defined."
746
773
  )
747
- if secret_params and params and secret_params != params:
748
- raise mlrun.errors.MLRunInvalidArgumentError(
749
- "Both 'secret_params' and 'params' are defined but they contain different values"
750
- )
751
774
 
752
- notification_class.validate_params(secret_params or params)
775
+ notification_class.validate_params(secret_params | params)
776
+
777
+ def enrich_unmasked_secret_params_from_project_secret(self):
778
+ """
779
+ Fill the notification secret params from the project secret.
780
+ We are using this function instead of unmask_secret_params_from_project_secret when we run inside the
781
+ workflow runner pod that doesn't have access to the k8s secrets (but have access to the project secret)
782
+ """
783
+ secret = self.secret_params.get("secret")
784
+ if secret:
785
+ secret_value = mlrun.get_secret_or_env(secret)
786
+ if secret_value:
787
+ try:
788
+ self.secret_params = json.loads(secret_value)
789
+ except ValueError as exc:
790
+ raise mlrun.errors.MLRunValueError(
791
+ "Failed to parse secret value"
792
+ ) from exc
753
793
 
754
794
  @staticmethod
755
795
  def validate_notification_uniqueness(notifications: list["Notification"]):
@@ -1306,7 +1346,7 @@ class RunTemplate(ModelObj):
1306
1346
 
1307
1347
  task.with_input("data", "/file-dir/path/to/file")
1308
1348
  task.with_input("data", "s3://<bucket>/path/to/file")
1309
- task.with_input("data", "v3io://[<remote-host>]/<data-container>/path/to/file")
1349
+ task.with_input("data", "v3io://<data-container>/path/to/file")
1310
1350
  """
1311
1351
  if not self.spec.inputs:
1312
1352
  self.spec.inputs = {}
@@ -1463,7 +1503,11 @@ class RunObject(RunTemplate):
1463
1503
  @property
1464
1504
  def error(self) -> str:
1465
1505
  """error string if failed"""
1466
- if self.status:
1506
+ if (
1507
+ self.status
1508
+ and self.status.state
1509
+ in mlrun.common.runtimes.constants.RunStates.error_and_abortion_states()
1510
+ ):
1467
1511
  unknown_error = ""
1468
1512
  if (
1469
1513
  self.status.state
@@ -1479,20 +1523,43 @@ class RunObject(RunTemplate):
1479
1523
 
1480
1524
  return (
1481
1525
  self.status.error
1482
- or self.status.reason
1483
1526
  or self.status.status_text
1527
+ or self.status.reason
1484
1528
  or unknown_error
1485
1529
  )
1486
1530
  return ""
1487
1531
 
1488
- def output(self, key):
1489
- """return the value of a specific result or artifact by key"""
1532
+ def output(self, key: str):
1533
+ """
1534
+ Return the value of a specific result or artifact by key.
1535
+
1536
+ This method waits for the outputs to complete and retrieves the value corresponding to the provided key.
1537
+ If the key exists in the results, it returns the corresponding result value.
1538
+ If not found in results, it attempts to fetch the artifact by key (cached in the run status).
1539
+ If the artifact is not found, it tries to fetch the artifact URI by key.
1540
+ If no artifact or result is found for the key, returns None.
1541
+
1542
+ :param key: The key of the result or artifact to retrieve.
1543
+ :return: The value of the result or the artifact URI corresponding to the key, or None if not found.
1544
+ """
1490
1545
  self._outputs_wait_for_completion()
1546
+
1547
+ # Check if the key exists in results and return the result value
1491
1548
  if self.status.results and key in self.status.results:
1492
- return self.status.results.get(key)
1549
+ return self.status.results[key]
1550
+
1551
+ # Artifacts are usually cached in the run object under `status.artifacts`. However, the artifacts are not
1552
+ # stored in the DB as part of the run. The server may enrich the run with the artifacts or provide
1553
+ # `status.artifact_uris` instead. See mlrun.common.formatters.run.RunFormat.
1554
+ # When running locally - `status.artifact_uri` does not exist in the run.
1555
+ # When listing runs - `status.artifacts` does not exist in the run.
1493
1556
  artifact = self._artifact(key)
1494
1557
  if artifact:
1495
1558
  return get_artifact_target(artifact, self.metadata.project)
1559
+
1560
+ if self.status.artifact_uris and key in self.status.artifact_uris:
1561
+ return self.status.artifact_uris[key]
1562
+
1496
1563
  return None
1497
1564
 
1498
1565
  @property
@@ -1505,26 +1572,50 @@ class RunObject(RunTemplate):
1505
1572
 
1506
1573
  @property
1507
1574
  def outputs(self):
1508
- """return a dict of outputs, result values and artifact uris"""
1509
- outputs = {}
1575
+ """
1576
+ Return a dictionary of outputs, including result values and artifact URIs.
1577
+
1578
+ This method waits for the outputs to complete and combines result values
1579
+ and artifact URIs into a single dictionary. If there are multiple artifacts
1580
+ for the same key, only include the artifact that does not have the "latest" tag.
1581
+ If there is no other tag, include the "latest" tag as a fallback.
1582
+
1583
+ :return: Dictionary containing result values and artifact URIs.
1584
+ """
1510
1585
  self._outputs_wait_for_completion()
1586
+ outputs = {}
1587
+
1588
+ # Add results if available
1511
1589
  if self.status.results:
1512
- outputs = {k: v for k, v in self.status.results.items()}
1590
+ outputs.update(self.status.results)
1591
+
1592
+ # Artifacts are usually cached in the run object under `status.artifacts`. However, the artifacts are not
1593
+ # stored in the DB as part of the run. The server may enrich the run with the artifacts or provide
1594
+ # `status.artifact_uris` instead. See mlrun.common.formatters.run.RunFormat.
1595
+ # When running locally - `status.artifact_uri` does not exist in the run.
1596
+ # When listing runs - `status.artifacts` does not exist in the run.
1513
1597
  if self.status.artifacts:
1514
- for a in self.status.artifacts:
1515
- key = a["metadata"]["key"]
1516
- outputs[key] = get_artifact_target(a, self.metadata.project)
1598
+ outputs.update(self._process_artifacts(self.status.artifacts))
1599
+ elif self.status.artifact_uris:
1600
+ outputs.update(self.status.artifact_uris)
1601
+
1517
1602
  return outputs
1518
1603
 
1519
- def artifact(self, key) -> "mlrun.DataItem":
1520
- """return artifact DataItem by key"""
1604
+ def artifact(self, key: str) -> "mlrun.DataItem":
1605
+ """Return artifact DataItem by key.
1606
+
1607
+ This method waits for the outputs to complete, searches for the artifact matching the given key,
1608
+ and returns a DataItem if the artifact is found.
1609
+
1610
+ :param key: The key of the artifact to find.
1611
+ :return: A DataItem corresponding to the artifact with the given key, or None if no such artifact is found.
1612
+ """
1521
1613
  self._outputs_wait_for_completion()
1522
1614
  artifact = self._artifact(key)
1523
- if artifact:
1524
- uri = get_artifact_target(artifact, self.metadata.project)
1525
- if uri:
1526
- return mlrun.get_dataitem(uri)
1527
- return None
1615
+ if not artifact:
1616
+ return None
1617
+ uri = get_artifact_target(artifact, self.metadata.project)
1618
+ return mlrun.get_dataitem(uri) if uri else None
1528
1619
 
1529
1620
  def _outputs_wait_for_completion(
1530
1621
  self,
@@ -1542,12 +1633,85 @@ class RunObject(RunTemplate):
1542
1633
  )
1543
1634
 
1544
1635
  def _artifact(self, key):
1545
- """return artifact DataItem by key"""
1546
- if self.status.artifacts:
1547
- for a in self.status.artifacts:
1548
- if a["metadata"]["key"] == key:
1549
- return a
1550
- return None
1636
+ """
1637
+ Return the last artifact DataItem that matches the given key.
1638
+
1639
+ If multiple artifacts with the same key exist, return the last one in the list.
1640
+ If there are artifacts with different tags, the method will return the one with a tag other than 'latest'
1641
+ if available.
1642
+ If no artifact with the given key is found, return None.
1643
+
1644
+ :param key: The key of the artifact to retrieve.
1645
+ :return: The last artifact DataItem with the given key, or None if no such artifact is found.
1646
+ """
1647
+ if not self.status.artifacts:
1648
+ return None
1649
+
1650
+ # Collect artifacts that match the key
1651
+ matching_artifacts = [
1652
+ artifact
1653
+ for artifact in self.status.artifacts
1654
+ if artifact["metadata"].get("key") == key
1655
+ ]
1656
+
1657
+ if not matching_artifacts:
1658
+ return None
1659
+
1660
+ # Sort matching artifacts by creation date in ascending order.
1661
+ # The last element in the list will be the one created most recently.
1662
+ # In case the `created` field does not exist in the artifact, that artifact will appear first in the sorted list
1663
+ matching_artifacts.sort(
1664
+ key=lambda artifact: artifact["metadata"].get("created", datetime.min)
1665
+ )
1666
+
1667
+ # Filter out artifacts with 'latest' tag
1668
+ non_latest_artifacts = [
1669
+ artifact
1670
+ for artifact in matching_artifacts
1671
+ if artifact["metadata"].get("tag") != "latest"
1672
+ ]
1673
+
1674
+ # Return the last non-'latest' artifact if available, otherwise return the last artifact
1675
+ # In the case of only one tag, `status.artifacts` includes [v1, latest]. In that case, we want to return v1.
1676
+ # In the case of multiple tags, `status.artifacts` includes [v1, latest, v2, v3].
1677
+ # In that case, we need to return the last one (v3).
1678
+ return (non_latest_artifacts or matching_artifacts)[-1]
1679
+
1680
+ def _process_artifacts(self, artifacts):
1681
+ artifacts_by_key = {}
1682
+
1683
+ # Organize artifacts by key
1684
+ for artifact in artifacts:
1685
+ key = artifact["metadata"]["key"]
1686
+ if key not in artifacts_by_key:
1687
+ artifacts_by_key[key] = []
1688
+ artifacts_by_key[key].append(artifact)
1689
+
1690
+ outputs = {}
1691
+ for key, artifacts in artifacts_by_key.items():
1692
+ # Sort matching artifacts by creation date in ascending order.
1693
+ # The last element in the list will be the one created most recently.
1694
+ # In case the `created` field does not exist in the artifactthat artifact will appear
1695
+ # first in the sorted list
1696
+ artifacts.sort(
1697
+ key=lambda artifact: artifact["metadata"].get("created", datetime.min)
1698
+ )
1699
+
1700
+ # Filter out artifacts with 'latest' tag
1701
+ non_latest_artifacts = [
1702
+ artifact
1703
+ for artifact in artifacts
1704
+ if artifact["metadata"].get("tag") != "latest"
1705
+ ]
1706
+
1707
+ # Save the last non-'latest' artifact if available, otherwise save the last artifact
1708
+ # In the case of only one tag, `artifacts` includes [v1, latest], in that case, we want to save v1.
1709
+ # In the case of multiple tags, `artifacts` includes [v1, latest, v2, v3].
1710
+ # In that case, we need to save the last one (v3).
1711
+ artifact_to_save = (non_latest_artifacts or artifacts)[-1]
1712
+ outputs[key] = get_artifact_target(artifact_to_save, self.metadata.project)
1713
+
1714
+ return outputs
1551
1715
 
1552
1716
  def uid(self):
1553
1717
  """run unique id"""
@@ -1664,6 +1828,11 @@ class RunObject(RunTemplate):
1664
1828
 
1665
1829
  return state
1666
1830
 
1831
+ def abort(self):
1832
+ """abort the run"""
1833
+ db = mlrun.get_run_db()
1834
+ db.abort_run(self.metadata.uid, self.metadata.project)
1835
+
1667
1836
  @staticmethod
1668
1837
  def create_uri(project: str, uid: str, iteration: Union[int, str], tag: str = ""):
1669
1838
  if tag:
@@ -1892,6 +2061,8 @@ class DataSource(ModelObj):
1892
2061
  ]
1893
2062
  kind = None
1894
2063
 
2064
+ _fields_to_serialize = ["start_time", "end_time"]
2065
+
1895
2066
  def __init__(
1896
2067
  self,
1897
2068
  name: str = None,
@@ -1920,6 +2091,16 @@ class DataSource(ModelObj):
1920
2091
  def set_secrets(self, secrets):
1921
2092
  self._secrets = secrets
1922
2093
 
2094
+ def _serialize_field(
2095
+ self, struct: dict, field_name: str = None, strip: bool = False
2096
+ ) -> typing.Any:
2097
+ value = super()._serialize_field(struct, field_name, strip)
2098
+ # We pull the field from self and not from struct because it was excluded from the struct when looping over
2099
+ # the fields to save.
2100
+ if field_name in ("start_time", "end_time") and isinstance(value, datetime):
2101
+ return value.isoformat()
2102
+ return value
2103
+
1923
2104
 
1924
2105
  class DataTargetBase(ModelObj):
1925
2106
  """data target spec, specify a destination for the feature set data"""