mlrun 1.7.0rc4__py3-none-any.whl → 1.7.2__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/__init__.py +11 -1
- mlrun/__main__.py +39 -121
- mlrun/{datastore/helpers.py → alerts/__init__.py} +2 -5
- mlrun/alerts/alert.py +248 -0
- mlrun/api/schemas/__init__.py +4 -3
- mlrun/artifacts/__init__.py +8 -3
- mlrun/artifacts/base.py +39 -254
- mlrun/artifacts/dataset.py +9 -190
- mlrun/artifacts/manager.py +73 -46
- mlrun/artifacts/model.py +30 -158
- mlrun/artifacts/plots.py +23 -380
- mlrun/common/constants.py +73 -1
- mlrun/common/db/sql_session.py +3 -2
- mlrun/common/formatters/__init__.py +21 -0
- mlrun/common/formatters/artifact.py +46 -0
- mlrun/common/formatters/base.py +113 -0
- mlrun/common/formatters/feature_set.py +44 -0
- mlrun/common/formatters/function.py +46 -0
- mlrun/common/formatters/pipeline.py +53 -0
- mlrun/common/formatters/project.py +51 -0
- mlrun/common/formatters/run.py +29 -0
- mlrun/common/helpers.py +11 -1
- mlrun/{runtimes → common/runtimes}/constants.py +32 -4
- mlrun/common/schemas/__init__.py +31 -4
- mlrun/common/schemas/alert.py +202 -0
- mlrun/common/schemas/api_gateway.py +196 -0
- mlrun/common/schemas/artifact.py +28 -1
- mlrun/common/schemas/auth.py +13 -2
- mlrun/common/schemas/client_spec.py +2 -1
- mlrun/common/schemas/common.py +7 -4
- mlrun/common/schemas/constants.py +3 -0
- mlrun/common/schemas/feature_store.py +58 -28
- mlrun/common/schemas/frontend_spec.py +8 -0
- mlrun/common/schemas/function.py +11 -0
- mlrun/common/schemas/hub.py +7 -9
- mlrun/common/schemas/model_monitoring/__init__.py +21 -4
- mlrun/common/schemas/model_monitoring/constants.py +136 -42
- mlrun/common/schemas/model_monitoring/grafana.py +9 -5
- mlrun/common/schemas/model_monitoring/model_endpoints.py +89 -41
- mlrun/common/schemas/notification.py +69 -12
- mlrun/{runtimes/mpijob/v1alpha1.py → common/schemas/pagination.py} +10 -13
- mlrun/common/schemas/pipeline.py +7 -0
- mlrun/common/schemas/project.py +67 -16
- mlrun/common/schemas/runs.py +17 -0
- mlrun/common/schemas/schedule.py +1 -1
- mlrun/common/schemas/workflow.py +10 -2
- mlrun/common/types.py +14 -1
- mlrun/config.py +233 -58
- mlrun/data_types/data_types.py +11 -1
- mlrun/data_types/spark.py +5 -4
- mlrun/data_types/to_pandas.py +75 -34
- mlrun/datastore/__init__.py +8 -10
- mlrun/datastore/alibaba_oss.py +131 -0
- mlrun/datastore/azure_blob.py +131 -43
- mlrun/datastore/base.py +107 -47
- mlrun/datastore/datastore.py +17 -7
- mlrun/datastore/datastore_profile.py +91 -7
- mlrun/datastore/dbfs_store.py +3 -7
- mlrun/datastore/filestore.py +1 -3
- mlrun/datastore/google_cloud_storage.py +92 -32
- mlrun/datastore/hdfs.py +5 -0
- mlrun/datastore/inmem.py +6 -3
- mlrun/datastore/redis.py +3 -2
- mlrun/datastore/s3.py +30 -12
- mlrun/datastore/snowflake_utils.py +45 -0
- mlrun/datastore/sources.py +274 -59
- mlrun/datastore/spark_utils.py +30 -0
- mlrun/datastore/store_resources.py +9 -7
- mlrun/datastore/storeytargets.py +151 -0
- mlrun/datastore/targets.py +387 -119
- mlrun/datastore/utils.py +68 -5
- mlrun/datastore/v3io.py +28 -50
- mlrun/db/auth_utils.py +152 -0
- mlrun/db/base.py +245 -20
- mlrun/db/factory.py +1 -4
- mlrun/db/httpdb.py +909 -231
- mlrun/db/nopdb.py +279 -14
- mlrun/errors.py +35 -5
- mlrun/execution.py +111 -38
- mlrun/feature_store/__init__.py +0 -2
- mlrun/feature_store/api.py +46 -53
- mlrun/feature_store/common.py +6 -11
- mlrun/feature_store/feature_set.py +48 -23
- mlrun/feature_store/feature_vector.py +13 -2
- mlrun/feature_store/ingestion.py +7 -6
- mlrun/feature_store/retrieval/base.py +9 -4
- mlrun/feature_store/retrieval/dask_merger.py +2 -0
- mlrun/feature_store/retrieval/job.py +13 -4
- mlrun/feature_store/retrieval/local_merger.py +2 -0
- mlrun/feature_store/retrieval/spark_merger.py +24 -32
- mlrun/feature_store/steps.py +38 -19
- mlrun/features.py +6 -14
- mlrun/frameworks/_common/plan.py +3 -3
- mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +7 -12
- mlrun/frameworks/_ml_common/plan.py +1 -1
- mlrun/frameworks/auto_mlrun/auto_mlrun.py +2 -2
- mlrun/frameworks/lgbm/__init__.py +1 -1
- mlrun/frameworks/lgbm/callbacks/callback.py +2 -4
- mlrun/frameworks/lgbm/model_handler.py +1 -1
- mlrun/frameworks/parallel_coordinates.py +4 -4
- mlrun/frameworks/pytorch/__init__.py +2 -2
- mlrun/frameworks/sklearn/__init__.py +1 -1
- mlrun/frameworks/sklearn/mlrun_interface.py +13 -3
- mlrun/frameworks/tf_keras/__init__.py +5 -2
- mlrun/frameworks/tf_keras/callbacks/logging_callback.py +1 -1
- mlrun/frameworks/tf_keras/mlrun_interface.py +2 -2
- mlrun/frameworks/xgboost/__init__.py +1 -1
- mlrun/k8s_utils.py +57 -12
- mlrun/launcher/__init__.py +1 -1
- mlrun/launcher/base.py +6 -5
- mlrun/launcher/client.py +13 -11
- mlrun/launcher/factory.py +1 -1
- mlrun/launcher/local.py +15 -5
- mlrun/launcher/remote.py +10 -3
- mlrun/lists.py +6 -2
- mlrun/model.py +297 -48
- mlrun/model_monitoring/__init__.py +1 -1
- mlrun/model_monitoring/api.py +152 -357
- mlrun/model_monitoring/applications/__init__.py +10 -0
- mlrun/model_monitoring/applications/_application_steps.py +190 -0
- mlrun/model_monitoring/applications/base.py +108 -0
- mlrun/model_monitoring/applications/context.py +341 -0
- mlrun/model_monitoring/{evidently_application.py → applications/evidently_base.py} +27 -22
- mlrun/model_monitoring/applications/histogram_data_drift.py +227 -91
- mlrun/model_monitoring/applications/results.py +99 -0
- mlrun/model_monitoring/controller.py +130 -303
- mlrun/model_monitoring/{stores/models/sqlite.py → db/__init__.py} +5 -10
- mlrun/model_monitoring/db/stores/__init__.py +136 -0
- mlrun/model_monitoring/db/stores/base/__init__.py +15 -0
- mlrun/model_monitoring/db/stores/base/store.py +213 -0
- mlrun/model_monitoring/db/stores/sqldb/__init__.py +13 -0
- mlrun/model_monitoring/db/stores/sqldb/models/__init__.py +71 -0
- mlrun/model_monitoring/db/stores/sqldb/models/base.py +190 -0
- mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +103 -0
- mlrun/model_monitoring/{stores/models/mysql.py → db/stores/sqldb/models/sqlite.py} +19 -13
- mlrun/model_monitoring/db/stores/sqldb/sql_store.py +659 -0
- mlrun/model_monitoring/db/stores/v3io_kv/__init__.py +13 -0
- mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +726 -0
- mlrun/model_monitoring/db/tsdb/__init__.py +105 -0
- mlrun/model_monitoring/db/tsdb/base.py +448 -0
- mlrun/model_monitoring/db/tsdb/helpers.py +30 -0
- mlrun/model_monitoring/db/tsdb/tdengine/__init__.py +15 -0
- mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +298 -0
- mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +42 -0
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +522 -0
- mlrun/model_monitoring/db/tsdb/v3io/__init__.py +15 -0
- mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +158 -0
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +849 -0
- mlrun/model_monitoring/features_drift_table.py +34 -22
- mlrun/model_monitoring/helpers.py +177 -39
- mlrun/model_monitoring/model_endpoint.py +3 -2
- mlrun/model_monitoring/stream_processing.py +165 -398
- mlrun/model_monitoring/tracking_policy.py +7 -1
- mlrun/model_monitoring/writer.py +161 -125
- mlrun/package/packagers/default_packager.py +2 -2
- mlrun/package/packagers_manager.py +1 -0
- mlrun/package/utils/_formatter.py +2 -2
- mlrun/platforms/__init__.py +11 -10
- mlrun/platforms/iguazio.py +67 -228
- mlrun/projects/__init__.py +6 -1
- mlrun/projects/operations.py +47 -20
- mlrun/projects/pipelines.py +396 -249
- mlrun/projects/project.py +1176 -406
- mlrun/render.py +28 -22
- mlrun/run.py +208 -181
- mlrun/runtimes/__init__.py +76 -11
- mlrun/runtimes/base.py +54 -24
- mlrun/runtimes/daskjob.py +9 -2
- mlrun/runtimes/databricks_job/databricks_runtime.py +1 -0
- mlrun/runtimes/databricks_job/databricks_wrapper.py +1 -1
- mlrun/runtimes/funcdoc.py +1 -29
- mlrun/runtimes/kubejob.py +34 -128
- mlrun/runtimes/local.py +39 -10
- mlrun/runtimes/mpijob/__init__.py +0 -20
- mlrun/runtimes/mpijob/abstract.py +8 -8
- mlrun/runtimes/mpijob/v1.py +1 -1
- mlrun/runtimes/nuclio/__init__.py +1 -0
- mlrun/runtimes/nuclio/api_gateway.py +769 -0
- mlrun/runtimes/nuclio/application/__init__.py +15 -0
- mlrun/runtimes/nuclio/application/application.py +758 -0
- mlrun/runtimes/nuclio/application/reverse_proxy.go +95 -0
- mlrun/runtimes/nuclio/function.py +188 -68
- mlrun/runtimes/nuclio/serving.py +57 -60
- mlrun/runtimes/pod.py +191 -58
- mlrun/runtimes/remotesparkjob.py +11 -8
- mlrun/runtimes/sparkjob/spark3job.py +17 -18
- mlrun/runtimes/utils.py +40 -73
- mlrun/secrets.py +6 -2
- mlrun/serving/__init__.py +8 -1
- mlrun/serving/remote.py +2 -3
- mlrun/serving/routers.py +89 -64
- mlrun/serving/server.py +54 -26
- mlrun/serving/states.py +187 -56
- mlrun/serving/utils.py +19 -11
- mlrun/serving/v2_serving.py +136 -63
- mlrun/track/tracker.py +2 -1
- mlrun/track/trackers/mlflow_tracker.py +5 -0
- mlrun/utils/async_http.py +26 -6
- mlrun/utils/db.py +18 -0
- mlrun/utils/helpers.py +375 -105
- mlrun/utils/http.py +2 -2
- mlrun/utils/logger.py +75 -9
- mlrun/utils/notifications/notification/__init__.py +14 -10
- mlrun/utils/notifications/notification/base.py +48 -0
- mlrun/utils/notifications/notification/console.py +2 -0
- mlrun/utils/notifications/notification/git.py +24 -1
- mlrun/utils/notifications/notification/ipython.py +2 -0
- mlrun/utils/notifications/notification/slack.py +96 -21
- mlrun/utils/notifications/notification/webhook.py +63 -2
- mlrun/utils/notifications/notification_pusher.py +146 -16
- mlrun/utils/regex.py +9 -0
- mlrun/utils/retryer.py +3 -2
- mlrun/utils/v3io_clients.py +2 -3
- mlrun/utils/version/version.json +2 -2
- mlrun-1.7.2.dist-info/METADATA +390 -0
- mlrun-1.7.2.dist-info/RECORD +351 -0
- {mlrun-1.7.0rc4.dist-info → mlrun-1.7.2.dist-info}/WHEEL +1 -1
- mlrun/feature_store/retrieval/conversion.py +0 -271
- mlrun/kfpops.py +0 -868
- mlrun/model_monitoring/application.py +0 -310
- mlrun/model_monitoring/batch.py +0 -974
- mlrun/model_monitoring/controller_handler.py +0 -37
- mlrun/model_monitoring/prometheus.py +0 -216
- mlrun/model_monitoring/stores/__init__.py +0 -111
- mlrun/model_monitoring/stores/kv_model_endpoint_store.py +0 -574
- mlrun/model_monitoring/stores/model_endpoint_store.py +0 -145
- mlrun/model_monitoring/stores/models/__init__.py +0 -27
- mlrun/model_monitoring/stores/models/base.py +0 -84
- mlrun/model_monitoring/stores/sql_model_endpoint_store.py +0 -382
- mlrun/platforms/other.py +0 -305
- mlrun-1.7.0rc4.dist-info/METADATA +0 -269
- mlrun-1.7.0rc4.dist-info/RECORD +0 -321
- {mlrun-1.7.0rc4.dist-info → mlrun-1.7.2.dist-info}/LICENSE +0 -0
- {mlrun-1.7.0rc4.dist-info → mlrun-1.7.2.dist-info}/entry_points.txt +0 -0
- {mlrun-1.7.0rc4.dist-info → mlrun-1.7.2.dist-info}/top_level.txt +0 -0
mlrun/utils/helpers.py
CHANGED
|
@@ -24,9 +24,10 @@ import re
|
|
|
24
24
|
import string
|
|
25
25
|
import sys
|
|
26
26
|
import typing
|
|
27
|
+
import uuid
|
|
27
28
|
import warnings
|
|
28
29
|
from datetime import datetime, timezone
|
|
29
|
-
from importlib import import_module
|
|
30
|
+
from importlib import import_module, reload
|
|
30
31
|
from os import path
|
|
31
32
|
from types import ModuleType
|
|
32
33
|
from typing import Any, Optional
|
|
@@ -39,8 +40,8 @@ import pandas
|
|
|
39
40
|
import semver
|
|
40
41
|
import yaml
|
|
41
42
|
from dateutil import parser
|
|
42
|
-
from
|
|
43
|
-
from pandas
|
|
43
|
+
from mlrun_pipelines.models import PipelineRun
|
|
44
|
+
from pandas import Timedelta, Timestamp
|
|
44
45
|
from yaml.representer import RepresenterError
|
|
45
46
|
|
|
46
47
|
import mlrun
|
|
@@ -76,19 +77,6 @@ class OverwriteBuildParamsWarning(FutureWarning):
|
|
|
76
77
|
pass
|
|
77
78
|
|
|
78
79
|
|
|
79
|
-
# TODO: remove in 1.7.0
|
|
80
|
-
@deprecated(
|
|
81
|
-
version="1.5.0",
|
|
82
|
-
reason="'parse_versioned_object_uri' will be removed from this file in 1.7.0, use "
|
|
83
|
-
"'mlrun.common.helpers.parse_versioned_object_uri' instead",
|
|
84
|
-
category=FutureWarning,
|
|
85
|
-
)
|
|
86
|
-
def parse_versioned_object_uri(uri: str, default_project: str = ""):
|
|
87
|
-
return mlrun.common.helpers.parse_versioned_object_uri(
|
|
88
|
-
uri=uri, default_project=default_project
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
80
|
class StorePrefix:
|
|
93
81
|
"""map mlrun store objects to prefixes"""
|
|
94
82
|
|
|
@@ -119,51 +107,61 @@ class StorePrefix:
|
|
|
119
107
|
|
|
120
108
|
|
|
121
109
|
def get_artifact_target(item: dict, project=None):
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
else:
|
|
127
|
-
db_key = item["spec"].get("db_key")
|
|
128
|
-
project_str = project or item["metadata"].get("project")
|
|
129
|
-
tree = item["metadata"].get("tree")
|
|
130
|
-
|
|
110
|
+
db_key = item["spec"].get("db_key")
|
|
111
|
+
project_str = project or item["metadata"].get("project")
|
|
112
|
+
tree = item["metadata"].get("tree")
|
|
113
|
+
tag = item["metadata"].get("tag")
|
|
131
114
|
kind = item.get("kind")
|
|
132
|
-
|
|
133
|
-
|
|
115
|
+
|
|
116
|
+
if kind in {"dataset", "model", "artifact"} and db_key:
|
|
117
|
+
target = (
|
|
118
|
+
f"{DB_SCHEMA}://{StorePrefix.kind_to_prefix(kind)}/{project_str}/{db_key}"
|
|
119
|
+
)
|
|
120
|
+
target += f":{tag}" if tag else ":latest"
|
|
134
121
|
if tree:
|
|
135
|
-
target
|
|
122
|
+
target += f"@{tree}"
|
|
136
123
|
return target
|
|
137
124
|
|
|
138
|
-
return (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
125
|
+
return item["spec"].get("target_path")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# TODO: left for migrations testing purposes. Remove in 1.8.0.
|
|
129
|
+
def is_legacy_artifact(artifact):
|
|
130
|
+
if isinstance(artifact, dict):
|
|
131
|
+
return "metadata" not in artifact
|
|
132
|
+
else:
|
|
133
|
+
return not hasattr(artifact, "metadata")
|
|
143
134
|
|
|
144
135
|
|
|
145
136
|
logger = create_logger(config.log_level, config.log_formatter, "mlrun", sys.stdout)
|
|
146
137
|
missing = object()
|
|
147
138
|
|
|
148
|
-
is_ipython = False
|
|
139
|
+
is_ipython = False # is IPython terminal, including Jupyter
|
|
140
|
+
is_jupyter = False # is Jupyter notebook/lab terminal
|
|
149
141
|
try:
|
|
150
|
-
import IPython
|
|
142
|
+
import IPython.core.getipython
|
|
151
143
|
|
|
152
|
-
ipy = IPython.get_ipython()
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
144
|
+
ipy = IPython.core.getipython.get_ipython()
|
|
145
|
+
|
|
146
|
+
is_ipython = ipy is not None
|
|
147
|
+
is_jupyter = (
|
|
148
|
+
is_ipython
|
|
149
|
+
# not IPython
|
|
150
|
+
and "Terminal" not in str(type(ipy))
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
del ipy
|
|
154
|
+
except ModuleNotFoundError:
|
|
157
155
|
pass
|
|
158
156
|
|
|
159
|
-
if
|
|
157
|
+
if is_jupyter and config.nest_asyncio_enabled in ["1", "True"]:
|
|
160
158
|
# bypass Jupyter asyncio bug
|
|
161
159
|
import nest_asyncio
|
|
162
160
|
|
|
163
161
|
nest_asyncio.apply()
|
|
164
162
|
|
|
165
163
|
|
|
166
|
-
class
|
|
164
|
+
class RunKeys:
|
|
167
165
|
input_path = "input_path"
|
|
168
166
|
output_path = "output_path"
|
|
169
167
|
inputs = "inputs"
|
|
@@ -174,6 +172,10 @@ class run_keys:
|
|
|
174
172
|
secrets = "secret_sources"
|
|
175
173
|
|
|
176
174
|
|
|
175
|
+
# for Backward compatibility
|
|
176
|
+
run_keys = RunKeys
|
|
177
|
+
|
|
178
|
+
|
|
177
179
|
def verify_field_regex(
|
|
178
180
|
field_name,
|
|
179
181
|
field_value,
|
|
@@ -195,8 +197,12 @@ def verify_field_regex(
|
|
|
195
197
|
)
|
|
196
198
|
if mode == mlrun.common.schemas.RegexMatchModes.all:
|
|
197
199
|
if raise_on_failure:
|
|
200
|
+
if len(field_name) > max_chars:
|
|
201
|
+
field_name = field_name[:max_chars] + "...truncated"
|
|
202
|
+
if len(field_value) > max_chars:
|
|
203
|
+
field_value = field_value[:max_chars] + "...truncated"
|
|
198
204
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
199
|
-
f"Field '{field_name
|
|
205
|
+
f"Field '{field_name}' is malformed. '{field_value}' "
|
|
200
206
|
f"does not match required pattern: {pattern}"
|
|
201
207
|
)
|
|
202
208
|
return False
|
|
@@ -437,7 +443,7 @@ class LogBatchWriter:
|
|
|
437
443
|
|
|
438
444
|
def get_in(obj, keys, default=None):
|
|
439
445
|
"""
|
|
440
|
-
>>> get_in({
|
|
446
|
+
>>> get_in({"a": {"b": 1}}, "a.b")
|
|
441
447
|
1
|
|
442
448
|
"""
|
|
443
449
|
if isinstance(keys, str):
|
|
@@ -669,7 +675,7 @@ def parse_artifact_uri(uri, default_project=""):
|
|
|
669
675
|
[3] = tag
|
|
670
676
|
[4] = tree
|
|
671
677
|
"""
|
|
672
|
-
uri_pattern =
|
|
678
|
+
uri_pattern = mlrun.utils.regex.artifact_uri_pattern
|
|
673
679
|
match = re.match(uri_pattern, uri)
|
|
674
680
|
if not match:
|
|
675
681
|
raise ValueError(
|
|
@@ -684,6 +690,8 @@ def parse_artifact_uri(uri, default_project=""):
|
|
|
684
690
|
raise ValueError(
|
|
685
691
|
f"illegal store path '{uri}', iteration must be integer value"
|
|
686
692
|
)
|
|
693
|
+
else:
|
|
694
|
+
iteration = 0
|
|
687
695
|
return (
|
|
688
696
|
group_dict["project"] or default_project,
|
|
689
697
|
group_dict["key"],
|
|
@@ -801,34 +809,6 @@ def gen_html_table(header, rows=None):
|
|
|
801
809
|
return style + '<table class="tg">\n' + out + "</table>\n\n"
|
|
802
810
|
|
|
803
811
|
|
|
804
|
-
def new_pipe_metadata(
|
|
805
|
-
artifact_path: str = None,
|
|
806
|
-
cleanup_ttl: int = None,
|
|
807
|
-
op_transformers: list[typing.Callable] = None,
|
|
808
|
-
):
|
|
809
|
-
from kfp.dsl import PipelineConf
|
|
810
|
-
|
|
811
|
-
def _set_artifact_path(task):
|
|
812
|
-
from kubernetes import client as k8s_client
|
|
813
|
-
|
|
814
|
-
task.add_env_variable(
|
|
815
|
-
k8s_client.V1EnvVar(name="MLRUN_ARTIFACT_PATH", value=artifact_path)
|
|
816
|
-
)
|
|
817
|
-
return task
|
|
818
|
-
|
|
819
|
-
conf = PipelineConf()
|
|
820
|
-
cleanup_ttl = cleanup_ttl or int(config.kfp_ttl)
|
|
821
|
-
|
|
822
|
-
if cleanup_ttl:
|
|
823
|
-
conf.set_ttl_seconds_after_finished(cleanup_ttl)
|
|
824
|
-
if artifact_path:
|
|
825
|
-
conf.add_op_transformer(_set_artifact_path)
|
|
826
|
-
if op_transformers:
|
|
827
|
-
for op_transformer in op_transformers:
|
|
828
|
-
conf.add_op_transformer(op_transformer)
|
|
829
|
-
return conf
|
|
830
|
-
|
|
831
|
-
|
|
832
812
|
def _convert_python_package_version_to_image_tag(version: typing.Optional[str]):
|
|
833
813
|
return (
|
|
834
814
|
version.replace("+", "-").replace("0.0.0-", "") if version is not None else None
|
|
@@ -848,7 +828,6 @@ def enrich_image_url(
|
|
|
848
828
|
tag += resolve_image_tag_suffix(
|
|
849
829
|
mlrun_version=mlrun_version, python_version=client_python_version
|
|
850
830
|
)
|
|
851
|
-
registry = config.images_registry
|
|
852
831
|
|
|
853
832
|
# it's an mlrun image if the repository is mlrun
|
|
854
833
|
is_mlrun_image = image_url.startswith("mlrun/") or "/mlrun/" in image_url
|
|
@@ -856,6 +835,10 @@ def enrich_image_url(
|
|
|
856
835
|
if is_mlrun_image and tag and ":" not in image_url:
|
|
857
836
|
image_url = f"{image_url}:{tag}"
|
|
858
837
|
|
|
838
|
+
registry = (
|
|
839
|
+
config.images_registry if is_mlrun_image else config.vendor_images_registry
|
|
840
|
+
)
|
|
841
|
+
|
|
859
842
|
enrich_registry = False
|
|
860
843
|
# enrich registry only if images_to_enrich_registry provided
|
|
861
844
|
# example: "^mlrun/*" means enrich only if the image repository is mlrun and registry is not specified (in which
|
|
@@ -1015,17 +998,44 @@ def get_ui_url(project, uid=None):
|
|
|
1015
998
|
return url
|
|
1016
999
|
|
|
1017
1000
|
|
|
1001
|
+
def get_model_endpoint_url(project, model_name, model_endpoint_id):
|
|
1002
|
+
url = ""
|
|
1003
|
+
if mlrun.mlconf.resolve_ui_url():
|
|
1004
|
+
url = f"{mlrun.mlconf.resolve_ui_url()}/{mlrun.mlconf.ui.projects_prefix}/{project}/models"
|
|
1005
|
+
if model_name:
|
|
1006
|
+
url += f"/model-endpoints/{model_name}/{model_endpoint_id}/overview"
|
|
1007
|
+
return url
|
|
1008
|
+
|
|
1009
|
+
|
|
1018
1010
|
def get_workflow_url(project, id=None):
|
|
1019
1011
|
url = ""
|
|
1020
1012
|
if mlrun.mlconf.resolve_ui_url():
|
|
1021
|
-
url =
|
|
1022
|
-
mlrun.mlconf.resolve_ui_url()
|
|
1013
|
+
url = (
|
|
1014
|
+
f"{mlrun.mlconf.resolve_ui_url()}/{mlrun.mlconf.ui.projects_prefix}"
|
|
1015
|
+
f"/{project}/jobs/monitor-workflows/workflow/{id}"
|
|
1023
1016
|
)
|
|
1024
1017
|
return url
|
|
1025
1018
|
|
|
1026
1019
|
|
|
1020
|
+
def get_kfp_project_filter(project_name: str) -> str:
|
|
1021
|
+
"""
|
|
1022
|
+
Generates a filter string for KFP runs, using a substring predicate
|
|
1023
|
+
on the run's 'name' field. This is used as a heuristic to retrieve runs that are associated
|
|
1024
|
+
with a specific project. The 'op: 9' operator indicates that the filter checks if the
|
|
1025
|
+
project name appears as a substring in the run's name, ensuring that we can identify
|
|
1026
|
+
runs belonging to the desired project.
|
|
1027
|
+
"""
|
|
1028
|
+
is_substring_op = 9
|
|
1029
|
+
project_name_filter = {
|
|
1030
|
+
"predicates": [
|
|
1031
|
+
{"key": "name", "op": is_substring_op, "string_value": project_name}
|
|
1032
|
+
]
|
|
1033
|
+
}
|
|
1034
|
+
return json.dumps(project_name_filter)
|
|
1035
|
+
|
|
1036
|
+
|
|
1027
1037
|
def are_strings_in_exception_chain_messages(
|
|
1028
|
-
exception: Exception, strings_list
|
|
1038
|
+
exception: Exception, strings_list: list[str]
|
|
1029
1039
|
) -> bool:
|
|
1030
1040
|
while exception is not None:
|
|
1031
1041
|
if any([string in str(exception) for string in strings_list]):
|
|
@@ -1047,16 +1057,35 @@ def create_class(pkg_class: str):
|
|
|
1047
1057
|
return class_
|
|
1048
1058
|
|
|
1049
1059
|
|
|
1050
|
-
def create_function(pkg_func: str):
|
|
1060
|
+
def create_function(pkg_func: str, reload_modules: bool = False):
|
|
1051
1061
|
"""Create a function from a package.module.function string
|
|
1052
1062
|
|
|
1053
1063
|
:param pkg_func: full function location,
|
|
1054
1064
|
e.g. "sklearn.feature_selection.f_classif"
|
|
1065
|
+
:param reload_modules: reload the function again.
|
|
1055
1066
|
"""
|
|
1056
1067
|
splits = pkg_func.split(".")
|
|
1057
1068
|
pkg_module = ".".join(splits[:-1])
|
|
1058
1069
|
cb_fname = splits[-1]
|
|
1059
1070
|
pkg_module = __import__(pkg_module, fromlist=[cb_fname])
|
|
1071
|
+
|
|
1072
|
+
if reload_modules:
|
|
1073
|
+
# Even though the function appears in the modules list, we need to reload
|
|
1074
|
+
# the code again because it may have changed
|
|
1075
|
+
try:
|
|
1076
|
+
logger.debug("Reloading module", module=pkg_func)
|
|
1077
|
+
_reload(
|
|
1078
|
+
pkg_module,
|
|
1079
|
+
max_recursion_depth=mlrun.mlconf.function.spec.reload_max_recursion_depth,
|
|
1080
|
+
)
|
|
1081
|
+
except Exception as exc:
|
|
1082
|
+
logger.warning(
|
|
1083
|
+
"Failed to reload module. Not all associated modules can be reloaded, import them manually."
|
|
1084
|
+
"Or, with Jupyter, restart the Python kernel.",
|
|
1085
|
+
module=pkg_func,
|
|
1086
|
+
err=mlrun.errors.err_to_str(exc),
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1060
1089
|
function_ = getattr(pkg_module, cb_fname)
|
|
1061
1090
|
return function_
|
|
1062
1091
|
|
|
@@ -1114,8 +1143,14 @@ def get_class(class_name, namespace=None):
|
|
|
1114
1143
|
return class_object
|
|
1115
1144
|
|
|
1116
1145
|
|
|
1117
|
-
def get_function(function,
|
|
1118
|
-
"""
|
|
1146
|
+
def get_function(function, namespaces, reload_modules: bool = False):
|
|
1147
|
+
"""Return function callable object from function name string
|
|
1148
|
+
|
|
1149
|
+
:param function: path to the function ([class_name::]function)
|
|
1150
|
+
:param namespaces: one or list of namespaces/modules to search the function in
|
|
1151
|
+
:param reload_modules: reload the function again
|
|
1152
|
+
:return: function handler (callable)
|
|
1153
|
+
"""
|
|
1119
1154
|
if callable(function):
|
|
1120
1155
|
return function
|
|
1121
1156
|
|
|
@@ -1124,12 +1159,12 @@ def get_function(function, namespace):
|
|
|
1124
1159
|
if not function.endswith(")"):
|
|
1125
1160
|
raise ValueError('function expression must start with "(" and end with ")"')
|
|
1126
1161
|
return eval("lambda event: " + function[1:-1], {}, {})
|
|
1127
|
-
function_object = _search_in_namespaces(function,
|
|
1162
|
+
function_object = _search_in_namespaces(function, namespaces)
|
|
1128
1163
|
if function_object is not None:
|
|
1129
1164
|
return function_object
|
|
1130
1165
|
|
|
1131
1166
|
try:
|
|
1132
|
-
function_object = create_function(function)
|
|
1167
|
+
function_object = create_function(function, reload_modules)
|
|
1133
1168
|
except (ImportError, ValueError) as exc:
|
|
1134
1169
|
raise ImportError(
|
|
1135
1170
|
f"state/function init failed, handler '{function}' not found"
|
|
@@ -1138,18 +1173,24 @@ def get_function(function, namespace):
|
|
|
1138
1173
|
|
|
1139
1174
|
|
|
1140
1175
|
def get_handler_extended(
|
|
1141
|
-
handler_path: str,
|
|
1176
|
+
handler_path: str,
|
|
1177
|
+
context=None,
|
|
1178
|
+
class_args: dict = None,
|
|
1179
|
+
namespaces=None,
|
|
1180
|
+
reload_modules: bool = False,
|
|
1142
1181
|
):
|
|
1143
|
-
"""
|
|
1182
|
+
"""Get function handler from [class_name::]handler string
|
|
1144
1183
|
|
|
1145
1184
|
:param handler_path: path to the function ([class_name::]handler)
|
|
1146
1185
|
:param context: MLRun function/job client context
|
|
1147
1186
|
:param class_args: optional dict of class init kwargs
|
|
1148
1187
|
:param namespaces: one or list of namespaces/modules to search the handler in
|
|
1188
|
+
:param reload_modules: reload the function again
|
|
1149
1189
|
:return: function handler (callable)
|
|
1150
1190
|
"""
|
|
1191
|
+
class_args = class_args or {}
|
|
1151
1192
|
if "::" not in handler_path:
|
|
1152
|
-
return get_function(handler_path, namespaces)
|
|
1193
|
+
return get_function(handler_path, namespaces, reload_modules)
|
|
1153
1194
|
|
|
1154
1195
|
splitted = handler_path.split("::")
|
|
1155
1196
|
class_path = splitted[0].strip()
|
|
@@ -1185,14 +1226,24 @@ def datetime_to_iso(time_obj: Optional[datetime]) -> Optional[str]:
|
|
|
1185
1226
|
return time_obj.isoformat()
|
|
1186
1227
|
|
|
1187
1228
|
|
|
1188
|
-
def enrich_datetime_with_tz_info(timestamp_string):
|
|
1229
|
+
def enrich_datetime_with_tz_info(timestamp_string) -> Optional[datetime]:
|
|
1189
1230
|
if not timestamp_string:
|
|
1190
1231
|
return timestamp_string
|
|
1191
1232
|
|
|
1192
1233
|
if timestamp_string and not mlrun.utils.helpers.has_timezone(timestamp_string):
|
|
1193
1234
|
timestamp_string += datetime.now(timezone.utc).astimezone().strftime("%z")
|
|
1194
1235
|
|
|
1195
|
-
|
|
1236
|
+
for _format in [
|
|
1237
|
+
# e.g: 2021-08-25 12:00:00.000Z
|
|
1238
|
+
"%Y-%m-%d %H:%M:%S.%f%z",
|
|
1239
|
+
# e.g: 2024-11-11 07:44:56+0000
|
|
1240
|
+
"%Y-%m-%d %H:%M:%S%z",
|
|
1241
|
+
]:
|
|
1242
|
+
try:
|
|
1243
|
+
return datetime.strptime(timestamp_string, _format)
|
|
1244
|
+
except ValueError as exc:
|
|
1245
|
+
last_exc = exc
|
|
1246
|
+
raise last_exc
|
|
1196
1247
|
|
|
1197
1248
|
|
|
1198
1249
|
def has_timezone(timestamp):
|
|
@@ -1224,7 +1275,7 @@ def calculate_dataframe_hash(dataframe: pandas.DataFrame):
|
|
|
1224
1275
|
return hashlib.sha1(pandas.util.hash_pandas_object(dataframe).values).hexdigest()
|
|
1225
1276
|
|
|
1226
1277
|
|
|
1227
|
-
def template_artifact_path(artifact_path, project, run_uid=
|
|
1278
|
+
def template_artifact_path(artifact_path, project, run_uid=None):
|
|
1228
1279
|
"""
|
|
1229
1280
|
Replace {{run.uid}} with the run uid and {{project}} with the project name in the artifact path.
|
|
1230
1281
|
If no run uid is provided, the word `project` will be used instead as it is assumed to be a project
|
|
@@ -1232,6 +1283,7 @@ def template_artifact_path(artifact_path, project, run_uid="project"):
|
|
|
1232
1283
|
"""
|
|
1233
1284
|
if not artifact_path:
|
|
1234
1285
|
return artifact_path
|
|
1286
|
+
run_uid = run_uid or "project"
|
|
1235
1287
|
artifact_path = artifact_path.replace("{{run.uid}}", run_uid)
|
|
1236
1288
|
artifact_path = _fill_project_path_template(artifact_path, project)
|
|
1237
1289
|
return artifact_path
|
|
@@ -1253,6 +1305,10 @@ def _fill_project_path_template(artifact_path, project):
|
|
|
1253
1305
|
return artifact_path
|
|
1254
1306
|
|
|
1255
1307
|
|
|
1308
|
+
def to_non_empty_values_dict(input_dict: dict) -> dict:
|
|
1309
|
+
return {key: value for key, value in input_dict.items() if value}
|
|
1310
|
+
|
|
1311
|
+
|
|
1256
1312
|
def str_to_timestamp(time_str: str, now_time: Timestamp = None):
|
|
1257
1313
|
"""convert fixed/relative time string to Pandas Timestamp
|
|
1258
1314
|
|
|
@@ -1291,13 +1347,6 @@ def str_to_timestamp(time_str: str, now_time: Timestamp = None):
|
|
|
1291
1347
|
return Timestamp(time_str)
|
|
1292
1348
|
|
|
1293
1349
|
|
|
1294
|
-
def is_legacy_artifact(artifact):
|
|
1295
|
-
if isinstance(artifact, dict):
|
|
1296
|
-
return "metadata" not in artifact
|
|
1297
|
-
else:
|
|
1298
|
-
return not hasattr(artifact, "metadata")
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
1350
|
def is_link_artifact(artifact):
|
|
1302
1351
|
if isinstance(artifact, dict):
|
|
1303
1352
|
return (
|
|
@@ -1307,7 +1356,7 @@ def is_link_artifact(artifact):
|
|
|
1307
1356
|
return artifact.kind == mlrun.common.schemas.ArtifactCategories.link.value
|
|
1308
1357
|
|
|
1309
1358
|
|
|
1310
|
-
def format_run(run:
|
|
1359
|
+
def format_run(run: PipelineRun, with_project=False) -> dict:
|
|
1311
1360
|
fields = [
|
|
1312
1361
|
"id",
|
|
1313
1362
|
"name",
|
|
@@ -1317,6 +1366,7 @@ def format_run(run: dict, with_project=False) -> dict:
|
|
|
1317
1366
|
"scheduled_at",
|
|
1318
1367
|
"finished_at",
|
|
1319
1368
|
"description",
|
|
1369
|
+
"experiment_id",
|
|
1320
1370
|
]
|
|
1321
1371
|
|
|
1322
1372
|
if with_project:
|
|
@@ -1344,17 +1394,17 @@ def format_run(run: dict, with_project=False) -> dict:
|
|
|
1344
1394
|
# pipelines are yet to populate the status or workflow has failed
|
|
1345
1395
|
# as observed https://jira.iguazeng.com/browse/ML-5195
|
|
1346
1396
|
# set to unknown to ensure a status is returned
|
|
1347
|
-
if run
|
|
1348
|
-
run["status"] = inflection.titleize(
|
|
1397
|
+
if run.get("status", None) is None:
|
|
1398
|
+
run["status"] = inflection.titleize(
|
|
1399
|
+
mlrun.common.runtimes.constants.RunStates.unknown
|
|
1400
|
+
)
|
|
1349
1401
|
|
|
1350
1402
|
return run
|
|
1351
1403
|
|
|
1352
1404
|
|
|
1353
1405
|
def get_in_artifact(artifact: dict, key, default=None, raise_on_missing=False):
|
|
1354
1406
|
"""artifact can be dict or Artifact object"""
|
|
1355
|
-
if
|
|
1356
|
-
return artifact.get(key, default)
|
|
1357
|
-
elif key == "kind":
|
|
1407
|
+
if key == "kind":
|
|
1358
1408
|
return artifact.get(key, default)
|
|
1359
1409
|
else:
|
|
1360
1410
|
for block in ["metadata", "spec", "status"]:
|
|
@@ -1391,11 +1441,27 @@ def is_running_in_jupyter_notebook() -> bool:
|
|
|
1391
1441
|
Check if the code is running inside a Jupyter Notebook.
|
|
1392
1442
|
:return: True if running inside a Jupyter Notebook, False otherwise.
|
|
1393
1443
|
"""
|
|
1394
|
-
|
|
1444
|
+
return is_jupyter
|
|
1445
|
+
|
|
1446
|
+
|
|
1447
|
+
def create_ipython_display():
|
|
1448
|
+
"""
|
|
1449
|
+
Create an IPython display object and fill it with initial content.
|
|
1450
|
+
We can later use the returned display_id with the update_display method to update the content.
|
|
1451
|
+
If IPython is not installed, a warning will be logged and None will be returned.
|
|
1452
|
+
"""
|
|
1453
|
+
if is_ipython:
|
|
1454
|
+
import IPython
|
|
1455
|
+
|
|
1456
|
+
display_id = uuid.uuid4().hex
|
|
1457
|
+
content = IPython.display.HTML(
|
|
1458
|
+
f'<div id="{display_id}">Temporary Display Content</div>'
|
|
1459
|
+
)
|
|
1460
|
+
IPython.display.display(content, display_id=display_id)
|
|
1461
|
+
return display_id
|
|
1395
1462
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
return ipy and "Terminal" not in str(type(ipy))
|
|
1463
|
+
# returning None if IPython is not installed, this method shouldn't be called in that case but logging for sanity
|
|
1464
|
+
logger.debug("IPython is not installed, cannot create IPython display")
|
|
1399
1465
|
|
|
1400
1466
|
|
|
1401
1467
|
def as_number(field_name, field_value):
|
|
@@ -1405,6 +1471,18 @@ def as_number(field_name, field_value):
|
|
|
1405
1471
|
|
|
1406
1472
|
|
|
1407
1473
|
def filter_warnings(action, category):
|
|
1474
|
+
"""
|
|
1475
|
+
Decorator to filter warnings
|
|
1476
|
+
|
|
1477
|
+
Example::
|
|
1478
|
+
@filter_warnings("ignore", FutureWarning)
|
|
1479
|
+
def my_function():
|
|
1480
|
+
pass
|
|
1481
|
+
|
|
1482
|
+
:param action: one of "error", "ignore", "always", "default", "module", or "once"
|
|
1483
|
+
:param category: a class that the warning must be a subclass of
|
|
1484
|
+
"""
|
|
1485
|
+
|
|
1408
1486
|
def decorator(function):
|
|
1409
1487
|
def wrapper(*args, **kwargs):
|
|
1410
1488
|
# context manager that copies and, upon exit, restores the warnings filter and the showwarning() function.
|
|
@@ -1562,3 +1640,195 @@ def is_safe_path(base, filepath, is_symlink=False):
|
|
|
1562
1640
|
os.path.abspath(filepath) if not is_symlink else os.path.realpath(filepath)
|
|
1563
1641
|
)
|
|
1564
1642
|
return base == os.path.commonpath((base, resolved_filepath))
|
|
1643
|
+
|
|
1644
|
+
|
|
1645
|
+
def get_serving_spec():
|
|
1646
|
+
data = None
|
|
1647
|
+
|
|
1648
|
+
# we will have the serving spec in either mounted config map
|
|
1649
|
+
# or env depending on the size of the spec and configuration
|
|
1650
|
+
|
|
1651
|
+
try:
|
|
1652
|
+
with open(mlrun.common.constants.MLRUN_SERVING_SPEC_PATH) as f:
|
|
1653
|
+
data = f.read()
|
|
1654
|
+
except FileNotFoundError:
|
|
1655
|
+
pass
|
|
1656
|
+
|
|
1657
|
+
if data is None:
|
|
1658
|
+
data = os.environ.get("SERVING_SPEC_ENV", "")
|
|
1659
|
+
if not data:
|
|
1660
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
1661
|
+
"Failed to find serving spec in env var or config file"
|
|
1662
|
+
)
|
|
1663
|
+
spec = json.loads(data)
|
|
1664
|
+
return spec
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
def additional_filters_warning(additional_filters, class_name):
|
|
1668
|
+
if additional_filters and any(additional_filters):
|
|
1669
|
+
mlrun.utils.logger.warn(
|
|
1670
|
+
f"additional_filters parameter is not supported in {class_name},"
|
|
1671
|
+
f" parameter has been ignored."
|
|
1672
|
+
)
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
def merge_dicts_with_precedence(*dicts: dict) -> dict:
|
|
1676
|
+
"""
|
|
1677
|
+
Merge multiple dictionaries with precedence given to keys from later dictionaries.
|
|
1678
|
+
|
|
1679
|
+
This function merges an arbitrary number of dictionaries, where keys from dictionaries later
|
|
1680
|
+
in the argument list take precedence over keys from dictionaries earlier in the list. If all
|
|
1681
|
+
dictionaries contain the same key, the value from the last dictionary with that key will
|
|
1682
|
+
overwrite the values from earlier dictionaries.
|
|
1683
|
+
|
|
1684
|
+
Example:
|
|
1685
|
+
>>> first_dict = {"key1": "value1", "key2": "value2"}
|
|
1686
|
+
>>> second_dict = {"key2": "new_value2", "key3": "value3"}
|
|
1687
|
+
>>> third_dict = {"key3": "new_value3", "key4": "value4"}
|
|
1688
|
+
>>> merge_dicts_with_precedence(first_dict, second_dict, third_dict)
|
|
1689
|
+
{'key1': 'value1', 'key2': 'new_value2', 'key3': 'new_value3', 'key4': 'value4'}
|
|
1690
|
+
|
|
1691
|
+
- If no dictionaries are provided, the function returns an empty dictionary.
|
|
1692
|
+
"""
|
|
1693
|
+
return {k: v for d in dicts if d for k, v in d.items()}
|
|
1694
|
+
|
|
1695
|
+
|
|
1696
|
+
def validate_component_version_compatibility(
|
|
1697
|
+
component_name: typing.Literal["iguazio", "nuclio"], *min_versions: str
|
|
1698
|
+
):
|
|
1699
|
+
"""
|
|
1700
|
+
:param component_name: Name of the component to validate compatibility for.
|
|
1701
|
+
:param min_versions: Valid minimum version(s) required, assuming no 2 versions has equal major and minor.
|
|
1702
|
+
"""
|
|
1703
|
+
parsed_min_versions = [
|
|
1704
|
+
semver.VersionInfo.parse(min_version) for min_version in min_versions
|
|
1705
|
+
]
|
|
1706
|
+
parsed_current_version = None
|
|
1707
|
+
component_current_version = None
|
|
1708
|
+
try:
|
|
1709
|
+
if component_name == "iguazio":
|
|
1710
|
+
component_current_version = mlrun.mlconf.igz_version
|
|
1711
|
+
parsed_current_version = mlrun.mlconf.get_parsed_igz_version()
|
|
1712
|
+
|
|
1713
|
+
if parsed_current_version:
|
|
1714
|
+
# ignore pre-release and build metadata, as iguazio version always has them, and we only care about the
|
|
1715
|
+
# major, minor, and patch versions
|
|
1716
|
+
parsed_current_version = semver.VersionInfo.parse(
|
|
1717
|
+
f"{parsed_current_version.major}.{parsed_current_version.minor}.{parsed_current_version.patch}"
|
|
1718
|
+
)
|
|
1719
|
+
if component_name == "nuclio":
|
|
1720
|
+
component_current_version = mlrun.mlconf.nuclio_version
|
|
1721
|
+
parsed_current_version = semver.VersionInfo.parse(
|
|
1722
|
+
mlrun.mlconf.nuclio_version
|
|
1723
|
+
)
|
|
1724
|
+
if not parsed_current_version:
|
|
1725
|
+
return True
|
|
1726
|
+
except ValueError:
|
|
1727
|
+
# only log when version is set but invalid
|
|
1728
|
+
if component_current_version:
|
|
1729
|
+
logger.warning(
|
|
1730
|
+
"Unable to parse current version, assuming compatibility",
|
|
1731
|
+
component_name=component_name,
|
|
1732
|
+
current_version=component_current_version,
|
|
1733
|
+
min_versions=min_versions,
|
|
1734
|
+
)
|
|
1735
|
+
return True
|
|
1736
|
+
|
|
1737
|
+
# Feature might have been back-ported e.g. nuclio node selection is supported from
|
|
1738
|
+
# 1.5.20 and 1.6.10 but not in 1.6.9 - therefore we reverse sort to validate against 1.6.x 1st and
|
|
1739
|
+
# then against 1.5.x
|
|
1740
|
+
parsed_min_versions.sort(reverse=True)
|
|
1741
|
+
for parsed_min_version in parsed_min_versions:
|
|
1742
|
+
if (
|
|
1743
|
+
parsed_current_version.major == parsed_min_version.major
|
|
1744
|
+
and parsed_current_version.minor == parsed_min_version.minor
|
|
1745
|
+
and parsed_current_version.patch < parsed_min_version.patch
|
|
1746
|
+
):
|
|
1747
|
+
return False
|
|
1748
|
+
|
|
1749
|
+
if parsed_current_version >= parsed_min_version:
|
|
1750
|
+
return True
|
|
1751
|
+
return False
|
|
1752
|
+
|
|
1753
|
+
|
|
1754
|
+
def format_alert_summary(
|
|
1755
|
+
alert: mlrun.common.schemas.AlertConfig, event_data: mlrun.common.schemas.Event
|
|
1756
|
+
) -> str:
|
|
1757
|
+
result = alert.summary.replace("{{project}}", alert.project)
|
|
1758
|
+
result = result.replace("{{name}}", alert.name)
|
|
1759
|
+
result = result.replace("{{entity}}", event_data.entity.ids[0])
|
|
1760
|
+
return result
|
|
1761
|
+
|
|
1762
|
+
|
|
1763
|
+
def is_parquet_file(file_path, format_=None):
|
|
1764
|
+
return (file_path and file_path.endswith((".parquet", ".pq"))) or (
|
|
1765
|
+
format_ == "parquet"
|
|
1766
|
+
)
|
|
1767
|
+
|
|
1768
|
+
|
|
1769
|
+
def validate_single_def_handler(function_kind: str, code: str):
|
|
1770
|
+
# The name of MLRun's wrapper is 'handler', which is why the handler function name cannot be 'handler'
|
|
1771
|
+
# it would override MLRun's wrapper
|
|
1772
|
+
if function_kind == "mlrun":
|
|
1773
|
+
# Find all lines that start with "def handler("
|
|
1774
|
+
pattern = re.compile(r"^def handler\(", re.MULTILINE)
|
|
1775
|
+
matches = pattern.findall(code)
|
|
1776
|
+
|
|
1777
|
+
# Only MLRun's wrapper handler (footer) can be in the code
|
|
1778
|
+
if len(matches) > 1:
|
|
1779
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
1780
|
+
"The code file contains a function named “handler“, which is reserved. "
|
|
1781
|
+
+ "Use a different name for your function."
|
|
1782
|
+
)
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
def _reload(module, max_recursion_depth):
|
|
1786
|
+
"""Recursively reload modules."""
|
|
1787
|
+
if max_recursion_depth <= 0:
|
|
1788
|
+
return
|
|
1789
|
+
|
|
1790
|
+
reload(module)
|
|
1791
|
+
for attribute_name in dir(module):
|
|
1792
|
+
attribute = getattr(module, attribute_name)
|
|
1793
|
+
if type(attribute) is ModuleType:
|
|
1794
|
+
_reload(attribute, max_recursion_depth - 1)
|
|
1795
|
+
|
|
1796
|
+
|
|
1797
|
+
def run_with_retry(
|
|
1798
|
+
retry_count: int,
|
|
1799
|
+
func: typing.Callable,
|
|
1800
|
+
retry_on_exceptions: typing.Union[
|
|
1801
|
+
type[Exception],
|
|
1802
|
+
tuple[type[Exception]],
|
|
1803
|
+
] = None,
|
|
1804
|
+
*args,
|
|
1805
|
+
**kwargs,
|
|
1806
|
+
):
|
|
1807
|
+
"""
|
|
1808
|
+
Executes a function with retry logic upon encountering specified exceptions.
|
|
1809
|
+
|
|
1810
|
+
:param retry_count: The number of times to retry the function execution.
|
|
1811
|
+
:param func: The function to execute.
|
|
1812
|
+
:param retry_on_exceptions: Exception(s) that trigger a retry. Can be a single exception or a tuple of exceptions.
|
|
1813
|
+
:param args: Positional arguments to pass to the function.
|
|
1814
|
+
:param kwargs: Keyword arguments to pass to the function.
|
|
1815
|
+
:return: The result of the function execution if successful.
|
|
1816
|
+
:raises Exception: Re-raises the last exception encountered after all retries are exhausted.
|
|
1817
|
+
"""
|
|
1818
|
+
if retry_on_exceptions is None:
|
|
1819
|
+
retry_on_exceptions = (Exception,)
|
|
1820
|
+
elif isinstance(retry_on_exceptions, list):
|
|
1821
|
+
retry_on_exceptions = tuple(retry_on_exceptions)
|
|
1822
|
+
|
|
1823
|
+
last_exception = None
|
|
1824
|
+
for attempt in range(retry_count + 1):
|
|
1825
|
+
try:
|
|
1826
|
+
return func(*args, **kwargs)
|
|
1827
|
+
except retry_on_exceptions as exc:
|
|
1828
|
+
last_exception = exc
|
|
1829
|
+
logger.warning(
|
|
1830
|
+
f"Attempt {{{attempt}/ {retry_count}}} failed with exception: {exc}",
|
|
1831
|
+
)
|
|
1832
|
+
if attempt == retry_count:
|
|
1833
|
+
raise
|
|
1834
|
+
raise last_exception
|