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.
- mlrun/__main__.py +4 -2
- mlrun/alerts/alert.py +75 -8
- mlrun/artifacts/base.py +1 -0
- mlrun/artifacts/manager.py +9 -2
- mlrun/common/constants.py +4 -1
- mlrun/common/db/sql_session.py +3 -2
- mlrun/common/formatters/__init__.py +1 -0
- mlrun/common/formatters/artifact.py +1 -0
- mlrun/{model_monitoring/application.py → common/formatters/feature_set.py} +20 -6
- mlrun/common/formatters/run.py +3 -0
- mlrun/common/helpers.py +0 -1
- mlrun/common/schemas/__init__.py +3 -1
- mlrun/common/schemas/alert.py +15 -12
- mlrun/common/schemas/api_gateway.py +6 -6
- mlrun/common/schemas/auth.py +5 -0
- mlrun/common/schemas/client_spec.py +0 -1
- mlrun/common/schemas/common.py +7 -4
- mlrun/common/schemas/frontend_spec.py +7 -0
- mlrun/common/schemas/function.py +7 -0
- mlrun/common/schemas/model_monitoring/__init__.py +4 -3
- mlrun/common/schemas/model_monitoring/constants.py +41 -26
- mlrun/common/schemas/model_monitoring/model_endpoints.py +23 -47
- mlrun/common/schemas/notification.py +69 -12
- mlrun/common/schemas/project.py +45 -12
- mlrun/common/schemas/workflow.py +10 -2
- mlrun/common/types.py +1 -0
- mlrun/config.py +91 -35
- mlrun/data_types/data_types.py +6 -1
- mlrun/data_types/spark.py +2 -2
- mlrun/data_types/to_pandas.py +57 -25
- mlrun/datastore/__init__.py +1 -0
- mlrun/datastore/alibaba_oss.py +3 -2
- mlrun/datastore/azure_blob.py +125 -37
- mlrun/datastore/base.py +42 -21
- mlrun/datastore/datastore.py +4 -2
- mlrun/datastore/datastore_profile.py +1 -1
- mlrun/datastore/dbfs_store.py +3 -7
- mlrun/datastore/filestore.py +1 -3
- mlrun/datastore/google_cloud_storage.py +85 -29
- mlrun/datastore/inmem.py +4 -1
- mlrun/datastore/redis.py +1 -0
- mlrun/datastore/s3.py +25 -12
- mlrun/datastore/sources.py +76 -4
- mlrun/datastore/spark_utils.py +30 -0
- mlrun/datastore/storeytargets.py +151 -0
- mlrun/datastore/targets.py +102 -131
- mlrun/datastore/v3io.py +1 -0
- mlrun/db/base.py +15 -6
- mlrun/db/httpdb.py +57 -28
- mlrun/db/nopdb.py +29 -5
- mlrun/errors.py +20 -3
- mlrun/execution.py +46 -5
- mlrun/feature_store/api.py +25 -1
- mlrun/feature_store/common.py +6 -11
- mlrun/feature_store/feature_vector.py +3 -1
- mlrun/feature_store/retrieval/job.py +4 -1
- mlrun/feature_store/retrieval/spark_merger.py +10 -39
- mlrun/feature_store/steps.py +8 -0
- mlrun/frameworks/_common/plan.py +3 -3
- mlrun/frameworks/_ml_common/plan.py +1 -1
- mlrun/frameworks/parallel_coordinates.py +2 -3
- mlrun/frameworks/sklearn/mlrun_interface.py +13 -3
- mlrun/k8s_utils.py +48 -2
- mlrun/launcher/client.py +6 -6
- mlrun/launcher/local.py +2 -2
- mlrun/model.py +215 -34
- mlrun/model_monitoring/api.py +38 -24
- mlrun/model_monitoring/applications/__init__.py +1 -2
- mlrun/model_monitoring/applications/_application_steps.py +60 -29
- mlrun/model_monitoring/applications/base.py +2 -174
- mlrun/model_monitoring/applications/context.py +197 -70
- mlrun/model_monitoring/applications/evidently_base.py +11 -85
- mlrun/model_monitoring/applications/histogram_data_drift.py +21 -16
- mlrun/model_monitoring/applications/results.py +4 -4
- mlrun/model_monitoring/controller.py +110 -282
- mlrun/model_monitoring/db/stores/__init__.py +8 -3
- mlrun/model_monitoring/db/stores/base/store.py +3 -0
- mlrun/model_monitoring/db/stores/sqldb/models/base.py +9 -7
- mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +18 -3
- mlrun/model_monitoring/db/stores/sqldb/sql_store.py +43 -23
- mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +48 -35
- mlrun/model_monitoring/db/tsdb/__init__.py +7 -2
- mlrun/model_monitoring/db/tsdb/base.py +147 -15
- mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +94 -55
- mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +0 -3
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +144 -38
- mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +44 -3
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +246 -57
- mlrun/model_monitoring/helpers.py +70 -50
- mlrun/model_monitoring/stream_processing.py +96 -195
- mlrun/model_monitoring/writer.py +13 -5
- mlrun/package/packagers/default_packager.py +2 -2
- mlrun/projects/operations.py +16 -8
- mlrun/projects/pipelines.py +126 -115
- mlrun/projects/project.py +286 -129
- mlrun/render.py +3 -3
- mlrun/run.py +38 -19
- mlrun/runtimes/__init__.py +19 -8
- mlrun/runtimes/base.py +4 -1
- mlrun/runtimes/daskjob.py +1 -1
- mlrun/runtimes/funcdoc.py +1 -1
- mlrun/runtimes/kubejob.py +6 -6
- mlrun/runtimes/local.py +12 -5
- mlrun/runtimes/nuclio/api_gateway.py +68 -8
- mlrun/runtimes/nuclio/application/application.py +307 -70
- mlrun/runtimes/nuclio/function.py +63 -14
- mlrun/runtimes/nuclio/serving.py +10 -10
- mlrun/runtimes/pod.py +25 -19
- mlrun/runtimes/remotesparkjob.py +2 -5
- mlrun/runtimes/sparkjob/spark3job.py +16 -17
- mlrun/runtimes/utils.py +34 -0
- mlrun/serving/routers.py +2 -5
- mlrun/serving/server.py +37 -19
- mlrun/serving/states.py +30 -3
- mlrun/serving/v2_serving.py +44 -35
- mlrun/track/trackers/mlflow_tracker.py +5 -0
- mlrun/utils/async_http.py +1 -1
- mlrun/utils/db.py +18 -0
- mlrun/utils/helpers.py +150 -36
- mlrun/utils/http.py +1 -1
- mlrun/utils/notifications/notification/__init__.py +0 -1
- mlrun/utils/notifications/notification/webhook.py +8 -1
- mlrun/utils/notifications/notification_pusher.py +1 -1
- mlrun/utils/v3io_clients.py +2 -2
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.7.0rc28.dist-info → mlrun-1.7.0rc55.dist-info}/METADATA +153 -66
- {mlrun-1.7.0rc28.dist-info → mlrun-1.7.0rc55.dist-info}/RECORD +131 -134
- {mlrun-1.7.0rc28.dist-info → mlrun-1.7.0rc55.dist-info}/WHEEL +1 -1
- mlrun/feature_store/retrieval/conversion.py +0 -271
- mlrun/model_monitoring/controller_handler.py +0 -37
- mlrun/model_monitoring/evidently_application.py +0 -20
- mlrun/model_monitoring/prometheus.py +0 -216
- {mlrun-1.7.0rc28.dist-info → mlrun-1.7.0rc55.dist-info}/LICENSE +0 -0
- {mlrun-1.7.0rc28.dist-info → mlrun-1.7.0rc55.dist-info}/entry_points.txt +0 -0
- {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 =
|
|
221
|
-
|
|
222
|
-
|
|
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:
|
mlrun/feature_store/steps.py
CHANGED
|
@@ -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
|
+
)
|
mlrun/frameworks/_common/plan.py
CHANGED
|
@@ -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
|
|
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
|
|
87
|
+
if is_jupyter:
|
|
88
88
|
self._gui_display()
|
|
89
89
|
else:
|
|
90
90
|
self._cli_display()
|
|
@@ -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.
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
75
|
+
if schedule is not None:
|
|
76
76
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
77
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"""
|
|
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
|
|
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
|
-
"""
|
|
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
|
|
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
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
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
|
-
"""
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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"""
|